waveform-analysis 0.1.54 → 0.1.57

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.
@@ -0,0 +1,129 @@
1
+ import { zoomIdentity } from 'd3'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
+ import { buildTrackLayouts, buildYAxisSlots } from './layout'
6
+ import type { DisplaySeries, DisplayTrack } from './types'
7
+ import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
8
+
9
+ function series(id: string, minimum: number, maximum: number): DisplaySeries {
10
+ return {
11
+ id,
12
+ name: id,
13
+ color: '#1677ff',
14
+ lineType: 'linear',
15
+ lineStyle: 'solid',
16
+ pointType: 'none',
17
+ errorBar: { visible: false, width: 1.5, capWidth: 8 },
18
+ points: [
19
+ { x: 0, y: minimum },
20
+ { x: 1, y: maximum },
21
+ ],
22
+ xDomain: [0, 1],
23
+ yDomain: [minimum, maximum],
24
+ hasErrorPoints: false,
25
+ }
26
+ }
27
+
28
+ function track(seriesList: DisplaySeries[]): DisplayTrack {
29
+ return {
30
+ id: seriesList[0]!.id,
31
+ series: seriesList,
32
+ visibleSeries: seriesList,
33
+ xDomain: [0, 1],
34
+ yDomain: [0, 1],
35
+ }
36
+ }
37
+
38
+ describe('Y-axis slots', () => {
39
+ it('aligns matching slots across single and multi-axis tracks', () => {
40
+ const tracks = [
41
+ track([series('single', -1e120, 1e120)]),
42
+ track([series('left', 0, 1), series('right', 1000, 3000)]),
43
+ track([series('left-two', 0, 1), series('right-two', 1000, 3000)]),
44
+ ]
45
+ const yAxisSlots = buildYAxisSlots(tracks, 'multi-axis')
46
+ const layouts = buildTrackLayouts({
47
+ cells: tracks.map((series, index) => ({
48
+ slotIndex: index,
49
+ row: index,
50
+ column: 0,
51
+ left: 0,
52
+ top: index * 100,
53
+ width: 600,
54
+ height: 100,
55
+ plotHeight: 100,
56
+ cellHeight: 130,
57
+ xAxisBand: 30,
58
+ series,
59
+ })),
60
+ grid: {
61
+ rowCount: 3,
62
+ columnCount: 1,
63
+ showPagination: false,
64
+ fillIncompleteLastRow: false,
65
+ trackLines: {},
66
+ },
67
+ displayMode: 'independent',
68
+ overlayMode: 'multi-axis',
69
+ independentTransforms: [zoomIdentity, zoomIdentity, zoomIdentity],
70
+ sharedZoomDomain: [0, 1],
71
+ timeUnit: 'ms',
72
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
73
+ hideSecondaryLabels: false,
74
+ yAxisLabelX: yAxisSlots.slots.find((slot) => slot.side === 'left')!.labelOffset,
75
+ yAxisSlots: yAxisSlots.slots,
76
+ showCompactEmptyTracks: false,
77
+ })
78
+
79
+ expect(layouts.map((layout) => layout.yAxes[0]?.labelX)).toEqual([
80
+ layouts[0]?.yAxes[0]?.labelX,
81
+ layouts[0]?.yAxes[0]?.labelX,
82
+ layouts[0]?.yAxes[0]?.labelX,
83
+ ])
84
+ expect(layouts[1]?.yAxes[1]?.labelX).toBe(layouts[2]?.yAxes[1]?.labelX)
85
+ expect(layouts[1]?.yAxes[1]?.labelX).toBe(
86
+ 600 +
87
+ yAxisSlots.slots.find((slot) => slot.side === 'right')!.labelOffset -
88
+ Y_AXIS_RIGHT_LABEL_OFFSET,
89
+ )
90
+ expect(yAxisSlots.slots).toMatchObject([
91
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
92
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
93
+ ])
94
+ expect(yAxisSlots.clearance.left).toBeGreaterThan(yAxisSlots.clearance.right)
95
+ })
96
+
97
+ it('reserves outward second slots for three and four value axes', () => {
98
+ const threeAxisSlots = buildYAxisSlots(
99
+ [track([series('left', 0, 1), series('right-one', 10, 20), series('right-two', 30, 40)])],
100
+ 'multi-axis',
101
+ )
102
+ const fourAxisSlots = buildYAxisSlots(
103
+ [
104
+ track([
105
+ series('left-one', 0, 1),
106
+ series('left-two', 10, 20),
107
+ series('right-one', 30, 40),
108
+ series('right-two', 50, 60),
109
+ ]),
110
+ ],
111
+ 'multi-axis',
112
+ )
113
+
114
+ expect(threeAxisSlots.slots).toMatchObject([
115
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
116
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
117
+ { side: 'right', sideIndex: 1 },
118
+ ])
119
+ expect(threeAxisSlots.slots[2]!.axisOffset).toBeGreaterThan(0)
120
+ expect(fourAxisSlots.slots).toMatchObject([
121
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
122
+ { side: 'left', sideIndex: 1 },
123
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
124
+ { side: 'right', sideIndex: 1 },
125
+ ])
126
+ expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
127
+ expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
128
+ })
129
+ })
@@ -62,12 +62,14 @@ export function useWaveformHover(context: HoverContext) {
62
62
  nextPoints: HoveredSeriesPoint[],
63
63
  trackIndex: number | null,
64
64
  queryX: number,
65
+ crosshairXByTrack: Record<number, number>,
65
66
  position: { x: number; y: number },
66
67
  ) => {
67
68
  if (isPresentationMode.value) return
68
69
  if (!hoveredPointsMatch(nextPoints)) hoverState.points = nextPoints
69
70
  hoverState.trackIndex = trackIndex
70
71
  hoverState.queryX = queryX
72
+ hoverState.crosshairXByTrack = crosshairXByTrack
71
73
  hoverState.position = position
72
74
  emit('point-hover', hoverState.points.find((point) => point.point)?.point ?? null)
73
75
  }
