Created
April 6, 2020 17:11
-
-
Save ShopifyEng/96b5f2f55b274dab3a957bb12c3f2c4d to your computer and use it in GitHub Desktop.
Building Arrive's Confetti in React Native with Reanimated - Confetti Final
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {useMemo} from 'react' | |
import Animated from 'react-native-reanimated' | |
import {View, Dimensions, StyleSheet} from 'react-native' | |
import FastImage from 'react-native-fast-image' | |
import ConfettiImage from 'assets/images/confetti.png' | |
const NUM_CONFETTI = 100 | |
const COLORS = ['#00e4b2', '#09aec5', '#107ed5'] | |
const CONFETTI_SIZE = 16 | |
const createConfetti = () => { | |
const {width: screenWidth} = Dimensions.get('screen') | |
return [...new Array(NUM_CONFETTI)].map((_, i) => { | |
const clock = new Animated.Clock() | |
return { | |
key: i, | |
// Spawn confetti from two different sources, a quarter | |
// from the left and a quarter from the right edge of the screen. | |
x: new Animated.Value( | |
screenWidth * (i % 2 ? 0.25 : 0.75) - CONFETTI_SIZE / 2 | |
), | |
y: new Animated.Value(-60), | |
angle: new Animated.Value(0), | |
xVel: new Animated.Value(Math.random() * 400 - 200), | |
yVel: new Animated.Value(Math.random() * 150 + 150), | |
angleVel: new Animated.Value((Math.random() * 3 - 1.5) * Math.PI), | |
delay: new Animated.Value(Math.floor(i / 10) * 0.3), | |
elasticity: Math.random() * 0.3 + 0.1, | |
color: COLORS[i % COLORS.length], | |
clock, | |
} | |
}) | |
} | |
const Confetti = () => { | |
const confetti = useMemo(createConfetti, []) | |
return ( | |
<View pointerEvents="none" style={StyleSheet.absoluteFill}> | |
{confetti.map( | |
({ | |
key, | |
x, | |
y, | |
angle, | |
xVel, | |
yVel, | |
angleVel, | |
color, | |
elasticity, | |
delay, | |
clock, | |
}) => { | |
return ( | |
<React.Fragment key={key}> | |
<Animated.Code> | |
{() => { | |
const { | |
startClock, | |
set, | |
add, | |
sub, | |
divide, | |
diff, | |
multiply, | |
cond, | |
clockRunning, | |
greaterThan, | |
lessThan, | |
} = Animated | |
const {width: screenWidth} = Dimensions.get('window') | |
const timeDiff = diff(clock) | |
const dt = divide(timeDiff, 1000) | |
const dy = multiply(dt, yVel) | |
const dx = multiply(dt, xVel) | |
const dAngle = multiply(dt, angleVel) | |
return cond( | |
clockRunning(clock), | |
[ | |
cond( | |
greaterThan(delay, 0), | |
[set(delay, sub(delay, dt))], | |
[ | |
set(y, add(y, dy)), | |
set(x, add(x, dx)), | |
set(angle, add(angle, dAngle)), | |
] | |
), | |
cond(greaterThan(x, screenWidth - CONFETTI_SIZE), [ | |
set(x, screenWidth - CONFETTI_SIZE), | |
set(xVel, multiply(xVel, -elasticity)), | |
]), | |
cond(lessThan(x, 0), [ | |
set(x, 0), | |
set(xVel, multiply(xVel, -elasticity)), | |
]), | |
], | |
[startClock(clock), timeDiff] | |
) | |
}} | |
</Animated.Code> | |
<Animated.View | |
style={[ | |
styles.confettiContainer, | |
{ | |
transform: [ | |
{translateX: x}, | |
{translateY: y}, | |
{rotate: angle}, | |
{rotateX: angle}, | |
{rotateY: angle}, | |
], | |
}, | |
]} | |
> | |
<FastImage | |
tintColor={color} | |
source={ConfettiImage} | |
style={styles.confetti} | |
/> | |
</Animated.View> | |
</React.Fragment> | |
) | |
} | |
)} | |
</View> | |
) | |
} | |
const styles = StyleSheet.create({ | |
confettiContainer: { | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
}, | |
confetti: { | |
width: CONFETTI_SIZE, | |
height: CONFETTI_SIZE, | |
}, | |
}) | |
export default Confetti |
@peralmq
I also get the "excessive number of pending callbacks" error. Would you mind sharing how you used the useCode
hook? The Reanimated documentation is very sparse on that.
Here's the adaptation in an NPM package:
https://www.npmjs.com/package/react-native-make-it-rain
This was huge! Thanks so much. For those interested, I've ported this gist over to Reanimate v2. You can find it here: https://gist.github.com/imcrainjames/e86893a1d6f85328174d036a9b263dd0
Also a shout to @wcandillon. His excellent content helped me figure this out.
Can you add a link to the Confetti image
Wow awesome, did anyone by any chance do the porting to the new version of reanimated 3?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for a great guide!
We used this to build a nice confetti based on an SVG logo but ran into some performance problems (similar to software-mansion/react-native-reanimated#381) which we solved by moving the
Animated.Code
code into theAnimated.useCode
hook (and make it dependent on theconfetti
list).Might make sense for you too? Since you're already have a functional component that is using hooks like the
useMemo
one.