react-native-livechart 4.24.1 → 4.25.0
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/dist/components/DegenParticlesOverlay.d.ts +2 -2
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts +5 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +3 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +3 -3
- package/dist/core/historyRangeCache.d.ts +25 -0
- package/dist/core/historyRangeCache.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +4 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +1 -1
- package/dist/core/useHistoryRevision.d.ts +5 -0
- package/dist/core/useHistoryRevision.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +17 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +2 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candleBodyPath.d.ts +10 -0
- package/dist/draw/candleBodyPath.d.ts.map +1 -0
- package/dist/draw/lineFillPaths.d.ts +10 -0
- package/dist/draw/lineFillPaths.d.ts.map +1 -0
- package/dist/draw/volume.d.ts +4 -1
- package/dist/draw/volume.d.ts.map +1 -1
- package/dist/draw/volumeRange.d.ts +12 -0
- package/dist/draw/volumeRange.d.ts.map +1 -0
- package/dist/hooks/useCandlePaths.d.ts +1 -1
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +1 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +1 -0
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useDemandFrameLoop.d.ts +11 -0
- package/dist/hooks/useDemandFrameLoop.d.ts.map +1 -0
- package/dist/hooks/useLoadingLook.d.ts +12 -0
- package/dist/hooks/useLoadingLook.d.ts.map +1 -0
- package/dist/hooks/useMarkers.d.ts +3 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesDegen.d.ts +1 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +2 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -1
- package/dist/hooks/usePinchZoom.d.ts +2 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -1
- package/dist/hooks/useVolumeRange.d.ts +14 -0
- package/dist/hooks/useVolumeRange.d.ts.map +1 -0
- package/dist/hooks/useXAxis.d.ts +7 -0
- package/dist/hooks/useXAxis.d.ts.map +1 -1
- package/dist/hooks/useYAxis.d.ts.map +1 -1
- package/dist/math/spline.d.ts +3 -1
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +8 -0
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/DegenParticlesOverlay.tsx +5 -5
- package/src/components/LiveChart.tsx +34 -4
- package/src/components/LiveChartSeries.tsx +14 -6
- package/src/components/LoadingOverlay.tsx +53 -12
- package/src/components/YAxisOverlay.tsx +38 -2
- package/src/constants.ts +3 -3
- package/src/core/historyRangeCache.ts +109 -0
- package/src/core/liveChartEngineTick.ts +24 -57
- package/src/core/resolveConfig.ts +1 -1
- package/src/core/useHistoryRevision.ts +36 -0
- package/src/core/useLiveChartEngine.ts +151 -64
- package/src/core/useLiveChartSeriesEngine.ts +16 -1
- package/src/draw/candleBodyPath.ts +38 -0
- package/src/draw/lineFillPaths.ts +63 -0
- package/src/draw/volume.ts +21 -23
- package/src/draw/volumeRange.ts +51 -0
- package/src/hooks/useCandlePaths.ts +68 -94
- package/src/hooks/useChartPaths.ts +87 -111
- package/src/hooks/useDegen.ts +10 -6
- package/src/hooks/useDemandFrameLoop.ts +82 -0
- package/src/hooks/useLoadingLook.ts +30 -0
- package/src/hooks/useMarkers.ts +18 -4
- package/src/hooks/useMultiSeriesDegen.ts +7 -3
- package/src/hooks/usePanScroll.ts +5 -0
- package/src/hooks/usePinchZoom.ts +5 -0
- package/src/hooks/useVolumeRange.ts +40 -0
- package/src/hooks/useXAxis.ts +27 -3
- package/src/hooks/useYAxis.ts +18 -2
- package/src/math/spline.ts +16 -8
- package/src/math/squiggly.ts +12 -0
- package/src/types.ts +11 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
3
2
|
import {
|
|
4
3
|
useDerivedValue,
|
|
5
4
|
useFrameCallback,
|
|
@@ -8,12 +7,15 @@ import {
|
|
|
8
7
|
} from "react-native-reanimated";
|
|
9
8
|
import { CANDLE_METRICS_DEFAULTS, MS_PER_FRAME_60FPS } from "../constants";
|
|
10
9
|
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
10
|
+
import { buildCandleBodyPath } from "../draw/candleBodyPath";
|
|
11
11
|
import { buildCandleGeometry } from "../draw/candle";
|
|
12
12
|
import { buildVolumeGeometry } from "../draw/volume";
|
|
13
13
|
import type { ChartPadding } from "../draw/line";
|
|
14
14
|
import { lerp } from "../math/lerp";
|
|
15
15
|
import type { CandleMetrics, CandlePoint } from "../types";
|
|
16
|
+
import { useVolumeRange } from "./useVolumeRange";
|
|
16
17
|
import { usePathBuilder } from "./usePathBuilder";
|
|
18
|
+
import { useDemandFrameLoop } from "./useDemandFrameLoop";
|
|
17
19
|
|
|
18
20
|
const CANDLE_WIDTH_LERP_SPEED = 0.08;
|
|
19
21
|
|
|
@@ -40,6 +42,7 @@ export function useCandleWidthLerp(
|
|
|
40
42
|
active: boolean,
|
|
41
43
|
/** Runtime gate shared with the chart engine. */
|
|
42
44
|
isFrameLoopActive?: SharedValue<boolean>,
|
|
45
|
+
autoSleep = false,
|
|
43
46
|
): SharedValue<number> {
|
|
44
47
|
const targetCandleWidth = useDerivedValue(() => candleWidthSecs);
|
|
45
48
|
const displayCandleWidth = useSharedValue(candleWidthSecs);
|
|
@@ -51,10 +54,10 @@ export function useCandleWidthLerp(
|
|
|
51
54
|
() => candleLerpSpeed ?? CANDLE_WIDTH_LERP_SPEED,
|
|
52
55
|
);
|
|
53
56
|
|
|
54
|
-
const
|
|
57
|
+
const tickWidth = useCallback((dt: number) => {
|
|
55
58
|
"worklet";
|
|
56
|
-
if (!active || isFrameLoopActive?.get() === false) return;
|
|
57
|
-
const
|
|
59
|
+
if (!active || isFrameLoopActive?.get() === false) return false;
|
|
60
|
+
const previous = displayCandleWidth.get();
|
|
58
61
|
const target = targetCandleWidth.get();
|
|
59
62
|
const next = lerp(
|
|
60
63
|
displayCandleWidth.get(),
|
|
@@ -62,12 +65,24 @@ export function useCandleWidthLerp(
|
|
|
62
65
|
widthLerpSpeed.get(),
|
|
63
66
|
dt,
|
|
64
67
|
);
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const settled = Math.abs(target - next) < 0.01 ? target : next;
|
|
69
|
+
displayCandleWidth.set(settled);
|
|
70
|
+
return previous !== settled;
|
|
71
|
+
}, [active, isFrameLoopActive, displayCandleWidth, targetCandleWidth, widthLerpSpeed]);
|
|
72
|
+
const prepareWidth = useCallback(() => {
|
|
73
|
+
"worklet";
|
|
74
|
+
return [targetCandleWidth.get(), widthLerpSpeed.get(), isFrameLoopActive?.get()];
|
|
75
|
+
}, [targetCandleWidth, widthLerpSpeed, isFrameLoopActive]);
|
|
76
|
+
useDemandFrameLoop(autoSleep && autostart && active, prepareWidth, tickWidth);
|
|
77
|
+
const legacyActive = autostart && !autoSleep;
|
|
78
|
+
const widthFrameCallback = useFrameCallback((frameInfo) => {
|
|
79
|
+
"worklet";
|
|
80
|
+
tickWidth(frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS);
|
|
81
|
+
}, legacyActive);
|
|
67
82
|
useEffect(() => {
|
|
68
83
|
// `autostart` is an initial seed in Reanimated, not a reactive switch.
|
|
69
|
-
widthFrameCallback.setActive(
|
|
70
|
-
}, [
|
|
84
|
+
widthFrameCallback.setActive(legacyActive);
|
|
85
|
+
}, [legacyActive, widthFrameCallback]);
|
|
71
86
|
|
|
72
87
|
return displayCandleWidth;
|
|
73
88
|
}
|
|
@@ -118,50 +133,24 @@ export function useCandlePaths(
|
|
|
118
133
|
});
|
|
119
134
|
|
|
120
135
|
/* istanbul ignore next -- worklet */
|
|
121
|
-
const upBodiesPath = useDerivedValue(() =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (rr > 0) {
|
|
130
|
-
b.addRRect({
|
|
131
|
-
rect: { x: bd.x, y: bd.y, width: bd.w, height: bd.h },
|
|
132
|
-
rx: rr,
|
|
133
|
-
ry: rr,
|
|
134
|
-
});
|
|
135
|
-
} else {
|
|
136
|
-
b.addRect(Skia.XYWHRect(bd.x, bd.y, bd.w, bd.h));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return b.detach();
|
|
141
|
-
});
|
|
136
|
+
const upBodiesPath = useDerivedValue(() =>
|
|
137
|
+
buildCandleBodyPath(
|
|
138
|
+
upBodiesBuilder.get(),
|
|
139
|
+
geometry.get().bodies,
|
|
140
|
+
true,
|
|
141
|
+
candleMetrics.bodyRadius,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
142
144
|
|
|
143
145
|
/* istanbul ignore next -- worklet */
|
|
144
|
-
const downBodiesPath = useDerivedValue(() =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (rr > 0) {
|
|
153
|
-
b.addRRect({
|
|
154
|
-
rect: { x: bd.x, y: bd.y, width: bd.w, height: bd.h },
|
|
155
|
-
rx: rr,
|
|
156
|
-
ry: rr,
|
|
157
|
-
});
|
|
158
|
-
} else {
|
|
159
|
-
b.addRect(Skia.XYWHRect(bd.x, bd.y, bd.w, bd.h));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return b.detach();
|
|
164
|
-
});
|
|
146
|
+
const downBodiesPath = useDerivedValue(() =>
|
|
147
|
+
buildCandleBodyPath(
|
|
148
|
+
downBodiesBuilder.get(),
|
|
149
|
+
geometry.get().bodies,
|
|
150
|
+
false,
|
|
151
|
+
candleMetrics.bodyRadius,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
165
154
|
|
|
166
155
|
/* istanbul ignore next -- worklet */
|
|
167
156
|
const upWicksPath = useDerivedValue(() => {
|
|
@@ -192,6 +181,13 @@ export function useCandlePaths(
|
|
|
192
181
|
// Volume bars share the candle window + (lerped) candle width so each bar sits
|
|
193
182
|
// directly under its candle body. Empty unless a volume band is reserved.
|
|
194
183
|
/* istanbul ignore next -- worklet */
|
|
184
|
+
const volumeRange = useVolumeRange(
|
|
185
|
+
candles,
|
|
186
|
+
engine.timestamp,
|
|
187
|
+
engine.displayWindow,
|
|
188
|
+
displayCandleWidth,
|
|
189
|
+
active && volumeBandHeight > 0,
|
|
190
|
+
);
|
|
195
191
|
const volumeGeometry = useDerivedValue(() => {
|
|
196
192
|
if (!active || !candles || volumeBandHeight <= 0) return { bars: [] };
|
|
197
193
|
return buildVolumeGeometry(
|
|
@@ -205,54 +201,32 @@ export function useCandlePaths(
|
|
|
205
201
|
volumeBandHeight,
|
|
206
202
|
displayCandleWidth.get(),
|
|
207
203
|
candleMetrics,
|
|
204
|
+
{
|
|
205
|
+
start: volumeRange.start.get(),
|
|
206
|
+
end: volumeRange.end.get(),
|
|
207
|
+
max: volumeRange.max.get(),
|
|
208
|
+
},
|
|
208
209
|
);
|
|
209
210
|
});
|
|
210
211
|
|
|
211
212
|
/* istanbul ignore next -- worklet */
|
|
212
|
-
const upBarsPath = useDerivedValue(() =>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (rr > 0) {
|
|
221
|
-
b.addRRect({
|
|
222
|
-
rect: { x: bar.x, y: bar.y, width: bar.w, height: bar.h },
|
|
223
|
-
rx: rr,
|
|
224
|
-
ry: rr,
|
|
225
|
-
});
|
|
226
|
-
} else {
|
|
227
|
-
b.addRect(Skia.XYWHRect(bar.x, bar.y, bar.w, bar.h));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return b.detach();
|
|
232
|
-
});
|
|
233
|
-
|
|
213
|
+
const upBarsPath = useDerivedValue(() =>
|
|
214
|
+
buildCandleBodyPath(
|
|
215
|
+
upBarsBuilder.get(),
|
|
216
|
+
volumeGeometry.get().bars,
|
|
217
|
+
true,
|
|
218
|
+
volumeRadius,
|
|
219
|
+
),
|
|
220
|
+
);
|
|
234
221
|
/* istanbul ignore next -- worklet */
|
|
235
|
-
const downBarsPath = useDerivedValue(() =>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (rr > 0) {
|
|
244
|
-
b.addRRect({
|
|
245
|
-
rect: { x: bar.x, y: bar.y, width: bar.w, height: bar.h },
|
|
246
|
-
rx: rr,
|
|
247
|
-
ry: rr,
|
|
248
|
-
});
|
|
249
|
-
} else {
|
|
250
|
-
b.addRect(Skia.XYWHRect(bar.x, bar.y, bar.w, bar.h));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return b.detach();
|
|
255
|
-
});
|
|
222
|
+
const downBarsPath = useDerivedValue(() =>
|
|
223
|
+
buildCandleBodyPath(
|
|
224
|
+
downBarsBuilder.get(),
|
|
225
|
+
volumeGeometry.get().bars,
|
|
226
|
+
false,
|
|
227
|
+
volumeRadius,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
256
230
|
|
|
257
231
|
return {
|
|
258
232
|
upBodiesPath,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildLineFillPaths } from "../draw/lineFillPaths";
|
|
1
2
|
import { Skia, type SkPath } from "@shopify/react-native-skia";
|
|
2
3
|
import { useRef } from "react";
|
|
3
4
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
@@ -8,13 +9,14 @@ import type {
|
|
|
8
9
|
} from "../core/useLiveChartEngine";
|
|
9
10
|
import { buildLinePoints, type ChartPadding } from "../draw/line";
|
|
10
11
|
import { buildLineGapSegmentRanges } from "../draw/lineGap";
|
|
12
|
+
import { makeLineSimplifyScratch, simplifyLinePoints } from "../math/simplify";
|
|
13
|
+
import { makeSplineScratch } from "../math/spline";
|
|
14
|
+
import { thresholdSampleSpanX } from "../math/threshold";
|
|
11
15
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { sampleThresholdYAt, thresholdSampleSpanX } from "../math/threshold";
|
|
17
|
-
import { blendPtsY, squigglifyPts } from "../math/squiggly";
|
|
16
|
+
blendPtsY,
|
|
17
|
+
squiggleClockSeconds,
|
|
18
|
+
squigglifyPts,
|
|
19
|
+
} from "../math/squiggly";
|
|
18
20
|
import { usePathBuilder } from "./usePathBuilder";
|
|
19
21
|
import type { CandleGap } from "../types";
|
|
20
22
|
|
|
@@ -31,7 +33,7 @@ export function resolveLineTipValue(
|
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Builds the `linePath` / `fillPath` with `Skia.PathBuilder`s reused across
|
|
34
|
-
*
|
|
36
|
+
* geometry updates (one per curve, held in a SharedValue) and finalized with `detach()` —
|
|
35
37
|
* which returns a fresh immutable `SkPath` each frame and resets the builder.
|
|
36
38
|
* The fresh reference makes Reanimated notify subscribers (re-record + repaint)
|
|
37
39
|
* without the two-SkPath ping-pong the mutable-path pool needed.
|
|
@@ -69,6 +71,12 @@ export function useChartPaths(
|
|
|
69
71
|
|
|
70
72
|
const cacheRef = useRef<{
|
|
71
73
|
emptyPath: SkPath;
|
|
74
|
+
lastPts: number[] | null;
|
|
75
|
+
lastRanges: number[] | null;
|
|
76
|
+
lastLinear: boolean;
|
|
77
|
+
lastBottom: number;
|
|
78
|
+
lineOutput: SkPath | null;
|
|
79
|
+
fillOutput: SkPath | null;
|
|
72
80
|
ptsA: number[];
|
|
73
81
|
ptsB: number[];
|
|
74
82
|
rawPts: number[];
|
|
@@ -87,6 +95,12 @@ export function useChartPaths(
|
|
|
87
95
|
if (cacheRef.current === null) {
|
|
88
96
|
cacheRef.current = {
|
|
89
97
|
emptyPath: Skia.Path.Make(),
|
|
98
|
+
lastPts: null,
|
|
99
|
+
lastRanges: null,
|
|
100
|
+
lastLinear: false,
|
|
101
|
+
lastBottom: NaN,
|
|
102
|
+
lineOutput: null,
|
|
103
|
+
fillOutput: null,
|
|
90
104
|
ptsA: [] as number[],
|
|
91
105
|
ptsB: [] as number[],
|
|
92
106
|
rawPts: [] as number[],
|
|
@@ -160,9 +174,11 @@ export function useChartPaths(
|
|
|
160
174
|
// Compute squiggly Y values at the same X positions as the real line
|
|
161
175
|
const centerY =
|
|
162
176
|
(engine.canvasHeight.get() - padding.bottom + padding.top) / 2;
|
|
177
|
+
// Same wall clock as the loading shell's squiggle, so the wave stays
|
|
178
|
+
// continuous where the shell hands over to the morph.
|
|
163
179
|
const squigglyPts = squigglifyPts(
|
|
164
180
|
renderPts,
|
|
165
|
-
|
|
181
|
+
squiggleClockSeconds(),
|
|
166
182
|
centerY,
|
|
167
183
|
squiggleAmplitude,
|
|
168
184
|
squiggleSpeed,
|
|
@@ -200,117 +216,77 @@ export function useChartPaths(
|
|
|
200
216
|
);
|
|
201
217
|
});
|
|
202
218
|
|
|
203
|
-
|
|
219
|
+
// One derived rebuild keeps curve inputs and all outputs in the same revision.
|
|
220
|
+
// The spline solver emits to separate builders without extra command buffers.
|
|
221
|
+
const paths = useDerivedValue(() => {
|
|
204
222
|
const cache = cacheRef.current!;
|
|
205
223
|
const pts = flatPts.get();
|
|
206
|
-
const n = pts.length >> 1;
|
|
207
|
-
if (n < 2) return cache.emptyPath;
|
|
208
|
-
const b = lineBuilder.value;
|
|
209
224
|
const ranges = segmentRanges.get();
|
|
210
|
-
if (ranges.length === 0)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
if (pts.length < 4 || ranges.length === 0) {
|
|
226
|
+
cache.lastPts = null;
|
|
227
|
+
return {
|
|
228
|
+
line: cache.emptyPath,
|
|
229
|
+
fill: cache.emptyPath,
|
|
230
|
+
band: cache.emptyPath,
|
|
231
|
+
};
|
|
216
232
|
}
|
|
217
|
-
return b.detach();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const fillPath = useDerivedValue(() => {
|
|
221
|
-
const cache = cacheRef.current!;
|
|
222
|
-
const pts = flatPts.get();
|
|
223
|
-
const n = pts.length >> 1;
|
|
224
|
-
if (n < 2) return cache.emptyPath;
|
|
225
|
-
const b = fillBuilder.value;
|
|
226
|
-
const ranges = segmentRanges.get();
|
|
227
|
-
if (ranges.length === 0) return cache.emptyPath;
|
|
228
233
|
const bottom = engine.canvasHeight.get() - padding.bottom;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// The samples live on the time-anchored, gliding grid — interpolate them
|
|
268
|
-
// across that span (same as the shader), not the static plot edges.
|
|
269
|
-
const [x0, x1] = thresholdSampleSpanX(
|
|
270
|
-
engine.timestamp.get(),
|
|
271
|
-
engine.displayWindow.get(),
|
|
272
|
-
padding.left,
|
|
273
|
-
engine.canvasWidth.get() - padding.right,
|
|
274
|
-
count,
|
|
234
|
+
// Buffers alternate on each geometry update. Identical references here
|
|
235
|
+
// therefore mean only the threshold changed, not the line geometry.
|
|
236
|
+
const bandOnly =
|
|
237
|
+
cache.lastPts === pts &&
|
|
238
|
+
cache.lastRanges === ranges &&
|
|
239
|
+
cache.lastLinear === linear &&
|
|
240
|
+
cache.lastBottom === bottom;
|
|
241
|
+
const samples = thresholdSamples?.get();
|
|
242
|
+
const sampled = samples != null && samples.length >= 2;
|
|
243
|
+
const y = thresholdY?.get() ?? NaN;
|
|
244
|
+
const hasBand = sampled || Number.isFinite(y);
|
|
245
|
+
const span = sampled
|
|
246
|
+
? thresholdSampleSpanX(
|
|
247
|
+
engine.timestamp.get(),
|
|
248
|
+
engine.displayWindow.get(),
|
|
249
|
+
padding.left,
|
|
250
|
+
engine.canvasWidth.get() - padding.right,
|
|
251
|
+
samples.length,
|
|
252
|
+
)
|
|
253
|
+
: [0, 0];
|
|
254
|
+
const line = lineBuilder.get(),
|
|
255
|
+
fill = fillBuilder.get();
|
|
256
|
+
const band = hasBand ? thresholdFillBuilder.get() : undefined;
|
|
257
|
+
if (!bandOnly || band)
|
|
258
|
+
buildLineFillPaths(
|
|
259
|
+
line,
|
|
260
|
+
fill,
|
|
261
|
+
band,
|
|
262
|
+
pts,
|
|
263
|
+
ranges,
|
|
264
|
+
cache.scratch,
|
|
265
|
+
linear,
|
|
266
|
+
bottom,
|
|
267
|
+
y,
|
|
268
|
+
samples,
|
|
269
|
+
span[0],
|
|
270
|
+
span[1],
|
|
271
|
+
bandOnly,
|
|
275
272
|
);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const end = ranges[range + 1];
|
|
280
|
-
const first = start * 2;
|
|
281
|
-
const last = (end - 1) * 2;
|
|
282
|
-
const leftX = pts[first];
|
|
283
|
-
const rightX = pts[last];
|
|
284
|
-
b.moveTo(leftX, pts[first + 1]);
|
|
285
|
-
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
286
|
-
b.lineTo(rightX, sampleThresholdYAt(tsamples, x0, x1, rightX));
|
|
287
|
-
for (let i = count - 1; i >= 0; i--) {
|
|
288
|
-
const sx = x0 + step * i;
|
|
289
|
-
if (sx > leftX && sx < rightX) b.lineTo(sx, tsamples[i]);
|
|
290
|
-
}
|
|
291
|
-
b.lineTo(leftX, sampleThresholdYAt(tsamples, x0, x1, leftX));
|
|
292
|
-
b.close();
|
|
293
|
-
}
|
|
294
|
-
return b.detach();
|
|
273
|
+
if (!bandOnly) {
|
|
274
|
+
cache.lineOutput = line.detach();
|
|
275
|
+
cache.fillOutput = fill.detach();
|
|
295
276
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const last = (end - 1) * 2;
|
|
306
|
-
b.moveTo(pts[first], pts[first + 1]);
|
|
307
|
-
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
308
|
-
b.lineTo(pts[last], yT);
|
|
309
|
-
b.lineTo(pts[first], yT);
|
|
310
|
-
b.close();
|
|
311
|
-
}
|
|
312
|
-
return b.detach();
|
|
277
|
+
cache.lastPts = pts;
|
|
278
|
+
cache.lastRanges = ranges;
|
|
279
|
+
cache.lastLinear = linear;
|
|
280
|
+
cache.lastBottom = bottom;
|
|
281
|
+
return {
|
|
282
|
+
line: cache.lineOutput!,
|
|
283
|
+
fill: cache.fillOutput!,
|
|
284
|
+
band: band?.detach() ?? cache.emptyPath,
|
|
285
|
+
};
|
|
313
286
|
});
|
|
287
|
+
const linePath = useDerivedValue(() => paths.get().line);
|
|
288
|
+
const fillPath = useDerivedValue(() => paths.get().fill);
|
|
289
|
+
const thresholdFillPath = useDerivedValue(() => paths.get().band);
|
|
314
290
|
|
|
315
291
|
return { linePath, fillPath, thresholdFillPath } as const;
|
|
316
292
|
}
|
package/src/hooks/useDegen.ts
CHANGED
|
@@ -50,6 +50,7 @@ export function useDegen(
|
|
|
50
50
|
): {
|
|
51
51
|
pack: SharedValue<Float64Array<ArrayBuffer>>;
|
|
52
52
|
packRevision: SharedValue<number>;
|
|
53
|
+
particleTimestamp: SharedValue<number>;
|
|
53
54
|
shakeTransform: DerivedValue<
|
|
54
55
|
[{ translateX: number }, { translateY: number }]
|
|
55
56
|
>;
|
|
@@ -57,6 +58,7 @@ export function useDegen(
|
|
|
57
58
|
const MAX_SLOTS = 80;
|
|
58
59
|
const pack = useSharedValue(new Float64Array(MAX_SLOTS * DEGEN_STRIDE));
|
|
59
60
|
const packRevision = useSharedValue(0);
|
|
61
|
+
const particleTimestamp = useSharedValue(0);
|
|
60
62
|
const prevM = useSharedValue<Momentum>("flat");
|
|
61
63
|
const writeRot = useSharedValue(0);
|
|
62
64
|
const enabledSV = useSharedValue(0);
|
|
@@ -185,10 +187,12 @@ export function useDegen(
|
|
|
185
187
|
// ChartWithDegen only mounts while the resolved effect is enabled; static
|
|
186
188
|
// charts force that config to null and unmount this callback entirely.
|
|
187
189
|
useFrameCallback(
|
|
188
|
-
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
190
|
+
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (frame) => {
|
|
189
191
|
"worklet";
|
|
190
192
|
if (isFrameLoopActive?.get() === false) return;
|
|
191
|
-
|
|
193
|
+
// The viewport clock advances in half-pixel steps and may be pinned by
|
|
194
|
+
// nowOverride or pan. Effects need the continuous display-frame clock.
|
|
195
|
+
const now = frame.timestamp / 1000;
|
|
192
196
|
const buf = pack.get();
|
|
193
197
|
const slots = slotCountSV.get();
|
|
194
198
|
|
|
@@ -273,10 +277,10 @@ export function useDegen(
|
|
|
273
277
|
);
|
|
274
278
|
|
|
275
279
|
// Repaint only while particles are alive (or on the frame they all expire,
|
|
276
|
-
// so the overlay clears).
|
|
277
|
-
//
|
|
278
|
-
// worklet churn for an idle particle system.
|
|
280
|
+
// so the overlay clears). Publish the same clock for Atlas size/opacity,
|
|
281
|
+
// only while particles are alive, to keep an idle field from repainting.
|
|
279
282
|
if (activeCount > 0 || prevActiveCount.get() > 0) {
|
|
283
|
+
particleTimestamp.set(now);
|
|
280
284
|
packRevision.set(packRevision.get() + 1);
|
|
281
285
|
}
|
|
282
286
|
prevActiveCount.set(activeCount);
|
|
@@ -291,5 +295,5 @@ export function useDegen(
|
|
|
291
295
|
];
|
|
292
296
|
});
|
|
293
297
|
|
|
294
|
-
return { pack, packRevision, shakeTransform };
|
|
298
|
+
return { pack, packRevision, particleTimestamp, shakeTransform };
|
|
295
299
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
|
|
3
|
+
import { scheduleOnUI } from "react-native-worklets";
|
|
4
|
+
|
|
5
|
+
/** Wait through several unchanged frames, rather than mistaking one quiet frame for rest. */
|
|
6
|
+
export const IDLE_SETTLE_MS = 160;
|
|
7
|
+
|
|
8
|
+
function noInputs() { "worklet"; return null; }
|
|
9
|
+
function noReaction() { "worklet"; }
|
|
10
|
+
|
|
11
|
+
export function nextQuietTime(previous: number, changed: boolean, dt: number): number {
|
|
12
|
+
"worklet";
|
|
13
|
+
return changed ? 0 : previous + Math.min(Math.max(dt, 0), 34);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Opt-in UI-runtime RAF loop. Inputs wake it without a JS-thread round trip.
|
|
18
|
+
* `tick` returns true while work changed or a continuous effect needs frames.
|
|
19
|
+
* `prepare` must read INPUTS only: subscribing to tick outputs causes feedback.
|
|
20
|
+
* A generation token makes queued frames harmless after replacement/unmount.
|
|
21
|
+
*/
|
|
22
|
+
export function useDemandFrameLoop(
|
|
23
|
+
enabled: boolean,
|
|
24
|
+
prepare: () => unknown,
|
|
25
|
+
tick: (dt: number) => boolean,
|
|
26
|
+
): () => void {
|
|
27
|
+
const mounted = useSharedValue(false);
|
|
28
|
+
const running = useSharedValue(false);
|
|
29
|
+
const generation = useSharedValue(0);
|
|
30
|
+
const quietTime = useSharedValue(0);
|
|
31
|
+
|
|
32
|
+
const wake = useCallback(() => {
|
|
33
|
+
"worklet";
|
|
34
|
+
if (!enabled || !mounted.get()) return;
|
|
35
|
+
quietTime.set(0);
|
|
36
|
+
if (running.get()) return;
|
|
37
|
+
running.set(true);
|
|
38
|
+
const token = generation.get();
|
|
39
|
+
let previous: number | null = null;
|
|
40
|
+
function step(timestamp: number) {
|
|
41
|
+
"worklet";
|
|
42
|
+
if (!mounted.get() || generation.get() !== token) return;
|
|
43
|
+
// Resuming never integrates the entire sleeping interval as a single frame.
|
|
44
|
+
const dt = previous === null ? 1000 / 60 : Math.min(timestamp - previous, 34);
|
|
45
|
+
previous = timestamp;
|
|
46
|
+
const quiet = nextQuietTime(quietTime.get(), tick(dt), dt);
|
|
47
|
+
quietTime.set(quiet);
|
|
48
|
+
if (quiet < IDLE_SETTLE_MS) requestAnimationFrame(step);
|
|
49
|
+
else running.set(false);
|
|
50
|
+
}
|
|
51
|
+
requestAnimationFrame(step);
|
|
52
|
+
}, [enabled, mounted, running, generation, quietTime, tick]);
|
|
53
|
+
|
|
54
|
+
useAnimatedReaction(
|
|
55
|
+
// Pass the worklet itself: Reanimated extracts SharedValue subscriptions
|
|
56
|
+
// from its closure. Wrapping it in () => prepare() hides those inputs.
|
|
57
|
+
enabled ? prepare : noInputs,
|
|
58
|
+
enabled ? wake : noReaction,
|
|
59
|
+
[enabled, enabled ? prepare : noInputs, enabled ? wake : noReaction],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!enabled) return;
|
|
64
|
+
scheduleOnUI(() => {
|
|
65
|
+
"worklet";
|
|
66
|
+
generation.set(generation.get() + 1);
|
|
67
|
+
running.set(false);
|
|
68
|
+
mounted.set(true);
|
|
69
|
+
wake();
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
scheduleOnUI(() => {
|
|
73
|
+
"worklet";
|
|
74
|
+
mounted.set(false);
|
|
75
|
+
generation.set(generation.get() + 1);
|
|
76
|
+
running.set(false);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}, [enabled, generation, mounted, running, wake]);
|
|
80
|
+
|
|
81
|
+
return wake;
|
|
82
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ResolvedLoadingConfig } from "../core/resolveConfig";
|
|
4
|
+
|
|
5
|
+
/** Field by field, with `Object.is` (a `NaN` must equal itself, or the render
|
|
6
|
+
* below would never settle), over every field the config has. */
|
|
7
|
+
function sameLook(a: ResolvedLoadingConfig, b: ResolvedLoadingConfig | null) {
|
|
8
|
+
if (b === null) return false;
|
|
9
|
+
const keys = Object.keys(a) as (keyof ResolvedLoadingConfig)[];
|
|
10
|
+
return keys.every((key) => Object.is(a[key], b[key]));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The loading shell's look: the live `loading` config while loading, and the
|
|
15
|
+
* last one after. The shell fades out and the reveal morph melts its squiggle
|
|
16
|
+
* into the line only once `loading` has turned off, when `resolveLoading`
|
|
17
|
+
* already returns `null` — so they would otherwise snap to the default color,
|
|
18
|
+
* stroke, wave and Y-axis placeholders just as the data arrives. The empty
|
|
19
|
+
* shell ("No data") shown later keeps it too; before any loading, it has none
|
|
20
|
+
* and the defaults apply.
|
|
21
|
+
*/
|
|
22
|
+
export function useLoadingLook(
|
|
23
|
+
loadingCfg: ResolvedLoadingConfig | null,
|
|
24
|
+
): ResolvedLoadingConfig | null {
|
|
25
|
+
const [last, setLast] = useState(loadingCfg);
|
|
26
|
+
// Stored from render (React's "previous props" pattern), compared by value:
|
|
27
|
+
// `resolveLoading` returns a fresh object for an inline config every render.
|
|
28
|
+
if (loadingCfg !== null && !sameLook(loadingCfg, last)) setLast(loadingCfg);
|
|
29
|
+
return loadingCfg ?? last;
|
|
30
|
+
}
|