react-native-laminar 1.4.5 → 1.5.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 +36 -6
- package/lib/commonjs/hooks/use-leading-animation.js +49 -0
- package/lib/commonjs/hooks/use-leading-animation.js.map +1 -0
- package/lib/commonjs/hooks/use-morph-motion.js +3 -1
- package/lib/commonjs/hooks/use-morph-motion.js.map +1 -1
- package/lib/commonjs/index.js +35 -6
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/motion/preset-map.js +5 -0
- package/lib/commonjs/motion/preset-map.js.map +1 -1
- package/lib/commonjs/view/leading-element.js +37 -0
- package/lib/commonjs/view/leading-element.js.map +1 -0
- package/lib/commonjs/view/morph-viewport.js +26 -0
- package/lib/commonjs/view/morph-viewport.js.map +1 -1
- package/lib/commonjs/view/number-run.js +70 -22
- package/lib/commonjs/view/number-run.js.map +1 -1
- package/lib/commonjs/view/slot-shadow.js +77 -0
- package/lib/commonjs/view/slot-shadow.js.map +1 -0
- package/lib/commonjs/view/slots-run.js +111 -42
- package/lib/commonjs/view/slots-run.js.map +1 -1
- package/lib/commonjs/view/text-run.js +13 -24
- package/lib/commonjs/view/text-run.js.map +1 -1
- package/lib/commonjs/view/ticker-run.js +127 -0
- package/lib/commonjs/view/ticker-run.js.map +1 -0
- package/lib/module/hooks/use-leading-animation.js +44 -0
- package/lib/module/hooks/use-leading-animation.js.map +1 -0
- package/lib/module/hooks/use-morph-motion.js +3 -1
- package/lib/module/hooks/use-morph-motion.js.map +1 -1
- package/lib/module/index.js +35 -6
- package/lib/module/index.js.map +1 -1
- package/lib/module/motion/preset-map.js +5 -0
- package/lib/module/motion/preset-map.js.map +1 -1
- package/lib/module/view/leading-element.js +32 -0
- package/lib/module/view/leading-element.js.map +1 -0
- package/lib/module/view/morph-viewport.js +28 -2
- package/lib/module/view/morph-viewport.js.map +1 -1
- package/lib/module/view/number-run.js +70 -23
- package/lib/module/view/number-run.js.map +1 -1
- package/lib/module/view/slot-shadow.js +72 -0
- package/lib/module/view/slot-shadow.js.map +1 -0
- package/lib/module/view/slots-run.js +111 -42
- package/lib/module/view/slots-run.js.map +1 -1
- package/lib/module/view/text-run.js +14 -25
- package/lib/module/view/text-run.js.map +1 -1
- package/lib/module/view/ticker-run.js +121 -0
- package/lib/module/view/ticker-run.js.map +1 -0
- package/lib/typescript/commonjs/hooks/use-leading-animation.d.ts +16 -0
- package/lib/typescript/commonjs/hooks/use-leading-animation.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts +3 -2
- package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/motion/preset-map.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +14 -2
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/leading-element.d.ts +14 -0
- package/lib/typescript/commonjs/view/leading-element.d.ts.map +1 -0
- package/lib/typescript/commonjs/view/morph-viewport.d.ts +4 -1
- package/lib/typescript/commonjs/view/morph-viewport.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/number-run.d.ts +6 -2
- package/lib/typescript/commonjs/view/number-run.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/slot-shadow.d.ts +11 -0
- package/lib/typescript/commonjs/view/slot-shadow.d.ts.map +1 -0
- package/lib/typescript/commonjs/view/slots-run.d.ts +8 -3
- package/lib/typescript/commonjs/view/slots-run.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/text-run.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/ticker-run.d.ts +21 -0
- package/lib/typescript/commonjs/view/ticker-run.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-leading-animation.d.ts +16 -0
- package/lib/typescript/module/hooks/use-leading-animation.d.ts.map +1 -0
- package/lib/typescript/module/hooks/use-morph-motion.d.ts +3 -2
- package/lib/typescript/module/hooks/use-morph-motion.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/motion/preset-map.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +14 -2
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/view/leading-element.d.ts +14 -0
- package/lib/typescript/module/view/leading-element.d.ts.map +1 -0
- package/lib/typescript/module/view/morph-viewport.d.ts +4 -1
- package/lib/typescript/module/view/morph-viewport.d.ts.map +1 -1
- package/lib/typescript/module/view/number-run.d.ts +6 -2
- package/lib/typescript/module/view/number-run.d.ts.map +1 -1
- package/lib/typescript/module/view/slot-shadow.d.ts +11 -0
- package/lib/typescript/module/view/slot-shadow.d.ts.map +1 -0
- package/lib/typescript/module/view/slots-run.d.ts +8 -3
- package/lib/typescript/module/view/slots-run.d.ts.map +1 -1
- package/lib/typescript/module/view/text-run.d.ts.map +1 -1
- package/lib/typescript/module/view/ticker-run.d.ts +21 -0
- package/lib/typescript/module/view/ticker-run.d.ts.map +1 -0
- package/package.json +3 -1
- package/src/hooks/use-leading-animation.ts +74 -0
- package/src/hooks/use-morph-motion.ts +10 -1
- package/src/index.tsx +44 -6
- package/src/motion/preset-map.ts +5 -0
- package/src/types.ts +17 -2
- package/src/view/leading-element.tsx +45 -0
- package/src/view/morph-viewport.tsx +39 -1
- package/src/view/number-run.tsx +94 -26
- package/src/view/slot-shadow.tsx +99 -0
- package/src/view/slots-run.tsx +163 -58
- package/src/view/text-run.tsx +14 -51
- package/src/view/ticker-run.tsx +167 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"number-run.d.ts","sourceRoot":"","sources":["../../../../src/view/number-run.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"number-run.d.ts","sourceRoot":"","sources":["../../../../src/view/number-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAgB3D,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAGF,eAAO,MAAM,SAAS,iJAajB,QAAQ,CAAC,cAAc,CAAC,uBAuG5B,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type SlotShadowProps = {
|
|
3
|
+
readonly slotHeight: number;
|
|
4
|
+
readonly viewportHeight: number;
|
|
5
|
+
readonly color?: string;
|
|
6
|
+
readonly size?: number;
|
|
7
|
+
readonly children: React.ReactElement;
|
|
8
|
+
};
|
|
9
|
+
export declare const SlotShadow: React.NamedExoticComponent<SlotShadowProps>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=slot-shadow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-shadow.d.ts","sourceRoot":"","sources":["../../../../src/view/slot-shadow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAkD1B,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC;CACvC,CAAC;AAIF,eAAO,MAAM,UAAU,6CA+BrB,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
2
|
import { type StyleProp, type TextStyle } from "react-native";
|
|
3
|
-
import type { LaminarAlign, MotionRecipe } from "../types";
|
|
3
|
+
import type { LaminarAlign, LaminarShadow, MotionRecipe } from "../types";
|
|
4
4
|
type SlotsRunProps = {
|
|
5
5
|
readonly value: string;
|
|
6
6
|
readonly motionRecipe: MotionRecipe;
|
|
@@ -9,7 +9,12 @@ type SlotsRunProps = {
|
|
|
9
9
|
readonly textStyle?: StyleProp<TextStyle>;
|
|
10
10
|
readonly staggerMs: number;
|
|
11
11
|
readonly className?: string;
|
|
12
|
+
readonly leading?: ReactNode;
|
|
13
|
+
readonly leadingKey?: string | number;
|
|
14
|
+
readonly leadingGap?: number;
|
|
15
|
+
readonly ready?: boolean;
|
|
16
|
+
readonly shadow?: LaminarShadow;
|
|
12
17
|
};
|
|
13
|
-
export declare const SlotsRun: React.MemoExoticComponent<({ value, motionRecipe, align, fontSize, textStyle, staggerMs, className, }: Readonly<SlotsRunProps>) => React.JSX.Element>;
|
|
18
|
+
export declare const SlotsRun: React.MemoExoticComponent<({ value, motionRecipe, align, fontSize, textStyle, staggerMs, className, leading, leadingKey, leadingGap, ready, shadow, }: Readonly<SlotsRunProps>) => React.JSX.Element>;
|
|
14
19
|
export {};
|
|
15
20
|
//# sourceMappingURL=slots-run.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slots-run.d.ts","sourceRoot":"","sources":["../../../../src/view/slots-run.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"slots-run.d.ts","sourceRoot":"","sources":["../../../../src/view/slots-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAA8B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1E,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,SAAS,EAEf,MAAM,cAAc,CAAC;AAStB,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EAEb,MAAM,UAAU,CAAC;AAqSlB,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC,CAAC;AAGF,eAAO,MAAM,QAAQ,yJAchB,QAAQ,CAAC,aAAa,CAAC,uBA+I3B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-run.d.ts","sourceRoot":"","sources":["../../../../src/view/text-run.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"text-run.d.ts","sourceRoot":"","sources":["../../../../src/view/text-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG3D,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,OAAO,gJAYf,YAAY,uBAqFhB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import { type StyleProp, type TextStyle } from "react-native";
|
|
3
|
+
import type { LaminarAlign, LaminarShadow, MotionRecipe } from "../types";
|
|
4
|
+
type TickerRunProps = {
|
|
5
|
+
readonly value: string;
|
|
6
|
+
readonly motionRecipe: MotionRecipe;
|
|
7
|
+
readonly align: LaminarAlign;
|
|
8
|
+
readonly fontSize?: number;
|
|
9
|
+
readonly textStyle?: StyleProp<TextStyle>;
|
|
10
|
+
readonly staggerMs: number;
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
readonly leading?: ReactNode;
|
|
13
|
+
readonly leadingKey?: string | number;
|
|
14
|
+
readonly leadingGap?: number;
|
|
15
|
+
readonly ready?: boolean;
|
|
16
|
+
/** accepted for API compatibility; flow values stay crisp and unmasked */
|
|
17
|
+
readonly shadow?: LaminarShadow;
|
|
18
|
+
};
|
|
19
|
+
export declare const TickerRun: React.MemoExoticComponent<({ value, motionRecipe, align, textStyle, className, leading, leadingKey, leadingGap, ready, }: Readonly<TickerRunProps>) => React.JSX.Element>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=ticker-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticker-run.d.ts","sourceRoot":"","sources":["../../../../src/view/ticker-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAKpE,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACb,MAAM,UAAU,CAAC;AAsBlB,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC,CAAC;AAKF,eAAO,MAAM,SAAS,4HAWjB,QAAQ,CAAC,cAAc,CAAC,uBAsG5B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-laminar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A React Native component for morphing text and numbers with character-level identity.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
},
|
|
51
51
|
"sideEffects": false,
|
|
52
52
|
"peerDependencies": {
|
|
53
|
+
"@react-native-masked-view/masked-view": ">=0.3",
|
|
54
|
+
"expo-linear-gradient": ">=15",
|
|
53
55
|
"react": ">=18",
|
|
54
56
|
"react-native": ">=0.74",
|
|
55
57
|
"react-native-reanimated": ">=3"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
|
|
2
|
+
import { useSharedValue } from "react-native-reanimated";
|
|
3
|
+
import {
|
|
4
|
+
createLeadingEnterTransition,
|
|
5
|
+
createLeadingExitTransition,
|
|
6
|
+
} from "../motion/entry-exit-builders";
|
|
7
|
+
import type { MotionRecipe } from "../types";
|
|
8
|
+
|
|
9
|
+
type UseLeadingAnimationParams = {
|
|
10
|
+
readonly leading?: ReactNode;
|
|
11
|
+
readonly leadingKey?: string | number;
|
|
12
|
+
readonly leadingGap: number;
|
|
13
|
+
readonly motionRecipe: MotionRecipe;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// keep leading presence and replacement motion consistent across every content variant
|
|
17
|
+
export const useLeadingAnimation = ({
|
|
18
|
+
leading,
|
|
19
|
+
leadingKey,
|
|
20
|
+
leadingGap,
|
|
21
|
+
motionRecipe,
|
|
22
|
+
}: UseLeadingAnimationParams) => {
|
|
23
|
+
const lastLeadingPresenceRef = useRef(Boolean(leading));
|
|
24
|
+
const lastLeadingKeyRef = useRef(leadingKey);
|
|
25
|
+
const isLeadingSwap =
|
|
26
|
+
Boolean(leading) &&
|
|
27
|
+
lastLeadingPresenceRef.current &&
|
|
28
|
+
leadingKey !== lastLeadingKeyRef.current;
|
|
29
|
+
const isLeadingChange =
|
|
30
|
+
Boolean(leading) !== lastLeadingPresenceRef.current ||
|
|
31
|
+
(Boolean(leading) && leadingKey !== lastLeadingKeyRef.current);
|
|
32
|
+
const leadingSwapProgress = useSharedValue(0);
|
|
33
|
+
|
|
34
|
+
// update the worklet flag after commit so render never writes a shared value
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
leadingSwapProgress.value = isLeadingSwap ? 1 : 0;
|
|
37
|
+
}, [isLeadingSwap, leadingSwapProgress]);
|
|
38
|
+
|
|
39
|
+
const elementEnterTransition = useMemo(
|
|
40
|
+
() =>
|
|
41
|
+
createLeadingEnterTransition({
|
|
42
|
+
durationMs: motionRecipe.durationMs,
|
|
43
|
+
easing: motionRecipe.easing,
|
|
44
|
+
}),
|
|
45
|
+
[motionRecipe.durationMs, motionRecipe.easing]
|
|
46
|
+
);
|
|
47
|
+
const elementExitTransition = useMemo(
|
|
48
|
+
() =>
|
|
49
|
+
createLeadingExitTransition({
|
|
50
|
+
durationMs: motionRecipe.durationMs,
|
|
51
|
+
easing: motionRecipe.easing,
|
|
52
|
+
leadingGap,
|
|
53
|
+
scale: leadingSwapProgress,
|
|
54
|
+
}),
|
|
55
|
+
[
|
|
56
|
+
leadingGap,
|
|
57
|
+
leadingSwapProgress,
|
|
58
|
+
motionRecipe.durationMs,
|
|
59
|
+
motionRecipe.easing,
|
|
60
|
+
]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (isLeadingChange) {
|
|
64
|
+
lastLeadingPresenceRef.current = Boolean(leading);
|
|
65
|
+
lastLeadingKeyRef.current = leadingKey;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
elementEnterTransition,
|
|
70
|
+
elementExitTransition,
|
|
71
|
+
isLeadingChange,
|
|
72
|
+
isLeadingSwap,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
@@ -4,10 +4,12 @@ import type {
|
|
|
4
4
|
MotionRecipe,
|
|
5
5
|
MorphAnimationPresetName,
|
|
6
6
|
MorphContentVariant,
|
|
7
|
+
MorphNumberMode,
|
|
7
8
|
} from "../types";
|
|
8
9
|
|
|
9
10
|
type Params = {
|
|
10
11
|
readonly variant: MorphContentVariant;
|
|
12
|
+
readonly numberMode?: MorphNumberMode;
|
|
11
13
|
readonly animationPreset?: MorphAnimationPresetName;
|
|
12
14
|
readonly animationDuration?: number;
|
|
13
15
|
readonly stagger: number;
|
|
@@ -21,13 +23,20 @@ type MorphMotion = {
|
|
|
21
23
|
// resolve one motion recipe and one shared stagger unit for a render tree
|
|
22
24
|
export const useMorphMotion = ({
|
|
23
25
|
variant,
|
|
26
|
+
numberMode,
|
|
24
27
|
animationPreset,
|
|
25
28
|
animationDuration,
|
|
26
29
|
stagger,
|
|
27
30
|
}: Params): MorphMotion => {
|
|
31
|
+
const isTicker = variant === "number" && numberMode === "ticker";
|
|
28
32
|
// numeric variants need a quicker default because their lanes update often
|
|
29
33
|
const resolvedPreset =
|
|
30
|
-
animationPreset ??
|
|
34
|
+
animationPreset ??
|
|
35
|
+
(isTicker
|
|
36
|
+
? "ticker"
|
|
37
|
+
: variant === "text"
|
|
38
|
+
? "default"
|
|
39
|
+
: "snappy");
|
|
31
40
|
|
|
32
41
|
const motionRecipe = useMemo(
|
|
33
42
|
() => resolveMotionRecipe(resolvedPreset, animationDuration),
|
package/src/index.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import type { LaminarLeadingMap, MorphingTextProps } from "./types";
|
|
|
11
11
|
import { MorphViewport } from "./view/morph-viewport";
|
|
12
12
|
import { NumberRun } from "./view/number-run";
|
|
13
13
|
import { SlotsRun } from "./view/slots-run";
|
|
14
|
+
import { TickerRun } from "./view/ticker-run";
|
|
14
15
|
import { TextRun } from "./view/text-run";
|
|
15
16
|
|
|
16
17
|
// choose the content variant and connect it to the shared viewport and measurement flow
|
|
@@ -24,12 +25,14 @@ export const Laminar = React.memo(function Laminar({
|
|
|
24
25
|
leading,
|
|
25
26
|
leadingKey,
|
|
26
27
|
leadingGap = 0,
|
|
28
|
+
numberMode = "morph",
|
|
29
|
+
shadow = false,
|
|
27
30
|
style,
|
|
28
31
|
containerStyle,
|
|
29
32
|
fontStyle,
|
|
30
33
|
animationDuration,
|
|
31
34
|
animationPreset,
|
|
32
|
-
stagger
|
|
35
|
+
stagger,
|
|
33
36
|
autoSize = true,
|
|
34
37
|
clipToBounds = false,
|
|
35
38
|
}: Readonly<MorphingTextProps>) {
|
|
@@ -40,11 +43,16 @@ export const Laminar = React.memo(function Laminar({
|
|
|
40
43
|
? leadingMap[resolvedValue]
|
|
41
44
|
: (leading as React.ReactNode);
|
|
42
45
|
const resolvedLeadingKey = leadingMap ? resolvedValue : leadingKey;
|
|
46
|
+
const resolvedStagger =
|
|
47
|
+
stagger ?? (variant === "number" && numberMode === "ticker" ? 0 : 0.02);
|
|
48
|
+
const useLayoutAutoSize =
|
|
49
|
+
autoSize && variant === "number" && numberMode === "ticker";
|
|
43
50
|
const { motionRecipe, staggerMs } = useMorphMotion({
|
|
44
51
|
variant,
|
|
52
|
+
numberMode,
|
|
45
53
|
animationPreset,
|
|
46
54
|
animationDuration,
|
|
47
|
-
stagger,
|
|
55
|
+
stagger: resolvedStagger,
|
|
48
56
|
});
|
|
49
57
|
const { textStyle } = useMorphTextStyle({
|
|
50
58
|
fontSize,
|
|
@@ -67,9 +75,9 @@ export const Laminar = React.memo(function Laminar({
|
|
|
67
75
|
flattenedStyle?.letterSpacing,
|
|
68
76
|
flattenedStyle?.lineHeight,
|
|
69
77
|
flattenedStyle?.textTransform,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
Boolean(resolvedLeading),
|
|
79
|
+
resolvedLeadingKey,
|
|
80
|
+
leadingGap,
|
|
73
81
|
]);
|
|
74
82
|
}, [
|
|
75
83
|
leadingGap,
|
|
@@ -109,6 +117,9 @@ export const Laminar = React.memo(function Laminar({
|
|
|
109
117
|
align={align}
|
|
110
118
|
containerStyle={containerStyle}
|
|
111
119
|
animatedWidthStyle={animatedWidthStyle}
|
|
120
|
+
autoSizeLayoutTransition={
|
|
121
|
+
useLayoutAutoSize ? motionRecipe.layoutTransition : undefined
|
|
122
|
+
}
|
|
112
123
|
onVisibleLayout={autoSize ? captureVisibleLayout : undefined}
|
|
113
124
|
measurement={
|
|
114
125
|
shouldMeasure ? (
|
|
@@ -117,7 +128,7 @@ export const Laminar = React.memo(function Laminar({
|
|
|
117
128
|
onLayout={captureLayout}
|
|
118
129
|
style={{ flexDirection: "row", alignSelf: "flex-start" }}
|
|
119
130
|
>
|
|
120
|
-
{
|
|
131
|
+
{resolvedLeading ? (
|
|
121
132
|
<View
|
|
122
133
|
style={{
|
|
123
134
|
alignItems: "center",
|
|
@@ -146,6 +157,26 @@ export const Laminar = React.memo(function Laminar({
|
|
|
146
157
|
textStyle={textStyle}
|
|
147
158
|
staggerMs={staggerMs}
|
|
148
159
|
className={className}
|
|
160
|
+
leading={resolvedLeading}
|
|
161
|
+
leadingKey={resolvedLeadingKey}
|
|
162
|
+
leadingGap={leadingGap}
|
|
163
|
+
ready={!autoSize || isAutoSizeReady}
|
|
164
|
+
shadow={shadow}
|
|
165
|
+
/>
|
|
166
|
+
) : variant === "number" && numberMode === "ticker" ? (
|
|
167
|
+
<TickerRun
|
|
168
|
+
value={resolvedValue}
|
|
169
|
+
motionRecipe={motionRecipe}
|
|
170
|
+
align={align}
|
|
171
|
+
fontSize={fontSize}
|
|
172
|
+
textStyle={textStyle}
|
|
173
|
+
staggerMs={staggerMs}
|
|
174
|
+
className={className}
|
|
175
|
+
leading={resolvedLeading}
|
|
176
|
+
leadingKey={resolvedLeadingKey}
|
|
177
|
+
leadingGap={leadingGap}
|
|
178
|
+
ready={!autoSize || isAutoSizeReady}
|
|
179
|
+
shadow={shadow}
|
|
149
180
|
/>
|
|
150
181
|
) : variant === "number" ? (
|
|
151
182
|
<NumberRun
|
|
@@ -156,6 +187,10 @@ export const Laminar = React.memo(function Laminar({
|
|
|
156
187
|
textStyle={textStyle}
|
|
157
188
|
staggerMs={staggerMs}
|
|
158
189
|
className={className}
|
|
190
|
+
leading={resolvedLeading}
|
|
191
|
+
leadingKey={resolvedLeadingKey}
|
|
192
|
+
leadingGap={leadingGap}
|
|
193
|
+
ready={!autoSize || isAutoSizeReady}
|
|
159
194
|
/>
|
|
160
195
|
) : (
|
|
161
196
|
<TextRun
|
|
@@ -190,8 +225,11 @@ function isLeadingMap(
|
|
|
190
225
|
export type {
|
|
191
226
|
LaminarAlign,
|
|
192
227
|
LaminarLeadingMap,
|
|
228
|
+
LaminarShadow,
|
|
229
|
+
LaminarShadowOptions,
|
|
193
230
|
LaminarProps,
|
|
194
231
|
MorphAnimationPresetName,
|
|
195
232
|
MorphContentVariant,
|
|
233
|
+
MorphNumberMode,
|
|
196
234
|
MorphingTextProps,
|
|
197
235
|
} from "./types";
|
package/src/motion/preset-map.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
|
|
16
16
|
// keep all presets on one curve family so layout and glyph motion agree
|
|
17
17
|
const DEFAULT_CURVE = [0.19, 1, 0.22, 1] as const;
|
|
18
|
+
const TICKER_CURVE = [0.33, 1, 0.68, 1] as const;
|
|
18
19
|
|
|
19
20
|
export const MOTION_PRESETS: Record<
|
|
20
21
|
MorphAnimationPresetName,
|
|
@@ -39,6 +40,10 @@ export const MOTION_PRESETS: Record<
|
|
|
39
40
|
duration: 0.5,
|
|
40
41
|
bounce: 0.3,
|
|
41
42
|
},
|
|
43
|
+
ticker: {
|
|
44
|
+
duration: 0.3,
|
|
45
|
+
ease: TICKER_CURVE,
|
|
46
|
+
},
|
|
42
47
|
} as const;
|
|
43
48
|
|
|
44
49
|
// the public preset table uses seconds while reanimated timing uses milliseconds
|
package/src/types.ts
CHANGED
|
@@ -10,13 +10,24 @@ export type MorphAnimationPresetName =
|
|
|
10
10
|
| "default"
|
|
11
11
|
| "smooth"
|
|
12
12
|
| "snappy"
|
|
13
|
-
| "bouncy"
|
|
13
|
+
| "bouncy"
|
|
14
|
+
| "ticker";
|
|
14
15
|
|
|
15
16
|
export type MorphContentVariant = "text" | "number" | "slots";
|
|
17
|
+
export type MorphNumberMode = "morph" | "ticker";
|
|
16
18
|
export type LaminarAlign = "left" | "center" | "right";
|
|
17
19
|
// text keys let leading content follow the same state as the displayed value
|
|
18
20
|
export type LaminarLeadingMap = Readonly<Record<string, ReactNode>>;
|
|
19
21
|
|
|
22
|
+
export type LaminarShadowOptions = {
|
|
23
|
+
/** surface color used by the alpha mask when a platform requires it */
|
|
24
|
+
readonly color?: string;
|
|
25
|
+
/** fade depth in pixels; defaults to roughly one third of the viewport */
|
|
26
|
+
readonly size?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type LaminarShadow = boolean | LaminarShadowOptions;
|
|
30
|
+
|
|
20
31
|
type CubicBezierTuple = readonly [number, number, number, number];
|
|
21
32
|
|
|
22
33
|
export type MorphAnimationPreset =
|
|
@@ -52,16 +63,20 @@ export type MotionRecipe = {
|
|
|
52
63
|
export type MorphingTextProps = {
|
|
53
64
|
readonly text: string | number;
|
|
54
65
|
readonly variant?: MorphContentVariant;
|
|
66
|
+
/** number-only renderer mode; ticker uses crisp in-place digit flow */
|
|
67
|
+
readonly numberMode?: MorphNumberMode;
|
|
55
68
|
readonly fontSize?: number;
|
|
56
69
|
readonly color?: string;
|
|
57
70
|
readonly align?: LaminarAlign;
|
|
58
71
|
readonly className?: string;
|
|
59
|
-
/** optional inline element reconciled and animated
|
|
72
|
+
/** optional inline element reconciled and animated before the rendered value */
|
|
60
73
|
readonly leading?: ReactNode | LaminarLeadingMap;
|
|
61
74
|
/** identity for swapping one leading element for another */
|
|
62
75
|
readonly leadingKey?: string | number;
|
|
63
76
|
/** spacing between the leading element and the first text glyph */
|
|
64
77
|
readonly leadingGap?: number;
|
|
78
|
+
/** opt-in top and bottom fades for slot reels */
|
|
79
|
+
readonly shadow?: LaminarShadow;
|
|
65
80
|
readonly style?: StyleProp<TextStyle>;
|
|
66
81
|
readonly containerStyle?: StyleProp<ViewStyle>;
|
|
67
82
|
readonly fontStyle?: StyleProp<TextStyle>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import Animated, {
|
|
3
|
+
type EntryExitAnimationFunction,
|
|
4
|
+
} from "react-native-reanimated";
|
|
5
|
+
|
|
6
|
+
type LeadingElementProps = {
|
|
7
|
+
readonly leading?: ReactNode;
|
|
8
|
+
readonly leadingKey?: string | number;
|
|
9
|
+
readonly leadingGap: number;
|
|
10
|
+
readonly enterTransition?: EntryExitAnimationFunction;
|
|
11
|
+
readonly elementEnterTransition?: EntryExitAnimationFunction;
|
|
12
|
+
readonly exitTransition?: EntryExitAnimationFunction;
|
|
13
|
+
readonly elementExitTransition?: EntryExitAnimationFunction;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// render an inline element independently so it can enter, exit, and swap beside any run
|
|
17
|
+
export const LeadingElement = React.memo(function LeadingElement({
|
|
18
|
+
leading,
|
|
19
|
+
leadingKey,
|
|
20
|
+
leadingGap,
|
|
21
|
+
enterTransition,
|
|
22
|
+
elementEnterTransition,
|
|
23
|
+
exitTransition,
|
|
24
|
+
elementExitTransition,
|
|
25
|
+
}: LeadingElementProps) {
|
|
26
|
+
if (!leading) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Animated.View
|
|
32
|
+
key={`leading:${String(leadingKey ?? "default")}`}
|
|
33
|
+
entering={elementEnterTransition ?? enterTransition}
|
|
34
|
+
exiting={elementExitTransition ?? exitTransition}
|
|
35
|
+
style={[
|
|
36
|
+
{ alignItems: "center", justifyContent: "center" },
|
|
37
|
+
leadingGap > 0 ? { marginRight: leadingGap } : undefined,
|
|
38
|
+
]}
|
|
39
|
+
>
|
|
40
|
+
{leading}
|
|
41
|
+
</Animated.View>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
LeadingElement.displayName = "LeadingElement";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useMemo } from "react";
|
|
1
|
+
import React, { useMemo, useRef } from "react";
|
|
2
2
|
import type { LayoutChangeEvent, StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
import { View } from "react-native";
|
|
4
4
|
import Animated from "react-native-reanimated";
|
|
5
|
+
import type { ComplexAnimationBuilder } from "react-native-reanimated";
|
|
5
6
|
import type { LaminarAlign } from "../types";
|
|
6
7
|
|
|
7
8
|
const shellStyle = {
|
|
@@ -52,6 +53,8 @@ type MorphViewportProps = {
|
|
|
52
53
|
readonly align: LaminarAlign;
|
|
53
54
|
readonly containerStyle?: StyleProp<ViewStyle>;
|
|
54
55
|
readonly animatedWidthStyle?: React.ComponentProps<typeof Animated.View>["style"];
|
|
56
|
+
/** optional layout-driven auto-size path for content that needs an aligned anchor */
|
|
57
|
+
readonly autoSizeLayoutTransition?: ComplexAnimationBuilder;
|
|
55
58
|
readonly onVisibleLayout?: (event: LayoutChangeEvent) => void;
|
|
56
59
|
readonly measurement?: React.ReactNode;
|
|
57
60
|
readonly children: React.ReactNode;
|
|
@@ -65,10 +68,16 @@ export const MorphViewport = React.memo(
|
|
|
65
68
|
align,
|
|
66
69
|
containerStyle,
|
|
67
70
|
animatedWidthStyle,
|
|
71
|
+
autoSizeLayoutTransition,
|
|
68
72
|
onVisibleLayout,
|
|
69
73
|
measurement,
|
|
70
74
|
children,
|
|
71
75
|
}: MorphViewportProps) => {
|
|
76
|
+
const hasRenderedRef = useRef(false);
|
|
77
|
+
const shouldAnimateAutoSizeLayout =
|
|
78
|
+
autoSizeLayoutTransition !== undefined && hasRenderedRef.current;
|
|
79
|
+
hasRenderedRef.current = true;
|
|
80
|
+
|
|
72
81
|
// compute alignment once so the animated shell and its child share one layout contract
|
|
73
82
|
const resolvedViewportStyle = useMemo(
|
|
74
83
|
() => [
|
|
@@ -80,6 +89,35 @@ export const MorphViewport = React.memo(
|
|
|
80
89
|
);
|
|
81
90
|
|
|
82
91
|
// keep measurement outside the visible viewport so it cannot affect accessibility or layout
|
|
92
|
+
if (autoSizeLayoutTransition) {
|
|
93
|
+
// Layout animation owns both width and the shell's aligned x-position.
|
|
94
|
+
// This is important for center/right auto-size: animating only a child
|
|
95
|
+
// width leaves the parent anchor at its old position.
|
|
96
|
+
return (
|
|
97
|
+
<Animated.View
|
|
98
|
+
layout={
|
|
99
|
+
shouldAnimateAutoSizeLayout
|
|
100
|
+
? autoSizeLayoutTransition
|
|
101
|
+
: undefined
|
|
102
|
+
}
|
|
103
|
+
style={[shellStyle, shellAlignStyles[align], containerStyle]}
|
|
104
|
+
>
|
|
105
|
+
<View
|
|
106
|
+
accessibilityElementsHidden
|
|
107
|
+
collapsable={false}
|
|
108
|
+
importantForAccessibility="no-hide-descendants"
|
|
109
|
+
pointerEvents="none"
|
|
110
|
+
style={measuredContentStyle}
|
|
111
|
+
>
|
|
112
|
+
{measurement}
|
|
113
|
+
</View>
|
|
114
|
+
<Animated.View onLayout={onVisibleLayout} style={resolvedViewportStyle}>
|
|
115
|
+
{children}
|
|
116
|
+
</Animated.View>
|
|
117
|
+
</Animated.View>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
83
121
|
return (
|
|
84
122
|
<View style={[shellStyle, shellAlignStyles[align], containerStyle]}>
|
|
85
123
|
{autoSize ? (
|