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,148 @@
|
|
|
1
|
+
import type { ScaleLinear } from 'd3'
|
|
2
|
+
import type { WaveformPoint, WaveformLineType } from '@/types'
|
|
3
|
+
import type { TrackSeriesPath } from '../core/types'
|
|
4
|
+
import type { NumericGeometry } from '@/animation/types'
|
|
5
|
+
import { resolveWaveformPointErrors } from '@/core'
|
|
6
|
+
import { createPathBuffer } from './pathBuffer'
|
|
7
|
+
|
|
8
|
+
// Weak ownership releases cached SVG strings and point arrays with their numeric snapshots.
|
|
9
|
+
const projections = new WeakMap<
|
|
10
|
+
NumericGeometry,
|
|
11
|
+
{
|
|
12
|
+
path: TrackSeriesPath
|
|
13
|
+
x: ScaleLinear<number, number>
|
|
14
|
+
y: ScaleLinear<number, number>
|
|
15
|
+
result: TrackSeriesPath
|
|
16
|
+
}
|
|
17
|
+
>()
|
|
18
|
+
const dirty = new WeakSet<NumericGeometry>()
|
|
19
|
+
const transitions = new WeakMap<NumericGeometry, WeakMap<NumericGeometry, NumericGeometry>>()
|
|
20
|
+
const snapshots = new WeakMap<TrackSeriesPath, NumericGeometry>()
|
|
21
|
+
const pathBuffers = new WeakMap<WaveformPoint[], ReturnType<typeof createPathBuffer>>()
|
|
22
|
+
|
|
23
|
+
/** Private presentation buffer; the source/target snapshots and business points stay immutable. */
|
|
24
|
+
export function interpolateGeometry(from: NumericGeometry, to: NumericGeometry, p: number) {
|
|
25
|
+
let targets = transitions.get(from)
|
|
26
|
+
if (!targets) transitions.set(from, (targets = new WeakMap()))
|
|
27
|
+
let result = targets.get(to)
|
|
28
|
+
if (!result) {
|
|
29
|
+
result = Object.fromEntries(
|
|
30
|
+
Object.entries(to).map(([key, rows]) => [
|
|
31
|
+
key,
|
|
32
|
+
// oxlint-disable-next-line unicorn/no-new-array -- Allocate fixed-length interpolation output.
|
|
33
|
+
(rows as number[][]).map((row) => new Array<number>(row.length)),
|
|
34
|
+
]),
|
|
35
|
+
)
|
|
36
|
+
targets.set(to, result)
|
|
37
|
+
}
|
|
38
|
+
for (const key of Object.keys(to)) {
|
|
39
|
+
const a = from[key] as number[][],
|
|
40
|
+
b = to[key] as number[][],
|
|
41
|
+
output = result[key] as number[][]
|
|
42
|
+
for (let i = 0; i < b.length; i++)
|
|
43
|
+
for (let j = 0; j < b[i].length; j++) {
|
|
44
|
+
const value = p === 0 ? a[i][j] : p === 1 ? b[i][j] : (1 - p) * a[i][j] + p * b[i][j]
|
|
45
|
+
if (!Number.isFinite(value)) throw new RangeError('Nonfinite presentation geometry')
|
|
46
|
+
output[i][j] = value
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
dirty.add(result)
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function generateWaveformPath(
|
|
54
|
+
points: WaveformPoint[],
|
|
55
|
+
type: WaveformLineType,
|
|
56
|
+
x: ScaleLinear<number, number>,
|
|
57
|
+
y: ScaleLinear<number, number>,
|
|
58
|
+
): string | null {
|
|
59
|
+
if (!points.length || type === 'none') return null
|
|
60
|
+
let buffer = pathBuffers.get(points)
|
|
61
|
+
if (!buffer) pathBuffers.set(points, (buffer = createPathBuffer(x, y)))
|
|
62
|
+
return buffer(points, type, x, y)
|
|
63
|
+
}
|
|
64
|
+
function values(points: WaveformPoint[]): number[][] {
|
|
65
|
+
return points.map((point) => {
|
|
66
|
+
const error = resolveWaveformPointErrors(point)
|
|
67
|
+
return [point.x, point.y, error.lower, error.upper]
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
export function captureGeometry(
|
|
71
|
+
path: TrackSeriesPath,
|
|
72
|
+
x: ScaleLinear<number, number>,
|
|
73
|
+
): NumericGeometry {
|
|
74
|
+
let geometry = snapshots.get(path)
|
|
75
|
+
if (!geometry) {
|
|
76
|
+
let line: number[][] | undefined, points: number[][] | undefined, errors: number[][] | undefined
|
|
77
|
+
// Layout paths are immutable. Static/enter rendering uses their existing SVG and does not
|
|
78
|
+
// need numeric copies; materialize a private endpoint only when an update reads it.
|
|
79
|
+
geometry = {
|
|
80
|
+
get line() {
|
|
81
|
+
return (line ??= values(path.lineRenderPoints ?? []))
|
|
82
|
+
},
|
|
83
|
+
get points() {
|
|
84
|
+
return (points ??= values(path.pointRenderPoints))
|
|
85
|
+
},
|
|
86
|
+
get errors() {
|
|
87
|
+
return (errors ??= values(path.errorBarRenderPoints))
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
snapshots.set(path, geometry)
|
|
91
|
+
}
|
|
92
|
+
projections.set(geometry, { path, x, y: path.yScale, result: path })
|
|
93
|
+
return geometry
|
|
94
|
+
}
|
|
95
|
+
function points(values: number[][], buffer: WaveformPoint[] = []): WaveformPoint[] {
|
|
96
|
+
for (let i = 0; i < values.length; i++) {
|
|
97
|
+
// Initialize all numeric fields together; overwrite before publishing the private buffer.
|
|
98
|
+
const point = buffer[i] ?? (buffer[i] = { x: NaN, y: NaN, lowerError: NaN, upperError: NaN })
|
|
99
|
+
const row = values[i]
|
|
100
|
+
point.x = row[0]
|
|
101
|
+
point.y = row[1]
|
|
102
|
+
point.lowerError = row[2]
|
|
103
|
+
point.upperError = row[3]
|
|
104
|
+
}
|
|
105
|
+
buffer.length = values.length
|
|
106
|
+
return buffer
|
|
107
|
+
}
|
|
108
|
+
export function projectGeometry(
|
|
109
|
+
path: TrackSeriesPath,
|
|
110
|
+
geometry: NumericGeometry,
|
|
111
|
+
x: ScaleLinear<number, number>,
|
|
112
|
+
y: ScaleLinear<number, number>,
|
|
113
|
+
): TrackSeriesPath {
|
|
114
|
+
const cached = projections.get(geometry)
|
|
115
|
+
if (!dirty.has(geometry) && cached?.path === path && cached.x === x && cached.y === y)
|
|
116
|
+
return cached.result
|
|
117
|
+
const buffer = cached?.path === path && cached.result !== path ? cached.result : undefined
|
|
118
|
+
const linePoints = points(geometry.line as number[][], buffer?.lineRenderPoints)
|
|
119
|
+
const result = {
|
|
120
|
+
...path,
|
|
121
|
+
lineRenderPoints: linePoints,
|
|
122
|
+
path: generateWaveformPath(linePoints, path.series.lineType, x, y),
|
|
123
|
+
pointRenderPoints: points(geometry.points as number[][], buffer?.pointRenderPoints),
|
|
124
|
+
errorBarRenderPoints: points(geometry.errors as number[][], buffer?.errorBarRenderPoints),
|
|
125
|
+
yScale: y,
|
|
126
|
+
}
|
|
127
|
+
projections.set(geometry, { path, x, y, result })
|
|
128
|
+
dirty.delete(geometry)
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
131
|
+
export function compatibleGeometry(
|
|
132
|
+
a: TrackSeriesPath,
|
|
133
|
+
b: TrackSeriesPath,
|
|
134
|
+
from: NumericGeometry,
|
|
135
|
+
to: NumericGeometry,
|
|
136
|
+
): boolean {
|
|
137
|
+
if (
|
|
138
|
+
a.series.lineType !== b.series.lineType ||
|
|
139
|
+
a.series.pointType !== b.series.pointType ||
|
|
140
|
+
a.series.errorBar.visible !== b.series.errorBar.visible
|
|
141
|
+
)
|
|
142
|
+
return false
|
|
143
|
+
return ['line', 'points', 'errors'].every((key) => {
|
|
144
|
+
const old = from[key] as number[][],
|
|
145
|
+
next = to[key] as number[][]
|
|
146
|
+
return old.length === next.length && old.every((point, i) => point[0] === next[i][0])
|
|
147
|
+
})
|
|
148
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { shallowRef, type ComputedRef } from 'vue'
|
|
2
|
+
import type { AnimationContext } from './context'
|
|
3
|
+
import type { ResolvedAnimationProps } from './config'
|
|
4
|
+
import type { WaveformAnimationController } from '@/animation/WaveformAnimationController'
|
|
5
|
+
import { clampProgress } from '@/animation/presets/shared'
|
|
6
|
+
import { chartExportOptions } from '../controls/chartExportOptions'
|
|
7
|
+
|
|
8
|
+
/** A single owner-scoped fade; it holds no old page geometry. */
|
|
9
|
+
export function createPaginationPresentation(
|
|
10
|
+
context: AnimationContext,
|
|
11
|
+
controller: WaveformAnimationController,
|
|
12
|
+
config: ComputedRef<ResolvedAnimationProps>,
|
|
13
|
+
effective: ComputedRef<boolean>,
|
|
14
|
+
commit: () => void,
|
|
15
|
+
) {
|
|
16
|
+
const opacity = shallowRef(1)
|
|
17
|
+
const key = { kind: 'custom', id: 'pagination', scope: 'update' } as const
|
|
18
|
+
let sequence = 0
|
|
19
|
+
let request: { page: number; version: number; environment: string; started: boolean } | undefined
|
|
20
|
+
const environment = () =>
|
|
21
|
+
JSON.stringify({
|
|
22
|
+
options: Object.fromEntries(
|
|
23
|
+
Object.entries(chartExportOptions(context.props)).filter(
|
|
24
|
+
([name]) => !['annotations', 'annotationsVisible'].includes(name),
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
size: context.containerSize.value,
|
|
28
|
+
hidden: context.props.hiddenSeriesIds,
|
|
29
|
+
presentation: context.props.presentationMode,
|
|
30
|
+
})
|
|
31
|
+
function reset() {
|
|
32
|
+
request = undefined
|
|
33
|
+
opacity.value = 1
|
|
34
|
+
}
|
|
35
|
+
function cancel() {
|
|
36
|
+
controller.cancel(key)
|
|
37
|
+
reset()
|
|
38
|
+
}
|
|
39
|
+
function requestPage(page: number) {
|
|
40
|
+
cancel()
|
|
41
|
+
controller.finishAll()
|
|
42
|
+
if (!effective.value || !config.value.animationUpdate.enabled) return
|
|
43
|
+
request = { page, version: ++sequence, environment: environment(), started: false }
|
|
44
|
+
if (config.value.animationUpdate.duration > 0) opacity.value = 0
|
|
45
|
+
}
|
|
46
|
+
function reconcile(dataChanged: boolean) {
|
|
47
|
+
const current = request
|
|
48
|
+
if (!current) return false
|
|
49
|
+
if (
|
|
50
|
+
dataChanged ||
|
|
51
|
+
current.page !== context.currentPage.value ||
|
|
52
|
+
current.environment !== environment() ||
|
|
53
|
+
!effective.value ||
|
|
54
|
+
!config.value.animationUpdate.enabled
|
|
55
|
+
) {
|
|
56
|
+
controller.finishAll()
|
|
57
|
+
reset()
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
if (!current.started && config.value.animationUpdate.duration === 0) opacity.value = 1
|
|
61
|
+
commit()
|
|
62
|
+
if (context.sampling.state.value === 'pending' || !context.layoutReady.value) return true
|
|
63
|
+
if (context.sampling.state.value !== 'success') {
|
|
64
|
+
controller.finishAll()
|
|
65
|
+
reset()
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
if (current.started) return true
|
|
69
|
+
const ids = context.tracks.value.flatMap((track) =>
|
|
70
|
+
track.width > 0 && track.height > 0
|
|
71
|
+
? track.seriesList
|
|
72
|
+
.filter((series) => (series.source?.length ?? series.points.length) > 0)
|
|
73
|
+
.map((series) => series.id)
|
|
74
|
+
: [],
|
|
75
|
+
)
|
|
76
|
+
if (!ids.length) {
|
|
77
|
+
reset()
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
current.started = true
|
|
81
|
+
const options = config.value.animationUpdate
|
|
82
|
+
const notify = (callback?: () => void) => ids.forEach(() => callback?.())
|
|
83
|
+
controller.animate(key, {
|
|
84
|
+
duration: options.duration,
|
|
85
|
+
easing: options.easing,
|
|
86
|
+
force: true,
|
|
87
|
+
initialize: () => {
|
|
88
|
+
opacity.value = options.duration > 0 ? 0 : 1
|
|
89
|
+
},
|
|
90
|
+
during: (progress, frame) => {
|
|
91
|
+
if (request?.version === current.version)
|
|
92
|
+
opacity.value = frame.terminal ? 1 : clampProgress(progress)
|
|
93
|
+
},
|
|
94
|
+
done: () => {
|
|
95
|
+
if (request?.version === current.version) reset()
|
|
96
|
+
notify(options.onComplete)
|
|
97
|
+
},
|
|
98
|
+
aborted: () => {
|
|
99
|
+
if (request?.version === current.version) reset()
|
|
100
|
+
notify(options.onAbort)
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
function wrapPagination(goToPage: (page: number) => void, count: ComputedRef<number>) {
|
|
106
|
+
return (page: number) => {
|
|
107
|
+
const target = Math.min(count.value, Math.max(1, Math.floor(page)))
|
|
108
|
+
if (target === context.currentPage.value) return
|
|
109
|
+
requestPage(target)
|
|
110
|
+
goToPage(page)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { opacity, reset, cancel, reconcile, wrapPagination, active: () => !!request }
|
|
114
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { line, curveLinear, curveStep, curveStepBefore, curveStepAfter } from 'd3'
|
|
2
|
+
import type { ScaleLinear } from 'd3'
|
|
3
|
+
import type { WaveformPoint, WaveformLineType } from '@/types'
|
|
4
|
+
|
|
5
|
+
let decoder: TextDecoder | undefined
|
|
6
|
+
|
|
7
|
+
/** Stable D3 accessors/context keep their feedback across frames; storage is private to points. */
|
|
8
|
+
export function createPathBuffer(x: ScaleLinear<number, number>, y: ScaleLinear<number, number>) {
|
|
9
|
+
let xScale = x,
|
|
10
|
+
yScale = y,
|
|
11
|
+
type: WaveformLineType | undefined
|
|
12
|
+
let bytes = new Uint8Array(1024),
|
|
13
|
+
length = 0
|
|
14
|
+
const append = (text: string) => {
|
|
15
|
+
if (length + text.length > bytes.length) {
|
|
16
|
+
const next = new Uint8Array(Math.max(bytes.length * 2, length + text.length))
|
|
17
|
+
next.set(bytes)
|
|
18
|
+
bytes = next
|
|
19
|
+
}
|
|
20
|
+
for (let i = 0; i < text.length; i++) bytes[length++] = text.charCodeAt(i)
|
|
21
|
+
}
|
|
22
|
+
const coordinate = (value: number) => append(String(Math.round(value * 1000) / 1000))
|
|
23
|
+
const point = (command: string, x: number, y: number) => {
|
|
24
|
+
append(command)
|
|
25
|
+
coordinate(x)
|
|
26
|
+
append(',')
|
|
27
|
+
coordinate(y)
|
|
28
|
+
}
|
|
29
|
+
const generator = line<WaveformPoint>()
|
|
30
|
+
.x((point) => xScale(point.x))
|
|
31
|
+
.y((point) => yScale(point.y))
|
|
32
|
+
.context({
|
|
33
|
+
moveTo: (x: number, y: number) => point('M', x, y),
|
|
34
|
+
lineTo: (x: number, y: number) => point('L', x, y),
|
|
35
|
+
closePath: () => append('Z'),
|
|
36
|
+
} as unknown as CanvasRenderingContext2D)
|
|
37
|
+
return (
|
|
38
|
+
points: WaveformPoint[],
|
|
39
|
+
nextType: WaveformLineType,
|
|
40
|
+
x: ScaleLinear<number, number>,
|
|
41
|
+
y: ScaleLinear<number, number>,
|
|
42
|
+
): string => {
|
|
43
|
+
xScale = x
|
|
44
|
+
yScale = y
|
|
45
|
+
if (type !== nextType) {
|
|
46
|
+
generator.curve(
|
|
47
|
+
nextType === 'step-start'
|
|
48
|
+
? curveStepBefore
|
|
49
|
+
: nextType === 'step-middle'
|
|
50
|
+
? curveStep
|
|
51
|
+
: nextType === 'step-end' || nextType === 'step-after'
|
|
52
|
+
? curveStepAfter
|
|
53
|
+
: curveLinear,
|
|
54
|
+
)
|
|
55
|
+
type = nextType
|
|
56
|
+
}
|
|
57
|
+
length = 0
|
|
58
|
+
generator(points)
|
|
59
|
+
return (decoder ??= new TextDecoder()).decode(bytes.subarray(0, length))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { TrackLayout, TrackSeriesPath } from '../core/types'
|
|
2
|
+
import type { WaveformAnnotation } from '@/types'
|
|
3
|
+
import type { NumericGeometry } from '@/animation/types'
|
|
4
|
+
import {
|
|
5
|
+
captureGeometry,
|
|
6
|
+
compatibleGeometry,
|
|
7
|
+
projectGeometry,
|
|
8
|
+
interpolateGeometry,
|
|
9
|
+
} from './geometry'
|
|
10
|
+
import { interpolateValue } from '@/animation/core/values'
|
|
11
|
+
import { clampProgress, validDomain } from '@/animation/presets/shared'
|
|
12
|
+
import type { ScaleLinear } from 'd3'
|
|
13
|
+
|
|
14
|
+
function presentationScale(scale: ScaleLinear<number, number>, domain: [number, number]) {
|
|
15
|
+
const current = scale.domain()
|
|
16
|
+
return current[0] === domain[0] && current[1] === domain[1] ? scale : scale.copy().domain(domain)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface AnchorValue {
|
|
20
|
+
seriesId: string
|
|
21
|
+
annotationY: number
|
|
22
|
+
x: number
|
|
23
|
+
y: number
|
|
24
|
+
}
|
|
25
|
+
interface SeriesGeometry {
|
|
26
|
+
path: TrackSeriesPath
|
|
27
|
+
values: NumericGeometry
|
|
28
|
+
old?: { path: TrackSeriesPath; values: NumericGeometry }
|
|
29
|
+
opacity: number
|
|
30
|
+
}
|
|
31
|
+
export interface TrackPresentation {
|
|
32
|
+
target: TrackLayout
|
|
33
|
+
series: Map<string, SeriesGeometry>
|
|
34
|
+
xDomain: [number, number]
|
|
35
|
+
yDomains: [number, number][]
|
|
36
|
+
anchors: Map<string, AnchorValue>
|
|
37
|
+
clips: Map<string, { id: string; x: number; width: number }>
|
|
38
|
+
}
|
|
39
|
+
export function staticTrack(
|
|
40
|
+
track: TrackLayout,
|
|
41
|
+
annotations: WaveformAnnotation[],
|
|
42
|
+
previous?: TrackPresentation,
|
|
43
|
+
refreshAnchors = false,
|
|
44
|
+
): TrackPresentation {
|
|
45
|
+
const anchors = new Map<string, AnchorValue>()
|
|
46
|
+
for (const annotation of annotations) {
|
|
47
|
+
const series = track.seriesList.find((series) => series.id === annotation.seriesId)
|
|
48
|
+
if (!series || (series.source?.length ?? series.points.length) === 0) continue
|
|
49
|
+
const cached = previous?.anchors.get(annotation.id)
|
|
50
|
+
const point = refreshAnchors ? series.source?.nearestPoint(annotation.x) : undefined
|
|
51
|
+
if (refreshAnchors && !point) continue
|
|
52
|
+
anchors.set(annotation.id, {
|
|
53
|
+
seriesId: series.id,
|
|
54
|
+
annotationY: annotation.y,
|
|
55
|
+
x: annotation.x,
|
|
56
|
+
y:
|
|
57
|
+
point?.y ??
|
|
58
|
+
(cached?.x === annotation.x && cached.annotationY === annotation.y
|
|
59
|
+
? cached.y
|
|
60
|
+
: annotation.y),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
target: track,
|
|
65
|
+
series: new Map(
|
|
66
|
+
track.seriesPaths.map((path) => [
|
|
67
|
+
path.series.id,
|
|
68
|
+
{ path, values: captureGeometry(path, track.xScale), opacity: 1 },
|
|
69
|
+
]),
|
|
70
|
+
),
|
|
71
|
+
xDomain: track.xScale.domain() as [number, number],
|
|
72
|
+
yDomains: track.yAxes.map((axis) => axis.scale.domain() as [number, number]),
|
|
73
|
+
anchors,
|
|
74
|
+
clips: new Map(),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function domain(from: [number, number], to: [number, number], p: number): [number, number] {
|
|
78
|
+
if (!validDomain(to)) return from
|
|
79
|
+
const value = interpolateValue(from, to, clampProgress(p)) as [number, number]
|
|
80
|
+
return validDomain(value) ? value : to
|
|
81
|
+
}
|
|
82
|
+
export function hasThirdGeometry(from: TrackPresentation): boolean {
|
|
83
|
+
return [...from.series.values()].some((series) => !!series.old)
|
|
84
|
+
}
|
|
85
|
+
export function transitionTrack(
|
|
86
|
+
from: TrackPresentation,
|
|
87
|
+
to: TrackPresentation,
|
|
88
|
+
p: number,
|
|
89
|
+
terminal: boolean,
|
|
90
|
+
): TrackPresentation {
|
|
91
|
+
if (terminal) return to
|
|
92
|
+
const series = new Map<string, SeriesGeometry>()
|
|
93
|
+
for (const [id, next] of to.series) {
|
|
94
|
+
const previous = from.series.get(id)
|
|
95
|
+
if (!previous) {
|
|
96
|
+
series.set(id, next)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
const compatible = compatibleGeometry(previous.path, next.path, previous.values, next.values)
|
|
100
|
+
series.set(
|
|
101
|
+
id,
|
|
102
|
+
compatible
|
|
103
|
+
? {
|
|
104
|
+
...next,
|
|
105
|
+
values: interpolateGeometry(previous.values, next.values, p),
|
|
106
|
+
}
|
|
107
|
+
: {
|
|
108
|
+
...next,
|
|
109
|
+
old: { path: previous.path, values: previous.values },
|
|
110
|
+
opacity: clampProgress(p),
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
const anchors = new Map<string, AnchorValue>()
|
|
115
|
+
for (const [id, next] of to.anchors) {
|
|
116
|
+
const previous = from.anchors.get(id)
|
|
117
|
+
anchors.set(
|
|
118
|
+
id,
|
|
119
|
+
previous && !series.get(next.seriesId)?.old
|
|
120
|
+
? {
|
|
121
|
+
seriesId: next.seriesId,
|
|
122
|
+
annotationY: next.annotationY,
|
|
123
|
+
x: (1 - p) * previous.x + p * next.x,
|
|
124
|
+
y: (1 - p) * previous.y + p * next.y,
|
|
125
|
+
}
|
|
126
|
+
: next,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...to,
|
|
131
|
+
series,
|
|
132
|
+
anchors,
|
|
133
|
+
xDomain: domain(from.xDomain, to.xDomain, p),
|
|
134
|
+
yDomains: to.yDomains.map((target, i) => domain(from.yDomains[i] ?? target, target, p)),
|
|
135
|
+
clips: to.clips,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function projectTrack(state: TrackPresentation): TrackLayout {
|
|
139
|
+
const target = state.target
|
|
140
|
+
const xScale = presentationScale(target.xScale, state.xDomain)
|
|
141
|
+
const projectedAxes = target.yAxes.map((axis, index) => {
|
|
142
|
+
const scale = presentationScale(axis.scale, state.yDomains[index])
|
|
143
|
+
return scale === axis.scale ? axis : { ...axis, scale }
|
|
144
|
+
})
|
|
145
|
+
const yAxes = projectedAxes.every((axis, index) => axis === target.yAxes[index])
|
|
146
|
+
? target.yAxes
|
|
147
|
+
: projectedAxes
|
|
148
|
+
const seriesPaths: TrackSeriesPath[] = []
|
|
149
|
+
for (const [id, series] of state.series) {
|
|
150
|
+
const y = yAxes[series.path.yAxisIndex]?.scale ?? target.yScale
|
|
151
|
+
if (series.old)
|
|
152
|
+
seriesPaths.push({
|
|
153
|
+
...projectGeometry(series.old.path, series.old.values, xScale, y),
|
|
154
|
+
presentationKey: `${id}:old`,
|
|
155
|
+
opacity: 1 - series.opacity,
|
|
156
|
+
})
|
|
157
|
+
seriesPaths.push({
|
|
158
|
+
...projectGeometry(series.path, series.values, xScale, y),
|
|
159
|
+
presentationKey: id,
|
|
160
|
+
opacity: series.opacity,
|
|
161
|
+
animationClip: state.clips.get(id),
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...target,
|
|
166
|
+
xScale,
|
|
167
|
+
yScale: yAxes[0]?.scale ?? target.yScale,
|
|
168
|
+
yAxes,
|
|
169
|
+
seriesPaths,
|
|
170
|
+
path: seriesPaths[0]?.path ?? null,
|
|
171
|
+
}
|
|
172
|
+
}
|