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.
- package/README.md +422 -60
- 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/amplitudeLimits.d.ts +6 -0
- package/dist/types/components/core/layout.d.ts +6 -9
- package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
- package/dist/types/components/core/types.d.ts +8 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +26 -1
- package/dist/types/components/core/useWaveformData.d.ts +2 -2
- package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +6 -3
- package/dist/types/components/core/xAxisTicks.d.ts +2 -0
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
- package/dist/types/components/core/yAxisTicks.d.ts +6 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/integerZoom.d.ts +3 -3
- package/dist/types/components/interaction/stepZoom.d.ts +5 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
- package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
- 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/core/data.d.ts +1 -1
- package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
- package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
- package/dist/types/demo/types.d.ts +11 -2
- package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/types/chart.d.ts +8 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/formatters.d.ts +4 -4
- package/dist/types/utils/xAxisNumber.d.ts +2 -0
- package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
- package/package.json +5 -2
- package/src/App.vue +34 -100
- 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 -6
- 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/useChartCommands.ts +12 -13
- package/src/components/controls/useChartImageExport.ts +4 -0
- package/src/components/core/amplitudeLimits.ts +44 -0
- package/src/components/core/layout.ts +22 -34
- package/src/components/core/trackLayoutBuilder.ts +21 -23
- package/src/components/core/types.ts +4 -0
- package/src/components/core/useWaveformChartController.ts +27 -23
- package/src/components/core/useWaveformChartLifecycle.ts +11 -2
- package/src/components/core/useWaveformData.ts +11 -5
- package/src/components/core/useWaveformLayout.ts +9 -4
- package/src/components/core/useWaveformMeasurements.ts +3 -0
- package/src/components/core/useWaveformRenderSampling.ts +23 -1
- package/src/components/core/useWaveformYAxisLayout.ts +2 -0
- package/src/components/core/waveformChartTypes.ts +6 -2
- package/src/components/core/xAxisTicks.ts +69 -0
- package/src/components/core/yAxisLayoutMetrics.ts +4 -0
- package/src/components/core/yAxisTicks.ts +39 -0
- package/src/components/data/types.ts +2 -0
- package/src/components/interaction/integerZoom.ts +31 -12
- package/src/components/interaction/remoteViewport.ts +26 -4
- package/src/components/interaction/stepZoom.ts +33 -0
- package/src/components/interaction/useWaveformViewport.ts +3 -26
- package/src/components/interaction/useWaveformZoom.ts +50 -157
- package/src/components/interaction/zoomConstraints.ts +5 -5
- 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/core/data.ts +5 -2
- package/src/data/simulatedWaveforms.ts +0 -10
- package/src/demo/DemoChartHost.vue +49 -4
- package/src/demo/remoteWaveformLoader.ts +114 -0
- package/src/demo/types.ts +11 -1
- package/src/demo/useRemoteWaveformWindow.ts +229 -0
- package/src/index.ts +54 -0
- package/src/router.ts +1 -0
- package/src/types/chart.ts +10 -1
- package/src/types/index.ts +2 -0
- package/src/utils/formatters.ts +10 -5
- package/src/utils/xAxisNumber.ts +36 -0
- package/src/views/AnimationDemo.vue +129 -0
- package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
- package/dist/types/data/sx2Channel.d.ts +0 -10
- package/src/data/sx2Channel.ts +0 -315
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Animator, globalAnimator } from './core/Animator'
|
|
2
|
+
import type { AnimationConfig, AnimationHandle, AnimationState } from './core/types'
|
|
3
|
+
import { copyAnimationValue, validateAnimationConfig } from './core/validation'
|
|
4
|
+
import { animationError, notify, reportAnimationError } from './core/errors'
|
|
5
|
+
import { createAnimationOwner } from './core/owner'
|
|
6
|
+
import { createAnimationHandle } from './core/handle'
|
|
7
|
+
import type {
|
|
8
|
+
AnimationObjectKey,
|
|
9
|
+
WaveformAnimationControllerOptions,
|
|
10
|
+
AnimationBinding,
|
|
11
|
+
ClipAnimationState,
|
|
12
|
+
LineDrawingOptions,
|
|
13
|
+
StaggeredLineDrawingOptions,
|
|
14
|
+
GeometryTransitionInput,
|
|
15
|
+
GeometryTransitionFrame,
|
|
16
|
+
DataUpdateOptions,
|
|
17
|
+
AnnotationAnimationState,
|
|
18
|
+
AnnotationAnimationOptions,
|
|
19
|
+
} from './types'
|
|
20
|
+
import {
|
|
21
|
+
createLineDrawingAnimation,
|
|
22
|
+
createStaggeredLineDrawingAnimations,
|
|
23
|
+
} from './presets/lineDrawing'
|
|
24
|
+
import { createPathTransitionAnimation, createYAxisScaleAnimation } from './presets/transitions'
|
|
25
|
+
import {
|
|
26
|
+
createAnnotationAppearAnimation,
|
|
27
|
+
createAnnotationDisappearAnimation,
|
|
28
|
+
} from './presets/annotation'
|
|
29
|
+
|
|
30
|
+
interface RecordEntry {
|
|
31
|
+
key: AnimationObjectKey
|
|
32
|
+
config: AnimationConfig
|
|
33
|
+
id: string
|
|
34
|
+
raw?: AnimationHandle
|
|
35
|
+
state?: AnimationState
|
|
36
|
+
handle: AnimationHandle
|
|
37
|
+
release: () => void
|
|
38
|
+
manualPaused: boolean
|
|
39
|
+
}
|
|
40
|
+
function conflicts(a: AnimationObjectKey, b: AnimationObjectKey): boolean {
|
|
41
|
+
if (a.kind !== b.kind || a.id !== b.id) return false
|
|
42
|
+
if (
|
|
43
|
+
a.kind === 'annotation' &&
|
|
44
|
+
['enter', 'leave'].includes(a.scope) &&
|
|
45
|
+
['enter', 'leave'].includes(b.scope)
|
|
46
|
+
)
|
|
47
|
+
return true
|
|
48
|
+
if (
|
|
49
|
+
a.kind === 'series' &&
|
|
50
|
+
['enter', 'update'].includes(a.scope) &&
|
|
51
|
+
['enter', 'update'].includes(b.scope)
|
|
52
|
+
)
|
|
53
|
+
return true
|
|
54
|
+
return a.scope === b.scope
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Owner-scoped coordination; it never installs browser listeners or writes DOM. */
|
|
58
|
+
export class WaveformAnimationController {
|
|
59
|
+
#records = new Map<string, RecordEntry>()
|
|
60
|
+
#owner = createAnimationOwner()
|
|
61
|
+
#enabled: boolean
|
|
62
|
+
#animator: NonNullable<WaveformAnimationControllerOptions['animator']>
|
|
63
|
+
#barrier = false
|
|
64
|
+
#disposed = false
|
|
65
|
+
#paused = new Set<string>()
|
|
66
|
+
constructor(private options: WaveformAnimationControllerOptions) {
|
|
67
|
+
if (!options || typeof options.commitFinalState !== 'function')
|
|
68
|
+
throw new TypeError('commitFinalState is required')
|
|
69
|
+
this.#enabled = options.enabled ?? true
|
|
70
|
+
this.#animator = options.animator ?? globalAnimator
|
|
71
|
+
}
|
|
72
|
+
#assertOpen(): void {
|
|
73
|
+
if (this.#barrier) throw animationError('animation-terminating')
|
|
74
|
+
if (this.#disposed) throw animationError('animation-disposed')
|
|
75
|
+
}
|
|
76
|
+
animate(key: AnimationObjectKey, config: AnimationConfig): AnimationHandle {
|
|
77
|
+
this.#assertOpen()
|
|
78
|
+
if (
|
|
79
|
+
!key ||
|
|
80
|
+
!['series', 'annotation', 'axis', 'custom'].includes(key.kind) ||
|
|
81
|
+
typeof key.id !== 'string' ||
|
|
82
|
+
!['enter', 'update', 'leave', 'domain'].includes(key.scope)
|
|
83
|
+
)
|
|
84
|
+
throw new TypeError('Invalid animation object key')
|
|
85
|
+
if (this.#animator instanceof Animator) this.#animator.validate(config)
|
|
86
|
+
else validateAnimationConfig(config)
|
|
87
|
+
// Old terminal callbacks may mutate caller-owned arrays before manager registration.
|
|
88
|
+
// Preserve the group-aware preflight contract without sharing those endpoints.
|
|
89
|
+
const snapshot = {
|
|
90
|
+
...config,
|
|
91
|
+
tracks: config.tracks?.map((track) => ({
|
|
92
|
+
...track,
|
|
93
|
+
from: track.from === undefined ? undefined : copyAnimationValue(track.from, 'from'),
|
|
94
|
+
to: copyAnimationValue(track.to, 'to'),
|
|
95
|
+
})),
|
|
96
|
+
}
|
|
97
|
+
const id = this.#owner(key)
|
|
98
|
+
const olds = [...this.#records.values()].filter((record) => conflicts(record.key, key))
|
|
99
|
+
const record = { key: { ...key }, config: snapshot, id, manualPaused: false } as RecordEntry
|
|
100
|
+
const { handle, release } = createAnimationHandle(
|
|
101
|
+
id,
|
|
102
|
+
() => record.state ?? record.raw?.state ?? 'PENDING',
|
|
103
|
+
(method) => {
|
|
104
|
+
if (method === 'pause') {
|
|
105
|
+
record.manualPaused = true
|
|
106
|
+
record.raw?.pause()
|
|
107
|
+
} else if (method === 'resume') {
|
|
108
|
+
record.manualPaused = false
|
|
109
|
+
if (this.#animator instanceof Animator || !this.#paused.size) record.raw?.resume()
|
|
110
|
+
} else if (method === 'abort') {
|
|
111
|
+
if (record.raw) record.raw.abort()
|
|
112
|
+
else this.#finishRecord(record, 'ABORTED')
|
|
113
|
+
} else record.raw?.finish()
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
record.handle = handle
|
|
117
|
+
record.release = release
|
|
118
|
+
this.#records.set(id, record)
|
|
119
|
+
for (const old of olds) {
|
|
120
|
+
if (key.kind === 'series' && key.scope === 'update' && old.key.scope === 'enter') {
|
|
121
|
+
try {
|
|
122
|
+
old.config.during?.(1, {
|
|
123
|
+
rawProgress: 1,
|
|
124
|
+
directedProgress: 1,
|
|
125
|
+
iteration: 0,
|
|
126
|
+
terminal: true,
|
|
127
|
+
})
|
|
128
|
+
} catch (error) {
|
|
129
|
+
reportAnimationError(old.config, error)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
old.handle.abort()
|
|
133
|
+
}
|
|
134
|
+
if (record.state) return record.handle
|
|
135
|
+
const wrapped: AnimationConfig = {
|
|
136
|
+
...snapshot,
|
|
137
|
+
initialize:
|
|
138
|
+
this.#animator instanceof Animator
|
|
139
|
+
? snapshot.initialize
|
|
140
|
+
: () => {
|
|
141
|
+
snapshot.initialize?.()
|
|
142
|
+
if (this.#paused.size) this.#animator.pause(id)
|
|
143
|
+
},
|
|
144
|
+
duration: this.#enabled ? snapshot.duration : 0,
|
|
145
|
+
loop: this.#enabled ? snapshot.loop : 1,
|
|
146
|
+
done: () => {
|
|
147
|
+
if (!this.#barrier) this.#finishRecord(record, 'FINISHED')
|
|
148
|
+
},
|
|
149
|
+
aborted: () => {
|
|
150
|
+
if (!this.#barrier) this.#finishRecord(record, 'ABORTED')
|
|
151
|
+
},
|
|
152
|
+
onError: (error) => {
|
|
153
|
+
reportAnimationError(snapshot, error)
|
|
154
|
+
if (!this.#barrier && !this.#disposed) this.finishAll()
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
record.raw =
|
|
159
|
+
this.#animator instanceof Animator
|
|
160
|
+
? this.#animator.animatePaused(
|
|
161
|
+
wrapped,
|
|
162
|
+
id,
|
|
163
|
+
[...this.#paused].map((reason) => this.#ownerPause(reason)),
|
|
164
|
+
)
|
|
165
|
+
: this.#animator.animate(wrapped, id)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.#finishRecord(record, 'ABORTED')
|
|
168
|
+
throw error
|
|
169
|
+
}
|
|
170
|
+
if (this.#animator instanceof Animator)
|
|
171
|
+
for (const reason of this.#paused) {
|
|
172
|
+
this.#animator.setPauseReason(this.#ownerPause(reason), true, [id])
|
|
173
|
+
}
|
|
174
|
+
else if (this.#paused.size) record.raw.pause()
|
|
175
|
+
return record.handle
|
|
176
|
+
}
|
|
177
|
+
#finishRecord(record: RecordEntry, state: 'FINISHED' | 'ABORTED'): void {
|
|
178
|
+
if (record.state === 'FINISHED' || record.state === 'ABORTED') return
|
|
179
|
+
record.state = state
|
|
180
|
+
if (this.#records.get(record.id) === record) this.#records.delete(record.id)
|
|
181
|
+
record.release()
|
|
182
|
+
notify(record.config, state === 'FINISHED' ? record.config.done : record.config.aborted)
|
|
183
|
+
}
|
|
184
|
+
animateSeriesEnter(
|
|
185
|
+
id: string,
|
|
186
|
+
binding: AnimationBinding<ClipAnimationState>,
|
|
187
|
+
width: number,
|
|
188
|
+
options?: LineDrawingOptions,
|
|
189
|
+
): AnimationHandle {
|
|
190
|
+
this.#assertOpen()
|
|
191
|
+
return this.animate(
|
|
192
|
+
{ kind: 'series', id, scope: 'enter' },
|
|
193
|
+
createLineDrawingAnimation(binding, width, options),
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
animateMultiSeriesEnter(
|
|
197
|
+
series: readonly { id: string; binding: AnimationBinding<ClipAnimationState>; width: number }[],
|
|
198
|
+
options?: StaggeredLineDrawingOptions,
|
|
199
|
+
): AnimationHandle[] {
|
|
200
|
+
this.#assertOpen()
|
|
201
|
+
const configs = createStaggeredLineDrawingAnimations(series, options)
|
|
202
|
+
return configs.map((config, index) =>
|
|
203
|
+
this.animate({ kind: 'series', id: series[index].id, scope: 'enter' }, config),
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
animateDataUpdate(
|
|
207
|
+
id: string,
|
|
208
|
+
input: GeometryTransitionInput,
|
|
209
|
+
write: (frame: GeometryTransitionFrame) => void,
|
|
210
|
+
options?: DataUpdateOptions,
|
|
211
|
+
): AnimationHandle {
|
|
212
|
+
this.#assertOpen()
|
|
213
|
+
return this.animate(
|
|
214
|
+
{ kind: 'series', id, scope: 'update' },
|
|
215
|
+
createPathTransitionAnimation(input, write, options),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
animateYAxisScale(
|
|
219
|
+
id: string,
|
|
220
|
+
binding: AnimationBinding<[number, number]>,
|
|
221
|
+
to: [number, number],
|
|
222
|
+
options?: DataUpdateOptions,
|
|
223
|
+
): AnimationHandle {
|
|
224
|
+
this.#assertOpen()
|
|
225
|
+
return this.animate(
|
|
226
|
+
{ kind: 'axis', id, scope: 'domain' },
|
|
227
|
+
createYAxisScaleAnimation(binding, to, options),
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
animateAnnotationAppear(
|
|
231
|
+
id: string,
|
|
232
|
+
binding: AnimationBinding<AnnotationAnimationState>,
|
|
233
|
+
options?: AnnotationAnimationOptions,
|
|
234
|
+
): AnimationHandle {
|
|
235
|
+
this.#assertOpen()
|
|
236
|
+
return this.animate(
|
|
237
|
+
{ kind: 'annotation', id, scope: 'enter' },
|
|
238
|
+
createAnnotationAppearAnimation(binding, options),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
animateAnnotationDisappear(
|
|
242
|
+
id: string,
|
|
243
|
+
binding: AnimationBinding<AnnotationAnimationState>,
|
|
244
|
+
options?: AnnotationAnimationOptions,
|
|
245
|
+
): AnimationHandle {
|
|
246
|
+
this.#assertOpen()
|
|
247
|
+
return this.animate(
|
|
248
|
+
{ kind: 'annotation', id, scope: 'leave' },
|
|
249
|
+
createAnnotationDisappearAnimation(binding, options),
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
cancel(key: AnimationObjectKey): void {
|
|
253
|
+
this.#records.get(this.#owner(key))?.handle.abort()
|
|
254
|
+
}
|
|
255
|
+
cancelAll(): void {
|
|
256
|
+
this.#bulk(false, false)
|
|
257
|
+
}
|
|
258
|
+
finishAll(): void {
|
|
259
|
+
this.#bulk(true, true)
|
|
260
|
+
}
|
|
261
|
+
/** Adapter category commits are separate from the whole-chart final-state commit. */
|
|
262
|
+
finishWhere(predicate: (key: AnimationObjectKey) => boolean, commit: () => void): void {
|
|
263
|
+
this.#bulk(true, true, predicate, commit)
|
|
264
|
+
}
|
|
265
|
+
#bulk(
|
|
266
|
+
finish: boolean,
|
|
267
|
+
commit: boolean,
|
|
268
|
+
predicate: (key: AnimationObjectKey) => boolean = () => true,
|
|
269
|
+
submit = this.options.commitFinalState,
|
|
270
|
+
): void {
|
|
271
|
+
if (this.#barrier || this.#disposed) return
|
|
272
|
+
this.#barrier = true
|
|
273
|
+
const records = [...this.#records.values()].filter((record) => predicate(record.key))
|
|
274
|
+
try {
|
|
275
|
+
for (const record of records) {
|
|
276
|
+
if (this.#records.get(record.id) === record) this.#records.delete(record.id)
|
|
277
|
+
record.raw?.abort()
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
if (commit) submit()
|
|
281
|
+
} catch (error) {
|
|
282
|
+
for (const record of records) {
|
|
283
|
+
this.#finishRecord(record, 'ABORTED')
|
|
284
|
+
reportAnimationError(record.config, error)
|
|
285
|
+
}
|
|
286
|
+
throw error
|
|
287
|
+
}
|
|
288
|
+
for (const record of records) this.#finishRecord(record, finish ? 'FINISHED' : 'ABORTED')
|
|
289
|
+
} finally {
|
|
290
|
+
this.#barrier = false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
pauseAll(): void {
|
|
294
|
+
this.setPauseReason('manual-all', true)
|
|
295
|
+
}
|
|
296
|
+
resumeAll(): void {
|
|
297
|
+
this.setPauseReason('manual-all', false)
|
|
298
|
+
}
|
|
299
|
+
#ownerPause(reason: string) {
|
|
300
|
+
return `${this.#owner({ kind: 'custom', id: 'pause', scope: 'domain' })}:${reason}`
|
|
301
|
+
}
|
|
302
|
+
setPauseReason(reason: string, pause: boolean): void {
|
|
303
|
+
if (this.#disposed) return
|
|
304
|
+
if (pause) this.#paused.add(reason)
|
|
305
|
+
else this.#paused.delete(reason)
|
|
306
|
+
if (this.#animator instanceof Animator)
|
|
307
|
+
this.#animator.setPauseReason(this.#ownerPause(reason), pause, [...this.#records.keys()])
|
|
308
|
+
else if (pause) this.#records.forEach((record) => record.raw?.pause())
|
|
309
|
+
else if (!this.#paused.size)
|
|
310
|
+
this.#records.forEach((record) => {
|
|
311
|
+
if (!record.manualPaused) record.raw?.resume()
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
setEnabled(enabled: boolean): void {
|
|
315
|
+
if (this.#disposed || this.#enabled === enabled) return
|
|
316
|
+
this.#enabled = enabled
|
|
317
|
+
if (!enabled) this.finishAll()
|
|
318
|
+
}
|
|
319
|
+
getActiveAnimationCount(): number {
|
|
320
|
+
return this.#records.size
|
|
321
|
+
}
|
|
322
|
+
destroy(): void {
|
|
323
|
+
if (this.#disposed || this.#barrier) return
|
|
324
|
+
this.#bulk(false, false)
|
|
325
|
+
this.#disposed = true
|
|
326
|
+
this.#paused.clear()
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AnimationConfig, AnimationState } from './types'
|
|
2
|
+
import { validateAnimationConfig } from './validation'
|
|
3
|
+
import { PropertyGroups } from './propertyGroups'
|
|
4
|
+
import { AnimationTask } from './task'
|
|
5
|
+
|
|
6
|
+
/** Standalone manual timeline; use Animator for concurrent writers and additive groups. */
|
|
7
|
+
export class Animation {
|
|
8
|
+
readonly #task: AnimationTask
|
|
9
|
+
constructor(config: AnimationConfig) {
|
|
10
|
+
const groups = new PropertyGroups()
|
|
11
|
+
const validated = validateAnimationConfig(config)
|
|
12
|
+
validated.tracks = groups.capture(validated)
|
|
13
|
+
this.#task = new AnimationTask(validated, groups)
|
|
14
|
+
groups.bind(this.#task)
|
|
15
|
+
}
|
|
16
|
+
get state(): AnimationState {
|
|
17
|
+
return this.#task.state
|
|
18
|
+
}
|
|
19
|
+
start(time: number): void {
|
|
20
|
+
this.#task.start(time)
|
|
21
|
+
}
|
|
22
|
+
tick(time: number): boolean {
|
|
23
|
+
return this.#task.tick(time)
|
|
24
|
+
}
|
|
25
|
+
pause(time: number): void {
|
|
26
|
+
this.#task.pause(time)
|
|
27
|
+
}
|
|
28
|
+
resume(time: number): void {
|
|
29
|
+
this.#task.resume(time)
|
|
30
|
+
}
|
|
31
|
+
abort(): void {
|
|
32
|
+
this.#task.abort()
|
|
33
|
+
}
|
|
34
|
+
finish(): void {
|
|
35
|
+
this.#task.finish()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { AnimationConfig, AnimationHandle, AnimationScheduler } from './types'
|
|
2
|
+
import { validateAnimationConfig, validateAnimationScheduler } from './validation'
|
|
3
|
+
import { PropertyGroups } from './propertyGroups'
|
|
4
|
+
import { AnimationTask } from './task'
|
|
5
|
+
import { animationError } from './errors'
|
|
6
|
+
import { createAnimationHandle } from './handle'
|
|
7
|
+
|
|
8
|
+
function defaultScheduler(): AnimationScheduler {
|
|
9
|
+
// Constructed only on first animate, never during module import.
|
|
10
|
+
const raf =
|
|
11
|
+
typeof globalThis.requestAnimationFrame === 'function' &&
|
|
12
|
+
typeof globalThis.cancelAnimationFrame === 'function'
|
|
13
|
+
return {
|
|
14
|
+
now: () => globalThis.performance?.now() ?? Date.now(),
|
|
15
|
+
requestFrame: raf ? (callback) => globalThis.requestAnimationFrame(callback) : undefined,
|
|
16
|
+
cancelFrame: raf ? (id) => globalThis.cancelAnimationFrame(id) : undefined,
|
|
17
|
+
setTimer: (callback, delay) => setTimeout(callback, delay),
|
|
18
|
+
clearTimer: (id) => clearTimeout(id as ReturnType<typeof setTimeout>),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Shared numeric task scheduler. Empty, delayed-only and paused queues never spin RAF. */
|
|
23
|
+
export class Animator {
|
|
24
|
+
#scheduler?: AnimationScheduler
|
|
25
|
+
readonly #injected: boolean
|
|
26
|
+
#tasks = new Map<string, AnimationTask>()
|
|
27
|
+
#groups = new PropertyGroups()
|
|
28
|
+
#reasons = new Set<string>()
|
|
29
|
+
#frameId?: number
|
|
30
|
+
#timerId?: unknown
|
|
31
|
+
#hasTimer = false
|
|
32
|
+
#sequence = 0
|
|
33
|
+
#barrier = false
|
|
34
|
+
#frameRunning = false
|
|
35
|
+
constructor(scheduler?: AnimationScheduler) {
|
|
36
|
+
if (scheduler) validateAnimationScheduler(scheduler)
|
|
37
|
+
this.#scheduler = scheduler
|
|
38
|
+
this.#injected = !!scheduler
|
|
39
|
+
}
|
|
40
|
+
#clock() {
|
|
41
|
+
return (this.#scheduler ??= defaultScheduler())
|
|
42
|
+
}
|
|
43
|
+
validate(config: AnimationConfig): void {
|
|
44
|
+
validateAnimationConfig(config, this.#groups.context)
|
|
45
|
+
}
|
|
46
|
+
animate(config: AnimationConfig, id?: string): AnimationHandle {
|
|
47
|
+
return this.animatePaused(config, id, [])
|
|
48
|
+
}
|
|
49
|
+
/** Adapter registration with inherited owner reasons, before any scheduling occurs. */
|
|
50
|
+
animatePaused(
|
|
51
|
+
config: AnimationConfig,
|
|
52
|
+
id: string | undefined,
|
|
53
|
+
reasons: readonly string[],
|
|
54
|
+
): AnimationHandle {
|
|
55
|
+
if (this.#barrier) throw animationError('animation-terminating')
|
|
56
|
+
if (id !== undefined && typeof id !== 'string')
|
|
57
|
+
throw new TypeError('Animation id must be string')
|
|
58
|
+
const validated = validateAnimationConfig(config, this.#groups.context)
|
|
59
|
+
const conflicts = this.#groups.conflicts(validated) as Set<AnimationTask>
|
|
60
|
+
if (id !== undefined && this.#tasks.has(id)) conflicts.add(this.#tasks.get(id)!)
|
|
61
|
+
validated.tracks = this.#groups.capture(validated)
|
|
62
|
+
const task = new AnimationTask(validated, this.#groups)
|
|
63
|
+
let key = id
|
|
64
|
+
if (key === undefined) {
|
|
65
|
+
do {
|
|
66
|
+
key = `animation-${++this.#sequence}`
|
|
67
|
+
} while (this.#tasks.has(key))
|
|
68
|
+
}
|
|
69
|
+
const callbacks = [...conflicts].map((old) => old.settle('ABORTED', false))
|
|
70
|
+
this.#tasks.set(key, task)
|
|
71
|
+
this.#groups.bind(task)
|
|
72
|
+
const scheduler = this.#clock()
|
|
73
|
+
const { handle, release } = createAnimationHandle(
|
|
74
|
+
key,
|
|
75
|
+
() => task.state,
|
|
76
|
+
(method) => {
|
|
77
|
+
task[method](scheduler.now())
|
|
78
|
+
this.#schedule()
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
task.setTermination(() => {
|
|
82
|
+
if (this.#tasks.get(key) === task) this.#tasks.delete(key)
|
|
83
|
+
release()
|
|
84
|
+
this.#schedule()
|
|
85
|
+
})
|
|
86
|
+
for (const reason of new Set([...this.#reasons, ...reasons]))
|
|
87
|
+
task.pause(scheduler.now(), reason)
|
|
88
|
+
// The new owner is visible to reentrant requests before old user notifications.
|
|
89
|
+
callbacks.forEach((callback) => callback())
|
|
90
|
+
if (!task.ended) task.start(scheduler.now(), !scheduler.requestFrame)
|
|
91
|
+
this.#schedule()
|
|
92
|
+
return handle
|
|
93
|
+
}
|
|
94
|
+
remove(id: string): void {
|
|
95
|
+
this.#tasks.get(id)?.abort()
|
|
96
|
+
this.#schedule()
|
|
97
|
+
}
|
|
98
|
+
pause(id: string): void {
|
|
99
|
+
this.#tasks.get(id)?.pause(this.#clock().now())
|
|
100
|
+
this.#schedule()
|
|
101
|
+
}
|
|
102
|
+
resume(id: string): void {
|
|
103
|
+
this.#tasks.get(id)?.resume(this.#clock().now())
|
|
104
|
+
this.#schedule()
|
|
105
|
+
}
|
|
106
|
+
pauseAll(): void {
|
|
107
|
+
this.setPauseReason('manual-all', true)
|
|
108
|
+
}
|
|
109
|
+
resumeAll(): void {
|
|
110
|
+
this.setPauseReason('manual-all', false)
|
|
111
|
+
}
|
|
112
|
+
/** Independent adapter-owned pause reasons, including document-hidden and owner scopes. */
|
|
113
|
+
setPauseReason(reason: string, paused: boolean, ids?: readonly string[]): void {
|
|
114
|
+
if (!ids) {
|
|
115
|
+
if (paused) this.#reasons.add(reason)
|
|
116
|
+
else this.#reasons.delete(reason)
|
|
117
|
+
}
|
|
118
|
+
const tasks = ids
|
|
119
|
+
? ids.map((id) => this.#tasks.get(id)).filter((task) => !!task)
|
|
120
|
+
: [...this.#tasks.values()]
|
|
121
|
+
const time = this.#clock().now()
|
|
122
|
+
for (const task of tasks) {
|
|
123
|
+
if (paused) task.pause(time, reason)
|
|
124
|
+
else task.resume(time, reason)
|
|
125
|
+
}
|
|
126
|
+
this.#schedule()
|
|
127
|
+
}
|
|
128
|
+
finishAll(): void {
|
|
129
|
+
this.#bulk('finish')
|
|
130
|
+
}
|
|
131
|
+
clear(): void {
|
|
132
|
+
this.#bulk('abort')
|
|
133
|
+
}
|
|
134
|
+
destroy(): void {
|
|
135
|
+
if (this.#barrier) return
|
|
136
|
+
this.#bulk('abort')
|
|
137
|
+
this.#reasons.clear()
|
|
138
|
+
this.#groups = new PropertyGroups()
|
|
139
|
+
if (!this.#injected) this.#scheduler = undefined
|
|
140
|
+
}
|
|
141
|
+
getAnimationCount(): number {
|
|
142
|
+
return this.#tasks.size
|
|
143
|
+
}
|
|
144
|
+
#bulk(method: 'finish' | 'abort'): void {
|
|
145
|
+
if (this.#barrier) return
|
|
146
|
+
this.#barrier = true
|
|
147
|
+
try {
|
|
148
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- Callbacks mutate tasks; snapshot membership.
|
|
149
|
+
for (const task of [...this.#tasks.values()]) {
|
|
150
|
+
if (method === 'finish') task.finish(this.#clock().now())
|
|
151
|
+
else task.abort()
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
this.#barrier = false
|
|
155
|
+
this.#schedule()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
#stop(): void {
|
|
159
|
+
if (this.#frameId !== undefined) this.#scheduler?.cancelFrame?.(this.#frameId)
|
|
160
|
+
if (this.#hasTimer) this.#scheduler?.clearTimer(this.#timerId)
|
|
161
|
+
this.#frameId = undefined
|
|
162
|
+
this.#timerId = undefined
|
|
163
|
+
this.#hasTimer = false
|
|
164
|
+
}
|
|
165
|
+
#schedule(): void {
|
|
166
|
+
if (this.#frameRunning || this.#barrier) return
|
|
167
|
+
this.#stop()
|
|
168
|
+
if (!this.#tasks.size) return
|
|
169
|
+
const scheduler = this.#clock()
|
|
170
|
+
if (!scheduler.requestFrame) return
|
|
171
|
+
const now = scheduler.now()
|
|
172
|
+
const deadline = Math.min(...[...this.#tasks.values()].map((task) => task.deadline(now)))
|
|
173
|
+
if (deadline === Infinity) return
|
|
174
|
+
if (deadline > now) {
|
|
175
|
+
this.#hasTimer = true
|
|
176
|
+
this.#timerId = scheduler.setTimer(() => {
|
|
177
|
+
this.#hasTimer = false
|
|
178
|
+
this.#schedule()
|
|
179
|
+
}, deadline - now)
|
|
180
|
+
} else this.#frameId = scheduler.requestFrame(() => this.#frame())
|
|
181
|
+
}
|
|
182
|
+
#frame(): void {
|
|
183
|
+
this.#frameId = undefined
|
|
184
|
+
this.#frameRunning = true
|
|
185
|
+
const time = this.#clock().now()
|
|
186
|
+
try {
|
|
187
|
+
const frames = [...this.#tasks].map(([id, task]) => ({ id, task, next: task.prepare(time) }))
|
|
188
|
+
this.#groups.flush()
|
|
189
|
+
for (const { id, task, next } of frames) {
|
|
190
|
+
if (next && this.#tasks.get(id) === task) task.deliver(next.progress, next.frame)
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
this.#frameRunning = false
|
|
194
|
+
this.#schedule()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Construction is side-effect free; browser capabilities are read lazily on animate. */
|
|
200
|
+
export const globalAnimator = /* @__PURE__ */ new Animator()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
easeLinear,
|
|
3
|
+
easeQuadIn,
|
|
4
|
+
easeQuadOut,
|
|
5
|
+
easeQuadInOut,
|
|
6
|
+
easeCubicIn,
|
|
7
|
+
easeCubicOut,
|
|
8
|
+
easeCubicInOut,
|
|
9
|
+
easePolyIn,
|
|
10
|
+
easePolyOut,
|
|
11
|
+
easePolyInOut,
|
|
12
|
+
easeSinIn,
|
|
13
|
+
easeSinOut,
|
|
14
|
+
easeSinInOut,
|
|
15
|
+
easeExpIn,
|
|
16
|
+
easeExpOut,
|
|
17
|
+
easeExpInOut,
|
|
18
|
+
easeCircleIn,
|
|
19
|
+
easeCircleOut,
|
|
20
|
+
easeCircleInOut,
|
|
21
|
+
easeBackIn,
|
|
22
|
+
easeBackOut,
|
|
23
|
+
easeBackInOut,
|
|
24
|
+
easeElasticIn,
|
|
25
|
+
easeElasticOut,
|
|
26
|
+
easeElasticInOut,
|
|
27
|
+
easeBounceIn,
|
|
28
|
+
easeBounceOut,
|
|
29
|
+
easeBounceInOut,
|
|
30
|
+
} from 'd3'
|
|
31
|
+
import type { EasingFunction, EasingType } from './types'
|
|
32
|
+
|
|
33
|
+
const easings: Record<EasingType, EasingFunction> = {
|
|
34
|
+
linear: easeLinear,
|
|
35
|
+
quadIn: easeQuadIn,
|
|
36
|
+
quadOut: easeQuadOut,
|
|
37
|
+
quadInOut: easeQuadInOut,
|
|
38
|
+
cubicIn: easeCubicIn,
|
|
39
|
+
cubicOut: easeCubicOut,
|
|
40
|
+
cubicInOut: easeCubicInOut,
|
|
41
|
+
quartIn: easePolyIn.exponent(4),
|
|
42
|
+
quartOut: easePolyOut.exponent(4),
|
|
43
|
+
quartInOut: easePolyInOut.exponent(4),
|
|
44
|
+
quintIn: easePolyIn.exponent(5),
|
|
45
|
+
quintOut: easePolyOut.exponent(5),
|
|
46
|
+
quintInOut: easePolyInOut.exponent(5),
|
|
47
|
+
sineIn: easeSinIn,
|
|
48
|
+
sineOut: easeSinOut,
|
|
49
|
+
sineInOut: easeSinInOut,
|
|
50
|
+
expoIn: easeExpIn,
|
|
51
|
+
expoOut: easeExpOut,
|
|
52
|
+
expoInOut: easeExpInOut,
|
|
53
|
+
circIn: easeCircleIn,
|
|
54
|
+
circOut: easeCircleOut,
|
|
55
|
+
circInOut: easeCircleInOut,
|
|
56
|
+
backIn: easeBackIn,
|
|
57
|
+
backOut: easeBackOut,
|
|
58
|
+
backInOut: easeBackInOut,
|
|
59
|
+
elasticIn: easeElasticIn,
|
|
60
|
+
elasticOut: easeElasticOut,
|
|
61
|
+
elasticInOut: easeElasticInOut,
|
|
62
|
+
bounceIn: easeBounceIn,
|
|
63
|
+
bounceOut: easeBounceOut,
|
|
64
|
+
bounceInOut: easeBounceInOut,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Preserve custom functions and overshoot; execution errors belong to the task runtime. */
|
|
68
|
+
export function getEasingFunction(easing: EasingType | EasingFunction): EasingFunction {
|
|
69
|
+
if (typeof easing === 'function') return easing
|
|
70
|
+
if (typeof easing !== 'string' || !Object.hasOwn(easings, easing)) {
|
|
71
|
+
throw new TypeError(`Unknown animation easing: ${String(easing)}`)
|
|
72
|
+
}
|
|
73
|
+
return easings[easing]
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnimationConfig } from './types'
|
|
2
|
+
|
|
3
|
+
export function animationError(code: 'animation-terminating' | 'animation-disposed'): Error {
|
|
4
|
+
return Object.assign(new Error(code), { code })
|
|
5
|
+
}
|
|
6
|
+
export function reportAnimationError(config: AnimationConfig, error: unknown): void {
|
|
7
|
+
try {
|
|
8
|
+
if (config.onError) config.onError(error)
|
|
9
|
+
else console.error(error)
|
|
10
|
+
} catch (reporterError) {
|
|
11
|
+
try {
|
|
12
|
+
console.error(reporterError)
|
|
13
|
+
} catch {
|
|
14
|
+
/* Reporting must never interrupt peers. */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function notify(config: AnimationConfig, callback?: () => void): void {
|
|
19
|
+
try {
|
|
20
|
+
callback?.()
|
|
21
|
+
} catch (error) {
|
|
22
|
+
reportAnimationError(config, error)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AnimationHandle, AnimationState } from './types'
|
|
2
|
+
|
|
3
|
+
export type AnimationCommand = 'pause' | 'resume' | 'abort' | 'finish'
|
|
4
|
+
|
|
5
|
+
/** Terminal handles keep only identity/state, never their task, clock or owner. */
|
|
6
|
+
export function createAnimationHandle(
|
|
7
|
+
id: string,
|
|
8
|
+
read: () => AnimationState,
|
|
9
|
+
command: (method: AnimationCommand) => void,
|
|
10
|
+
): { handle: AnimationHandle; release: () => void } {
|
|
11
|
+
let access: { read: () => AnimationState; command: typeof command } | undefined = {
|
|
12
|
+
read,
|
|
13
|
+
command,
|
|
14
|
+
}
|
|
15
|
+
let terminalState: AnimationState = 'PENDING'
|
|
16
|
+
const invoke = (method: AnimationCommand) => access?.command(method)
|
|
17
|
+
return {
|
|
18
|
+
handle: {
|
|
19
|
+
id,
|
|
20
|
+
get state() {
|
|
21
|
+
return access?.read() ?? terminalState
|
|
22
|
+
},
|
|
23
|
+
pause: () => invoke('pause'),
|
|
24
|
+
resume: () => invoke('resume'),
|
|
25
|
+
abort: () => invoke('abort'),
|
|
26
|
+
finish: () => invoke('finish'),
|
|
27
|
+
},
|
|
28
|
+
release: () => {
|
|
29
|
+
if (!access) return
|
|
30
|
+
terminalState = access.read()
|
|
31
|
+
access = undefined
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|