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.
- package/README.md +256 -46
- package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
- package/dist/index.cjs +1 -2
- package/dist/index.js +1 -7115
- 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/layout.d.ts +2 -1
- package/dist/types/components/core/types.d.ts +8 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +25 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
- package/dist/types/components/core/yAxisTitleLayout.d.ts +1 -1
- package/dist/types/components/interaction/useExtremaTickViewport.d.ts +1 -1
- package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
- package/dist/types/components/rendering/updateAxis.d.ts +5 -0
- package/dist/types/components/rendering/updateGrid.d.ts +3 -0
- package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
- package/dist/types/components/utils/assignElement.d.ts +2 -0
- package/dist/types/demo/types.d.ts +2 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/types/types/chart.d.ts +4 -2
- package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
- package/package.json +5 -2
- package/src/App.vue +6 -3
- package/src/DemoRouterApp.vue +1 -0
- package/src/animation/WaveformAnimationController.ts +328 -0
- package/src/animation/core/Animation.ts +37 -0
- package/src/animation/core/Animator.ts +200 -0
- package/src/animation/core/easing.ts +74 -0
- package/src/animation/core/errors.ts +24 -0
- package/src/animation/core/handle.ts +34 -0
- package/src/animation/core/owner.ts +9 -0
- package/src/animation/core/propertyGroups.ts +143 -0
- package/src/animation/core/task.ts +153 -0
- package/src/animation/core/types.ts +81 -0
- package/src/animation/core/validation.ts +173 -0
- package/src/animation/core/values.ts +38 -0
- package/src/animation/presets/annotation.ts +66 -0
- package/src/animation/presets/lineDrawing.ts +51 -0
- package/src/animation/presets/shared.ts +58 -0
- package/src/animation/presets/transitions.ts +77 -0
- package/src/animation/types.ts +81 -0
- package/src/components/WaveformChart.vue +1 -0
- package/src/components/WaveformChartView.vue +13 -4
- package/src/components/animation/annotationPresentation.ts +145 -0
- package/src/components/animation/chartExportVersion.ts +5 -0
- package/src/components/animation/config.ts +87 -0
- package/src/components/animation/context.ts +56 -0
- package/src/components/animation/geometry.ts +148 -0
- package/src/components/animation/paginationPresentation.ts +114 -0
- package/src/components/animation/pathBuffer.ts +61 -0
- package/src/components/animation/trackPresentation.ts +172 -0
- package/src/components/animation/useWaveformAnimations.ts +420 -0
- package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
- package/src/components/annotation/types.ts +3 -0
- package/src/components/controls/useChartImageExport.ts +4 -0
- package/src/components/core/layout.ts +36 -22
- package/src/components/core/trackLayoutBuilder.ts +17 -23
- package/src/components/core/types.ts +4 -0
- package/src/components/core/useWaveformChartController.ts +24 -20
- package/src/components/core/useWaveformLayout.ts +3 -1
- package/src/components/core/useWaveformRenderSampling.ts +23 -1
- package/src/components/core/waveformChartTypes.ts +2 -1
- package/src/components/core/yAxisLayoutMetrics.ts +24 -16
- package/src/components/core/yAxisPadding.ts +10 -5
- package/src/components/core/yAxisTitleLayout.ts +9 -48
- package/src/components/interaction/useExtremaTickViewport.ts +1 -1
- package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
- package/src/components/rendering/WaveformTrack.vue +6 -1
- package/src/components/rendering/WaveformTrackAxes.vue +64 -51
- package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
- package/src/components/rendering/updateAxis.ts +57 -0
- package/src/components/rendering/updateGrid.ts +42 -0
- package/src/components/rendering/yAxisTickVisibility.ts +45 -18
- package/src/components/utils/assignElement.ts +7 -0
- package/src/demo/DemoYAxisPaddingControls.vue +11 -3
- package/src/demo/types.ts +2 -1
- package/src/index.ts +53 -1
- package/src/router.ts +1 -0
- package/src/types/chart.ts +4 -2
- package/src/views/AnimationDemo.vue +129 -0
- 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
|
+
}
|
|
@@ -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
|
|
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="
|
|
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
|
+
}
|