react-native-slot-text 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ import type { AnimatedNumbersProps } from './types';
2
+ /**
3
+ * AnimatedNumbers Component
4
+ *
5
+ * This component animates numeric values, transitioning between old and new numbers.
6
+ * It supports animation of individual digits, optional commas, and a customizable prefix.
7
+ * The animation occurs over a defined duration and can be repeated as the value changes.
8
+ *
9
+ * @param {number|string} props.value - The value to animate to. Can be string of numbers.
10
+ * @param {Object} props.fontStyle - The style of the text, passed as a TextStyle object.
11
+ * @param {number} [props.animationDuration=200] - The duration of the animation in milliseconds. Defaults to 200ms.
12
+ * @param {string} [props.prefix=""] - A prefix to the number, such as a currency symbol.
13
+ * @param {boolean} [props.includeComma=false] - Whether to include commas as thousand separators.
14
+ *
15
+ * @returns {JSX.Element} The animated number component with slots for digits and commas.
16
+ */
17
+ declare const AnimatedNumbers: (props: AnimatedNumbersProps) => import("react/jsx-runtime").JSX.Element;
18
+ export default AnimatedNumbers;
@@ -0,0 +1,147 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState, useRef, useCallback } from "react";
3
+ import { View, Text, Animated } from "react-native";
4
+ import ReAnimated, { ZoomIn, StretchOutX } from 'react-native-reanimated';
5
+ import styles from './styles';
6
+ import Slot from './Slot';
7
+ import { formatString } from "./helpers";
8
+ const DEFAULT_DURTION = 200;
9
+ /**
10
+ * AnimatedNumbers Component
11
+ *
12
+ * This component animates numeric values, transitioning between old and new numbers.
13
+ * It supports animation of individual digits, optional commas, and a customizable prefix.
14
+ * The animation occurs over a defined duration and can be repeated as the value changes.
15
+ *
16
+ * @param {number|string} props.value - The value to animate to. Can be string of numbers.
17
+ * @param {Object} props.fontStyle - The style of the text, passed as a TextStyle object.
18
+ * @param {number} [props.animationDuration=200] - The duration of the animation in milliseconds. Defaults to 200ms.
19
+ * @param {string} [props.prefix=""] - A prefix to the number, such as a currency symbol.
20
+ * @param {boolean} [props.includeComma=false] - Whether to include commas as thousand separators.
21
+ *
22
+ * @returns {JSX.Element} The animated number component with slots for digits and commas.
23
+ */
24
+ const AnimatedNumbers = (props) => {
25
+ const [state, setState] = useState('idle');
26
+ const [oldNumber, setOldNumber] = useState([]);
27
+ const [newNumber, setNewNumber] = useState([]);
28
+ const [animatingValue, setAnimatingValue] = useState();
29
+ // The initial positions of the slots that are animating in
30
+ // -1 indicates above the slot, 0 indicates the slot, 1 indicates below the slot
31
+ const [inZeroPositions, setInZeroPositions] = useState([]);
32
+ // The final positions of the slots animating out
33
+ const [outFinalPositions, setOutFinalPositions] = useState([]);
34
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
35
+ const containerWidth = useRef(new Animated.Value(0)).current;
36
+ const containerHeight = useRef(new Animated.Value(0)).current;
37
+ const measureRef = useRef(null);
38
+ useEffect(() => {
39
+ setOldNumber(`${props.prefix || ''}${props.value}`
40
+ .split('')
41
+ .map((val, i) => isNaN(val) ? val : parseInt(val)));
42
+ }, []);
43
+ useEffect(() => {
44
+ // Animation is in progress, queue the new value
45
+ if (state === 'idle' && oldNumber.join('') !== formatString(props.value, props.prefix)) {
46
+ setAnimatingValue(formatString(props.value, props.prefix));
47
+ }
48
+ }, [props.value, state]);
49
+ useEffect(() => {
50
+ if (!animatingValue)
51
+ return;
52
+ setState('animating');
53
+ // If the old number is not set, this means it's the initial rendering of this component
54
+ // and the value needs to be loaded
55
+ if (oldNumber?.length === 0) {
56
+ setOldNumber(animatingValue
57
+ .split('')
58
+ .map((val, i) => isNaN(val) ? val : parseInt(val)));
59
+ }
60
+ // Prepare the new number for animation
61
+ else {
62
+ setNewNumber(animatingValue
63
+ .split('')
64
+ .map((val, i) => isNaN(val) ? val : parseInt(val)));
65
+ // The positions of the slots that are animating in
66
+ let newValuePositions = Array(animatingValue.length).fill(0);
67
+ // Compare from the right to the left
68
+ // * If the slots are the same value, no animation is needed for the slot, just stick it in the -1 position
69
+ // * If one of the slots is a string, the new slot will move down
70
+ // * If the new slot is greater than the old slot, the new slot will move down
71
+ // or up depending on which is greater
72
+ for (let i = 0; i < newValuePositions.length; i++) {
73
+ const oldNum = oldNumber[i];
74
+ const newNum = isNaN(animatingValue.charAt(i))
75
+ ? animatingValue.charAt(i)
76
+ : parseInt(animatingValue.charAt(i));
77
+ if (oldNum === newNum) {
78
+ newValuePositions[i] = undefined;
79
+ }
80
+ else {
81
+ newValuePositions[i] = typeof oldNum === 'string' || typeof newNum === 'string'
82
+ ? -1
83
+ : oldNum === undefined ? -1 : newNum > oldNum ? -1 : 1;
84
+ }
85
+ }
86
+ setInZeroPositions(newValuePositions);
87
+ setOutFinalPositions([
88
+ ...newValuePositions.slice(0, oldNumber.length).map(val => val ? val * -1 : 0),
89
+ ...Array(Math.max(0, oldNumber.length - animatingValue.length)).fill(-1)
90
+ ]);
91
+ }
92
+ }, [animatingValue]);
93
+ const onMeasureLayout = useCallback((e) => {
94
+ if (containerSize.width === 0) {
95
+ containerWidth.setValue(e.nativeEvent.layout.width);
96
+ containerHeight.setValue(e.nativeEvent.layout.height);
97
+ }
98
+ else {
99
+ Animated.timing(containerWidth, {
100
+ toValue: e.nativeEvent.layout.width,
101
+ duration: props.animationDuration || DEFAULT_DURTION,
102
+ useNativeDriver: false,
103
+ delay: containerSize.width > e.nativeEvent.layout.width ? props.animationDuration || DEFAULT_DURTION : 0
104
+ }).start();
105
+ Animated.timing(containerHeight, {
106
+ toValue: e.nativeEvent.layout.height,
107
+ duration: props.animationDuration || DEFAULT_DURTION,
108
+ useNativeDriver: false
109
+ }).start();
110
+ }
111
+ setContainerSize(e.nativeEvent.layout);
112
+ }, []);
113
+ const onCompleted = useCallback(() => {
114
+ setOldNumber(newNumber);
115
+ setNewNumber([]);
116
+ setInZeroPositions([]);
117
+ setOutFinalPositions([]);
118
+ setState('idle');
119
+ }, [newNumber]);
120
+ return (_jsxs(_Fragment, { children: [_jsxs(Animated.View, { style: [
121
+ { width: containerWidth, height: containerHeight },
122
+ styles.animatedNumbers
123
+ ], children: [_jsx(View, { style: styles.slotsContainer, children: oldNumber.map((val, i) => (_jsxs(_Fragment, { children: [(oldNumber.length - i) % 3 === 0 && i > 1 && props.includeComma &&
124
+ _jsx(ReAnimated.View, { entering: ZoomIn.delay(props.animationDuration || DEFAULT_DURTION).withInitialValues({ opacity: 0 }), exiting: StretchOutX.withInitialValues({ opacity: 1 }), children: _jsx(Text, { style: props.fontStyle, children: "," }) }, `${val}-${i}-comma`), _jsx(Slot, { value: val, height: containerSize.height, initial: 0, final: outFinalPositions[i] || 0, animationDuration: props.animationDuration || DEFAULT_DURTION, fontStyle: props.fontStyle }, `${val}-${i}`)] }))) }), _jsx(View, { style: styles.slotsContainer, children: newNumber.map((val, i) => (_jsxs(_Fragment, { children: [(newNumber.length - i) % 3 === 0 && i > 1 && props.includeComma &&
125
+ _jsx(ReAnimated.View, { entering: ZoomIn.delay(props.animationDuration || DEFAULT_DURTION).withInitialValues({ opacity: 0 }), exiting: StretchOutX.withInitialValues({ opacity: 1 }), children: _jsx(Text, { style: props.fontStyle, children: "," }) }, `${val}-${i}-comma-new`), _jsx(Slot, { value: val, initial: inZeroPositions[i] || -1, final: inZeroPositions[i] ? 0 : -1, height: containerSize.height, animationDuration: props.animationDuration || DEFAULT_DURTION, fontStyle: props.fontStyle, onCompleted: onCompleted }, `${val}-${i}`)] }))) })] }), _jsxs(View, { onLayout: onMeasureLayout, style: styles.spacer, ref: measureRef, children: [props.prefix && _jsx(Text, { style: props.fontStyle, children: props.prefix }), oldNumber.map((val, i) => (_jsx(Text, { style: props.fontStyle, children: val }, `${val}-${i}`)))] })] }));
126
+ };
127
+ /*
128
+ Basic logic:
129
+
130
+ When a new value comes in:
131
+
132
+ 1. If there is an animation currently in progress then queue the new value with any prefix prepended
133
+
134
+ 2. For an animation cycle, split the new number and set the new number state
135
+
136
+ 3. Loop through the new number and old number and compare digits, determing which way the new numbers
137
+ and old numers need to animate. There are two versions of the number, one that's visible, and one outside
138
+ the visible clipping container. If a slot has the same number between the old and new nummer, it wont be animated.
139
+
140
+ 4. Set the new positions wich will trigger the animations of the slots
141
+
142
+ 5. At the end, clear the new number and set the old number to the new number
143
+
144
+ 6. Pop any queued values and set the formated value, which will retriger the animation cycle
145
+
146
+ */
147
+ export default AnimatedNumbers;
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Geist UI.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # react-native-slot-text
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install react-native-slot-text
7
+ ```
8
+
9
+ This project was created using `bun init` in bun v1.1.21. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
10
+
11
+ https://github.com/user-attachments/assets/df5449f1-e20c-4a18-8687-67f29652d7b6
12
+
13
+
14
+ ## Usage
15
+
16
+ ```
17
+ <Slider
18
+ value={value}
19
+ onValueChange={(value) => setValue(value[0])}
20
+ minimumValue={0}
21
+ maximumValue={limit_amount || 100}
22
+ step={1}
23
+ maximumTrackTintColor={theme.colors.quinaryText}
24
+ minimumTrackTintColor={theme.colors.blueText}
25
+ thumbTintColor={theme.colors.whiteText}
26
+ thumbStyle={{
27
+ width: 18,
28
+ height: 18,
29
+ shadowColor: theme.colors.navShadow,
30
+ shadowOffset: { width: 0, height: 1 },
31
+ shadowOpacity: 1,
32
+ shadowRadius: 1,
33
+ }}
34
+ />
35
+ ```
36
+
37
+ ### Props
38
+
39
+ | Prop | Type | Default | Description |
40
+ |---------------------|-----------------------|----------|--------------------------------------------------------------------------------------------------|
41
+ | `value` | '${number}' | `N/A` | The value to animate to. Can be a number or a string of numbers. |
42
+ | `fontStyle` | `Object` | `N/A` | The style of the text, passed as a `TextStyle` object. |
43
+ | `animationDuration` | `number` | `200` | The duration of the animation in milliseconds. Defaults to 200ms. |
44
+ | `prefix` | `string` | `""` | A prefix to the number, such as a currency symbol. |
45
+ | `includeComma` | `boolean` | `false` | Whether to include commas as thousand separators. |
package/Slot.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { SlotProps } from './types';
2
+ declare const Slot: (props: SlotProps) => import("react/jsx-runtime").JSX.Element;
3
+ export default Slot;
package/Slot.js ADDED
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useRef, useEffect } from 'react';
3
+ import { Text, Animated } from 'react-native';
4
+ const Slot = (props) => {
5
+ const Y = useRef(new Animated.Value(props.initial * props.height));
6
+ useEffect(() => {
7
+ if (props.final !== undefined) {
8
+ Animated.timing(Y.current, {
9
+ toValue: props.final * props.height,
10
+ duration: props.animationDuration,
11
+ useNativeDriver: true
12
+ }).start(() => {
13
+ if (props.onCompleted) {
14
+ props.onCompleted();
15
+ }
16
+ });
17
+ }
18
+ }, [props.final]);
19
+ return (_jsx(Animated.View, { style: [{
20
+ transform: [{ translateY: Y.current }],
21
+ opacity: Y.current.interpolate({
22
+ inputRange: [-1 * props.height, 0, props.height],
23
+ outputRange: [0, 1, 0]
24
+ })
25
+ }], children: _jsx(Text, { style: props.fontStyle, children: props.value }) }));
26
+ };
27
+ export default Slot;
package/helpers.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const formatString: (v: string, p?: string) => string;
package/helpers.js ADDED
@@ -0,0 +1 @@
1
+ export const formatString = (v, p) => `${p || ''}${v}`;
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as SlotText } from './AnimatedNumbers';
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default as SlotText } from './AnimatedNumbers';
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "react-native-slot-text",
3
+ "version": "1.0.0",
4
+ "author": "sc-mitton <scm.mitton@gmail.com> (https://github.com/sc-mitton)",
5
+ "main": "index.js",
6
+ "module": "index.ts",
7
+ "devDependencies": {
8
+ "@types/lodash": "^4.17.9",
9
+ "@types/react": "^18.3.10",
10
+ "@types/react-native": "^0.73.0",
11
+ "typescript": "^5.0.0",
12
+ "tsx": "^4.15.5",
13
+ "@types/bun": "latest"
14
+ },
15
+ "peerDependencies": {
16
+ "react": "^18.3.1",
17
+ "react-native": "^0.75.3",
18
+ "react-native-reanimated": "^3.15.4"
19
+ },
20
+ "description": "Slot machine style animted text for React Native",
21
+ "keywords": [
22
+ "slot",
23
+ "text",
24
+ "react-native",
25
+ "react",
26
+ "slot-machine",
27
+ "react-native-slot-text",
28
+ "animation"
29
+ ],
30
+ "license": "MIT",
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "release": "tsx src/pre-release.ts && cd lib && npm publish --access public --no-git-checks"
34
+ },
35
+ "type": "module",
36
+ "types": "src/types.d.ts"
37
+ }
package/styles.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ declare const styles: {
2
+ animatedNumbers: {
3
+ overflow: "hidden";
4
+ };
5
+ spacer: {
6
+ position: "absolute";
7
+ flexDirection: "row";
8
+ opacity: number;
9
+ gap: number;
10
+ };
11
+ slotsContainer: {
12
+ position: "absolute";
13
+ flexDirection: "row";
14
+ justifyContent: "center";
15
+ gap: number;
16
+ };
17
+ };
18
+ export default styles;
package/styles.js ADDED
@@ -0,0 +1,19 @@
1
+ import { StyleSheet } from 'react-native';
2
+ const styles = StyleSheet.create({
3
+ animatedNumbers: {
4
+ overflow: 'hidden',
5
+ },
6
+ spacer: {
7
+ position: 'absolute',
8
+ flexDirection: 'row',
9
+ opacity: 0,
10
+ gap: 2
11
+ },
12
+ slotsContainer: {
13
+ position: 'absolute',
14
+ flexDirection: 'row',
15
+ justifyContent: 'center',
16
+ gap: 2,
17
+ }
18
+ });
19
+ export default styles;
package/types.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { StyleProp, TextStyle } from 'react-native';
2
+ export type Position = -1 | 0 | 1;
3
+ export interface SlotProps {
4
+ value: number | string;
5
+ animationDuration: number;
6
+ fontStyle?: StyleProp<TextStyle>;
7
+ initial: Position;
8
+ final: Position;
9
+ height: number;
10
+ isNew?: boolean;
11
+ onCompleted?: () => void;
12
+ }
13
+ export interface AnimatedNumbersProps {
14
+ value: `${number}`;
15
+ fontStyle?: StyleProp<TextStyle>;
16
+ animationDuration?: number;
17
+ prefix?: string;
18
+ includeComma?: boolean;
19
+ }
package/types.js ADDED
@@ -0,0 +1 @@
1
+ import { Text } from 'react-native';