react-native-slot-text 3.0.0 → 3.2.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/AnimatedNumbers.d.ts +10 -4
- package/AnimatedNumbers.js +10 -4
- package/ContinuousSlot.js +75 -22
- package/ContinuousSlots.js +1 -1
- package/Slot.js +139 -69
- package/Slots.js +25 -33
- package/constants.d.ts +3 -0
- package/constants.js +5 -0
- package/helpers.d.ts +1 -1
- package/helpers.js +10 -10
- package/package.json +1 -1
- package/types.d.ts +22 -4
package/AnimatedNumbers.d.ts
CHANGED
@@ -6,16 +6,22 @@ import type { AnimatedNumbersProps } from './types';
|
|
6
6
|
* It supports animation of individual digits, optional commas, and a customizable prefix.
|
7
7
|
* The animation occurs over a defined duration and can be repeated as the value changes.
|
8
8
|
*
|
9
|
-
* @param {number
|
9
|
+
* @param {number} props.value - The value to animate to.
|
10
10
|
* @param {Object} props.fontStyle - The style of the text, passed as a TextStyle object.
|
11
|
-
* @param {number} [props.animationDuration=
|
12
|
-
*
|
11
|
+
* @param {number} [props.animationDuration=500] - The duration of the animation in milliseconds.
|
12
|
+
* Defaults to 500ms (700ms when also animating intermediate values) Only supported when
|
13
|
+
* animateIntermediateValues is false.
|
13
14
|
* @param {string} [props.prefix=""] - A prefix to the number, such as a currency symbol.
|
14
15
|
* @param {boolean} [props.includeComma=false] - Whether to include commas as thousand separators.
|
15
16
|
* @param {true} [props.animateIntermediateValues=false] - Whether to animate all intermediate numbers between new value
|
16
17
|
* and current value of a slot. If the value is changing rapidly, this option is best. Otherwise the animations
|
17
18
|
* may glitch or act unexpectedly.
|
18
|
-
* @param {number} [props.precision] - Number of decimal places.
|
19
|
+
* @param {number} [props.precision] - Number of decimal places. For example, a value prop of 42069 with precision of 2 would become 420.69.
|
20
|
+
* @param {boolean|Object} [props.spring=false] - Spring transition, can be used in place of easing for physics-based transitions.
|
21
|
+
* If true, a default spring configuration will be applied. If an object, it must include the following keys:
|
22
|
+
* - {number} mass - The mass of the spring (affects how heavy it feels).
|
23
|
+
* - {number} stiffness - The stiffness of the spring (affects how bouncy it feels).
|
24
|
+
* - {number} damping - The damping of the spring (affects how quickly it slows down).
|
19
25
|
*
|
20
26
|
* @returns {JSX.Element} The animated number component with slots for digits and commas.
|
21
27
|
*/
|
package/AnimatedNumbers.js
CHANGED
@@ -9,16 +9,22 @@ import Slots from './Slots';
|
|
9
9
|
* It supports animation of individual digits, optional commas, and a customizable prefix.
|
10
10
|
* The animation occurs over a defined duration and can be repeated as the value changes.
|
11
11
|
*
|
12
|
-
* @param {number
|
12
|
+
* @param {number} props.value - The value to animate to.
|
13
13
|
* @param {Object} props.fontStyle - The style of the text, passed as a TextStyle object.
|
14
|
-
* @param {number} [props.animationDuration=
|
15
|
-
*
|
14
|
+
* @param {number} [props.animationDuration=500] - The duration of the animation in milliseconds.
|
15
|
+
* Defaults to 500ms (700ms when also animating intermediate values) Only supported when
|
16
|
+
* animateIntermediateValues is false.
|
16
17
|
* @param {string} [props.prefix=""] - A prefix to the number, such as a currency symbol.
|
17
18
|
* @param {boolean} [props.includeComma=false] - Whether to include commas as thousand separators.
|
18
19
|
* @param {true} [props.animateIntermediateValues=false] - Whether to animate all intermediate numbers between new value
|
19
20
|
* and current value of a slot. If the value is changing rapidly, this option is best. Otherwise the animations
|
20
21
|
* may glitch or act unexpectedly.
|
21
|
-
* @param {number} [props.precision] - Number of decimal places.
|
22
|
+
* @param {number} [props.precision] - Number of decimal places. For example, a value prop of 42069 with precision of 2 would become 420.69.
|
23
|
+
* @param {boolean|Object} [props.spring=false] - Spring transition, can be used in place of easing for physics-based transitions.
|
24
|
+
* If true, a default spring configuration will be applied. If an object, it must include the following keys:
|
25
|
+
* - {number} mass - The mass of the spring (affects how heavy it feels).
|
26
|
+
* - {number} stiffness - The stiffness of the spring (affects how bouncy it feels).
|
27
|
+
* - {number} damping - The damping of the spring (affects how quickly it slows down).
|
22
28
|
*
|
23
29
|
* @returns {JSX.Element} The animated number component with slots for digits and commas.
|
24
30
|
*/
|
package/ContinuousSlot.js
CHANGED
@@ -3,17 +3,36 @@ import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Text, StyleSheet, Animated, useAnimatedValue, Easing } from 'react-native';
|
4
4
|
import MaskedView from "@react-native-masked-view/masked-view";
|
5
5
|
import { easeGradient } from "react-native-easing-gradient";
|
6
|
-
import ReAnimated, { LinearTransition, FadeOutDown, FadeInUp, useSharedValue, withTiming } from 'react-native-reanimated';
|
6
|
+
import ReAnimated, { LinearTransition, FadeOutDown, FadeInUp, useSharedValue, withTiming, Easing as ReEasing, ReduceMotion, withSpring } from 'react-native-reanimated';
|
7
7
|
import { LinearGradient } from "expo-linear-gradient";
|
8
8
|
import styles from './styles';
|
9
|
+
import { bezier_points } from './constants';
|
9
10
|
const GAP = 12;
|
10
11
|
const PADDING_FRACTION = .25;
|
11
12
|
const AnimatedMaskView = Animated.createAnimatedComponent(MaskedView);
|
12
13
|
const ContinuousSlot = (props) => {
|
14
|
+
const easing = bezier_points[props.easing || 'linear'];
|
15
|
+
const springConfig = {
|
16
|
+
mass: 1,
|
17
|
+
damping: 20,
|
18
|
+
stiffness: 170,
|
19
|
+
useNativeDriver: true,
|
20
|
+
...(typeof props.spring === 'object' ? props.spring : {})
|
21
|
+
};
|
22
|
+
const reSpringConfig = {
|
23
|
+
mass: 1,
|
24
|
+
damping: 27,
|
25
|
+
stiffness: 315,
|
26
|
+
overshootClamping: false,
|
27
|
+
restDisplacementThreshold: 0.01,
|
28
|
+
restSpeedThreshold: 0.01,
|
29
|
+
reduceMotion: ReduceMotion.System,
|
30
|
+
...(typeof props.spring === 'object' ? props.spring : {})
|
31
|
+
};
|
13
32
|
const config = {
|
14
33
|
useNativeDriver: true,
|
15
34
|
duration: props.animationDuration,
|
16
|
-
easing: Easing.bezier(
|
35
|
+
easing: Easing.bezier(...easing)
|
17
36
|
};
|
18
37
|
const id = useRef(`rn-slottext-slot-${Math.random().toString(36).slice(0, 9)}`);
|
19
38
|
const width = useSharedValue(0);
|
@@ -35,30 +54,56 @@ const ContinuousSlot = (props) => {
|
|
35
54
|
: props.slot[0] === ',' ? 10 : 11];
|
36
55
|
width.value = newWidth > width.value
|
37
56
|
? newWidth // Growing
|
38
|
-
:
|
57
|
+
: props.spring
|
58
|
+
? withSpring(newWidth, reSpringConfig)
|
59
|
+
: withTiming(newWidth, { duration: props.animationDuration });
|
39
60
|
if (typeof props.slot[0] === 'number') {
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
...
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
61
|
+
const finalY = -1 * (((GAP + measuredHeight.current) * (9 - props.slot[0])) -
|
62
|
+
(PADDING_FRACTION * measuredHeight.current));
|
63
|
+
if (props.spring) {
|
64
|
+
Animated.spring(slotOpacity, { toValue: 1, ...springConfig }).start();
|
65
|
+
Animated.spring(y, { toValue: finalY, ...springConfig }).start();
|
66
|
+
Animated.spring(commaY, { toValue: 0, ...springConfig }).start();
|
67
|
+
Animated.spring(periodY, { toValue: 0, ...springConfig }).start();
|
68
|
+
Animated.spring(commaOpacity, { toValue: 0, ...springConfig }).start();
|
69
|
+
Animated.spring(periodOpacity, { toValue: 0, ...springConfig }).start();
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
Animated.timing(slotOpacity, { toValue: 1, ...config }).start();
|
73
|
+
Animated.timing(y, { toValue: finalY, ...config }).start();
|
74
|
+
Animated.timing(commaY, { toValue: 0, ...config }).start();
|
75
|
+
Animated.timing(periodY, { toValue: 0, ...config }).start();
|
76
|
+
Animated.timing(commaOpacity, { toValue: 0, ...config }).start();
|
77
|
+
Animated.timing(periodOpacity, { toValue: 0, ...config }).start();
|
78
|
+
}
|
50
79
|
}
|
51
80
|
else if (props.slot[0] === '.') {
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
81
|
+
if (props.spring) {
|
82
|
+
Animated.spring(periodY, { toValue: (PADDING_FRACTION * measuredHeight.current), ...springConfig }).start();
|
83
|
+
Animated.spring(periodOpacity, { toValue: 1, ...springConfig }).start();
|
84
|
+
Animated.spring(slotOpacity, { toValue: 0, ...springConfig }).start();
|
85
|
+
Animated.spring(commaY, { toValue: 0, ...springConfig }).start();
|
86
|
+
}
|
87
|
+
else {
|
88
|
+
Animated.timing(periodY, { toValue: (PADDING_FRACTION * measuredHeight.current), ...config }).start();
|
89
|
+
Animated.timing(periodOpacity, { toValue: 1, ...config }).start();
|
90
|
+
Animated.timing(slotOpacity, { toValue: 0, ...config }).start();
|
91
|
+
Animated.timing(commaY, { toValue: 0, ...config }).start();
|
92
|
+
}
|
56
93
|
}
|
57
94
|
else if (props.slot[0] === ',') {
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
95
|
+
if (props.spring) {
|
96
|
+
Animated.spring(commaY, { toValue: (PADDING_FRACTION * measuredHeight.current), ...springConfig }).start();
|
97
|
+
Animated.spring(commaOpacity, { toValue: 1, ...springConfig }).start();
|
98
|
+
Animated.spring(periodY, { toValue: 1, ...springConfig }).start();
|
99
|
+
Animated.spring(slotOpacity, { toValue: 0, ...springConfig }).start();
|
100
|
+
}
|
101
|
+
else {
|
102
|
+
Animated.timing(commaY, { toValue: (PADDING_FRACTION * measuredHeight.current), ...config }).start();
|
103
|
+
Animated.timing(commaOpacity, { toValue: 1, ...config }).start();
|
104
|
+
Animated.timing(periodY, { toValue: 1, ...config }).start();
|
105
|
+
Animated.timing(slotOpacity, { toValue: 0, ...config }).start();
|
106
|
+
}
|
62
107
|
}
|
63
108
|
}, [props.slot]);
|
64
109
|
// LINEAR GRADIENT
|
@@ -70,7 +115,15 @@ const ContinuousSlot = (props) => {
|
|
70
115
|
1: { color: 'transparent' }
|
71
116
|
},
|
72
117
|
});
|
73
|
-
return (_jsx(ReAnimated.View, { layout:
|
118
|
+
return (_jsx(ReAnimated.View, { layout: props.spring
|
119
|
+
? LinearTransition.springify().mass(reSpringConfig.mass).stiffness(reSpringConfig.stiffness).damping(reSpringConfig.damping)
|
120
|
+
: LinearTransition.duration(props.animationDuration).easing(ReEasing.bezier(...easing).factory()), entering: props.charSizes[0]
|
121
|
+
? props.spring
|
122
|
+
? FadeInUp.springify().mass(reSpringConfig.mass).stiffness(reSpringConfig.stiffness).damping(reSpringConfig.damping)
|
123
|
+
: FadeInUp.duration(props.animationDuration / 1.5).easing(ReEasing.bezier(...easing).factory())
|
124
|
+
: undefined, exiting: props.spring
|
125
|
+
? FadeOutDown.springify().mass(reSpringConfig.mass).stiffness(reSpringConfig.stiffness).damping(reSpringConfig.damping)
|
126
|
+
: FadeOutDown.duration(props.animationDuration / 1.5).easing(ReEasing.bezier(...easing).factory()), children: isMeasured
|
74
127
|
?
|
75
128
|
_jsx(ReAnimated.View, { style: { height, width }, children: _jsxs(AnimatedMaskView, { maskElement: _jsx(LinearGradient, { locations: locations, colors: colors, style: StyleSheet.absoluteFill }), style: [styles.mask, { top: maskTop, bottom: maskTop }], children: [_jsx(Animated.View, { style: {
|
76
129
|
position: 'absolute',
|
package/ContinuousSlots.js
CHANGED
@@ -54,7 +54,7 @@ const ContinuousSlots = (props) => {
|
|
54
54
|
});
|
55
55
|
setSlots(parseFromLeft ? newSlots : newSlots.reverse());
|
56
56
|
}, [props.value]);
|
57
|
-
return (_jsxs(_Fragment, { children: [_jsxs(View, { style: styles.slotsContainer, children: [props.prefix && (_jsx(Animated.Text, { style: props.fontStyle, layout: LinearTransition, children: props.prefix })), slots.map((slot, i) => (_jsx(ContinuousSlot, { slot: slot, index: i, charSizes: charSizes, fontStyle: props.fontStyle, animationDuration: DEFAULT_DURATION }, `${idRef.current}-${slot[1]}`))), _jsx(Text, { style: [styles.spacer, props.fontStyle], children: "1" })] }), !sizesMeasured &&
|
57
|
+
return (_jsxs(_Fragment, { children: [_jsxs(View, { style: styles.slotsContainer, children: [props.prefix && (_jsx(Animated.Text, { style: props.fontStyle, layout: LinearTransition, children: props.prefix })), slots.map((slot, i) => (_jsx(ContinuousSlot, { slot: slot, index: i, charSizes: charSizes, fontStyle: props.fontStyle, easing: props.easing, spring: props.spring, animationDuration: DEFAULT_DURATION }, `${idRef.current}-${slot[1]}`))), _jsx(Text, { style: [styles.spacer, props.fontStyle], children: "1" })] }), !sizesMeasured &&
|
58
58
|
Array.from({ length: 10 }, (_, i) => i)
|
59
59
|
.concat([',', '.'])
|
60
60
|
.map((char, i) => (_jsx(Fragment, { children: _jsx(Text, { style: [styles.hiddenSlot, props.fontStyle], onLayout: (e) => {
|
package/Slot.js
CHANGED
@@ -1,68 +1,128 @@
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
-
import { useLayoutEffect, useState } from 'react';
|
3
|
-
import { View } from 'react-native';
|
4
|
-
import {
|
5
|
-
import Animated, { useSharedValue, withTiming, withSequence, useAnimatedStyle, interpolate, runOnJS, Easing } from 'react-native-reanimated';
|
2
|
+
import { useEffect, useLayoutEffect, useState, useRef } from 'react';
|
3
|
+
import { Animated, Text, View, Easing } from 'react-native';
|
4
|
+
import Reanimated, { useSharedValue, withTiming, withSpring, withSequence, useAnimatedStyle, Easing as ReEasing, ReduceMotion } from 'react-native-reanimated';
|
6
5
|
import styles from './styles';
|
7
|
-
|
6
|
+
import { bezier_points } from './constants';
|
8
7
|
const Slot = (props) => {
|
9
|
-
const
|
10
|
-
const
|
11
|
-
|
8
|
+
const reEasing = ReEasing.bezier(...bezier_points[props.easing || 'linear']);
|
9
|
+
const reSpringConfig = {
|
10
|
+
mass: 1,
|
11
|
+
damping: 20,
|
12
|
+
stiffness: 170,
|
13
|
+
overshootClamping: false,
|
14
|
+
restDisplacementThreshold: 0.01,
|
15
|
+
restSpeedThreshold: 0.01,
|
16
|
+
reduceMotion: ReduceMotion.System
|
17
|
+
};
|
18
|
+
const easing = bezier_points[props.easing || 'linear'];
|
19
|
+
const springConfig = {
|
20
|
+
mass: 1,
|
21
|
+
damping: 20,
|
22
|
+
stiffness: 170,
|
23
|
+
useNativeDriver: true,
|
24
|
+
...(typeof props.spring === 'object' ? props.spring : {})
|
25
|
+
};
|
26
|
+
const timedConfig = {
|
27
|
+
duration: props.animationDuration,
|
28
|
+
useNativeDriver: true,
|
29
|
+
easing: Easing.bezier(...easing)
|
30
|
+
};
|
31
|
+
const width = useSharedValue(props.slot[0].length === 1 ? -1 : 0);
|
32
|
+
const yA = useRef(new Animated.Value(0)).current;
|
33
|
+
const yB = useRef(new Animated.Value(0)).current;
|
12
34
|
const commaScale = useSharedValue(0);
|
13
35
|
const commaWidth = useSharedValue(0);
|
36
|
+
const [valA, setValA] = useState(props.slot[0][0]);
|
37
|
+
const [valB, setValB] = useState();
|
14
38
|
// React to slot value changes
|
15
39
|
useLayoutEffect(() => {
|
16
|
-
if (!props.slot)
|
40
|
+
if (!props.slot)
|
17
41
|
return;
|
18
|
-
|
19
|
-
const
|
20
|
-
const
|
42
|
+
const incomingValue = props.slot[0][1];
|
43
|
+
const currentValue = props.slot[0][0];
|
44
|
+
const shown = currentValue === valA ? 'A' : 'B';
|
21
45
|
if (incomingValue === undefined) {
|
22
|
-
|
23
|
-
|
24
|
-
|
46
|
+
if (props.charSizes[0])
|
47
|
+
width.value = props.charSizes[currentValue];
|
48
|
+
return;
|
25
49
|
}
|
50
|
+
;
|
26
51
|
// Removing slot
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
52
|
+
if (incomingValue === null) {
|
53
|
+
width.value = props.spring
|
54
|
+
? withSpring(0, { ...reSpringConfig, damping: reSpringConfig.damping + 12 })
|
55
|
+
: withTiming(0, { duration: props.animationDuration / 2, easing: reEasing });
|
56
|
+
const animation = props.spring
|
57
|
+
? Animated.spring(shown === 'A' ? yA : yB, { toValue: -1 * props.height, ...springConfig })
|
58
|
+
: Animated.timing(shown === 'A' ? yA : yB, { toValue: -1 * props.height, ...timedConfig });
|
59
|
+
animation.start(() => props.onCompleted && props.onCompleted());
|
32
60
|
}
|
33
61
|
// Adding slot
|
34
62
|
else if (currentValue === null) {
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
63
|
+
setValA(incomingValue);
|
64
|
+
width.value = props.spring
|
65
|
+
? withSpring(props.charSizes[incomingValue], reSpringConfig)
|
66
|
+
: withTiming(props.charSizes[incomingValue], { duration: props.animationDuration / 2, easing: reEasing });
|
67
|
+
yB.setValue(-1 * props.height); // Set outside of view
|
68
|
+
const sequence = [
|
69
|
+
Animated.timing(yA, { toValue: -1 * props.height, duration: 0, useNativeDriver: true }),
|
70
|
+
props.spring
|
71
|
+
? Animated.spring(yA, { toValue: 0, ...springConfig })
|
72
|
+
: Animated.timing(yA, { toValue: 0, ...timedConfig })
|
73
|
+
];
|
74
|
+
Animated.sequence(sequence).start(() => { props.onCompleted && props.onCompleted(); });
|
39
75
|
}
|
40
76
|
// Animating slot
|
41
|
-
else {
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
else if (incomingValue < currentValue) {
|
50
|
-
y.value = withTiming(-1 * props.height, { duration: props.animationDuration, easing });
|
51
|
-
incomingY.value = withSequence(withTiming(1 * props.height, { duration: 0, easing }), withTiming(0, { duration: props.animationDuration }, () => {
|
52
|
-
props.onCompleted && runOnJS(props.onCompleted)();
|
53
|
-
}));
|
54
|
-
}
|
77
|
+
else if (Number.isFinite(incomingValue) && Number.isFinite(currentValue)) {
|
78
|
+
width.value = props.spring
|
79
|
+
? withSpring(props.charSizes[incomingValue], { ...reSpringConfig, damping: reSpringConfig.damping + 12 })
|
80
|
+
: withTiming(props.charSizes[incomingValue], { duration: props.animationDuration / 2 });
|
81
|
+
const finalY = incomingValue > currentValue ? props.height : -1 * props.height;
|
82
|
+
Animated.timing(shown === 'A' ? yB : yA, { toValue: finalY, ...timedConfig, duration: 0 }).start(() => {
|
83
|
+
shown === 'A' ? setValB(incomingValue) : setValA(incomingValue);
|
84
|
+
});
|
55
85
|
}
|
56
|
-
}, [props.slot]);
|
86
|
+
}, [props.slot, props.charSizes]);
|
87
|
+
useEffect(() => {
|
88
|
+
if (typeof valA === 'number' && typeof valB === 'number') {
|
89
|
+
const shown = props.slot[0][0] === valA ? 'A' : 'B';
|
90
|
+
const shownValFinalY = valA > valB
|
91
|
+
? shown === 'A' ? props.height * -1 : props.height
|
92
|
+
: shown === 'A' ? props.height : props.height * -1;
|
93
|
+
const outAnimation = props.spring
|
94
|
+
? Animated.spring(shown === 'A' ? yA : yB, { toValue: shownValFinalY, ...springConfig })
|
95
|
+
: Animated.timing(shown === 'A' ? yA : yB, { toValue: shownValFinalY, ...timedConfig });
|
96
|
+
outAnimation.start();
|
97
|
+
const sequence = [
|
98
|
+
Animated.timing(shown === 'A' ? yB : yA, { toValue: shownValFinalY * -1, duration: 0, useNativeDriver: true }),
|
99
|
+
props.spring
|
100
|
+
? Animated.spring(shown === 'A' ? yB : yA, { toValue: 0, ...springConfig })
|
101
|
+
: Animated.timing(shown === 'A' ? yB : yA, { toValue: 0, ...timedConfig })
|
102
|
+
];
|
103
|
+
Animated.sequence(sequence).start(() => {
|
104
|
+
shown === 'A' ? setValA(undefined) : setValB(undefined);
|
105
|
+
props.onCompleted && props.onCompleted();
|
106
|
+
});
|
107
|
+
}
|
108
|
+
}, [valA, valB]);
|
57
109
|
useLayoutEffect(() => {
|
58
110
|
if (props.commaPositions?.[props.index] === 1) {
|
59
|
-
commaScale.value =
|
60
|
-
|
111
|
+
commaScale.value = props.spring
|
112
|
+
? withSpring(1, reSpringConfig)
|
113
|
+
: withTiming(1, { duration: props.animationDuration, easing: reEasing });
|
114
|
+
commaWidth.value = withSequence(withTiming(0, { duration: 0 }), props.spring
|
115
|
+
? withSpring(props.commaWidth, reSpringConfig)
|
116
|
+
: withTiming(props.commaWidth, { duration: props.animationDuration, easing: reEasing }));
|
61
117
|
}
|
62
118
|
else if (props.commaPositions?.[props.index] === -1) {
|
63
|
-
commaScale.value = withSequence(withTiming(1, { duration: 0 }),
|
119
|
+
commaScale.value = withSequence(withTiming(1, { duration: 0 }), props.spring
|
120
|
+
? withSpring(0, reSpringConfig)
|
121
|
+
: withTiming(0, { duration: props.animationDuration, easing: reEasing }));
|
64
122
|
const timeout = setTimeout(() => {
|
65
|
-
commaWidth.value =
|
123
|
+
commaWidth.value = props.spring
|
124
|
+
? withSpring(0, { ...reSpringConfig, damping: reSpringConfig.damping + 12 })
|
125
|
+
: withTiming(0, { duration: props.animationDuration, easing: reEasing });
|
66
126
|
}, 0);
|
67
127
|
return () => clearTimeout(timeout);
|
68
128
|
}
|
@@ -79,32 +139,42 @@ const Slot = (props) => {
|
|
79
139
|
transform: [{ scale: commaScale.value }],
|
80
140
|
width: commaWidth.value
|
81
141
|
}));
|
82
|
-
const
|
83
|
-
|
84
|
-
{ translateY: y.value },
|
85
|
-
{ scale: interpolate(y.value, [-1 * props.height, 0, props.height], [.25, 1, .25]) }
|
86
|
-
],
|
87
|
-
marginRight: margin.value,
|
88
|
-
opacity: interpolate(y.value, [-1 * props.height / 2, 0, 1 * props.height / 2], [0, 1, 0]),
|
89
|
-
}));
|
90
|
-
const incomingStyle = useAnimatedStyle(() => {
|
91
|
-
return ({
|
92
|
-
transform: [
|
93
|
-
{ translateY: incomingY.value },
|
94
|
-
{ scale: interpolate(incomingY.value, [-1 * props.height, 0, props.height], [.25, 1, .25]) },
|
95
|
-
],
|
96
|
-
opacity: interpolate(incomingY.value, [-1 * props.height / 2, 0, 1 * props.height / 2], [0, 1, 0]),
|
97
|
-
position: 'absolute'
|
98
|
-
});
|
99
|
-
});
|
100
|
-
const incomingTextStyle = useAnimatedStyle(() => ({
|
101
|
-
transform: [{ rotateX: `${interpolate(incomingY.value, [-1 * props.height / 2, 0, props.height / 2], [45, 0, -45])}deg` }]
|
102
|
-
}));
|
103
|
-
const currentTextStyle = useAnimatedStyle(() => ({
|
104
|
-
transform: [{ rotateX: `${interpolate(y.value, [-1 * props.height / 2, 0, props.height / 2], [45, 0, -45])}deg` }]
|
142
|
+
const widthAnimation = useAnimatedStyle(() => ({
|
143
|
+
width: width.value,
|
105
144
|
}));
|
106
|
-
return (_jsxs(View, { style: styles.slotContainer, children: [_jsx(Animated.View, { style:
|
107
|
-
|
108
|
-
|
145
|
+
return (_jsxs(View, { style: styles.slotContainer, children: [_jsx(Animated.View, { style: {
|
146
|
+
transform: [
|
147
|
+
{ translateY: yA },
|
148
|
+
{ scale: yA.interpolate({ inputRange: [-1 * props.height, 0, props.height], outputRange: [.25, 1, .25] }) }
|
149
|
+
],
|
150
|
+
opacity: yA.interpolate({ inputRange: [-1 * props.height / 2, 0, 1 * props.height / 2], outputRange: [0, 1, 0] })
|
151
|
+
}, children: _jsx(Reanimated.View, { style: widthAnimation, children: _jsx(Animated.Text, { style: [
|
152
|
+
props.fontStyle,
|
153
|
+
{
|
154
|
+
transform: [{
|
155
|
+
rotateX: `${yA.interpolate({
|
156
|
+
inputRange: [-1 * props.height / 2, 0, props.height / 2],
|
157
|
+
outputRange: [45, 0, -45]
|
158
|
+
})}deg`
|
159
|
+
}]
|
160
|
+
}
|
161
|
+
], children: valA }) }) }), _jsx(Animated.View, { style: {
|
162
|
+
position: 'absolute',
|
163
|
+
transform: [
|
164
|
+
{ translateY: yB },
|
165
|
+
{ scale: yB.interpolate({ inputRange: [-1 * props.height, 0, props.height], outputRange: [.25, 1, .25] }) }
|
166
|
+
],
|
167
|
+
opacity: yB.interpolate({ inputRange: [-1 * props.height / 2, 0, 1 * props.height / 2], outputRange: [0, 1, 0] })
|
168
|
+
}, children: _jsx(Animated.Text, { style: [
|
169
|
+
props.fontStyle,
|
170
|
+
{
|
171
|
+
transform: [{
|
172
|
+
rotateX: `${yB.interpolate({
|
173
|
+
inputRange: [-1 * props.height / 2, 0, props.height / 2],
|
174
|
+
outputRange: [45, 0, -45]
|
175
|
+
})}deg`
|
176
|
+
}]
|
177
|
+
}
|
178
|
+
], children: valB }) }), _jsx(Reanimated.View, { style: commaStyle, children: _jsx(Text, { style: props.fontStyle, children: "," }) })] }));
|
109
179
|
};
|
110
180
|
export default Slot;
|
package/Slots.js
CHANGED
@@ -1,22 +1,17 @@
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
2
|
-
import React, { useState, useLayoutEffect, useCallback, Fragment, useRef } from 'react';
|
2
|
+
import React, { useState, useLayoutEffect, useCallback, Fragment, useRef, useEffect } from 'react';
|
3
3
|
import { View, Text } from 'react-native';
|
4
4
|
import styles from './styles';
|
5
|
-
import {
|
5
|
+
import { getNewSlotValues, getNewCommaPositions } from './helpers';
|
6
6
|
import Slot from './Slot';
|
7
|
-
const DEFAULT_DURATION =
|
7
|
+
const DEFAULT_DURATION = 500;
|
8
8
|
const Slots = (props) => {
|
9
9
|
const idRef = useRef(`slots-${Math.random().toString(36).substring(7)}`);
|
10
|
-
// [number] : static number
|
11
|
-
/// [number, null]: removing slot
|
12
|
-
// [null, number]: adding slot
|
13
|
-
// [number, number]: animating slot
|
14
10
|
const [slots, setSlots] = useState([]);
|
15
11
|
const [commaPositions, setCommaPositions] = useState([]);
|
16
12
|
const [commaWidth, setCommaWidth] = useState(0);
|
17
13
|
const [periodWidth, setPeriodWidth] = useState(0);
|
18
14
|
const [sizesMeasured, setSizesMeasured] = useState(false);
|
19
|
-
const [keys, setKeys] = useState(Array.from({ length: props.value.toString().replace('.', '').length }).map((_, i) => Math.random().toString(36).substring(2, 9)));
|
20
15
|
const [slotHeight, setSlotHeight] = useState(0);
|
21
16
|
const [charSizes, setCharSizes] = useState(Array.from({ length: 10 }).map((_, i) => 0));
|
22
17
|
useLayoutEffect(() => {
|
@@ -28,39 +23,36 @@ const Slots = (props) => {
|
|
28
23
|
const newCommaPositions = getNewCommaPositions(stringValue, commaPositions, parseFromLeft, props.precision);
|
29
24
|
setCommaPositions(newCommaPositions);
|
30
25
|
}
|
31
|
-
const
|
32
|
-
|
33
|
-
|
34
|
-
?
|
35
|
-
:
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
26
|
+
const newSlotValues = getNewSlotValues(stringValue, slots, parseFromLeft);
|
27
|
+
setSlots(prev => newSlotValues.map((v, i) => {
|
28
|
+
const shift = parseFromLeft
|
29
|
+
? 0
|
30
|
+
: Math.max(newSlotValues.length - prev.length, 0);
|
31
|
+
// When adding slots, they will need a new key, otherwise
|
32
|
+
// mantain the same slot key. Also if this is the first run through
|
33
|
+
// and the slots are empty, the keys will need to be set.
|
34
|
+
const key = v[0] === null || prev.length === 0
|
35
|
+
? Math.random().toString(36).slice(0, 9)
|
36
|
+
: prev[i - shift][1] || Math.random().toString(36).slice(0, 9);
|
37
|
+
return [v, key];
|
38
|
+
}));
|
42
39
|
}, [props.value]);
|
43
40
|
const onCompleted = useCallback(() => {
|
44
41
|
const cleanedSlots = slots
|
45
|
-
.filter(s => (Number.isFinite(s[0]) && s[1] !== null) || Number.isFinite(s[1]))
|
46
|
-
.map(s => Number.isFinite(s[1]) ? [s[1]] : [s[0]]);
|
42
|
+
.filter(s => (Number.isFinite(s[0][0]) && s[0][1] !== null) || Number.isFinite(s[0][1]))
|
43
|
+
.map(s => Number.isFinite(s[0][1]) ? [[s[0][1]], s[1]] : [[s[0][0]], s[1]]);
|
47
44
|
const numberOfSlotsRemoved = Math.max(slots.length - cleanedSlots.length, 0);
|
48
45
|
const cleanedCommas = commaPositions
|
49
46
|
.map(c => c === -1 ? null : c === 1 ? 0 : c)
|
50
|
-
.slice(slots[0]?.[1] === null ? numberOfSlotsRemoved : 0) // Trim from left
|
51
|
-
.slice(0, slots[slots.length - 1]?.[1] === null ? -1 * numberOfSlotsRemoved : undefined); // Trim from right
|
52
|
-
|
53
|
-
|
54
|
-
setCommaPositions(cleanedCommas);
|
55
|
-
}, 200);
|
47
|
+
.slice(slots[0]?.[0][1] === null ? numberOfSlotsRemoved : 0) // Trim from left
|
48
|
+
.slice(0, slots[slots.length - 1]?.[0][1] === null ? -1 * numberOfSlotsRemoved : undefined); // Trim from right
|
49
|
+
setSlots(cleanedSlots);
|
50
|
+
setCommaPositions(cleanedCommas);
|
56
51
|
}, [slots, commaPositions]);
|
57
52
|
return (_jsxs(_Fragment, { children: [_jsxs(View, { style: styles.slotsContainer, children: [props.prefix && (_jsx(Text, { style: props.fontStyle, children: props.prefix })), _jsx(Text, { style: [styles.spacer, props.fontStyle], children: "1" }), slots.map((slot, i) => {
|
58
|
-
const callback = i === slots.findIndex(s => s.length > 1) ? onCompleted : undefined;
|
59
|
-
|
60
|
-
|
61
|
-
: DEFAULT_DURATION;
|
62
|
-
return (_jsxs(Fragment, { children: [_jsx(Slot, { slot: slot, index: i, height: slotHeight, charSizes: charSizes, commaWidth: commaWidth, periodWidth: periodWidth, commaPositions: props.includeComma && commaPositions.length ? commaPositions : undefined, onCompleted: callback, animationDuration: animationDuration, fontStyle: props.fontStyle }), (props.precision || 0) > 0 && props.precision === (slots.length - 1 - i) &&
|
63
|
-
_jsx(Text, { style: props.fontStyle, children: "." })] }, `${idRef.current}-${keys[i]}`));
|
53
|
+
const callback = i === slots.findIndex(s => s[0].length > 1) ? onCompleted : undefined;
|
54
|
+
return (_jsxs(Fragment, { children: [_jsx(Slot, { slot: slot, index: i, height: slotHeight, charSizes: charSizes, commaWidth: commaWidth, periodWidth: periodWidth, onCompleted: callback, easing: props.easing, fontStyle: props.fontStyle, animationDuration: props.animationDuration || DEFAULT_DURATION, spring: props.spring, commaPositions: props.includeComma && commaPositions.length ? commaPositions : undefined }), (props.precision || 0) > 0 && props.precision === (slots.length - 1 - i) &&
|
55
|
+
_jsx(Text, { style: props.fontStyle, children: "." })] }, `${idRef.current}-${slot[1]}`));
|
64
56
|
})] }), !sizesMeasured &&
|
65
57
|
Array.from({ length: 10 }, (_, i) => i)
|
66
58
|
.concat([',', '.'])
|
package/constants.d.ts
ADDED
package/constants.js
ADDED
package/helpers.d.ts
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
import type { SlotValue, CommaPosition } from "./types";
|
2
|
-
export declare const
|
2
|
+
export declare const getNewSlotValues: (newValue: string, currentSlots: SlotValue[], parseFromLeft: boolean) => ([number] | [number | null, number | null])[];
|
3
3
|
export declare const getNewCommaPositions: (newValue: string, currentCommaPositions: CommaPosition[], parseFromLeft: boolean, precision?: number) => CommaPosition[];
|
package/helpers.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
export const
|
1
|
+
export const getNewSlotValues = (newValue, currentSlots, parseFromLeft) => {
|
2
2
|
const iterLength = Math.max(newValue.length, currentSlots.length);
|
3
3
|
const shiftCurrent = parseFromLeft
|
4
4
|
? 0
|
@@ -6,35 +6,35 @@ export const getNewSlots = (newValue, currentSlots, parseFromLeft) => {
|
|
6
6
|
const shiftIncoming = parseFromLeft
|
7
7
|
? 0
|
8
8
|
: currentSlots.length > newValue.length ? newValue.length - currentSlots.length : 0;
|
9
|
-
const
|
9
|
+
const newSlotValues = Array.from({ length: iterLength }, (_, i) => [i]);
|
10
10
|
Array.from({ length: iterLength }, (_, i) => parseFromLeft ? i : iterLength - i - 1).forEach(i => {
|
11
11
|
const newValueParsed = parseInt(`${newValue}`[i + shiftIncoming]);
|
12
|
-
const currentSlotValue = currentSlots[i + shiftCurrent]?.[0];
|
12
|
+
const currentSlotValue = currentSlots[i + shiftCurrent]?.[0][0];
|
13
13
|
// First pass through, just set the slots since shared slots is empty
|
14
14
|
if (currentSlots.length === 0) {
|
15
|
-
return
|
15
|
+
return newSlotValues[i] = [newValueParsed];
|
16
16
|
}
|
17
17
|
// Removing slot
|
18
18
|
if (isNaN(newValueParsed)) {
|
19
|
-
|
19
|
+
newSlotValues[i] = [currentSlotValue, null];
|
20
20
|
}
|
21
21
|
// Adding slot
|
22
22
|
else if (currentSlotValue === undefined || currentSlotValue === null) {
|
23
|
-
|
23
|
+
newSlotValues[i] = [null, newValueParsed];
|
24
24
|
}
|
25
25
|
// Animating slot
|
26
26
|
else if (currentSlotValue !== newValueParsed) {
|
27
|
-
|
27
|
+
newSlotValues[i] = [currentSlotValue, newValueParsed];
|
28
28
|
}
|
29
29
|
// Static slot
|
30
30
|
else {
|
31
|
-
|
31
|
+
newSlotValues[i] = [newValueParsed];
|
32
32
|
}
|
33
33
|
});
|
34
|
-
return
|
34
|
+
return newSlotValues;
|
35
35
|
};
|
36
36
|
// Takes in a new value and spits out an array of comma positions
|
37
|
-
// for all of the commas being added, removed, or staying the
|
37
|
+
// for all of the commas being added, removed, or staying the same
|
38
38
|
export const getNewCommaPositions = (newValue, currentCommaPositions, parseFromLeft, precision) => {
|
39
39
|
const parseLength = Math.max(newValue.length, currentCommaPositions.length);
|
40
40
|
// On first render, no size change
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
@@ -1,10 +1,17 @@
|
|
1
1
|
import type { StyleProp, TextStyle } from 'react-native';
|
2
2
|
import type { SharedValue } from 'react-native-reanimated';
|
3
3
|
export type Position = -1 | 0 | 1;
|
4
|
-
export type SlotValue = [number] | [number | null, number | null];
|
4
|
+
export type SlotValue = [[number] | [number | null, number | null], string];
|
5
|
+
type EasingT = 'linear' | 'in-out' | 'out';
|
6
|
+
type Spring = {
|
7
|
+
damping: number;
|
8
|
+
mass: number;
|
9
|
+
stiffness: number;
|
10
|
+
} | true;
|
5
11
|
export interface SlotProps {
|
6
12
|
slot: SlotValue;
|
7
13
|
index: number;
|
14
|
+
easing?: EasingT;
|
8
15
|
animationDuration: number;
|
9
16
|
fontStyle?: StyleProp<TextStyle>;
|
10
17
|
commaWidth: number;
|
@@ -13,10 +20,13 @@ export interface SlotProps {
|
|
13
20
|
commaPositions?: CommaPosition[];
|
14
21
|
charSizes: number[];
|
15
22
|
height: number;
|
23
|
+
spring?: Spring;
|
16
24
|
}
|
17
|
-
type Shared = 'fontStyle' | 'index' | 'charSizes' | 'animationDuration';
|
25
|
+
type Shared = 'fontStyle' | 'index' | 'charSizes' | 'animationDuration' | 'spring';
|
18
26
|
export interface ContinuousSlotProps extends Pick<SlotProps, Shared> {
|
19
27
|
slot: [number | string, string];
|
28
|
+
spring?: Spring;
|
29
|
+
easing?: EasingT;
|
20
30
|
}
|
21
31
|
export type CommaPosition = 1 | -1 | 0 | null;
|
22
32
|
export interface CommaProps {
|
@@ -28,13 +38,21 @@ export interface CommaProps {
|
|
28
38
|
onExited: () => void;
|
29
39
|
onEntered: () => void;
|
30
40
|
}
|
31
|
-
|
41
|
+
interface AnimatedNumbersPropsBase {
|
32
42
|
value: number;
|
33
43
|
fontStyle?: StyleProp<TextStyle>;
|
34
44
|
prefix?: string;
|
35
45
|
includeComma?: boolean;
|
36
46
|
precision?: number;
|
37
|
-
animationDuration?: number;
|
38
47
|
animateIntermediateValues?: true;
|
39
48
|
}
|
49
|
+
export type AnimatedNumbersProps = ({
|
50
|
+
easing?: EasingT;
|
51
|
+
animationDuration?: number;
|
52
|
+
spring?: never;
|
53
|
+
} & AnimatedNumbersPropsBase) | ({
|
54
|
+
easing?: never;
|
55
|
+
animationDuration?: never;
|
56
|
+
spring?: Spring;
|
57
|
+
} & AnimatedNumbersPropsBase);
|
40
58
|
export {};
|