@umituz/react-native-mascot 1.0.12 → 1.3.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.0
|
|
3
|
+
"version": "1.3.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,6 +52,7 @@
|
|
|
52
52
|
"lottie-react-native": "^7.3.4"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
+
"@umituz/react-native-animation": "*",
|
|
55
56
|
"@umituz/react-native-design-system": "*",
|
|
56
57
|
"expo": ">=54.0.0",
|
|
57
58
|
"react": ">=19.0.0",
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mascot Component
|
|
3
|
+
*
|
|
4
|
+
* A simplified, configurable mascot component that works across different apps.
|
|
5
|
+
* Uses Reanimated animations and accepts dynamic theme colors and image sources.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { Mascot } from '@umituz/react-native-mascot';
|
|
10
|
+
*
|
|
11
|
+
* <Mascot
|
|
12
|
+
* source={require('../assets/mascot.png')}
|
|
13
|
+
* state="thinking"
|
|
14
|
+
* size="large"
|
|
15
|
+
* theme={{
|
|
16
|
+
* primary: '#FF6B6B',
|
|
17
|
+
* glow: 'rgba(255, 107, 107, 0.15)',
|
|
18
|
+
* }}
|
|
19
|
+
* />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useEffect, memo } from 'react';
|
|
24
|
+
import { View, Image, StyleSheet, ViewStyle, ImageSource } from 'react-native';
|
|
25
|
+
import Animated, {
|
|
26
|
+
useSharedValue,
|
|
27
|
+
useAnimatedStyle,
|
|
28
|
+
withTiming,
|
|
29
|
+
withRepeat,
|
|
30
|
+
withSequence,
|
|
31
|
+
Easing,
|
|
32
|
+
} from '@umituz/react-native-animation';
|
|
33
|
+
|
|
34
|
+
import type { MascotState } from './types';
|
|
35
|
+
|
|
36
|
+
export interface MascotProps {
|
|
37
|
+
/** Image source for the mascot (require() or URI) */
|
|
38
|
+
source: ImageSource;
|
|
39
|
+
|
|
40
|
+
/** Animation state */
|
|
41
|
+
state?: MascotState;
|
|
42
|
+
|
|
43
|
+
/** Size preset or custom size */
|
|
44
|
+
size?: number | 'small' | 'medium' | 'large';
|
|
45
|
+
|
|
46
|
+
/** Theme colors */
|
|
47
|
+
theme?: {
|
|
48
|
+
/** Primary glow color */
|
|
49
|
+
primary?: string;
|
|
50
|
+
/** Glow effect color (with alpha) */
|
|
51
|
+
glow?: string;
|
|
52
|
+
/** Message background color */
|
|
53
|
+
background?: string;
|
|
54
|
+
/** Message text color */
|
|
55
|
+
text?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Optional message to display */
|
|
59
|
+
message?: string;
|
|
60
|
+
|
|
61
|
+
/** Enable/disable animations */
|
|
62
|
+
animate?: boolean;
|
|
63
|
+
|
|
64
|
+
/** Additional styles */
|
|
65
|
+
style?: ViewStyle;
|
|
66
|
+
|
|
67
|
+
/** Test ID for testing */
|
|
68
|
+
testID?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const SIZE_PRESETS: Record<string, number> = {
|
|
72
|
+
small: 60,
|
|
73
|
+
medium: 100,
|
|
74
|
+
large: 140,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function MascotComponent({
|
|
78
|
+
source,
|
|
79
|
+
state = 'idle',
|
|
80
|
+
size = 'medium',
|
|
81
|
+
theme = {},
|
|
82
|
+
message,
|
|
83
|
+
animate = true,
|
|
84
|
+
style,
|
|
85
|
+
testID = 'simple-mascot',
|
|
86
|
+
}: MascotProps) {
|
|
87
|
+
const translateY = useSharedValue(0);
|
|
88
|
+
const scale = useSharedValue(1);
|
|
89
|
+
const rotate = useSharedValue(0);
|
|
90
|
+
|
|
91
|
+
// Resolve size
|
|
92
|
+
const imageSize = typeof size === 'number' ? size : SIZE_PRESETS[size] || SIZE_PRESETS.medium;
|
|
93
|
+
|
|
94
|
+
// Resolve theme colors with defaults
|
|
95
|
+
const primaryColor = theme.primary || '#FF6B6B';
|
|
96
|
+
const glowColor = theme.glow || `${primaryColor}26`; // 15% opacity
|
|
97
|
+
const backgroundColor = theme.background || 'rgba(30, 30, 30, 0.9)';
|
|
98
|
+
const textColor = theme.text || '#FFFFFF';
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
// Reset animations
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- Reanimated shared values are designed to be mutable
|
|
103
|
+
translateY.value = 0;
|
|
104
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
|
+
scale.value = 1;
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
rotate.value = 0;
|
|
108
|
+
|
|
109
|
+
if (!animate) return;
|
|
110
|
+
|
|
111
|
+
switch (state) {
|
|
112
|
+
case 'idle':
|
|
113
|
+
// Gentle floating animation
|
|
114
|
+
translateY.value = withRepeat(
|
|
115
|
+
withSequence(
|
|
116
|
+
withTiming(-6, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
|
117
|
+
withTiming(0, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
|
118
|
+
),
|
|
119
|
+
-1,
|
|
120
|
+
true,
|
|
121
|
+
);
|
|
122
|
+
scale.value = withRepeat(
|
|
123
|
+
withSequence(
|
|
124
|
+
withTiming(1.03, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
|
125
|
+
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
|
126
|
+
),
|
|
127
|
+
-1,
|
|
128
|
+
true,
|
|
129
|
+
);
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'thinking':
|
|
133
|
+
// Wiggle/twitch animation
|
|
134
|
+
rotate.value = withRepeat(
|
|
135
|
+
withSequence(
|
|
136
|
+
withTiming(-5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
137
|
+
withTiming(5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
138
|
+
withTiming(-5, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
139
|
+
withTiming(0, { duration: 200, easing: Easing.inOut(Easing.ease) }),
|
|
140
|
+
),
|
|
141
|
+
-1,
|
|
142
|
+
false,
|
|
143
|
+
);
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'reacting':
|
|
147
|
+
// Quick pulse + rotation
|
|
148
|
+
scale.value = withSequence(
|
|
149
|
+
withTiming(1.15, { duration: 150, easing: Easing.out(Easing.ease) }),
|
|
150
|
+
withTiming(1, { duration: 150, easing: Easing.inOut(Easing.ease) }),
|
|
151
|
+
);
|
|
152
|
+
rotate.value = withSequence(
|
|
153
|
+
withTiming(-10, { duration: 100, easing: Easing.out(Easing.ease) }),
|
|
154
|
+
withTiming(10, { duration: 100, easing: Easing.out(Easing.ease) }),
|
|
155
|
+
withTiming(0, { duration: 100, easing: Easing.inOut(Easing.ease) }),
|
|
156
|
+
);
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case 'excited':
|
|
160
|
+
// Bouncy jump animation
|
|
161
|
+
translateY.value = withRepeat(
|
|
162
|
+
withSequence(
|
|
163
|
+
withTiming(-15, { duration: 400, easing: Easing.out(Easing.ease) }),
|
|
164
|
+
withTiming(0, { duration: 300, easing: Easing.in(Easing.ease) }),
|
|
165
|
+
withTiming(-10, { duration: 300, easing: Easing.out(Easing.ease) }),
|
|
166
|
+
withTiming(0, { duration: 200, easing: Easing.in(Easing.ease) }),
|
|
167
|
+
),
|
|
168
|
+
-1,
|
|
169
|
+
false,
|
|
170
|
+
);
|
|
171
|
+
scale.value = withRepeat(
|
|
172
|
+
withSequence(
|
|
173
|
+
withTiming(1.08, { duration: 400, easing: Easing.out(Easing.ease) }),
|
|
174
|
+
withTiming(1, { duration: 300, easing: Easing.in(Easing.ease) }),
|
|
175
|
+
),
|
|
176
|
+
-1,
|
|
177
|
+
false,
|
|
178
|
+
);
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'speaking':
|
|
182
|
+
// Subtle breathing animation
|
|
183
|
+
scale.value = withRepeat(
|
|
184
|
+
withSequence(
|
|
185
|
+
withTiming(1.04, { duration: 800, easing: Easing.inOut(Easing.ease) }),
|
|
186
|
+
withTiming(1, { duration: 800, easing: Easing.inOut(Easing.ease) }),
|
|
187
|
+
),
|
|
188
|
+
-1,
|
|
189
|
+
true,
|
|
190
|
+
);
|
|
191
|
+
translateY.value = withRepeat(
|
|
192
|
+
withSequence(
|
|
193
|
+
withTiming(-3, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
|
194
|
+
withTiming(0, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
|
195
|
+
),
|
|
196
|
+
-1,
|
|
197
|
+
true,
|
|
198
|
+
);
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
default:
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}, [animate, state, translateY, scale, rotate]);
|
|
205
|
+
|
|
206
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
207
|
+
transform: [
|
|
208
|
+
{ translateY: translateY.value },
|
|
209
|
+
{ scale: scale.value },
|
|
210
|
+
{ rotate: `${rotate.value}deg` },
|
|
211
|
+
],
|
|
212
|
+
}));
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<View style={[styles.container, style]} testID={testID}>
|
|
216
|
+
<Animated.View style={animatedStyle}>
|
|
217
|
+
<View
|
|
218
|
+
style={[
|
|
219
|
+
styles.glowContainer,
|
|
220
|
+
{ width: imageSize + 20, height: imageSize + 20 },
|
|
221
|
+
]}
|
|
222
|
+
>
|
|
223
|
+
<View
|
|
224
|
+
style={[
|
|
225
|
+
styles.glow,
|
|
226
|
+
{ width: imageSize + 20, height: imageSize + 20, backgroundColor: glowColor },
|
|
227
|
+
]}
|
|
228
|
+
/>
|
|
229
|
+
<Image
|
|
230
|
+
source={source}
|
|
231
|
+
style={[
|
|
232
|
+
styles.mascotImage,
|
|
233
|
+
{ width: imageSize, height: imageSize },
|
|
234
|
+
]}
|
|
235
|
+
resizeMode="contain"
|
|
236
|
+
/>
|
|
237
|
+
</View>
|
|
238
|
+
</Animated.View>
|
|
239
|
+
{message ? (
|
|
240
|
+
<View style={[styles.messageContainer, { backgroundColor }]}>
|
|
241
|
+
<MascotText style={{ color: textColor }}>{message}</MascotText>
|
|
242
|
+
</View>
|
|
243
|
+
) : null}
|
|
244
|
+
</View>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
MascotComponent.displayName = 'Mascot';
|
|
249
|
+
|
|
250
|
+
const MascotText = memo(({ style, children }: { style: any; children: React.ReactNode }) => (
|
|
251
|
+
<View>
|
|
252
|
+
{typeof children === 'string' ? (
|
|
253
|
+
<Animated.Text style={style}>{children}</Animated.Text>
|
|
254
|
+
) : (
|
|
255
|
+
children
|
|
256
|
+
)}
|
|
257
|
+
</View>
|
|
258
|
+
));
|
|
259
|
+
|
|
260
|
+
export const Mascot = memo(MascotComponent);
|
|
261
|
+
|
|
262
|
+
const styles = StyleSheet.create({
|
|
263
|
+
container: {
|
|
264
|
+
alignItems: 'center',
|
|
265
|
+
},
|
|
266
|
+
glow: {
|
|
267
|
+
borderRadius: 9999, // Circular
|
|
268
|
+
position: 'absolute',
|
|
269
|
+
},
|
|
270
|
+
glowContainer: {
|
|
271
|
+
alignItems: 'center',
|
|
272
|
+
justifyContent: 'center',
|
|
273
|
+
position: 'relative',
|
|
274
|
+
},
|
|
275
|
+
mascotImage: {
|
|
276
|
+
borderRadius: 9999, // Circular
|
|
277
|
+
},
|
|
278
|
+
messageContainer: {
|
|
279
|
+
borderRadius: 12,
|
|
280
|
+
marginTop: 12,
|
|
281
|
+
maxWidth: 280,
|
|
282
|
+
paddingHorizontal: 16,
|
|
283
|
+
paddingVertical: 8,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mascot Types
|
|
3
|
+
* Shared type definitions for mascot components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type MascotState = 'idle' | 'thinking' | 'reacting' | 'excited' | 'speaking';
|
|
7
|
+
|
|
8
|
+
export type MascotSize = 'small' | 'medium' | 'large' | number;
|
|
9
|
+
|
|
10
|
+
export interface MascotTheme {
|
|
11
|
+
/** Primary color (used for glow) */
|
|
12
|
+
primary?: string;
|
|
13
|
+
/** Glow effect color with alpha */
|
|
14
|
+
glow?: string;
|
|
15
|
+
/** Message bubble background */
|
|
16
|
+
background?: string;
|
|
17
|
+
/** Message text color */
|
|
18
|
+
text?: string;
|
|
19
|
+
}
|
package/src/presentation.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Presentation Layer Exports
|
|
2
|
+
* Presentation Layer Exports
|
|
3
3
|
* Components, hooks, and contexts
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
export { MascotView } from './presentation/components/MascotView';
|
|
8
8
|
export type { MascotViewProps } from './presentation/components/MascotView';
|
|
9
9
|
|
|
10
|
+
export { Mascot } from './presentation/components/Mascot';
|
|
11
|
+
export type { MascotProps } from './presentation/components/Mascot';
|
|
12
|
+
|
|
10
13
|
export { LottieMascot } from './presentation/components/LottieMascot';
|
|
11
14
|
export type { LottieMascotProps } from './presentation/components/LottieMascot';
|
|
12
15
|
|
|
13
16
|
export { SVGMascot } from './presentation/components/SVGMascot';
|
|
14
17
|
export type { SVGMascotProps } from './presentation/components/SVGMascot';
|
|
15
18
|
|
|
19
|
+
// Presentation - Types
|
|
20
|
+
export type { MascotState, MascotSize, MascotTheme } from './presentation/components/types';
|
|
21
|
+
|
|
16
22
|
// Presentation - Hooks
|
|
17
23
|
export { useMascot } from './presentation/hooks/useMascot';
|
|
18
24
|
export type {
|