react-native-slot-text 3.1.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.
@@ -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|string} props.value - The value to animate to. Can be string of numbers.
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=400] - The duration of the animation in milliseconds. Defaults to 400ms.
12
- * Only supported when animateIntermediateValues is false
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. e.g. value prop of 42069 with precision of 2 would become 420.69
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
  */
@@ -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|string} props.value - The value to animate to. Can be string of numbers.
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=400] - The duration of the animation in milliseconds. Defaults to 400ms.
15
- * Only supported when animateIntermediateValues is false
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. e.g. value prop of 42069 with precision of 2 would become 420.69
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,7 +3,7 @@ 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, Easing as ReEasing } 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
9
  import { bezier_points } from './constants';
@@ -12,6 +12,23 @@ const PADDING_FRACTION = .25;
12
12
  const AnimatedMaskView = Animated.createAnimatedComponent(MaskedView);
13
13
  const ContinuousSlot = (props) => {
14
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
+ };
15
32
  const config = {
16
33
  useNativeDriver: true,
17
34
  duration: props.animationDuration,
@@ -37,30 +54,56 @@ const ContinuousSlot = (props) => {
37
54
  : props.slot[0] === ',' ? 10 : 11];
38
55
  width.value = newWidth > width.value
39
56
  ? newWidth // Growing
40
- : withTiming(newWidth, { duration: props.animationDuration });
57
+ : props.spring
58
+ ? withSpring(newWidth, reSpringConfig)
59
+ : withTiming(newWidth, { duration: props.animationDuration });
41
60
  if (typeof props.slot[0] === 'number') {
42
- Animated.timing(slotOpacity, { toValue: 1, ...config }).start();
43
- Animated.timing(y, {
44
- toValue: -1 * (((GAP + measuredHeight.current) * (9 - props.slot[0])) -
45
- (PADDING_FRACTION * measuredHeight.current)),
46
- ...config
47
- }).start();
48
- Animated.timing(commaY, { toValue: 0, ...config }).start();
49
- Animated.timing(periodY, { toValue: 0, ...config }).start();
50
- Animated.timing(commaOpacity, { toValue: 0, ...config }).start();
51
- Animated.timing(periodOpacity, { toValue: 0, ...config }).start();
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
+ }
52
79
  }
53
80
  else if (props.slot[0] === '.') {
54
- Animated.timing(periodY, { toValue: (PADDING_FRACTION * measuredHeight.current), ...config }).start();
55
- Animated.timing(periodOpacity, { toValue: 1, ...config }).start();
56
- Animated.timing(slotOpacity, { toValue: 0, ...config }).start();
57
- Animated.timing(commaY, { toValue: 0, ...config }).start();
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
+ }
58
93
  }
59
94
  else if (props.slot[0] === ',') {
60
- Animated.timing(commaY, { toValue: (PADDING_FRACTION * measuredHeight.current), ...config }).start();
61
- Animated.timing(commaOpacity, { toValue: 1, ...config }).start();
62
- Animated.timing(periodY, { toValue: 1, ...config }).start();
63
- Animated.timing(slotOpacity, { toValue: 0, ...config }).start();
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
+ }
64
107
  }
65
108
  }, [props.slot]);
66
109
  // LINEAR GRADIENT
@@ -72,9 +115,15 @@ const ContinuousSlot = (props) => {
72
115
  1: { color: 'transparent' }
73
116
  },
74
117
  });
