rn-confetti-love 1.0.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/LICENSE +21 -0
- package/README.md +232 -0
- package/package.json +54 -0
- package/src/Confetti.tsx +667 -0
- package/src/LoveConfetti.tsx +34 -0
- package/src/index.ts +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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,232 @@
|
|
|
1
|
+
# 🎊 rn-confetti-love
|
|
2
|
+
|
|
3
|
+
Beautiful animated confetti effects for React Native. While designed with love-themed apps in mind, it is fully customizable for any celebration!
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
|
|
10
|
+
- 🎨 **Full Customization**: Control colors, emojis, texts, images, and shapes
|
|
11
|
+
- 💖 **Love Presets**: Built-in specialized presets for romantic effects
|
|
12
|
+
- ⬆️ **Direction Control**: Top-to-bottom (fall) or bottom-to-top (rise) animations
|
|
13
|
+
- 🎭 **Custom Content**: Mix emojis, text, avatars, and custom views
|
|
14
|
+
- ⚡ **Performant**: Built with React Native Reanimated for 60fps animations
|
|
15
|
+
- 📱 **Responsive**: Auto-adjusts to screen dimensions
|
|
16
|
+
|
|
17
|
+
## 📦 Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install rn-confetti-love
|
|
21
|
+
# or
|
|
22
|
+
yarn add rn-confetti-love
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Peer Dependencies
|
|
26
|
+
|
|
27
|
+
Make sure you have the following packages installed:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install react-native-reanimated expo-image
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> **Note**: `expo-image` is optional and only required if you want to display avatar images/custom images in the confetti.
|
|
34
|
+
|
|
35
|
+
## 🚀 Usage
|
|
36
|
+
|
|
37
|
+
### `LoveConfetti` Component
|
|
38
|
+
|
|
39
|
+
A simplified wrapper component that uses the default love-themed configuration.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { LoveConfetti } from 'rn-confetti-love';
|
|
43
|
+
|
|
44
|
+
<LoveConfetti
|
|
45
|
+
isVisible={showConfetti}
|
|
46
|
+
onComplete={() => setShowConfetti(false)}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> **Note**: `LoveConfetti` is primarily for backward compatibility. For new implementations, we recommend using the main `Confetti` component.
|
|
51
|
+
|
|
52
|
+
### `Confetti` Component (Customizable)
|
|
53
|
+
|
|
54
|
+
For full control, use the `Confetti` component with the `config` prop.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { Confetti } from 'rn-confetti-love';
|
|
58
|
+
|
|
59
|
+
<Confetti
|
|
60
|
+
isVisible={true}
|
|
61
|
+
config={{
|
|
62
|
+
emojis: ['🎉', '🥳'],
|
|
63
|
+
colors: ['#FFD700', '#FF0000']
|
|
64
|
+
}}
|
|
65
|
+
onComplete={onDone}
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 🛠️ Advanced Customization
|
|
70
|
+
|
|
71
|
+
Pass a `config` object to customize every aspect of the confetti.
|
|
72
|
+
|
|
73
|
+
### Custom Emojis & Colors
|
|
74
|
+
|
|
75
|
+
Create a birthday or celebration theme:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Confetti
|
|
79
|
+
isVisible={true}
|
|
80
|
+
pieceCount={100}
|
|
81
|
+
config={{
|
|
82
|
+
emojis: ['🎉', '🎂', '🥳', '🎈'],
|
|
83
|
+
colors: ['#FFD700', '#FF6347', '#4169E1', '#32CD32'],
|
|
84
|
+
distribution: { emojis: 1 } // 100% emojis
|
|
85
|
+
}}
|
|
86
|
+
onComplete={onDone}
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Custom Text/Names
|
|
91
|
+
|
|
92
|
+
Display custom messages or names:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<Confetti
|
|
96
|
+
isVisible={true}
|
|
97
|
+
config={{
|
|
98
|
+
texts: ['Winner!', 'Level Up', '+100 XP'],
|
|
99
|
+
colors: ['#FFD700'], // Gold text
|
|
100
|
+
distribution: { texts: 1 }
|
|
101
|
+
}}
|
|
102
|
+
onComplete={onDone}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Custom Images & Avatars
|
|
107
|
+
|
|
108
|
+
Mix images with other confetti pieces:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<Confetti
|
|
112
|
+
isVisible={true}
|
|
113
|
+
config={{
|
|
114
|
+
images: ['https://example.com/avatar1.png', 'https://example.com/avatar2.png'],
|
|
115
|
+
emojis: ['🌟', '✨'],
|
|
116
|
+
distribution: { images: 0.3, emojis: 0.7 } // 30% images, 70% sparkles
|
|
117
|
+
}}
|
|
118
|
+
onComplete={onDone}
|
|
119
|
+
/>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Fine-Tuning Animations
|
|
123
|
+
|
|
124
|
+
Control speed, sway, and rotation:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<Confetti
|
|
128
|
+
isVisible={true}
|
|
129
|
+
config={{
|
|
130
|
+
animation: {
|
|
131
|
+
minDuration: 4000, // Slower animation
|
|
132
|
+
maxDuration: 6000,
|
|
133
|
+
minSway: 50, // Wider sway
|
|
134
|
+
maxSway: 100,
|
|
135
|
+
maxRotation: 720, // More spinning
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
onComplete={onDone}
|
|
139
|
+
/>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 🔄 Direction Options
|
|
143
|
+
|
|
144
|
+
### Top to Bottom (`direction="top-to-bottom"`)
|
|
145
|
+
Classic confetti falling from the sky.
|
|
146
|
+
|
|
147
|
+
### Bottom to Top (`direction="bottom-to-top"`)
|
|
148
|
+
Hearts rising up like floating balloons - perfect for "send love" interactions!
|
|
149
|
+
|
|
150
|
+
## 🎯 Use Cases
|
|
151
|
+
|
|
152
|
+
- 💑 Anniversary celebrations
|
|
153
|
+
- 💝 Valentine's Day features
|
|
154
|
+
- 💌 Sending love messages
|
|
155
|
+
- 🎉 Relationship milestones
|
|
156
|
+
- 💒 Wedding apps
|
|
157
|
+
- 💕 Romantic date apps
|
|
158
|
+
|
|
159
|
+
## 📖 API Reference
|
|
160
|
+
|
|
161
|
+
### `Confetti` Props
|
|
162
|
+
|
|
163
|
+
| Prop | Type | Default | Description |
|
|
164
|
+
|------|------|---------|-------------|
|
|
165
|
+
| `isVisible` | `boolean` | **required** | Controls visibility of the confetti |
|
|
166
|
+
| `config` | `ConfettiConfig` | `undefined` | Configuration object for full customization |
|
|
167
|
+
| `direction` | `'top-to-bottom' \| 'bottom-to-top'` | `'top-to-bottom'` | Animation direction |
|
|
168
|
+
| `pieceCount` | `number` | `55` | Number of confetti pieces |
|
|
169
|
+
| `onComplete` | `() => void` | `undefined` | Callback when all pieces finish animating |
|
|
170
|
+
|
|
171
|
+
### `ConfettiConfig` Object
|
|
172
|
+
|
|
173
|
+
The `config` prop accepts an object with the following optional keys:
|
|
174
|
+
|
|
175
|
+
| Key | Type | Description |
|
|
176
|
+
|-----|------|-------------|
|
|
177
|
+
| `emojis` | `string[]` | Array of emoji characters to use |
|
|
178
|
+
| `texts` | `string[]` | Array of text strings to display |
|
|
179
|
+
| `initials` | `string[]` | Array of characters for initial circles |
|
|
180
|
+
| `images` | `string[]` | Array of image URIs |
|
|
181
|
+
| `colors` | `string[]` | Array of colors for text and shapes |
|
|
182
|
+
| `distribution` | `ContentDistribution` | Object defining % of each content type |
|
|
183
|
+
| `animation` | `AnimationConfig` | Timing and movement settings |
|
|
184
|
+
| `sizes` | `SizeConfig` | Size boundaries for different pieces |
|
|
185
|
+
| `styles` | `ConfettiStyles` | Custom RN styles for piece components |
|
|
186
|
+
| `customContent` | `ConfettiContentItem[]` | Array for fully custom weighted content |
|
|
187
|
+
|
|
188
|
+
#### `AnimationConfig`
|
|
189
|
+
|
|
190
|
+
| Key | Default | Description |
|
|
191
|
+
|-----|---------|-------------|
|
|
192
|
+
| `minDuration` | `3000` | Minimum animation duration (ms) |
|
|
193
|
+
| `maxDuration` | `5000` | Maximum animation duration (ms) |
|
|
194
|
+
| `maxDelay` | `1500` | Max start delay for separate pieces (ms) |
|
|
195
|
+
| `minSway` | `30` | Minimum horizontal sway (px) |
|
|
196
|
+
| `maxSway` | `90` | Maximum horizontal sway (px) |
|
|
197
|
+
| `maxRotation` | `360` | Max rotation in degrees |
|
|
198
|
+
|
|
199
|
+
### Legacy Props (Deprecated)
|
|
200
|
+
|
|
201
|
+
These props are still supported but mapped internally to the new `config` system.
|
|
202
|
+
|
|
203
|
+
- `variant`: Use `config` presets instead
|
|
204
|
+
- `partner1Name`/`partner2Name`: Use `config.texts`
|
|
205
|
+
- `partner1Avatar`/`partner2Avatar`: Use `config.images`
|
|
206
|
+
|
|
207
|
+
## 🧩 Types
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
export interface ConfettiConfig {
|
|
211
|
+
emojis?: string[];
|
|
212
|
+
texts?: string[];
|
|
213
|
+
initials?: string[];
|
|
214
|
+
images?: string[];
|
|
215
|
+
colors?: string[];
|
|
216
|
+
distribution?: {
|
|
217
|
+
emojis?: number; // 0-1
|
|
218
|
+
texts?: number; // 0-1
|
|
219
|
+
initials?: number; // 0-1
|
|
220
|
+
images?: number; // 0-1
|
|
221
|
+
};
|
|
222
|
+
// ... and more
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## 📄 License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
Made with 💖
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rn-confetti-love",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Beautiful animated confetti effects for React Native with love-themed variants featuring hearts, names, initials, and avatars",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"source": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typescript": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react-native",
|
|
19
|
+
"confetti",
|
|
20
|
+
"animation",
|
|
21
|
+
"love",
|
|
22
|
+
"hearts",
|
|
23
|
+
"celebration",
|
|
24
|
+
"effects",
|
|
25
|
+
"reanimated",
|
|
26
|
+
"expo"
|
|
27
|
+
],
|
|
28
|
+
"author": "ShanuChandi",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/ShanuChandi/rn-confetti-love.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": " https://github.com/ShanuChandi/rn-confetti-love/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/ShanuChandi/rn-confetti-love#readme",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"expo-image": ">=1.0.0",
|
|
40
|
+
"react": ">=17.0.0",
|
|
41
|
+
"react-native": ">=0.64.0",
|
|
42
|
+
"react-native-reanimated": ">=2.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"expo-image": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "^18.3.27",
|
|
51
|
+
"@types/react-native": "^0.72.8",
|
|
52
|
+
"typescript": "^5.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/Confetti.tsx
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { Image } from 'expo-image';
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Dimensions, StyleSheet, Text, View, ImageStyle, TextStyle, ViewStyle } from 'react-native';
|
|
4
|
+
import Animated, {
|
|
5
|
+
Easing,
|
|
6
|
+
useAnimatedStyle,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withDelay,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
|
|
12
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// TYPES & INTERFACES
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/** Types of confetti pieces that can be rendered */
|
|
19
|
+
export type ConfettiPieceType = 'emoji' | 'text' | 'image';
|
|
20
|
+
|
|
21
|
+
/** Direction for confetti animation */
|
|
22
|
+
export type ConfettiDirection = 'top-to-bottom' | 'bottom-to-top';
|
|
23
|
+
|
|
24
|
+
/** Animation timing configuration */
|
|
25
|
+
export interface AnimationConfig {
|
|
26
|
+
/** Minimum animation duration in ms (default: 3000) */
|
|
27
|
+
minDuration?: number;
|
|
28
|
+
/** Maximum animation duration in ms (default: 5000) */
|
|
29
|
+
maxDuration?: number;
|
|
30
|
+
/** Maximum delay before piece starts animating in ms (default: 1500) */
|
|
31
|
+
maxDelay?: number;
|
|
32
|
+
/** Minimum horizontal sway amount in pixels (default: 30) */
|
|
33
|
+
minSway?: number;
|
|
34
|
+
/** Maximum horizontal sway amount in pixels (default: 90) */
|
|
35
|
+
maxSway?: number;
|
|
36
|
+
/** Sway cycle duration in ms (default: 800) */
|
|
37
|
+
swayCycleDuration?: number;
|
|
38
|
+
/** Maximum rotation in degrees (default: 360) */
|
|
39
|
+
maxRotation?: number;
|
|
40
|
+
/** Fade out starts at this percentage of duration (default: 0.7 = 70%) */
|
|
41
|
+
fadeOutStart?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Size configuration for confetti pieces */
|
|
45
|
+
export interface SizeConfig {
|
|
46
|
+
/** Minimum scale factor (default: 0.5) */
|
|
47
|
+
minScale?: number;
|
|
48
|
+
/** Maximum scale factor (default: 1.5) */
|
|
49
|
+
maxScale?: number;
|
|
50
|
+
/** Emoji font size (default: 28) */
|
|
51
|
+
emojiFontSize?: number;
|
|
52
|
+
/** Text font size (default: 14) */
|
|
53
|
+
textFontSize?: number;
|
|
54
|
+
/** Initial circle size (default: 36) */
|
|
55
|
+
initialCircleSize?: number;
|
|
56
|
+
/** Initial text font size (default: 18) */
|
|
57
|
+
initialFontSize?: number;
|
|
58
|
+
/** Avatar image size (default: 40) */
|
|
59
|
+
avatarSize?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Custom content item for confetti */
|
|
63
|
+
export interface ConfettiContentItem {
|
|
64
|
+
/** Type of content */
|
|
65
|
+
type: ConfettiPieceType;
|
|
66
|
+
/** Content value - emoji/text string or image URI */
|
|
67
|
+
value: string;
|
|
68
|
+
/** Optional weight for random selection (default: 1) */
|
|
69
|
+
weight?: number;
|
|
70
|
+
/** Optional custom color for this item */
|
|
71
|
+
color?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Distribution configuration for built-in content types */
|
|
75
|
+
export interface ContentDistribution {
|
|
76
|
+
/** Percentage of emoji pieces (0-1, default: 0.5) */
|
|
77
|
+
emojis?: number;
|
|
78
|
+
/** Percentage of text pieces like names (0-1, default: 0.3) */
|
|
79
|
+
texts?: number;
|
|
80
|
+
/** Percentage of initial/letter pieces (0-1, default: 0.1) */
|
|
81
|
+
initials?: number;
|
|
82
|
+
/** Percentage of image pieces (0-1, default: 0.1) */
|
|
83
|
+
images?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Style customization for confetti pieces */
|
|
87
|
+
export interface ConfettiStyles {
|
|
88
|
+
/** Custom emoji text style */
|
|
89
|
+
emoji?: TextStyle;
|
|
90
|
+
/** Custom text style */
|
|
91
|
+
text?: TextStyle;
|
|
92
|
+
/** Custom initial circle container style */
|
|
93
|
+
initialCircle?: ViewStyle;
|
|
94
|
+
/** Custom initial text style */
|
|
95
|
+
initialText?: TextStyle;
|
|
96
|
+
/** Custom image container style */
|
|
97
|
+
imageContainer?: ViewStyle;
|
|
98
|
+
/** Custom image style */
|
|
99
|
+
image?: ImageStyle;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Main configuration object for full customization */
|
|
103
|
+
export interface ConfettiConfig {
|
|
104
|
+
// === Content Configuration ===
|
|
105
|
+
/** Array of emoji strings to use (default: heart emojis) */
|
|
106
|
+
emojis?: string[];
|
|
107
|
+
/** Array of text strings to display (e.g., names) */
|
|
108
|
+
texts?: string[];
|
|
109
|
+
/** Array of single characters/initials to display */
|
|
110
|
+
initials?: string[];
|
|
111
|
+
/** Array of image URIs to display */
|
|
112
|
+
images?: string[];
|
|
113
|
+
/** Custom content items with full control */
|
|
114
|
+
customContent?: ConfettiContentItem[];
|
|
115
|
+
/** Distribution weights for content types (ignored if customContent is provided) */
|
|
116
|
+
distribution?: ContentDistribution;
|
|
117
|
+
|
|
118
|
+
// === Color Configuration ===
|
|
119
|
+
/** Array of colors to use for text/initials (default: pink/love colors) */
|
|
120
|
+
colors?: string[];
|
|
121
|
+
|
|
122
|
+
// === Animation Configuration ===
|
|
123
|
+
/** Animation timing settings */
|
|
124
|
+
animation?: AnimationConfig;
|
|
125
|
+
|
|
126
|
+
// === Size Configuration ===
|
|
127
|
+
/** Size settings for pieces */
|
|
128
|
+
sizes?: SizeConfig;
|
|
129
|
+
|
|
130
|
+
// === Style Overrides ===
|
|
131
|
+
/** Custom styles for different piece types */
|
|
132
|
+
styles?: ConfettiStyles;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Internal confetti piece data structure */
|
|
136
|
+
interface ConfettiPiece {
|
|
137
|
+
id: number;
|
|
138
|
+
type: ConfettiPieceType;
|
|
139
|
+
content: string;
|
|
140
|
+
imageUri?: string;
|
|
141
|
+
isInitial?: boolean;
|
|
142
|
+
startX: number;
|
|
143
|
+
startY: number;
|
|
144
|
+
endY: number;
|
|
145
|
+
rotation: number;
|
|
146
|
+
scale: number;
|
|
147
|
+
delay: number;
|
|
148
|
+
duration: number;
|
|
149
|
+
swayAmount: number;
|
|
150
|
+
swayCycleDuration: number;
|
|
151
|
+
color: string;
|
|
152
|
+
direction: ConfettiDirection;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Props for the main Confetti component */
|
|
156
|
+
export interface ConfettiProps {
|
|
157
|
+
/** Controls visibility of the confetti */
|
|
158
|
+
isVisible: boolean;
|
|
159
|
+
/** Animation direction (default: 'top-to-bottom') */
|
|
160
|
+
direction?: ConfettiDirection;
|
|
161
|
+
/** Number of confetti pieces (default: 55) */
|
|
162
|
+
pieceCount?: number;
|
|
163
|
+
/** Full configuration object for customization */
|
|
164
|
+
config?: ConfettiConfig;
|
|
165
|
+
/** Callback when all pieces finish animating */
|
|
166
|
+
onComplete?: () => void;
|
|
167
|
+
|
|
168
|
+
// === Legacy props for backward compatibility ===
|
|
169
|
+
/** @deprecated Use config.texts instead */
|
|
170
|
+
partner1Name?: string;
|
|
171
|
+
/** @deprecated Use config.texts instead */
|
|
172
|
+
partner2Name?: string;
|
|
173
|
+
/** @deprecated Use config.images instead */
|
|
174
|
+
partner1Avatar?: string;
|
|
175
|
+
/** @deprecated Use config.images instead */
|
|
176
|
+
partner2Avatar?: string;
|
|
177
|
+
/** @deprecated Use config instead */
|
|
178
|
+
variant?: 'love' | 'hearts-only';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// DEFAULT VALUES
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
/** Default heart/love emojis */
|
|
186
|
+
export const DEFAULT_EMOJIS = ['❤️', '💕', '💖', '💗', '💝', '💘', '💓', '💞', '🩷', '♥️'];
|
|
187
|
+
|
|
188
|
+
/** Default love-themed colors */
|
|
189
|
+
export const DEFAULT_COLORS = [
|
|
190
|
+
'#E29588', // Primary pink
|
|
191
|
+
'#FF6B8A', // Bright pink
|
|
192
|
+
'#FF85A2', // Light pink
|
|
193
|
+
'#FFB6C1', // Light pink 2
|
|
194
|
+
'#FFC0CB', // Pink
|
|
195
|
+
'#FF69B4', // Hot pink
|
|
196
|
+
'#FF1493', // Deep pink
|
|
197
|
+
'#DB7093', // Pale violet red
|
|
198
|
+
'#E67E92', // Rose
|
|
199
|
+
'#F8A5B8', // Sakura pink
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
/** Default animation configuration */
|
|
203
|
+
const DEFAULT_ANIMATION: Required<AnimationConfig> = {
|
|
204
|
+
minDuration: 3000,
|
|
205
|
+
maxDuration: 5000,
|
|
206
|
+
maxDelay: 1500,
|
|
207
|
+
minSway: 30,
|
|
208
|
+
maxSway: 90,
|
|
209
|
+
swayCycleDuration: 800,
|
|
210
|
+
maxRotation: 360,
|
|
211
|
+
fadeOutStart: 0.7,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Default size configuration */
|
|
215
|
+
const DEFAULT_SIZES: Required<SizeConfig> = {
|
|
216
|
+
minScale: 0.5,
|
|
217
|
+
maxScale: 1.5,
|
|
218
|
+
emojiFontSize: 28,
|
|
219
|
+
textFontSize: 14,
|
|
220
|
+
initialCircleSize: 36,
|
|
221
|
+
initialFontSize: 18,
|
|
222
|
+
avatarSize: 40,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/** Default content distribution */
|
|
226
|
+
const DEFAULT_DISTRIBUTION: Required<ContentDistribution> = {
|
|
227
|
+
emojis: 0.5,
|
|
228
|
+
texts: 0.2,
|
|
229
|
+
initials: 0.2,
|
|
230
|
+
images: 0.1,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// HELPER FUNCTIONS
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
/** Merge user config with defaults */
|
|
238
|
+
const mergeConfig = (config?: ConfettiConfig): Required<Omit<ConfettiConfig, 'customContent' | 'texts' | 'initials' | 'images'>> & ConfettiConfig => {
|
|
239
|
+
return {
|
|
240
|
+
emojis: config?.emojis ?? DEFAULT_EMOJIS,
|
|
241
|
+
texts: config?.texts,
|
|
242
|
+
initials: config?.initials,
|
|
243
|
+
images: config?.images,
|
|
244
|
+
customContent: config?.customContent,
|
|
245
|
+
distribution: { ...DEFAULT_DISTRIBUTION, ...config?.distribution },
|
|
246
|
+
colors: config?.colors ?? DEFAULT_COLORS,
|
|
247
|
+
animation: { ...DEFAULT_ANIMATION, ...config?.animation },
|
|
248
|
+
sizes: { ...DEFAULT_SIZES, ...config?.sizes },
|
|
249
|
+
styles: config?.styles,
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/** Generate confetti pieces based on configuration */
|
|
254
|
+
const generateConfettiPieces = (
|
|
255
|
+
direction: ConfettiDirection,
|
|
256
|
+
count: number,
|
|
257
|
+
config: ReturnType<typeof mergeConfig>,
|
|
258
|
+
// Legacy props
|
|
259
|
+
legacyTexts?: string[],
|
|
260
|
+
legacyImages?: string[],
|
|
261
|
+
): ConfettiPiece[] => {
|
|
262
|
+
const pieces: ConfettiPiece[] = [];
|
|
263
|
+
const { emojis, texts, initials, images, customContent, distribution, colors, animation, sizes } = config;
|
|
264
|
+
|
|
265
|
+
// Merge legacy props with config
|
|
266
|
+
const allTexts = texts ?? legacyTexts ?? [];
|
|
267
|
+
const allImages = images ?? legacyImages ?? [];
|
|
268
|
+
const allInitials = initials ?? allTexts.map(t => t.charAt(0).toUpperCase());
|
|
269
|
+
|
|
270
|
+
// Calculate start and end positions based on direction
|
|
271
|
+
const isBottomToTop = direction === 'bottom-to-top';
|
|
272
|
+
const getStartY = () => isBottomToTop
|
|
273
|
+
? SCREEN_HEIGHT + 100 + Math.random() * 200
|
|
274
|
+
: -100 - Math.random() * 200;
|
|
275
|
+
const getEndY = () => isBottomToTop
|
|
276
|
+
? -150
|
|
277
|
+
: SCREEN_HEIGHT + 150;
|
|
278
|
+
|
|
279
|
+
for (let i = 0; i < count; i++) {
|
|
280
|
+
let type: ConfettiPieceType;
|
|
281
|
+
let content = '';
|
|
282
|
+
let imageUri: string | undefined;
|
|
283
|
+
let isInitial = false;
|
|
284
|
+
let pieceColor = colors[Math.floor(Math.random() * colors.length)];
|
|
285
|
+
|
|
286
|
+
// If custom content is provided, use weighted random selection
|
|
287
|
+
if (customContent && customContent.length > 0) {
|
|
288
|
+
const totalWeight = customContent.reduce((sum, item) => sum + (item.weight ?? 1), 0);
|
|
289
|
+
let random = Math.random() * totalWeight;
|
|
290
|
+
|
|
291
|
+
for (const item of customContent) {
|
|
292
|
+
random -= item.weight ?? 1;
|
|
293
|
+
if (random <= 0) {
|
|
294
|
+
type = item.type;
|
|
295
|
+
if (item.type === 'image') {
|
|
296
|
+
imageUri = item.value;
|
|
297
|
+
} else {
|
|
298
|
+
content = item.value;
|
|
299
|
+
}
|
|
300
|
+
if (item.color) {
|
|
301
|
+
pieceColor = item.color;
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
type = type! ?? 'emoji';
|
|
307
|
+
} else {
|
|
308
|
+
// Use distribution-based selection
|
|
309
|
+
const rand = Math.random();
|
|
310
|
+
const dist = distribution!;
|
|
311
|
+
|
|
312
|
+
if (rand < dist.emojis! && emojis.length > 0) {
|
|
313
|
+
type = 'emoji';
|
|
314
|
+
content = emojis[Math.floor(Math.random() * emojis.length)];
|
|
315
|
+
} else if (rand < dist.emojis! + dist.initials! && allInitials.length > 0) {
|
|
316
|
+
type = 'text';
|
|
317
|
+
content = allInitials[Math.floor(Math.random() * allInitials.length)];
|
|
318
|
+
isInitial = true;
|
|
319
|
+
} else if (rand < dist.emojis! + dist.initials! + dist.texts! && allTexts.length > 0) {
|
|
320
|
+
type = 'text';
|
|
321
|
+
content = allTexts[Math.floor(Math.random() * allTexts.length)];
|
|
322
|
+
} else if (allImages.length > 0) {
|
|
323
|
+
type = 'image';
|
|
324
|
+
imageUri = allImages[Math.floor(Math.random() * allImages.length)];
|
|
325
|
+
} else {
|
|
326
|
+
// Fallback to emoji
|
|
327
|
+
type = 'emoji';
|
|
328
|
+
content = emojis[Math.floor(Math.random() * emojis.length)];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const duration = animation!.minDuration! + Math.random() * (animation!.maxDuration! - animation!.minDuration!);
|
|
333
|
+
const swayAmount = animation!.minSway! + Math.random() * (animation!.maxSway! - animation!.minSway!);
|
|
334
|
+
const scale = sizes!.minScale! + Math.random() * (sizes!.maxScale! - sizes!.minScale!);
|
|
335
|
+
|
|
336
|
+
pieces.push({
|
|
337
|
+
id: i,
|
|
338
|
+
type,
|
|
339
|
+
content,
|
|
340
|
+
imageUri,
|
|
341
|
+
isInitial,
|
|
342
|
+
startX: Math.random() * SCREEN_WIDTH,
|
|
343
|
+
startY: getStartY(),
|
|
344
|
+
endY: getEndY(),
|
|
345
|
+
rotation: Math.random() * animation!.maxRotation! * 2 - animation!.maxRotation!,
|
|
346
|
+
scale,
|
|
347
|
+
delay: Math.random() * animation!.maxDelay!,
|
|
348
|
+
duration,
|
|
349
|
+
swayAmount,
|
|
350
|
+
swayCycleDuration: animation!.swayCycleDuration!,
|
|
351
|
+
color: pieceColor,
|
|
352
|
+
direction,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return pieces;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// COMPONENTS
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
interface ConfettiPieceComponentProps {
|
|
364
|
+
piece: ConfettiPiece;
|
|
365
|
+
config: ReturnType<typeof mergeConfig>;
|
|
366
|
+
onFinish: () => void;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Individual animated confetti piece */
|
|
370
|
+
const ConfettiPieceComponent: React.FC<ConfettiPieceComponentProps> = ({ piece, config, onFinish }) => {
|
|
371
|
+
const translateY = useSharedValue(piece.startY);
|
|
372
|
+
const translateX = useSharedValue(piece.startX);
|
|
373
|
+
const rotate = useSharedValue(0);
|
|
374
|
+
const opacity = useSharedValue(1);
|
|
375
|
+
|
|
376
|
+
const { animation, sizes, styles: customStyles } = config;
|
|
377
|
+
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
// Animate movement
|
|
380
|
+
translateY.value = withDelay(
|
|
381
|
+
piece.delay,
|
|
382
|
+
withTiming(piece.endY, {
|
|
383
|
+
duration: piece.duration,
|
|
384
|
+
easing: Easing.out(Easing.quad),
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Handle completion
|
|
389
|
+
const completionTimeout = setTimeout(() => {
|
|
390
|
+
onFinish();
|
|
391
|
+
}, piece.delay + piece.duration);
|
|
392
|
+
|
|
393
|
+
// Animate rotation
|
|
394
|
+
rotate.value = withDelay(
|
|
395
|
+
piece.delay,
|
|
396
|
+
withTiming(piece.rotation, {
|
|
397
|
+
duration: piece.duration,
|
|
398
|
+
easing: Easing.linear,
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Animate horizontal sway
|
|
403
|
+
const swayCycle = () => {
|
|
404
|
+
translateX.value = withTiming(
|
|
405
|
+
piece.startX + piece.swayAmount,
|
|
406
|
+
{ duration: piece.swayCycleDuration, easing: Easing.inOut(Easing.sin) },
|
|
407
|
+
() => {
|
|
408
|
+
translateX.value = withTiming(
|
|
409
|
+
piece.startX - piece.swayAmount,
|
|
410
|
+
{ duration: piece.swayCycleDuration, easing: Easing.inOut(Easing.sin) },
|
|
411
|
+
() => {
|
|
412
|
+
translateX.value = withTiming(
|
|
413
|
+
piece.startX,
|
|
414
|
+
{ duration: piece.swayCycleDuration, easing: Easing.inOut(Easing.sin) }
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const timeout = setTimeout(swayCycle, piece.delay);
|
|
423
|
+
|
|
424
|
+
// Fade out
|
|
425
|
+
opacity.value = withDelay(
|
|
426
|
+
piece.delay + piece.duration * animation!.fadeOutStart!,
|
|
427
|
+
withTiming(0, { duration: piece.duration * (1 - animation!.fadeOutStart!) })
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
return () => {
|
|
431
|
+
clearTimeout(timeout);
|
|
432
|
+
clearTimeout(completionTimeout);
|
|
433
|
+
};
|
|
434
|
+
}, []);
|
|
435
|
+
|
|
436
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
437
|
+
return {
|
|
438
|
+
transform: [
|
|
439
|
+
{ translateX: translateX.value },
|
|
440
|
+
{ translateY: translateY.value },
|
|
441
|
+
{ rotate: `${rotate.value}deg` },
|
|
442
|
+
{ scale: piece.scale },
|
|
443
|
+
],
|
|
444
|
+
opacity: opacity.value,
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const renderContent = () => {
|
|
449
|
+
switch (piece.type) {
|
|
450
|
+
case 'emoji':
|
|
451
|
+
return (
|
|
452
|
+
<Text style={[
|
|
453
|
+
styles.emoji,
|
|
454
|
+
{ fontSize: sizes!.emojiFontSize },
|
|
455
|
+
customStyles?.emoji
|
|
456
|
+
]}>
|
|
457
|
+
{piece.content}
|
|
458
|
+
</Text>
|
|
459
|
+
);
|
|
460
|
+
case 'text':
|
|
461
|
+
if (piece.isInitial) {
|
|
462
|
+
return (
|
|
463
|
+
<View style={[
|
|
464
|
+
styles.initialCircle,
|
|
465
|
+
{
|
|
466
|
+
width: sizes!.initialCircleSize,
|
|
467
|
+
height: sizes!.initialCircleSize,
|
|
468
|
+
borderRadius: sizes!.initialCircleSize! / 2,
|
|
469
|
+
backgroundColor: `${piece.color}30`
|
|
470
|
+
},
|
|
471
|
+
customStyles?.initialCircle
|
|
472
|
+
]}>
|
|
473
|
+
<Text style={[
|
|
474
|
+
styles.initialText,
|
|
475
|
+
{ fontSize: sizes!.initialFontSize, color: piece.color },
|
|
476
|
+
customStyles?.initialText
|
|
477
|
+
]}>
|
|
478
|
+
{piece.content}
|
|
479
|
+
</Text>
|
|
480
|
+
</View>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return (
|
|
484
|
+
<Text style={[
|
|
485
|
+
styles.text,
|
|
486
|
+
{ fontSize: sizes!.textFontSize, color: piece.color },
|
|
487
|
+
customStyles?.text
|
|
488
|
+
]}>
|
|
489
|
+
{piece.content}
|
|
490
|
+
</Text>
|
|
491
|
+
);
|
|
492
|
+
case 'image':
|
|
493
|
+
return (
|
|
494
|
+
<View style={[
|
|
495
|
+
styles.imageContainer,
|
|
496
|
+
{
|
|
497
|
+
width: sizes!.avatarSize,
|
|
498
|
+
height: sizes!.avatarSize,
|
|
499
|
+
borderRadius: sizes!.avatarSize! / 2
|
|
500
|
+
},
|
|
501
|
+
customStyles?.imageContainer
|
|
502
|
+
]}>
|
|
503
|
+
<Image
|
|
504
|
+
source={{ uri: piece.imageUri }}
|
|
505
|
+
style={[styles.image, customStyles?.image]}
|
|
506
|
+
contentFit="cover"
|
|
507
|
+
/>
|
|
508
|
+
</View>
|
|
509
|
+
);
|
|
510
|
+
default:
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<Animated.View style={[styles.confettiPiece, animatedStyle]}>
|
|
517
|
+
{renderContent()}
|
|
518
|
+
</Animated.View>
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/** Main Confetti Component */
|
|
523
|
+
const Confetti: React.FC<ConfettiProps> = ({
|
|
524
|
+
isVisible,
|
|
525
|
+
direction = 'top-to-bottom',
|
|
526
|
+
pieceCount = 55,
|
|
527
|
+
config,
|
|
528
|
+
onComplete,
|
|
529
|
+
// Legacy props
|
|
530
|
+
partner1Name,
|
|
531
|
+
partner2Name,
|
|
532
|
+
partner1Avatar,
|
|
533
|
+
partner2Avatar,
|
|
534
|
+
variant,
|
|
535
|
+
}) => {
|
|
536
|
+
const [pieces, setPieces] = useState<ConfettiPiece[]>([]);
|
|
537
|
+
const finishedCount = useRef(0);
|
|
538
|
+
const totalPieces = useRef(0);
|
|
539
|
+
|
|
540
|
+
// Merge configuration with defaults
|
|
541
|
+
const mergedConfig = React.useMemo(() => {
|
|
542
|
+
// Handle legacy variant prop
|
|
543
|
+
if (variant === 'hearts-only') {
|
|
544
|
+
return mergeConfig({
|
|
545
|
+
...config,
|
|
546
|
+
distribution: { emojis: 1, texts: 0, initials: 0, images: 0 },
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return mergeConfig(config);
|
|
550
|
+
}, [config, variant]);
|
|
551
|
+
|
|
552
|
+
// Build legacy arrays for backward compatibility
|
|
553
|
+
const legacyTexts = React.useMemo(() => {
|
|
554
|
+
const texts: string[] = [];
|
|
555
|
+
if (partner1Name) texts.push(partner1Name);
|
|
556
|
+
if (partner2Name) texts.push(partner2Name);
|
|
557
|
+
return texts.length > 0 ? texts : undefined;
|
|
558
|
+
}, [partner1Name, partner2Name]);
|
|
559
|
+
|
|
560
|
+
const legacyImages = React.useMemo(() => {
|
|
561
|
+
const images: string[] = [];
|
|
562
|
+
if (partner1Avatar) images.push(partner1Avatar);
|
|
563
|
+
if (partner2Avatar) images.push(partner2Avatar);
|
|
564
|
+
return images.length > 0 ? images : undefined;
|
|
565
|
+
}, [partner1Avatar, partner2Avatar]);
|
|
566
|
+
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
if (isVisible) {
|
|
569
|
+
const newPieces = generateConfettiPieces(
|
|
570
|
+
direction,
|
|
571
|
+
pieceCount,
|
|
572
|
+
mergedConfig,
|
|
573
|
+
legacyTexts,
|
|
574
|
+
legacyImages
|
|
575
|
+
);
|
|
576
|
+
setPieces(newPieces);
|
|
577
|
+
totalPieces.current = newPieces.length;
|
|
578
|
+
finishedCount.current = 0;
|
|
579
|
+
} else {
|
|
580
|
+
setPieces([]);
|
|
581
|
+
}
|
|
582
|
+
}, [isVisible, direction, pieceCount, mergedConfig, legacyTexts, legacyImages]);
|
|
583
|
+
|
|
584
|
+
const handlePieceFinish = useCallback(() => {
|
|
585
|
+
finishedCount.current += 1;
|
|
586
|
+
if (finishedCount.current >= totalPieces.current && onComplete) {
|
|
587
|
+
onComplete();
|
|
588
|
+
}
|
|
589
|
+
}, [onComplete]);
|
|
590
|
+
|
|
591
|
+
if (!isVisible || pieces.length === 0) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<View style={styles.container} pointerEvents="none">
|
|
597
|
+
{pieces.map((piece) => (
|
|
598
|
+
<ConfettiPieceComponent
|
|
599
|
+
key={piece.id}
|
|
600
|
+
piece={piece}
|
|
601
|
+
config={mergedConfig}
|
|
602
|
+
onFinish={handlePieceFinish}
|
|
603
|
+
/>
|
|
604
|
+
))}
|
|
605
|
+
</View>
|
|
606
|
+
);
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// STYLES
|
|
611
|
+
// ============================================================================
|
|
612
|
+
|
|
613
|
+
const styles = StyleSheet.create({
|
|
614
|
+
container: {
|
|
615
|
+
...StyleSheet.absoluteFillObject,
|
|
616
|
+
zIndex: 9999,
|
|
617
|
+
elevation: 9999,
|
|
618
|
+
},
|
|
619
|
+
confettiPiece: {
|
|
620
|
+
position: 'absolute',
|
|
621
|
+
},
|
|
622
|
+
emoji: {
|
|
623
|
+
fontSize: 28,
|
|
624
|
+
textShadowColor: 'rgba(0, 0, 0, 0.1)',
|
|
625
|
+
textShadowOffset: { width: 1, height: 1 },
|
|
626
|
+
textShadowRadius: 2,
|
|
627
|
+
},
|
|
628
|
+
text: {
|
|
629
|
+
fontSize: 14,
|
|
630
|
+
fontWeight: '600',
|
|
631
|
+
textShadowColor: 'rgba(0, 0, 0, 0.15)',
|
|
632
|
+
textShadowOffset: { width: 1, height: 1 },
|
|
633
|
+
textShadowRadius: 2,
|
|
634
|
+
},
|
|
635
|
+
initialCircle: {
|
|
636
|
+
width: 36,
|
|
637
|
+
height: 36,
|
|
638
|
+
borderRadius: 18,
|
|
639
|
+
alignItems: 'center',
|
|
640
|
+
justifyContent: 'center',
|
|
641
|
+
borderWidth: 2,
|
|
642
|
+
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
643
|
+
},
|
|
644
|
+
initialText: {
|
|
645
|
+
fontSize: 18,
|
|
646
|
+
fontWeight: '700',
|
|
647
|
+
},
|
|
648
|
+
imageContainer: {
|
|
649
|
+
width: 40,
|
|
650
|
+
height: 40,
|
|
651
|
+
borderRadius: 20,
|
|
652
|
+
overflow: 'hidden',
|
|
653
|
+
borderWidth: 2,
|
|
654
|
+
borderColor: 'rgba(255, 255, 255, 0.6)',
|
|
655
|
+
shadowColor: '#000',
|
|
656
|
+
shadowOffset: { width: 0, height: 2 },
|
|
657
|
+
shadowOpacity: 0.2,
|
|
658
|
+
shadowRadius: 4,
|
|
659
|
+
elevation: 4,
|
|
660
|
+
},
|
|
661
|
+
image: {
|
|
662
|
+
width: '100%',
|
|
663
|
+
height: '100%',
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
export default Confetti;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoveConfetti - A wrapper around the base Confetti component
|
|
3
|
+
* for backward compatibility with existing usage.
|
|
4
|
+
*
|
|
5
|
+
* For new implementations, consider using the Confetti component directly
|
|
6
|
+
* with the appropriate variant and direction props.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import Confetti, { ConfettiConfig } from './Confetti';
|
|
11
|
+
|
|
12
|
+
interface LoveConfettiProps {
|
|
13
|
+
isVisible: boolean;
|
|
14
|
+
config?: ConfettiConfig;
|
|
15
|
+
onComplete: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LoveConfetti: React.FC<LoveConfettiProps> = ({
|
|
19
|
+
isVisible,
|
|
20
|
+
config,
|
|
21
|
+
onComplete,
|
|
22
|
+
}) => {
|
|
23
|
+
return (
|
|
24
|
+
<Confetti
|
|
25
|
+
isVisible={isVisible}
|
|
26
|
+
direction="top-to-bottom"
|
|
27
|
+
pieceCount={55}
|
|
28
|
+
config={config}
|
|
29
|
+
onComplete={onComplete}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default LoveConfetti;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rn-confetti-love
|
|
3
|
+
* Beautiful animated confetti effects for React Native
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Main components
|
|
7
|
+
export { default as Confetti } from './Confetti';
|
|
8
|
+
export { default as LoveConfetti } from './LoveConfetti';
|
|
9
|
+
|
|
10
|
+
// Types
|
|
11
|
+
export type {
|
|
12
|
+
ConfettiProps,
|
|
13
|
+
ConfettiDirection,
|
|
14
|
+
ConfettiConfig,
|
|
15
|
+
ConfettiPieceType,
|
|
16
|
+
AnimationConfig,
|
|
17
|
+
SizeConfig,
|
|
18
|
+
ConfettiContentItem,
|
|
19
|
+
ContentDistribution,
|
|
20
|
+
ConfettiStyles,
|
|
21
|
+
} from './Confetti';
|