Skip to content

Commit

Permalink
Stage 5: Moved the animation logic to a vanilla JS class.
Browse files Browse the repository at this point in the history
We dont need refs to be passed from parent to control 'start' and 'stop' functionality. It will be controlled by props now.
But on mounting multiple animation component in parent wont work
  • Loading branch information
A M Akankshit authored and A M Akankshit committed Oct 31, 2020
1 parent 61446d5 commit a10dedd
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 78 deletions.
18 changes: 5 additions & 13 deletions src/App.js
@@ -1,18 +1,10 @@
import React, { useState, useRef } from 'react';
import React, { useState } from 'react';
import Confetti from './confetti';

function App() {
const startBtn = useRef(null);
const stopBtn = useRef(null);
const [showConfetti, setShowConfetti] = useState(false);
const [streamAnimation, toggleStreamAnimation] = useState(false);
const [dummyState, setDummyState] = useState(false);

const startAnimation = () => {
startBtn.current.click();
};
const stopAnimation = () => {
stopBtn.current.click();
};
const rerender = () => {
setDummyState(!dummyState);
};
Expand All @@ -32,10 +24,10 @@ function App() {
</button>
{showConfetti && (
<div style={{ width: '100vh', height: '100vh', background: 'pink' }}>
<button onClick={startAnimation}>start</button>
<button onClick={stopAnimation}>stop</button>
<button onClick={() => toggleStreamAnimation(true)}>start</button>
<button onClick={() => toggleStreamAnimation(false)}>stop</button>
<button onClick={rerender}>rerender</button>
<Confetti startRef={startBtn} stopRef={stopBtn} />
<Confetti options={{ count: 50 }} streamAnimation={streamAnimation} />
</div>
)}
</div>
Expand Down
170 changes: 105 additions & 65 deletions src/confetti/index.js
@@ -1,6 +1,9 @@
import React, { Fragment, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';

const confetti = {
const globalThis = window;
const { requestAnimationFrame, cancelAnimationFrame } = globalThis;

const confettiDefault = {
colors: [
'DodgerBlue',
'OliveDrab',
Expand All @@ -15,41 +18,50 @@ const confetti = {
'Chocolate',
'Crimson'
],
count: 200,
waveAngle: 0,
timeout: null,
speed: 8
};
const globalThis = window;

const { requestAnimationFrame, cancelAnimationFrame } = globalThis;
class BridalConfetti {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.options = {
...confettiDefault,
...options
};
this.height = 0;
this.particles = [];
this.width = 0;
this.stopStremingConfetti = false;
this.animationId = null;
}

const Confetti = ({ styles = {}, startRef, stopRef }) => {
const canvasRef = useRef(null);
let count = 200;
const particles = [];
let waveAngle = 0;
let animationId;
let height;
let width;
let stopStremingConfetti = false;

const updateAndDrawParticles = (context) => {
waveAngle = waveAngle + 0.01;
updateAndDrawParticles(context) {
this.options.waveAngle = this.options.waveAngle + 0.01;
let x2, y2;
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];

for (let i = 0; i < this.particles.length; i++) {
const particle = this.particles[i];
//update particle
particle.tiltAngle += particle.tiltAngleIncrement;
particle.x = particle.x + Math.sin(particle.tiltAngle) * 2 - 1;
particle.y =
particle.y +
(Math.cos(waveAngle) + particle.diameter + confetti.speed) * 0.2;
(Math.cos(this.options.waveAngle) +
particle.diameter +
this.options.speed) *
0.2;
particle.tilt = Math.sin(particle.tiltAngle);
if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
if (stopStremingConfetti) {
particle.y = -20;
particles.splice(i, 1);
if (
particle.x > this.width + 20 ||
particle.x < -20 ||
particle.y > this.height
) {
if (this.stopStremingConfetti) {
this.particles.splice(i, 1);
} else {
setParticle(particle);
particle.y = -20;
this.setParticle(particle);
}
}
//draw particle
Expand All @@ -64,64 +76,92 @@ const Confetti = ({ styles = {}, startRef, stopRef }) => {
context.stroke();
context.closePath();
}
};
}

const runAnimation = (ctx) => {
runAnimation(ctx, height, width) {
ctx.clearRect(0, 0, width, height);
updateAndDrawParticles(ctx);
animationId = requestAnimationFrame(() => runAnimation(ctx));
};
this.updateAndDrawParticles(ctx);
this.animationId = requestAnimationFrame(() =>
this.runAnimation(ctx, height, width)
);
}

const setParticle = (particle) => {
setParticle(particle) {
const randomFn = Math.random;
const colors = confetti.colors;
const colors = this.options.colors;
particle.color = colors[parseInt(randomFn() * colors.length, 10)];
particle.x = randomFn() * width;
particle.y = randomFn() * height - height;
particle.x = randomFn() * this.width;
particle.y = randomFn() * this.height - this.height;
particle.diameter = randomFn() * 6 + 6;
particle.tilt = randomFn() * 10 - 10;
particle.tiltAngleIncrement = randomFn() * 0.07 + 0.05;
particle.tiltAngle = randomFn() * Math.PI;
return particle;
};
}

const startAnimation = () => {
cancelAnimationFrame(animationId);
stopStremingConfetti = false;
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
({ width, height } = canvas.getBoundingClientRect());
if (canvas.width !== width || canvas.height !== height) {
startAnimation() {
cancelAnimationFrame(this.animationId);
this.stopStremingConfetti = false;
const context = this.canvas.getContext('2d');
const { width, height } = this.canvas.getBoundingClientRect();
if (this.canvas.width !== width || this.canvas.height !== height) {
const { devicePixelRatio: ratio = 1 } = globalThis;
/**
* In case the device pixel ratio is more, the particles will be pixelated and will semm blur.
* That's why we are resizing the canvas.
* */
canvas.width = width * ratio;
canvas.height = height * ratio;
count = parseInt(canvas.width / 10, 10);

this.canvas.width = width * ratio;
this.canvas.height = height * ratio;
context.scale(ratio, ratio);
}
while (particles.length < count) {
particles.push(setParticle({}, canvas.width, canvas.height));
this.height = height;
this.width = width;
while (this.particles.length < this.options.count) {
this.particles.push(this.setParticle({}));
}
runAnimation(context);
};
this.runAnimation(context, height, width);
const { timeout } = this.options;
if (timeout && Number.isInteger(timeout)) {
setTimeout(this.stopAnimation.bind(this), timeout);
}
}

const stopAnimation = () => {
stopStremingConfetti = true;
};
stopAnimation() {
this.stopStremingConfetti = true;
}

useEffect(() => {
return () => {
cancelAnimationFrame(animationId);
};
}, []);
ummountCanvas() {
this.particles = [];
cancelAnimationFrame(this.animationId);
}
}

// IIFE
const Confetti = (() => {
let canvas = null;

// The component to be exported and used outside
return ({ styles = {}, options = {}, streamAnimation }) => {
const canvasRef = useRef(null);

return (
<Fragment>
<button onClick={startAnimation} hidden={true} ref={startRef}></button>
<button onClick={stopAnimation} hidden={true} ref={stopRef}></button>
useEffect(() => {
canvas = new BridalConfetti(canvasRef.current, options);

return () => {
canvas.ummountCanvas();
};
}, []);

useEffect(() => {
if (streamAnimation) {
canvas.startAnimation();
} else {
canvas.stopAnimation();
}
}, [streamAnimation]);

return (
<canvas
ref={canvasRef}
style={{
Expand All @@ -130,8 +170,8 @@ const Confetti = ({ styles = {}, startRef, stopRef }) => {
...styles
}}
/>
</Fragment>
);
};
);
};
})();

export default Confetti;

0 comments on commit a10dedd

Please sign in to comment.