William Kurniawan
Aug 21, 2020
stripe-bubbles
.mkdir stripe-bubbles
cd stripe-bubbles
yarn add -E next react react-dom
yarn add -DE @types/node @types/react typescript
package.json
."scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
div
element with equal height and width and border-radius: 50%
. Let's create a new directory called styles
and add a css
file called main.css
. And, copy and paste the following code to main.css
..bubble {
background-color: white;
border-radius: 50%;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1), 0 3px 10px rgba(0, 0, 0, 0.1);
height: 152px;
margin-right: 20px;
width: 152px;
}
import
beyond JavaScript. To add a global stylesheet to your application, import the CSS file within pages/_app.tsx
. Create a pages/_app.tsx
file and import the main.css
file.import React from 'react';
// Modules
import { AppProps } from 'next/app';
// CSS
import '../styles/main.css';
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};
export default MyApp;
pages/index.tsx
file and add a div
to display the bubble.import React from 'react';
// Modules
import { NextPage } from 'next/types';
const HomePage: NextPage = () => {
return <div className="bubble" />;
};
export default HomePage;
yarn dev
in your terminal.public/images
directory.background-size
CSS property such that one logo in the image has the same size as one bubble. Update your .bubble
class with the following code.background-image: url(/images/spritesheet.png);
background-size: 1076px 1076px;
background-position
CSS property to shift the image's position in each bubble and reveal different logos..company-1 {
background-position: -154px 0;
}
.company-2 {
background-position: -308px 0;
}
.company-3 {
background-position: -462px 0;
}
pages/index.tsx
file to display more logos.import React from 'react';
// Modules
import { NextPage } from 'next/types';
const HomePage: NextPage = () => {
return (
<div>
<div className="bubble company-1" />
<div className="bubble company-2" />
<div className="bubble company-3" />
</div>
);
};
export default HomePage;
styles/main.css
file to display better UI.html,
body,
#__next {
height: 100%;
margin: 0;
width: 100%;
}
.bubble {
background-color: white;
background-image: url(/images/spritesheet.png);
background-size: 1076px 1076px;
border-radius: 50%;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1), 0 3px 10px rgba(0, 0, 0, 0.1);
height: 152px;
margin-right: 20px;
position: absolute;
transition: opacity ease-in-out 1s;
width: 152px;
}
.bubbles {
height: 600px;
overflow: hidden;
position: relative;
}
.bubbles-wrapper {
background-color: salmon;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
pages/index.tsx
file and use it to generate all the bubbles on the fly....
const bubbles = [
{
s: 0.6,
x: 1134,
y: 45,
},
{
s: 0.6,
x: 1620,
y: 271,
},
{
s: 0.6,
x: 1761,
y: 372,
},
{
s: 0.6,
x: 2499,
y: 79,
},
{
s: 0.8,
x: 2704,
y: 334,
},
{
s: 0.6,
x: 2271,
y: 356,
},
{
s: 0.6,
x: 795,
y: 226,
},
{
s: 0.6,
x: 276,
y: 256,
},
{
s: 0.6,
x: 1210,
y: 365,
},
{
s: 0.6,
x: 444,
y: 193,
},
{
s: 0.6,
x: 2545,
y: 387,
},
{
s: 0.8,
x: 1303,
y: 193,
},
{
s: 0.8,
x: 907,
y: 88,
},
{
s: 0.8,
x: 633,
y: 320,
},
{
s: 0.8,
x: 323,
y: 60,
},
{
s: 0.8,
x: 129,
y: 357,
},
{
s: 0.8,
x: 1440,
y: 342,
},
{
s: 0.8,
x: 1929,
y: 293,
},
{
s: 0.8,
x: 2135,
y: 198,
},
{
s: 0.8,
x: 2276,
y: 82,
},
{
s: 0.8,
x: 2654,
y: 182,
},
{
s: 0.8,
x: 2783,
y: 60,
},
{
s: 1.0,
x: 1519,
y: 118,
},
{
s: 1.0,
x: 1071,
y: 233,
},
{
s: 1.0,
x: 1773,
y: 148,
},
{
s: 1.0,
x: 2098,
y: 385,
},
{
s: 1.0,
x: 2423,
y: 244,
},
{
s: 1.0,
x: 901,
y: 385,
},
{
s: 1.0,
x: 624,
y: 111,
},
{
s: 1.0,
x: 75,
y: 103,
},
{
s: 1.0,
x: 413,
y: 367,
},
{
s: 1.0,
x: 2895,
y: 271,
},
{
s: 1.0,
x: 1990,
y: 75,
},
];
const backgroundPositions: string[] = [];
for (let i = 0; i < 7; i++) {
for (let j = 0; j < 7; j++) {
backgroundPositions.push(`${-154 * j}px ${-154 * i}px`);
}
}
const HomePage: NextPage = () => {
return (
<div className="bubbles-wrapper">
<div className="bubbles">
{bubbles.map((bubble, index) => (
<div
className="bubble"
id={`bubble-${index}`}
key={`${bubble.x} ${bubble.y}`}
style={{
backgroundPosition: backgroundPositions[index],
transform: `translate(${bubble.x}px, ${bubble.y}px) scale(${bubble.s})`,
}}
/>
))}
</div>
</div>
);
};
...
yarn dev
in your terminal.noisejs
. Let's add noisejs
into our project dependencies by running the following command.yarn add -E noisejs
noisejs
into our pages/index.tsx
file....
import { Noise } from 'noisejs';
...
Math.random()
produces random values that have no relationship with the previously generated values. Perlin noise, on the other hand, allows us to create a sequence of "random" values that create a smooth and organic experience. As the main focus of this tutorial is to clone logo bubbles from Stripe, I will keep the explanation about perlin noise simple. If you'd like to learn more about perlin noise, you can read chapter 1.6 of The Nature of Code.pages/index.tsx
file....
const CANVAS_WIDTH = 3000;
// The amplitude. The amount the noise affects the movement.
const NOISE_AMOUNT = 5;
// The frequency. Smaller for flat slopes, higher for jagged spikes.
const NOISE_SPEED = 0.004;
// Pixels to move per frame. At 60fps, this would be 18px a sec.
const SCROLL_SPEED = 0.3;
const bubbles = [
...
...
const noise = new Noise();
const HomePage: NextPage = () => {
...
const HomePage: NextPage = () => {
const animationRef = React.useRef<number>();
const bubblesRef = React.useRef(
bubbles.map((bubble) => ({
...bubble,
noiseSeedX: Math.floor(Math.random() * 64000),
noiseSeedY: Math.floor(Math.random() * 64000),
xWithNoise: bubble.x,
yWithNoise: bubble.y,
})),
);
const [isReady, setReady] = React.useState(false);
React.useEffect(() => {
setTimeout(() => {
setReady(true);
}, 200);
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
function animate() {
bubblesRef.current = bubblesRef.current.map((bubble, index) => {
const newNoiseSeedX = bubble.noiseSeedX + NOISE_SPEED;
const newNoiseSeedY = bubble.noiseSeedY + NOISE_SPEED;
const randomX = noise.simplex2(newNoiseSeedX, 0);
const randomY = noise.simplex2(newNoiseSeedY, 0);
const newX = bubble.x - SCROLL_SPEED;
const newXWithNoise = newX + randomX * NOISE_AMOUNT;
const newYWithNoise = bubble.y + randomY * NOISE_AMOUNT;
const element = document.getElementById(`bubble-${index}`);
if (element) {
element.style.transform = `translate(${newXWithNoise}px, ${newYWithNoise}px) scale(${bubble.s})`;
}
return {
...bubble,
noiseSeedX: newNoiseSeedX,
noiseSeedY: newNoiseSeedY,
x: newX < -200 ? CANVAS_WIDTH : newX,
xWithNoise: newXWithNoise,
yWithNoise: newYWithNoise,
};
});
animationRef.current = requestAnimationFrame(animate);
}
return (
<div className="bubbles-wrapper">
<div className="bubbles">
{bubbles.map((bubble, index) => (
<div
className="bubble"
id={`bubble-${index}`}
key={`${bubble.x} ${bubble.y}`}
style={{
backgroundPosition: backgroundPositions[index],
opacity: isReady ? 1 : 0,
transform: `translate(${bubble.x}px, ${bubble.y}px) scale(${bubble.s})`,
}}
/>
))}
</div>
</div>
);
};
useState
, I use useRef
to keep the positions. By using useRef
, we will not cause the component to re-render due to state changes. Therefore, it has a better performance compared to useState
in this scenario. I've been trying a couple of solutions before coming to this final and most performant solution. For example, I tried to make each bubble as a separate component which has its own state and does the animation individually. However, this solution consumes a lot of processing powers.yarn dev
in your terminal.Copyright © 2020 William Kurniawan. All rights reserved.