waveform-analysis 0.1.74 → 0.1.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +422 -60
- package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
- package/dist/index.cjs +1 -2
- package/dist/index.js +1 -7119
- package/dist/style.css +1 -1
- package/dist/types/animation/WaveformAnimationController.d.ts +30 -0
- package/dist/types/animation/core/Animation.d.ts +13 -0
- package/dist/types/animation/core/Animator.d.ts +23 -0
- package/dist/types/animation/core/easing.d.ts +3 -0
- package/dist/types/animation/core/errors.d.ts +4 -0
- package/dist/types/animation/core/handle.d.ts +7 -0
- package/dist/types/animation/core/owner.d.ts +3 -0
- package/dist/types/animation/core/propertyGroups.d.ts +17 -0
- package/dist/types/animation/core/task.d.ts +29 -0
- package/dist/types/animation/core/types.d.ts +74 -0
- package/dist/types/animation/core/validation.d.ts +30 -0
- package/dist/types/animation/core/values.d.ts +8 -0
- package/dist/types/animation/presets/annotation.d.ts +4 -0
- package/dist/types/animation/presets/lineDrawing.d.ts +7 -0
- package/dist/types/animation/presets/shared.d.ts +14 -0
- package/dist/types/animation/presets/transitions.d.ts +4 -0
- package/dist/types/animation/types.d.ts +84 -0
- package/dist/types/components/WaveformChart.vue.d.ts +3 -2
- package/dist/types/components/animation/annotationPresentation.d.ts +17 -0
- package/dist/types/components/animation/chartExportVersion.d.ts +2 -0
- package/dist/types/components/animation/config.d.ts +31 -0
- package/dist/types/components/animation/context.d.ts +20 -0
- package/dist/types/components/animation/geometry.d.ts +10 -0
- package/dist/types/components/animation/paginationPresentation.d.ts +13 -0
- package/dist/types/components/animation/pathBuffer.d.ts +4 -0
- package/dist/types/components/animation/trackPresentation.d.ts +35 -0
- package/dist/types/components/animation/useWaveformAnimations.d.ts +27 -0
- package/dist/types/components/annotation/WaveformAnnotationContextMenu.vue.d.ts +2 -2
- package/dist/types/components/annotation/types.d.ts +7 -0
- package/dist/types/components/controls/useChartImageExport.d.ts +2 -0
- package/dist/types/components/core/amplitudeLimits.d.ts +6 -0
- package/dist/types/components/core/layout.d.ts +6 -9
- package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
- package/dist/types/components/core/types.d.ts +8 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +26 -1
- package/dist/types/components/core/useWaveformData.d.ts +2 -2
- package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +6 -3
- package/dist/types/components/core/xAxisTicks.d.ts +2 -0
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
- package/dist/types/components/core/yAxisTicks.d.ts +6 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/integerZoom.d.ts +3 -3
- package/dist/types/components/interaction/stepZoom.d.ts +5 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
- package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
- package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
- package/dist/types/components/rendering/updateAxis.d.ts +5 -0
- package/dist/types/components/rendering/updateGrid.d.ts +3 -0
- package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
- package/dist/types/components/utils/assignElement.d.ts +2 -0
- package/dist/types/core/data.d.ts +1 -1
- package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
- package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
- package/dist/types/demo/types.d.ts +11 -2
- package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/types/chart.d.ts +8 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/formatters.d.ts +4 -4
- package/dist/types/utils/xAxisNumber.d.ts +2 -0
- package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
- package/package.json +5 -2
- package/src/App.vue +34 -100
- package/src/DemoRouterApp.vue +1 -0
- package/src/animation/WaveformAnimationController.ts +328 -0
- package/src/animation/core/Animation.ts +37 -0
- package/src/animation/core/Animator.ts +200 -0
- package/src/animation/core/easing.ts +74 -0
- package/src/animation/core/errors.ts +24 -0
- package/src/animation/core/handle.ts +34 -0
- package/src/animation/core/owner.ts +9 -0
- package/src/animation/core/propertyGroups.ts +143 -0
- package/src/animation/core/task.ts +153 -0
- package/src/animation/core/types.ts +81 -0
- package/src/animation/core/validation.ts +173 -0
- package/src/animation/core/values.ts +38 -0
- package/src/animation/presets/annotation.ts +66 -0
- package/src/animation/presets/lineDrawing.ts +51 -0
- package/src/animation/presets/shared.ts +58 -0
- package/src/animation/presets/transitions.ts +77 -0
- package/src/animation/types.ts +81 -0
- package/src/components/WaveformChart.vue +1 -0
- package/src/components/WaveformChartView.vue +13 -6
- package/src/components/animation/annotationPresentation.ts +145 -0
- package/src/components/animation/chartExportVersion.ts +5 -0
- package/src/components/animation/config.ts +87 -0
- package/src/components/animation/context.ts +56 -0
- package/src/components/animation/geometry.ts +148 -0
- package/src/components/animation/paginationPresentation.ts +114 -0
- package/src/components/animation/pathBuffer.ts +61 -0
- package/src/components/animation/trackPresentation.ts +172 -0
- package/src/components/animation/useWaveformAnimations.ts +420 -0
- package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
- package/src/components/annotation/types.ts +3 -0
- package/src/components/controls/useChartCommands.ts +12 -13
- package/src/components/controls/useChartImageExport.ts +4 -0
- package/src/components/core/amplitudeLimits.ts +44 -0
- package/src/components/core/layout.ts +22 -34
- package/src/components/core/trackLayoutBuilder.ts +21 -23
- package/src/components/core/types.ts +4 -0
- package/src/components/core/useWaveformChartController.ts +27 -23
- package/src/components/core/useWaveformChartLifecycle.ts +11 -2
- package/src/components/core/useWaveformData.ts +11 -5
- package/src/components/core/useWaveformLayout.ts +9 -4
- package/src/components/core/useWaveformMeasurements.ts +3 -0
- package/src/components/core/useWaveformRenderSampling.ts +23 -1
- package/src/components/core/useWaveformYAxisLayout.ts +2 -0
- package/src/components/core/waveformChartTypes.ts +6 -2
- package/src/components/core/xAxisTicks.ts +69 -0
- package/src/components/core/yAxisLayoutMetrics.ts +4 -0
- package/src/components/core/yAxisTicks.ts +39 -0
- package/src/components/data/types.ts +2 -0
- package/src/components/interaction/integerZoom.ts +31 -12
- package/src/components/interaction/remoteViewport.ts +26 -4
- package/src/components/interaction/stepZoom.ts +33 -0
- package/src/components/interaction/useWaveformViewport.ts +3 -26
- package/src/components/interaction/useWaveformZoom.ts +50 -157
- package/src/components/interaction/zoomConstraints.ts +5 -5
- package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
- package/src/components/rendering/WaveformTrack.vue +6 -1
- package/src/components/rendering/WaveformTrackAxes.vue +64 -51
- package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
- package/src/components/rendering/updateAxis.ts +57 -0
- package/src/components/rendering/updateGrid.ts +42 -0
- package/src/components/rendering/yAxisTickVisibility.ts +45 -18
- package/src/components/utils/assignElement.ts +7 -0
- package/src/core/data.ts +5 -2
- package/src/data/simulatedWaveforms.ts +0 -10
- package/src/demo/DemoChartHost.vue +49 -4
- package/src/demo/remoteWaveformLoader.ts +114 -0
- package/src/demo/types.ts +11 -1
- package/src/demo/useRemoteWaveformWindow.ts +229 -0
- package/src/index.ts +54 -0
- package/src/router.ts +1 -0
- package/src/types/chart.ts +10 -1
- package/src/types/index.ts +2 -0
- package/src/utils/formatters.ts +10 -5
- package/src/utils/xAxisNumber.ts +36 -0
- package/src/views/AnimationDemo.vue +129 -0
- package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
- package/dist/types/data/sx2Channel.d.ts +0 -10
- package/src/data/sx2Channel.ts +0 -315
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -46,8 +46,11 @@ const {
|
|
|
46
46
|
chartWidth,
|
|
47
47
|
drawingHeight,
|
|
48
48
|
hasWaveformData,
|
|
49
|
-
requestViewportReset,
|
|
50
49
|
trackLayouts,
|
|
50
|
+
presentationTracks,
|
|
51
|
+
presentationAnnotations,
|
|
52
|
+
paginationOpacity,
|
|
53
|
+
stabilizeAnimations,
|
|
51
54
|
clipPathId,
|
|
52
55
|
chartTopMargin,
|
|
53
56
|
gridCells,
|
|
@@ -73,7 +76,6 @@ const {
|
|
|
73
76
|
hoverState,
|
|
74
77
|
showTooltip,
|
|
75
78
|
selectionBox,
|
|
76
|
-
renderedAnnotations,
|
|
77
79
|
annotationsVisible,
|
|
78
80
|
handleExistingAnnotationContextMenu,
|
|
79
81
|
beginAnnotationDrag,
|
|
@@ -142,9 +144,14 @@ function handleChartPointerLeave() {
|
|
|
142
144
|
:data-plot-margin-top="resolvedPlotMargin.top"
|
|
143
145
|
:data-plot-margin-bottom="resolvedPlotMargin.bottom"
|
|
144
146
|
:data-title-area-height="titleAreaHeight"
|
|
147
|
+
@pointermove.capture="stabilizeAnimations"
|
|
148
|
+
@pointerdown.capture="stabilizeAnimations"
|
|
149
|
+
@wheel.capture="stabilizeAnimations"
|
|
150
|
+
@dblclick.capture="stabilizeAnimations"
|
|
151
|
+
@contextmenu.capture="stabilizeAnimations"
|
|
145
152
|
@pointerenter="pointerInsideChart = true"
|
|
146
153
|
@pointerleave="handleChartPointerLeave"
|
|
147
|
-
@contextmenu
|
|
154
|
+
@contextmenu="handleNativeContextMenu"
|
|
148
155
|
>
|
|
149
156
|
<div
|
|
150
157
|
v-if="titleAreaReserved"
|
|
@@ -181,7 +188,6 @@ function handleChartPointerLeave() {
|
|
|
181
188
|
:height="drawingHeight"
|
|
182
189
|
role="img"
|
|
183
190
|
:aria-label="hasWaveformData ? '波形折线图' : '暂无波形数据'"
|
|
184
|
-
@dblclick="requestViewportReset"
|
|
185
191
|
>
|
|
186
192
|
<defs>
|
|
187
193
|
<clipPath
|
|
@@ -235,7 +241,8 @@ function handleChartPointerLeave() {
|
|
|
235
241
|
<WaveformTrack
|
|
236
242
|
v-for="track in trackLayouts"
|
|
237
243
|
:key="`${track.index}-${track.id}`"
|
|
238
|
-
:track="track"
|
|
244
|
+
:track="presentationTracks.find((item) => item.id === track.id) ?? track"
|
|
245
|
+
:pagination-opacity="paginationOpacity"
|
|
239
246
|
:frame-path="
|
|
240
247
|
displayMode === 'compact'
|
|
241
248
|
? compactFramePath(track, trackLayouts, compactFrameInset)
|
|
@@ -291,7 +298,7 @@ function handleChartPointerLeave() {
|
|
|
291
298
|
:cancel-key="activeInteractionMode"
|
|
292
299
|
:data-version="controller.data"
|
|
293
300
|
v-if="!isCleanView"
|
|
294
|
-
:annotations="
|
|
301
|
+
:annotations="presentationAnnotations"
|
|
295
302
|
:visible="annotationsVisible"
|
|
296
303
|
:interactive="!isPresentationMode && activeInteractionMode !== 'none'"
|
|
297
304
|
@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
|
+
}
|