react-native-laminar 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/README.md +11 -0
- package/lib/commonjs/hooks/use-display-units.js +13 -0
- package/lib/commonjs/hooks/use-display-units.js.map +1 -0
- package/lib/commonjs/hooks/use-inline-auto-width.js +45 -0
- package/lib/commonjs/hooks/use-inline-auto-width.js.map +1 -0
- package/lib/commonjs/hooks/use-morph-motion.js +24 -0
- package/lib/commonjs/hooks/use-morph-motion.js.map +1 -0
- package/lib/commonjs/hooks/use-morph-text-style.js +32 -0
- package/lib/commonjs/hooks/use-morph-text-style.js.map +1 -0
- package/lib/commonjs/hooks/use-numeric-lanes.js +37 -0
- package/lib/commonjs/hooks/use-numeric-lanes.js.map +1 -0
- package/lib/commonjs/hooks/use-text-glyphs.js +35 -0
- package/lib/commonjs/hooks/use-text-glyphs.js.map +1 -0
- package/lib/commonjs/index.js +89 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/model/display-units.js +29 -0
- package/lib/commonjs/model/display-units.js.map +1 -0
- package/lib/commonjs/model/numeric-lanes.js +45 -0
- package/lib/commonjs/model/numeric-lanes.js.map +1 -0
- package/lib/commonjs/model/text-keys.js +80 -0
- package/lib/commonjs/model/text-keys.js.map +1 -0
- package/lib/commonjs/motion/entry-exit-builders.js +57 -0
- package/lib/commonjs/motion/entry-exit-builders.js.map +1 -0
- package/lib/commonjs/motion/preset-map.js +91 -0
- package/lib/commonjs/motion/preset-map.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/view/glyph-run.js +34 -0
- package/lib/commonjs/view/glyph-run.js.map +1 -0
- package/lib/commonjs/view/morph-viewport.js +64 -0
- package/lib/commonjs/view/morph-viewport.js.map +1 -0
- package/lib/commonjs/view/number-lane.js +85 -0
- package/lib/commonjs/view/number-lane.js.map +1 -0
- package/lib/commonjs/view/number-run.js +61 -0
- package/lib/commonjs/view/number-run.js.map +1 -0
- package/lib/commonjs/view/text-run.js +35 -0
- package/lib/commonjs/view/text-run.js.map +1 -0
- package/lib/module/hooks/use-display-units.js +8 -0
- package/lib/module/hooks/use-display-units.js.map +1 -0
- package/lib/module/hooks/use-inline-auto-width.js +40 -0
- package/lib/module/hooks/use-inline-auto-width.js.map +1 -0
- package/lib/module/hooks/use-morph-motion.js +19 -0
- package/lib/module/hooks/use-morph-motion.js.map +1 -0
- package/lib/module/hooks/use-morph-text-style.js +27 -0
- package/lib/module/hooks/use-morph-text-style.js.map +1 -0
- package/lib/module/hooks/use-numeric-lanes.js +32 -0
- package/lib/module/hooks/use-numeric-lanes.js.map +1 -0
- package/lib/module/hooks/use-text-glyphs.js +30 -0
- package/lib/module/hooks/use-text-glyphs.js.map +1 -0
- package/lib/module/index.js +84 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/model/display-units.js +21 -0
- package/lib/module/model/display-units.js.map +1 -0
- package/lib/module/model/numeric-lanes.js +40 -0
- package/lib/module/model/numeric-lanes.js.map +1 -0
- package/lib/module/model/text-keys.js +75 -0
- package/lib/module/model/text-keys.js.map +1 -0
- package/lib/module/motion/entry-exit-builders.js +52 -0
- package/lib/module/motion/entry-exit-builders.js.map +1 -0
- package/lib/module/motion/preset-map.js +86 -0
- package/lib/module/motion/preset-map.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/view/glyph-run.js +29 -0
- package/lib/module/view/glyph-run.js.map +1 -0
- package/lib/module/view/morph-viewport.js +58 -0
- package/lib/module/view/morph-viewport.js.map +1 -0
- package/lib/module/view/number-lane.js +79 -0
- package/lib/module/view/number-lane.js.map +1 -0
- package/lib/module/view/number-run.js +56 -0
- package/lib/module/view/number-run.js.map +1 -0
- package/lib/module/view/text-run.js +30 -0
- package/lib/module/view/text-run.js.map +1 -0
- package/lib/typescript/commonjs/hooks/use-display-units.d.ts +2 -0
- package/lib/typescript/commonjs/hooks/use-display-units.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts +15 -0
- package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts +14 -0
- package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/use-morph-text-style.d.ts +13 -0
- package/lib/typescript/commonjs/hooks/use-morph-text-style.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/use-numeric-lanes.d.ts +10 -0
- package/lib/typescript/commonjs/hooks/use-numeric-lanes.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts +3 -0
- package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +7 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/model/display-units.d.ts +5 -0
- package/lib/typescript/commonjs/model/display-units.d.ts.map +1 -0
- package/lib/typescript/commonjs/model/numeric-lanes.d.ts +9 -0
- package/lib/typescript/commonjs/model/numeric-lanes.d.ts.map +1 -0
- package/lib/typescript/commonjs/model/text-keys.d.ts +7 -0
- package/lib/typescript/commonjs/model/text-keys.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/entry-exit-builders.d.ts +15 -0
- package/lib/typescript/commonjs/motion/entry-exit-builders.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/preset-map.d.ts +4 -0
- package/lib/typescript/commonjs/motion/preset-map.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/types.d.ts +43 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/view/glyph-run.d.ts +12 -0
- package/lib/typescript/commonjs/view/glyph-run.d.ts.map +1 -0
- package/lib/typescript/commonjs/view/morph-viewport.d.ts +14 -0
- package/lib/typescript/commonjs/view/morph-viewport.d.ts.map +1 -0
- package/lib/typescript/commonjs/view/number-lane.d.ts +17 -0
- package/lib/typescript/commonjs/view/number-lane.d.ts.map +1 -0
- package/lib/typescript/commonjs/view/number-run.d.ts +13 -0
- package/lib/typescript/commonjs/view/number-run.d.ts.map +1 -0
- package/lib/typescript/commonjs/view/text-run.d.ts +11 -0
- package/lib/typescript/commonjs/view/text-run.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-display-units.d.ts +2 -0
- package/lib/typescript/module/hooks/use-display-units.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-inline-auto-width.d.ts +15 -0
- package/lib/typescript/module/hooks/use-inline-auto-width.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-morph-motion.d.ts +14 -0
- package/lib/typescript/module/hooks/use-morph-motion.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-morph-text-style.d.ts +13 -0
- package/lib/typescript/module/hooks/use-morph-text-style.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-numeric-lanes.d.ts +10 -0
- package/lib/typescript/module/hooks/use-numeric-lanes.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-text-glyphs.d.ts +3 -0
- package/lib/typescript/module/hooks/use-text-glyphs.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +7 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/model/display-units.d.ts +5 -0
- package/lib/typescript/module/model/display-units.d.ts.map +1 -0
- package/lib/typescript/module/model/numeric-lanes.d.ts +9 -0
- package/lib/typescript/module/model/numeric-lanes.d.ts.map +1 -0
- package/lib/typescript/module/model/text-keys.d.ts +7 -0
- package/lib/typescript/module/model/text-keys.d.ts.map +1 -0
- package/lib/typescript/module/motion/entry-exit-builders.d.ts +15 -0
- package/lib/typescript/module/motion/entry-exit-builders.d.ts.map +1 -0
- package/lib/typescript/module/motion/preset-map.d.ts +4 -0
- package/lib/typescript/module/motion/preset-map.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/types.d.ts +43 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- package/lib/typescript/module/view/glyph-run.d.ts +12 -0
- package/lib/typescript/module/view/glyph-run.d.ts.map +1 -0
- package/lib/typescript/module/view/morph-viewport.d.ts +14 -0
- package/lib/typescript/module/view/morph-viewport.d.ts.map +1 -0
- package/lib/typescript/module/view/number-lane.d.ts +17 -0
- package/lib/typescript/module/view/number-lane.d.ts.map +1 -0
- package/lib/typescript/module/view/number-run.d.ts +13 -0
- package/lib/typescript/module/view/number-run.d.ts.map +1 -0
- package/lib/typescript/module/view/text-run.d.ts +11 -0
- package/lib/typescript/module/view/text-run.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/hooks/use-display-units.ts +18 -0
- package/src/hooks/use-inline-auto-width.ts +57 -0
- package/src/hooks/use-morph-motion.ts +40 -0
- package/src/hooks/use-morph-text-style.ts +45 -0
- package/src/hooks/use-numeric-lanes.ts +55 -0
- package/src/hooks/use-text-glyphs.ts +56 -0
- package/src/index.tsx +98 -0
- package/src/model/display-units.ts +28 -0
- package/src/model/numeric-lanes.ts +80 -0
- package/src/model/text-keys.ts +123 -0
- package/src/motion/entry-exit-builders.ts +74 -0
- package/src/motion/preset-map.ts +127 -0
- package/src/types.ts +60 -0
- package/src/view/glyph-run.tsx +47 -0
- package/src/view/morph-viewport.tsx +83 -0
- package/src/view/number-lane.tsx +128 -0
- package/src/view/number-run.tsx +73 -0
- package/src/view/text-run.tsx +40 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { NumericFlowDirection } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
findNumericLeadLength,
|
|
4
|
+
splitDisplayUnits,
|
|
5
|
+
} from "./display-units";
|
|
6
|
+
|
|
7
|
+
const readNumericMagnitude = (input: string) =>
|
|
8
|
+
parseFloat(input.replace(/[^0-9.-]/g, "")) || 0;
|
|
9
|
+
|
|
10
|
+
const deriveFlowDirection = (
|
|
11
|
+
previousValue: string,
|
|
12
|
+
nextValue: string
|
|
13
|
+
): NumericFlowDirection =>
|
|
14
|
+
Math.sign(readNumericMagnitude(nextValue) - readNumericMagnitude(previousValue)) as NumericFlowDirection;
|
|
15
|
+
|
|
16
|
+
type ReconciledLaneState = {
|
|
17
|
+
readonly laneKeys: readonly number[];
|
|
18
|
+
readonly nextSeed: number;
|
|
19
|
+
readonly direction: NumericFlowDirection;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const reconcileNumericLanes = (
|
|
23
|
+
previousValue: string,
|
|
24
|
+
nextValue: string,
|
|
25
|
+
previousKeys: readonly number[],
|
|
26
|
+
seed: number
|
|
27
|
+
): ReconciledLaneState => {
|
|
28
|
+
const previousUnits = splitDisplayUnits(previousValue);
|
|
29
|
+
const nextUnits = splitDisplayUnits(nextValue);
|
|
30
|
+
const nextLaneKeys = new Array(nextUnits.length);
|
|
31
|
+
let nextSeed = seed;
|
|
32
|
+
|
|
33
|
+
const nextLeadLength = findNumericLeadLength(nextUnits);
|
|
34
|
+
const previousLeadLength = findNumericLeadLength(previousUnits);
|
|
35
|
+
const sharedLeadLength = Math.min(nextLeadLength, previousLeadLength);
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < nextLeadLength; index += 1) {
|
|
38
|
+
nextLaneKeys[index] =
|
|
39
|
+
index < sharedLeadLength && nextUnits[index] === previousUnits[index]
|
|
40
|
+
? previousKeys[index]
|
|
41
|
+
: nextSeed++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const previousTailUnits = previousUnits.slice(previousLeadLength);
|
|
45
|
+
const nextTailUnits = nextUnits.slice(nextLeadLength);
|
|
46
|
+
const previousTailKeys = previousKeys.slice(previousLeadLength);
|
|
47
|
+
const laneCount = Math.max(previousTailUnits.length, nextTailUnits.length);
|
|
48
|
+
|
|
49
|
+
const leftPaddedPreviousUnits = [
|
|
50
|
+
...Array<string>(Math.max(0, laneCount - previousTailUnits.length)).fill(""),
|
|
51
|
+
...previousTailUnits,
|
|
52
|
+
];
|
|
53
|
+
const leftPaddedNextUnits = [
|
|
54
|
+
...Array<string>(Math.max(0, laneCount - nextTailUnits.length)).fill(""),
|
|
55
|
+
...nextTailUnits,
|
|
56
|
+
];
|
|
57
|
+
const leftPaddedPreviousKeys = [
|
|
58
|
+
...Array<number>(Math.max(0, laneCount - previousTailKeys.length)).fill(-1),
|
|
59
|
+
...previousTailKeys,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// pad from the left so the rightmost digits keep their place value lanes
|
|
63
|
+
const nextTailOffset = laneCount - nextTailUnits.length;
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < nextTailUnits.length; index += 1) {
|
|
66
|
+
const paddedIndex = nextTailOffset + index;
|
|
67
|
+
const nextUnit = leftPaddedNextUnits[paddedIndex];
|
|
68
|
+
const previousUnit = leftPaddedPreviousUnits[paddedIndex];
|
|
69
|
+
const previousKey = leftPaddedPreviousKeys[paddedIndex];
|
|
70
|
+
|
|
71
|
+
nextLaneKeys[nextLeadLength + index] =
|
|
72
|
+
nextUnit === previousUnit && previousKey >= 0 ? previousKey : nextSeed++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
laneKeys: nextLaneKeys,
|
|
77
|
+
nextSeed,
|
|
78
|
+
direction: deriveFlowDirection(previousValue, nextValue),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const computeLcsPairs = (
|
|
2
|
+
previousUnits: readonly string[],
|
|
3
|
+
nextUnits: readonly string[]
|
|
4
|
+
): readonly [number, number][] => {
|
|
5
|
+
const previousLength = previousUnits.length;
|
|
6
|
+
const nextLength = nextUnits.length;
|
|
7
|
+
|
|
8
|
+
if (previousLength === 0 || nextLength === 0) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const dp = Array.from({ length: previousLength + 1 }, () =>
|
|
13
|
+
new Array<number>(nextLength + 1).fill(0)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
for (let previousIndex = 1; previousIndex <= previousLength; previousIndex += 1) {
|
|
17
|
+
for (let nextIndex = 1; nextIndex <= nextLength; nextIndex += 1) {
|
|
18
|
+
dp[previousIndex][nextIndex] =
|
|
19
|
+
previousUnits[previousIndex - 1] === nextUnits[nextIndex - 1]
|
|
20
|
+
? dp[previousIndex - 1][nextIndex - 1] + 1
|
|
21
|
+
: Math.max(dp[previousIndex - 1][nextIndex], dp[previousIndex][nextIndex - 1]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pairs: [number, number][] = [];
|
|
26
|
+
let previousIndex = previousLength;
|
|
27
|
+
let nextIndex = nextLength;
|
|
28
|
+
|
|
29
|
+
while (previousIndex > 0 && nextIndex > 0) {
|
|
30
|
+
if (previousUnits[previousIndex - 1] === nextUnits[nextIndex - 1]) {
|
|
31
|
+
pairs.push([previousIndex - 1, nextIndex - 1]);
|
|
32
|
+
previousIndex -= 1;
|
|
33
|
+
nextIndex -= 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
dp[previousIndex - 1][nextIndex] > dp[previousIndex][nextIndex - 1] ||
|
|
39
|
+
(dp[previousIndex - 1][nextIndex] === dp[previousIndex][nextIndex - 1] &&
|
|
40
|
+
previousIndex >= nextIndex)
|
|
41
|
+
) {
|
|
42
|
+
previousIndex -= 1;
|
|
43
|
+
} else {
|
|
44
|
+
nextIndex -= 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pairs.reverse();
|
|
49
|
+
return pairs;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type ReconciledTextGlyphState = {
|
|
53
|
+
readonly glyphKeys: readonly string[];
|
|
54
|
+
readonly nextSeed: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const reconcileTextGlyphKeys = (
|
|
58
|
+
previousUnits: readonly string[],
|
|
59
|
+
nextUnits: readonly string[],
|
|
60
|
+
previousKeys: readonly string[],
|
|
61
|
+
seed: number,
|
|
62
|
+
namespace: string
|
|
63
|
+
): ReconciledTextGlyphState => {
|
|
64
|
+
const previousLength = previousUnits.length;
|
|
65
|
+
const nextLength = nextUnits.length;
|
|
66
|
+
const nextGlyphKeys = new Array<string>(nextUnits.length).fill("");
|
|
67
|
+
let nextSeed = seed;
|
|
68
|
+
|
|
69
|
+
let sharedPrefixLength = 0;
|
|
70
|
+
while (
|
|
71
|
+
sharedPrefixLength < previousLength &&
|
|
72
|
+
sharedPrefixLength < nextLength &&
|
|
73
|
+
previousUnits[sharedPrefixLength] === nextUnits[sharedPrefixLength]
|
|
74
|
+
) {
|
|
75
|
+
nextGlyphKeys[sharedPrefixLength] =
|
|
76
|
+
previousKeys[sharedPrefixLength] ?? `${namespace}:c${nextSeed++}`;
|
|
77
|
+
sharedPrefixLength += 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let sharedSuffixLength = 0;
|
|
81
|
+
while (
|
|
82
|
+
sharedSuffixLength < previousLength - sharedPrefixLength &&
|
|
83
|
+
sharedSuffixLength < nextLength - sharedPrefixLength &&
|
|
84
|
+
previousUnits[previousLength - 1 - sharedSuffixLength] ===
|
|
85
|
+
nextUnits[nextLength - 1 - sharedSuffixLength]
|
|
86
|
+
) {
|
|
87
|
+
const previousIndex = previousLength - 1 - sharedSuffixLength;
|
|
88
|
+
const nextIndex = nextLength - 1 - sharedSuffixLength;
|
|
89
|
+
nextGlyphKeys[nextIndex] =
|
|
90
|
+
previousKeys[previousIndex] ?? `${namespace}:c${nextSeed++}`;
|
|
91
|
+
sharedSuffixLength += 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const previousMiddleEnd = previousLength - sharedSuffixLength;
|
|
95
|
+
const nextMiddleEnd = nextLength - sharedSuffixLength;
|
|
96
|
+
const previousMiddleUnits = previousUnits.slice(
|
|
97
|
+
sharedPrefixLength,
|
|
98
|
+
previousMiddleEnd
|
|
99
|
+
);
|
|
100
|
+
const nextMiddleUnits = nextUnits.slice(sharedPrefixLength, nextMiddleEnd);
|
|
101
|
+
|
|
102
|
+
// lcs keeps shared middle glyphs in place when the text changes
|
|
103
|
+
const matches = computeLcsPairs(previousMiddleUnits, nextMiddleUnits);
|
|
104
|
+
|
|
105
|
+
for (const [previousIndex, nextIndex] of matches) {
|
|
106
|
+
const absolutePreviousIndex = previousIndex + sharedPrefixLength;
|
|
107
|
+
const absoluteNextIndex = nextIndex + sharedPrefixLength;
|
|
108
|
+
|
|
109
|
+
nextGlyphKeys[absoluteNextIndex] =
|
|
110
|
+
previousKeys[absolutePreviousIndex] ?? `${namespace}:c${nextSeed++}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let nextIndex = 0; nextIndex < nextGlyphKeys.length; nextIndex += 1) {
|
|
114
|
+
if (!nextGlyphKeys[nextIndex]) {
|
|
115
|
+
nextGlyphKeys[nextIndex] = `${namespace}:c${nextSeed++}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
glyphKeys: nextGlyphKeys,
|
|
121
|
+
nextSeed,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EntryExitAnimationFunction,
|
|
3
|
+
type WithTimingConfig,
|
|
4
|
+
withDelay,
|
|
5
|
+
withTiming,
|
|
6
|
+
} from "react-native-reanimated";
|
|
7
|
+
|
|
8
|
+
type TransitionParams = {
|
|
9
|
+
readonly delayMs?: number;
|
|
10
|
+
readonly durationMs: number;
|
|
11
|
+
readonly easing: NonNullable<WithTimingConfig["easing"]>;
|
|
12
|
+
readonly fromOpacity: number;
|
|
13
|
+
readonly toOpacity: number;
|
|
14
|
+
readonly fromTranslateY: number;
|
|
15
|
+
readonly toTranslateY: number;
|
|
16
|
+
readonly fromScale: number;
|
|
17
|
+
readonly toScale: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const createShiftTransition = ({
|
|
21
|
+
delayMs = 0,
|
|
22
|
+
durationMs,
|
|
23
|
+
easing,
|
|
24
|
+
fromOpacity,
|
|
25
|
+
toOpacity,
|
|
26
|
+
fromTranslateY,
|
|
27
|
+
toTranslateY,
|
|
28
|
+
fromScale,
|
|
29
|
+
toScale,
|
|
30
|
+
}: TransitionParams): EntryExitAnimationFunction => {
|
|
31
|
+
return () => {
|
|
32
|
+
"worklet";
|
|
33
|
+
|
|
34
|
+
// drive opacity and transforms from one timing path
|
|
35
|
+
const animate = (toValue: number) =>
|
|
36
|
+
delayMs > 0
|
|
37
|
+
? withDelay(
|
|
38
|
+
delayMs,
|
|
39
|
+
withTiming(toValue, {
|
|
40
|
+
duration: durationMs,
|
|
41
|
+
easing,
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
: withTiming(toValue, {
|
|
45
|
+
duration: durationMs,
|
|
46
|
+
easing,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const initialValues: Record<string, unknown> = {
|
|
50
|
+
opacity: fromOpacity,
|
|
51
|
+
transform: [
|
|
52
|
+
{ translateY: fromTranslateY },
|
|
53
|
+
{ scale: fromScale },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const animations: Record<string, unknown> = {
|
|
58
|
+
opacity: animate(toOpacity),
|
|
59
|
+
transform: [
|
|
60
|
+
{ translateY: animate(toTranslateY) },
|
|
61
|
+
{ scale: animate(toScale) },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
initialValues: {
|
|
67
|
+
...initialValues,
|
|
68
|
+
},
|
|
69
|
+
animations: {
|
|
70
|
+
...animations,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Easing,
|
|
3
|
+
LinearTransition,
|
|
4
|
+
type EntryExitAnimationFunction,
|
|
5
|
+
type WithTimingConfig,
|
|
6
|
+
withDelay,
|
|
7
|
+
withSpring,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from "react-native-reanimated";
|
|
10
|
+
import type {
|
|
11
|
+
MotionRecipe,
|
|
12
|
+
MorphAnimationPreset,
|
|
13
|
+
MorphAnimationPresetName,
|
|
14
|
+
} from "../types";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CURVE = [0.19, 1, 0.22, 1] as const;
|
|
17
|
+
|
|
18
|
+
export const MOTION_PRESETS: Record<
|
|
19
|
+
MorphAnimationPresetName,
|
|
20
|
+
MorphAnimationPreset
|
|
21
|
+
> = {
|
|
22
|
+
default: {
|
|
23
|
+
duration: 0.38,
|
|
24
|
+
ease: DEFAULT_CURVE,
|
|
25
|
+
},
|
|
26
|
+
smooth: {
|
|
27
|
+
type: "spring",
|
|
28
|
+
duration: 0.4,
|
|
29
|
+
bounce: 0,
|
|
30
|
+
},
|
|
31
|
+
snappy: {
|
|
32
|
+
type: "spring",
|
|
33
|
+
duration: 0.35,
|
|
34
|
+
bounce: 0.15,
|
|
35
|
+
},
|
|
36
|
+
bouncy: {
|
|
37
|
+
type: "spring",
|
|
38
|
+
duration: 0.5,
|
|
39
|
+
bounce: 0.3,
|
|
40
|
+
},
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const toMilliseconds = (seconds: number) => Math.round(seconds * 1000);
|
|
44
|
+
|
|
45
|
+
const toDampingRatio = (bounce: number) =>
|
|
46
|
+
Math.max(0.55, Math.min(1, 1 - bounce));
|
|
47
|
+
|
|
48
|
+
const createOpacityTransition = (
|
|
49
|
+
fromOpacity: number,
|
|
50
|
+
toOpacity: number,
|
|
51
|
+
durationMs: number,
|
|
52
|
+
easing: NonNullable<WithTimingConfig["easing"]>
|
|
53
|
+
): EntryExitAnimationFunction => {
|
|
54
|
+
return () => {
|
|
55
|
+
"worklet";
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
initialValues: {
|
|
59
|
+
opacity: fromOpacity,
|
|
60
|
+
},
|
|
61
|
+
animations: {
|
|
62
|
+
opacity: withTiming(toOpacity, {
|
|
63
|
+
duration: durationMs,
|
|
64
|
+
easing,
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const resolveMotionRecipe = (
|
|
72
|
+
presetName: MorphAnimationPresetName = "default",
|
|
73
|
+
durationOverride?: number
|
|
74
|
+
): MotionRecipe => {
|
|
75
|
+
const preset = MOTION_PRESETS[presetName];
|
|
76
|
+
const durationMs = durationOverride ?? toMilliseconds(preset.duration);
|
|
77
|
+
|
|
78
|
+
if ("type" in preset && preset.type === "spring") {
|
|
79
|
+
const dampingRatio = toDampingRatio(preset.bounce);
|
|
80
|
+
const easing = Easing.bezier(...DEFAULT_CURVE);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
durationMs,
|
|
84
|
+
easing,
|
|
85
|
+
// keep layout on timing so width and reflow stay predictable
|
|
86
|
+
layoutTransition: LinearTransition.duration(durationMs).easing(
|
|
87
|
+
easing.factory()
|
|
88
|
+
),
|
|
89
|
+
enterTransition: createOpacityTransition(0, 1, durationMs, easing),
|
|
90
|
+
exitTransition: createOpacityTransition(1, 0, durationMs, easing),
|
|
91
|
+
driveNumber: (toValue, delayMs = 0) => {
|
|
92
|
+
const animation = withSpring(toValue, {
|
|
93
|
+
duration: durationMs,
|
|
94
|
+
dampingRatio,
|
|
95
|
+
overshootClamping: preset.bounce === 0,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return delayMs > 0 ? withDelay(delayMs, animation) : animation;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const timingPreset = preset as Extract<
|
|
104
|
+
MorphAnimationPreset,
|
|
105
|
+
{ ease: readonly [number, number, number, number] }
|
|
106
|
+
>;
|
|
107
|
+
const [x1, y1, x2, y2] = timingPreset.ease;
|
|
108
|
+
const easing = Easing.bezier(x1, y1, x2, y2);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
durationMs,
|
|
112
|
+
easing,
|
|
113
|
+
layoutTransition: LinearTransition.duration(durationMs).easing(
|
|
114
|
+
easing.factory()
|
|
115
|
+
),
|
|
116
|
+
enterTransition: createOpacityTransition(0, 1, durationMs, easing),
|
|
117
|
+
exitTransition: createOpacityTransition(1, 0, durationMs, easing),
|
|
118
|
+
driveNumber: (toValue, delayMs = 0) => {
|
|
119
|
+
const animation = withTiming(toValue, {
|
|
120
|
+
duration: durationMs,
|
|
121
|
+
easing,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return delayMs > 0 ? withDelay(delayMs, animation) : animation;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import type {
|
|
3
|
+
ComplexAnimationBuilder,
|
|
4
|
+
EntryExitAnimationFunction,
|
|
5
|
+
WithTimingConfig,
|
|
6
|
+
} from "react-native-reanimated";
|
|
7
|
+
|
|
8
|
+
export type MorphAnimationPresetName =
|
|
9
|
+
| "default"
|
|
10
|
+
| "smooth"
|
|
11
|
+
| "snappy"
|
|
12
|
+
| "bouncy";
|
|
13
|
+
|
|
14
|
+
export type MorphContentVariant = "text" | "number";
|
|
15
|
+
|
|
16
|
+
type CubicBezierTuple = readonly [number, number, number, number];
|
|
17
|
+
|
|
18
|
+
export type MorphAnimationPreset =
|
|
19
|
+
| {
|
|
20
|
+
readonly duration: number;
|
|
21
|
+
readonly ease: CubicBezierTuple;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
readonly type: "spring";
|
|
25
|
+
readonly duration: number;
|
|
26
|
+
readonly bounce: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type GlyphToken = {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly value: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type NumericFlowDirection = -1 | 0 | 1;
|
|
35
|
+
|
|
36
|
+
export type MotionRecipe = {
|
|
37
|
+
readonly durationMs: number;
|
|
38
|
+
readonly easing: NonNullable<WithTimingConfig["easing"]>;
|
|
39
|
+
readonly layoutTransition: ComplexAnimationBuilder;
|
|
40
|
+
readonly enterTransition: EntryExitAnimationFunction;
|
|
41
|
+
readonly exitTransition: EntryExitAnimationFunction;
|
|
42
|
+
readonly driveNumber: (toValue: number, delayMs?: number) => number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type MorphingTextProps = {
|
|
46
|
+
readonly text: string | number;
|
|
47
|
+
readonly variant?: MorphContentVariant;
|
|
48
|
+
readonly fontSize?: number;
|
|
49
|
+
readonly color?: string;
|
|
50
|
+
readonly style?: StyleProp<TextStyle>;
|
|
51
|
+
readonly containerStyle?: StyleProp<ViewStyle>;
|
|
52
|
+
readonly fontStyle?: StyleProp<TextStyle>;
|
|
53
|
+
readonly animationDuration?: number;
|
|
54
|
+
readonly animationPreset?: MorphAnimationPresetName;
|
|
55
|
+
readonly stagger?: number;
|
|
56
|
+
readonly autoSize?: boolean;
|
|
57
|
+
readonly clipToBounds?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type LaminarProps = MorphingTextProps;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type StyleProp, type TextStyle, View } from "react-native";
|
|
3
|
+
import Animated from "react-native-reanimated";
|
|
4
|
+
import type {
|
|
5
|
+
ComplexAnimationBuilder,
|
|
6
|
+
EntryExitAnimationFunction,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import type { GlyphToken } from "../types";
|
|
9
|
+
|
|
10
|
+
const rowStyle = {
|
|
11
|
+
flexDirection: "row",
|
|
12
|
+
alignItems: "center",
|
|
13
|
+
alignSelf: "flex-start",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export const GlyphRun = React.memo(
|
|
17
|
+
({
|
|
18
|
+
glyphs,
|
|
19
|
+
layoutTransition,
|
|
20
|
+
enterTransition,
|
|
21
|
+
exitTransition,
|
|
22
|
+
textStyle,
|
|
23
|
+
}: Readonly<{
|
|
24
|
+
glyphs: readonly GlyphToken[];
|
|
25
|
+
layoutTransition: ComplexAnimationBuilder;
|
|
26
|
+
enterTransition?: EntryExitAnimationFunction;
|
|
27
|
+
exitTransition?: EntryExitAnimationFunction;
|
|
28
|
+
textStyle?: StyleProp<TextStyle>;
|
|
29
|
+
}>) => (
|
|
30
|
+
<View style={rowStyle}>
|
|
31
|
+
{/* glyph ids decide what swaps, layout handles the row reflow */}
|
|
32
|
+
{glyphs.map((glyph) => (
|
|
33
|
+
<Animated.Text
|
|
34
|
+
key={glyph.id}
|
|
35
|
+
layout={layoutTransition}
|
|
36
|
+
entering={enterTransition}
|
|
37
|
+
exiting={exitTransition}
|
|
38
|
+
style={textStyle}
|
|
39
|
+
>
|
|
40
|
+
{glyph.value}
|
|
41
|
+
</Animated.Text>
|
|
42
|
+
))}
|
|
43
|
+
</View>
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
GlyphRun.displayName = "GlyphRun";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import Animated from "react-native-reanimated";
|
|
5
|
+
|
|
6
|
+
const shellStyle = {
|
|
7
|
+
position: "relative",
|
|
8
|
+
alignSelf: "flex-start",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
const viewportStyle = {
|
|
12
|
+
alignSelf: "flex-start",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
const measuredContentStyle: ViewStyle = {
|
|
16
|
+
position: "absolute",
|
|
17
|
+
left: 0,
|
|
18
|
+
top: 0,
|
|
19
|
+
opacity: 0,
|
|
20
|
+
alignSelf: "flex-start",
|
|
21
|
+
flexShrink: 0,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const clippedViewportStyle: ViewStyle = {
|
|
25
|
+
overflow: "hidden",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const unclippedViewportStyle: ViewStyle = {
|
|
29
|
+
overflow: "visible",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type MorphViewportProps = {
|
|
33
|
+
readonly autoSize: boolean;
|
|
34
|
+
readonly clipToBounds: boolean;
|
|
35
|
+
readonly containerStyle?: StyleProp<ViewStyle>;
|
|
36
|
+
readonly animatedWidthStyle?: React.ComponentProps<typeof Animated.View>["style"];
|
|
37
|
+
readonly measurement?: React.ReactNode;
|
|
38
|
+
readonly children: React.ReactNode;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const MorphViewport = React.memo(
|
|
42
|
+
({
|
|
43
|
+
autoSize,
|
|
44
|
+
clipToBounds,
|
|
45
|
+
containerStyle,
|
|
46
|
+
animatedWidthStyle,
|
|
47
|
+
measurement,
|
|
48
|
+
children,
|
|
49
|
+
}: MorphViewportProps) => {
|
|
50
|
+
const resolvedViewportStyle = useMemo(
|
|
51
|
+
() => [
|
|
52
|
+
viewportStyle,
|
|
53
|
+
clipToBounds ? clippedViewportStyle : unclippedViewportStyle,
|
|
54
|
+
],
|
|
55
|
+
[clipToBounds]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View style={[shellStyle, containerStyle]}>
|
|
60
|
+
{autoSize ? (
|
|
61
|
+
<>
|
|
62
|
+
<View
|
|
63
|
+
accessibilityElementsHidden
|
|
64
|
+
collapsable={false}
|
|
65
|
+
importantForAccessibility="no-hide-descendants"
|
|
66
|
+
pointerEvents="none"
|
|
67
|
+
style={measuredContentStyle}
|
|
68
|
+
>
|
|
69
|
+
{measurement}
|
|
70
|
+
</View>
|
|
71
|
+
<Animated.View style={[resolvedViewportStyle, animatedWidthStyle]}>
|
|
72
|
+
{children}
|
|
73
|
+
</Animated.View>
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<View style={resolvedViewportStyle}>{children}</View>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
MorphViewport.displayName = "MorphViewport";
|