75
- return (_jsx(ReAnimated.View, { layout: LinearTransition.duration(props.animationDuration).easing(ReEasing.bezier(...easing).factory()), entering: props.charSizes[0]
76
- ? FadeInUp.duration(props.animationDuration / 1.5).easing(ReEasing.bezier(...easing).factory())
77
- : undefined, exiting: FadeOutDown.duration(props.animationDuration / 1.5).easing(ReEasing.bezier(...easing).factory()), children: isMeasured
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
78
127
  ?
79
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: {
80
129
  position: 'absolute',
@@ -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, easing: props.easing, 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,69 +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 { Text } from 'react-native';
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 easing = Easing.bezier(...bezier_points[props.easing || 'linear']);
10
- const margin = useSharedValue(0);
11
- const y = useSharedValue(0);
12
- const incomingY = useSharedValue(0);
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;
13
34
  const commaScale = useSharedValue(0);
14
35
  const commaWidth = useSharedValue(0);
36
+ const [valA, setValA] = useState(props.slot[0][0]);
37
+ const [valB, setValB] = useState();
15
38
  // React to slot value changes
16
39
  useLayoutEffect(() => {
17
- if (!props.slot) {
40
+ if (!props.slot)
18
41
  return;
19
- }
20
- const incomingValue = props.slot[1];
21
- const currentValue = props.slot[0];
42
+ const incomingValue = props.slot[0][1];
43
+ const currentValue = props.slot[0][0];
44
+ const shown = currentValue === valA ? 'A' : 'B';
22
45
  if (incomingValue === undefined) {
23
- y.value = 0;
24
- incomingY.value = props.height;
25
- margin.value = 0;
46
+ if (props.charSizes[0])
47
+ width.value = props.charSizes[currentValue];
48
+ return;
26
49
  }
50
+ ;
27
51
  // Removing slot
28
- else if (incomingValue === null) {
29
- y.value = withTiming(-1 * props.height, { duration: props.animationDuration, easing }, () => {
30
- props.onCompleted && runOnJS(props.onCompleted)();
31
- });
32
- margin.value = withTiming(-1 * props.charSizes[currentValue], { duration: props.animationDuration / 2, easing });
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());
33
60
  }
34
61
  // Adding slot
35
62
  else if (currentValue === null) {
36
- margin.value = withTiming(props.charSizes[incomingValue], { duration: props.animationDuration / 2, easing });
37
- incomingY.value = withSequence(withTiming(-1 * props.height, { duration: 0, easing }), withTiming(0, { duration: props.animationDuration }, () => {
38
- props.onCompleted && runOnJS(props.onCompleted)();
39
- }));
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(); });
40
75
  }
41
76
  // Animating slot
42
- else {
43
- margin.value = withTiming((props.charSizes[incomingValue] - props.charSizes[currentValue]), { duration: props.animationDuration / 2 });
44
- if (incomingValue > currentValue) {
45
- y.value = withTiming(props.height, { duration: props.animationDuration, easing });
46
- incomingY.value = withSequence(withTiming(-1 * props.height, { duration: 0 }), withTiming(0, { duration: props.animationDuration }, () => {
47
- props.onCompleted && runOnJS(props.onCompleted)();
48
- }));
49
- }
50
- else if (incomingValue < currentValue) {
51
- y.value = withTiming(-1 * props.height, { duration: props.animationDuration, easing });
52
- incomingY.value = withSequence(withTiming(1 * props.height, { duration: 0 }), withTiming(0, { duration: props.animationDuration, easing }, () => {
53
- props.onCompleted && runOnJS(props.onCompleted)();
54
- }));
55
- }
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
+ });
56
85
  }
57
- }, [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]);
58
109
  useLayoutEffect(() => {
59
110
  if (props.commaPositions?.[props.index] === 1) {
60
- commaScale.value = withTiming(1, { duration: props.animationDuration, easing });
61
- commaWidth.value = withSequence(withTiming(0, { duration: 0 }), withTiming(props.commaWidth, { duration: props.animationDuration, easing }));
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 }));
62
117
  }
63
118
  else if (props.commaPositions?.[props.index] === -1) {
64
- commaScale.value = withSequence(withTiming(1, { duration: 0 }), withTiming(0, { duration: props.animationDuration, easing }));
119
+ commaScale.value = withSequence(withTiming(1, { duration: 0 }), props.spring
120
+ ? withSpring(0, reSpringConfig)
121
+ : withTiming(0, { duration: props.animationDuration, easing: reEasing }));
65
122
  const timeout = setTimeout(() => {
66
- commaWidth.value = withTiming(0, { duration: props.animationDuration, easing });
123
+ commaWidth.value = props.spring
124
+ ? withSpring(0, { ...reSpringConfig, damping: reSpringConfig.damping + 12 })
125
+ : withTiming(0, { duration: props.animationDuration, easing: reEasing });
67
126
  }, 0);
68
127
  return () => clearTimeout(timeout);
69
128
  }
@@ -80,32 +139,42 @@ const Slot = (props) => {
80
139
  transform: [{ scale: commaScale.value }],
81
140
  width: commaWidth.value
82
141
  }));
83
- const currentStyle = useAnimatedStyle(() => ({
84
- transform: [
85
- { translateY: y.value },
86
- { scale: interpolate(y.value, [-1 * props.height, 0, props.height], [.25, 1, .25]) }
87
- ],
88
- marginRight: margin.value,
89
- opacity: interpolate(y.value, [-1 * props.height / 2, 0, 1 * props.height / 2], [0, 1, 0]),
90
- }));
91
- const incomingStyle = useAnimatedStyle(() => {
92
- return ({
93
- transform: [
94
- { translateY: incomingY.value },
95
- { scale: interpolate(incomingY.value, [-1 * props.height, 0, props.height], [.25, 1, .25]) },
96
- ],
97
- opacity: interpolate(incomingY.value, [-1 * props.height / 2, 0, 1 * props.height / 2], [0, 1, 0]),
98
- position: 'absolute'
99
- });
100
- });
101
- const incomingTextStyle = useAnimatedStyle(() => ({
102
- transform: [{ rotateX: `${interpolate(incomingY.value, [-1 * props.height / 2, 0, props.height / 2], [45, 0, -45])}deg` }]
103
- }));
104
- const currentTextStyle = useAnimatedStyle(() => ({
105
- 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,
106
144
  }));