@@ -76,6 +78,7 @@ export function useWaveformHover(context: HoverContext) {
76
78
  hoverState.points = []
77
79
  hoverState.trackIndex = null
78
80
  hoverState.queryX = null
81
+ hoverState.crosshairXByTrack = {}
79
82
  emit('point-hover', null)
80
83
  }
81
84
  const handlePointerLeave = () => {
@@ -112,9 +115,30 @@ export function useWaveformHover(context: HoverContext) {
112
115
  const source = series.source ?? pointSourceFromPoints(series.points)
113
116
  const first = source.pointAt(0)
114
117
  const last = source.pointAt(source.length - 1)
115
- if (!first || !last || xValue < first.x || xValue > last.x) return undefined
118
+ if (!first || !last) return undefined
119
+ if (xValue <= first.x) return first
120
+ if (xValue >= last.x) return last
116
121
  return source.nearestPoint(xValue)
117
122
  }
123
+ const crosshairXForTrack = (track: TrackLayout, xValue: number): number | undefined => {
124
+ let longestRange: [number, number] | undefined
125
+ let longestSpan = -Infinity
126
+
127
+ track.seriesList.forEach((series) => {
128
+ const source = series.source ?? pointSourceFromPoints(series.points)
129
+ const first = source.pointAt(0)
130
+ const last = source.pointAt(source.length - 1)
131
+ if (!first || !last) return
132
+ const span = last.x - first.x
133
+ if (span > longestSpan) {
134
+ longestSpan = span
135
+ longestRange = [first.x, last.x]
136
+ }
137
+ })
138
+
139
+ if (!longestRange) return undefined
140
+ return Math.max(longestRange[0], Math.min(longestRange[1], xValue))
141
+ }
118
142
 
119
143
  const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => {
120
144
  if (isPresentationMode.value) return
@@ -135,10 +159,17 @@ export function useWaveformHover(context: HoverContext) {
135
159
  const nextPoints = track.seriesList.map((series) =>
136
160
  createHoveredSeriesPoint(series, trackIndex, nearestPoint(series, xValue) ?? null),
137
161
  )
138
- commitHover(nextPoints, trackIndex, xValue, {
139
- x: resolvedChartLeftMargin.value + track.left + pointerX,
140
- y: titleAreaHeight.value + chartTopMargin.value + track.top + pointerY,
141
- })
162
+ const crosshairX = crosshairXForTrack(track, xValue)
163
+ commitHover(
164
+ nextPoints,
165
+ trackIndex,
166
+ xValue,
167
+ crosshairX === undefined ? {} : { [trackIndex]: crosshairX },
168
+ {
169
+ x: resolvedChartLeftMargin.value + track.left + pointerX,
170
+ y: titleAreaHeight.value + chartTopMargin.value + track.top + pointerY,
171
+ },
172
+ )
142
173
  })
143
174
  }
