react-native-livechart 1.0.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/LICENSE +33 -0
- package/README.md +198 -0
- package/dist/components/AnimatedLabel.d.ts +21 -0
- package/dist/components/AnimatedLabel.d.ts.map +1 -0
- package/dist/components/BadgeOverlay.d.ts +16 -0
- package/dist/components/BadgeOverlay.d.ts.map +1 -0
- package/dist/components/CrosshairLine.d.ts +20 -0
- package/dist/components/CrosshairLine.d.ts.map +1 -0
- package/dist/components/CrosshairOverlay.d.ts +29 -0
- package/dist/components/CrosshairOverlay.d.ts.map +1 -0
- package/dist/components/DegenParticlesOverlay.d.ts +16 -0
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -0
- package/dist/components/DotOverlay.d.ts +17 -0
- package/dist/components/DotOverlay.d.ts.map +1 -0
- package/dist/components/LeftEdgeFade.d.ts +9 -0
- package/dist/components/LeftEdgeFade.d.ts.map +1 -0
- package/dist/components/LiveChart.d.ts +3 -0
- package/dist/components/LiveChart.d.ts.map +1 -0
- package/dist/components/LiveChartSeries.d.ts +3 -0
- package/dist/components/LiveChartSeries.d.ts.map +1 -0
- package/dist/components/LiveChartTransition.d.ts +28 -0
- package/dist/components/LiveChartTransition.d.ts.map +1 -0
- package/dist/components/LoadingOverlay.d.ts +24 -0
- package/dist/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/components/MarkerOverlay.d.ts +25 -0
- package/dist/components/MarkerOverlay.d.ts.map +1 -0
- package/dist/components/MultiSeriesDots.d.ts +11 -0
- package/dist/components/MultiSeriesDots.d.ts.map +1 -0
- package/dist/components/MultiSeriesStroke.d.ts +14 -0
- package/dist/components/MultiSeriesStroke.d.ts.map +1 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts +10 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLabels.d.ts +11 -0
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLines.d.ts +10 -0
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/SeriesToggleChips.d.ts +15 -0
- package/dist/components/SeriesToggleChips.d.ts.map +1 -0
- package/dist/components/TradeStreamOverlay.d.ts +14 -0
- package/dist/components/TradeStreamOverlay.d.ts.map +1 -0
- package/dist/components/ValueLineOverlay.d.ts +16 -0
- package/dist/components/ValueLineOverlay.d.ts.map +1 -0
- package/dist/components/ValueTextOverlay.d.ts +20 -0
- package/dist/components/ValueTextOverlay.d.ts.map +1 -0
- package/dist/components/XAxisOverlay.d.ts +14 -0
- package/dist/components/XAxisOverlay.d.ts.map +1 -0
- package/dist/components/YAxisOverlay.d.ts +23 -0
- package/dist/components/YAxisOverlay.d.ts.map +1 -0
- package/dist/components/seriesMetaSig.d.ts +4 -0
- package/dist/components/seriesMetaSig.d.ts.map +1 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +45 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts +38 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -0
- package/dist/core/multiSeriesLayout.d.ts +22 -0
- package/dist/core/multiSeriesLayout.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +188 -0
- package/dist/core/resolveConfig.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +79 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts +68 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -0
- package/dist/draw/candle.d.ts +25 -0
- package/dist/draw/candle.d.ts.map +1 -0
- package/dist/draw/grid.d.ts +24 -0
- package/dist/draw/grid.d.ts.map +1 -0
- package/dist/draw/line.d.ts +86 -0
- package/dist/draw/line.d.ts.map +1 -0
- package/dist/draw/trade.d.ts +38 -0
- package/dist/draw/trade.d.ts.map +1 -0
- package/dist/hooks/crosshairSeries.d.ts +13 -0
- package/dist/hooks/crosshairSeries.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +72 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +24 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +42 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -0
- package/dist/hooks/useBadge.d.ts +14 -0
- package/dist/hooks/useBadge.d.ts.map +1 -0
- package/dist/hooks/useCandlePaths.d.ts +19 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -0
- package/dist/hooks/useCanvasLayout.d.ts +12 -0
- package/dist/hooks/useCanvasLayout.d.ts.map +1 -0
- package/dist/hooks/useChartColors.d.ts +20 -0
- package/dist/hooks/useChartColors.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +19 -0
- package/dist/hooks/useChartPaths.d.ts.map +1 -0
- package/dist/hooks/useChartReveal.d.ts +30 -0
- package/dist/hooks/useChartReveal.d.ts.map +1 -0
- package/dist/hooks/useChartSkiaFont.d.ts +8 -0
- package/dist/hooks/useChartSkiaFont.d.ts.map +1 -0
- package/dist/hooks/useCrosshair.d.ts +20 -0
- package/dist/hooks/useCrosshair.d.ts.map +1 -0
- package/dist/hooks/useCrosshairSeries.d.ts +10 -0
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -0
- package/dist/hooks/useDegen.d.ts +17 -0
- package/dist/hooks/useDegen.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +17 -0
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -0
- package/dist/hooks/useLiveDot.d.ts +12 -0
- package/dist/hooks/useLiveDot.d.ts.map +1 -0
- package/dist/hooks/useMarkers.d.ts +15 -0
- package/dist/hooks/useMarkers.d.ts.map +1 -0
- package/dist/hooks/useModeBlend.d.ts +18 -0
- package/dist/hooks/useModeBlend.d.ts.map +1 -0
- package/dist/hooks/useMomentum.d.ts +16 -0
- package/dist/hooks/useMomentum.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts +25 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +15 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +32 -0
- package/dist/hooks/useReferenceLine.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts +24 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts +12 -0
- package/dist/hooks/useTradeStream.d.ts.map +1 -0
- package/dist/hooks/useXAxis.d.ts +32 -0
- package/dist/hooks/useXAxis.d.ts.map +1 -0
- package/dist/hooks/useYAxis.d.ts +13 -0
- package/dist/hooks/useYAxis.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/format.d.ts +5 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/measureFontTextWidth.d.ts +7 -0
- package/dist/lib/measureFontTextWidth.d.ts.map +1 -0
- package/dist/lib/monoFontFamily.d.ts +6 -0
- package/dist/lib/monoFontFamily.d.ts.map +1 -0
- package/dist/math/color.d.ts +5 -0
- package/dist/math/color.d.ts.map +1 -0
- package/dist/math/degenTick.d.ts +57 -0
- package/dist/math/degenTick.d.ts.map +1 -0
- package/dist/math/interpolate.d.ts +7 -0
- package/dist/math/interpolate.d.ts.map +1 -0
- package/dist/math/intervals.d.ts +3 -0
- package/dist/math/intervals.d.ts.map +1 -0
- package/dist/math/lerp.d.ts +7 -0
- package/dist/math/lerp.d.ts.map +1 -0
- package/dist/math/markers.d.ts +44 -0
- package/dist/math/markers.d.ts.map +1 -0
- package/dist/math/momentum.d.ts +8 -0
- package/dist/math/momentum.d.ts.map +1 -0
- package/dist/math/pickCandle.d.ts +8 -0
- package/dist/math/pickCandle.d.ts.map +1 -0
- package/dist/math/range.d.ts +10 -0
- package/dist/math/range.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +20 -0
- package/dist/math/referenceLines.d.ts.map +1 -0
- package/dist/math/spline.d.ts +45 -0
- package/dist/math/spline.d.ts.map +1 -0
- package/dist/math/squiggly.d.ts +36 -0
- package/dist/math/squiggly.d.ts.map +1 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/types.d.ts +720 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/components/AnimatedLabel.tsx +41 -0
- package/src/components/BadgeOverlay.tsx +39 -0
- package/src/components/CrosshairLine.tsx +90 -0
- package/src/components/CrosshairOverlay.tsx +170 -0
- package/src/components/DegenParticlesOverlay.tsx +141 -0
- package/src/components/DotOverlay.tsx +69 -0
- package/src/components/LeftEdgeFade.tsx +36 -0
- package/src/components/LiveChart.tsx +821 -0
- package/src/components/LiveChartSeries.tsx +607 -0
- package/src/components/LiveChartTransition.tsx +135 -0
- package/src/components/LoadingOverlay.tsx +301 -0
- package/src/components/MarkerOverlay.tsx +331 -0
- package/src/components/MultiSeriesDots.tsx +117 -0
- package/src/components/MultiSeriesStroke.tsx +79 -0
- package/src/components/MultiSeriesTooltipStack.tsx +78 -0
- package/src/components/MultiSeriesValueLabels.tsx +103 -0
- package/src/components/MultiSeriesValueLines.tsx +115 -0
- package/src/components/ReferenceLineOverlay.tsx +317 -0
- package/src/components/SeriesToggleChips.tsx +201 -0
- package/src/components/TradeStreamOverlay.tsx +117 -0
- package/src/components/ValueLineOverlay.tsx +63 -0
- package/src/components/ValueTextOverlay.tsx +43 -0
- package/src/components/XAxisOverlay.tsx +109 -0
- package/src/components/YAxisOverlay.tsx +135 -0
- package/src/components/seriesMetaSig.ts +13 -0
- package/src/constants.ts +44 -0
- package/src/core/liveChartEngineTick.ts +185 -0
- package/src/core/liveChartSeriesEngineTick.ts +163 -0
- package/src/core/multiSeriesLayout.ts +80 -0
- package/src/core/resolveConfig.ts +538 -0
- package/src/core/useLiveChartEngine.ts +207 -0
- package/src/core/useLiveChartSeriesEngine.ts +215 -0
- package/src/draw/candle.ts +210 -0
- package/src/draw/grid.ts +174 -0
- package/src/draw/line.ts +257 -0
- package/src/draw/trade.ts +202 -0
- package/src/hooks/crosshairSeries.ts +78 -0
- package/src/hooks/crosshairShared.ts +296 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/resolveChartLayout.ts +144 -0
- package/src/hooks/useBadge.ts +192 -0
- package/src/hooks/useCandlePaths.ts +165 -0
- package/src/hooks/useCanvasLayout.ts +22 -0
- package/src/hooks/useChartColors.ts +48 -0
- package/src/hooks/useChartPaths.ts +121 -0
- package/src/hooks/useChartReveal.ts +123 -0
- package/src/hooks/useChartSkiaFont.ts +34 -0
- package/src/hooks/useCrosshair.ts +250 -0
- package/src/hooks/useCrosshairSeries.ts +163 -0
- package/src/hooks/useDegen.ts +262 -0
- package/src/hooks/useLiveChartHasData.ts +31 -0
- package/src/hooks/useLiveDot.ts +31 -0
- package/src/hooks/useMarkers.ts +95 -0
- package/src/hooks/useModeBlend.ts +52 -0
- package/src/hooks/useMomentum.ts +37 -0
- package/src/hooks/useMultiSeriesDegen.ts +260 -0
- package/src/hooks/useMultiSeriesLinePaths.ts +76 -0
- package/src/hooks/useReferenceLine.ts +212 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +154 -0
- package/src/hooks/useTradeStream.ts +64 -0
- package/src/hooks/useXAxis.ts +200 -0
- package/src/hooks/useYAxis.ts +48 -0
- package/src/index.ts +69 -0
- package/src/lib/format.ts +36 -0
- package/src/lib/measureFontTextWidth.ts +10 -0
- package/src/lib/monoFontFamily.ts +8 -0
- package/src/math/color.ts +31 -0
- package/src/math/degenTick.ts +151 -0
- package/src/math/interpolate.ts +32 -0
- package/src/math/intervals.ts +17 -0
- package/src/math/lerp.ts +17 -0
- package/src/math/markers.ts +175 -0
- package/src/math/momentum.ts +38 -0
- package/src/math/pickCandle.ts +42 -0
- package/src/math/range.ts +54 -0
- package/src/math/referenceLines.ts +56 -0
- package/src/math/spline.ts +183 -0
- package/src/math/squiggly.ts +108 -0
- package/src/theme.ts +231 -0
- package/src/types.ts +789 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { type SkFont } from "@shopify/react-native-skia";
|
|
2
|
+
import { type Gesture } from "react-native-gesture-handler";
|
|
3
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
4
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
5
|
+
import { type ChartPadding } from "../draw/line";
|
|
6
|
+
import { interpolateAtTime } from "../math/interpolate";
|
|
7
|
+
|
|
8
|
+
const TOOLTIP_PAD_X = 8;
|
|
9
|
+
const TOOLTIP_PAD_Y = 6;
|
|
10
|
+
const TOOLTIP_LINE_GAP = 4;
|
|
11
|
+
const TOOLTIP_OFFSET_X = 12;
|
|
12
|
+
const TOOLTIP_EDGE_GAP = 4;
|
|
13
|
+
const TOOLTIP_TOP_MARGIN = 8;
|
|
14
|
+
const FADE_ZONE = 4;
|
|
15
|
+
|
|
16
|
+
export interface TooltipLayout {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
w: number;
|
|
20
|
+
h: number;
|
|
21
|
+
valueStr: string;
|
|
22
|
+
timeStr: string;
|
|
23
|
+
valueTextX: number;
|
|
24
|
+
timeTextX: number;
|
|
25
|
+
line1Y: number;
|
|
26
|
+
line2Y: number;
|
|
27
|
+
/** Multi-series tooltip: time + per-series rows (replaces valueStr/timeStr rendering). */
|
|
28
|
+
stackedLines?: {
|
|
29
|
+
text: string;
|
|
30
|
+
textX: number;
|
|
31
|
+
baselineY: number;
|
|
32
|
+
dim: boolean;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const HIDDEN_TOOLTIP: TooltipLayout = {
|
|
37
|
+
x: -400,
|
|
38
|
+
y: 0,
|
|
39
|
+
w: 0,
|
|
40
|
+
h: 0,
|
|
41
|
+
valueStr: "",
|
|
42
|
+
timeStr: "",
|
|
43
|
+
valueTextX: -400,
|
|
44
|
+
timeTextX: -400,
|
|
45
|
+
line1Y: 0,
|
|
46
|
+
line2Y: 0,
|
|
47
|
+
stackedLines: undefined,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface CrosshairState {
|
|
51
|
+
scrubX: SharedValue<number>;
|
|
52
|
+
scrubActive: SharedValue<boolean>;
|
|
53
|
+
scrubTime: SharedValue<number>;
|
|
54
|
+
scrubValue: SharedValue<number | null>;
|
|
55
|
+
crosshairOpacity: SharedValue<number>;
|
|
56
|
+
tooltipLayout: SharedValue<TooltipLayout>;
|
|
57
|
+
gesture: ReturnType<typeof Gesture.Pan>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Maps a scrub X position to a window timestamp.
|
|
62
|
+
* Returns -1 when inactive or when the canvas is not yet laid out.
|
|
63
|
+
*/
|
|
64
|
+
export function computeScrubTime(
|
|
65
|
+
scrubActive: boolean,
|
|
66
|
+
scrubX: number,
|
|
67
|
+
padding: ChartPadding,
|
|
68
|
+
canvasWidth: number,
|
|
69
|
+
timestamp: number,
|
|
70
|
+
windowSecs: number,
|
|
71
|
+
): number {
|
|
72
|
+
"worklet";
|
|
73
|
+
if (!scrubActive) return -1;
|
|
74
|
+
const chartW = canvasWidth - padding.left - padding.right;
|
|
75
|
+
if (chartW <= 0) return -1;
|
|
76
|
+
const winStart = timestamp - windowSecs;
|
|
77
|
+
const fraction = (scrubX - padding.left) / chartW;
|
|
78
|
+
return winStart + fraction * windowSecs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Crosshair opacity: fades 1→0 over FADE_ZONE px as the crosshair
|
|
83
|
+
* approaches the live dot at the right chart edge.
|
|
84
|
+
*/
|
|
85
|
+
export function computeCrosshairOpacity(
|
|
86
|
+
scrubActive: boolean,
|
|
87
|
+
scrubX: number,
|
|
88
|
+
canvasWidth: number,
|
|
89
|
+
paddingRight: number,
|
|
90
|
+
): number {
|
|
91
|
+
"worklet";
|
|
92
|
+
if (!scrubActive) return 0;
|
|
93
|
+
const dotX = canvasWidth - paddingRight;
|
|
94
|
+
const dist = dotX - scrubX;
|
|
95
|
+
return Math.min(1, Math.max(0, dist / FADE_ZONE));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Full tooltip pill layout. Returns HIDDEN_TOOLTIP when the scrub is
|
|
100
|
+
* inactive or no value can be interpolated at the current time.
|
|
101
|
+
*/
|
|
102
|
+
export function computeTooltipLayout(
|
|
103
|
+
scrubActive: boolean,
|
|
104
|
+
scrubX: number,
|
|
105
|
+
scrubValue: number | null,
|
|
106
|
+
scrubTime: number,
|
|
107
|
+
padding: ChartPadding,
|
|
108
|
+
canvasWidth: number,
|
|
109
|
+
formatValue: (v: number) => string,
|
|
110
|
+
formatTime: (t: number) => string,
|
|
111
|
+
font: SkFont,
|
|
112
|
+
): TooltipLayout {
|
|
113
|
+
"worklet";
|
|
114
|
+
if (!scrubActive || scrubValue === null) return HIDDEN_TOOLTIP;
|
|
115
|
+
|
|
116
|
+
const v = scrubValue;
|
|
117
|
+
const t = scrubTime;
|
|
118
|
+
const valueStr = formatValue(v);
|
|
119
|
+
const timeStr = formatTime(t);
|
|
120
|
+
|
|
121
|
+
const fm = font.getMetrics();
|
|
122
|
+
const lineH = -fm.ascent + fm.descent;
|
|
123
|
+
const totalH = TOOLTIP_PAD_Y * 2 + lineH * 2 + TOOLTIP_LINE_GAP;
|
|
124
|
+
|
|
125
|
+
const valueW = measureFontTextWidth(font, valueStr);
|
|
126
|
+
const timeW = measureFontTextWidth(font, timeStr);
|
|
127
|
+
const contentW = Math.max(valueW, timeW);
|
|
128
|
+
const pillW = contentW + TOOLTIP_PAD_X * 2;
|
|
129
|
+
|
|
130
|
+
const rightEdge = canvasWidth - padding.right;
|
|
131
|
+
|
|
132
|
+
let pillX = scrubX + TOOLTIP_OFFSET_X;
|
|
133
|
+
if (pillX + pillW > rightEdge - TOOLTIP_EDGE_GAP) {
|
|
134
|
+
pillX = scrubX - pillW - TOOLTIP_OFFSET_X;
|
|
135
|
+
}
|
|
136
|
+
const pillY = padding.top + TOOLTIP_TOP_MARGIN;
|
|
137
|
+
|
|
138
|
+
const line1Y = pillY + TOOLTIP_PAD_Y - fm.ascent;
|
|
139
|
+
const line2Y = line1Y + lineH + TOOLTIP_LINE_GAP;
|
|
140
|
+
|
|
141
|
+
const valueTextX = pillX + TOOLTIP_PAD_X + (contentW - valueW) / 2;
|
|
142
|
+
const timeTextX = pillX + TOOLTIP_PAD_X + (contentW - timeW) / 2;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
x: pillX,
|
|
146
|
+
y: pillY,
|
|
147
|
+
w: pillW,
|
|
148
|
+
h: totalH,
|
|
149
|
+
valueStr,
|
|
150
|
+
timeStr,
|
|
151
|
+
valueTextX,
|
|
152
|
+
timeTextX,
|
|
153
|
+
line1Y,
|
|
154
|
+
line2Y,
|
|
155
|
+
stackedLines: undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Multi-series scrub tooltip — stacked lines (first row typically time, dim).
|
|
161
|
+
*/
|
|
162
|
+
export function computeTooltipLayoutMulti(
|
|
163
|
+
scrubActive: boolean,
|
|
164
|
+
scrubX: number,
|
|
165
|
+
lines: { text: string; dim: boolean }[],
|
|
166
|
+
padding: ChartPadding,
|
|
167
|
+
canvasWidth: number,
|
|
168
|
+
font: SkFont,
|
|
169
|
+
): TooltipLayout {
|
|
170
|
+
"worklet";
|
|
171
|
+
if (!scrubActive || lines.length === 0) return HIDDEN_TOOLTIP;
|
|
172
|
+
|
|
173
|
+
const fm = font.getMetrics();
|
|
174
|
+
const lineH = -fm.ascent + fm.descent;
|
|
175
|
+
const lineGap = TOOLTIP_LINE_GAP;
|
|
176
|
+
const n = lines.length;
|
|
177
|
+
const totalH = TOOLTIP_PAD_Y * 2 + lineH * n + lineGap * Math.max(0, n - 1);
|
|
178
|
+
|
|
179
|
+
let contentW = 0;
|
|
180
|
+
const lineWidths: number[] = [];
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
const w = measureFontTextWidth(font, lines[i].text);
|
|
183
|
+
lineWidths.push(w);
|
|
184
|
+
if (w > contentW) contentW = w;
|
|
185
|
+
}
|
|
186
|
+
const pillW = contentW + TOOLTIP_PAD_X * 2;
|
|
187
|
+
|
|
188
|
+
const rightEdge = canvasWidth - padding.right;
|
|
189
|
+
let pillX = scrubX + TOOLTIP_OFFSET_X;
|
|
190
|
+
if (pillX + pillW > rightEdge - TOOLTIP_EDGE_GAP) {
|
|
191
|
+
pillX = scrubX - pillW - TOOLTIP_OFFSET_X;
|
|
192
|
+
}
|
|
193
|
+
const pillY = padding.top + TOOLTIP_TOP_MARGIN;
|
|
194
|
+
|
|
195
|
+
const stackedLines: {
|
|
196
|
+
text: string;
|
|
197
|
+
textX: number;
|
|
198
|
+
baselineY: number;
|
|
199
|
+
dim: boolean;
|
|
200
|
+
}[] = [];
|
|
201
|
+
let y = pillY + TOOLTIP_PAD_Y - fm.ascent;
|
|
202
|
+
for (let i = 0; i < n; i++) {
|
|
203
|
+
stackedLines.push({
|
|
204
|
+
text: lines[i].text,
|
|
205
|
+
textX: pillX + TOOLTIP_PAD_X + (contentW - lineWidths[i]) / 2,
|
|
206
|
+
baselineY: y,
|
|
207
|
+
dim: lines[i].dim,
|
|
208
|
+
});
|
|
209
|
+
y += lineH + lineGap;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
x: pillX,
|
|
214
|
+
y: pillY,
|
|
215
|
+
w: pillW,
|
|
216
|
+
h: totalH,
|
|
217
|
+
valueStr: "",
|
|
218
|
+
timeStr: "",
|
|
219
|
+
valueTextX: -400,
|
|
220
|
+
timeTextX: -400,
|
|
221
|
+
line1Y: 0,
|
|
222
|
+
line2Y: 0,
|
|
223
|
+
stackedLines,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Candle-mode tooltip — 5 stacked rows: O, H, L, C (bright) + time (dim).
|
|
229
|
+
*/
|
|
230
|
+
export function computeCandleTooltipLayout(
|
|
231
|
+
scrubActive: boolean,
|
|
232
|
+
scrubX: number,
|
|
233
|
+
candle: { open: number; high: number; low: number; close: number } | null,
|
|
234
|
+
scrubTime: number,
|
|
235
|
+
padding: ChartPadding,
|
|
236
|
+
canvasWidth: number,
|
|
237
|
+
formatValue: (v: number) => string,
|
|
238
|
+
formatTime: (t: number) => string,
|
|
239
|
+
font: SkFont,
|
|
240
|
+
): TooltipLayout {
|
|
241
|
+
"worklet";
|
|
242
|
+
if (!scrubActive || !candle) return HIDDEN_TOOLTIP;
|
|
243
|
+
const lines: { text: string; dim: boolean }[] = [
|
|
244
|
+
{ text: `O ${formatValue(candle.open)}`, dim: false },
|
|
245
|
+
{ text: `H ${formatValue(candle.high)}`, dim: false },
|
|
246
|
+
{ text: `L ${formatValue(candle.low)}`, dim: false },
|
|
247
|
+
{ text: `C ${formatValue(candle.close)}`, dim: false },
|
|
248
|
+
{ text: formatTime(scrubTime), dim: true },
|
|
249
|
+
];
|
|
250
|
+
return computeTooltipLayoutMulti(
|
|
251
|
+
scrubActive,
|
|
252
|
+
scrubX,
|
|
253
|
+
lines,
|
|
254
|
+
padding,
|
|
255
|
+
canvasWidth,
|
|
256
|
+
font,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Single-series scrub value at window time — extracted for tests. */
|
|
261
|
+
export function deriveScrubValueSingle(
|
|
262
|
+
scrubActive: boolean,
|
|
263
|
+
scrubTime: number,
|
|
264
|
+
data: { time: number; value: number }[],
|
|
265
|
+
): number | null {
|
|
266
|
+
"worklet";
|
|
267
|
+
if (!scrubActive || scrubTime < 0) return null;
|
|
268
|
+
return interpolateAtTime(data, scrubTime);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Single-series crosshair tooltip — extracted for tests. */
|
|
272
|
+
export function deriveCrosshairTooltipSingle(
|
|
273
|
+
scrubActive: boolean,
|
|
274
|
+
scrubX: number,
|
|
275
|
+
scrubTime: number,
|
|
276
|
+
scrubValue: number | null,
|
|
277
|
+
padding: ChartPadding,
|
|
278
|
+
canvasWidth: number,
|
|
279
|
+
formatValue: (v: number) => string,
|
|
280
|
+
formatTime: (t: number) => string,
|
|
281
|
+
font: SkFont,
|
|
282
|
+
): TooltipLayout {
|
|
283
|
+
"worklet";
|
|
284
|
+
if (!scrubActive || scrubTime < 0) return HIDDEN_TOOLTIP;
|
|
285
|
+
return computeTooltipLayout(
|
|
286
|
+
scrubActive,
|
|
287
|
+
scrubX,
|
|
288
|
+
scrubValue,
|
|
289
|
+
scrubTime,
|
|
290
|
+
padding,
|
|
291
|
+
canvasWidth,
|
|
292
|
+
formatValue,
|
|
293
|
+
formatTime,
|
|
294
|
+
font,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { resolveChartLayout } from "./resolveChartLayout";
|
|
2
|
+
export { useBadge } from "./useBadge";
|
|
3
|
+
export { useCandlePaths } from "./useCandlePaths";
|
|
4
|
+
export { useCanvasLayout } from "./useCanvasLayout";
|
|
5
|
+
export { useChartColors } from "./useChartColors";
|
|
6
|
+
export { useChartPaths } from "./useChartPaths";
|
|
7
|
+
export { useChartReveal } from "./useChartReveal";
|
|
8
|
+
export { useChartSkiaFont } from "./useChartSkiaFont";
|
|
9
|
+
export { useCrosshair } from "./useCrosshair";
|
|
10
|
+
export { useCrosshairSeries } from "./useCrosshairSeries";
|
|
11
|
+
export { useDegen } from "./useDegen";
|
|
12
|
+
export { useLiveChartHasData } from "./useLiveChartHasData";
|
|
13
|
+
export { useLiveDot } from "./useLiveDot";
|
|
14
|
+
export { useMarkers } from "./useMarkers";
|
|
15
|
+
export { useModeBlend } from "./useModeBlend";
|
|
16
|
+
export { useMomentum } from "./useMomentum";
|
|
17
|
+
export { useMultiSeriesDegen } from "./useMultiSeriesDegen";
|
|
18
|
+
export { useMultiSeriesLinePaths } from "./useMultiSeriesLinePaths";
|
|
19
|
+
export { useReferenceLine } from "./useReferenceLine";
|
|
20
|
+
export {
|
|
21
|
+
useMultiSeriesReverseMorphInputs,
|
|
22
|
+
useSingleChartReverseMorphInputs,
|
|
23
|
+
} from "./useReverseMorphEngineInputs";
|
|
24
|
+
export { useTradeStream } from "./useTradeStream";
|
|
25
|
+
export { useXAxis } from "./useXAxis";
|
|
26
|
+
export { useYAxis } from "./useYAxis";
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { SkFont } from "@shopify/react-native-skia";
|
|
2
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
3
|
+
import {
|
|
4
|
+
minPaddingRightForBadgeYAxisAlign,
|
|
5
|
+
minPaddingRightForYAxisWithPulse,
|
|
6
|
+
pulseRadialOutset,
|
|
7
|
+
resolveAutoLeft,
|
|
8
|
+
resolveAutoRight,
|
|
9
|
+
resolvePadding,
|
|
10
|
+
type ChartPadding,
|
|
11
|
+
} from "../draw/line";
|
|
12
|
+
import type { ChartInsets, LiveChartPalette } from "../types";
|
|
13
|
+
|
|
14
|
+
export interface ChartLayoutConfig {
|
|
15
|
+
palette: LiveChartPalette;
|
|
16
|
+
lineWidthOverride?: number;
|
|
17
|
+
insetsOverride?: ChartInsets;
|
|
18
|
+
yAxis: boolean;
|
|
19
|
+
badge: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When true (default if omitted and `badge` is true), reserve the wide right gutter for the badge.
|
|
22
|
+
* Set false when the badge is anchored left of the live dot (`position: "left"`).
|
|
23
|
+
*/
|
|
24
|
+
badgeUsesRightGutter?: boolean;
|
|
25
|
+
/** When false, bottom inset shrinks (no x-axis labels). Default true. */
|
|
26
|
+
xAxis?: boolean;
|
|
27
|
+
/** Skia font for measuring label width. When provided with formatValue + currentValue, padding auto-sizes. */
|
|
28
|
+
font?: SkFont;
|
|
29
|
+
/** Worklet formatter — called on JS thread here to measure label width */
|
|
30
|
+
formatValue?: (v: number) => string;
|
|
31
|
+
/** Current value read from SharedValue on JS thread — used to produce a sample label for measurement */
|
|
32
|
+
currentValue?: number;
|
|
33
|
+
/** Live dot pulse (LiveChart); expands top/right/bottom insets so the ring is not clipped. */
|
|
34
|
+
pulse?: { maxRadius: number; strokeWidth: number } | null;
|
|
35
|
+
/** When false and badge uses the right gutter, omit BADGE_TAIL_LEN from the right padding. */
|
|
36
|
+
badgeShowTail?: boolean;
|
|
37
|
+
/** Multi-series dot radius — used to add spacing between dots and Y-axis labels. */
|
|
38
|
+
multiSeriesDotRadius?: number;
|
|
39
|
+
/** When true, inline series labels sit right of dots and need extra right padding. */
|
|
40
|
+
multiSeriesValueLabel?: boolean;
|
|
41
|
+
/** Measured width of the widest series label (e.g. "Maybe"). Used when `multiSeriesValueLabel` is true. */
|
|
42
|
+
multiSeriesMaxLabelWidth?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ChartLayoutResult {
|
|
46
|
+
strokeWidth: number;
|
|
47
|
+
padding: ChartPadding;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveChartLayout(
|
|
51
|
+
config: ChartLayoutConfig,
|
|
52
|
+
): ChartLayoutResult {
|
|
53
|
+
const badgeUsesRightGutter =
|
|
54
|
+
config.badge && (config.badgeUsesRightGutter ?? true);
|
|
55
|
+
const showTail = config.badgeShowTail ?? true;
|
|
56
|
+
const xAxis = config.xAxis ?? true;
|
|
57
|
+
|
|
58
|
+
let measuredYAxisLabelWidth: number | undefined;
|
|
59
|
+
|
|
60
|
+
// ── Right inset ──────────────────────────────────────────────────────────
|
|
61
|
+
let rightPad: number;
|
|
62
|
+
if (config.insetsOverride?.right != null) {
|
|
63
|
+
rightPad = config.insetsOverride.right;
|
|
64
|
+
} else if (
|
|
65
|
+
config.font &&
|
|
66
|
+
config.formatValue &&
|
|
67
|
+
config.currentValue != null &&
|
|
68
|
+
(badgeUsesRightGutter || !config.badge || config.yAxis)
|
|
69
|
+
) {
|
|
70
|
+
const v = config.currentValue;
|
|
71
|
+
const samples = [v, v / 10, v / 100, v * 10].map(config.formatValue);
|
|
72
|
+
measuredYAxisLabelWidth = Math.max(
|
|
73
|
+
...samples.map((s) => measureFontTextWidth(config.font!, s)),
|
|
74
|
+
);
|
|
75
|
+
const dotR = config.multiSeriesDotRadius ?? 0;
|
|
76
|
+
const seriesLabelW = config.multiSeriesMaxLabelWidth ?? 0;
|
|
77
|
+
rightPad = badgeUsesRightGutter
|
|
78
|
+
? minPaddingRightForBadgeYAxisAlign(
|
|
79
|
+
config.font.getSize(),
|
|
80
|
+
measuredYAxisLabelWidth,
|
|
81
|
+
showTail,
|
|
82
|
+
)
|
|
83
|
+
: config.multiSeriesValueLabel && config.yAxis
|
|
84
|
+
? Math.max(
|
|
85
|
+
dotR + 8 + seriesLabelW + 8 + measuredYAxisLabelWidth + 8,
|
|
86
|
+
44,
|
|
87
|
+
)
|
|
88
|
+
: config.multiSeriesValueLabel
|
|
89
|
+
? Math.max(dotR + 8 + seriesLabelW + 8, 44)
|
|
90
|
+
: config.yAxis
|
|
91
|
+
? Math.max(measuredYAxisLabelWidth + 16 + dotR * 2, 44)
|
|
92
|
+
: resolveAutoRight(false, false);
|
|
93
|
+
} else {
|
|
94
|
+
rightPad = resolveAutoRight(config.yAxis, badgeUsesRightGutter, showTail);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config.pulse && config.yAxis && config.insetsOverride?.right == null) {
|
|
98
|
+
const outlet = pulseRadialOutset(
|
|
99
|
+
config.pulse.maxRadius,
|
|
100
|
+
config.pulse.strokeWidth,
|
|
101
|
+
);
|
|
102
|
+
const labelW = measuredYAxisLabelWidth ?? 49;
|
|
103
|
+
rightPad = Math.max(
|
|
104
|
+
rightPad,
|
|
105
|
+
minPaddingRightForYAxisWithPulse(outlet, labelW),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Left inset ───────────────────────────────────────────────────────────
|
|
110
|
+
let leftPad: number;
|
|
111
|
+
if (config.insetsOverride?.left != null) {
|
|
112
|
+
leftPad = config.insetsOverride.left;
|
|
113
|
+
} else {
|
|
114
|
+
leftPad = resolveAutoLeft(false);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const base = resolvePadding(
|
|
118
|
+
config.insetsOverride,
|
|
119
|
+
config.yAxis,
|
|
120
|
+
badgeUsesRightGutter,
|
|
121
|
+
false,
|
|
122
|
+
xAxis,
|
|
123
|
+
showTail,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
let padding: ChartPadding = { ...base, right: rightPad, left: leftPad };
|
|
127
|
+
if (config.pulse) {
|
|
128
|
+
const outlet = pulseRadialOutset(
|
|
129
|
+
config.pulse.maxRadius,
|
|
130
|
+
config.pulse.strokeWidth,
|
|
131
|
+
);
|
|
132
|
+
padding = {
|
|
133
|
+
...padding,
|
|
134
|
+
right: Math.max(padding.right, outlet),
|
|
135
|
+
top: Math.max(padding.top, outlet),
|
|
136
|
+
bottom: Math.max(padding.bottom, outlet),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
strokeWidth: config.lineWidthOverride ?? config.palette.lineWidth,
|
|
142
|
+
padding,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Skia, type SkFont, type SkPath } from "@shopify/react-native-skia";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import {
|
|
4
|
+
useDerivedValue,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import {
|
|
9
|
+
BADGE_DOT_GAP,
|
|
10
|
+
BADGE_MARGIN_RIGHT,
|
|
11
|
+
BADGE_PILL_PAD_X,
|
|
12
|
+
BADGE_PILL_PAD_Y,
|
|
13
|
+
BADGE_TAIL_LEN,
|
|
14
|
+
MS_PER_FRAME_60FPS,
|
|
15
|
+
} from "../constants";
|
|
16
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
17
|
+
import type { ChartEngineWithLiveValue } from "../core/useLiveChartEngine";
|
|
18
|
+
import {
|
|
19
|
+
badgeTailAndCap,
|
|
20
|
+
pillTextLeftX,
|
|
21
|
+
type ChartPadding,
|
|
22
|
+
} from "../draw/line";
|
|
23
|
+
import { hexToRgb, lerpColor } from "../math/color";
|
|
24
|
+
import { lerp } from "../math/lerp";
|
|
25
|
+
import type { BadgeVariant, LiveChartPalette, Momentum } from "../types";
|
|
26
|
+
|
|
27
|
+
const TAIL_SPREAD = 2.5;
|
|
28
|
+
|
|
29
|
+
export function useBadge(
|
|
30
|
+
engine: ChartEngineWithLiveValue,
|
|
31
|
+
padding: ChartPadding,
|
|
32
|
+
palette: LiveChartPalette,
|
|
33
|
+
formatValue: (v: number) => string,
|
|
34
|
+
font: SkFont,
|
|
35
|
+
variant: BadgeVariant = "default",
|
|
36
|
+
showTail = true,
|
|
37
|
+
momentum?: SharedValue<Momentum>,
|
|
38
|
+
position: "right" | "left" = "right",
|
|
39
|
+
background?: string,
|
|
40
|
+
) {
|
|
41
|
+
const colorR = useSharedValue(0);
|
|
42
|
+
const colorG = useSharedValue(0);
|
|
43
|
+
const colorB = useSharedValue(0);
|
|
44
|
+
|
|
45
|
+
const upRgb = hexToRgb(palette.dotUp);
|
|
46
|
+
const downRgb = hexToRgb(palette.dotDown);
|
|
47
|
+
const accentRgb = hexToRgb(palette.badgeBg);
|
|
48
|
+
|
|
49
|
+
// Ping-pong between two persistent badge paths so the derived value always
|
|
50
|
+
// returns a freshly-mutated SkPath without allocating a new one every frame.
|
|
51
|
+
// See useChartPaths for the full rationale on per-frame Skia.Path.Make().
|
|
52
|
+
const cacheRef = useRef<{
|
|
53
|
+
a: SkPath;
|
|
54
|
+
b: SkPath;
|
|
55
|
+
tick: boolean;
|
|
56
|
+
} | null>(null);
|
|
57
|
+
if (cacheRef.current === null) {
|
|
58
|
+
cacheRef.current = {
|
|
59
|
+
a: Skia.Path.Make(),
|
|
60
|
+
b: Skia.Path.Make(),
|
|
61
|
+
tick: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const badge = useDerivedValue(() => {
|
|
66
|
+
const cache = cacheRef.current!;
|
|
67
|
+
const w = engine.canvasWidth.get();
|
|
68
|
+
const h = engine.canvasHeight.get();
|
|
69
|
+
cache.tick = !cache.tick;
|
|
70
|
+
const path = cache.tick ? cache.a : cache.b;
|
|
71
|
+
path.reset();
|
|
72
|
+
if (w === 0 || h === 0) {
|
|
73
|
+
return {
|
|
74
|
+
path,
|
|
75
|
+
textX: 0,
|
|
76
|
+
textY: 0,
|
|
77
|
+
text: "",
|
|
78
|
+
bgColor: palette.badgeBg,
|
|
79
|
+
textColor: palette.badgeText,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const chartH = h - padding.top - padding.bottom;
|
|
84
|
+
const dMin = engine.displayMin.get();
|
|
85
|
+
const dMax = engine.displayMax.get();
|
|
86
|
+
const valRange = dMax - dMin;
|
|
87
|
+
const dotY =
|
|
88
|
+
valRange === 0
|
|
89
|
+
? padding.top + chartH / 2
|
|
90
|
+
: padding.top +
|
|
91
|
+
((dMax - engine.displayValue.get()) / valRange) * chartH;
|
|
92
|
+
|
|
93
|
+
const text = formatValue(engine.displayValue.get());
|
|
94
|
+
const textW = measureFontTextWidth(font, text);
|
|
95
|
+
|
|
96
|
+
const pillH = font.getSize() + BADGE_PILL_PAD_Y * 2;
|
|
97
|
+
const r = pillH / 2;
|
|
98
|
+
const badgeY = dotY - pillH / 2;
|
|
99
|
+
let textX: number;
|
|
100
|
+
|
|
101
|
+
if (position === "left") {
|
|
102
|
+
// Pill to the left of the live dot; no tail (`showTail` applies to right gutter only).
|
|
103
|
+
const dotXPos = w - padding.right;
|
|
104
|
+
const pillW = 2 * BADGE_PILL_PAD_X + textW;
|
|
105
|
+
const bodyRight = dotXPos - BADGE_DOT_GAP;
|
|
106
|
+
const bodyLeft = Math.max(BADGE_MARGIN_RIGHT, bodyRight - pillW);
|
|
107
|
+
const pillBodyW = bodyRight - bodyLeft;
|
|
108
|
+
textX = (bodyLeft + bodyRight - textW) / 2;
|
|
109
|
+
path.addRRect({
|
|
110
|
+
rect: { x: bodyLeft, y: badgeY, width: pillBodyW, height: pillH },
|
|
111
|
+
rx: r,
|
|
112
|
+
ry: r,
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
// Right-gutter badge (default): asymmetric layout with optional tail.
|
|
116
|
+
const tl = badgeTailAndCap(font.getSize(), showTail);
|
|
117
|
+
const bodyLeft = w - padding.right + BADGE_DOT_GAP + tl;
|
|
118
|
+
const bodyRight = w - BADGE_MARGIN_RIGHT;
|
|
119
|
+
const pillW = bodyRight - bodyLeft;
|
|
120
|
+
// Text centered in pill body — same formula used by GridOverlay.
|
|
121
|
+
textX = pillTextLeftX(w, padding.right, BADGE_DOT_GAP + tl, textW);
|
|
122
|
+
|
|
123
|
+
if (showTail) {
|
|
124
|
+
const badgeX = w - padding.right + BADGE_DOT_GAP;
|
|
125
|
+
const cx = tl + pillW - r;
|
|
126
|
+
|
|
127
|
+
path.moveTo(badgeX + tl, badgeY);
|
|
128
|
+
path.lineTo(badgeX + cx, badgeY);
|
|
129
|
+
path.arcToOval(
|
|
130
|
+
{ x: badgeX + cx - r, y: badgeY, width: r * 2, height: pillH },
|
|
131
|
+
-90,
|
|
132
|
+
180,
|
|
133
|
+
false,
|
|
134
|
+
);
|
|
135
|
+
path.lineTo(badgeX + tl, badgeY + pillH);
|
|
136
|
+
path.cubicTo(
|
|
137
|
+
badgeX + BADGE_TAIL_LEN + 2,
|
|
138
|
+
badgeY + pillH,
|
|
139
|
+
badgeX + 3,
|
|
140
|
+
badgeY + r + TAIL_SPREAD,
|
|
141
|
+
badgeX,
|
|
142
|
+
badgeY + r,
|
|
143
|
+
);
|
|
144
|
+
path.cubicTo(
|
|
145
|
+
badgeX + 3,
|
|
146
|
+
badgeY + r - TAIL_SPREAD,
|
|
147
|
+
badgeX + BADGE_TAIL_LEN + 2,
|
|
148
|
+
badgeY,
|
|
149
|
+
badgeX + tl,
|
|
150
|
+
badgeY,
|
|
151
|
+
);
|
|
152
|
+
path.close();
|
|
153
|
+
} else {
|
|
154
|
+
path.addRRect({
|
|
155
|
+
rect: { x: bodyLeft, y: badgeY, width: pillW, height: pillH },
|
|
156
|
+
rx: r,
|
|
157
|
+
ry: r,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const fm = font.getMetrics();
|
|
163
|
+
const textY = dotY - (fm.ascent + fm.descent) / 2;
|
|
164
|
+
|
|
165
|
+
let bgColor: string;
|
|
166
|
+
if (background) {
|
|
167
|
+
bgColor = background;
|
|
168
|
+
} else if (variant === "minimal") {
|
|
169
|
+
bgColor = "rgba(255,255,255,0.95)";
|
|
170
|
+
} else if (momentum) {
|
|
171
|
+
const m = momentum.get();
|
|
172
|
+
const targetRgb = m === "up" ? upRgb : m === "down" ? downRgb : accentRgb;
|
|
173
|
+
colorR.set(lerp(colorR.get(), targetRgb[0], 0.08, MS_PER_FRAME_60FPS));
|
|
174
|
+
colorG.set(lerp(colorG.get(), targetRgb[1], 0.08, MS_PER_FRAME_60FPS));
|
|
175
|
+
colorB.set(lerp(colorB.get(), targetRgb[2], 0.08, MS_PER_FRAME_60FPS));
|
|
176
|
+
bgColor = lerpColor(
|
|
177
|
+
[colorR.get(), colorG.get(), colorB.get()],
|
|
178
|
+
[colorR.get(), colorG.get(), colorB.get()],
|
|
179
|
+
0,
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
bgColor = palette.badgeBg;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const textColor =
|
|
186
|
+
variant === "minimal" ? "rgba(100,100,100,1)" : palette.badgeText;
|
|
187
|
+
|
|
188
|
+
return { path, textX, textY, text, bgColor, textColor };
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return badge;
|
|
192
|
+
}
|