@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.6",
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": "^1.3.17",
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
- // Reset animations
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
- if (!animate) return;
223
+ // Cleanup previous animations first
224
+ cleanupAnimations();
113
225
 
114
226
  switch (state) {
115
227
  case 'idle':
116
- // Gentle floating animation
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
- // Wiggle/twitch animation
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
- // Quick pulse + rotation
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
- // Bouncy jump animation
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
- // Subtle breathing animation
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
- }, [animate, state, translateY, scale, rotate]);
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
- <MoodMood mood={mascot.personality.mood} />
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 MoodMoodProps {
38
+ interface MoodExpressionProps {
39
39
  mood: string;
40
40
  }
41
41
 
42
- const MoodMoodComponent = memo<MoodMoodProps>(({ mood }) => {
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
- MoodMoodComponent.displayName = 'MoodMood';
59
+ MoodExpressionComponent.displayName = 'MoodExpression';
60
60
 
61
- const MoodMood = memo(MoodMoodComponent);
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
- if (initialConfig) {
37
- service.initialize(initialConfig);
38
- } else if (template) {
39
- service.fromTemplate(template);
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
- console.warn('Invalid state transition:', error);
140
+ // Invalid state transition - allow the transition anyway for flexibility
141
141
  setState(newState);
142
142
  onStateChangeRef.current?.(state, newState);
143
143
  }