144
175
  const handleSharedPointerMove = (event: PointerEvent) => {
@@ -166,7 +197,13 @@ export function useWaveformHover(context: HoverContext) {
166
197
  createHoveredSeriesPoint(series, track.index, nearestPoint(series, xValue) ?? null),
167
198
  ),
168
199
  )
169
- commitHover(nextPoints, null, xValue, {
200
+ const crosshairXByTrack = Object.fromEntries(
201
+ trackLayouts.value.flatMap((track) => {
202
+ const crosshairX = crosshairXForTrack(track, xValue)
203
+ return crosshairX === undefined ? [] : [[track.index, crosshairX]]
204
+ }),
205
+ )
206
+ commitHover(nextPoints, null, xValue, crosshairXByTrack, {
170
207
  x: resolvedChartLeftMargin.value + pointerX,
171
208
  y: titleAreaHeight.value + chartTopMargin.value + pointerY,
172
209
  })
@@ -20,8 +20,9 @@ const crosshairs = computed<Crosshair[]>(() => {
20
20
 
21
21
  return props.tracks.flatMap((track) => {
22
22
  const trackIsActive = props.state.trackIndex === null || props.state.trackIndex === track.index
23
- return trackIsActive && !track.isEmpty && track.hasVisibleSeries
24
- ? [{ track, x: track.xScale(props.state.queryX!) }]
23
+ const crosshairX = props.state.crosshairXByTrack[track.index]
24
+ return trackIsActive && !track.isEmpty && track.hasVisibleSeries && crosshairX !== undefined
25
+ ? [{ track, x: track.xScale(crosshairX) }]
25
26
  : []
26
27
  })
27
28
  })
@@ -5,63 +5,151 @@ import { flushAnimationFrames } from '../../test/setup'
5
5
  import { mountSizedChart } from '../../test/waveformChart'
6
6
 
7
7
  describe('WaveformChart incomplete data hover', () => {
8
- it('keeps the shared crosshair at the query time and marks out-of-range series unavailable', async () => {
9
- const wrapper = await mountSizedChart(
10
- {
11
- kind: 'series',
12
- series: [
13
- {
14
- name: '完整数据',
15
- data: {
16
- kind: 'points',
17
- points: [
18
- { x: -1, y: 1 },
19
- { x: -0.5, y: 2 },
20
- { x: 1, y: 3 },
21
- ],
8
+ it.each(['separated', 'compact'] as const)(
9
+ 'clamps each track crosshair and tooltip values to data endpoints in %s mode',
10
+ async (displayMode) => {
11
+ const wrapper = await mountSizedChart(
12
+ {
13
+ kind: 'series',
14
+ series: [
15
+ {
16
+ name: '完整数据',
17
+ data: {
18
+ kind: 'points',
19
+ points: [
20
+ { x: -1, y: 1 },
21
+ { x: -0.5, y: 2 },
22
+ { x: 1, y: 3 },
23
+ ],
24
+ },
22
25
  },
23
- },
24
- {
25
- name: '从零开始',
26
- data: {
27
- kind: 'points',
28
- points: [
29
- { x: 0, y: 10 },
30
- { x: 1, y: 20 },
31
- ],
26
+ {
27
+ name: '从零开始',
28
+ data: {
29
+ kind: 'points',
30
+ points: [
31
+ { x: 0, y: 10 },
32
+ { x: 1, y: 20 },
33
+ ],
34
+ },
32
35
  },
33
- },
34
- ],
35
- },
36
- { displayMode: 'separated' },
37
- )
38
- const overlay = wrapper.get('.waveform-chart__overlay--shared')
39
- const overlayWidth = Number(overlay.attributes('width'))
40
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
41
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
42
- })
36
+ ],
37
+ },
38
+ { displayMode },
39
+ )
40
+ const overlay = wrapper.get('.waveform-chart__overlay--shared')
41
+ const overlayWidth = Number(overlay.attributes('width'))
42
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
43
+ value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
44
+ })
43
45
 
44
- overlay.element.dispatchEvent(
45
- new MouseEvent('pointermove', {
46
- clientX: overlayWidth / 4,
47
- clientY: 120,
48
- bubbles: true,
49
- }),
50
- )
51
- flushAnimationFrames()
52
- await flushPromises()
46
+ overlay.element.dispatchEvent(
47
+ new MouseEvent('pointermove', {
48
+ clientX: overlayWidth / 4,
49
+ clientY: 120,
50
+ bubbles: true,
51
+ }),
52
+ )
53
+ flushAnimationFrames()
54
+ await flushPromises()
53
55
 
54
- const crosshairPositions = wrapper
55
- .findAll('.waveform-chart__crosshair line')
56
- .map((line) => Number(line.attributes('x1')))
57
- expect(crosshairPositions).toHaveLength(2)
58
- expect(crosshairPositions[0]).toBeCloseTo(overlayWidth / 4)
59
- expect(crosshairPositions[1]).toBeCloseTo(overlayWidth / 4)
60
- const tooltipRows = wrapper.findAll('.waveform-chart__tooltip-series')
61
- expect(tooltipRows).toHaveLength(2)
62
- expect(tooltipRows[0].text()).toContain('(x:-500 y:2)')
63
- expect(tooltipRows[1].text()).toContain('无数据')
64
- expect(tooltipRows[1].text()).not.toContain('x:')
65
- expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: -0.5, y: 2 }])
66
- })
56
+ const crosshairPositions = wrapper
57
+ .findAll('.waveform-chart__crosshair line')
58
+ .map((line) => Number(line.attributes('x1')))
59
+ expect(crosshairPositions).toHaveLength(2)
60
+ expect(crosshairPositions[0]).toBeCloseTo(overlayWidth / 4)
61
+ expect(crosshairPositions[1]).toBeCloseTo(overlayWidth / 2)
62
+ const tooltipRows = wrapper.findAll('.waveform-chart__tooltip-series')
63
+ expect(tooltipRows).toHaveLength(2)
64
+ expect(tooltipRows[0].text()).toContain('(x:-500 y:2)')
65
+ expect(tooltipRows[1].text()).toContain('(x:0 y:10)')
66
+ expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: -0.5, y: 2 }])
67
+ },
68
+ )
69
+
70
+ it.each(['separated', 'compact'] as const)(
71
+ 'uses the longest series for a shared track crosshair while clamping each tooltip row in %s mode',
72
+ async (displayMode) => {
73
+ const wrapper = await mountSizedChart(
74
+ {
75
+ kind: 'series',
76
+ series: [
77
+ {
78
+ name: '范围参考',
79
+ data: {
80
+ kind: 'points',
81
+ points: [
82
+ { x: -2, y: 1 },
83
+ { x: 2, y: 2 },
84
+ ],
85
+ },
86
+ },
87
+ {
88
+ name: '最长曲线',
89
+ trackId: 'same-track',
90
+ data: {
91
+ kind: 'points',
92
+ points: [
93
+ { x: 0, y: 10 },
94
+ { x: 1, y: 20 },
95
+ ],
96
+ },
97
+ },
98
+ {
99
+ name: '较短曲线',
100
+ trackId: 'same-track',
101
+ data: {
102
+ kind: 'points',
103
+ points: [
104
+ { x: 0.25, y: 30 },
105
+ { x: 0.5, y: 40 },
106
+ ],
107
+ },
108
+ },
109
+ ],
110
+ },
111
+ { displayMode },
112
+ )
113
+ const overlay = wrapper.get('.waveform-chart__overlay--shared')
114
+ const overlayWidth = Number(overlay.attributes('width'))
115
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
116
+ value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
117
+ })
118
+
119
+ overlay.element.dispatchEvent(
120
+ new MouseEvent('pointermove', {
121
+ clientX: overlayWidth / 8,
122
+ clientY: 120,
123
+ bubbles: true,
124
+ }),
125
+ )
126
+ flushAnimationFrames()
127
+ await flushPromises()
128
+
129
+ let crosshairPositions = wrapper
130
+ .findAll('.waveform-chart__crosshair line')
131
+ .map((line) => Number(line.attributes('x1')))
132
+ expect(crosshairPositions).toHaveLength(2)
133
+ expect(crosshairPositions[1]).toBeCloseTo(overlayWidth / 2)
134
+ expect(wrapper.text()).toContain('最长曲线(x:0 y:10)')
135
+ expect(wrapper.text()).toContain('较短曲线(x:250 y:30)')
136
+
137
+ overlay.element.dispatchEvent(
138
+ new MouseEvent('pointermove', {
139
+ clientX: (overlayWidth * 7) / 8,
140
+ clientY: 120,
141
+ bubbles: true,
142
+ }),
143
+ )
144
+ flushAnimationFrames()
145
+ await flushPromises()
146
+
147
+ crosshairPositions = wrapper
148
+ .findAll('.waveform-chart__crosshair line')
149
+ .map((line) => Number(line.attributes('x1')))
150
+ expect(crosshairPositions[1]).toBeCloseTo((overlayWidth * 3) / 4)
151
+ expect(wrapper.text()).toContain('最长曲线(x:1,000 y:20)')
152
+ expect(wrapper.text()).toContain('较短曲线(x:500 y:40)')
153
+ },
154
+ )
67
155
  })
