์ด๋ ฅ์ ์ง์์ ํ๋ค๋ณด๋ฉด ์ ์ถ ํ ๋ ์์ข ์ด ์กฐ๊ฐ์ด ํญ์ฃฝ์ฒ๋ผ ํฐ์ง๋ ํจ๊ณผ๊ฐ ์๊ธฐ๋ ๊ฑธ ๋ณผ ์ ์๋ค. ์ด๊ฒ์ ๋ณด๊ณ ๊ถ๊ธํด์ ๋ง๋ค์ด ๋ณด๊ฒ ๋์๋ค.
Github ๋ค์ ๋ค์ ํ๋ฉด์ ์ฐพ์๋ณด๋ค๊ฐ 2๊ฐ๋ฅผ ๋ฐ๊ฒฌํ๋ค.
npm i react-canvas-confetti
๋๋ถ๋ถ์ ๋งค๊ฐ๋ณ์๊ฐ canvas-confetti ์ธํฐํ์ด์ค ์ด๋ฏ๋ก ์ด ๋ชจ๋์ ์ฌ์ฉํ๊ธฐ ์ ์ ๋จผ์ canvas-confetti ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ์ต์ํด์ง๋ ๊ฒ์ด ์ข๋ค๊ณ ํ๋ค.
๊นํ๋ธ์ ์์ ๊ฐ ์ ๋์์๊ณ , ์ค๋ช
๋ ์๋์ด์์ด ์ฝ๊ฒ ๊ตฌํํ ์ ์์๋ค.
์์ ๋ฅผ ์ดํด๋ณด๋ ์ด 4๊ฐ์ง ํจ๊ณผ๊ฐ ์์๋ค.
์ ์ผ ์ฒ์ ์ผ๋จ ํจ๊ณผ๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํ Canvas ์ปดํฌ๋ํธ๋ฅผ ์์ฑํด ์ค๋ค.
// ReactCanvasConfetti.tsx
import canvasConfetti, {
CreateTypes,
GlobalOptions,
Options,
} from "canvas-confetti";
import { Component, createRef, CSSProperties, RefObject } from "react";
export interface IProps extends Options, GlobalOptions {
fire?: any;
reset?: any;
width?: string | number;
height?: string | number;
className?: string;
style?: CSSProperties;
refConfetti?: (confetti: CreateTypes | null) => void;
onDecay?: () => void;
onFire?: () => void;
onReset?: () => void;
}
export default class ReactCanvasConfetti extends Component<IProps> {
private refCanvas: RefObject<HTMLCanvasElement>;
private confetti: CreateTypes | null;
constructor(props: IProps) {
super(props);
this.refCanvas = createRef();
this.confetti = null;
}
componentDidMount() {
if (!this.refCanvas.current) {
return;
}
const { resize, useWorker } = this.props;
const globalOptions: GlobalOptions = {
resize: typeof resize === "undefined" ? true : resize,
useWorker: typeof useWorker === "undefined" ? true : useWorker,
};
this.confetti = canvasConfetti.create(
this.refCanvas.current,
globalOptions
);
this.setRefConfetti();
}
componentDidUpdate(prevProps: Readonly<IProps>) {
const { fire, reset } = this.props;
const isFireTrue = !!fire;
const isFireChanged = fire !== prevProps.fire;
if (isFireTrue && isFireChanged) {
this.fireConfetti();
}
const isResetTrue = !!reset;
const isResetChanged = reset !== prevProps.reset;
if (isResetTrue && isResetChanged) {
this.resetConfetti();
}
}
componentWillUnmount() {
this.unsetRefConfetti();
}
private setRefConfetti() {
const { refConfetti } = this.props;
refConfetti && refConfetti(this.confetti);
}
private unsetRefConfetti() {
const { refConfetti } = this.props;
refConfetti && refConfetti(null);
}
private fireConfetti() {
if (!this.confetti) {
return;
}
const {
onFire,
onDecay,
onReset,
className,
style,
width,
height,
refConfetti,
fire,
reset,
...confettiProps
} = this.props;
onFire && onFire();
const promise = this.confetti(confettiProps);
promise &&
promise.then(() => {
onDecay && onDecay();
});
}
private resetConfetti() {
if (!this.confetti) {
return;
}
this.confetti.reset();
const { onReset } = this.props;
onReset && onReset();
}
render() {
const { style, className, width, height } = this.props;
return (
<canvas
ref={this.refCanvas}
style={style}
className={className}
width={width}
height={height}
/>
);
}
}
GlobalStyle ์ Canvas์ ํฌ๊ธฐ๋ฅผ ์ค์ ํด ์ค๋ค.
.canvas {
height: 100vh;
left: 0px;
pointer-events: none;
position: fixed;
top: 0px;
width: 100vw;
}
๊ทธ ๋ค์ ์ด์ ํจ๊ณผ๋ฅผ ๋ฃ์ด๋ณด์. Canvas-confetti ์๋ ์ค์ ํ ์ ์๋ ๋ง์ ์ต์ ๋ค์ด ์๋ค.
Canvas-confetti props (more details)
particleCount: number (default: 50)
- ๋ฐ์ฌ ํ ์์ข ์ด์ ์.angle: number (default: 90)
- ์์ข ์ด ์กฐ๊ฐ์ ๋ฐ์ฌํ๋ ๊ฐ๋.spread: number (default: 45)
- ์์ข ์ด ์กฐ๊ฐ์ด ์ค์์์ ์ผ๋ง๋ ๋ฉ๋ฆฌ ๊ฐ ์ ์๋์ง.(๋ ๋จ์)startVelocity: number (default: 45)
- ์์ข ์ด ์กฐ๊ฐ์ด ์ผ๋ง๋ ๋นจ๋ฆฌ ์์๋๋์ง ํฝ์ ๋จ์๋ก ํ์.decay: number (default: 0.9)
- ์์ข ์ด ์กฐ๊ฐ์ด ์ผ๋ง๋ ๋นจ๋ฆฌ ์๋๋ฅผ ์์์ง.(์๋ฐ๊พธ๋๊ฒ ์ข๋ค๊ณ ํ๋ค.)gravity: number (default: 1)
- ์ ์๊ฐ ์ผ๋ง๋ ๋นจ๋ฆฌ ๋น๊ฒจ์ง๋์ง. 1์ ์ ์ฒด ์ค๋ ฅ, 0.5๋ ์ ๋ฐ ์ค๋ ฅ, ๋ฑ์ด ์์ง๋ง ์ ํ์ ์๋ค. ์ํ๋ ๊ฒฝ์ฐ ์ ์๋ฅผ ์๋ก ์ฌ๋ฆด ์๋ ์๋ค.drift: number (default: 0)
- ์์ข ์ด ์กฐ๊ฐ์ด ์์ผ๋ก ์ผ๋ง๋ ํ๋ฌ๊ฐ๋์ง๋ฅผ ์ค์ . ์ผ์ชฝ์๋ ์์๋ฅผ, ์ค๋ฅธ์ชฝ์๋ ์์๋ฅผ ์ฌ์ฉ.ticks: number (default: 200)
- ์์ข ์ด ์กฐ๊ฐ์ด ๋ช ๋ฒ์ด๋ ์์ง์ผ์ง.origin: { x: number, y: number }
- ์์ข ์ด ๋ฐ์ฌ๋ฅผ ์์ํ ์์น. ์ํ๋ ๊ฒฝ์ฐ ํ๋ฉด ๋ฐ์์ ์คํํด๋ ๋๋ค.origin.x: number (default: 0.5)
- ํ์ด์ง์ x ์์น, 0์ ์ผ์ชฝ ๊ฐ์ฅ์๋ฆฌ์ด๊ณ 1์ ์ค๋ฅธ์ชฝ ๊ฐ์ฅ์๋ฆฌ์ด๋ค.origin.y: number (default: 0.5)
- ํ์ด์ง์ y ์์น. 0์ ์์ชฝ ๊ฐ์ฅ์๋ฆฌ์ด๊ณ 1์ ์๋์ชฝ ๊ฐ์ฅ์๋ฆฌ์ด๋ค.colors: Array<String>
- HEX ํ์์ ์์ ๋ฌธ์์ด ๋ฐฐ์ด.shapes: Array<String>
- ์์ข ์ด ๋ชจ์์ ๋ฐฐ์ด. ๊ฐ๋ฅํ ๊ฐ์ ์ ์ฌ๊ฐํ๊ณผ ์. ๊ธฐ๋ณธ๊ฐ์
๊ท ์ผํ ํผํฉ์์ ๋ ๋ชจ์์ ๋ชจ๋ ์ฌ์ฉํ๋ค. ๋ค์๊ณผ ๊ฐ์ ๊ฐ์ ์ ๊ณตํ์ฌ ๋ฏน์ค๋ฅผ ๋ณ๊ฒฝํ ์๋ ์์ต๋๋ค.
['circle', 'circle', 'square']์ ๊ฐ์ด 2/3 ์๊ณผ 1/3 ์ ์ฌ๊ฐํ์ ์ฌ์ฉํ๋ค.scalar: number (default: 1)
- ๊ฐ ์์ข ์ด ์ ์์ ๋ํ ํฌ๊ธฐ. ์์ข ์ด ์กฐ๊ฐ์ ์๊ฒ ๋ง๋ค๋ ค๋ฉด ์์์ ์ ์ฌ์ฉ.zIndex: number (default: 100)
- ๊ฒฐ๊ตญ ์์ข ์ด๊ฐ ๋งจ ์์ ์์ด์ผํ๋ค. ํ์ง๋ง ์์ฒญ๋๊ฒ ๋์ ํ์ด์ง๊ฐ ์๋ค๋ฉด ๋ ๋๊ฒ ์ค์ .resize: boolean (default: true)
- ์บ๋ฒ์ค ์ด๋ฏธ์ง ํฌ๊ธฐ ์ค์ ์ ํ์ฉํ๊ณ ์ฌ๋ฐ๋ฅธ ํฌ๊ธฐ๋ฅผ ์ ์งํ ์ง ์ฌ๋ถ. ์ฐฝ ํฌ๊ธฐ๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ(์: ์ฐฝ ํฌ๊ธฐ ์กฐ์ , ๋ชจ๋ฐ์ผ ์ฅ์น ํ์ ๋ฑ). ๊ธฐ๋ณธ์ ์ผ๋ก ์บ๋ฒ์ค ํฌ๊ธฐ๋ ์์ ํ ์ ์๋ค.
์ด์ ์์ข ์ด ๋ ๋ฆผ ํจ๊ณผ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด ์ค๋ค.
// Realistic.tsx
import { faFire } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CreateTypes } from "canvas-confetti";
import { Component } from "react";
import { Icon } from "../../header/Mode";
import ReactCanvasConfetti from "../ReactCanvasConfetti";
export default class Realistic extends Component {
private isAnimationEnabled: boolean;
private animationInstance: CreateTypes | null = null;
constructor(props: {}) {
super(props);
this.isAnimationEnabled = false;
this.fire = this.fire.bind(this);
}
makeShot(particleRatio: number, opts: object) {
this.animationInstance &&
this.animationInstance({
...opts,
origin: { y: 0.8 },
particleCount: Math.floor(200 * particleRatio),
});
}
// ์ด ๋ถ๋ถ์์ ์ฌ์ฉํ๊ณ ์ถ์ ์ค์ ์ ํ๋ฉด ๋๋ค.
fire() {
this.makeShot(0.25, {
spread: 25,
startVelocity: 55,
});
}
handlerFire = () => {
if (!this.isAnimationEnabled) {
this.isAnimationEnabled = true;
}
requestAnimationFrame(this.fire);
this.fire();
};
getInstance = (instance: CreateTypes | null) => {
this.animationInstance = instance;
};
render() {
return (
<>
<Icon mode="fire" onClick={this.handlerFire}>
<FontAwesomeIcon icon={faFire} />
</Icon>
<ReactCanvasConfetti
refConfetti={this.getInstance}
className="canvas"
/>
</>
);
}
}
์ ์ปดํฌ๋ํธ๋ฅผ ํ๋ฉด์ ๋ถ๋ฌ์ค๋ฉด ๋๋ค. ๋ฒํผ ํด๋ฆญ์ ์๋์ ๊ฐ์ด ๊ตฌํ์ด ๋๋ค.
makeShot์ ์ฌ๋ฌ๊ฐ ๋ง๋ค์ด ์ฃผ์ด์ ํญ์ฃฝ์ด ํฐ์ง๋ ๊ฒ์ฒ๋ผ ๋ง๋ค์ด ๋ณด์.
์ ์ฝ๋์์ fire()
๋ถ๋ถ์ makeShot
์ ๋ ์ถ๊ฐํ๊ณ ์ค์ ์ ๊ฐ๊ฐ ๋ค๋ฅด๊ฒ ๋ฐ๊พธ์ด ์ฃผ๋ฉด๋๋ค.
fire() {
this.makeShot(0.25, {
spread: 26,
startVelocity: 55,
});
this.makeShot(0.2, {
spread: 20,
});
this.makeShot(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
});
this.makeShot(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
});
this.makeShot(0.1, {
spread: 120,
startVelocity: 45,
});
}
๋น์ทํ ๋ฐฉ๋ฒ์ผ๋ก ๊ฐ๊ธฐ ๋ค๋ฅธ ์ค์ ์ ์ ์ฉ์ํค๋ฉด ๋ค์ํ ํจ๊ณผ๋ฅผ ๋ง๋ค ์ ์๋ค.
react-canvas-confetti ๋ณด๋ค ํจ์ฌ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์์๋ค. ์์ข ์ด ํจ๊ณผ๋ ํ๊ฐ์ง ๊ณ ์ ์ผ๋ก ๋ฐ๋ก ๋ณ๊ฒฝ ํ ์ ์๋ ๊ฒ ๊ฐ๊ณ ,
์์ข ์ด์ ์์, ํํ, ์ฌ์ด์ฆ, ๊ฐ์
๋ง ๋ณ๊ฒฝ ํ ์ ์๋ ๊ฒ ๊ฐ๋ค.
npm i js-confetti
์ ์ผ ์ฒ์ ์์ข ์ด ํจ๊ณผ๋ฅผ ๋ํ๋ผ Canvas๋ฅผ Document์ ์์ฑ ํ๊ณ , ๊ทธ์์
addConfetti
๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด ์์ข ์ด ํญ๋ฐ ํจ๊ณผ๋ฅผ ๋ณผ ์ ์๋ค.
//Home.tsx
import JSConfetti from "js-confetti";
function Home() {
//HTML Canvas ์์๋ฅผ ์์ฑํ์ฌ ํ์ด์ง์ ์ถ๊ฐ
const jsConfetti = new JSConfetti();
//์์ข
์ด ์ปค์คํฐ๋ง์ด์ง
const handleClick = () => {
jsConfetti.addConfetti({
confettiColors: [
"#ff0a54",
"#ff477e",
"#ff7096",
"#ff85a1",
"#fbb1bd",
"#f9bec7",
],
confettiRadius: 5,
confettiNumber: 500,
});
};
return <button onClick={handleClick}>CLICK</button>;
}
export default Home;
JSConfetti addConfetti props
confettiColors : ์์ข ์ด ์์ ์ค์ .
confettiRadius : ์์ข ์ด ์กฐ๊ฐ ๋ฐ์ง๋ฆ ๊ธธ์ด ์ค์ .
confettiNumber : ์์ข ์ด ์กฐ๊ฐ ๊ฐ์ ์ค์ .
emojis : ์ด๋ชจํฐ์ฝ์ผ๋ก ์์ข ์ด ์กฐ๊ฐ ์ฌ์ฉ.
emojiSize : ์ด๋ชจํฐ์ฝ ์ฌ์ด์ฆ ์ค์ .
์์ข ์ด ๋์ ์ด๋ชจํฐ์ฝ์ ๋ฃ์ด์ ์ฌ์ฉ ํ ์๋ ์๋ค.
//Home.tsx
import JSConfetti from "js-confetti";
function Home() {
//HTML Canvas ์์๋ฅผ ์์ฑํ์ฌ ํ์ด์ง์ ์ถ๊ฐ
const jsConfetti = new JSConfetti();
//์์ข
์ด ์ปค์คํฐ๋ง์ด์ง
const handleClick = () => {
jsConfetti.addConfetti({
emojis: ["๐", "๐", "๐บ"],
emojiSize: 100,
confettiNumber: 30,
});
};
return <button onClick={handleClick}>CLICK</button>;
}
export default Home;
์์ ๊ฐ์ด ํ๊ฒ๋๋ฉด Home ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง ๋ ๋๋ง๋ค,
new JSConfetti()
์ ์ํด์ HTML์ Canvas ์์๊ฐ ๊ณ์ ์ถ๊ฐ ๋๊ฒ ๋๋ค. ๊นํ๋ธ ์๋"new JSConfetti() ๋ HTML Canvas ์์๋ฅผ ์์ฑํ์ฌ ํ์ด์ง์ ์ถ๊ฐํ๋ฏ๋ก ํ ๋ฒ๋ง ํธ์ถํ์ญ์์ค!"
๋ผ๊ณ ์จ์ ธ์๋ค.
์ ์์์ ๋ณด๋ฉด Home์ ๋ค์ด๊ฐ๋ ๋ง๋ค canvas ์์๊ฐ ๊ณ์ ์ถ๊ฐ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
const jsConfetti = new JSConfetti();
์ ์์น๋ฅผ App.js
( ์ฒ์ ์คํ ๋ ๋ )๋ก ์ฎ๊ธฐ๊ณ export ์์ผ์ฃผ์๋ค.
import JSConfetti from "js-confetti";
export const conteffi = new JSConfetti();
๊ทธ๋ฆฌ๊ณ Home ๋๋ ์ฌ์ฉํ ๊ณณ์์ import ํ์ฌ ๋๊ฐ์ด ์ฌ์ฉํ๋ฉด ๋๋ค.
//Home.tsx
import JSConfetti from "js-confetti";
import { conteffi } from "../App";
function Home() {
//์์ข
์ด ์ปค์คํฐ๋ง์ด์ง
const handleClick = () => {
conteffi.addConfetti({
emojis: ["๐", "๐", "๐บ"],
emojiSize: 100,
confettiNumber: 30,
});
};
return <button onClick={handleClick}>CLICK</button>;
}
export default Home;
์ 2๊ฐ ๋ง๊ณ ๋ ๋ค๋ฅธ ์ฌ๋ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ์์๋๋ฐ ์ฌ์ฉ๋ฒ์ด ๋ค ๋น์ท๋น์ท ํ ๊ฒ ๊ฐ์๋ค. ๊ฐ์ธ์ ์ผ๋ก
react-canvas-confetti ๊ฐ ์กฐ๊ธ ๋ ๋ณต์กํ์ง๋ง ๋ค์ํ๊ฒ ์ปค์คํฐ๋ง์ด์ง ํ ์ ์์ด์, ๋ค์ํ ํจ๊ณผ๋ค์ ๋ฃ๊ธฐ์๋ ์ข์ ๊ฒ ๊ฐ๋ค.
ReactCanvasConfetti.tsx ํ์ผ์์
import { Icon } from "../../header/Mode"; ์์
์ฌ๊ธฐ์ Mode ๊ฐ ๋ญ๊น์.... ๋ชจ๋์ ์ฐพ์์๊ฐ ์๋ค๊ณ ๋ ์ ๋ง์ด์์ ใ ใ