react-native-confetti-reanimated 0.1.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 +22 -0
- package/README.md +277 -0
- package/lib/commonjs/ConfettiCanvas.js +89 -0
- package/lib/commonjs/ConfettiCanvas.js.map +1 -0
- package/lib/commonjs/ConfettiParticle.js +116 -0
- package/lib/commonjs/ConfettiParticle.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/presets.js +146 -0
- package/lib/commonjs/presets.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useConfetti.js +27 -0
- package/lib/commonjs/useConfetti.js.map +1 -0
- package/lib/commonjs/utils.js +125 -0
- package/lib/commonjs/utils.js.map +1 -0
- package/lib/module/ConfettiCanvas.js +84 -0
- package/lib/module/ConfettiCanvas.js.map +1 -0
- package/lib/module/ConfettiParticle.js +110 -0
- package/lib/module/ConfettiParticle.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/presets.js +142 -0
- package/lib/module/presets.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useConfetti.js +22 -0
- package/lib/module/useConfetti.js.map +1 -0
- package/lib/module/utils.js +116 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/typescript/ConfettiCanvas.d.ts +20 -0
- package/lib/typescript/ConfettiCanvas.d.ts.map +1 -0
- package/lib/typescript/ConfettiParticle.d.ts +10 -0
- package/lib/typescript/ConfettiParticle.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/presets.d.ts +52 -0
- package/lib/typescript/presets.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +118 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/useConfetti.d.ts +11 -0
- package/lib/typescript/useConfetti.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +24 -0
- package/lib/typescript/utils.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/ConfettiCanvas.tsx +121 -0
- package/src/ConfettiParticle.tsx +141 -0
- package/src/index.tsx +6 -0
- package/src/presets.ts +126 -0
- package/src/types.ts +137 -0
- package/src/useConfetti.ts +25 -0
- package/src/utils.ts +135 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useCallback, useImperativeHandle, useState } from 'react';
|
|
2
|
+
import { StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
3
|
+
import type { ConfettiConfig, ConfettiMethods } from './types';
|
|
4
|
+
import { createConfettiParticles, DEFAULT_CONFIG } from './utils';
|
|
5
|
+
import { ConfettiParticle } from './ConfettiParticle';
|
|
6
|
+
import type { ConfettiParticle as ConfettiParticleType } from './types';
|
|
7
|
+
|
|
8
|
+
export interface ConfettiCanvasProps {
|
|
9
|
+
/**
|
|
10
|
+
* Style for the confetti container
|
|
11
|
+
*/
|
|
12
|
+
containerStyle?: any;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Z-index for the confetti container
|
|
16
|
+
* @default 1000
|
|
17
|
+
*/
|
|
18
|
+
zIndex?: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether confetti should be allowed to go outside of safe area
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
fullScreen?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ConfettiCanvas = React.forwardRef<ConfettiMethods, ConfettiCanvasProps>(
|
|
28
|
+
({ containerStyle, zIndex = 1000, fullScreen = true }, ref) => {
|
|
29
|
+
const [particles, setParticles] = useState<ConfettiParticleType[]>([]);
|
|
30
|
+
const [activeCount, setActiveCount] = useState(0);
|
|
31
|
+
const { width, height } = useWindowDimensions();
|
|
32
|
+
|
|
33
|
+
const fire = useCallback(
|
|
34
|
+
(config: ConfettiConfig = {}): Promise<null> => {
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
const mergedConfig = {
|
|
37
|
+
...DEFAULT_CONFIG,
|
|
38
|
+
...config,
|
|
39
|
+
origin: {
|
|
40
|
+
...DEFAULT_CONFIG.origin,
|
|
41
|
+
...config.origin,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const newParticles = createConfettiParticles(mergedConfig, width, height);
|
|
46
|
+
setParticles(prev => [...prev, ...newParticles]);
|
|
47
|
+
setActiveCount(prev => prev + newParticles.length);
|
|
48
|
+
|
|
49
|
+
// Resolve after the duration
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
resolve(null);
|
|
52
|
+
}, mergedConfig.duration);
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
[width, height]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const reset = useCallback(() => {
|
|
59
|
+
setParticles([]);
|
|
60
|
+
setActiveCount(0);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
useImperativeHandle(
|
|
64
|
+
ref,
|
|
65
|
+
() => {
|
|
66
|
+
const confetti = fire as ConfettiMethods;
|
|
67
|
+
confetti.reset = reset;
|
|
68
|
+
return confetti;
|
|
69
|
+
},
|
|
70
|
+
[fire, reset]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const handleParticleComplete = useCallback((particleId: string) => {
|
|
74
|
+
setActiveCount(prev => prev - 1);
|
|
75
|
+
setParticles(prev => {
|
|
76
|
+
// Clean up completed particles periodically
|
|
77
|
+
if (prev.length > 100) {
|
|
78
|
+
return prev.filter(p => p.id !== particleId).slice(-50);
|
|
79
|
+
}
|
|
80
|
+
return prev;
|
|
81
|
+
});
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View
|
|
86
|
+
style={[
|
|
87
|
+
styles.container,
|
|
88
|
+
fullScreen && styles.fullScreen,
|
|
89
|
+
{ zIndex },
|
|
90
|
+
containerStyle,
|
|
91
|
+
]}
|
|
92
|
+
pointerEvents="none">
|
|
93
|
+
{particles.map(particle => (
|
|
94
|
+
<ConfettiParticle
|
|
95
|
+
key={particle.id}
|
|
96
|
+
particle={particle}
|
|
97
|
+
duration={DEFAULT_CONFIG.duration}
|
|
98
|
+
onComplete={() => handleParticleComplete(particle.id)}
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
ConfettiCanvas.displayName = 'ConfettiCanvas';
|
|
107
|
+
|
|
108
|
+
const styles = StyleSheet.create({
|
|
109
|
+
container: {
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
width: '100%',
|
|
112
|
+
height: '100%',
|
|
113
|
+
},
|
|
114
|
+
fullScreen: {
|
|
115
|
+
top: 0,
|
|
116
|
+
left: 0,
|
|
117
|
+
right: 0,
|
|
118
|
+
bottom: 0,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
Easing,
|
|
8
|
+
runOnJS,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
import type { ConfettiParticle as ConfettiParticleType } from './types';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
particle: ConfettiParticleType;
|
|
14
|
+
duration: number;
|
|
15
|
+
onComplete?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ConfettiParticle: React.FC<Props> = ({ particle, duration, onComplete }) => {
|
|
19
|
+
const translateX = useSharedValue(0);
|
|
20
|
+
const translateY = useSharedValue(0);
|
|
21
|
+
const rotation = useSharedValue(particle.rotation);
|
|
22
|
+
const opacity = useSharedValue(1);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Animate the particle
|
|
26
|
+
translateX.value = withTiming(particle.velocity.x * (duration / 16), {
|
|
27
|
+
duration,
|
|
28
|
+
easing: Easing.linear,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
translateY.value = withTiming(particle.velocity.y * (duration / 16), {
|
|
32
|
+
duration,
|
|
33
|
+
easing: Easing.bezier(0.33, 1, 0.68, 1), // Custom easing for gravity effect
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
rotation.value = withTiming(
|
|
37
|
+
particle.rotation + particle.rotationVelocity * (duration / 16),
|
|
38
|
+
{
|
|
39
|
+
duration,
|
|
40
|
+
easing: Easing.linear,
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
opacity.value = withTiming(
|
|
45
|
+
0,
|
|
46
|
+
{
|
|
47
|
+
duration,
|
|
48
|
+
easing: Easing.linear,
|
|
49
|
+
},
|
|
50
|
+
finished => {
|
|
51
|
+
if (finished && onComplete) {
|
|
52
|
+
runOnJS(onComplete)();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}, [duration, onComplete, opacity, particle, rotation, translateX, translateY]);
|
|
57
|
+
|
|
58
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
59
|
+
return {
|
|
60
|
+
transform: [
|
|
61
|
+
{ translateX: translateX.value },
|
|
62
|
+
{ translateY: translateY.value },
|
|
63
|
+
{ rotate: `${rotation.value}deg` },
|
|
64
|
+
],
|
|
65
|
+
opacity: opacity.value,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const renderShape = () => {
|
|
70
|
+
const baseStyle = [
|
|
71
|
+
styles.particle,
|
|
72
|
+
{
|
|
73
|
+
width: particle.size,
|
|
74
|
+
height: particle.size,
|
|
75
|
+
backgroundColor: particle.color,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (particle.shape === 'circle') {
|
|
80
|
+
return (
|
|
81
|
+
<Animated.View style={[...baseStyle, styles.circle, animatedStyle]} />
|
|
82
|
+
);
|
|
83
|
+
} else if (particle.shape === 'triangle') {
|
|
84
|
+
return (
|
|
85
|
+
<Animated.View style={[...baseStyle, styles.transparent, animatedStyle]}>
|
|
86
|
+
<Animated.View
|
|
87
|
+
style={[
|
|
88
|
+
styles.triangle,
|
|
89
|
+
{
|
|
90
|
+
borderLeftWidth: particle.size / 2,
|
|
91
|
+
borderRightWidth: particle.size / 2,
|
|
92
|
+
borderBottomWidth: particle.size,
|
|
93
|
+
borderBottomColor: particle.color,
|
|
94
|
+
},
|
|
95
|
+
]}
|
|
96
|
+
/>
|
|
97
|
+
</Animated.View>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Square (default)
|
|
102
|
+
return <Animated.View style={[...baseStyle, animatedStyle]} />;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Animated.View
|
|
107
|
+
style={[
|
|
108
|
+
styles.container,
|
|
109
|
+
{
|
|
110
|
+
left: particle.x,
|
|
111
|
+
top: particle.y,
|
|
112
|
+
},
|
|
113
|
+
]}>
|
|
114
|
+
{renderShape()}
|
|
115
|
+
</Animated.View>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const styles = StyleSheet.create({
|
|
120
|
+
container: {
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
},
|
|
123
|
+
particle: {
|
|
124
|
+
position: 'absolute',
|
|
125
|
+
},
|
|
126
|
+
circle: {
|
|
127
|
+
borderRadius: 999,
|
|
128
|
+
},
|
|
129
|
+
transparent: {
|
|
130
|
+
backgroundColor: 'transparent',
|
|
131
|
+
},
|
|
132
|
+
triangle: {
|
|
133
|
+
width: 0,
|
|
134
|
+
height: 0,
|
|
135
|
+
backgroundColor: 'transparent',
|
|
136
|
+
borderStyle: 'solid',
|
|
137
|
+
borderLeftColor: 'transparent',
|
|
138
|
+
borderRightColor: 'transparent',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ConfettiCanvas } from './ConfettiCanvas';
|
|
2
|
+
export { useConfetti } from './useConfetti';
|
|
3
|
+
export { presets } from './presets';
|
|
4
|
+
export type { ConfettiConfig, ConfettiMethods, ConfettiParticle } from './types';
|
|
5
|
+
export type { ConfettiCanvasProps } from './ConfettiCanvas';
|
|
6
|
+
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ConfettiConfig } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Predefined confetti presets for common use cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Basic celebration with confetti from the center
|
|
9
|
+
*/
|
|
10
|
+
export const celebration: ConfettiConfig = {
|
|
11
|
+
particleCount: 100,
|
|
12
|
+
spread: 70,
|
|
13
|
+
origin: { y: 0.6 },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fireworks effect
|
|
18
|
+
*/
|
|
19
|
+
export const fireworks: ConfettiConfig = {
|
|
20
|
+
particleCount: 150,
|
|
21
|
+
spread: 360,
|
|
22
|
+
startVelocity: 30,
|
|
23
|
+
decay: 0.94,
|
|
24
|
+
scalar: 1.2,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Confetti from the bottom
|
|
29
|
+
*/
|
|
30
|
+
export const bottomCannon: ConfettiConfig = {
|
|
31
|
+
particleCount: 50,
|
|
32
|
+
angle: 60,
|
|
33
|
+
spread: 55,
|
|
34
|
+
origin: { y: 0.8, x: 0.5 },
|
|
35
|
+
startVelocity: 55,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Confetti from left side
|
|
40
|
+
*/
|
|
41
|
+
export const leftCannon: ConfettiConfig = {
|
|
42
|
+
particleCount: 50,
|
|
43
|
+
angle: 45,
|
|
44
|
+
spread: 55,
|
|
45
|
+
origin: { x: 0, y: 0.6 },
|
|
46
|
+
startVelocity: 55,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Confetti from right side
|
|
51
|
+
*/
|
|
52
|
+
export const rightCannon: ConfettiConfig = {
|
|
53
|
+
particleCount: 50,
|
|
54
|
+
angle: 135,
|
|
55
|
+
spread: 55,
|
|
56
|
+
origin: { x: 1, y: 0.6 },
|
|
57
|
+
startVelocity: 55,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Realistic looking confetti
|
|
62
|
+
*/
|
|
63
|
+
export const realistic: ConfettiConfig = {
|
|
64
|
+
particleCount: 200,
|
|
65
|
+
spread: 160,
|
|
66
|
+
origin: { y: 0.5 },
|
|
67
|
+
startVelocity: 35,
|
|
68
|
+
gravity: 1.5,
|
|
69
|
+
drift: 1,
|
|
70
|
+
tilt: true,
|
|
71
|
+
shapes: ['square', 'circle', 'triangle'],
|
|
72
|
+
scalar: 0.8,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Snow effect
|
|
77
|
+
*/
|
|
78
|
+
export const snow: ConfettiConfig = {
|
|
79
|
+
particleCount: 100,
|
|
80
|
+
spread: 180,
|
|
81
|
+
origin: { y: -0.1 },
|
|
82
|
+
startVelocity: 0,
|
|
83
|
+
gravity: 0.3,
|
|
84
|
+
drift: 1,
|
|
85
|
+
colors: ['#ffffff', '#e8f4ff', '#c9e5ff'],
|
|
86
|
+
shapes: ['circle'],
|
|
87
|
+
scalar: 0.6,
|
|
88
|
+
angle: 270,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Stars effect
|
|
93
|
+
*/
|
|
94
|
+
export const stars: ConfettiConfig = {
|
|
95
|
+
particleCount: 50,
|
|
96
|
+
spread: 360,
|
|
97
|
+
startVelocity: 20,
|
|
98
|
+
decay: 0.95,
|
|
99
|
+
gravity: 0.5,
|
|
100
|
+
colors: ['#FFD700', '#FFA500', '#FFFF00'],
|
|
101
|
+
shapes: ['circle'],
|
|
102
|
+
scalar: 0.5,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* School pride (custom colors)
|
|
107
|
+
*/
|
|
108
|
+
export const schoolPride: ConfettiConfig = {
|
|
109
|
+
particleCount: 100,
|
|
110
|
+
spread: 160,
|
|
111
|
+
origin: { y: 0.6 },
|
|
112
|
+
colors: ['#bb0000', '#ffffff'],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const presets = {
|
|
116
|
+
celebration,
|
|
117
|
+
fireworks,
|
|
118
|
+
bottomCannon,
|
|
119
|
+
leftCannon,
|
|
120
|
+
rightCannon,
|
|
121
|
+
realistic,
|
|
122
|
+
snow,
|
|
123
|
+
stars,
|
|
124
|
+
schoolPride,
|
|
125
|
+
};
|
|
126
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export interface ConfettiConfig {
|
|
2
|
+
/**
|
|
3
|
+
* The number of confetti pieces to launch
|
|
4
|
+
* @default 50
|
|
5
|
+
*/
|
|
6
|
+
particleCount?: number;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The angle in degrees at which to launch the confetti
|
|
10
|
+
* @default 90
|
|
11
|
+
*/
|
|
12
|
+
angle?: number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How far the confetti can spread from the origin
|
|
16
|
+
* @default 45
|
|
17
|
+
*/
|
|
18
|
+
spread?: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The starting velocity of the confetti
|
|
22
|
+
* @default 45
|
|
23
|
+
*/
|
|
24
|
+
startVelocity?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How fast the confetti decays
|
|
28
|
+
* @default 0.9
|
|
29
|
+
*/
|
|
30
|
+
decay?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gravity to apply to the confetti
|
|
34
|
+
* @default 1
|
|
35
|
+
*/
|
|
36
|
+
gravity?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How much to the side the confetti will drift
|
|
40
|
+
* @default 0
|
|
41
|
+
*/
|
|
42
|
+
drift?: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Time in milliseconds to run the confetti animation
|
|
46
|
+
* @default 3000
|
|
47
|
+
*/
|
|
48
|
+
duration?: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Array of color strings, in any format (hex, rgb, hsl)
|
|
52
|
+
* @default ['#26ccff', '#a25afd', '#ff5e7e', '#88ff5a', '#fcff42', '#ffa62d', '#ff36ff']
|
|
53
|
+
*/
|
|
54
|
+
colors?: string[];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Scale of the confetti particles
|
|
58
|
+
* @default 1
|
|
59
|
+
*/
|
|
60
|
+
scalar?: number;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The x position on the screen where confetti will originate (0-1)
|
|
64
|
+
* 0 is left edge, 0.5 is center, 1 is right edge
|
|
65
|
+
* @default 0.5
|
|
66
|
+
*/
|
|
67
|
+
origin?: {
|
|
68
|
+
x?: number;
|
|
69
|
+
y?: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Shapes for confetti
|
|
74
|
+
* @default ['square', 'circle']
|
|
75
|
+
*/
|
|
76
|
+
shapes?: Array<'square' | 'circle' | 'triangle'>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether the confetti should be affected by tilt
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
tilt?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Max angle for tilt
|
|
86
|
+
* @default 10
|
|
87
|
+
*/
|
|
88
|
+
tiltAngleIncrement?: number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The number of times the confetti will move
|
|
92
|
+
* @default 200
|
|
93
|
+
*/
|
|
94
|
+
tickDuration?: number;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Disable physics
|
|
98
|
+
* @default false
|
|
99
|
+
*/
|
|
100
|
+
disableForReducedMotion?: boolean;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Use performance mode (fewer updates)
|
|
104
|
+
* @default false
|
|
105
|
+
*/
|
|
106
|
+
usePerformanceMode?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ConfettiParticle {
|
|
110
|
+
id: string;
|
|
111
|
+
color: string;
|
|
112
|
+
shape: 'square' | 'circle' | 'triangle';
|
|
113
|
+
x: number;
|
|
114
|
+
y: number;
|
|
115
|
+
size: number;
|
|
116
|
+
velocity: {
|
|
117
|
+
x: number;
|
|
118
|
+
y: number;
|
|
119
|
+
};
|
|
120
|
+
rotation: number;
|
|
121
|
+
rotationVelocity: number;
|
|
122
|
+
tiltAngle: number;
|
|
123
|
+
opacity: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ConfettiMethods {
|
|
127
|
+
/**
|
|
128
|
+
* Fire confetti with the given configuration
|
|
129
|
+
*/
|
|
130
|
+
(config?: ConfettiConfig): Promise<null>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reset confetti
|
|
134
|
+
*/
|
|
135
|
+
reset: () => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
import type { ConfettiConfig, ConfettiMethods } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to use confetti imperatively
|
|
6
|
+
* Returns a ref to pass to ConfettiCanvas and a fire function
|
|
7
|
+
*/
|
|
8
|
+
export const useConfetti = () => {
|
|
9
|
+
const confettiRef = useRef<ConfettiMethods>(null);
|
|
10
|
+
|
|
11
|
+
const fire = useCallback((config?: ConfettiConfig) => {
|
|
12
|
+
return confettiRef.current?.(config);
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const reset = useCallback(() => {
|
|
16
|
+
confettiRef.current?.reset();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
confettiRef,
|
|
21
|
+
fire,
|
|
22
|
+
reset,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ConfettiConfig, ConfettiParticle } from './types';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_COLORS = [
|
|
4
|
+
'#26ccff',
|
|
5
|
+
'#a25afd',
|
|
6
|
+
'#ff5e7e',
|
|
7
|
+
'#88ff5a',
|
|
8
|
+
'#fcff42',
|
|
9
|
+
'#ffa62d',
|
|
10
|
+
'#ff36ff',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_CONFIG: Required<ConfettiConfig> = {
|
|
14
|
+
particleCount: 50,
|
|
15
|
+
angle: 90,
|
|
16
|
+
spread: 45,
|
|
17
|
+
startVelocity: 45,
|
|
18
|
+
decay: 0.9,
|
|
19
|
+
gravity: 1,
|
|
20
|
+
drift: 0,
|
|
21
|
+
duration: 3000,
|
|
22
|
+
colors: DEFAULT_COLORS,
|
|
23
|
+
scalar: 1,
|
|
24
|
+
origin: { x: 0.5, y: 0.5 },
|
|
25
|
+
shapes: ['square', 'circle'],
|
|
26
|
+
tilt: true,
|
|
27
|
+
tiltAngleIncrement: 10,
|
|
28
|
+
tickDuration: 200,
|
|
29
|
+
disableForReducedMotion: false,
|
|
30
|
+
usePerformanceMode: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert degrees to radians
|
|
35
|
+
*/
|
|
36
|
+
export const degreesToRadians = (degrees: number): number => {
|
|
37
|
+
return (degrees * Math.PI) / 180;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate a random number between min and max
|
|
42
|
+
*/
|
|
43
|
+
export const randomRange = (min: number, max: number): number => {
|
|
44
|
+
return Math.random() * (max - min) + min;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pick a random item from an array
|
|
49
|
+
*/
|
|
50
|
+
export const randomFromArray = <T>(arr: T[]): T => {
|
|
51
|
+
const item = arr[Math.floor(Math.random() * arr.length)];
|
|
52
|
+
if (item === undefined) {
|
|
53
|
+
throw new Error('Array is empty');
|
|
54
|
+
}
|
|
55
|
+
return item;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create initial confetti particles
|
|
60
|
+
*/
|
|
61
|
+
export const createConfettiParticles = (
|
|
62
|
+
config: Required<ConfettiConfig>,
|
|
63
|
+
screenWidth: number,
|
|
64
|
+
screenHeight: number
|
|
65
|
+
): ConfettiParticle[] => {
|
|
66
|
+
const particles: ConfettiParticle[] = [];
|
|
67
|
+
const angleInRadians = degreesToRadians(config.angle);
|
|
68
|
+
const spreadInRadians = degreesToRadians(config.spread);
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < config.particleCount; i++) {
|
|
71
|
+
const angle = angleInRadians + randomRange(-spreadInRadians / 2, spreadInRadians / 2);
|
|
72
|
+
const velocity = config.startVelocity * (0.5 + Math.random() * 0.5);
|
|
73
|
+
|
|
74
|
+
const particle: ConfettiParticle = {
|
|
75
|
+
id: `confetti-${i}-${Date.now()}`,
|
|
76
|
+
color: randomFromArray(config.colors),
|
|
77
|
+
shape: randomFromArray(config.shapes),
|
|
78
|
+
x: (config.origin.x ?? 0.5) * screenWidth,
|
|
79
|
+
y: (config.origin.y ?? 0.5) * screenHeight,
|
|
80
|
+
size: (5 + Math.random() * 5) * config.scalar,
|
|
81
|
+
velocity: {
|
|
82
|
+
x: Math.cos(angle) * velocity,
|
|
83
|
+
y: Math.sin(angle) * velocity,
|
|
84
|
+
},
|
|
85
|
+
rotation: Math.random() * 360,
|
|
86
|
+
rotationVelocity: randomRange(-10, 10),
|
|
87
|
+
tiltAngle: config.tilt ? Math.random() * config.tiltAngleIncrement : 0,
|
|
88
|
+
opacity: 1,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
particles.push(particle);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return particles;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update a confetti particle's position
|
|
99
|
+
*/
|
|
100
|
+
export const updateParticle = (
|
|
101
|
+
particle: ConfettiParticle,
|
|
102
|
+
config: Required<ConfettiConfig>,
|
|
103
|
+
deltaTime: number
|
|
104
|
+
): ConfettiParticle => {
|
|
105
|
+
const dt = deltaTime / 16; // Normalize to 60fps
|
|
106
|
+
|
|
107
|
+
// Apply gravity
|
|
108
|
+
const newVelocityY = particle.velocity.y - config.gravity * dt;
|
|
109
|
+
|
|
110
|
+
// Apply drift
|
|
111
|
+
const newVelocityX = particle.velocity.x + config.drift * dt;
|
|
112
|
+
|
|
113
|
+
// Update position
|
|
114
|
+
const newX = particle.x + newVelocityX * dt;
|
|
115
|
+
const newY = particle.y - newVelocityY * dt;
|
|
116
|
+
|
|
117
|
+
// Update rotation
|
|
118
|
+
const newRotation = particle.rotation + particle.rotationVelocity * dt;
|
|
119
|
+
|
|
120
|
+
// Apply decay to opacity
|
|
121
|
+
const newOpacity = particle.opacity * Math.pow(config.decay, dt);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...particle,
|
|
125
|
+
x: newX,
|
|
126
|
+
y: newY,
|
|
127
|
+
velocity: {
|
|
128
|
+
x: newVelocityX,
|
|
129
|
+
y: newVelocityY,
|
|
130
|
+
},
|
|
131
|
+
rotation: newRotation,
|
|
132
|
+
opacity: newOpacity,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|