@@ -86,7 +86,7 @@ describe('WaveformChart', () => {
86
86
  expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
87
87
  })
88
88
 
89
- it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
89
+ it('updates the Y-axis label position while preserving the chart gutter between pages', async () => {
90
90
  const wrapper = await mountSizedChart(
91
91
  {
92
92
  kind: 'series',
@@ -118,14 +118,12 @@ describe('WaveformChart', () => {
118
118
  { grid: { rowCount: 1, columnCount: 1 } },
119
119
  )
120
120
  const initialMargin = wrapper.attributes('data-chart-left-margin')
121
- const initialLabelX = wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')
121
+ expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-34')
122
122
 
123
123
  await wrapper.get('.ant-pagination-next button').trigger('click')
124
124
 
125
125
  expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
126
- expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe(
127
- initialLabelX,
128
- )
126
+ expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-62')
129
127
  })
130
128
 
131
129
  it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { mountSizedChart } from '../../test/waveformChart'
4
+
5
+ describe('WaveformChart multi-axis labels', () => {
6
+ it('aligns single-axis and multi-axis Y-axis titles in one column', async () => {
7
+ const wrapper = await mountSizedChart(
8
+ {
9
+ kind: 'series',
10
+ series: [
11
+ {
12
+ id: 'single',
13
+ trackId: 'single-track',
14
+ name: '单轴',
15
+ data: {
16
+ kind: 'points',
17
+ points: [
18
+ { x: 0, y: -1e120 },
19
+ { x: 1, y: 1e120 },
20
+ ],
21
+ },
22
+ },
23
+ {
24
+ id: 'left',
25
+ trackId: 'multi-track',
26
+ name: '左轴',
27
+ data: {
28
+ kind: 'points',
29
+ points: [
30
+ { x: 0, y: 0 },
31
+ { x: 1, y: 1 },
32
+ ],
33
+ },
34
+ },
35
+ {
36
+ id: 'right',
37
+ trackId: 'multi-track',
38
+ name: '右轴',
39
+ data: {
40
+ kind: 'points',
41
+ points: [
42
+ { x: 0, y: 1000 },
43
+ { x: 1, y: 3000 },
44
+ ],
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ { overlayMode: 'multi-axis', grid: { rowCount: 2, columnCount: 1 } },
50
+ )
51
+ const tracks = wrapper.findAll('.waveform-chart__track')
52
+ const titleX = (transform: string | undefined) =>
53
+ Number(transform?.match(/translate\(([^,]+)/)?.[1])
54
+
55
+ expect(titleX(tracks[0]!.get('.waveform-chart__y-axis-label').attributes('transform'))).toBe(
56
+ titleX(
57
+ tracks[1]!.findAll('.waveform-track__multi-axis-title text')[0]!.attributes('transform'),
58
+ ),
59
+ )
60
+ })
61
+ })
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
3
3
  import { createSimulatedWaveformData } from './simulatedWaveforms'
4
4
 
5
5
  describe('simulated waveform data', () => {
6
- it('creates deterministic, finite seven-channel data with a two-series second frame', () => {
6
+ it('creates deterministic, finite eight-channel data with a three-series second frame', () => {
7
7
  const first = createSimulatedWaveformData()
8
8
  const second = createSimulatedWaveformData()
9
9
 
@@ -11,12 +11,12 @@ describe('simulated waveform data', () => {
11
11
  expect(first.kind).toBe('series')
12
12
  if (first.kind !== 'series') return
13
13
 
14
- expect(first.series).toHaveLength(7)
15
- expect(new Set(first.series.map((series) => series.id)).size).toBe(7)
14
+ expect(first.series).toHaveLength(8)
15
+ expect(new Set(first.series.map((series) => series.id)).size).toBe(8)
16
16
  const secondFrame = first.series.filter(
17
17
  (series) => series.trackId === 'simulated-harmonic-frame',
18
18
  )
19
- expect(secondFrame).toHaveLength(2)
19
+ expect(secondFrame).toHaveLength(3)
20
20
  expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
21
21
  const firstSeries = first.series[0]
22
22
  expect(firstSeries).toMatchObject({
@@ -84,6 +84,18 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
84
84
  signal: (time) =>
85
85
  0.62 * Math.sin(TWO_PI * 0.55 * time + 0.55) + 0.18 * Math.sin(TWO_PI * 2.2 * time - 0.2),
86
86
  },
87
+ {
88
+ id: 'simulated-harmonic-correction',
89
+ trackId: 'simulated-harmonic-frame',
90
+ shotNo: '13300',
91
+ name: '谐波校正',
92
+ unit: 'V',
93
+ color: '#722ed1',
94
+ lineType: 'linear',
95
+ pointType: 'none',
96
+ signal: (time) =>
97
+ 0.48 * Math.sin(TWO_PI * 0.55 * time - 0.35) + 0.12 * Math.sin(TWO_PI * 2.2 * time + 0.7),
98
+ },
87
99
  {
88
100
  id: 'simulated-damped',
89
101
  shotNo: '13300',