waveform-analysis 0.1.74 → 0.1.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +422 -60
  2. package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
  3. package/dist/index.cjs +1 -2
  4. package/dist/index.js +1 -7119
  5. package/dist/style.css +1 -1
  6. package/dist/types/animation/WaveformAnimationController.d.ts +30 -0
  7. package/dist/types/animation/core/Animation.d.ts +13 -0
  8. package/dist/types/animation/core/Animator.d.ts +23 -0
  9. package/dist/types/animation/core/easing.d.ts +3 -0
  10. package/dist/types/animation/core/errors.d.ts +4 -0
  11. package/dist/types/animation/core/handle.d.ts +7 -0
  12. package/dist/types/animation/core/owner.d.ts +3 -0
  13. package/dist/types/animation/core/propertyGroups.d.ts +17 -0
  14. package/dist/types/animation/core/task.d.ts +29 -0
  15. package/dist/types/animation/core/types.d.ts +74 -0
  16. package/dist/types/animation/core/validation.d.ts +30 -0
  17. package/dist/types/animation/core/values.d.ts +8 -0
  18. package/dist/types/animation/presets/annotation.d.ts +4 -0
  19. package/dist/types/animation/presets/lineDrawing.d.ts +7 -0
  20. package/dist/types/animation/presets/shared.d.ts +14 -0
  21. package/dist/types/animation/presets/transitions.d.ts +4 -0
  22. package/dist/types/animation/types.d.ts +84 -0
  23. package/dist/types/components/WaveformChart.vue.d.ts +3 -2
  24. package/dist/types/components/animation/annotationPresentation.d.ts +17 -0
  25. package/dist/types/components/animation/chartExportVersion.d.ts +2 -0
  26. package/dist/types/components/animation/config.d.ts +31 -0
  27. package/dist/types/components/animation/context.d.ts +20 -0
  28. package/dist/types/components/animation/geometry.d.ts +10 -0
  29. package/dist/types/components/animation/paginationPresentation.d.ts +13 -0
  30. package/dist/types/components/animation/pathBuffer.d.ts +4 -0
  31. package/dist/types/components/animation/trackPresentation.d.ts +35 -0
  32. package/dist/types/components/animation/useWaveformAnimations.d.ts +27 -0
  33. package/dist/types/components/annotation/WaveformAnnotationContextMenu.vue.d.ts +2 -2
  34. package/dist/types/components/annotation/types.d.ts +7 -0
  35. package/dist/types/components/controls/useChartImageExport.d.ts +2 -0
  36. package/dist/types/components/core/amplitudeLimits.d.ts +6 -0
  37. package/dist/types/components/core/layout.d.ts +6 -9
  38. package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
  39. package/dist/types/components/core/types.d.ts +8 -0
  40. package/dist/types/components/core/useWaveformChartController.d.ts +26 -1
  41. package/dist/types/components/core/useWaveformData.d.ts +2 -2
  42. package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
  43. package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
  44. package/dist/types/components/core/waveformChartTypes.d.ts +6 -3
  45. package/dist/types/components/core/xAxisTicks.d.ts +2 -0
  46. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
  47. package/dist/types/components/core/yAxisTicks.d.ts +6 -0
  48. package/dist/types/components/data/types.d.ts +1 -1
  49. package/dist/types/components/interaction/integerZoom.d.ts +3 -3
  50. package/dist/types/components/interaction/stepZoom.d.ts +5 -0
  51. package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
  52. package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
  53. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
  54. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
  55. package/dist/types/components/rendering/updateAxis.d.ts +5 -0
  56. package/dist/types/components/rendering/updateGrid.d.ts +3 -0
  57. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
  58. package/dist/types/components/utils/assignElement.d.ts +2 -0
  59. package/dist/types/core/data.d.ts +1 -1
  60. package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
  61. package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
  62. package/dist/types/demo/types.d.ts +11 -2
  63. package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
  64. package/dist/types/index.d.ts +10 -1
  65. package/dist/types/types/chart.d.ts +8 -1
  66. package/dist/types/types/index.d.ts +1 -1
  67. package/dist/types/utils/formatters.d.ts +4 -4
  68. package/dist/types/utils/xAxisNumber.d.ts +2 -0
  69. package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
  70. package/package.json +5 -2
  71. package/src/App.vue +34 -100
  72. package/src/DemoRouterApp.vue +1 -0
  73. package/src/animation/WaveformAnimationController.ts +328 -0
  74. package/src/animation/core/Animation.ts +37 -0
  75. package/src/animation/core/Animator.ts +200 -0
  76. package/src/animation/core/easing.ts +74 -0
  77. package/src/animation/core/errors.ts +24 -0
  78. package/src/animation/core/handle.ts +34 -0
  79. package/src/animation/core/owner.ts +9 -0
  80. package/src/animation/core/propertyGroups.ts +143 -0
  81. package/src/animation/core/task.ts +153 -0
  82. package/src/animation/core/types.ts +81 -0
  83. package/src/animation/core/validation.ts +173 -0
  84. package/src/animation/core/values.ts +38 -0
  85. package/src/animation/presets/annotation.ts +66 -0
  86. package/src/animation/presets/lineDrawing.ts +51 -0
  87. package/src/animation/presets/shared.ts +58 -0
  88. package/src/animation/presets/transitions.ts +77 -0
  89. package/src/animation/types.ts +81 -0
  90. package/src/components/WaveformChart.vue +1 -0
  91. package/src/components/WaveformChartView.vue +13 -6
  92. package/src/components/animation/annotationPresentation.ts +145 -0
  93. package/src/components/animation/chartExportVersion.ts +5 -0
  94. package/src/components/animation/config.ts +87 -0
  95. package/src/components/animation/context.ts +56 -0
  96. package/src/components/animation/geometry.ts +148 -0
  97. package/src/components/animation/paginationPresentation.ts +114 -0
  98. package/src/components/animation/pathBuffer.ts +61 -0
  99. package/src/components/animation/trackPresentation.ts +172 -0
  100. package/src/components/animation/useWaveformAnimations.ts +420 -0
  101. package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
  102. package/src/components/annotation/types.ts +3 -0
  103. package/src/components/controls/useChartCommands.ts +12 -13
  104. package/src/components/controls/useChartImageExport.ts +4 -0
  105. package/src/components/core/amplitudeLimits.ts +44 -0
  106. package/src/components/core/layout.ts +22 -34
  107. package/src/components/core/trackLayoutBuilder.ts +21 -23
  108. package/src/components/core/types.ts +4 -0
  109. package/src/components/core/useWaveformChartController.ts +27 -23
  110. package/src/components/core/useWaveformChartLifecycle.ts +11 -2
  111. package/src/components/core/useWaveformData.ts +11 -5
  112. package/src/components/core/useWaveformLayout.ts +9 -4
  113. package/src/components/core/useWaveformMeasurements.ts +3 -0
  114. package/src/components/core/useWaveformRenderSampling.ts +23 -1
  115. package/src/components/core/useWaveformYAxisLayout.ts +2 -0
  116. package/src/components/core/waveformChartTypes.ts +6 -2
  117. package/src/components/core/xAxisTicks.ts +69 -0
  118. package/src/components/core/yAxisLayoutMetrics.ts +4 -0
  119. package/src/components/core/yAxisTicks.ts +39 -0
  120. package/src/components/data/types.ts +2 -0
  121. package/src/components/interaction/integerZoom.ts +31 -12
  122. package/src/components/interaction/remoteViewport.ts +26 -4
  123. package/src/components/interaction/stepZoom.ts +33 -0
  124. package/src/components/interaction/useWaveformViewport.ts +3 -26
  125. package/src/components/interaction/useWaveformZoom.ts +50 -157
  126. package/src/components/interaction/zoomConstraints.ts +5 -5
  127. package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
  128. package/src/components/rendering/WaveformTrack.vue +6 -1
  129. package/src/components/rendering/WaveformTrackAxes.vue +64 -51
  130. package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
  131. package/src/components/rendering/updateAxis.ts +57 -0
  132. package/src/components/rendering/updateGrid.ts +42 -0
  133. package/src/components/rendering/yAxisTickVisibility.ts +45 -18
  134. package/src/components/utils/assignElement.ts +7 -0
  135. package/src/core/data.ts +5 -2
  136. package/src/data/simulatedWaveforms.ts +0 -10
  137. package/src/demo/DemoChartHost.vue +49 -4
  138. package/src/demo/remoteWaveformLoader.ts +114 -0
  139. package/src/demo/types.ts +11 -1
  140. package/src/demo/useRemoteWaveformWindow.ts +229 -0
  141. package/src/index.ts +54 -0
  142. package/src/router.ts +1 -0
  143. package/src/types/chart.ts +10 -1
  144. package/src/types/index.ts +2 -0
  145. package/src/utils/formatters.ts +10 -5
  146. package/src/utils/xAxisNumber.ts +36 -0
  147. package/src/views/AnimationDemo.vue +129 -0
  148. package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
  149. package/dist/types/data/sx2Channel.d.ts +0 -10
  150. package/src/data/sx2Channel.ts +0 -315
