waveform-analysis 0.1.73 → 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 (112) hide show
  1. package/README.md +256 -46
  2. package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
  3. package/dist/index.cjs +1 -2
  4. package/dist/index.js +1 -7115
  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/layout.d.ts +2 -1
  37. package/dist/types/components/core/types.d.ts +8 -0
  38. package/dist/types/components/core/useWaveformChartController.d.ts +25 -0
  39. package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
  40. package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
  41. package/dist/types/components/core/yAxisTitleLayout.d.ts +1 -1
  42. package/dist/types/components/interaction/useExtremaTickViewport.d.ts +1 -1
  43. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
  44. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
  45. package/dist/types/components/rendering/updateAxis.d.ts +5 -0
  46. package/dist/types/components/rendering/updateGrid.d.ts +3 -0
  47. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
  48. package/dist/types/components/utils/assignElement.d.ts +2 -0
  49. package/dist/types/demo/types.d.ts +2 -1
  50. package/dist/types/index.d.ts +9 -0
  51. package/dist/types/types/chart.d.ts +4 -2
  52. package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
  53. package/package.json +5 -2
  54. package/src/App.vue +6 -3
  55. package/src/DemoRouterApp.vue +1 -0
  56. package/src/animation/WaveformAnimationController.ts +328 -0
  57. package/src/animation/core/Animation.ts +37 -0
  58. package/src/animation/core/Animator.ts +200 -0
  59. package/src/animation/core/easing.ts +74 -0
  60. package/src/animation/core/errors.ts +24 -0
  61. package/src/animation/core/handle.ts +34 -0
  62. package/src/animation/core/owner.ts +9 -0
  63. package/src/animation/core/propertyGroups.ts +143 -0
  64. package/src/animation/core/task.ts +153 -0
  65. package/src/animation/core/types.ts +81 -0
  66. package/src/animation/core/validation.ts +173 -0
  67. package/src/animation/core/values.ts +38 -0
  68. package/src/animation/presets/annotation.ts +66 -0
  69. package/src/animation/presets/lineDrawing.ts +51 -0
  70. package/src/animation/presets/shared.ts +58 -0
  71. package/src/animation/presets/transitions.ts +77 -0
  72. package/src/animation/types.ts +81 -0
  73. package/src/components/WaveformChart.vue +1 -0
  74. package/src/components/WaveformChartView.vue +13 -4
  75. package/src/components/animation/annotationPresentation.ts +145 -0
  76. package/src/components/animation/chartExportVersion.ts +5 -0
  77. package/src/components/animation/config.ts +87 -0
  78. package/src/components/animation/context.ts +56 -0
  79. package/src/components/animation/geometry.ts +148 -0
  80. package/src/components/animation/paginationPresentation.ts +114 -0
  81. package/src/components/animation/pathBuffer.ts +61 -0
  82. package/src/components/animation/trackPresentation.ts +172 -0
  83. package/src/components/animation/useWaveformAnimations.ts +420 -0
  84. package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
  85. package/src/components/annotation/types.ts +3 -0
  86. package/src/components/controls/useChartImageExport.ts +4 -0
  87. package/src/components/core/layout.ts +36 -22
  88. package/src/components/core/trackLayoutBuilder.ts +17 -23
  89. package/src/components/core/types.ts +4 -0
  90. package/src/components/core/useWaveformChartController.ts +24 -20
  91. package/src/components/core/useWaveformLayout.ts +3 -1
  92. package/src/components/core/useWaveformRenderSampling.ts +23 -1
  93. package/src/components/core/waveformChartTypes.ts +2 -1
  94. package/src/components/core/yAxisLayoutMetrics.ts +24 -16
  95. package/src/components/core/yAxisPadding.ts +10 -5
  96. package/src/components/core/yAxisTitleLayout.ts +9 -48
  97. package/src/components/interaction/useExtremaTickViewport.ts +1 -1
  98. package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
  99. package/src/components/rendering/WaveformTrack.vue +6 -1
  100. package/src/components/rendering/WaveformTrackAxes.vue +64 -51
  101. package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
  102. package/src/components/rendering/updateAxis.ts +57 -0
  103. package/src/components/rendering/updateGrid.ts +42 -0
  104. package/src/components/rendering/yAxisTickVisibility.ts +45 -18
  105. package/src/components/utils/assignElement.ts +7 -0
  106. package/src/demo/DemoYAxisPaddingControls.vue +11 -3
  107. package/src/demo/types.ts +2 -1
  108. package/src/index.ts +53 -1
  109. package/src/router.ts +1 -0
  110. package/src/types/chart.ts +4 -2
  111. package/src/views/AnimationDemo.vue +129 -0
  112. package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
