number-flow-react-native 0.1.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.
Files changed (245) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/lib/module/core/constants.js +21 -0
  4. package/lib/module/core/constants.js.map +1 -0
  5. package/lib/module/core/intlHelpers.js +310 -0
  6. package/lib/module/core/intlHelpers.js.map +1 -0
  7. package/lib/module/core/layout.js +71 -0
  8. package/lib/module/core/layout.js.map +1 -0
  9. package/lib/module/core/mask.js +50 -0
  10. package/lib/module/core/mask.js.map +1 -0
  11. package/lib/module/core/numerals/detection.js +105 -0
  12. package/lib/module/core/numerals/detection.js.map +1 -0
  13. package/lib/module/core/numerals/digits.js +128 -0
  14. package/lib/module/core/numerals/digits.js.map +1 -0
  15. package/lib/module/core/numerals/index.js +5 -0
  16. package/lib/module/core/numerals/index.js.map +1 -0
  17. package/lib/module/core/numerals/tables.js +114 -0
  18. package/lib/module/core/numerals/tables.js.map +1 -0
  19. package/lib/module/core/superscript.js +31 -0
  20. package/lib/module/core/superscript.js.map +1 -0
  21. package/lib/module/core/timeLayout.js +98 -0
  22. package/lib/module/core/timeLayout.js.map +1 -0
  23. package/lib/module/core/timeTypes.js +4 -0
  24. package/lib/module/core/timeTypes.js.map +1 -0
  25. package/lib/module/core/timing.js +45 -0
  26. package/lib/module/core/timing.js.map +1 -0
  27. package/lib/module/core/types.js +58 -0
  28. package/lib/module/core/types.js.map +1 -0
  29. package/lib/module/core/useAccessibilityAnnouncement.js +27 -0
  30. package/lib/module/core/useAccessibilityAnnouncement.js.map +1 -0
  31. package/lib/module/core/useAnimatedX.js +25 -0
  32. package/lib/module/core/useAnimatedX.js.map +1 -0
  33. package/lib/module/core/useAnimationLifecycle.js +37 -0
  34. package/lib/module/core/useAnimationLifecycle.js.map +1 -0
  35. package/lib/module/core/useCanAnimate.js +22 -0
  36. package/lib/module/core/useCanAnimate.js.map +1 -0
  37. package/lib/module/core/useContinuousSpin.js +89 -0
  38. package/lib/module/core/useContinuousSpin.js.map +1 -0
  39. package/lib/module/core/useDebouncedWidths.js +74 -0
  40. package/lib/module/core/useDebouncedWidths.js.map +1 -0
  41. package/lib/module/core/useDigitAnimation.js +138 -0
  42. package/lib/module/core/useDigitAnimation.js.map +1 -0
  43. package/lib/module/core/useFlowPipeline.js +85 -0
  44. package/lib/module/core/useFlowPipeline.js.map +1 -0
  45. package/lib/module/core/useFormattedValue.js +28 -0
  46. package/lib/module/core/useFormattedValue.js.map +1 -0
  47. package/lib/module/core/useLayoutDiff.js +59 -0
  48. package/lib/module/core/useLayoutDiff.js.map +1 -0
  49. package/lib/module/core/useNumberFormatting.js +158 -0
  50. package/lib/module/core/useNumberFormatting.js.map +1 -0
  51. package/lib/module/core/useSlotOpacity.js +53 -0
  52. package/lib/module/core/useSlotOpacity.js.map +1 -0
  53. package/lib/module/core/useTimeFormatting.js +74 -0
  54. package/lib/module/core/useTimeFormatting.js.map +1 -0
  55. package/lib/module/core/useTimingResolution.js +21 -0
  56. package/lib/module/core/useTimingResolution.js.map +1 -0
  57. package/lib/module/core/useWorkletFormatting.js +49 -0
  58. package/lib/module/core/useWorkletFormatting.js.map +1 -0
  59. package/lib/module/core/utils.js +132 -0
  60. package/lib/module/core/utils.js.map +1 -0
  61. package/lib/module/core/warnings.js +10 -0
  62. package/lib/module/core/warnings.js.map +1 -0
  63. package/lib/module/index.js +7 -0
  64. package/lib/module/index.js.map +1 -0
  65. package/lib/module/native/DigitSlot.js +163 -0
  66. package/lib/module/native/DigitSlot.js.map +1 -0
  67. package/lib/module/native/NumberFlow.js +244 -0
  68. package/lib/module/native/NumberFlow.js.map +1 -0
  69. package/lib/module/native/SymbolSlot.js +52 -0
  70. package/lib/module/native/SymbolSlot.js.map +1 -0
  71. package/lib/module/native/TimeFlow.js +270 -0
  72. package/lib/module/native/TimeFlow.js.map +1 -0
  73. package/lib/module/native/index.js +5 -0
  74. package/lib/module/native/index.js.map +1 -0
  75. package/lib/module/native/renderSlots.js +108 -0
  76. package/lib/module/native/renderSlots.js.map +1 -0
  77. package/lib/module/native/types.js +4 -0
  78. package/lib/module/native/types.js.map +1 -0
  79. package/lib/module/native/useMeasuredGlyphMetrics.js +156 -0
  80. package/lib/module/native/useMeasuredGlyphMetrics.js.map +1 -0
  81. package/lib/module/package.json +1 -0
  82. package/lib/module/skia/DigitSlot.js +171 -0
  83. package/lib/module/skia/DigitSlot.js.map +1 -0
  84. package/lib/module/skia/SkiaNumberFlow.js +430 -0
  85. package/lib/module/skia/SkiaNumberFlow.js.map +1 -0
  86. package/lib/module/skia/SkiaTimeFlow.js +226 -0
  87. package/lib/module/skia/SkiaTimeFlow.js.map +1 -0
  88. package/lib/module/skia/SymbolSlot.js +92 -0
  89. package/lib/module/skia/SymbolSlot.js.map +1 -0
  90. package/lib/module/skia/index.js +6 -0
  91. package/lib/module/skia/index.js.map +1 -0
  92. package/lib/module/skia/renderSlots.js +131 -0
  93. package/lib/module/skia/renderSlots.js.map +1 -0
  94. package/lib/module/skia/useGlyphMetrics.js +72 -0
  95. package/lib/module/skia/useGlyphMetrics.js.map +1 -0
  96. package/lib/module/skia/useScrubbing.js +165 -0
  97. package/lib/module/skia/useScrubbing.js.map +1 -0
  98. package/lib/module/skia/useSkiaFont.js +23 -0
  99. package/lib/module/skia/useSkiaFont.js.map +1 -0
  100. package/lib/typescript/core/constants.d.ts +15 -0
  101. package/lib/typescript/core/constants.d.ts.map +1 -0
  102. package/lib/typescript/core/intlHelpers.d.ts +10 -0
  103. package/lib/typescript/core/intlHelpers.d.ts.map +1 -0
  104. package/lib/typescript/core/layout.d.ts +22 -0
  105. package/lib/typescript/core/layout.d.ts.map +1 -0
  106. package/lib/typescript/core/mask.d.ts +18 -0
  107. package/lib/typescript/core/mask.d.ts.map +1 -0
  108. package/lib/typescript/core/numerals/detection.d.ts +17 -0
  109. package/lib/typescript/core/numerals/detection.d.ts.map +1 -0
  110. package/lib/typescript/core/numerals/digits.d.ts +43 -0
  111. package/lib/typescript/core/numerals/digits.d.ts.map +1 -0
  112. package/lib/typescript/core/numerals/index.d.ts +3 -0
  113. package/lib/typescript/core/numerals/index.d.ts.map +1 -0
  114. package/lib/typescript/core/numerals/tables.d.ts +32 -0
  115. package/lib/typescript/core/numerals/tables.d.ts.map +1 -0
  116. package/lib/typescript/core/superscript.d.ts +16 -0
  117. package/lib/typescript/core/superscript.d.ts.map +1 -0
  118. package/lib/typescript/core/timeLayout.d.ts +19 -0
  119. package/lib/typescript/core/timeLayout.d.ts.map +1 -0
  120. package/lib/typescript/core/timeTypes.d.ts +80 -0
  121. package/lib/typescript/core/timeTypes.d.ts.map +1 -0
  122. package/lib/typescript/core/timing.d.ts +6 -0
  123. package/lib/typescript/core/timing.d.ts.map +1 -0
  124. package/lib/typescript/core/types.d.ts +165 -0
  125. package/lib/typescript/core/types.d.ts.map +1 -0
  126. package/lib/typescript/core/useAccessibilityAnnouncement.d.ts +10 -0
  127. package/lib/typescript/core/useAccessibilityAnnouncement.d.ts.map +1 -0
  128. package/lib/typescript/core/useAnimatedX.d.ts +9 -0
  129. package/lib/typescript/core/useAnimatedX.d.ts.map +1 -0
  130. package/lib/typescript/core/useAnimationLifecycle.d.ts +14 -0
  131. package/lib/typescript/core/useAnimationLifecycle.d.ts.map +1 -0
  132. package/lib/typescript/core/useCanAnimate.d.ts +14 -0
  133. package/lib/typescript/core/useCanAnimate.d.ts.map +1 -0
  134. package/lib/typescript/core/useContinuousSpin.d.ts +23 -0
  135. package/lib/typescript/core/useContinuousSpin.d.ts.map +1 -0
  136. package/lib/typescript/core/useDebouncedWidths.d.ts +17 -0
  137. package/lib/typescript/core/useDebouncedWidths.d.ts.map +1 -0
  138. package/lib/typescript/core/useDigitAnimation.d.ts +38 -0
  139. package/lib/typescript/core/useDigitAnimation.d.ts.map +1 -0
  140. package/lib/typescript/core/useFlowPipeline.d.ts +46 -0
  141. package/lib/typescript/core/useFlowPipeline.d.ts.map +1 -0
  142. package/lib/typescript/core/useFormattedValue.d.ts +14 -0
  143. package/lib/typescript/core/useFormattedValue.d.ts.map +1 -0
  144. package/lib/typescript/core/useLayoutDiff.d.ts +18 -0
  145. package/lib/typescript/core/useLayoutDiff.d.ts.map +1 -0
  146. package/lib/typescript/core/useNumberFormatting.d.ts +18 -0
  147. package/lib/typescript/core/useNumberFormatting.d.ts.map +1 -0
  148. package/lib/typescript/core/useSlotOpacity.d.ts +18 -0
  149. package/lib/typescript/core/useSlotOpacity.d.ts.map +1 -0
  150. package/lib/typescript/core/useTimeFormatting.d.ts +22 -0
  151. package/lib/typescript/core/useTimeFormatting.d.ts.map +1 -0
  152. package/lib/typescript/core/useTimingResolution.d.ts +13 -0
  153. package/lib/typescript/core/useTimingResolution.d.ts.map +1 -0
  154. package/lib/typescript/core/useWorkletFormatting.d.ts +14 -0
  155. package/lib/typescript/core/useWorkletFormatting.d.ts.map +1 -0
  156. package/lib/typescript/core/utils.d.ts +44 -0
  157. package/lib/typescript/core/utils.d.ts.map +1 -0
  158. package/lib/typescript/core/warnings.d.ts +2 -0
  159. package/lib/typescript/core/warnings.d.ts.map +1 -0
  160. package/lib/typescript/index.d.ts +8 -0
  161. package/lib/typescript/index.d.ts.map +1 -0
  162. package/lib/typescript/native/DigitSlot.d.ts +27 -0
  163. package/lib/typescript/native/DigitSlot.d.ts.map +1 -0
  164. package/lib/typescript/native/NumberFlow.d.ts +3 -0
  165. package/lib/typescript/native/NumberFlow.d.ts.map +1 -0
  166. package/lib/typescript/native/SymbolSlot.d.ts +19 -0
  167. package/lib/typescript/native/SymbolSlot.d.ts.map +1 -0
  168. package/lib/typescript/native/TimeFlow.d.ts +3 -0
  169. package/lib/typescript/native/TimeFlow.d.ts.map +1 -0
  170. package/lib/typescript/native/index.d.ts +3 -0
  171. package/lib/typescript/native/index.d.ts.map +1 -0
  172. package/lib/typescript/native/renderSlots.d.ts +31 -0
  173. package/lib/typescript/native/renderSlots.d.ts.map +1 -0
  174. package/lib/typescript/native/types.d.ts +36 -0
  175. package/lib/typescript/native/types.d.ts.map +1 -0
  176. package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts +8 -0
  177. package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts.map +1 -0
  178. package/lib/typescript/package.json +1 -0
  179. package/lib/typescript/skia/DigitSlot.d.ts +35 -0
  180. package/lib/typescript/skia/DigitSlot.d.ts.map +1 -0
  181. package/lib/typescript/skia/SkiaNumberFlow.d.ts +3 -0
  182. package/lib/typescript/skia/SkiaNumberFlow.d.ts.map +1 -0
  183. package/lib/typescript/skia/SkiaTimeFlow.d.ts +3 -0
  184. package/lib/typescript/skia/SkiaTimeFlow.d.ts.map +1 -0
  185. package/lib/typescript/skia/SymbolSlot.d.ts +26 -0
  186. package/lib/typescript/skia/SymbolSlot.d.ts.map +1 -0
  187. package/lib/typescript/skia/index.d.ts +6 -0
  188. package/lib/typescript/skia/index.d.ts.map +1 -0
  189. package/lib/typescript/skia/renderSlots.d.ts +40 -0
  190. package/lib/typescript/skia/renderSlots.d.ts.map +1 -0
  191. package/lib/typescript/skia/useGlyphMetrics.d.ts +16 -0
  192. package/lib/typescript/skia/useGlyphMetrics.d.ts.map +1 -0
  193. package/lib/typescript/skia/useScrubbing.d.ts +59 -0
  194. package/lib/typescript/skia/useScrubbing.d.ts.map +1 -0
  195. package/lib/typescript/skia/useSkiaFont.d.ts +13 -0
  196. package/lib/typescript/skia/useSkiaFont.d.ts.map +1 -0
  197. package/package.json +104 -0
  198. package/src/core/constants.ts +20 -0
  199. package/src/core/intlHelpers.ts +351 -0
  200. package/src/core/layout.ts +108 -0
  201. package/src/core/mask.ts +72 -0
  202. package/src/core/numerals/detection.ts +112 -0
  203. package/src/core/numerals/digits.ts +102 -0
  204. package/src/core/numerals/index.ts +9 -0
  205. package/src/core/numerals/tables.ts +112 -0
  206. package/src/core/superscript.ts +27 -0
  207. package/src/core/timeLayout.ts +119 -0
  208. package/src/core/timeTypes.ts +88 -0
  209. package/src/core/timing.ts +60 -0
  210. package/src/core/types.ts +189 -0
  211. package/src/core/useAccessibilityAnnouncement.ts +27 -0
  212. package/src/core/useAnimatedX.ts +30 -0
  213. package/src/core/useAnimationLifecycle.ts +54 -0
  214. package/src/core/useCanAnimate.ts +21 -0
  215. package/src/core/useContinuousSpin.ts +112 -0
  216. package/src/core/useDebouncedWidths.ts +93 -0
  217. package/src/core/useDigitAnimation.ts +192 -0
  218. package/src/core/useFlowPipeline.ts +126 -0
  219. package/src/core/useFormattedValue.ts +32 -0
  220. package/src/core/useLayoutDiff.ts +71 -0
  221. package/src/core/useNumberFormatting.ts +164 -0
  222. package/src/core/useSlotOpacity.ts +66 -0
  223. package/src/core/useTimeFormatting.ts +95 -0
  224. package/src/core/useTimingResolution.ts +47 -0
  225. package/src/core/useWorkletFormatting.ts +59 -0
  226. package/src/core/utils.ts +149 -0
  227. package/src/core/warnings.ts +8 -0
  228. package/src/index.ts +15 -0
  229. package/src/native/DigitSlot.tsx +203 -0
  230. package/src/native/NumberFlow.tsx +287 -0
  231. package/src/native/SymbolSlot.tsx +68 -0
  232. package/src/native/TimeFlow.tsx +287 -0
  233. package/src/native/index.ts +2 -0
  234. package/src/native/renderSlots.tsx +150 -0
  235. package/src/native/types.ts +40 -0
  236. package/src/native/useMeasuredGlyphMetrics.tsx +205 -0
  237. package/src/skia/DigitSlot.tsx +221 -0
  238. package/src/skia/SkiaNumberFlow.tsx +506 -0
  239. package/src/skia/SkiaTimeFlow.tsx +257 -0
  240. package/src/skia/SymbolSlot.tsx +120 -0
  241. package/src/skia/index.ts +5 -0
  242. package/src/skia/renderSlots.tsx +180 -0
  243. package/src/skia/useGlyphMetrics.ts +79 -0
  244. package/src/skia/useScrubbing.ts +223 -0
  245. package/src/skia/useSkiaFont.ts +25 -0
