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.
Files changed (97) hide show
  1. package/README.md +246 -39
  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/types.d.ts +8 -0
  37. package/dist/types/components/core/useWaveformChartController.d.ts +25 -0
  38. package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
  39. package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
  40. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
  41. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
  42. package/dist/types/components/rendering/updateAxis.d.ts +5 -0
  43. package/dist/types/components/rendering/updateGrid.d.ts +3 -0
  44. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
  45. package/dist/types/components/utils/assignElement.d.ts +2 -0
  46. package/dist/types/index.d.ts +9 -0
  47. package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
  48. package/package.json +5 -2
  49. package/src/DemoRouterApp.vue +1 -0
  50. package/src/animation/WaveformAnimationController.ts +328 -0
  51. package/src/animation/core/Animation.ts +37 -0
  52. package/src/animation/core/Animator.ts +200 -0
  53. package/src/animation/core/easing.ts +74 -0
  54. package/src/animation/core/errors.ts +24 -0
  55. package/src/animation/core/handle.ts +34 -0
  56. package/src/animation/core/owner.ts +9 -0
  57. package/src/animation/core/propertyGroups.ts +143 -0
  58. package/src/animation/core/task.ts +153 -0
  59. package/src/animation/core/types.ts +81 -0
  60. package/src/animation/core/validation.ts +173 -0
  61. package/src/animation/core/values.ts +38 -0
  62. package/src/animation/presets/annotation.ts +66 -0
  63. package/src/animation/presets/lineDrawing.ts +51 -0
  64. package/src/animation/presets/shared.ts +58 -0
  65. package/src/animation/presets/transitions.ts +77 -0
  66. package/src/animation/types.ts +81 -0
  67. package/src/components/WaveformChart.vue +1 -0
  68. package/src/components/WaveformChartView.vue +13 -4
  69. package/src/components/animation/annotationPresentation.ts +145 -0
  70. package/src/components/animation/chartExportVersion.ts +5 -0
  71. package/src/components/animation/config.ts +87 -0
  72. package/src/components/animation/context.ts +56 -0
  73. package/src/components/animation/geometry.ts +148 -0
  74. package/src/components/animation/paginationPresentation.ts +114 -0
  75. package/src/components/animation/pathBuffer.ts +61 -0
  76. package/src/components/animation/trackPresentation.ts +172 -0
  77. package/src/components/animation/useWaveformAnimations.ts +420 -0
  78. package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
  79. package/src/components/annotation/types.ts +3 -0
  80. package/src/components/controls/useChartImageExport.ts +4 -0
  81. package/src/components/core/trackLayoutBuilder.ts +9 -18
  82. package/src/components/core/types.ts +4 -0
  83. package/src/components/core/useWaveformChartController.ts +24 -20
  84. package/src/components/core/useWaveformRenderSampling.ts +23 -1
  85. package/src/components/core/waveformChartTypes.ts +2 -1
  86. package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
  87. package/src/components/rendering/WaveformTrack.vue +6 -1
  88. package/src/components/rendering/WaveformTrackAxes.vue +64 -51
  89. package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
  90. package/src/components/rendering/updateAxis.ts +57 -0
  91. package/src/components/rendering/updateGrid.ts +42 -0
  92. package/src/components/rendering/yAxisTickVisibility.ts +45 -18
  93. package/src/components/utils/assignElement.ts +7 -0
  94. package/src/index.ts +52 -0
  95. package/src/router.ts +1 -0
  96. package/src/views/AnimationDemo.vue +129 -0
  97. package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
@@ -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
+ }