@umituz/react-native-mascot 1.3.6 → 1.4.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-mascot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Interactive mascot system for React Native apps - Customizable animated characters with Lottie and SVG support, mood system, and easy integration",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"lottie-react-native": "^7.3.4"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@umituz/react-native-animation": "
|
|
55
|
+
"@umituz/react-native-animation": "*",
|
|
56
56
|
"@umituz/react-native-design-system": "*",
|
|
57
57
|
"expo": ">=54.0.0",
|
|
58
58
|
"react": ">=19.0.0",
|
|
59
|
-
"react-native": "
|
|
59
|
+
"react-native": ">=0.74.0",
|
|
60
60
|
"react-native-reanimated": ">=3.0.0",
|
|
61
61
|
"react-native-svg": ">=15.0.0"
|
|
62
62
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mascot Component
|
|
2
|
+
* Mascot Component - Performance Optimized
|
|
3
3
|
*
|
|
4
4
|
* A simplified, configurable mascot component that works across different apps.
|
|
5
5
|
* Uses Reanimated animations and accepts dynamic theme colors and image sources.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import React, { useEffect, memo } from 'react';
|
|
23
|
+
import React, { useEffect, memo, useRef, useCallback } from 'react';
|
|
24
24
|
import { View, Image, StyleSheet, ViewStyle, ImageSourcePropType, TextStyle } from 'react-native';
|
|
25
25
|
import Animated, {
|
|
26
26
|
useSharedValue,
|
|
@@ -29,6 +29,7 @@ import Animated, {
|
|
|
29
29
|
withRepeat,
|
|
30
30
|
withSequence,
|
|
31
31
|
Easing,
|
|
32
|
+
cancelAnimation,
|
|
32
33
|
} from '@umituz/react-native-animation';
|
|
33
34
|
|
|
34
35
|
import type { MascotState } from './types';
|
|
@@ -77,6 +78,112 @@ const SIZE_PRESETS: Record<string, number> = {
|
|
|
77
78
|
large: 140,
|
|
78
79
|
};
|
|
79
80
|
|
|
81
|
+
// Worklet: Idle animation
|
|
82
|
+
const animateIdle = (
|
|
83
|
+
translateY: Animated.SharedValue<number>,
|
|
84
|
+
scale: Animated.SharedValue<number>
|
|
85
|
+
) => {
|
|
86
|
+
'worklet';
|
|
87
|
+
translateY.value = withRepeat(
|
|
88
|
+
withSequence(
|
|
89
|
+
withTiming(-6, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
|
90
|
+
withTiming(0, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
|
91
|
+
),
|
|
92
|
+
-1,
|
|
93
|
+
true,
|
|
94
|
+
);
|
|
95
|
+
scale.value = withRepeat(
|
|
96
|
+
withSequence(
|
|
97
|
+
withTiming(1.03, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
|
98
|
+
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
|
99
|
+
),
|
|
100
|
+
-1,
|
|
101
|
+
true,
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Worklet: Thinking animation
|
|
106
|
+
const animateThinking = (rotate: Animated.SharedValue<number>) => {
|
|
107
|
+
'worklet';
|
|
108
|
+
rotate.value = withRepeat(
|
|
109
|
+
withSequence(
|
|
110
|
+
withTiming(-5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
111
|
+
withTiming(5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
112
|
+
withTiming(-5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
113
|
+
withTiming(0, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
114
|
+
),
|
|
115
|
+
-1,
|
|
116
|
+
false,
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Worklet: Reacting animation
|
|
121
|
+
const animateReacting = (
|
|
122
|
+
scale: Animated.SharedValue<number>,
|
|
123
|
+
rotate: Animated.SharedValue<number>
|
|
124
|
+
) => {
|
|
125
|
+
'worklet';
|
|
126
|
+
scale.value = withSequence(
|
|
127
|
+
withTiming(1.15, { duration: 150, easing: Easing.out(Easing.ease) }),
|
|
128
|
+
withTiming(1, { duration: 150, easing: Easing.inOut(Easing.ease) }),
|
|
129
|
+
);
|
|
130
|
+
rotate.value = withSequence(
|
|
131
|
+
withTiming(-10, { duration: 100, easing: Easing.out(Easing.ease) }),
|
|
132
|
+
withTiming(10, { duration: 100, easing: Easing.out(Easing.ease) }),
|
|
133
|
+
withTiming(0, { duration: 100, easing: Easing.inOut(Easing.ease) }),
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Worklet: Excited animation
|
|
138
|
+
const animateExcited = (
|
|
139
|
+
translateY: Animated.SharedValue<number>,
|
|
140
|
+
scale: Animated.SharedValue<number>
|
|
141
|
+
) => {
|
|
142
|
+
'worklet';
|
|
143
|
+
translateY.value = withRepeat(
|
|
144
|
+
withSequence(
|
|
145
|
+
withTiming(-15, { duration: 400, easing: Easing.out(Easing.ease) }),
|
|
146
|
+
withTiming(0, { duration: 300, easing: Easing.in(Easing.ease) }),
|
|
147
|
+
withTiming(-10, { duration: 300, easing: Easing.out(Easing.ease) }),
|
|
148
|
+
withTiming(0, { duration: 200, easing: Easing.ease }),
|
|
149
|
+
),
|
|
150
|
+
-1,
|
|
151
|
+
false,
|
|
152
|
+
);
|
|
153
|
+
scale.value = withRepeat(
|
|
154
|
+
withSequence(
|
|
155
|
+
withTiming(1.08, { duration: 400, easing: Easing.out(Easing.ease) }),
|
|
156
|
+
withTiming(1, { duration: 300, easing: Easing.ease }),
|
|
157
|
+
),
|
|
158
|
+
-1,
|
|
159
|
+
false,
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Worklet: Speaking animation
|
|
164
|
+
const animateSpeaking = (
|
|
165
|
+
scale: Animated.SharedValue<number>,
|
|
166
|
+
translateY: Animated.SharedValue<number>
|
|
167
|
+
) => {
|
|
168
|
+
'worklet';
|
|
169
|
+
scale.value = withRepeat(
|
|
170
|
+
withSequence(
|
|
171
|
+
withTiming(1.04, { duration: 800, easing: Easing.inOut(Easing.ease) }),
|
|
172
|
+
withTiming(1, { duration: 800, easing: Easing.inOut(Easing.ease) }),
|
|
173
|
+
),
|
|
174
|
+
-1,
|
|
175
|
+
true,
|
|
176
|
+
);
|
|
177
|
+
translateY.value = withRepeat(
|
|
178
|
+
withSequence(
|
|
179
|
+
withTiming(-3, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
|
180
|
+
withTiming(0, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
|
181
|
+
),
|
|
182
|
+
-1,
|
|
183
|
+
true,
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
80
187
|
function MascotComponent({
|
|
81
188
|
source,
|
|
82
189
|
state = 'idle',
|
|
@@ -90,6 +197,7 @@ function MascotComponent({
|
|
|
90
197
|
const translateY = useSharedValue(0);
|
|
91
198
|
const scale = useSharedValue(1);
|
|
92
199
|
const rotate = useSharedValue(0);
|
|
200
|
+
const isMountedRef = useRef(true);
|
|
93
201
|
|
|
94
202
|
// Resolve size
|
|
95
203
|
const imageSize = typeof size === 'number' ? size : SIZE_PRESETS[size] || SIZE_PRESETS.medium;
|
|
@@ -100,111 +208,59 @@ function MascotComponent({
|
|
|
100
208
|
const backgroundColor = theme.background || 'rgba(30, 30, 30, 0.9)';
|
|
101
209
|
const textColor = theme.text || '#FFFFFF';
|
|
102
210
|
|
|
211
|
+
// Cleanup function
|
|
212
|
+
const cleanupAnimations = useCallback(() => {
|
|
213
|
+
if (!isMountedRef.current) return;
|
|
214
|
+
cancelAnimation(translateY);
|
|
215
|
+
cancelAnimation(scale);
|
|
216
|
+
cancelAnimation(rotate);
|
|
217
|
+
}, []);
|
|
218
|
+
|
|
219
|
+
// Trigger animations based on state
|
|
103
220
|
useEffect(() => {
|
|
104
|
-
|
|
105
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- Reanimated shared values are designed to be mutable
|
|
106
|
-
translateY.value = 0;
|
|
107
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
|
-
scale.value = 1;
|
|
109
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
-
rotate.value = 0;
|
|
221
|
+
if (!isMountedRef.current || !animate) return;
|
|
111
222
|
|
|
112
|
-
|
|
223
|
+
// Cleanup previous animations first
|
|
224
|
+
cleanupAnimations();
|
|
113
225
|
|
|
114
226
|
switch (state) {
|
|
115
227
|
case 'idle':
|
|
116
|
-
|
|
117
|
-
translateY.value = withRepeat(
|
|
118
|
-
withSequence(
|
|
119
|
-
withTiming(-6, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
|
120
|
-
withTiming(0, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
|
121
|
-
),
|
|
122
|
-
-1,
|
|
123
|
-
true,
|
|
124
|
-
);
|
|
125
|
-
scale.value = withRepeat(
|
|
126
|
-
withSequence(
|
|
127
|
-
withTiming(1.03, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
|
128
|
-
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
|
129
|
-
),
|
|
130
|
-
-1,
|
|
131
|
-
true,
|
|
132
|
-
);
|
|
228
|
+
animateIdle(translateY, scale);
|
|
133
229
|
break;
|
|
134
230
|
|
|
135
231
|
case 'thinking':
|
|
136
|
-
|
|
137
|
-
rotate.value = withRepeat(
|
|
138
|
-
withSequence(
|
|
139
|
-
withTiming(-5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
140
|
-
withTiming(5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
141
|
-
withTiming(-5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
142
|
-
withTiming(0, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
143
|
-
),
|
|
144
|
-
-1,
|
|
145
|
-
false,
|
|
146
|
-
);
|
|
232
|
+
animateThinking(rotate);
|
|
147
233
|
break;
|
|
148
234
|
|
|
149
235
|
case 'reacting':
|
|
150
|
-
|
|
151
|
-
scale.value = withSequence(
|
|
152
|
-
withTiming(1.15, { duration: 150, easing: Easing.out(Easing.ease) }),
|
|
153
|
-
withTiming(1, { duration: 150, easing: Easing.inOut(Easing.ease) }),
|
|
154
|
-
);
|
|
155
|
-
rotate.value = withSequence(
|
|
156
|
-
withTiming(-10, { duration: 100, easing: Easing.out(Easing.ease) }),
|
|
157
|
-
withTiming(10, { duration: 100, easing: Easing.out(Easing.ease) }),
|
|
158
|
-
withTiming(0, { duration: 100, easing: Easing.inOut(Easing.ease) }),
|
|
159
|
-
);
|
|
236
|
+
animateReacting(scale, rotate);
|
|
160
237
|
break;
|
|
161
238
|
|
|
162
239
|
case 'excited':
|
|
163
|
-
|
|
164
|
-
translateY.value = withRepeat(
|
|
165
|
-
withSequence(
|
|
166
|
-
withTiming(-15, { duration: 400, easing: Easing.out(Easing.ease) }),
|
|
167
|
-
withTiming(0, { duration: 300, easing: Easing.in(Easing.ease) }),
|
|
168
|
-
withTiming(-10, { duration: 300, easing: Easing.out(Easing.ease) }),
|
|
169
|
-
withTiming(0, { duration: 200, easing: Easing.in(Easing.ease) }),
|
|
170
|
-
),
|
|
171
|
-
-1,
|
|
172
|
-
false,
|
|
173
|
-
);
|
|
174
|
-
scale.value = withRepeat(
|
|
175
|
-
withSequence(
|
|
176
|
-
withTiming(1.08, { duration: 400, easing: Easing.out(Easing.ease) }),
|
|
177
|
-
withTiming(1, { duration: 300, easing: Easing.in(Easing.ease) }),
|
|
178
|
-
),
|
|
179
|
-
-1,
|
|
180
|
-
false,
|
|
181
|
-
);
|
|
240
|
+
animateExcited(translateY, scale);
|
|
182
241
|
break;
|
|
183
242
|
|
|
184
243
|
case 'speaking':
|
|
185
|
-
|
|
186
|
-
scale.value = withRepeat(
|
|
187
|
-
withSequence(
|
|
188
|
-
withTiming(1.04, { duration: 800, easing: Easing.inOut(Easing.ease) }),
|
|
189
|
-
withTiming(1, { duration: 800, easing: Easing.inOut(Easing.ease) }),
|
|
190
|
-
),
|
|
191
|
-
-1,
|
|
192
|
-
true,
|
|
193
|
-
);
|
|
194
|
-
translateY.value = withRepeat(
|
|
195
|
-
withSequence(
|
|
196
|
-
withTiming(-3, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
|
197
|
-
withTiming(0, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
|
198
|
-
),
|
|
199
|
-
-1,
|
|
200
|
-
true,
|
|
201
|
-
);
|
|
244
|
+
animateSpeaking(scale, translateY);
|
|
202
245
|
break;
|
|
203
246
|
|
|
204
247
|
default:
|
|
205
248
|
break;
|
|
206
249
|
}
|
|
207
|
-
|
|
250
|
+
|
|
251
|
+
return () => {
|
|
252
|
+
cleanupAnimations();
|
|
253
|
+
};
|
|
254
|
+
}, [animate, state, translateY, scale, rotate, cleanupAnimations]);
|
|
255
|
+
|
|
256
|
+
// Cleanup on unmount
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
isMountedRef.current = true;
|
|
259
|
+
return () => {
|
|
260
|
+
isMountedRef.current = false;
|
|
261
|
+
cleanupAnimations();
|
|
262
|
+
};
|
|
263
|
+
}, [cleanupAnimations]);
|
|
208
264
|
|
|
209
265
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
210
266
|
transform: [
|
|
@@ -25,7 +25,7 @@ const SVGMascotComponent = memo<SVGMascotProps>(({ mascot, size }) => {
|
|
|
25
25
|
<circle cx="35" cy="40" r="5" fill="#000" />
|
|
26
26
|
<circle cx="65" cy="40" r="5" fill="#000" />
|
|
27
27
|
{/* Mouth based on mood */}
|
|
28
|
-
<
|
|
28
|
+
<MoodExpression mood={mascot.personality.mood} />
|
|
29
29
|
</Svg>
|
|
30
30
|
);
|
|
31
31
|
});
|
|
@@ -35,11 +35,11 @@ SVGMascotComponent.displayName = 'SVGMascot';
|
|
|
35
35
|
export const SVGMascot = SVGMascotComponent;
|
|
36
36
|
|
|
37
37
|
// Mood-based mouth component
|
|
38
|
-
interface
|
|
38
|
+
interface MoodExpressionProps {
|
|
39
39
|
mood: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const MoodExpressionComponent = memo<MoodExpressionProps>(({ mood }) => {
|
|
43
43
|
switch (mood) {
|
|
44
44
|
case 'happy':
|
|
45
45
|
case 'excited':
|
|
@@ -56,6 +56,6 @@ const MoodMoodComponent = memo<MoodMoodProps>(({ mood }) => {
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
MoodExpressionComponent.displayName = 'MoodExpression';
|
|
60
60
|
|
|
61
|
-
const
|
|
61
|
+
const MoodExpression = memo(MoodExpressionComponent);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Thin wrapper that provides MascotService to components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { createContext, useContext, ReactNode } from 'react';
|
|
6
|
+
import React, { createContext, useContext, ReactNode, useEffect } from 'react';
|
|
7
7
|
import type { Mascot } from '../../domain/entities/Mascot';
|
|
8
8
|
import type { MascotConfig } from '../../domain/types/MascotTypes';
|
|
9
9
|
import type { MascotService, MascotTemplate } from '../../application/services/MascotService';
|
|
@@ -33,11 +33,13 @@ export const MascotProvider: React.FC<MascotProviderProps> = ({
|
|
|
33
33
|
const service = container.getMascotService();
|
|
34
34
|
|
|
35
35
|
// Auto-initialize if config or template provided
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (initialConfig) {
|
|
38
|
+
service.initialize(initialConfig);
|
|
39
|
+
} else if (template) {
|
|
40
|
+
service.fromTemplate(template);
|
|
41
|
+
}
|
|
42
|
+
}, [initialConfig, template, service]);
|
|
41
43
|
|
|
42
44
|
const value: MascotContextValue = {
|
|
43
45
|
mascot: service.mascot,
|
|
@@ -137,7 +137,7 @@ export function useMascotState(options: UseMascotStateOptions = {}): UseMascotSt
|
|
|
137
137
|
try {
|
|
138
138
|
manager.transitionTo(newState);
|
|
139
139
|
} catch (error) {
|
|
140
|
-
|
|
140
|
+
// Invalid state transition - allow the transition anyway for flexibility
|
|
141
141
|
setState(newState);
|
|
142
142
|
onStateChangeRef.current?.(state, newState);
|
|
143
143
|
}
|