@@ -0,0 +1,506 @@
1
+ import { Group, LinearGradient, Paint, Rect as SkiaRect, vec } from "@shopify/react-native-skia";
2
+ import { useMemo } from "react";
3
+ import type { SharedValue } from "react-native-reanimated";
4
+ import { MASK_WIDTH_RATIO } from "../core/constants";
5
+ import { computeKeyedLayout, computeStringLayout, type CharLayout } from "../core/layout";
6
+ import { detectNumberingSystem, getDigitStrings, getZeroCodePoint } from "../core/numerals";
7
+ import type { GlyphMetrics, KeyedPart, SkiaNumberFlowProps } from "../core/types";
8
+ import { useAccessibilityAnnouncement } from "../core/useAccessibilityAnnouncement";
9
+ import { useFlowPipeline } from "../core/useFlowPipeline";
10
+ import { getFormatCharacters } from "../core/intlHelpers";
11
+ import { useNumberFormatting } from "../core/useNumberFormatting";
12
+ import { getDigitCount } from "../core/utils";
13
+ import { warnOnce } from "../core/warnings";
14
+ import { renderSlots } from "./renderSlots";
15
+ import { useGlyphMetrics } from "./useGlyphMetrics";
16
+ import { useScrubbingBridge, useScrubbingLayout } from "./useScrubbing";
17
+
18
+ interface ModeBaseProps {
19
+ format: Intl.NumberFormatOptions | undefined;
20
+ locales: Intl.LocalesArgument | undefined;
21
+ font: NonNullable<SkiaNumberFlowProps["font"]>;
22
+ color: string;
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ textAlign: "left" | "right" | "center";
27
+ prefix: string;
28
+ suffix: string;
29
+ opacity: SkiaNumberFlowProps["opacity"];
30
+ spinTiming: SkiaNumberFlowProps["spinTiming"];
31
+ opacityTiming: SkiaNumberFlowProps["opacityTiming"];
32
+ transformTiming: SkiaNumberFlowProps["transformTiming"];
33
+ trend: SkiaNumberFlowProps["trend"];
34
+ animated: SkiaNumberFlowProps["animated"];
35
+ respectMotionPreference: SkiaNumberFlowProps["respectMotionPreference"];
36
+ continuous: SkiaNumberFlowProps["continuous"];
37
+ digits: SkiaNumberFlowProps["digits"];
38
+ scrubDigitWidthPercentile: number;
39
+ onAnimationsStart: SkiaNumberFlowProps["onAnimationsStart"];
40
+ onAnimationsFinish: SkiaNumberFlowProps["onAnimationsFinish"];
41
+ mask: SkiaNumberFlowProps["mask"];
42
+ metrics: GlyphMetrics;
43
+ digitStringsArr: string[];
44
+ zeroCodePoint: number;
45
+ }
46
+
47
+ interface ValueModeProps extends ModeBaseProps {
48
+ value: number;
49
+ }
50
+
51
+ interface SharedModeProps extends ModeBaseProps {
52
+ sharedValue: NonNullable<SkiaNumberFlowProps["sharedValue"]>;
53
+ }
54
+
55
+ interface RuntimeProps {
56
+ keyedParts: KeyedPart[];
57
+ trendValue: number | undefined;
58
+ layout: CharLayout[];
59
+ metrics: GlyphMetrics;
60
+ font: NonNullable<SkiaNumberFlowProps["font"]>;
61
+ color: string;
62
+ x: number;
63
+ y: number;
64
+ opacity: SkiaNumberFlowProps["opacity"];
65
+ spinTiming: SkiaNumberFlowProps["spinTiming"];
66
+ opacityTiming: SkiaNumberFlowProps["opacityTiming"];
67
+ transformTiming: SkiaNumberFlowProps["transformTiming"];
68
+ trend: SkiaNumberFlowProps["trend"];
69
+ animated: SkiaNumberFlowProps["animated"];
70
+ respectMotionPreference: SkiaNumberFlowProps["respectMotionPreference"];
71
+ continuous: SkiaNumberFlowProps["continuous"];
72
+ digits: SkiaNumberFlowProps["digits"];
73
+ onAnimationsStart: SkiaNumberFlowProps["onAnimationsStart"];
74
+ onAnimationsFinish: SkiaNumberFlowProps["onAnimationsFinish"];
75
+ mask: SkiaNumberFlowProps["mask"];
76
+ digitStringsArr: string[];
77
+ workletDigitValues?: SharedValue<number>[] | null;
78
+ workletLayout?: SharedValue<{ x: number; width: number }[]>;
79
+ }
80
+
81
+ function SkiaNumberFlowRuntime({
82
+ keyedParts,
83
+ trendValue,
84
+ layout,
85
+ metrics,
86
+ font,
87
+ color,
88
+ x,
89
+ y,
90
+ opacity,
91
+ spinTiming,
92
+ opacityTiming,
93
+ transformTiming,
94
+ trend,
95
+ animated,
96
+ respectMotionPreference,
97
+ continuous,
98
+ digits,
99
+ onAnimationsStart,
100
+ onAnimationsFinish,
101
+ mask,
102
+ digitStringsArr,
103
+ workletDigitValues,
104
+ workletLayout,
105
+ }: RuntimeProps) {
106
+ const {
107
+ resolvedSpinTiming,
108
+ resolvedOpacityTiming,
109
+ resolvedTransformTiming,
110
+ resolvedTrend,
111
+ spinGenerations,
112
+ prevMap,
113
+ isInitialRender,
114
+ exitingEntries,
115
+ onExitComplete,
116
+ accessibilityLabel,
117
+ adaptiveMask,
118
+ } = useFlowPipeline({
119
+ keyedParts,
120
+ trendValue,
121
+ layout,
122
+ metrics,
123
+ animated,
124
+ respectMotionPreference,
125
+ spinTiming,
126
+ opacityTiming,
127
+ transformTiming,
128
+ trend,
129
+ continuous,
130
+ mask,
131
+ onAnimationsStart,
132
+ onAnimationsFinish,
133
+ });
134
+
135
+ useAccessibilityAnnouncement(accessibilityLabel);
136
+
137
+ if (layout.length === 0 && exitingEntries.size === 0) {
138
+ return <Group />;
139
+ }
140
+
141
+ const baseY = y;
142
+ const resolvedMask = mask ?? true;
143
+
144
+ const maskTopHeight = resolvedMask ? adaptiveMask.top : 0;
145
+ const maskBottomHeight = resolvedMask ? adaptiveMask.bottom : 0;
146
+ const maskWidth = resolvedMask ? MASK_WIDTH_RATIO * metrics.lineHeight : 0;
147
+
148
+ // Content bounds in the content group's local coordinate space
149
+ const contentLeft = layout.reduce((min, entry) => Math.min(min, entry.x), Infinity);
150
+ const contentRight = layout.reduce((max, entry) => Math.max(max, entry.x + entry.width), 0);
151
+ const contentWidth = layout.length > 0 ? contentRight - contentLeft : 0;
152
+
153
+ const content = (
154
+ <Group transform={[{ translateX: x }]}>
155
+ {renderSlots({
156
+ layout,
157
+ exitingEntries,
158
+ prevMap,
159
+ isInitialRender,
160
+ onExitComplete,
161
+ metrics,
162
+ font,
163
+ color,
164
+ baseY,
165
+ resolvedTrend,
166
+ spinTiming: resolvedSpinTiming,
167
+ opacityTiming: resolvedOpacityTiming,
168
+ transformTiming: resolvedTransformTiming,
169
+ spinGenerations,
170
+ digitCountResolver: (key) => getDigitCount(digits, key),
171
+ maskTop: maskTopHeight,
172
+ maskBottom: maskBottomHeight,
173
+ digitStrings: digitStringsArr,
174
+ workletDigitValues,
175
+ workletLayout,
176
+ })}
177
+ </Group>
178
+ );
179
+
180
+ /**
181
+ * Container-level 2D gradient mask matching web NumberFlow's vignette.
182
+ * Two DstIn-blended rects compose independent horizontal and vertical fades:
183
+ * final_alpha = content_alpha * horizontal_alpha * vertical_alpha.
184
+ * This produces smooth corners naturally (alpha multiplication).
185
+ *
186
+ * Architecture: content draws into a saveLayer, then each gradient rect
187
+ * composites with DstIn (result = existing_content * gradient_alpha).
188
+ */
189
+ // Horizontal: fade extends OUTSIDE text edges (for enter/exit animations)
190
+ // Vertical: fade is WITHIN the text line height (digits roll through it)
191
+ const maskLeft = x + contentLeft - maskWidth;
192
+ const maskRight = x + contentRight + maskWidth;
193
+ const maskY = baseY + metrics.ascent;
194
+ const maskTotalWidth = contentWidth + 2 * maskWidth;
195
+ const maskTotalHeight = metrics.lineHeight;
196
+ const hRatio = maskTotalWidth > 0 ? maskWidth / maskTotalWidth : 0;
197
+ const vRatioTop = maskTotalHeight > 0 ? maskTopHeight / maskTotalHeight : 0;
198
+ const vRatioBottom = maskTotalHeight > 0 ? maskBottomHeight / maskTotalHeight : 0;
199
+
200
+ const maskedContent = resolvedMask ? (
201
+ <Group layer={<Paint />}>
202
+ {content}
203
+
204
+ {/* Horizontal fade */}
205
+ <Group layer={<Paint blendMode="dstIn" />}>
206
+ <SkiaRect height={maskTotalHeight} width={maskTotalWidth} x={maskLeft} y={maskY}>
207
+ <LinearGradient
208
+ colors={["transparent", "black", "black", "transparent"]}
209
+ end={vec(maskRight, 0)}
210
+ positions={[0, hRatio, 1 - hRatio, 1]}
211
+ start={vec(maskLeft, 0)}
212
+ />
213
+ </SkiaRect>
214
+ </Group>
215
+
216
+ {/* Vertical fade */}
217
+ <Group layer={<Paint blendMode="dstIn" />}>
218
+ <SkiaRect height={maskTotalHeight} width={maskTotalWidth} x={maskLeft} y={maskY}>
219
+ <LinearGradient
220
+ colors={["transparent", "black", "black", "transparent"]}
221
+ end={vec(0, maskY + maskTotalHeight)}
222
+ positions={[0, vRatioTop, 1 - vRatioBottom, 1]}
223
+ start={vec(0, maskY)}
224
+ />
225
+ </SkiaRect>
226
+ </Group>
227
+ </Group>
228
+ ) : (
229
+ content
230
+ );
231
+
232
+ if (opacity) {
233
+ return <Group layer={<Paint opacity={opacity} />}>{maskedContent}</Group>;
234
+ }
235
+
236
+ return maskedContent;
237
+ }
238
+
239
+ function SkiaNumberFlowValueMode({
240
+ value,
241
+ format,
242
+ locales,
243
+ font,
244
+ color,
245
+ x,
246
+ y,
247
+ width,
248
+ textAlign,
249
+ prefix,
250
+ suffix,
251
+ opacity,
252
+ spinTiming,
253
+ opacityTiming,
254
+ transformTiming,
255
+ trend,
256
+ animated,
257
+ respectMotionPreference,
258
+ continuous,
259
+ digits,
260
+ onAnimationsStart,
261
+ onAnimationsFinish,
262
+ mask,
263
+ metrics,
264
+ digitStringsArr,
265
+ }: ValueModeProps) {
266
+ const keyedParts = useNumberFormatting(value, format, locales, prefix, suffix);
267
+
268
+ const layout = useMemo(() => {
269
+ if (keyedParts.length === 0) return [];
270
+ return computeKeyedLayout(keyedParts, metrics, width, textAlign, digitStringsArr);
271
+ }, [keyedParts, metrics, width, textAlign, digitStringsArr]);
272
+
273
+ return (
274
+ <SkiaNumberFlowRuntime
275
+ animated={animated}
276
+ color={color}
277
+ continuous={continuous}
278
+ digitStringsArr={digitStringsArr}
279
+ digits={digits}
280
+ font={font}
281
+ keyedParts={keyedParts}
282
+ layout={layout}
283
+ mask={mask}
284
+ metrics={metrics}
285
+ onAnimationsFinish={onAnimationsFinish}
286
+ onAnimationsStart={onAnimationsStart}
287
+ opacity={opacity}
288
+ opacityTiming={opacityTiming}
289
+ respectMotionPreference={respectMotionPreference}
290
+ spinTiming={spinTiming}
291
+ transformTiming={transformTiming}
292
+ trend={trend}
293
+ trendValue={value}
294
+ x={x}
295
+ y={y}
296
+ />
297
+ );
298
+ }
299
+
300
+ function SkiaNumberFlowSharedMode({
301
+ sharedValue,
302
+ format,
303
+ locales,
304
+ font,
305
+ color,
306
+ x,
307
+ y,
308
+ width,
309
+ textAlign,
310
+ prefix,
311
+ suffix,
312
+ opacity,
313
+ spinTiming,
314
+ opacityTiming,
315
+ transformTiming,
316
+ trend,
317
+ animated,
318
+ respectMotionPreference,
319
+ continuous,
320
+ digits,
321
+ scrubDigitWidthPercentile,
322
+ onAnimationsStart,
323
+ onAnimationsFinish,
324
+ mask,
325
+ metrics,
326
+ digitStringsArr,
327
+ zeroCodePoint,
328
+ }: SharedModeProps) {
329
+ // Scrubbing bridge: digit-count bridging between worklet and JS thread
330
+ const { effectiveValue } = useScrubbingBridge({
331
+ sharedValue,
332
+ value: undefined,
333
+ prefix,
334
+ suffix,
335
+ zeroCodePoint,
336
+ });
337
+
338
+ const keyedParts = useNumberFormatting(effectiveValue, format, locales, prefix, suffix);
339
+
340
+ const layout = useMemo(() => {
341
+ const text = `${prefix}${sharedValue.value}${suffix}`;
342
+ return computeStringLayout(text, metrics, width, textAlign);
343
+ }, [metrics, width, textAlign, prefix, suffix, sharedValue]);
344
+
345
+ const layoutDigitCount = useMemo(() => {
346
+ let count = 0;
347
+ for (let i = 0; i < layout.length; i++) {
348
+ if (layout[i].isDigit) count++;
349
+ }
350
+ return count;
351
+ }, [layout]);
352
+
353
+ // Scrubbing layout: worklet-driven digit values and per-slot positioning
354
+ const { workletDigitValues, workletLayout } = useScrubbingLayout({
355
+ sharedValue,
356
+ prefix,
357
+ suffix,
358
+ zeroCodePoint,
359
+ metrics,
360
+ digitStringsArr,
361
+ scrubDigitWidthPercentile,
362
+ layout,
363
+ layoutDigitCount,
364
+ width,
365
+ textAlign,
366
+ });
367
+
368
+ return (
369
+ <SkiaNumberFlowRuntime
370
+ animated={animated}
371
+ color={color}
372
+ continuous={continuous}
373
+ digitStringsArr={digitStringsArr}
374
+ digits={digits}
375
+ font={font}
376
+ keyedParts={keyedParts}
377
+ layout={layout}
378
+ mask={mask}
379
+ metrics={metrics}
380
+ onAnimationsFinish={onAnimationsFinish}
381
+ onAnimationsStart={onAnimationsStart}
382
+ opacity={opacity}
383
+ opacityTiming={opacityTiming}
384
+ respectMotionPreference={respectMotionPreference}
385
+ spinTiming={spinTiming}
386
+ transformTiming={transformTiming}
387
+ trend={trend}
388
+ trendValue={undefined}
389
+ workletDigitValues={workletDigitValues}
390
+ workletLayout={workletLayout}
391
+ x={x}
392
+ y={y}
393
+ />
394
+ );
395
+ }
396
+
397
+ export const SkiaNumberFlow = ({
398
+ value,
399
+ format,
400
+ locales,
401
+ sharedValue,
402
+ font,
403
+ color = "#000000",
404
+ x = 0,
405
+ y = 0,
406
+ width = 0,
407
+ textAlign = "left",
408
+ prefix = "",
409
+ suffix = "",
410
+ opacity,
411
+ spinTiming,
412
+ opacityTiming,
413
+ transformTiming,
414
+ trend,
415
+ animated,
416
+ respectMotionPreference,
417
+ continuous,
418
+ digits,
419
+ scrubDigitWidthPercentile = 0.75,
420
+ onAnimationsStart,
421
+ onAnimationsFinish,
422
+ mask,
423
+ }: SkiaNumberFlowProps) => {
424
+ const formatChars = useMemo(
425
+ () => getFormatCharacters(locales, format, prefix, suffix),
426
+ [locales, format, prefix, suffix],
427
+ );
428
+ const numberingSystem = useMemo(() => detectNumberingSystem(locales, format), [locales, format]);
429
+ const zeroCodePoint = getZeroCodePoint(numberingSystem);
430
+ const digitStringsArr = useMemo(() => getDigitStrings(numberingSystem), [numberingSystem]);
431
+ const metrics = useGlyphMetrics(font, formatChars, digitStringsArr);
432
+
433
+ if (__DEV__) {
434
+ if (!font) {
435
+ warnOnce(
436
+ "skia-font",
437
+ "font is null — pass a loaded SkFont from useFont(). Component renders empty until font loads.",
438
+ );
439
+ }
440
+ if (value !== undefined && sharedValue !== undefined) {
441
+ warnOnce("skia-nf-both", "Both value and sharedValue provided. Use one or the other.");
442
+ }
443
+ if (value === undefined && sharedValue === undefined) {
444
+ warnOnce("skia-nf-neither", "Neither value nor sharedValue provided.");
445
+ }
446
+ if (scrubDigitWidthPercentile < 0 || scrubDigitWidthPercentile > 1) {
447
+ warnOnce("nf-percentile", "scrubDigitWidthPercentile should be between 0 and 1.");
448
+ }
449
+ if (digits) {
450
+ for (const [posStr, constraint] of Object.entries(digits)) {
451
+ if (constraint.max < 1 || constraint.max > 9) {
452
+ warnOnce(
453
+ `skia-nf-digit-max-${posStr}`,
454
+ `digits[${posStr}].max must be between 1 and 9, got ${constraint.max}.`,
455
+ );
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+ const clampedPercentile = Math.max(0, Math.min(1, scrubDigitWidthPercentile));
462
+
463
+ if (!font || !metrics) {
464
+ return <Group />;
465
+ }
466
+
467
+ const baseProps: ModeBaseProps = {
468
+ format,
469
+ locales,
470
+ font,
471
+ color,
472
+ x,
473
+ y,
474
+ width,
475
+ textAlign,
476
+ prefix,
477
+ suffix,
478
+ opacity,
479
+ spinTiming,
480
+ opacityTiming,
481
+ transformTiming,
482
+ trend,
483
+ animated,
484
+ respectMotionPreference,
485
+ continuous,
486
+ digits,
487
+ scrubDigitWidthPercentile: clampedPercentile,
488
+ onAnimationsStart,
489
+ onAnimationsFinish,
490
+ mask,
491
+ metrics,
492
+ digitStringsArr,
493
+ zeroCodePoint,
494
+ };
495
+
496
+ // Shared-value mode mounts scrubbing hooks; value mode avoids them entirely.
497
+ if (sharedValue !== undefined && value === undefined) {
498
+ return <SkiaNumberFlowSharedMode {...baseProps} sharedValue={sharedValue} />;
499
+ }
500
+
501
+ if (value === undefined) {
502
+ return <Group />;
503
+ }
504
+
505
+ return <SkiaNumberFlowValueMode {...baseProps} value={value} />;
506
+ };