@@ -0,0 +1,420 @@
1
+ import { computed, shallowRef, watch, onScopeDispose } from 'vue'
2
+ import { WaveformAnimationController } from '@/animation/WaveformAnimationController'
3
+ import { resolveAnimationProps } from './config'
4
+ import { createAnnotationPresentation } from './annotationPresentation'
5
+ import {
6
+ staticTrack,
7
+ transitionTrack,
8
+ projectTrack,
9
+ hasThirdGeometry,
10
+ type TrackPresentation,
11
+ } from './trackPresentation'
12
+ import { chartExportOptions } from '../controls/chartExportOptions'
13
+ import { layoutAnnotations } from '../annotation/annotationLayout'
14
+ import { useWaveformInstanceId } from '@/utils/waveformId'
15
+
16
+ import { animationLayoutSignature, wrapAnimationCommands, type AnimationContext } from './context'
17
+ import { createPaginationPresentation } from './paginationPresentation'
18
+
19
+ export function useWaveformAnimations(context: AnimationContext) {
20
+ const { props, tracks, preparedSeries, sampling } = context
21
+ const config = computed(() => resolveAnimationProps(props))
22
+ const revision = shallowRef(0)
23
+ const invalidate = () => revision.value++
24
+ const states = new Map<string, TrackPresentation>()
25
+ const entering = new Set<string>()
26
+ const seen = new Set<string>()
27
+ const prefix = useWaveformInstanceId('waveform-animation')
28
+ let clipSequence = 0
29
+ let previousAnnotations = props.annotations
30
+ let previousData = props.data
31
+ let previousLayout = ''
32
+ let previousOptions = ''
33
+ let previousTracks = tracks.value
34
+ let previousSamplingState = sampling.state.value
35
+ let pendingFrom: Map<string, TrackPresentation> | undefined
36
+ let skipSignature: string | undefined
37
+ const exportSuppressed = shallowRef(false)
38
+ let disposed = false
39
+ const media =
40
+ typeof globalThis.matchMedia === 'function'
41
+ ? globalThis.matchMedia('(prefers-reduced-motion: reduce)')
42
+ : undefined
43
+ const reduced = shallowRef(media?.matches ?? false)
44
+ const totalPoints = computed(() =>
45
+ preparedSeries.value.reduce((sum, series) => sum + series.source.length, 0),
46
+ )
47
+ const effective = computed(
48
+ () =>
49
+ config.value.animationEnabled &&
50
+ !reduced.value &&
51
+ !exportSuppressed.value &&
52
+ typeof globalThis.requestAnimationFrame === 'function' &&
53
+ typeof globalThis.cancelAnimationFrame === 'function' &&
54
+ totalPoints.value < config.value.animationThreshold,
55
+ )
56
+ const visibleAnnotations = () =>
57
+ props.annotationsVisible && !props.cleanView ? props.annotations : []
58
+ const controller = new WaveformAnimationController({ commitFinalState: () => commit() })
59
+ const annotations = createAnnotationPresentation(controller, invalidate)
60
+ const presentationTracks = computed(() => {
61
+ void revision.value
62
+ return tracks.value.map((track) =>
63
+ states.has(track.id) ? projectTrack(states.get(track.id)!) : track,
64
+ )
65
+ })
66
+ const pagination = createPaginationPresentation(context, controller, config, effective, () =>
67
+ commit(true),
68
+ )
69
+ const projectedAnnotations = computed(() => {
70
+ const anchors = new Map([...states.values()].flatMap((state) => [...state.anchors]))
71
+ const values = visibleAnnotations().flatMap((annotation) => {
72
+ const anchor = anchors.get(annotation.id)
73
+ const target = [...states.values()].find((state) => state.series.has(annotation.seriesId))
74
+ const domain = target?.target.xScale.domain()
75
+ return anchor && domain && annotation.x >= domain[0] && annotation.x <= domain[1]
76
+ ? [{ ...annotation, x: anchor.x, y: anchor.y }]
77
+ : []
78
+ })
79
+ const annotationTracks = presentationTracks.value.flatMap((track) =>
80
+ track.seriesList.map((series) => ({
81
+ ...track,
82
+ series,
83
+ yScale:
84
+ track.seriesPaths.find(
85
+ (path) => path.series.id === series.id && path.presentationKey !== `${series.id}:old`,
86
+ )?.yScale ?? track.yScale,
87
+ })),
88
+ )
89
+ return layoutAnnotations(
90
+ values,
91
+ annotationTracks,
92
+ context.innerWidth.value,
93
+ context.innerHeight.value,
94
+ )
95
+ })
96
+ const presentationAnnotations = computed(() => {
97
+ void revision.value
98
+ const opacities = new Map(
99
+ [...states.values()].flatMap((state) =>
100
+ [...state.series].map(([id, series]) => [id, series.opacity] as const),
101
+ ),
102
+ )
103
+ return annotations.project(projectedAnnotations.value, opacities).map((annotation) => ({
104
+ ...annotation,
105
+ seriesOpacity: (annotation.seriesOpacity ?? 1) * pagination.opacity.value,
106
+ }))
107
+ })
108
+ function commit(preservePagination = false) {
109
+ if (!preservePagination) pagination.reset()
110
+ const previous = new Map(states)
111
+ states.clear()
112
+ for (const track of tracks.value)
113
+ states.set(
114
+ track.id,
115
+ staticTrack(track, visibleAnnotations(), previous.get(track.id), !!pendingFrom),
116
+ )
117
+ for (const track of tracks.value)
118
+ if (track.width > 0 && track.height > 0) {
119
+ for (const series of track.seriesList)
120
+ if ((series.source?.length ?? series.points.length) > 0) seen.add(series.id)
121
+ }
122
+ entering.clear()
123
+ pendingFrom = undefined
124
+ skipSignature = sampling.requestSignature.value
125
+ invalidate()
126
+ annotations.finish(projectedAnnotations.value)
127
+ }
128
+ const layoutSignature = computed(() => animationLayoutSignature(context))
129
+ function reconcile() {
130
+ if (disposed) return
131
+ const nextConfig = config.value
132
+ const dataChanged = props.data !== previousData
133
+ if (dataChanged) {
134
+ pendingFrom = new Map(states)
135
+ previousData = props.data
136
+ for (const id of states.keys())
137
+ controller.cancel({ kind: 'custom', id: `track:${id}`, scope: 'update' })
138
+ const interrupted = [...entering]
139
+ for (const id of interrupted) controller.cancel({ kind: 'series', id, scope: 'enter' })
140
+ // Anchor updates and annotation styles are independent channels (CTL-06). An interrupted
141
+ // enter no longer owns the wait; release after reconciling the new layout/configuration.
142
+ queueMicrotask(() => {
143
+ if (!disposed) interrupted.forEach((id) => annotations.release(id))
144
+ })
145
+ entering.clear()
146
+ }
147
+ const optionSignature = JSON.stringify(
148
+ Object.fromEntries(
149
+ Object.entries(chartExportOptions(props)).filter(
150
+ ([key]) => !['annotations', 'annotationsVisible'].includes(key),
151
+ ),
152
+ ),
153
+ )
154
+ const layoutChanged =
155
+ (previousOptions !== '' && previousOptions !== optionSignature) ||
156
+ (seen.size > 0 &&
157
+ !dataChanged &&
158
+ ((previousLayout !== '' && previousLayout !== layoutSignature.value) ||
159
+ (!pendingFrom && previousSamplingState === 'success' && tracks.value !== previousTracks)))
160
+ previousOptions = optionSignature
161
+ previousLayout = layoutSignature.value
162
+ previousTracks = tracks.value
163
+ previousSamplingState = sampling.state.value
164
+ controller.setEnabled(effective.value)
165
+ if (pagination.reconcile(dataChanged)) return
166
+ if (!effective.value || layoutChanged) {
167
+ controller.finishAll()
168
+ commit()
169
+ return
170
+ }
171
+ const stable = sampling.state.value !== 'pending' && context.layoutReady.value
172
+ if (pendingFrom && !stable) return
173
+ const canUpdate =
174
+ pendingFrom &&
175
+ sampling.state.value === 'success' &&
176
+ skipSignature !== sampling.requestSignature.value &&
177
+ nextConfig.animationUpdate.enabled
178
+ const knownIds = new Set(
179
+ preparedSeries.value.filter((series) => series.source.length > 0).map((series) => series.id),
180
+ )
181
+ for (const id of seen) if (!knownIds.has(id)) seen.delete(id)
182
+ const visibleIds = tracks.value.flatMap((track) => track.seriesList.map((series) => series.id))
183
+ const oldStates = pendingFrom ?? new Map(states)
184
+ const live = new Set(tracks.value.map((track) => track.id))
185
+ for (const id of states.keys()) if (!live.has(id)) states.delete(id)
186
+ for (const track of tracks.value) {
187
+ if (track.width <= 0 || track.height <= 0) {
188
+ states.set(track.id, staticTrack(track, visibleAnnotations()))
189
+ continue
190
+ }
191
+ const from = oldStates.get(track.id)
192
+ const to = staticTrack(track, visibleAnnotations(), from, !!pendingFrom)
193
+ if (pendingFrom) {
194
+ states.set(track.id, to)
195
+ if (canUpdate && from && !hasThirdGeometry(from)) {
196
+ states.set(track.id, transitionTrack(from, to, 0, false))
197
+ controller.animate(
198
+ { kind: 'custom', id: `track:${track.id}`, scope: 'update' },
199
+ {
200
+ duration: nextConfig.animationUpdate.duration,
201
+ easing: nextConfig.animationUpdate.easing,
202
+ force: true,
203
+ during: (p, frame) => {
204
+ states.set(track.id, transitionTrack(from, to, p, frame.terminal))
205
+ invalidate()
206
+ },
207
+ done: () =>
208
+ track.seriesPaths.forEach(() => nextConfig.animationUpdate.onComplete?.()),
209
+ aborted: () =>
210
+ track.seriesPaths.forEach(() => nextConfig.animationUpdate.onAbort?.()),
211
+ },
212
+ )
213
+ }
214
+ } else if (!states.has(track.id)) states.set(track.id, to)
215
+ else if (!controller.getActiveAnimationCount()) states.set(track.id, to)
216
+ for (const path of track.seriesPaths) {
217
+ const id = path.series.id
218
+ const hasPoints = (path.series.source?.length ?? path.series.points.length) > 0
219
+ if (!hasPoints) {
220
+ seen.delete(id)
221
+ continue
222
+ }
223
+ if (!stable) {
224
+ if (
225
+ !seen.has(id) &&
226
+ nextConfig.animation.enabled &&
227
+ skipSignature !== sampling.requestSignature.value
228
+ ) {
229
+ const state = states.get(track.id)!
230
+ if (!state.clips.has(id))
231
+ state.clips.set(id, {
232
+ id: `${prefix}-${++clipSequence}`,
233
+ x: 0,
234
+ width: 0,
235
+ })
236
+ }
237
+ continue
238
+ }
239
+ if (seen.has(id)) continue
240
+ seen.add(id)
241
+ if (
242
+ !nextConfig.animation.enabled ||
243
+ sampling.state.value !== 'success' ||
244
+ skipSignature === sampling.requestSignature.value
245
+ )
246
+ continue
247
+ const state = states.get(track.id)!
248
+ const clip = { id: `${prefix}-${++clipSequence}`, x: 0, width: track.width }
249
+ state.clips.set(id, clip)
250
+ entering.add(id)
251
+ const binding = {
252
+ read: () => clip,
253
+ write: (value: { x: number; width: number }) => {
254
+ Object.assign(clip, value)
255
+ invalidate()
256
+ },
257
+ }
258
+ controller.animateSeriesEnter(id, binding, track.width, {
259
+ ...nextConfig.animation,
260
+ delay:
261
+ nextConfig.animation.delay + visibleIds.indexOf(id) * nextConfig.animation.staggerDelay,
262
+ onComplete: () => {
263
+ state.clips.delete(id)
264
+ entering.delete(id)
265
+ invalidate()
266
+ queueMicrotask(() => {
267
+ if (!disposed) annotations.release(id)
268
+ })
269
+ nextConfig.animation.onComplete?.()
270
+ },
271
+ onAbort: () => {
272
+ nextConfig.animation.onAbort?.()
273
+ },
274
+ })
275
+ }
276
+ }
277
+ if (props.annotations !== previousAnnotations) {
278
+ for (const track of tracks.value) {
279
+ const state = states.get(track.id)
280
+ if (state) state.anchors = staticTrack(track, visibleAnnotations(), state).anchors
281
+ }
282
+ previousAnnotations = props.annotations
283
+ }
284
+ pendingFrom = undefined
285
+ invalidate()
286
+ if (!stable) return
287
+ annotations.reconcile(
288
+ projectedAnnotations.value,
289
+ props.annotations,
290
+ nextConfig.animationAnnotation,
291
+ nextConfig.animationAnnotation.enabled &&
292
+ effective.value &&
293
+ props.annotationsVisible &&
294
+ !props.cleanView,
295
+ entering,
296
+ )
297
+ }
298
+ watch(
299
+ [
300
+ tracks,
301
+ () => props.data,
302
+ () => props.annotations,
303
+ () => props.annotationsVisible,
304
+ config,
305
+ layoutSignature,
306
+ sampling.state,
307
+ context.layoutReady,
308
+ context.currentPage,
309
+ effective,
310
+ ],
311
+ reconcile,
312
+ { immediate: true },
313
+ )
314
+ watch(
315
+ () => props.annotationsVisible,
316
+ (visible) => {
317
+ if (!visible)
318
+ controller.finishWhere(
319
+ (key) => key.kind === 'annotation' && ['enter', 'leave'].includes(key.scope),
320
+ () => annotations.finish([]),
321
+ )
322
+ },
323
+ )
324
+ watch(
325
+ () => config.value.animation.enabled,
326
+ (enabled) => {
327
+ if (!enabled)
328
+ controller.finishWhere(
329
+ (key) => key.kind === 'series' && key.scope === 'enter',
330
+ () => {
331
+ for (const state of states.values()) state.clips.clear()
332
+ entering.clear()
333
+ invalidate()
334
+ },
335
+ )
336
+ if (!enabled) annotations.release()
337
+ },
338
+ )
339
+ watch(
340
+ () => config.value.animationUpdate.enabled,
341
+ (enabled) => {
342
+ if (!enabled)
343
+ controller.finishWhere(
344
+ (key) => key.scope === 'update' || key.scope === 'domain',
345
+ () => {
346
+ pagination.reset()
347
+ for (const track of tracks.value) {
348
+ const old = states.get(track.id),
349
+ next = staticTrack(track, visibleAnnotations(), old, true)
350
+ if (old) next.clips = old.clips
351
+ states.set(track.id, next)
352
+ }
353
+ invalidate()
354
+ },
355
+ )
356
+ },
357
+ )
358
+ watch(
359
+ () => config.value.animationAnnotation.enabled,
360
+ (enabled) => {
361
+ if (!enabled)
362
+ controller.finishWhere(
363
+ (key) => key.kind === 'annotation' && ['enter', 'leave'].includes(key.scope),
364
+ () => annotations.finish(projectedAnnotations.value),
365
+ )
366
+ },
367
+ )
368
+ const reduceChanged = (event: MediaQueryListEvent) => {
369
+ reduced.value = event.matches
370
+ if (event.matches) controller.finishAll()
371
+ }
372
+ const visibilityChanged = () => controller.setPauseReason('document-hidden', document.hidden)
373
+ media?.addEventListener?.('change', reduceChanged)
374
+ if (typeof document !== 'undefined') {
375
+ document.addEventListener('visibilitychange', visibilityChanged)
376
+ visibilityChanged()
377
+ }
378
+ onScopeDispose(() => {
379
+ disposed = true
380
+ pagination.reset()
381
+ states.clear()
382
+ entering.clear()
383
+ annotations.clear()
384
+ media?.removeEventListener?.('change', reduceChanged)
385
+ if (typeof document !== 'undefined')
386
+ document.removeEventListener('visibilitychange', visibilityChanged)
387
+ controller.destroy()
388
+ })
389
+ function stabilizeAnimations(event?: Event) {
390
+ if (
391
+ pagination.active() &&
392
+ event?.target instanceof Element &&
393
+ event.target.closest('.waveform-chart__pagination')
394
+ )
395
+ return
396
+ if (sampling.state.value === 'pending') skipSignature = sampling.requestSignature.value
397
+ if (
398
+ controller.getActiveAnimationCount() ||
399
+ pendingFrom ||
400
+ pagination.active() ||
401
+ sampling.state.value === 'pending'
402
+ )
403
+ controller.finishAll()
404
+ }
405
+ return {
406
+ presentationTracks,
407
+ presentationAnnotations,
408
+ paginationOpacity: pagination.opacity,
409
+ stabilizeAnimations,
410
+ wrap: <T extends object>(commands: T) => wrapAnimationCommands(commands, stabilizeAnimations),
411
+ wrapPagination: pagination.wrapPagination,
412
+ prepareExport: () => {
413
+ exportSuppressed.value = true
414
+ stabilizeAnimations()
415
+ },
416
+ releaseExport: () => {
417
+ exportSuppressed.value = false
418
+ },
419
+ }
420
+ }
@@ -84,7 +84,7 @@ function scheduleDragFrame() {
84
84
  }