@@ -0,0 +1,51 @@
1
+ import type { AnimationConfig } from '../core/types'
2
+ import type {
3
+ AnimationBinding,
4
+ ClipAnimationState,
5
+ LineDrawingOptions,
6
+ StaggeredLineDrawingOptions,
7
+ } from '../types'
8
+ import { clampProgress, presetConfig, validateBinding } from './shared'
9
+
10
+ export function createLineDrawingAnimation(
11
+ binding: AnimationBinding<ClipAnimationState>,
12
+ width: number,
13
+ options: LineDrawingOptions = {},
14
+ ): AnimationConfig {
15
+ validateBinding(binding)
16
+ if (!Number.isFinite(width) || width < 0)
17
+ throw new RangeError('Width must be finite and nonnegative')
18
+ const direction = options.direction ?? 'left-to-right'
19
+ if (!['left-to-right', 'right-to-left', 'center-out'].includes(direction))
20
+ throw new TypeError('Invalid drawing direction')
21
+ const state = (p: number): ClipAnimationState => ({
22
+ x:
23
+ direction === 'right-to-left'
24
+ ? width * (1 - p)
25
+ : direction === 'center-out'
26
+ ? (width * (1 - p)) / 2
27
+ : 0,
28
+ width: width * p,
29
+ })
30
+ return presetConfig(
31
+ { ...options, duration: options.duration ?? 1000 },
32
+ {
33
+ delay: options.delay ?? 0,
34
+ initialize: () => binding.write(state(0)),
35
+ during: (p, frame) => binding.write(state(frame.terminal ? 1 : clampProgress(p))),
36
+ },
37
+ )
38
+ }
39
+ export function createStaggeredLineDrawingAnimations(
40
+ series: readonly { binding: AnimationBinding<ClipAnimationState>; width: number }[],
41
+ options: StaggeredLineDrawingOptions = {},
42
+ ): AnimationConfig[] {
43
+ const stagger = options.staggerDelay ?? 100
44
+ if (!Number.isFinite(stagger) || stagger < 0) throw new RangeError('Invalid stagger delay')
45
+ return series.map((item, index) =>
46
+ createLineDrawingAnimation(item.binding, item.width, {
47
+ ...options,
48
+ delay: (options.delay ?? 0) + index * stagger,
49
+ }),
50
+ )
51
+ }
@@ -0,0 +1,58 @@
1
+ import type { AnimationConfig, AnimationValue } from '../core/types'
2
+ import { copyAnimationValue, validateAnimationConfig } from '../core/validation'
3
+ import type {
4
+ AnimationBinding,
5
+ AnnotationAnimationType,
6
+ DataUpdateOptions,
7
+ NumericGeometry,
8
+ } from '../types'
9
+
10
+ export const annotationEasing = {
11
+ 'fade-in': 'cubicOut',
12
+ scale: 'backOut',
13
+ 'slide-in': 'cubicOut',
14
+ bounce: 'bounceOut',
15
+ } as const
16
+ export function isAnnotationEffect(value: unknown): value is AnnotationAnimationType {
17
+ return typeof value === 'string' && Object.hasOwn(annotationEasing, value)
18
+ }
19
+
20
+ export const clampProgress = (p: number) => Math.max(0, Math.min(1, p))
21
+ export function validateBinding<T>(binding: AnimationBinding<T>): void {
22
+ if (!binding || typeof binding.read !== 'function' || typeof binding.write !== 'function') {
23
+ throw new TypeError('Animation binding requires read and write functions')
24
+ }
25
+ }
26
+ export function presetConfig(options: DataUpdateOptions, config: AnimationConfig): AnimationConfig {
27
+ const result = {
28
+ duration: options.duration ?? 300,
29
+ easing: options.easing ?? 'cubicInOut',
30
+ done: options.onComplete,
31
+ aborted: options.onAbort,
32
+ force: true,
33
+ ...config,
34
+ } satisfies AnimationConfig
35
+ validateAnimationConfig(result)
36
+ return result
37
+ }
38
+ function freezeValue(value: AnimationValue): AnimationValue {
39
+ if (Array.isArray(value)) {
40
+ value.forEach((item) => {
41
+ if (Array.isArray(item)) Object.freeze(item)
42
+ })
43
+ Object.freeze(value)
44
+ }
45
+ return value
46
+ }
47
+ export function copyGeometry(input: NumericGeometry): NumericGeometry {
48
+ if (!input || typeof input !== 'object' || Array.isArray(input))
49
+ throw new TypeError('Invalid geometry')
50
+ return Object.freeze(
51
+ Object.fromEntries(
52
+ Object.entries(input).map(([key, value]) => [key, freezeValue(copyAnimationValue(value))]),
53
+ ),
54
+ )
55
+ }
56
+ export function validDomain(domain: readonly number[]): boolean {
57
+ return domain.length === 2 && domain.every(Number.isFinite) && domain[0] < domain[1]
58
+ }
@@ -0,0 +1,77 @@
1
+ import type { AnimationConfig } from '../core/types'
2
+ import { sameShape } from '../core/validation'
3
+ import { interpolateValue, equalValues } from '../core/values'
4
+ import type {
5
+ AnimationBinding,
6
+ DataUpdateOptions,
7
+ GeometryTransitionInput,
8
+ GeometryTransitionFrame,
9
+ } from '../types'
10
+ import { clampProgress, copyGeometry, presetConfig, validDomain, validateBinding } from './shared'
11
+
12
+ export function createPathTransitionAnimation(
13
+ input: GeometryTransitionInput,
14
+ write: (frame: GeometryTransitionFrame) => void,
15
+ options: DataUpdateOptions = {},
16
+ ): AnimationConfig {
17
+ const compatible = input.compatible
18
+ if (typeof write !== 'function' || typeof compatible !== 'boolean')
19
+ throw new TypeError('Invalid geometry transition')
20
+ const from = copyGeometry(input.from),
21
+ to = copyGeometry(input.to)
22
+ const keys = Object.keys(to)
23
+ if (compatible) {
24
+ if (keys.length !== Object.keys(from).length || keys.some((key) => !Object.hasOwn(from, key))) {
25
+ throw new TypeError('Compatible geometry requires identical channels')
26
+ }
27
+ if (keys.some((key) => !sameShape(from[key], to[key])))
28
+ throw new TypeError('Track values must have identical shapes')
29
+ }
30
+ const unchanged = compatible && keys.every((key) => equalValues(from[key], to[key]))
31
+ return presetConfig(options, {
32
+ duration: unchanged ? 0 : (options.duration ?? 300),
33
+ during: (p, frame) => {
34
+ if (frame.terminal) write({ kind: 'settled', values: to })
35
+ else if (compatible)
36
+ write({
37
+ kind: 'interpolate',
38
+ progress: p,
39
+ values: Object.fromEntries(
40
+ keys.map((key) => [key, interpolateValue(from[key], to[key], p)]),
41
+ ),
42
+ })
43
+ else {
44
+ const q = clampProgress(p)
45
+ write({ kind: 'crossfade', from, to, fromOpacity: 1 - q, toOpacity: q })
46
+ }
47
+ },
48
+ })
49
+ }
50
+ export function createYAxisScaleAnimation(
51
+ binding: AnimationBinding<[number, number]>,
52
+ to: [number, number],
53
+ options: DataUpdateOptions = {},
54
+ ): AnimationConfig {
55
+ validateBinding(binding)
56
+ const target: [number, number] = [...to]
57
+ if (!validDomain(target))
58
+ throw new RangeError('Target domain must be finite and strictly increasing')
59
+ let from: [number, number] | undefined
60
+ return presetConfig(options, {
61
+ initialize: () => {
62
+ from = [...binding.read()]
63
+ if (!validDomain(from))
64
+ throw new RangeError('Initial domain must be finite and strictly increasing')
65
+ },
66
+ during: (p, frame) => {
67
+ if (frame.terminal) {
68
+ binding.write([...target])
69
+ return
70
+ }
71
+ const q = clampProgress(p)
72
+ const start = from ?? target
73
+ const value = interpolateValue(start, target, q) as [number, number]
74
+ binding.write(validDomain(value) ? value : [...target])
75
+ },
76
+ })
77
+ }
@@ -0,0 +1,81 @@
1
+ import type { AnimationValue, Animator, EasingFunction, EasingType } from './core/types'
2
+
3
+ export type LineDrawingDirection = 'left-to-right' | 'right-to-left' | 'center-out'
4
+ export type AnnotationAnimationType = 'fade-in' | 'scale' | 'slide-in' | 'bounce'
5
+ export interface LineDrawingOptions {
6
+ duration?: number
7
+ delay?: number
8
+ easing?: EasingType | EasingFunction
9
+ direction?: LineDrawingDirection
10
+ onComplete?: () => void
11
+ onAbort?: () => void
12
+ }
13
+ export interface StaggeredLineDrawingOptions extends LineDrawingOptions {
14
+ staggerDelay?: number
15
+ }
16
+ export interface DataUpdateOptions {
17
+ duration?: number
18
+ easing?: EasingType | EasingFunction
19
+ onComplete?: () => void
20
+ onAbort?: () => void
21
+ }
22
+ export interface AnnotationAnimationOptions extends DataUpdateOptions {
23
+ type?: AnnotationAnimationType
24
+ }
25
+ export interface WaveformEnterAnimationOptions extends StaggeredLineDrawingOptions {
26
+ enabled?: boolean
27
+ }
28
+ export interface WaveformUpdateAnimationOptions extends DataUpdateOptions {
29
+ enabled?: boolean
30
+ }
31
+ export interface WaveformAnnotationAnimationOptions extends AnnotationAnimationOptions {
32
+ enabled?: boolean
33
+ }
34
+ /** Default-on presentation animation; immutable business data and queries remain authoritative. */
35
+ export interface WaveformAnimationProps {
36
+ animationEnabled?: boolean
37
+ animation?: WaveformEnterAnimationOptions
38
+ animationUpdate?: WaveformUpdateAnimationOptions
39
+ animationAnnotation?: WaveformAnnotationAnimationOptions
40
+ animationThreshold?: number
41
+ }
42
+ export interface ClipAnimationState {
43
+ x: number
44
+ width: number
45
+ }
46
+ export interface AnimationBinding<T> {
47
+ read(): T
48
+ write(state: T): void
49
+ }
50
+ export type NumericGeometry = Record<string, AnimationValue>
51
+ export interface GeometryTransitionInput {
52
+ from: NumericGeometry
53
+ to: NumericGeometry
54
+ compatible: boolean
55
+ }
56
+ export type GeometryTransitionFrame =
57
+ | { kind: 'settled'; values: NumericGeometry }
58
+ | { kind: 'interpolate'; values: NumericGeometry; progress: number }
59
+ | {
60
+ kind: 'crossfade'
61
+ from: NumericGeometry
62
+ to: NumericGeometry
63
+ fromOpacity: number
64
+ toOpacity: number
65
+ }
66
+ export interface AnnotationAnimationState {
67
+ opacity: number
68
+ scale: number
69
+ offsetY: number
70
+ }
71
+ export type AnimationScope = 'enter' | 'update' | 'leave' | 'domain'
72
+ export interface AnimationObjectKey {
73
+ kind: 'series' | 'annotation' | 'axis' | 'custom'
74
+ id: string
75
+ scope: AnimationScope
76
+ }
77
+ export interface WaveformAnimationControllerOptions {
78
+ enabled?: boolean
79
+ animator?: Animator
80
+ commitFinalState: () => void
81
+ }
@@ -8,6 +8,7 @@ import type {
8
8
  import WaveformChartView from './WaveformChartView.vue'
9
9
 
10
10
  const props = withDefaults(defineProps<WaveformChartProps>(), {
11
+ animationEnabled: true,
11
12
  displayMode: 'independent',
12
13
  overlayMode: 'single-axis',
13
14
  unitDisplayMode: 'axis',
@@ -48,6 +48,10 @@ const {
48
48
  hasWaveformData,
49
49
  requestViewportReset,
50
50
  trackLayouts,
51
+ presentationTracks,
52
+ presentationAnnotations,
53
+ paginationOpacity,
54
+ stabilizeAnimations,
51
55
  clipPathId,
52
56
  chartTopMargin,
53
57
  gridCells,
@@ -73,7 +77,6 @@ const {
73
77
  hoverState,
74
78
  showTooltip,
75
79
  selectionBox,
76
- renderedAnnotations,
77
80
  annotationsVisible,
78
81
  handleExistingAnnotationContextMenu,
79
82
  beginAnnotationDrag,
@@ -142,9 +145,14 @@ function handleChartPointerLeave() {
142
145
  :data-plot-margin-top="resolvedPlotMargin.top"
143
146
  :data-plot-margin-bottom="resolvedPlotMargin.bottom"
144
147
  :data-title-area-height="titleAreaHeight"
148
+ @pointermove.capture="stabilizeAnimations"
149
+ @pointerdown.capture="stabilizeAnimations"
150
+ @wheel.capture="stabilizeAnimations"
151
+ @dblclick.capture="stabilizeAnimations"
152
+ @contextmenu.capture="stabilizeAnimations"
145
153
  @pointerenter="pointerInsideChart = true"
146
154
  @pointerleave="handleChartPointerLeave"
147
- @contextmenu.capture="handleNativeContextMenu"
155
+ @contextmenu="handleNativeContextMenu"
148
156
  >
149
157
  <div
150
158
  v-if="titleAreaReserved"
@@ -235,7 +243,8 @@ function handleChartPointerLeave() {
235
243
  <WaveformTrack
236
244
  v-for="track in trackLayouts"
237
245
  :key="`${track.index}-${track.id}`"
238
- :track="track"
246
+ :track="presentationTracks.find((item) => item.id === track.id) ?? track"
247
+ :pagination-opacity="paginationOpacity"
239
248
  :frame-path="
240
249
  displayMode === 'compact'
241
250
  ? compactFramePath(track, trackLayouts, compactFrameInset)
@@ -291,7 +300,7 @@ function handleChartPointerLeave() {
291
300
  :cancel-key="activeInteractionMode"
292
301
  :data-version="controller.data"
293
302
  v-if="!isCleanView"
294
- :annotations="renderedAnnotations"
303
+ :annotations="presentationAnnotations"
295
304
  :visible="annotationsVisible"
296
305
  :interactive="!isPresentationMode && activeInteractionMode !== 'none'"
297
306
  @contextmenu="handleExistingAnnotationContextMenu"
@@ -0,0 +1,145 @@
1
+ import type { RenderedAnnotation } from '../annotation/types'
2
+ import type { ResolvedAnimationProps } from './config'
3
+ import type { WaveformAnnotation } from '@/types'
4
+ import type { AnnotationAnimationState } from '@/animation/types'
5
+ import { WaveformAnimationController } from '@/animation/WaveformAnimationController'
6
+
7
+ export interface PresentedAnnotation extends RenderedAnnotation {
8
+ animationStyle?: AnnotationAnimationState
9
+ leaving?: boolean
10
+ seriesOpacity?: number
11
+ }
12
+ interface Entry {
13
+ rendered: RenderedAnnotation
14
+ style: AnnotationAnimationState
15
+ leaving: boolean
16
+ waiting: boolean
17
+ }
18
+ export function createAnnotationPresentation(
19
+ controller: WaveformAnimationController,
20
+ invalidate: () => void,
21
+ ) {
22
+ const entries = new Map<string, Entry>()
23
+ const finalStyle = () => ({ opacity: 1, scale: 1, offsetY: 0 })
24
+ let options: ResolvedAnimationProps['animationAnnotation']
25
+ const binding = (id: string, entry: Entry) => ({
26
+ read: () => entry.style,
27
+ write: (style: AnnotationAnimationState) => {
28
+ entry.style = style
29
+ if (entry.leaving && style.opacity === 0 && entries.get(id) === entry) entries.delete(id)
30
+ invalidate()
31
+ },
32
+ })
33
+ function appear(id: string, entry: Entry) {
34
+ entry.waiting = false
35
+ controller.animateAnnotationAppear(id, binding(id, entry), options)
36
+ }
37
+ return {
38
+ reconcile(
39
+ rendered: RenderedAnnotation[],
40
+ controlled: WaveformAnnotation[],
41
+ config: ResolvedAnimationProps['animationAnnotation'],
42
+ enabled: boolean,
43
+ waitingSeries: Set<string>,
44
+ ) {
45
+ options = config
46
+ const visible = new Set(rendered.map((item) => item.annotation.id))
47
+ const existing = new Set(controlled.map((item) => item.id))
48
+ for (const [id, entry] of entries)
49
+ if (!visible.has(id) && (!entry.leaving || !enabled)) {
50
+ if (!existing.has(id) && enabled) {
51
+ entry.rendered = Object.freeze({
52
+ ...entry.rendered,
53
+ annotation: Object.freeze({
54
+ ...entry.rendered.annotation,
55
+ style: entry.rendered.annotation.style
56
+ ? Object.freeze({ ...entry.rendered.annotation.style })
57
+ : undefined,
58
+ }),
59
+ lines: Object.freeze([...entry.rendered.lines]) as unknown as string[],
60
+ box: Object.freeze({ ...entry.rendered.box }),
61
+ style: Object.freeze({ ...entry.rendered.style }),
62
+ })
63
+ entry.leaving = true
64
+ entry.waiting = false
65
+ controller.animateAnnotationDisappear(id, binding(id, entry), options)
66
+ } else {
67
+ entries.delete(id)
68
+ controller.cancel({ kind: 'annotation', id, scope: 'enter' })
69
+ controller.cancel({ kind: 'annotation', id, scope: 'leave' })
70
+ }
71
+ }
72
+ for (const item of rendered) {
73
+ const id = item.annotation.id
74
+ let entry = entries.get(id)
75
+ if (entry && !entry.leaving) {
76
+ entry.rendered = item
77
+ continue
78
+ }
79
+ if (entry) {
80
+ entry.leaving = false
81
+ controller.cancel({ kind: 'annotation', id, scope: 'leave' })
82
+ entry.rendered = item
83
+ } else {
84
+ entry = {
85
+ rendered: item,
86
+ leaving: false,
87
+ waiting: false,
88
+ style: enabled
89
+ ? {
90
+ opacity: 0,
91
+ scale: options.type === 'scale' || options.type === 'bounce' ? 0 : 1,
92
+ offsetY: options.type === 'slide-in' ? 8 : 0,
93
+ }
94
+ : finalStyle(),
95
+ }
96
+ entries.set(id, entry)
97
+ }
98
+ if (enabled) {
99
+ entry.waiting = waitingSeries.has(item.annotation.seriesId)
100
+ if (!entry.waiting) appear(id, entry)
101
+ } else entry.style = finalStyle()
102
+ }
103
+ invalidate()
104
+ },
105
+ release(seriesId?: string) {
106
+ for (const [id, entry] of entries)
107
+ if (entry.waiting && (!seriesId || entry.rendered.annotation.seriesId === seriesId))
108
+ appear(id, entry)
109
+ invalidate()
110
+ },
111
+ finish(rendered: RenderedAnnotation[]) {
112
+ entries.clear()
113
+ for (const item of rendered)
114
+ entries.set(item.annotation.id, {
115
+ rendered: item,
116
+ style: finalStyle(),
117
+ leaving: false,
118
+ waiting: false,
119
+ })
120
+ invalidate()
121
+ },
122
+ project(
123
+ rendered: RenderedAnnotation[],
124
+ seriesOpacity: Map<string, number>,
125
+ ): PresentedAnnotation[] {
126
+ const current = new Map(rendered.map((item) => [item.annotation.id, item]))
127
+ return [...entries.values()]
128
+ .filter((entry) => entry.leaving || current.has(entry.rendered.annotation.id))
129
+ .map((entry) => {
130
+ if (!entry.leaving)
131
+ entry.rendered = current.get(entry.rendered.annotation.id) ?? entry.rendered
132
+ return {
133
+ ...entry.rendered,
134
+ animationStyle: entry.style,
135
+ leaving: entry.leaving,
136
+ seriesOpacity: seriesOpacity.get(entry.rendered.annotation.seriesId) ?? 1,
137
+ }
138
+ })
139
+ },
140
+ clear() {
141
+ entries.clear()
142
+ invalidate()
143
+ },
144
+ }
145
+ }
@@ -0,0 +1,5 @@
1
+ import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
2
+ import { chartExportOptions } from '../controls/chartExportOptions'
3
+ export function chartExportVersion(props: ResolvedWaveformChartProps, state: object): unknown[] {
4
+ return [props.data, JSON.stringify({ ...chartExportOptions(props), ...state })]
5
+ }
@@ -0,0 +1,87 @@
1
+ import { getEasingFunction } from '@/animation/core/easing'
2
+ import { annotationEasing, isAnnotationEffect } from '@/animation/presets/shared'
3
+ import type {
4
+ WaveformAnimationProps,
5
+ AnnotationAnimationType,
6
+ LineDrawingDirection,
7
+ } from '@/animation/types'
8
+
9
+ export function resolveAnimationProps(
10
+ props: WaveformAnimationProps,
11
+ warn = (message: string) => {
12
+ if (import.meta.env.DEV) console.warn(message)
13
+ },
14
+ ) {
15
+ function field<T>(value: T | undefined, fallback: T, valid: (v: T) => boolean, key: string): T {
16
+ if (value === undefined) return fallback
17
+ if (valid(value)) return value
18
+ if (import.meta.env.DEV) warn(`Invalid animation configuration: ${key}`)
19
+ return fallback
20
+ }
21
+ const nonnegative = (v: number) => typeof v === 'number' && Number.isFinite(v) && v >= 0
22
+ const boolean = (v: boolean) => typeof v === 'boolean'
23
+ const easing = (v: unknown) => {
24
+ try {
25
+ getEasingFunction(v as never)
26
+ return true
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+ const callback = (v: unknown) => typeof v === 'function'
32
+ function options<
33
+ T extends {
34
+ enabled?: boolean
35
+ duration?: number
36
+ easing?: unknown
37
+ onComplete?: () => void
38
+ onAbort?: () => void
39
+ },
40
+ >(input: T | undefined, duration: number, defaultEasing: string) {
41
+ return {
42
+ enabled: field(input?.enabled, true, boolean, 'enabled'),
43
+ duration: field(input?.duration, duration, nonnegative, 'duration'),
44
+ easing: field(input?.easing, defaultEasing, easing, 'easing') as NonNullable<T['easing']>,
45
+ onComplete:
46
+ input?.onComplete === undefined
47
+ ? undefined
48
+ : field(input.onComplete, () => {}, callback, 'onComplete'),
49
+ onAbort:
50
+ input?.onAbort === undefined
51
+ ? undefined
52
+ : field(input.onAbort, () => {}, callback, 'onAbort'),
53
+ }
54
+ }
55
+ const annotationType = field<AnnotationAnimationType>(
56
+ props.animationAnnotation?.type,
57
+ 'scale',
58
+ isAnnotationEffect,
59
+ 'annotation.type',
60
+ )
61
+ return {
62
+ animationEnabled: field(props.animationEnabled, true, boolean, 'animationEnabled'),
63
+ animationThreshold: field(
64
+ props.animationThreshold,
65
+ 50000,
66
+ (v) => nonnegative(v) && Number.isInteger(v),
67
+ 'animationThreshold',
68
+ ),
69
+ animation: {
70
+ ...options(props.animation, 1000, 'cubicInOut'),
71
+ delay: field(props.animation?.delay, 0, nonnegative, 'delay'),
72
+ staggerDelay: field(props.animation?.staggerDelay, 100, nonnegative, 'staggerDelay'),
73
+ direction: field<LineDrawingDirection>(
74
+ props.animation?.direction,
75
+ 'left-to-right',
76
+ (v) => ['left-to-right', 'right-to-left', 'center-out'].includes(v),
77
+ 'direction',
78
+ ),
79
+ },
80
+ animationUpdate: options(props.animationUpdate, 300, 'cubicInOut'),
81
+ animationAnnotation: {
82
+ ...options(props.animationAnnotation, 300, annotationEasing[annotationType]),
83
+ type: annotationType,
84
+ },
85
+ }
86
+ }
87
+ export type ResolvedAnimationProps = ReturnType<typeof resolveAnimationProps>
@@ -0,0 +1,56 @@
1
+ import type { ComputedRef, Ref, ShallowRef } from 'vue'
2
+ import type { TrackLayout } from '../core/types'
3
+ import type { PreparedWaveformSeries } from '../core/useWaveformData'
4
+ import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
5
+
6
+ export interface AnimationContext {
7
+ props: ResolvedWaveformChartProps
8
+ tracks: ComputedRef<TrackLayout[]>
9
+ currentPage: Ref<number>
10
+ containerSize: ComputedRef<number[]>
11
+ preparedSeries: ShallowRef<PreparedWaveformSeries[]>
12
+ layoutReady: ComputedRef<boolean>
13
+ innerWidth: ComputedRef<number>
14
+ innerHeight: ComputedRef<number>
15
+ sampling: {
16
+ state: ComputedRef<'pending' | 'success' | 'fallback' | 'error'>
17
+ requestSignature: ComputedRef<string>
18
+ }
19
+ }
20
+
21
+ export function animationLayoutSignature(context: AnimationContext) {
22
+ const { props, tracks } = context
23
+ return JSON.stringify({
24
+ tracks: tracks.value.map((track) => [
25
+ track.id,
26
+ track.left,
27
+ track.top,
28
+ track.width,
29
+ track.height,
30
+ track.seriesList.map((series) => series.id),
31
+ track.yAxes.map((axis) => axis.seriesList.map((series) => series.id)),
32
+ ]),
33
+ width: context.innerWidth.value,
34
+ height: context.innerHeight.value,
35
+ mode: props.displayMode,
36
+ overlay: props.overlayMode,
37
+ hidden: props.hiddenSeriesIds,
38
+ clean: props.cleanView,
39
+ presentation: props.presentationMode,
40
+ page: tracks.value.map((track) => track.id),
41
+ })
42
+ }
43
+
44
+ export function wrapAnimationCommands<T extends object>(commands: T, stabilize: () => void): T {
45
+ return Object.fromEntries(
46
+ Object.entries(commands).map(([key, value]) => [
47
+ key,
48
+ typeof value === 'function' && key !== 'getControlState'
49
+ ? (...args: unknown[]) => {
50
+ stabilize()
51
+ return value(...args)
52
+ }
53
+ : value,
54
+ ]),
55
+ ) as T
56
+ }