How to make a confetti cannon with React Spring
Brian Neville-O'Neill
Posted on January 9, 2020
Written by Joshua Saunders✏️
You know what everybody loves in their daily lives? A little validation, a little pat on the back, a little celebration — and a little confetti.
In this tutorial, you’ll learn how to implement a confetti cannon that can fire off of any element using React Spring from scratch. No previous React Spring experience required! The only prerequisite is a basic understanding of React and hooks.
If you want to jump ahead, you can skip to the completed CodeSandbox example.
Note: this tutorial uses styled-components
. If you’ve never used styled-components
before, don’t sweat it. It’s a powerful library for inline styling of React components, but it’s very readable, so you’ll get the gist of it just by looking at the code.
Game plan
When I’m starting to implement something I’ve never seen before, I like to break it down into phases, starting with the core pieces, then polish afterward. We’ll attack this project step by step:
- Get something showing up on the page
- Set up React Spring
- Write some basic psuedo-physics helpers
- Anchor a single dot
- Get many dots moving like they’re being fired from a confetti cannon
- Add variation to confetti pieces, such as different shapes, colors, and sizes
Let’s get started!
1. Something on the page
First, let’s make up a little app. We’ll make it a to-do app and set it to fire confetti from the checkbox when you complete an item.
Now, let’s add a single confetti dot, which we’ll play with for the next few steps of this tutorial.
const StyledConfettiDot = styled.svg`
position: absolute;
will-change: transform;
`;
const Dot = () => (
<StyledConfettiDot>
<circle cx="5" cy="5" r="5" fill="blue" />
</StyledConfettiDot>
);
2. React Spring setup
React Spring is the animation library we’ll be using in this tutorial. It’s a unique library that takes the stance that animations powered by springs rather than keyframes look more natural. Instead of specifying how long an animation is and what changes occur at what time, you specify the tension, friction, and mass of the spring, as well as the start and end values of the animation, and let React Spring figure out how they relate to the spring.
Let’s get React Spring set up with our confetti dot. Run either of the following.
npm install react-spring
yarn add react-spring
Add the following import to ConfettiDot.js.
import { animated, config, useSpring } from 'react-spring';
-
animated
is used to wrap existing components to allow them to usereact-spring
-
config
s are the preset spring configs that ship withreact-spring
(we’ll be using thedefault
config) -
useSpring
is one of the main exports fromreact-spring
(there is a handful of other exports, but we’ll be focusing onuseSpring
)
ConfettiDot
enabled with react-spring
looks like this:
const AnimatedConfettiDot = animated(StyledConfettiDot);
const Dot = () => {
const { y } = useSpring({
config: config.default,
from: { y: 0 },
to: { y: -50 }
});
return (
<AnimatedConfettiDot
style={{
transform: y.interpolate(yValue => `translate3d(0,${yValue}px,0)`)
}}
>
<circle cx="5" cy="5" r="5" fill="blue" />
</AnimatedConfettiDot>
);
};
We’ve used animated
to wrap our StyledConfettiDot
component. All we have to do is call animated(<component>)
.
useSpring
takes an object with various properties. First, a config
object — we’ll use the default
one shipped with react-spring
since it has no bounceback. Next, a from
object that states arbitrary initial values, followed by a to
object that states matching end values. The whole hook returns an object that matches the from
and to
objects. In this example, we’ve set a y
initial and end value, and we’re destructing the result to get the y
animated value.
Instead of using ConfettiDot
or StyledConfettiDot
in the render, we’re now using AnimatedConfettiDot
, the result of the animated
call.
In the style
attribute of AnimatedConfettiDot
, we use the result of the objects in useSpring
to turn the values into valid style values.
Let’s break down the style
attribute in more detail. Firstly, we’re using the style
attribute instead of props because when the values change, since it’s using animated
, it’ll just change the DOM element’s style values as opposed to causing a rerender in React. That means you can have complex animations fully on only one render. Without this, performance would be extremely slow.
Secondly, we’re using the interpolate
function on y
to convert it into a real string value. For values that are already equal to their final style value, such as a color or percentage value, you wouldn’t need to use interpolate
. We’ll demonstrate this later on.
3. Pseudo-physics
While a circle moving upward is pretty fun, we want it to look like it’s firing out of a confetti cannon. To accomplish this, we’re going to make some pseudo-physics.
- When the confetti fires out of the cannon, it has a high velocity
- The confetti slows down quickly
- Eventually, gravity overtakes its velocity and it begins to fall back down
We’ll use react-spring
to simulate the confetti’s velocity at time t. Let’s make a spring that goes from 100 to 0.
const { upwards } = useSpring({
config: config.default,
from: { upwards: 100 },
to: { upwards: 0 },
});
Let’s pretend this velocity represents pixels per second — so, starting at 100 pixels per second to 0 pixels per second.
To actually use this to move the confetti dot, we’ll do the following.
const initialY = 0;
let totalUpwards = 0;
const startTime = new Date().getTime() / 1000;
let lastTime = startTime;
return (
<AnimatedConfettiDot
style={{
transform: upwards.interpolate(upwardsValue => {
const currentTime = new Date().getTime() / 1000;
const duration = currentTime - lastTime;
const verticalTraveled = upwardsValue * duration;
totalUpwards += verticalTraveled;
lastTime = currentTime;
return `translate3d(0, ${initialY - totalUpwards}px, 0)`;
})
}}
>
<circle cx="5" cy="5" r="5" fill="blue" />
</AnimatedConfettiDot>
);
This is a fun trick. Since interpolate
is called on every tick of react-spring
, we’re calculating the time between the current tick and the last tick, getting the current velocity, and calculating the distance traveled (velocity * duration since last tick), then adding that to the total distance traveled in totalUpwards
. Then we use totalUpwards
as the resulting translated value (using subtraction, since positive upward movement is negative y
axis movement in the DOM).
It’s looking great so far! We’ve successfully translated velocity into a translate
value. What’s still missing, though, is constant gravity. In terms of physics, that’s easy to implement, since gravity at time t
is just t * total time
.
const initialY = 0;
let totalUpwards = 0;
const startTime = new Date().getTime() / 1000;
let lastTime = startTime;
const gravityPerSecond = 30;
return (
<AnimatedConfettiDot
style={{
transform: upwards.interpolate(upwardsValue => {
const currentTime = new Date().getTime() / 1000;
const duration = currentTime - lastTime;
const verticalTraveled = upwardsValue * duration;
const totalDuration = currentTime - startTime;
totalUpwards += verticalTraveled;
lastTime = currentTime;
const totalGravity = gravityPerSecond * totalDuration;
const finalY = initialY - totalUpwards + totalGravity;
return `translate3d(0, ${finalY}px, 0)`;
})
}}
>
<circle cx="5" cy="5" r="5" fill="blue" />
</AnimatedConfettiDot>
);
};
Changing the initial upward velocity to 300 results in the following.
Let’s add horizontal movement as well. It’s a similar mechanism, so I’ll cut to the chase.
const { horizontal, upwards } = useSpring({
config: config.default,
from: {
horizontal: 200,
upwards: 300
},
to: {
horizontal: 0,
upwards: 0
}
});
const initialX = 0;
const initialY = 0;
let totalUpwards = 0;
let totalHorizontal = 0;
const startTime = new Date().getTime() / 1000;
let lastTime = startTime;
const gravityPerSecond = 30;
return (
<AnimatedConfettiDot
style={{
transform: interpolate([upwards, horizontal], (v, h) => {
const currentTime = new Date().getTime() / 1000;
const duration = currentTime - lastTime;
const totalDuration = currentTime - startTime;
const verticalTraveled = v * duration;
const horizontalTraveled = h * duration;
totalUpwards += verticalTraveled;
totalHorizontal += horizontalTraveled;
lastTime = currentTime;
const totalGravity = gravityPerSecond * totalDuration;
const finalX = initialX + totalHorizontal;
const finalY = initialY - totalUpwards + totalGravity;
return `translate3d(${finalX}px, ${finalY}px, 0)`;
})
}}
>
<circle cx="5" cy="5" r="5" fill="blue" />
</AnimatedConfettiDot>
);
Similar to the upward velocity, we’ve added a horizontal velocity spring in existing from
and to
values and calculated the horizontal distance traveled for each tick of the spring.
The one new thing is that we’re not just interpolating one value anymore, so we need to use the interpolate
function exported from react-spring
. This function’s first argument is an array of springs, and the second argument is a function that does something with each of the spring values in that array. So in this particular example, the first argument is a list of the upward and horizontal velocity, and the second argument is a function that has upward velocity as its first argument and horizontal velocity as its second argument.
4. Anchoring
Before we start making many pieces of confetti go flying, let’s make this single piece actually look like it’s coming out of a specific element.
The first step is to make the confetti appear when the checkbox is clicked.
const ToDo = ({ text }) => {
const [done, setDone] = useState(false);
return (
<StyledToDo>
<input type="checkbox" onChange={() => setDone(!done)} />
<span>
{text} {done ? ":ok_hand:" : ""}
</span>
{done && <ConfettiDot />}
</StyledToDo>
);
};
In each ToDo
component, when the done
state is true, render a ConfettiDot
.
It looks like it’s aligned with the checkbox, but if you look closely, you might notice that the animation starts at the top-left of the checkbox. It looks okay, but if it were a different element, such as a text box input, this would look pretty strange.
We’ll use ref
s to align the animation with the checkbox.
const alignWithAnchor = anchorRef => {
if (anchorRef.current == null) {
return {
initialX: 0,
initialY: 0
};
}
const { height, width } = anchorRef.current.getBoundingClientRect();
return {
initialX: width / 2,
initialY: height / 2
};
};
const Dot = ({ anchorRef }) => {
const { initialX, initialY } = alignWithAnchor(anchorRef);
// ...
}
const ToDo = ({ text }) => {
const confettiAnchorRef = useRef();
const [done, setDone] = useState(false);
return (
<StyledToDo>
<input
ref={confettiAnchorRef}
type="checkbox"
onChange={() => setDone(!done)}
/>
<span>
{text} {done ? ":ok_hand:" : ""}
</span>
{done && <ConfettiDot anchorRef={confettiAnchorRef} />}
</StyledToDo>
);
};
To use the ref
, follow these steps:
- In
ToDo
, calluseRef()
- Attach the resulting
ref
to theinput
by usingref={confettiAnchorRef}
(now the ref will contain the DOM element of theinput
) - Pass the ref to
ConfettiDot
- In
ConfettiDot
, access theref
and pass it to a helper - In the helper, calculate the middle of the
ref
element
Now the animation is a little cleaned up.
5. Making the cannon
Now that we’ve got a single confetti dot moving the way we want it to when we want it to, let’s make it a confetti cannon that sprays a randomized fan of confetti. We want our confetti cannon component to:
- Have an anchor
ref
prop for alignment - Have a vertical range
- Have a horizontal range
- Fire a certain number of confetti dots
const ToDo = ({ text }) => {
const confettiAnchorRef = useRef();
const [done, setDone] = useState(false);
return (
// ...
{done && }
);
};const ConfettiCannon = ({ anchorRef, dotCount }) => (
<>
{new Array(dotCount).fill().map((_, index) => ())}
</>
);
It doesn’t look too different, does it? Even though we’re rendering five confetti dots, they all have identical animations, since the confetti dots have their upward and horizontal movement props baked in. Let’s extract those and randomize them within a range.
const randomInRange = (min, max) => {
return Math.random() * (max - min) + min;
};
const ConfettiCannon = ({ anchorRef, dotCount }) => (
<>
{new Array(dotCount).fill().map((_, index) => (
<ConfettiDot
key={index}
anchorRef={anchorRef}
initialHorizontal={randomInRange(-250, 250)}
initialUpwards={randomInRange(200, 700)}
/>
))}
</>
);
const Dot = ({ anchorRef, initialHorizontal, initialUpwards }) => {
const { initialX, initialY } = alignWithAnchor(anchorRef);
const { horizontal, upwards } = useSpring({
config: config.default,
from: {
horizontal: initialHorizontal,
upwards: initialUpwards
},
to: {
horizontal: 0,
upwards: 0
}
});
// ...
}
Now instead of having a baked-in initial horizontal and upward velocity, we’ll randomize each dot. Horizontal velocity goes from -250 to 250 to represent dots flying both left of the anchor and right of the anchor, and the upward velocity goes from 200 to 700. Feel free to play around with these values.
6. Polish
At this point, we’ve done all the hard work required for this project. To polish it off, we’ll do the following.
- Fade out the confetti as it falls
- Randomize colors
- Randomize shapes
- Randomize sizes
Let’s break this down step by step.
Fade out
The confetti should disappear as it nears the end of its animation. To accomplish this, all we need to do is add the following in ConfettiDot
.
const Dot = ({ anchorRef, initialHorizontal, initialUpwards }) => {
const { initialX, initialY } = alignWithAnchor(anchorRef);
const { horizontal, opacity, upwards } = useSpring({
config: config.default,
from: {
horizontal: initialHorizontal,
opacity: 80,
upwards: initialUpwards
},
to: {
horizontal: 0,
opacity: 0,
upwards: 0
}
});
// ...
return (
<AnimatedConfettiDot
style={{
opacity,
transform: interpolate([upwards, horizontal], (v, h) => {
// ...
})
}}
>
<circle cx="5" cy="5" r="5" fill="blue" />
</AnimatedConfettiDot>
);
}
Since opacity actually returns a number, and that’s what the valid style
value is, we don’t need to interpolate it. We can drop it right into the style
attribute of AnimatedConfettiDot
.
Randomize colors
Blue is fine, but of course, more variance is better. Let’s add a color
prop to ConfettiDot
, add a colors
prop to ConfettiCannon
, and randomly pick colors from there to assign to created ConfettiDot
s.
const Dot = ({ anchorRef, color, initialHorizontal, initialUpwards }) => {
// ...
return (
<AnimatedConfettiDot
// ...
>
<circle cx="5" cy="5" r="5" fill={color} />
</AnimatedConfettiDot>
);
}
const randomInRange = (min, max) => {
return Math.random() * (max - min) + min;
};
const randomIntInRange = (min, max) => Math.floor(randomInRange(min, max));
const ConfettiCannon = ({ anchorRef, colors, dotCount }) => (
<>
{new Array(dotCount).fill().map((_, index) => (
<ConfettiDot
key={index}
anchorRef={anchorRef}
color={colors[randomIntInRange(0, colors.length)]}
initialHorizontal={randomInRange(-250, 250)}
initialUpwards={randomInRange(200, 700)}
/>
))}
</>
);
This can be especially useful if you want to stylize your confetti in the brand colors of the app using this library.
Randomize shapes
Circles are also fine, but they don’t look like the most convincing confetti pieces in the world. Let’s randomly make squares and triangles as well.
const Circle = ({ color, size }) => (
<circle
cx={`${size / 2}`}
cy={`${size / 2}`}
r={`${(size / 2) * 0.6}`}
fill={color}
/>
);
const Triangle = ({ color, size }) => {
const flipped = flipCoin();
return (
<polygon
points={`${size / 2},0 ${size},${randomInRange(
flipped ? size / 2 : 0,
size
)} 0,${randomInRange(flipped ? 0 : size / 2, size)}`}
fill={color}
/>
);
};
const Square = ({ color, size }) => {
const flipped = flipCoin();
return (
<rect
height={`${randomInRange(0, flipped ? size : size / 2)}`}
width={`${randomInRange(0, flipped ? size / 2 : size)}`}
fill={color}
/>
);
};
const getRandomShape = color => {
const Shape = randomFromArray([Circle, Square, Triangle]);
return <Shape color={color} size={10} />;
};
return (
<AnimatedConfettiDot
// ...
>
{getRandomShape(color)}
</AnimatedConfettiDot>
);
Now we’ll randomly get a triangle, square, or circle. The triangle and square have some extra code in them to make sure you never end up with a square that’s just a line or a triangle that’s just a line. I’ve left out the code for flipCoin
and randomFromArray
from this snippet, but it’s in the CodeSandbox.
One last thing that’d be nice to polish: as of now, there’s no rotation, which makes it so that each triangle has a point facing directly up, and each rectangle is either fully vertical or fully horizontal. Let’s fix that.
const ConfettiCannon = ({ anchorRef, colors, dotCount }) => (
<>
{new Array(dotCount).fill().map((_, index) => (
<ConfettiDot
key={index}
anchorRef={anchorRef}
color={colors[randomIntInRange(0, colors.length)]}
initialHorizontal={randomInRange(-250, 250)}
initialUpwards={randomInRange(200, 700)}
rotate={randomInRange(0, 360)}
/>
))}
</>
);
const Dot = ({
anchorRef,
color,
initialHorizontal,
initialUpwards,
rotate
}) => {
// ...
return (
<AnimatedConfettiDot
style={{
opacity,
transform: interpolate([upwards, horizontal], (v, h) => {
// ...
return `translate3d(${finalX}px, ${finalY}px, 0) rotate(${rotate}deg)`;
})
}}
>
{getRandomShape(color)}
</AnimatedConfettiDot>
);
};
Randomize size
The last aspect to randomize is the size of each dot. Currently, all the dots are the same size, and it’s especially obvious with the circles. Let’s use a similar approach as we did for rotation.
const getRandomShape = (color, size) => {
const Shape = randomFromArray([Circle, Square, Triangle]);
return <Shape color={color} size={size} />;
};
const Dot = ({
anchorRef,
color,
initialHorizontal,
initialUpwards,
rotate,
size
}) => {
// ...
return (
<AnimatedConfettiDot
// ...
>
{getRandomShape(color, size)}
</AnimatedConfettiDot>
);
};
const ConfettiCannon = ({ anchorRef, colors, dotCount }) => (
<>
{new Array(dotCount).fill().map((_, index) => (
<ConfettiDot
key={index}
anchorRef={anchorRef}
color={colors[randomIntInRange(0, colors.length)]}
initialHorizontal={randomInRange(-250, 250)}
initialUpwards={randomInRange(200, 700)}
rotate={randomInRange(0, 360)}
size={randomInRange(8, 12)}
/>
))}
</>
);
Conclusion
Congratulations! You’ve made confetti from scratch using React and React Spring. Now you should be much more familiar with using React Spring’s useSpring
hook to create powerful and performant animations.
I’ll leave you with these branded confetti cannons!
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
The post How to make a confetti cannon with React Spring appeared first on LogRocket Blog.
Posted on January 9, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 12, 2024