85
85
 
86
86
  function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) {
87
- if (!props.interactive || event.button !== 0 || dragState) return
87
+ if (rendered.leaving || !props.interactive || event.button !== 0 || dragState) return
88
88
  event.preventDefault()
89
89
  event.stopPropagation()
90
90
  const target = event.currentTarget as SVGGElement | null
@@ -226,6 +226,14 @@ function handleContextMenu(annotationId: string, event: MouseEvent) {
226
226
  emit('contextmenu', annotationId, event)
227
227
  }
228
228
 
229
+ function labelTransform(rendered: RenderedAnnotation) {
230
+ const style = rendered.animationStyle
231
+ if (!style) return undefined
232
+ const box = draggedBox(rendered),
233
+ x = box.x + box.width / 2,
234
+ y = box.y + box.height / 2
235
+ return `translate(${x},${y + style.offsetY}) scale(${style.scale}) translate(${-x},${-y})`
236
+ }
229
237
  function markerId(annotationId: string) {
230
238
  return `waveform-annotation-arrow-${annotationId.replace(/[^a-zA-Z0-9_-]/g, '-')}`
231
239
  }
@@ -243,6 +251,9 @@ function markerId(annotationId: string) {
243
251
  class="waveform-annotation"
244
252
  :data-annotation-id="rendered.annotation.id"
245
253
  :data-placement="rendered.placement"
254
+ :data-leaving="rendered.leaving || undefined"
255
+ :style="rendered.leaving ? { pointerEvents: 'none' } : undefined"
256
+ :opacity="(rendered.animationStyle?.opacity ?? 1) * (rendered.seriesOpacity ?? 1)"
246
257
  @pointerdown="handlePointerDown(rendered, $event)"
247
258
  @pointermove="handlePointerMove"
248
259
  @pointerup="finishPointerDrag"
@@ -271,37 +282,39 @@ function markerId(annotationId: string) {
271
282
  :stroke="rendered.style.borderColor"
272
283
  :marker-end="`url(#${markerId(rendered.annotation.id)})`"
273
284
  />
274
- <rect
275
- class="waveform-annotation__box"
276
- :x="draggedBox(rendered).x"
277
- :y="draggedBox(rendered).y"
278
- :width="draggedBox(rendered).width"
279
- :height="draggedBox(rendered).height"
280
- :fill="rendered.style.backgroundColor"
281
- :stroke="rendered.style.borderColor"
282
- />
283
- <text
284
- class="waveform-annotation__text"
285
- :x="draggedBox(rendered).x + draggedBox(rendered).width / 2"
286
- :y="
287
- draggedBox(rendered).y +
288
- draggedBox(rendered).height / 2 -
289
- ((rendered.lines.length - 1) * ANNOTATION_TEXT_LINE_HEIGHT) / 2
290
- "
291
- :fill="rendered.style.textColor"
292
- :style="{ font: ANNOTATION_TEXT_FONT }"
293
- text-anchor="middle"
294
- dominant-baseline="central"
295
- >
296
- <tspan
297
- v-for="(line, index) in rendered.lines"
298
- :key="`${rendered.annotation.id}-${index}`"
285
+ <g :transform="labelTransform(rendered)">
286
+ <rect
287
+ class="waveform-annotation__box"
288
+ :x="draggedBox(rendered).x"
289
+ :y="draggedBox(rendered).y"
290
+ :width="draggedBox(rendered).width"
291
+ :height="draggedBox(rendered).height"
292
+ :fill="rendered.style.backgroundColor"
293
+ :stroke="rendered.style.borderColor"
294
+ />
295
+ <text
296
+ class="waveform-annotation__text"
299
297
  :x="draggedBox(rendered).x + draggedBox(rendered).width / 2"
300
- :dy="index === 0 ? 0 : ANNOTATION_TEXT_LINE_HEIGHT"
298
+ :y="
299
+ draggedBox(rendered).y +
300
+ draggedBox(rendered).height / 2 -
301
+ ((rendered.lines.length - 1) * ANNOTATION_TEXT_LINE_HEIGHT) / 2
302
+ "
303
+ :fill="rendered.style.textColor"
304
+ :style="{ font: ANNOTATION_TEXT_FONT }"
305
+ text-anchor="middle"
306
+ dominant-baseline="central"
301
307
  >
302
- {{ line }}
303
- </tspan>
304
- </text>
308
+ <tspan
309
+ v-for="(line, index) in rendered.lines"
310
+ :key="`${rendered.annotation.id}-${index}`"
311
+ :x="draggedBox(rendered).x + draggedBox(rendered).width / 2"
312
+ :dy="index === 0 ? 0 : ANNOTATION_TEXT_LINE_HEIGHT"
313
+ >
314
+ {{ line }}
315
+ </tspan>
316
+ </text>
317
+ </g>
305
318
  </g>
306
319
  </g>
307
320
  </template>
@@ -57,6 +57,9 @@ export type AnnotationPlacement =
57
57
  'top' | 'bottom' | 'right' | 'left' | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
58
58
 
59
59
  export interface RenderedAnnotation {
60
+ animationStyle?: { opacity: number; scale: number; offsetY: number }
61
+ leaving?: boolean
62
+ seriesOpacity?: number
60
63
  annotation: WaveformAnnotation
61
64
  trackIndex: number
62
65
  anchorX: number
@@ -16,6 +16,7 @@ import { paddedDomain } from '../../utils'
16
16
  import { transformForDomain } from '../interaction/zoomConstraints'
17
17
  import { constrainViewportZoom } from '../interaction/remoteViewport'
18
18
  import { normalizeIntegerZoomDomain } from '../interaction/integerZoom'
19
+ import { resolveStepZoomDomain, sameZoomDomain } from '../interaction/stepZoom'
19
20
  import type { useControlMode } from './useControlMode'
20
21
 
21
22
  type Domain = [number, number]
@@ -34,7 +35,7 @@ interface Context {
34
35
  legacyReset: (index?: number) => void
35
36
  legacySet: (domain: Domain, index?: number) => void
36
37
  }
37
- const same = (a: Domain, b: Domain) => a.every((v, i) => Math.abs(v - b[i]!) < 1e-12)
38
+ const same = sameZoomDomain
38
39
  export function useChartCommands(c: Context) {
39
40
  let sequence = 0
40
41
  const independent = computed(() => c.props.displayMode === 'independent')
@@ -104,18 +105,16 @@ export function useChartCommands(c: Context) {
104
105
  paddedDomain(tracks.flatMap((t) => t.seriesList.flatMap((s) => s.xDomain))),
105
106
  c.props.xDomainStrategy,
106
107
  )
107
- let requested: Domain = domain ?? bounds
108
- if (before && (action === 'zoom-in' || action === 'zoom-out')) {
109
- const [a, b] = before.xDomain
110
- const half = (b - a) * (action === 'zoom-in' ? 0.25 : 1)
111
- requested = [(a + b) / 2 - half, (a + b) / 2 + half]
112
- }
113
- const xDomain = constrainViewportZoom(
114
- normalizeIntegerZoomDomain(requested, c.props),
115
- bounds,
116
- tracks.filter((t) => t.hasVisibleSeries).map((t) => t.seriesList),
117
- c.props,
118
- )
108
+ const seriesGroups = tracks.filter((t) => t.hasVisibleSeries).map((t) => t.seriesList)
109
+ const xDomain =
110
+ before && (action === 'zoom-in' || action === 'zoom-out')
111
+ ? resolveStepZoomDomain(before.xDomain, action, bounds, seriesGroups, c.props)
112
+ : constrainViewportZoom(
113
+ normalizeIntegerZoomDomain(domain ?? bounds, c.props),
114
+ bounds,
115
+ seriesGroups,
116
+ c.props,
117
+ )
119
118
  const resetsY = action === 'reset' || action === 'fit'
120
119
  const hasY =
121
120
  resetsY &&
@@ -24,6 +24,8 @@ interface Context {
24
24
  size: () => { width: number; height: number; titleHeight: number }
25
25
  version: () => unknown
26
26
  ready: () => boolean
27
+ prepare?: () => void
28
+ release?: () => void
27
29
  }
28
30
  export function useChartImageExport(c: Context) {
29
31
  let busy = false
@@ -129,6 +131,7 @@ export function useChartImageExport(c: Context) {
129
131
  return png
130
132
  }
131
133
  try {
134
+ c.prepare?.()
132
135
  return await Promise.race([cancelled, generate()])
133
136
  } catch (error) {
134
137
  if (error instanceof Error && 'code' in error) throw error
@@ -149,6 +152,7 @@ export function useChartImageExport(c: Context) {
149
152
  }
150
153
  abort = undefined
151
154
  busy = false
155
+ c.release?.()
152
156
  }
153
157
  }
154
158
  return { exportImage }
@@ -0,0 +1,44 @@
1
+ import type { WaveformAmplitudeLimit, WaveformAmplitudeLimits } from '../../types'
2
+
3
+ export function normalizeAmplitudeLimit(
4
+ limit?: WaveformAmplitudeLimit,
5
+ ): WaveformAmplitudeLimit | undefined {
6
+ if (!limit) return undefined
7
+ const lower = Number.isFinite(limit.lower) ? limit.lower : undefined
8
+ const upper = Number.isFinite(limit.upper) ? limit.upper : undefined
9
+ if (lower === undefined && upper === undefined) return undefined
10
+ if (lower !== undefined && upper !== undefined && lower >= upper) return undefined
11
+ return { lower, upper }
12
+ }
13
+
14
+ export function mergeAmplitudeLimits(
15
+ series: readonly { id: string }[],
16
+ limits?: WaveformAmplitudeLimits,
17
+ ): WaveformAmplitudeLimit | undefined {
18
+ const valid = series.flatMap((item) => {
19
+ const limit = normalizeAmplitudeLimit(limits?.[item.id])
20
+ return limit ? [limit] : []
21
+ })
22
+ const lowers = valid.flatMap((limit) => (limit.lower === undefined ? [] : [limit.lower]))
23
+ const uppers = valid.flatMap((limit) => (limit.upper === undefined ? [] : [limit.upper]))
24
+ return normalizeAmplitudeLimit({
25
+ lower: lowers.length ? Math.min(...lowers) : undefined,
26
+ upper: uppers.length ? Math.max(...uppers) : undefined,
27
+ })
28
+ }
29
+
30
+ export function applyAmplitudeLimits(
31
+ padded: [number, number],
32
+ limit: WaveformAmplitudeLimit,
33
+ automatic: [number, number] = padded,
34
+ ): [number, number] {
35
+ let lower = limit.lower ?? padded[0]
36
+ let upper = limit.upper ?? padded[1]
37
+ if (lower >= upper) {
38
+ const fixed = limit.lower ?? limit.upper!
39
+ const span = Math.max(automatic[1] - automatic[0], Math.abs(fixed) * 0.1, 1)
40
+ if (limit.lower !== undefined) upper = lower + span
41
+ else lower = upper - span
42
+ }
43
+ return [lower, upper]
44
+ }