waveform-analysis 0.1.74 → 0.1.75
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.
- package/README.md +246 -39
- package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
- package/dist/index.cjs +1 -2
- package/dist/index.js +1 -7119
- package/dist/style.css +1 -1
- package/dist/types/animation/WaveformAnimationController.d.ts +30 -0
- package/dist/types/animation/core/Animation.d.ts +13 -0
- package/dist/types/animation/core/Animator.d.ts +23 -0
- package/dist/types/animation/core/easing.d.ts +3 -0
- package/dist/types/animation/core/errors.d.ts +4 -0
- package/dist/types/animation/core/handle.d.ts +7 -0
- package/dist/types/animation/core/owner.d.ts +3 -0
- package/dist/types/animation/core/propertyGroups.d.ts +17 -0
- package/dist/types/animation/core/task.d.ts +29 -0
- package/dist/types/animation/core/types.d.ts +74 -0
- package/dist/types/animation/core/validation.d.ts +30 -0
- package/dist/types/animation/core/values.d.ts +8 -0
- package/dist/types/animation/presets/annotation.d.ts +4 -0
- package/dist/types/animation/presets/lineDrawing.d.ts +7 -0
- package/dist/types/animation/presets/shared.d.ts +14 -0
- package/dist/types/animation/presets/transitions.d.ts +4 -0
- package/dist/types/animation/types.d.ts +84 -0
- package/dist/types/components/WaveformChart.vue.d.ts +3 -2
- package/dist/types/components/animation/annotationPresentation.d.ts +17 -0
- package/dist/types/components/animation/chartExportVersion.d.ts +2 -0
- package/dist/types/components/animation/config.d.ts +31 -0
- package/dist/types/components/animation/context.d.ts +20 -0
- package/dist/types/components/animation/geometry.d.ts +10 -0
- package/dist/types/components/animation/paginationPresentation.d.ts +13 -0
- package/dist/types/components/animation/pathBuffer.d.ts +4 -0
- package/dist/types/components/animation/trackPresentation.d.ts +35 -0
- package/dist/types/components/animation/useWaveformAnimations.d.ts +27 -0
- package/dist/types/components/annotation/WaveformAnnotationContextMenu.vue.d.ts +2 -2
- package/dist/types/components/annotation/types.d.ts +7 -0
- package/dist/types/components/controls/useChartImageExport.d.ts +2 -0
- package/dist/types/components/core/types.d.ts +8 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +25 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
- package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
- package/dist/types/components/rendering/updateAxis.d.ts +5 -0
- package/dist/types/components/rendering/updateGrid.d.ts +3 -0
- package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
- package/dist/types/components/utils/assignElement.d.ts +2 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
- package/package.json +5 -2
- package/src/DemoRouterApp.vue +1 -0
- package/src/animation/WaveformAnimationController.ts +328 -0
- package/src/animation/core/Animation.ts +37 -0
- package/src/animation/core/Animator.ts +200 -0
- package/src/animation/core/easing.ts +74 -0
- package/src/animation/core/errors.ts +24 -0
- package/src/animation/core/handle.ts +34 -0
- package/src/animation/core/owner.ts +9 -0
- package/src/animation/core/propertyGroups.ts +143 -0
- package/src/animation/core/task.ts +153 -0
- package/src/animation/core/types.ts +81 -0
- package/src/animation/core/validation.ts +173 -0
- package/src/animation/core/values.ts +38 -0
- package/src/animation/presets/annotation.ts +66 -0
- package/src/animation/presets/lineDrawing.ts +51 -0
- package/src/animation/presets/shared.ts +58 -0
- package/src/animation/presets/transitions.ts +77 -0
- package/src/animation/types.ts +81 -0
- package/src/components/WaveformChart.vue +1 -0
- package/src/components/WaveformChartView.vue +13 -4
- package/src/components/animation/annotationPresentation.ts +145 -0
- package/src/components/animation/chartExportVersion.ts +5 -0
- package/src/components/animation/config.ts +87 -0
- package/src/components/animation/context.ts +56 -0
- package/src/components/animation/geometry.ts +148 -0
- package/src/components/animation/paginationPresentation.ts +114 -0
- package/src/components/animation/pathBuffer.ts +61 -0
- package/src/components/animation/trackPresentation.ts +172 -0
- package/src/components/animation/useWaveformAnimations.ts +420 -0
- package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
- package/src/components/annotation/types.ts +3 -0
- package/src/components/controls/useChartImageExport.ts +4 -0
- package/src/components/core/trackLayoutBuilder.ts +9 -18
- package/src/components/core/types.ts +4 -0
- package/src/components/core/useWaveformChartController.ts +24 -20
- package/src/components/core/useWaveformRenderSampling.ts +23 -1
- package/src/components/core/waveformChartTypes.ts +2 -1
- package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
- package/src/components/rendering/WaveformTrack.vue +6 -1
- package/src/components/rendering/WaveformTrackAxes.vue +64 -51
- package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
- package/src/components/rendering/updateAxis.ts +57 -0
- package/src/components/rendering/updateGrid.ts +42 -0
- package/src/components/rendering/yAxisTickVisibility.ts +45 -18
- package/src/components/utils/assignElement.ts +7 -0
- package/src/index.ts +52 -0
- package/src/router.ts +1 -0
- package/src/views/AnimationDemo.vue +129 -0
- package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
|
@@ -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
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
@@ -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 }
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
+
import { generateWaveformPath } from '../animation/geometry'
|
|
1
2
|
import { withMaximumTick, type YAxisPaddingOptions } from './yAxisPadding'
|
|
2
3
|
import { selectSeriesRenderSourcePoints } from '../../core/rendering'
|
|
3
4
|
import { pointSourceFromPoints } from '../../core/waveformPointSource'
|
|
4
|
-
import {
|
|
5
|
-
curveStep,
|
|
6
|
-
curveStepAfter,
|
|
7
|
-
curveStepBefore,
|
|
8
|
-
line,
|
|
9
|
-
scaleLinear,
|
|
10
|
-
zoomIdentity,
|
|
11
|
-
type ZoomTransform,
|
|
12
|
-
} from 'd3'
|
|
5
|
+
import { scaleLinear, zoomIdentity, type ZoomTransform } from 'd3'
|
|
13
6
|
|
|
14
7
|
import { type ResolvedWaveformRenderingOptions } from '../../core/rendering'
|
|
15
8
|
import type {
|
|
@@ -264,17 +257,15 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
264
257
|
hasErrorPoints: trackSeries.hasErrorPoints,
|
|
265
258
|
},
|
|
266
259
|
)
|
|
267
|
-
const pathGenerator = line<WaveformPoint>()
|
|
268
|
-
.x((point) => xScale(point.x))
|
|
269
|
-
.y((point) => seriesYScale(point.y))
|
|
270
|
-
if (trackSeries.lineType === 'step-start') pathGenerator.curve(curveStepBefore)
|
|
271
|
-
if (trackSeries.lineType === 'step-middle') pathGenerator.curve(curveStep)
|
|
272
|
-
if (trackSeries.lineType === 'step-end' || trackSeries.lineType === 'step-after') {
|
|
273
|
-
pathGenerator.curve(curveStepAfter)
|
|
274
|
-
}
|
|
275
260
|
return {
|
|
276
261
|
series: trackSeries,
|
|
277
|
-
path:
|
|
262
|
+
path: generateWaveformPath(
|
|
263
|
+
renderPoints.linePoints,
|
|
264
|
+
trackSeries.lineType,
|
|
265
|
+
xScale,
|
|
266
|
+
seriesYScale,
|
|
267
|
+
),
|
|
268
|
+
lineRenderPoints: renderPoints.linePoints,
|
|
278
269
|
pointRenderPoints: renderPoints.pointRenderPoints,
|
|
279
270
|
errorBarRenderPoints: renderPoints.errorBarRenderPoints,
|
|
280
271
|
yScale: seriesYScale,
|
|
@@ -47,6 +47,10 @@ export interface DisplayTrack {
|
|
|
47
47
|
export interface TrackSeriesPath {
|
|
48
48
|
series: DisplaySeries
|
|
49
49
|
path: string | null
|
|
50
|
+
lineRenderPoints?: WaveformPoint[]
|
|
51
|
+
presentationKey?: string
|
|
52
|
+
opacity?: number
|
|
53
|
+
animationClip?: { id: string; x: number; width: number }
|
|
50
54
|
pointRenderPoints: WaveformPoint[]
|
|
51
55
|
errorBarRenderPoints: WaveformPoint[]
|
|
52
56
|
yScale: ScaleLinear<number, number>
|