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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/lib/module/core/constants.js +21 -0
- package/lib/module/core/constants.js.map +1 -0
- package/lib/module/core/intlHelpers.js +310 -0
- package/lib/module/core/intlHelpers.js.map +1 -0
- package/lib/module/core/layout.js +71 -0
- package/lib/module/core/layout.js.map +1 -0
- package/lib/module/core/mask.js +50 -0
- package/lib/module/core/mask.js.map +1 -0
- package/lib/module/core/numerals/detection.js +105 -0
- package/lib/module/core/numerals/detection.js.map +1 -0
- package/lib/module/core/numerals/digits.js +128 -0
- package/lib/module/core/numerals/digits.js.map +1 -0
- package/lib/module/core/numerals/index.js +5 -0
- package/lib/module/core/numerals/index.js.map +1 -0
- package/lib/module/core/numerals/tables.js +114 -0
- package/lib/module/core/numerals/tables.js.map +1 -0
- package/lib/module/core/superscript.js +31 -0
- package/lib/module/core/superscript.js.map +1 -0
- package/lib/module/core/timeLayout.js +98 -0
- package/lib/module/core/timeLayout.js.map +1 -0
- package/lib/module/core/timeTypes.js +4 -0
- package/lib/module/core/timeTypes.js.map +1 -0
- package/lib/module/core/timing.js +45 -0
- package/lib/module/core/timing.js.map +1 -0
- package/lib/module/core/types.js +58 -0
- package/lib/module/core/types.js.map +1 -0
- package/lib/module/core/useAccessibilityAnnouncement.js +27 -0
- package/lib/module/core/useAccessibilityAnnouncement.js.map +1 -0
- package/lib/module/core/useAnimatedX.js +25 -0
- package/lib/module/core/useAnimatedX.js.map +1 -0
- package/lib/module/core/useAnimationLifecycle.js +37 -0
- package/lib/module/core/useAnimationLifecycle.js.map +1 -0
- package/lib/module/core/useCanAnimate.js +22 -0
- package/lib/module/core/useCanAnimate.js.map +1 -0
- package/lib/module/core/useContinuousSpin.js +89 -0
- package/lib/module/core/useContinuousSpin.js.map +1 -0
- package/lib/module/core/useDebouncedWidths.js +74 -0
- package/lib/module/core/useDebouncedWidths.js.map +1 -0
- package/lib/module/core/useDigitAnimation.js +138 -0
- package/lib/module/core/useDigitAnimation.js.map +1 -0
- package/lib/module/core/useFlowPipeline.js +85 -0
- package/lib/module/core/useFlowPipeline.js.map +1 -0
- package/lib/module/core/useFormattedValue.js +28 -0
- package/lib/module/core/useFormattedValue.js.map +1 -0
- package/lib/module/core/useLayoutDiff.js +59 -0
- package/lib/module/core/useLayoutDiff.js.map +1 -0
- package/lib/module/core/useNumberFormatting.js +158 -0
- package/lib/module/core/useNumberFormatting.js.map +1 -0
- package/lib/module/core/useSlotOpacity.js +53 -0
- package/lib/module/core/useSlotOpacity.js.map +1 -0
- package/lib/module/core/useTimeFormatting.js +74 -0
- package/lib/module/core/useTimeFormatting.js.map +1 -0
- package/lib/module/core/useTimingResolution.js +21 -0
- package/lib/module/core/useTimingResolution.js.map +1 -0
- package/lib/module/core/useWorkletFormatting.js +49 -0
- package/lib/module/core/useWorkletFormatting.js.map +1 -0
- package/lib/module/core/utils.js +132 -0
- package/lib/module/core/utils.js.map +1 -0
- package/lib/module/core/warnings.js +10 -0
- package/lib/module/core/warnings.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/DigitSlot.js +163 -0
- package/lib/module/native/DigitSlot.js.map +1 -0
- package/lib/module/native/NumberFlow.js +244 -0
- package/lib/module/native/NumberFlow.js.map +1 -0
- package/lib/module/native/SymbolSlot.js +52 -0
- package/lib/module/native/SymbolSlot.js.map +1 -0
- package/lib/module/native/TimeFlow.js +270 -0
- package/lib/module/native/TimeFlow.js.map +1 -0
- package/lib/module/native/index.js +5 -0
- package/lib/module/native/index.js.map +1 -0
- package/lib/module/native/renderSlots.js +108 -0
- package/lib/module/native/renderSlots.js.map +1 -0
- package/lib/module/native/types.js +4 -0
- package/lib/module/native/types.js.map +1 -0
- package/lib/module/native/useMeasuredGlyphMetrics.js +156 -0
- package/lib/module/native/useMeasuredGlyphMetrics.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/skia/DigitSlot.js +171 -0
- package/lib/module/skia/DigitSlot.js.map +1 -0
- package/lib/module/skia/SkiaNumberFlow.js +430 -0
- package/lib/module/skia/SkiaNumberFlow.js.map +1 -0
- package/lib/module/skia/SkiaTimeFlow.js +226 -0
- package/lib/module/skia/SkiaTimeFlow.js.map +1 -0
- package/lib/module/skia/SymbolSlot.js +92 -0
- package/lib/module/skia/SymbolSlot.js.map +1 -0
- package/lib/module/skia/index.js +6 -0
- package/lib/module/skia/index.js.map +1 -0
- package/lib/module/skia/renderSlots.js +131 -0
- package/lib/module/skia/renderSlots.js.map +1 -0
- package/lib/module/skia/useGlyphMetrics.js +72 -0
- package/lib/module/skia/useGlyphMetrics.js.map +1 -0
- package/lib/module/skia/useScrubbing.js +165 -0
- package/lib/module/skia/useScrubbing.js.map +1 -0
- package/lib/module/skia/useSkiaFont.js +23 -0
- package/lib/module/skia/useSkiaFont.js.map +1 -0
- package/lib/typescript/core/constants.d.ts +15 -0
- package/lib/typescript/core/constants.d.ts.map +1 -0
- package/lib/typescript/core/intlHelpers.d.ts +10 -0
- package/lib/typescript/core/intlHelpers.d.ts.map +1 -0
- package/lib/typescript/core/layout.d.ts +22 -0
- package/lib/typescript/core/layout.d.ts.map +1 -0
- package/lib/typescript/core/mask.d.ts +18 -0
- package/lib/typescript/core/mask.d.ts.map +1 -0
- package/lib/typescript/core/numerals/detection.d.ts +17 -0
- package/lib/typescript/core/numerals/detection.d.ts.map +1 -0
- package/lib/typescript/core/numerals/digits.d.ts +43 -0
- package/lib/typescript/core/numerals/digits.d.ts.map +1 -0
- package/lib/typescript/core/numerals/index.d.ts +3 -0
- package/lib/typescript/core/numerals/index.d.ts.map +1 -0
- package/lib/typescript/core/numerals/tables.d.ts +32 -0
- package/lib/typescript/core/numerals/tables.d.ts.map +1 -0
- package/lib/typescript/core/superscript.d.ts +16 -0
- package/lib/typescript/core/superscript.d.ts.map +1 -0
- package/lib/typescript/core/timeLayout.d.ts +19 -0
- package/lib/typescript/core/timeLayout.d.ts.map +1 -0
- package/lib/typescript/core/timeTypes.d.ts +80 -0
- package/lib/typescript/core/timeTypes.d.ts.map +1 -0
- package/lib/typescript/core/timing.d.ts +6 -0
- package/lib/typescript/core/timing.d.ts.map +1 -0
- package/lib/typescript/core/types.d.ts +165 -0
- package/lib/typescript/core/types.d.ts.map +1 -0
- package/lib/typescript/core/useAccessibilityAnnouncement.d.ts +10 -0
- package/lib/typescript/core/useAccessibilityAnnouncement.d.ts.map +1 -0
- package/lib/typescript/core/useAnimatedX.d.ts +9 -0
- package/lib/typescript/core/useAnimatedX.d.ts.map +1 -0
- package/lib/typescript/core/useAnimationLifecycle.d.ts +14 -0
- package/lib/typescript/core/useAnimationLifecycle.d.ts.map +1 -0
- package/lib/typescript/core/useCanAnimate.d.ts +14 -0
- package/lib/typescript/core/useCanAnimate.d.ts.map +1 -0
- package/lib/typescript/core/useContinuousSpin.d.ts +23 -0
- package/lib/typescript/core/useContinuousSpin.d.ts.map +1 -0
- package/lib/typescript/core/useDebouncedWidths.d.ts +17 -0
- package/lib/typescript/core/useDebouncedWidths.d.ts.map +1 -0
- package/lib/typescript/core/useDigitAnimation.d.ts +38 -0
- package/lib/typescript/core/useDigitAnimation.d.ts.map +1 -0
- package/lib/typescript/core/useFlowPipeline.d.ts +46 -0
- package/lib/typescript/core/useFlowPipeline.d.ts.map +1 -0
- package/lib/typescript/core/useFormattedValue.d.ts +14 -0
- package/lib/typescript/core/useFormattedValue.d.ts.map +1 -0
- package/lib/typescript/core/useLayoutDiff.d.ts +18 -0
- package/lib/typescript/core/useLayoutDiff.d.ts.map +1 -0
- package/lib/typescript/core/useNumberFormatting.d.ts +18 -0
- package/lib/typescript/core/useNumberFormatting.d.ts.map +1 -0
- package/lib/typescript/core/useSlotOpacity.d.ts +18 -0
- package/lib/typescript/core/useSlotOpacity.d.ts.map +1 -0
- package/lib/typescript/core/useTimeFormatting.d.ts +22 -0
- package/lib/typescript/core/useTimeFormatting.d.ts.map +1 -0
- package/lib/typescript/core/useTimingResolution.d.ts +13 -0
- package/lib/typescript/core/useTimingResolution.d.ts.map +1 -0
- package/lib/typescript/core/useWorkletFormatting.d.ts +14 -0
- package/lib/typescript/core/useWorkletFormatting.d.ts.map +1 -0
- package/lib/typescript/core/utils.d.ts +44 -0
- package/lib/typescript/core/utils.d.ts.map +1 -0
- package/lib/typescript/core/warnings.d.ts +2 -0
- package/lib/typescript/core/warnings.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/native/DigitSlot.d.ts +27 -0
- package/lib/typescript/native/DigitSlot.d.ts.map +1 -0
- package/lib/typescript/native/NumberFlow.d.ts +3 -0
- package/lib/typescript/native/NumberFlow.d.ts.map +1 -0
- package/lib/typescript/native/SymbolSlot.d.ts +19 -0
- package/lib/typescript/native/SymbolSlot.d.ts.map +1 -0
- package/lib/typescript/native/TimeFlow.d.ts +3 -0
- package/lib/typescript/native/TimeFlow.d.ts.map +1 -0
- package/lib/typescript/native/index.d.ts +3 -0
- package/lib/typescript/native/index.d.ts.map +1 -0
- package/lib/typescript/native/renderSlots.d.ts +31 -0
- package/lib/typescript/native/renderSlots.d.ts.map +1 -0
- package/lib/typescript/native/types.d.ts +36 -0
- package/lib/typescript/native/types.d.ts.map +1 -0
- package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts +8 -0
- package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/skia/DigitSlot.d.ts +35 -0
- package/lib/typescript/skia/DigitSlot.d.ts.map +1 -0
- package/lib/typescript/skia/SkiaNumberFlow.d.ts +3 -0
- package/lib/typescript/skia/SkiaNumberFlow.d.ts.map +1 -0
- package/lib/typescript/skia/SkiaTimeFlow.d.ts +3 -0
- package/lib/typescript/skia/SkiaTimeFlow.d.ts.map +1 -0
- package/lib/typescript/skia/SymbolSlot.d.ts +26 -0
- package/lib/typescript/skia/SymbolSlot.d.ts.map +1 -0
- package/lib/typescript/skia/index.d.ts +6 -0
- package/lib/typescript/skia/index.d.ts.map +1 -0
- package/lib/typescript/skia/renderSlots.d.ts +40 -0
- package/lib/typescript/skia/renderSlots.d.ts.map +1 -0
- package/lib/typescript/skia/useGlyphMetrics.d.ts +16 -0
- package/lib/typescript/skia/useGlyphMetrics.d.ts.map +1 -0
- package/lib/typescript/skia/useScrubbing.d.ts +59 -0
- package/lib/typescript/skia/useScrubbing.d.ts.map +1 -0
- package/lib/typescript/skia/useSkiaFont.d.ts +13 -0
- package/lib/typescript/skia/useSkiaFont.d.ts.map +1 -0
- package/package.json +104 -0
- package/src/core/constants.ts +20 -0
- package/src/core/intlHelpers.ts +351 -0
- package/src/core/layout.ts +108 -0
- package/src/core/mask.ts +72 -0
- package/src/core/numerals/detection.ts +112 -0
- package/src/core/numerals/digits.ts +102 -0
- package/src/core/numerals/index.ts +9 -0
- package/src/core/numerals/tables.ts +112 -0
- package/src/core/superscript.ts +27 -0
- package/src/core/timeLayout.ts +119 -0
- package/src/core/timeTypes.ts +88 -0
- package/src/core/timing.ts +60 -0
- package/src/core/types.ts +189 -0
- package/src/core/useAccessibilityAnnouncement.ts +27 -0
- package/src/core/useAnimatedX.ts +30 -0
- package/src/core/useAnimationLifecycle.ts +54 -0
- package/src/core/useCanAnimate.ts +21 -0
- package/src/core/useContinuousSpin.ts +112 -0
- package/src/core/useDebouncedWidths.ts +93 -0
- package/src/core/useDigitAnimation.ts +192 -0
- package/src/core/useFlowPipeline.ts +126 -0
- package/src/core/useFormattedValue.ts +32 -0
- package/src/core/useLayoutDiff.ts +71 -0
- package/src/core/useNumberFormatting.ts +164 -0
- package/src/core/useSlotOpacity.ts +66 -0
- package/src/core/useTimeFormatting.ts +95 -0
- package/src/core/useTimingResolution.ts +47 -0
- package/src/core/useWorkletFormatting.ts +59 -0
- package/src/core/utils.ts +149 -0
- package/src/core/warnings.ts +8 -0
- package/src/index.ts +15 -0
- package/src/native/DigitSlot.tsx +203 -0
- package/src/native/NumberFlow.tsx +287 -0
- package/src/native/SymbolSlot.tsx +68 -0
- package/src/native/TimeFlow.tsx +287 -0
- package/src/native/index.ts +2 -0
- package/src/native/renderSlots.tsx +150 -0
- package/src/native/types.ts +40 -0
- package/src/native/useMeasuredGlyphMetrics.tsx +205 -0
- package/src/skia/DigitSlot.tsx +221 -0
- package/src/skia/SkiaNumberFlow.tsx +506 -0
- package/src/skia/SkiaTimeFlow.tsx +257 -0
- package/src/skia/SymbolSlot.tsx +120 -0
- package/src/skia/index.ts +5 -0
- package/src/skia/renderSlots.tsx +180 -0
- package/src/skia/useGlyphMetrics.ts +79 -0
- package/src/skia/useScrubbing.ts +223 -0
- package/src/skia/useSkiaFont.ts +25 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { digitPart, symbolPart, type KeyedPart } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts 24h hours (0-23) to 12h display value (1-12).
|
|
6
|
+
* 0 → 12 (12 AM), 1-12 → as-is, 13-23 → subtract 12.
|
|
7
|
+
*/
|
|
8
|
+
export function to12Hour(h: number): number {
|
|
9
|
+
if (h === 0) return 12;
|
|
10
|
+
if (h > 12) return h - 12;
|
|
11
|
+
return h;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds a time display into stably-keyed character parts.
|
|
16
|
+
*
|
|
17
|
+
* Each digit position gets a fixed semantic key (h10, h1, m10, m1, s10, s1)
|
|
18
|
+
* regardless of the actual value. This ensures correct enter/exit animation
|
|
19
|
+
* behavior — e.g. when hours go from 9→10, the h10 key *enters* (fades in)
|
|
20
|
+
* rather than shifting all existing positions.
|
|
21
|
+
*/
|
|
22
|
+
export function formatTimeToKeyedParts(
|
|
23
|
+
hours: number | undefined,
|
|
24
|
+
minutes: number,
|
|
25
|
+
seconds: number | undefined,
|
|
26
|
+
is24Hour: boolean,
|
|
27
|
+
padHours: boolean,
|
|
28
|
+
): KeyedPart[] {
|
|
29
|
+
const parts: KeyedPart[] = [];
|
|
30
|
+
|
|
31
|
+
const hasHours = hours !== undefined;
|
|
32
|
+
const hasSeconds = seconds !== undefined;
|
|
33
|
+
|
|
34
|
+
if (hasHours) {
|
|
35
|
+
const displayHours = is24Hour ? hours : to12Hour(hours);
|
|
36
|
+
|
|
37
|
+
const h10 = Math.floor(displayHours / 10);
|
|
38
|
+
const h1 = displayHours % 10;
|
|
39
|
+
|
|
40
|
+
// Hours tens digit — only shown if >= 10 or padding is on
|
|
41
|
+
if (h10 > 0 || padHours) {
|
|
42
|
+
parts.push(digitPart("h10", h10));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
parts.push(digitPart("h1", h1));
|
|
46
|
+
|
|
47
|
+
parts.push(symbolPart("sep", ":"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const m10 = Math.floor(minutes / 10);
|
|
51
|
+
const m1 = minutes % 10;
|
|
52
|
+
parts.push(digitPart("m10", m10));
|
|
53
|
+
parts.push(digitPart("m1", m1));
|
|
54
|
+
|
|
55
|
+
if (hasSeconds) {
|
|
56
|
+
parts.push(symbolPart("sep2", ":"));
|
|
57
|
+
|
|
58
|
+
const s10 = Math.floor(seconds / 10);
|
|
59
|
+
const s1 = seconds % 10;
|
|
60
|
+
parts.push(digitPart("s10", s10));
|
|
61
|
+
parts.push(digitPart("s1", s1));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Each character gets a value-dependent key so AM→PM triggers exit/enter crossfade.
|
|
66
|
+
* Characters are emitted individually for correct glyph width measurement.
|
|
67
|
+
*/
|
|
68
|
+
if (hasHours && !is24Hour) {
|
|
69
|
+
const label = hours < 12 ? "AM" : "PM";
|
|
70
|
+
parts.push(symbolPart("ampm-sp", " "));
|
|
71
|
+
for (let i = 0; i < label.length; i++) {
|
|
72
|
+
parts.push(symbolPart(`ampm:${label}:${i}`, label[i]));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return parts;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Hook that formats time values into stably-keyed character parts.
|
|
81
|
+
* Uses fixed positional keys (h10, h1, sep, m10, m1, etc.) instead of
|
|
82
|
+
* NumberFlow's RTL integer keying.
|
|
83
|
+
*/
|
|
84
|
+
export function useTimeFormatting(
|
|
85
|
+
hours: number | undefined,
|
|
86
|
+
minutes: number,
|
|
87
|
+
seconds: number | undefined,
|
|
88
|
+
is24Hour: boolean,
|
|
89
|
+
padHours: boolean,
|
|
90
|
+
): KeyedPart[] {
|
|
91
|
+
return useMemo(
|
|
92
|
+
() => formatTimeToKeyedParts(hours, minutes, seconds, is24Hour, padHours),
|
|
93
|
+
[hours, minutes, seconds, is24Hour, padHours],
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { TimingConfig } from "./types";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_OPACITY_TIMING,
|
|
4
|
+
DEFAULT_SPIN_TIMING,
|
|
5
|
+
DEFAULT_TRANSFORM_TIMING,
|
|
6
|
+
ZERO_TIMING,
|
|
7
|
+
} from "./timing";
|
|
8
|
+
import { useCanAnimate } from "./useCanAnimate";
|
|
9
|
+
|
|
10
|
+
interface ResolvedTimings {
|
|
11
|
+
resolvedSpinTiming: TimingConfig;
|
|
12
|
+
resolvedOpacityTiming: TimingConfig;
|
|
13
|
+
resolvedTransformTiming: TimingConfig;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves animation timing based on the `animated` prop and reduced-motion preference.
|
|
18
|
+
* When animations are disabled, all timings collapse to zero-duration.
|
|
19
|
+
*/
|
|
20
|
+
export function useTimingResolution(
|
|
21
|
+
animated: boolean | undefined,
|
|
22
|
+
respectMotionPreference: boolean | undefined,
|
|
23
|
+
spinTiming?: TimingConfig,
|
|
24
|
+
opacityTiming?: TimingConfig,
|
|
25
|
+
transformTiming?: TimingConfig,
|
|
26
|
+
): ResolvedTimings {
|
|
27
|
+
const canAnimate = useCanAnimate(respectMotionPreference);
|
|
28
|
+
const shouldAnimate = (animated ?? true) && canAnimate;
|
|
29
|
+
|
|
30
|
+
const resolvedSpinTiming = shouldAnimate
|
|
31
|
+
? (spinTiming ?? DEFAULT_SPIN_TIMING)
|
|
32
|
+
: ZERO_TIMING;
|
|
33
|
+
|
|
34
|
+
const resolvedOpacityTiming = shouldAnimate
|
|
35
|
+
? (opacityTiming ?? DEFAULT_OPACITY_TIMING)
|
|
36
|
+
: ZERO_TIMING;
|
|
37
|
+
|
|
38
|
+
const resolvedTransformTiming = shouldAnimate
|
|
39
|
+
? (transformTiming ?? DEFAULT_TRANSFORM_TIMING)
|
|
40
|
+
: ZERO_TIMING;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
resolvedSpinTiming,
|
|
44
|
+
resolvedOpacityTiming,
|
|
45
|
+
resolvedTransformTiming,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { makeMutable, type SharedValue, useAnimatedReaction } from "react-native-reanimated";
|
|
3
|
+
import { MAX_SLOTS } from "./constants";
|
|
4
|
+
import { localeDigitValue } from "./numerals";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Watches a SharedValue<string> and extracts per-digit numeric values on the
|
|
8
|
+
* UI thread. Returns an array of SharedValues indexed by digit position
|
|
9
|
+
* (0 = first digit, 1 = second digit, etc.), skipping non-digit characters.
|
|
10
|
+
*
|
|
11
|
+
* This enables zero-latency digit updates during chart scrubbing — the worklet
|
|
12
|
+
* writes directly to SharedValues without crossing the JS bridge.
|
|
13
|
+
*
|
|
14
|
+
* When sharedValue is empty (e.g., scrubbing ended), all slots are set to
|
|
15
|
+
* -1, signaling DigitSlots to fall back to prop-driven animated updates.
|
|
16
|
+
*/
|
|
17
|
+
export function useWorkletFormatting(
|
|
18
|
+
sharedValue: SharedValue<string> | undefined,
|
|
19
|
+
prefix: string,
|
|
20
|
+
suffix: string,
|
|
21
|
+
zeroCodePoint = 48,
|
|
22
|
+
): SharedValue<number>[] | null {
|
|
23
|
+
const [digitValues] = useState(() => Array.from({ length: MAX_SLOTS }, () => makeMutable(-1)));
|
|
24
|
+
|
|
25
|
+
useAnimatedReaction(
|
|
26
|
+
() => sharedValue?.value ?? "",
|
|
27
|
+
(current, previous) => {
|
|
28
|
+
if (current === previous) return;
|
|
29
|
+
|
|
30
|
+
if (!current) {
|
|
31
|
+
for (let i = 0; i < MAX_SLOTS; i++) {
|
|
32
|
+
digitValues[i].value = -1;
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fullText = prefix + current + suffix;
|
|
38
|
+
const len = fullText.length;
|
|
39
|
+
let digitIndex = 0;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < len && digitIndex < MAX_SLOTS; i++) {
|
|
42
|
+
const code = fullText.charCodeAt(i);
|
|
43
|
+
const dv = localeDigitValue(code, zeroCodePoint);
|
|
44
|
+
if (dv >= 0) {
|
|
45
|
+
digitValues[digitIndex].value = dv;
|
|
46
|
+
digitIndex++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = digitIndex; i < MAX_SLOTS; i++) {
|
|
51
|
+
digitValues[i].value = -1;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[prefix, suffix, zeroCodePoint],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!sharedValue) return null;
|
|
58
|
+
return digitValues;
|
|
59
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { DIGIT_COUNT } from "./constants";
|
|
2
|
+
import type { Trend, TrendProp } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Computes the signed modular offset for digit n relative to virtual
|
|
6
|
+
* scroll position c. Returns a value in [-half, half), where 0 means
|
|
7
|
+
* the digit is centered in the viewport.
|
|
8
|
+
*
|
|
9
|
+
* This is the Reanimated equivalent of NumberFlow's CSS:
|
|
10
|
+
* offset-raw: mod(length + n - mod(c, length), length)
|
|
11
|
+
* offset: offset-raw - length * round(down, offset-raw / (length/2), 1)
|
|
12
|
+
*/
|
|
13
|
+
export function signedDigitOffset(n: number, c: number, digitCount: number = DIGIT_COUNT): number {
|
|
14
|
+
"worklet";
|
|
15
|
+
|
|
16
|
+
const raw = (((n - c) % digitCount) + digitCount) % digitCount;
|
|
17
|
+
const half = digitCount / 2;
|
|
18
|
+
|
|
19
|
+
return raw >= half ? raw - digitCount : raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Computes the roll delta for a digit transition, respecting the trend direction.
|
|
24
|
+
*
|
|
25
|
+
* For trend > 0 (always up): 8→2 = +4 (through 9,0,1,2), not -6
|
|
26
|
+
* For trend < 0 (always down): 2→8 = -6 (through 1,0,9,8), not +4
|
|
27
|
+
* For trend = 0 (auto): takes the shortest path
|
|
28
|
+
*/
|
|
29
|
+
export function computeRollDelta(
|
|
30
|
+
prev: number,
|
|
31
|
+
next: number,
|
|
32
|
+
trend: number,
|
|
33
|
+
digitCount: number = DIGIT_COUNT,
|
|
34
|
+
): number {
|
|
35
|
+
"worklet";
|
|
36
|
+
|
|
37
|
+
if (prev === next) return 0;
|
|
38
|
+
|
|
39
|
+
// Always roll up (positive delta)
|
|
40
|
+
if (trend > 0) {
|
|
41
|
+
return next >= prev ? next - prev : digitCount - prev + next;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Always roll down (negative delta)
|
|
45
|
+
if (trend < 0) {
|
|
46
|
+
return next <= prev ? next - prev : -(digitCount - next + prev);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Auto: shortest path
|
|
50
|
+
const half = digitCount / 2;
|
|
51
|
+
const diff = next - prev;
|
|
52
|
+
|
|
53
|
+
if (Math.abs(diff) <= half) return diff;
|
|
54
|
+
return diff > 0 ? diff - digitCount : diff + digitCount;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolves a TrendProp (static value or function) into a concrete Trend.
|
|
59
|
+
* When undefined, auto-detects direction from the value change.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveTrend(
|
|
62
|
+
trendProp: TrendProp | undefined,
|
|
63
|
+
prevValue: number | undefined,
|
|
64
|
+
nextValue: number | undefined,
|
|
65
|
+
): Trend {
|
|
66
|
+
const hasChange = prevValue !== undefined && nextValue !== undefined && prevValue !== nextValue;
|
|
67
|
+
|
|
68
|
+
// Static trend value — pass through
|
|
69
|
+
if (typeof trendProp === "number") {
|
|
70
|
+
return trendProp;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Function trend — call with prev/next when there's an actual change
|
|
74
|
+
if (typeof trendProp === "function") {
|
|
75
|
+
return hasChange ? trendProp(prevValue, nextValue) : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Auto-detect from direction of change
|
|
79
|
+
return hasChange ? (Math.sign(nextValue - prevValue) as Trend) : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extracts the significance position from a digit's keyed part key.
|
|
84
|
+
* "integer:2" → 2 (hundreds), "fraction:0" → -1 (tenths).
|
|
85
|
+
* Returns undefined for non-digit keys (symbols, decimals, etc.).
|
|
86
|
+
*/
|
|
87
|
+
export function parseDigitPosition(key: string): number | undefined {
|
|
88
|
+
"worklet";
|
|
89
|
+
|
|
90
|
+
// Exponent digits are a separate domain — they don't cascade in continuous spin
|
|
91
|
+
if (key.startsWith("exponentInteger:")) return undefined;
|
|
92
|
+
|
|
93
|
+
if (key.startsWith("integer:")) {
|
|
94
|
+
return parseInt(key.slice(8), 10);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (key.startsWith("fraction:")) {
|
|
98
|
+
return -(parseInt(key.slice(9), 10) + 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Time digit keys — significance ordered: s1 < s10 < m1 < m10 < h1 < h10
|
|
102
|
+
switch (key) {
|
|
103
|
+
case "s1":
|
|
104
|
+
return 0;
|
|
105
|
+
case "s10":
|
|
106
|
+
return 1;
|
|
107
|
+
case "m1":
|
|
108
|
+
return 2;
|
|
109
|
+
case "m10":
|
|
110
|
+
return 3;
|
|
111
|
+
case "h1":
|
|
112
|
+
return 4;
|
|
113
|
+
case "h10":
|
|
114
|
+
return 5;
|
|
115
|
+
default:
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Correct wheel size for each time digit position.
|
|
122
|
+
* s10/m10 only go 0-5, h10 only goes 0-2.
|
|
123
|
+
*/
|
|
124
|
+
export const TIME_DIGIT_COUNTS: Record<string, number> = {
|
|
125
|
+
s1: 10,
|
|
126
|
+
s10: 6,
|
|
127
|
+
m1: 10,
|
|
128
|
+
m10: 6,
|
|
129
|
+
h1: 10,
|
|
130
|
+
h10: 3,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolves the wheel size for a digit at a given key.
|
|
135
|
+
* Uses the `digits` prop for NumberFlow, or TIME_DIGIT_COUNTS for TimeFlow.
|
|
136
|
+
* Returns DIGIT_COUNT (10) when no constraint applies.
|
|
137
|
+
*/
|
|
138
|
+
export function getDigitCount(
|
|
139
|
+
digits: Record<number, { max: number }> | undefined,
|
|
140
|
+
key: string,
|
|
141
|
+
): number {
|
|
142
|
+
if (!digits) return DIGIT_COUNT;
|
|
143
|
+
|
|
144
|
+
const pos = parseDigitPosition(key);
|
|
145
|
+
if (pos === undefined || pos < 0) return DIGIT_COUNT;
|
|
146
|
+
|
|
147
|
+
const constraint = digits[pos];
|
|
148
|
+
return constraint ? constraint.max + 1 : DIGIT_COUNT;
|
|
149
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { detectNumberingSystem, getDigitStrings, getZeroCodePoint } from "./core/numerals";
|
|
2
|
+
export type { TimeFlowProps } from "./core/timeTypes";
|
|
3
|
+
export type {
|
|
4
|
+
AnimationConfig,
|
|
5
|
+
DigitConstraint,
|
|
6
|
+
DigitsProp,
|
|
7
|
+
TextAlign,
|
|
8
|
+
TimingConfig,
|
|
9
|
+
Trend,
|
|
10
|
+
TrendProp,
|
|
11
|
+
} from "./core/types";
|
|
12
|
+
export { useCanAnimate } from "./core/useCanAnimate";
|
|
13
|
+
export { useFormattedValue } from "./core/useFormattedValue";
|
|
14
|
+
export { NumberFlow, TimeFlow } from "./native";
|
|
15
|
+
export type { NumberFlowProps, NumberFlowStyle } from "./native/types";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { StyleSheet, Text, type TextStyle } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
makeMutable,
|
|
5
|
+
type SharedValue,
|
|
6
|
+
useAnimatedReaction,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from "react-native-reanimated";
|
|
10
|
+
import { DIGIT_COUNT, SUPERSCRIPT_SCALE } from "../core/constants";
|
|
11
|
+
import { getSuperscriptTextStyle } from "../core/superscript";
|
|
12
|
+
import type { GlyphMetrics, TimingConfig, Trend } from "../core/types";
|
|
13
|
+
import { useAnimatedX } from "../core/useAnimatedX";
|
|
14
|
+
import { useDigitAnimation } from "../core/useDigitAnimation";
|
|
15
|
+
import { signedDigitOffset } from "../core/utils";
|
|
16
|
+
|
|
17
|
+
// Extracted as its own component so useAnimatedStyle respects Rules of Hooks
|
|
18
|
+
interface DigitElementProps {
|
|
19
|
+
digitString: string;
|
|
20
|
+
yValue: SharedValue<number>;
|
|
21
|
+
textStyle: DigitSlotProps["textStyle"];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DigitElement = React.memo(({ digitString, yValue, textStyle }: DigitElementProps) => {
|
|
25
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
26
|
+
transform: [{ translateY: yValue.value }],
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Animated.View style={[styles.digitView, animatedStyle]}>
|
|
31
|
+
<Text style={textStyle}>{digitString}</Text>
|
|
32
|
+
</Animated.View>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
DigitElement.displayName = "DigitElement";
|
|
37
|
+
|
|
38
|
+
interface DigitSlotProps {
|
|
39
|
+
metrics: GlyphMetrics;
|
|
40
|
+
digitValue: number;
|
|
41
|
+
targetX: number;
|
|
42
|
+
charWidth: number;
|
|
43
|
+
textStyle: TextStyle;
|
|
44
|
+
spinTiming: TimingConfig;
|
|
45
|
+
opacityTiming: TimingConfig;
|
|
46
|
+
transformTiming: TimingConfig;
|
|
47
|
+
trend: Trend;
|
|
48
|
+
entering: boolean;
|
|
49
|
+
exiting: boolean;
|
|
50
|
+
exitKey?: string;
|
|
51
|
+
onExitComplete?: (key: string) => void;
|
|
52
|
+
digitCount?: number;
|
|
53
|
+
continuousSpinGeneration?: number;
|
|
54
|
+
maskTop?: number;
|
|
55
|
+
maskBottom?: number;
|
|
56
|
+
superscript?: boolean;
|
|
57
|
+
digitStrings?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DigitSlot = React.memo(
|
|
61
|
+
({
|
|
62
|
+
metrics,
|
|
63
|
+
digitValue,
|
|
64
|
+
targetX,
|
|
65
|
+
charWidth,
|
|
66
|
+
textStyle,
|
|
67
|
+
spinTiming,
|
|
68
|
+
opacityTiming,
|
|
69
|
+
transformTiming,
|
|
70
|
+
trend,
|
|
71
|
+
entering,
|
|
72
|
+
exiting,
|
|
73
|
+
exitKey,
|
|
74
|
+
onExitComplete,
|
|
75
|
+
digitCount,
|
|
76
|
+
continuousSpinGeneration,
|
|
77
|
+
maskTop = 0,
|
|
78
|
+
maskBottom = 0,
|
|
79
|
+
superscript,
|
|
80
|
+
digitStrings,
|
|
81
|
+
}: DigitSlotProps) => {
|
|
82
|
+
const resolvedDigitCount = digitCount ?? DIGIT_COUNT;
|
|
83
|
+
const resolvedDigitStrings =
|
|
84
|
+
digitStrings ?? Array.from({ length: resolvedDigitCount }, (_, i) => String(i));
|
|
85
|
+
|
|
86
|
+
// Superscript scaling — exponent digits/signs render smaller at the top of the line.
|
|
87
|
+
// Mask heights are zeroed for superscript: the container-level gradient doesn't cover
|
|
88
|
+
// the superscript position, so any buffer would show unmasked neighboring digits.
|
|
89
|
+
const scale = superscript ? SUPERSCRIPT_SCALE : 1;
|
|
90
|
+
const effectiveLH = metrics.lineHeight * scale;
|
|
91
|
+
const effectiveMaskTop = superscript ? 0 : maskTop;
|
|
92
|
+
const effectiveMaskBottom = superscript ? 0 : maskBottom;
|
|
93
|
+
|
|
94
|
+
const effectiveTextStyle = useMemo(
|
|
95
|
+
() => (superscript ? getSuperscriptTextStyle(textStyle, effectiveLH) : textStyle),
|
|
96
|
+
[textStyle, superscript, effectiveLH],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const { initialDigit, animDelta, currentDigitSV, slotOpacity } = useDigitAnimation({
|
|
100
|
+
digitValue,
|
|
101
|
+
entering,
|
|
102
|
+
exiting,
|
|
103
|
+
trend,
|
|
104
|
+
spinTiming,
|
|
105
|
+
opacityTiming,
|
|
106
|
+
exitKey,
|
|
107
|
+
onExitComplete,
|
|
108
|
+
digitCount: resolvedDigitCount,
|
|
109
|
+
continuousSpinGeneration,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Per-digit Y transforms stored as makeMutable shared values.
|
|
114
|
+
* Each digit independently positions itself based on its signed
|
|
115
|
+
* modular distance from the virtual scroll position.
|
|
116
|
+
*/
|
|
117
|
+
const [digitYValues] = useState(() =>
|
|
118
|
+
Array.from({ length: resolvedDigitCount }, (_, n) => {
|
|
119
|
+
const offset = signedDigitOffset(n, initialDigit, resolvedDigitCount);
|
|
120
|
+
const clamped = Math.max(-1.5, Math.min(1.5, offset));
|
|
121
|
+
return makeMutable(clamped * effectiveLH + effectiveMaskTop);
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Mirrors NumberFlow's CSS mod(): each digit n computes its signed
|
|
127
|
+
* offset from virtual position c, clamped to [-1.5, 1.5].
|
|
128
|
+
* Only the current digit (offset ~ 0) and its neighbors (offset ~ +/-1)
|
|
129
|
+
* are visible through the clip window. All others park just outside.
|
|
130
|
+
*/
|
|
131
|
+
useAnimatedReaction(
|
|
132
|
+
() => currentDigitSV.value - animDelta.value,
|
|
133
|
+
(c) => {
|
|
134
|
+
for (let n = 0; n < resolvedDigitCount; n++) {
|
|
135
|
+
const offset = signedDigitOffset(n, c, resolvedDigitCount);
|
|
136
|
+
const clamped = Math.max(-1.5, Math.min(1.5, offset));
|
|
137
|
+
digitYValues[n].value = clamped * effectiveLH + effectiveMaskTop;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[effectiveLH, resolvedDigitCount, effectiveMaskTop],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const animatedX = useAnimatedX(targetX, exiting, transformTiming);
|
|
144
|
+
|
|
145
|
+
const [animatedClipWidth] = useState(() => makeMutable(charWidth));
|
|
146
|
+
const prevWidthRef = useRef(charWidth);
|
|
147
|
+
|
|
148
|
+
useLayoutEffect(() => {
|
|
149
|
+
if (!exiting && prevWidthRef.current !== charWidth) {
|
|
150
|
+
prevWidthRef.current = charWidth;
|
|
151
|
+
animatedClipWidth.value = withTiming(charWidth, {
|
|
152
|
+
duration: transformTiming.duration,
|
|
153
|
+
easing: transformTiming.easing,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}, [charWidth, exiting, transformTiming, animatedClipWidth]);
|
|
157
|
+
|
|
158
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
159
|
+
transform: [{ translateX: animatedX.value }, { translateY: -effectiveMaskTop }],
|
|
160
|
+
opacity: slotOpacity.value,
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
const expandedHeight = effectiveLH + effectiveMaskTop + effectiveMaskBottom;
|
|
164
|
+
|
|
165
|
+
const animatedClipStyle = useAnimatedStyle(() => ({
|
|
166
|
+
overflow: "hidden" as const,
|
|
167
|
+
height: expandedHeight,
|
|
168
|
+
width: animatedClipWidth.value,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
const digitElements = useMemo(
|
|
172
|
+
() =>
|
|
173
|
+
Array.from({ length: resolvedDigitCount }, (_, n) => (
|
|
174
|
+
<DigitElement
|
|
175
|
+
digitString={resolvedDigitStrings[n]}
|
|
176
|
+
key={n}
|
|
177
|
+
textStyle={effectiveTextStyle}
|
|
178
|
+
yValue={digitYValues[n]}
|
|
179
|
+
/>
|
|
180
|
+
)),
|
|
181
|
+
[resolvedDigitCount, resolvedDigitStrings, digitYValues, effectiveTextStyle],
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Animated.View style={[styles.absolute, animatedStyle]}>
|
|
186
|
+
<Animated.View style={animatedClipStyle}>{digitElements}</Animated.View>
|
|
187
|
+
</Animated.View>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
DigitSlot.displayName = "DigitSlot";
|
|
193
|
+
|
|
194
|
+
const styles = StyleSheet.create({
|
|
195
|
+
absolute: {
|
|
196
|
+
position: "absolute",
|
|
197
|
+
},
|
|
198
|
+
digitView: {
|
|
199
|
+
position: "absolute",
|
|
200
|
+
left: 0,
|
|
201
|
+
top: 0,
|
|
202
|
+
},
|
|
203
|
+
});
|