107
- return (_jsxs(View, { style: styles.slotContainer, children: [_jsx(Animated.View, { style: currentStyle, children: Number.isFinite(props.slot[0]) &&
108
- _jsx(Animated.Text, { style: [props.fontStyle, currentTextStyle], children: props.slot[0] }) }), _jsx(Animated.View, { style: incomingStyle, children: Number.isFinite(props.slot[1]) &&
109
- _jsx(Animated.Text, { style: [props.fontStyle, incomingTextStyle], children: props.slot[1] }) }), _jsx(Text, { style: styles.hiddenSlot, children: "1" }), _jsx(Animated.View, { style: commaStyle, children: _jsx(Text, { style: props.fontStyle, children: "," }) })] }));
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: "," }) })] }));
110
179
  };
111
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 { getNewSlots, getNewCommaPositions } from './helpers';
5
+ import { getNewSlotValues, getNewCommaPositions } from './helpers';
6
6
  import Slot from './Slot';
7
- const DEFAULT_DURATION = 400;
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 newSlots = getNewSlots(stringValue, slots, parseFromLeft);
32
- setKeys(prev => prev.length > newSlots.length
33
- ? newSlots[0]?.[1] === null
34
- ? prev.slice(-1 * prev.length - newSlots.length)
35
- : prev.slice(0, newSlots.length)
36
- : prev.length < newSlots.length
37
- ? newSlots[0]?.[0] === null
38
- ? Array.from({ length: newSlots.length - prev.length }).map((_, i) => Math.random().toString(36).substring(2, 9)).concat(prev)
39
- : prev.concat(Array.from({ length: newSlots.length - prev.length }).map((_, i) => Math.random().toString(36).substring(2, 9)))
40
- : prev);
41
- setSlots(newSlots);
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
- setTimeout(() => {
53
- setSlots(cleanedSlots);
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
- const animationDuration = "animationDuration" in props
60
- ? props.animationDuration || DEFAULT_DURATION
61
- : DEFAULT_DURATION;
62
- return (_jsxs(Fragment, { children: [_jsx(Slot, { slot: slot, easing: props.easing, 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/helpers.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { SlotValue, CommaPosition } from "./types";
2
- export declare const getNewSlots: (newValue: string, currentSlots: SlotValue[], parseFromLeft: boolean) => SlotValue[];
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 getNewSlots = (newValue, currentSlots, parseFromLeft) => {
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 newSlots = Array.from({ length: iterLength }, (_, i) => [i]);
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 newSlots[i] = [newValueParsed];
15
+ return newSlotValues[i] = [newValueParsed];
16
16
  }
17
17
  // Removing slot
18
18
  if (isNaN(newValueParsed)) {
19
- newSlots[i] = [currentSlotValue, null];
19
+ newSlotValues[i] = [currentSlotValue, null];
20
20
  }
21
21
  // Adding slot
22
22
  else if (currentSlotValue === undefined || currentSlotValue === null) {
23
- newSlots[i] = [null, newValueParsed];
23
+ newSlotValues[i] = [null, newValueParsed];
24
24
  }
25
25
  // Animating slot
26
26
  else if (currentSlotValue !== newValueParsed) {
27
- newSlots[i] = [currentSlotValue, newValueParsed];
27
+ newSlotValues[i] = [currentSlotValue, newValueParsed];
28
28
  }
29
29
  // Static slot
30
30
  else {
31
- newSlots[i] = [newValueParsed];
31
+ newSlotValues[i] = [newValueParsed];
32
32
  }
33
33
  });
34
- return newSlots;
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 sameM
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-slot-text",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "author": "sc-mitton <scm.mitton@gmail.com> (https://github.com/sc-mitton)",
5
5
  "main": "index.js",
6
6
  "module": "index.ts",
package/types.d.ts CHANGED
@@ -1,8 +1,13 @@
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
5
  type EasingT = 'linear' | 'in-out' | 'out';
6
+ type Spring = {
7
+ damping: number;
8
+ mass: number;
9
+ stiffness: number;
10
+ } | true;
6
11
  export interface SlotProps {
7
12
  slot: SlotValue;
8
13
  index: number;
@@ -15,10 +20,12 @@ export interface SlotProps {
15
20
  commaPositions?: CommaPosition[];
16
21
  charSizes: number[];
17
22
  height: number;
23
+ spring?: Spring;
18
24
  }
19
- type Shared = 'fontStyle' | 'index' | 'charSizes' | 'animationDuration';
25
+ type Shared = 'fontStyle' | 'index' | 'charSizes' | 'animationDuration' | 'spring';
20
26
  export interface ContinuousSlotProps extends Pick<SlotProps, Shared> {
21
27
  slot: [number | string, string];
28
+ spring?: Spring;
22
29
  easing?: EasingT;
23
30
  }
24
31
  export type CommaPosition = 1 | -1 | 0 | null;
@@ -31,14 +38,21 @@ export interface CommaProps {
31
38
  onExited: () => void;
32
39
  onEntered: () => void;
33
40
  }
34
- export interface AnimatedNumbersProps {
41
+ interface AnimatedNumbersPropsBase {
35
42
  value: number;
36
43
  fontStyle?: StyleProp<TextStyle>;
37
44
  prefix?: string;
38
45
  includeComma?: boolean;
39
46
  precision?: number;
40
- animationDuration?: number;
41
47
  animateIntermediateValues?: true;
42
- easing?: EasingT;
43
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);
44
58
  export {};