diff --git a/.github/workflows/auto_release.yml b/.github/workflows/auto_release.yml new file mode 100644 index 0000000..cc276f6 --- /dev/null +++ b/.github/workflows/auto_release.yml @@ -0,0 +1,29 @@ +name: Auto Release + +on: + push: + branches: + - main + +jobs: + create_release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Get version from package.json + id: version + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + tag_name: v${{ env.version }} + release_name: Release v${{ env.version }} + body: Auto-generated release + draft: false + prerelease: false diff --git a/.github/workflows/deploy_to_github_pages.yml b/.github/workflows/deploy_to_github_pages.yml new file mode 100644 index 0000000..8f2c612 --- /dev/null +++ b/.github/workflows/deploy_to_github_pages.yml @@ -0,0 +1,33 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: 16 + + - name: Install dependencies + run: yarn install + + - name: Build project + run: yarn build + + - name: Build example + run: cd example && yarn install && yarn build + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + publish_dir: example/dist diff --git a/.github/workflows/publish_to_github.yml b/.github/workflows/publish_to_github.yml new file mode 100644 index 0000000..d5f87b5 --- /dev/null +++ b/.github/workflows/publish_to_github.yml @@ -0,0 +1,46 @@ +name: Publish Github Package + +on: + release: + types: [created] +#on: +# push: +# branches: +# - main + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.x' + registry-url: 'https://npm.pkg.github.com' + # Defaults to the user or organization that owns the workflow file + scope: '@almond-bongbong' + + - name: Install dependencies + run: yarn install + + - name: Build package + run: yarn build + + - name: Install jq + run: sudo apt-get install -y jq + + - name: Update package.json for GitHub Packages + run: | + jq '.name = "@almond-bongbong/react-confetti-boom"' package.json > package.temp.json + mv package.temp.json package.json + + - name: Publish to GitHub Packages + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish_to_npm.yml b/.github/workflows/publish_to_npm.yml new file mode 100644 index 0000000..3ddd61e --- /dev/null +++ b/.github/workflows/publish_to_npm.yml @@ -0,0 +1,33 @@ +name: Publish NPM Package + +on: + release: + types: [created] +#on: +# push: +# branches: +# - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.x' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: yarn install + + - name: Build package + run: yarn build + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 4ae0f1c..67e623d 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,77 @@ -# node-module-template +# React Confetti Boom -This is a template for creating a node module. +React Confetti Boom is a lightweight, customizable confetti animation component for React applications. Add a fun and engaging confetti effect to your app with just a few lines of code. -## Build - -Rollup is used to build the module. The configuration is in `rollup.config.js`. +## Installation ```bash -import commonjs from '@rollup/plugin-commonjs'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import typescript from 'rollup-plugin-typescript2'; -import peerDepsExternal from 'rollup-plugin-peer-deps-external'; -import postcss from 'rollup-plugin-postcss'; -import babel from '@rollup/plugin-babel'; - -import packageJson from './package.json' assert { type: 'json' }; - -const extensions = ['js', 'jsx', 'ts', 'tsx', 'mjs']; - -export default { - input: './src/index.tsx', - output: [ - { - file: packageJson.main, - format: 'cjs', - sourcemap: true, - }, - { - file: packageJson.module, - format: 'esm', - sourcemap: true, - }, - ], - plugins: [ - peerDepsExternal(), - nodeResolve({ extensions }), - commonjs(), - typescript({ - tsconfig: './tsconfig.json', - }), - babel({ - babelHelpers: 'bundled', - exclude: 'node_modules/**', - extensions, - include: ['src/**/*'], - }), - postcss({ - extract: false, - modules: true, - sourceMap: false, - use: ['sass'], - }), - ], -}; +npm install react-confetti-boom +``` + +## Usage + +Import the Confetti component and add it to your JSX. + +```jsx +import React from 'react'; +import Confetti from 'react-confetti-boom'; + +function MyApp() { + return ( +
+

My React App

+ +
+ ); +} + +export default MyApp; ``` -1. @rollup/plugin-babel: Babel 플러그인을 사용하여 최신 JavaScript 문법을 이전 버전의 호환 가능한 코드로 변환합니다. -2. @rollup/plugin-commonjs: CommonJS 모듈을 ES6 모듈로 변환합니다. 이를 통해 npm 패키지와 호환성을 보장합니다. -3. @rollup/plugin-node-resolve: Node.js 모듈 해석 알고리즘을 사용하여 모듈 의존성을 해결합니다. -4. rollup-plugin-typescript2: TypeScript 코드를 JavaScript로 변환합니다. -5. rollup-plugin-peer-deps-external: 외부 peerDependencies를 번들에서 자동으로 제외합니다. -6. rollup-plugin-postcss: CSS 및 SCSS와 같은 스타일시트를 처리하고 모듈로 번들링합니다. +## Props + +| Name | Type | Default | Description | +| -------------- | -------- | -------------------------------------------- | ---------------------------------------------------------------------------- | +| x | number | 0.5 | Horizontal starting position of confetti as a ratio of canvas width (0 to 1) | +| y | number | 0.5 | Vertical starting position of confetti as a ratio of canvas height (0 to 1) | +| particleCount | number | 30 | Number of confetti particles to generate | +| deg | number | 270 | Initial angle (in degrees) at which particles are emitted | +| shapeSize | number | 12 | Size of confetti particles | +| spreadDeg | number | 30 | Angle (in degrees) that particles can deviate from the initial angle (deg) | +| effectInterval | number | 3000 | Interval (in ms) between consecutive confetti bursts | +| effectCount | number | 1 | Number of confetti bursts to render | +| colors | string[] | ['#ff577f', '#ff884b', '#ffd384', '#fff9b0'] | Array of colors for confetti particles, in hex format | + +## Example -extensions 배열은 Rollup이 처리해야 하는 파일 확장자를 나열합니다. +```jsx +import React from 'react'; +import Confetti from 'react-confetti-boom'; + +function Celebration() { + return ( +
+

Congratulations!

+ +
+ ); +} + +export default Celebration; +``` -export default는 Rollup에 대한 기본 설정을 내보냅니다. input은 번들의 시작점을 지정하고, output은 번들의 결과물을 저장할 경로와 포맷을 설정합니다. 이 설정에서는 CommonJS 및 ES 모듈 포맷을 사용하여 두 개의 번들을 생성합니다. +This example will render a confetti animation with 50 particles starting at 10% from the top of the canvas. The particles will be emitted at a 270-degree angle, with a 45-degree spread. The confetti bursts will occur every 2 seconds, for a total of 3 bursts. The confetti particles will use the provided array of colors. -plugins 배열은 위에서 설명한 플러그인들을 포함합니다. 이 플러그인들은 번들링 과정에서 코드를 변환하고 최적화하는 데 사용됩니다. +## License -이 설정은 라이브러리의 소스 코드를 효율적으로 번들링하고, 최신 JavaScript 및 TypeScript 문법을 지원하며, 스타일시트와 모듈 처리를 포함하는 등 다양한 요구 사항을 충족합니다. +MIT diff --git a/example/package.json b/example/package.json index fce04eb..8450ad3 100644 --- a/example/package.json +++ b/example/package.json @@ -9,11 +9,13 @@ "preview": "vite preview" }, "dependencies": { + "dat.gui": "^0.7.9", "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-confetti-boom": "link:.." + "react-confetti-boom": "link:..", + "react-dom": "^18.2.0" }, "devDependencies": { + "@types/dat.gui": "^0.7.9", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@vitejs/plugin-react-swc": "^3.0.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index 169cc50..615a595 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,13 +1,90 @@ -import { useState } from 'react'; -import { Confetti } from 'react-confetti-boom'; +import { useCallback, useEffect, useState } from 'react'; +import Confetti from 'react-confetti-boom'; +import * as dat from 'dat.gui'; import './style.css'; +const DEFAULT_OPTIONS = { + deg: 270, + particleCount: 30, + effectCount: Infinity, + effectInterval: 3000, + shapeSize: 12, + spreadDeg: 30, + x: 0.5, + y: 0.5, + colors: ['#ff577f', '#ff884b', '#ffd384', '#fff9b0'], +}; + function App() { - const [count, setCount] = useState(0); + const [options, setOptions] = useState(DEFAULT_OPTIONS); + + const handleChangeOption = useCallback((key: string, value: number) => { + setOptions((prev) => ({ ...prev, [key]: value })); + }, []); + + const handleColors = useCallback((index: number, value: string) => { + setOptions((prev) => { + const newColors = [...prev.colors]; + newColors[index] = value; + return { ...prev, colors: newColors }; + }); + }, []); + + useEffect(() => { + const target = { ...DEFAULT_OPTIONS }; + const gui = new dat.GUI(); + gui.width = 300; + const confetti = gui.addFolder('Confetti'); + confetti.open(); + confetti + .add(target, 'particleCount', 0, 100) + .onChange((v) => handleChangeOption('particleCount', v)); + confetti + .add(target, 'deg', 0, 360) + .onChange((v) => handleChangeOption('deg', v)); + confetti + .add(target, 'effectCount', 1, 100) + .onChange((v) => handleChangeOption('effectCount', v)); + confetti + .add(target, 'effectInterval', 0, 10000) + .name('effectInterval (ms)') + .onChange((v) => handleChangeOption('effectInterval', v)); + confetti + .add(target, 'shapeSize', 1, 50) + .onChange((v) => handleChangeOption('shapeSize', v)); + confetti + .add(target, 'spreadDeg', 0, 100) + .onChange((v) => handleChangeOption('spreadDeg', v)); + confetti.add(target, 'x', 0, 1).onChange((v) => handleChangeOption('x', v)); + confetti.add(target, 'y', 0, 1).onChange((v) => handleChangeOption('y', v)); + + const colors = gui.addFolder('Colors'); + colors.open(); + + DEFAULT_OPTIONS.colors.forEach((color, i) => { + colors + .addColor(target.colors, i.toString()) + .onChange((v) => handleColors(i, v)); + }); + + return () => { + gui.destroy(); + }; + }, [handleChangeOption, handleColors]); return (
- +
); } diff --git a/example/yarn.lock b/example/yarn.lock index 4ceba0b..82de82e 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -178,6 +178,11 @@ "@swc/core-win32-ia32-msvc" "1.3.46" "@swc/core-win32-x64-msvc" "1.3.46" +"@types/dat.gui@^0.7.9": + version "0.7.9" + resolved "https://registry.yarnpkg.com/@types/dat.gui/-/dat.gui-0.7.9.tgz#9e16985a0a6b4ef652c53212ee813e5968aff9a8" + integrity sha512-UiqZasQIask5cUwWOO6BgOjP1dNj9ChYtmeAb4WTKs5IJ7ha3ATRTeXY7/TzlmEZznh40lS6Ov5BdNA+tU+fWQ== + "@types/prop-types@*": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" @@ -216,6 +221,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +dat.gui@^0.7.9: + version "0.7.9" + resolved "https://registry.yarnpkg.com/dat.gui/-/dat.gui-0.7.9.tgz#860cab06053b028e327820eabdf25a13cf07b17e" + integrity sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ== + esbuild@^0.17.5: version "0.17.15" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.15.tgz#209ebc87cb671ffb79574db93494b10ffaf43cbc" diff --git a/lib/index.d.ts b/lib/index.d.ts index 0a871bd..4182665 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,3 +1,14 @@ /// -declare function Confetti(): JSX.Element; -export { Confetti }; +interface Props { + x?: number; + y?: number; + particleCount?: number; + deg?: number; + shapeSize?: number; + spreadDeg?: number; + effectInterval?: number; + effectCount?: number; + colors?: string[]; +} +declare function Confetti({ x, y, particleCount, deg, shapeSize, spreadDeg, effectInterval, effectCount, colors, }: Props): JSX.Element; +export default Confetti; diff --git a/lib/index.esm.js b/lib/index.esm.js index 0f51902..2d91605 100644 --- a/lib/index.esm.js +++ b/lib/index.esm.js @@ -1,36 +1,5 @@ import React, { useRef, useCallback, useEffect } from 'react'; -function styleInject(css, ref) { - if ( ref === void 0 ) ref = {}; - var insertAt = ref.insertAt; - - if (!css || typeof document === 'undefined') { return; } - - var head = document.head || document.getElementsByTagName('head')[0]; - var style = document.createElement('style'); - style.type = 'text/css'; - - if (insertAt === 'top') { - if (head.firstChild) { - head.insertBefore(style, head.firstChild); - } else { - head.appendChild(style); - } - } else { - head.appendChild(style); - } - - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } -} - -var css_248z = ".index-module_canvas__H2w7d {\n pointer-events: none;\n}"; -var styles = {"canvas":"index-module_canvas__H2w7d"}; -styleInject(css_248z); - var randomNumBetween = function (min, max) { return Math.random() * (max - min) + min; }; @@ -46,29 +15,30 @@ var hexToRgb = function (hex) { }; var Particle = /** @class */function () { - function Particle(x, y, deg, colors, shapes, spread) { + function Particle(x, y, deg, colors, shapes, shapeSize, spread) { if (deg === void 0) { deg = 0; } - if (colors === void 0) { - colors = ['#ff577f', '#ff884b', '#ffd384', '#fff9b0']; - } if (shapes === void 0) { shapes = ['circle', 'square']; } + if (shapeSize === void 0) { + shapeSize = 12; + } if (spread === void 0) { spread = 30; } - this.x = x * window.innerWidth; - this.y = y * window.innerHeight; - this.width = 12; - this.height = 12; + var DPR = window.devicePixelRatio > 1 ? 2 : 1; + this.x = x * window.innerWidth * DPR; + this.y = y * window.innerHeight * DPR; + this.width = shapeSize; + this.height = shapeSize; this.theta = Math.PI / 180 * randomNumBetween(deg - spread, deg + spread); - this.radius = randomNumBetween(30, 100); + this.radius = randomNumBetween(20, 70); this.vx = this.radius * Math.cos(this.theta); this.vy = this.radius * Math.sin(this.theta); - this.friction = 0.89; - this.gravity = 0.5; + this.friction = 0.87; + this.gravity = 0.55; this.opacity = 1; this.rotate = randomNumBetween(0, 360); this.widthDelta = randomNumBetween(0, 360); @@ -78,17 +48,22 @@ var Particle = /** @class */function () { this.color = hexToRgb(this.colors[Math.floor(randomNumBetween(0, this.colors.length))]); this.shapes = shapes; this.shape = this.shapes[Math.floor(randomNumBetween(0, this.shapes.length))]; + this.swingOffset = randomNumBetween(0, Math.PI * 2); + this.swingSpeed = Math.random() * 0.05 + 0.01; + this.swingAmplitude = randomNumBetween(0, 0.4); } Particle.prototype.update = function () { - this.vy += this.gravity; this.vx *= this.friction; this.vy *= this.friction; + this.vy += this.gravity; + if (this.vy > 0) this.vx += Math.sin(this.swingOffset) * this.swingAmplitude; this.x += this.vx; this.y += this.vy; - this.opacity -= 0.005; + this.opacity -= 0.004; this.widthDelta += 2; this.heightDelta += 2; this.rotate += this.rotateDelta; + this.swingOffset += this.swingSpeed; }; Particle.prototype.drawSquare = function (ctx) { ctx.fillRect(this.x, this.y, this.width * Math.cos(Math.PI / 180 * this.widthDelta), this.height * Math.sin(Math.PI / 180 * this.heightDelta)); @@ -105,6 +80,7 @@ var Particle = /** @class */function () { ctx.translate(this.x + translateXAlpha, this.y + translateYAlpha); ctx.rotate(Math.PI / 180 * this.rotate); ctx.translate(-(this.x + translateXAlpha), -(this.y + translateYAlpha)); + // eslint-disable-next-line no-param-reassign ctx.fillStyle = "rgba(".concat(this.color.r, ", ").concat(this.color.g, ", ").concat(this.color.b, ", ").concat(this.opacity, ")"); if (this.shape === 'square') this.drawSquare(ctx); if (this.shape === 'circle') this.drawCircle(ctx); @@ -113,13 +89,63 @@ var Particle = /** @class */function () { return Particle; }(); +function styleInject(css, ref) { + if ( ref === void 0 ) ref = {}; + var insertAt = ref.insertAt; + + if (!css || typeof document === 'undefined') { return; } + + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.createElement('style'); + style.type = 'text/css'; + + if (insertAt === 'top') { + if (head.firstChild) { + head.insertBefore(style, head.firstChild); + } else { + head.appendChild(style); + } + } else { + head.appendChild(style); + } + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } +} + +var css_248z = ".index-module_canvas__H2w7d {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n pointer-events: none;\n}"; +var styles = {"canvas":"index-module_canvas__H2w7d"}; +styleInject(css_248z); + var FPS = 60; var INTERVAL = 1000 / FPS; -function Confetti() { +function Confetti(_a) { + var _b = _a.x, + x = _b === void 0 ? 0.5 : _b, + _c = _a.y, + y = _c === void 0 ? 0.5 : _c, + _d = _a.particleCount, + particleCount = _d === void 0 ? 30 : _d, + _e = _a.deg, + deg = _e === void 0 ? 270 : _e, + _f = _a.shapeSize, + shapeSize = _f === void 0 ? 12 : _f, + _g = _a.spreadDeg, + spreadDeg = _g === void 0 ? 30 : _g, + _h = _a.effectInterval, + effectInterval = _h === void 0 ? 3000 : _h, + _j = _a.effectCount, + effectCount = _j === void 0 ? 1 : _j, + _k = _a.colors, + colors = _k === void 0 ? ['#ff577f', '#ff884b', '#ffd384', '#fff9b0'] : _k; var canvasRef = useRef(null); - var ctxRef = useRef(null); - var particles = []; + var ctxRef = useRef(); + var particlesRef = useRef([]); var animationFrameRef = useRef(0); + var effectCountRef = useRef(0); var init = useCallback(function () { var canvas = canvasRef.current; var ctx = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('2d'); @@ -135,58 +161,73 @@ function Confetti() { ctx.scale(DPR, DPR); }, []); var createConfetti = useCallback(function (options) { - for (var i = 0; i < options.count; i++) { - particles.push(new Particle(options.x, options.y, options.deg, options.colors, options.shapes, options.spread)); + for (var i = 0; i < options.particleCount; i += 1) { + particlesRef.current.push(new Particle(options.x, options.y, options.deg, options.colors, options.shapes, options.shapeSize, options.spreadDeg)); } }, []); var render = useCallback(function () { - var canvas = canvasRef.current; - if (!ctxRef.current || !canvas) return; + if (!ctxRef.current) return; var now; var delta; var then = Date.now(); + var effectDelta; + var effectThen = Date.now() - effectInterval; var frame = function () { + var canvas = canvasRef.current; if (!ctxRef.current) return; + if (!canvas) return; animationFrameRef.current = requestAnimationFrame(frame); now = Date.now(); delta = now - then; + effectDelta = now - effectThen; if (delta < INTERVAL) return; ctxRef.current.clearRect(0, 0, canvas.width, canvas.height); - createConfetti({ - x: 0, - y: 0.5, - count: 10, - deg: -50 - }); - createConfetti({ - x: 1, - y: 0.5, - count: 10, - deg: 230 - }); - for (var i = particles.length - 1; i >= 0; i--) { + if (effectDelta > effectInterval && effectCountRef.current < effectCount) { + createConfetti({ + x: x, + y: y, + particleCount: particleCount, + deg: deg, + shapeSize: shapeSize, + spreadDeg: spreadDeg, + colors: colors + }); + effectThen = now - effectDelta % effectInterval; + effectCountRef.current += 1; + } + var particles = particlesRef.current; + for (var i = particles.length - 1; i >= 0; i -= 1) { var p = particles[i]; p.update(); p.draw(ctxRef.current); - if (p.opacity <= 0) particles.splice(particles.indexOf(p), 1); - if (p.y > window.innerHeight) particles.splice(particles.indexOf(p), 1); + var canvasHeight = (canvas === null || canvas === void 0 ? void 0 : canvas.height) || 0; + if (p.opacity <= 0 || p.y > canvasHeight) particles.splice(particles.indexOf(p), 1); } then = now - delta % INTERVAL; + if (effectCountRef.current >= effectCount && particles.length === 0) { + cancelAnimationFrame(animationFrameRef.current); + } }; animationFrameRef.current = requestAnimationFrame(frame); - }, []); + }, [x, y, particleCount, deg, effectInterval, shapeSize, effectCount, spreadDeg, colors, createConfetti]); useEffect(function () { init(); render(); return function () { - if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } }; - }, []); + }, [init, render]); + useEffect(function () { + effectCountRef.current = 0; + // render(); + }, [effectCount]); return React.createElement("canvas", { className: styles.canvas, ref: canvasRef }); } -export { Confetti }; +export { Confetti as default }; //# sourceMappingURL=index.esm.js.map diff --git a/lib/index.esm.js.map b/lib/index.esm.js.map index 25524af..965d608 100644 --- a/lib/index.esm.js.map +++ b/lib/index.esm.js.map @@ -1 +1 @@ -{"version":3,"file":"index.esm.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[0]} \ No newline at end of file +{"version":3,"file":"index.esm.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[0]} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index f7c77d5..c8887b1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,37 +2,6 @@ var React = require('react'); -function styleInject(css, ref) { - if ( ref === void 0 ) ref = {}; - var insertAt = ref.insertAt; - - if (!css || typeof document === 'undefined') { return; } - - var head = document.head || document.getElementsByTagName('head')[0]; - var style = document.createElement('style'); - style.type = 'text/css'; - - if (insertAt === 'top') { - if (head.firstChild) { - head.insertBefore(style, head.firstChild); - } else { - head.appendChild(style); - } - } else { - head.appendChild(style); - } - - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } -} - -var css_248z = ".index-module_canvas__H2w7d {\n pointer-events: none;\n}"; -var styles = {"canvas":"index-module_canvas__H2w7d"}; -styleInject(css_248z); - var randomNumBetween = function (min, max) { return Math.random() * (max - min) + min; }; @@ -48,29 +17,30 @@ var hexToRgb = function (hex) { }; var Particle = /** @class */function () { - function Particle(x, y, deg, colors, shapes, spread) { + function Particle(x, y, deg, colors, shapes, shapeSize, spread) { if (deg === void 0) { deg = 0; } - if (colors === void 0) { - colors = ['#ff577f', '#ff884b', '#ffd384', '#fff9b0']; - } if (shapes === void 0) { shapes = ['circle', 'square']; } + if (shapeSize === void 0) { + shapeSize = 12; + } if (spread === void 0) { spread = 30; } - this.x = x * window.innerWidth; - this.y = y * window.innerHeight; - this.width = 12; - this.height = 12; + var DPR = window.devicePixelRatio > 1 ? 2 : 1; + this.x = x * window.innerWidth * DPR; + this.y = y * window.innerHeight * DPR; + this.width = shapeSize; + this.height = shapeSize; this.theta = Math.PI / 180 * randomNumBetween(deg - spread, deg + spread); - this.radius = randomNumBetween(30, 100); + this.radius = randomNumBetween(20, 70); this.vx = this.radius * Math.cos(this.theta); this.vy = this.radius * Math.sin(this.theta); - this.friction = 0.89; - this.gravity = 0.5; + this.friction = 0.87; + this.gravity = 0.55; this.opacity = 1; this.rotate = randomNumBetween(0, 360); this.widthDelta = randomNumBetween(0, 360); @@ -80,17 +50,22 @@ var Particle = /** @class */function () { this.color = hexToRgb(this.colors[Math.floor(randomNumBetween(0, this.colors.length))]); this.shapes = shapes; this.shape = this.shapes[Math.floor(randomNumBetween(0, this.shapes.length))]; + this.swingOffset = randomNumBetween(0, Math.PI * 2); + this.swingSpeed = Math.random() * 0.05 + 0.01; + this.swingAmplitude = randomNumBetween(0, 0.4); } Particle.prototype.update = function () { - this.vy += this.gravity; this.vx *= this.friction; this.vy *= this.friction; + this.vy += this.gravity; + if (this.vy > 0) this.vx += Math.sin(this.swingOffset) * this.swingAmplitude; this.x += this.vx; this.y += this.vy; - this.opacity -= 0.005; + this.opacity -= 0.004; this.widthDelta += 2; this.heightDelta += 2; this.rotate += this.rotateDelta; + this.swingOffset += this.swingSpeed; }; Particle.prototype.drawSquare = function (ctx) { ctx.fillRect(this.x, this.y, this.width * Math.cos(Math.PI / 180 * this.widthDelta), this.height * Math.sin(Math.PI / 180 * this.heightDelta)); @@ -107,6 +82,7 @@ var Particle = /** @class */function () { ctx.translate(this.x + translateXAlpha, this.y + translateYAlpha); ctx.rotate(Math.PI / 180 * this.rotate); ctx.translate(-(this.x + translateXAlpha), -(this.y + translateYAlpha)); + // eslint-disable-next-line no-param-reassign ctx.fillStyle = "rgba(".concat(this.color.r, ", ").concat(this.color.g, ", ").concat(this.color.b, ", ").concat(this.opacity, ")"); if (this.shape === 'square') this.drawSquare(ctx); if (this.shape === 'circle') this.drawCircle(ctx); @@ -115,13 +91,63 @@ var Particle = /** @class */function () { return Particle; }(); +function styleInject(css, ref) { + if ( ref === void 0 ) ref = {}; + var insertAt = ref.insertAt; + + if (!css || typeof document === 'undefined') { return; } + + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.createElement('style'); + style.type = 'text/css'; + + if (insertAt === 'top') { + if (head.firstChild) { + head.insertBefore(style, head.firstChild); + } else { + head.appendChild(style); + } + } else { + head.appendChild(style); + } + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } +} + +var css_248z = ".index-module_canvas__H2w7d {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n pointer-events: none;\n}"; +var styles = {"canvas":"index-module_canvas__H2w7d"}; +styleInject(css_248z); + var FPS = 60; var INTERVAL = 1000 / FPS; -function Confetti() { +function Confetti(_a) { + var _b = _a.x, + x = _b === void 0 ? 0.5 : _b, + _c = _a.y, + y = _c === void 0 ? 0.5 : _c, + _d = _a.particleCount, + particleCount = _d === void 0 ? 30 : _d, + _e = _a.deg, + deg = _e === void 0 ? 270 : _e, + _f = _a.shapeSize, + shapeSize = _f === void 0 ? 12 : _f, + _g = _a.spreadDeg, + spreadDeg = _g === void 0 ? 30 : _g, + _h = _a.effectInterval, + effectInterval = _h === void 0 ? 3000 : _h, + _j = _a.effectCount, + effectCount = _j === void 0 ? 1 : _j, + _k = _a.colors, + colors = _k === void 0 ? ['#ff577f', '#ff884b', '#ffd384', '#fff9b0'] : _k; var canvasRef = React.useRef(null); - var ctxRef = React.useRef(null); - var particles = []; + var ctxRef = React.useRef(); + var particlesRef = React.useRef([]); var animationFrameRef = React.useRef(0); + var effectCountRef = React.useRef(0); var init = React.useCallback(function () { var canvas = canvasRef.current; var ctx = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('2d'); @@ -137,58 +163,73 @@ function Confetti() { ctx.scale(DPR, DPR); }, []); var createConfetti = React.useCallback(function (options) { - for (var i = 0; i < options.count; i++) { - particles.push(new Particle(options.x, options.y, options.deg, options.colors, options.shapes, options.spread)); + for (var i = 0; i < options.particleCount; i += 1) { + particlesRef.current.push(new Particle(options.x, options.y, options.deg, options.colors, options.shapes, options.shapeSize, options.spreadDeg)); } }, []); var render = React.useCallback(function () { - var canvas = canvasRef.current; - if (!ctxRef.current || !canvas) return; + if (!ctxRef.current) return; var now; var delta; var then = Date.now(); + var effectDelta; + var effectThen = Date.now() - effectInterval; var frame = function () { + var canvas = canvasRef.current; if (!ctxRef.current) return; + if (!canvas) return; animationFrameRef.current = requestAnimationFrame(frame); now = Date.now(); delta = now - then; + effectDelta = now - effectThen; if (delta < INTERVAL) return; ctxRef.current.clearRect(0, 0, canvas.width, canvas.height); - createConfetti({ - x: 0, - y: 0.5, - count: 10, - deg: -50 - }); - createConfetti({ - x: 1, - y: 0.5, - count: 10, - deg: 230 - }); - for (var i = particles.length - 1; i >= 0; i--) { + if (effectDelta > effectInterval && effectCountRef.current < effectCount) { + createConfetti({ + x: x, + y: y, + particleCount: particleCount, + deg: deg, + shapeSize: shapeSize, + spreadDeg: spreadDeg, + colors: colors + }); + effectThen = now - effectDelta % effectInterval; + effectCountRef.current += 1; + } + var particles = particlesRef.current; + for (var i = particles.length - 1; i >= 0; i -= 1) { var p = particles[i]; p.update(); p.draw(ctxRef.current); - if (p.opacity <= 0) particles.splice(particles.indexOf(p), 1); - if (p.y > window.innerHeight) particles.splice(particles.indexOf(p), 1); + var canvasHeight = (canvas === null || canvas === void 0 ? void 0 : canvas.height) || 0; + if (p.opacity <= 0 || p.y > canvasHeight) particles.splice(particles.indexOf(p), 1); } then = now - delta % INTERVAL; + if (effectCountRef.current >= effectCount && particles.length === 0) { + cancelAnimationFrame(animationFrameRef.current); + } }; animationFrameRef.current = requestAnimationFrame(frame); - }, []); + }, [x, y, particleCount, deg, effectInterval, shapeSize, effectCount, spreadDeg, colors, createConfetti]); React.useEffect(function () { init(); render(); return function () { - if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } }; - }, []); + }, [init, render]); + React.useEffect(function () { + effectCountRef.current = 0; + // render(); + }, [effectCount]); return React.createElement("canvas", { className: styles.canvas, ref: canvasRef }); } -exports.Confetti = Confetti; +module.exports = Confetti; //# sourceMappingURL=index.js.map diff --git a/lib/index.js.map b/lib/index.js.map index 00b8ca6..1259958 100644 --- a/lib/index.js.map +++ b/lib/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[0]} \ No newline at end of file +{"version":3,"file":"index.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[0]} \ No newline at end of file diff --git a/lib/js/Particle.d.ts b/lib/js/Particle.d.ts index 13ca7cb..af51118 100644 --- a/lib/js/Particle.d.ts +++ b/lib/js/Particle.d.ts @@ -20,9 +20,12 @@ declare class Particle { g: number; b: number; }; - shapes: string[]; + shapes: readonly ['circle', 'square']; shape: string; - constructor(x: number, y: number, deg?: number, colors?: string[], shapes?: ('circle' | 'square')[], spread?: number); + swingOffset: number; + swingSpeed: number; + swingAmplitude: number; + constructor(x: number, y: number, deg: number | undefined, colors: string[], shapes?: readonly ["circle", "square"], shapeSize?: number, spread?: number); update(): void; drawSquare(ctx: CanvasRenderingContext2D): void; drawCircle(ctx: CanvasRenderingContext2D): void; diff --git a/package.json b/package.json index b08da61..633b13f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-confetti-boom", - "version": "1.0.0", + "version": "0.0.1", "description": "react-confetti-boom", "author": "", "license": "MIT", @@ -39,6 +39,8 @@ "keywords": [ "react", "typescript", - "module" + "module", + "confetti", + "particle" ] } diff --git a/src/index.module.scss b/src/index.module.scss index d68c408..5b04de7 100644 --- a/src/index.module.scss +++ b/src/index.module.scss @@ -1,3 +1,8 @@ .canvas { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; pointer-events: none; } diff --git a/src/index.tsx b/src/index.tsx index 1e01e56..bcd1cb5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,38 @@ import React, { useCallback, useEffect, useRef } from 'react'; -import styles from './index.module.scss'; import Particle from './js/Particle'; +import styles from './index.module.scss'; const FPS = 60; const INTERVAL = 1000 / FPS; -function Confetti() { +interface Props { + x?: number; + y?: number; + particleCount?: number; + deg?: number; + shapeSize?: number; + spreadDeg?: number; + effectInterval?: number; + effectCount?: number; + colors?: string[]; +} + +function Confetti({ + x = 0.5, + y = 0.5, + particleCount = 30, + deg = 270, + shapeSize = 12, + spreadDeg = 30, + effectInterval = 3000, + effectCount = 1, + colors = ['#ff577f', '#ff884b', '#ffd384', '#fff9b0'], +}: Props) { const canvasRef = useRef(null); - const ctxRef = useRef(null); - const particles: Particle[] = []; - const animationFrameRef = useRef(0); + const ctxRef = useRef(); + const particlesRef = useRef([]); + const animationFrameRef = useRef(0); + const effectCountRef = useRef(0); const init = useCallback(() => { const canvas = canvasRef.current; @@ -32,21 +55,23 @@ function Confetti() { (options: { x: number; y: number; - count: number; - deg?: number; - colors?: string[]; - shapes?: ('circle' | 'square')[]; - spread?: number; + particleCount: number; + deg: number; + shapeSize: number; + spreadDeg: number; + colors: string[]; + shapes?: readonly ['circle', 'square']; }) => { - for (let i = 0; i < options.count; i++) { - particles.push( + for (let i = 0; i < options.particleCount; i += 1) { + particlesRef.current.push( new Particle( options.x, options.y, options.deg, options.colors, options.shapes, - options.spread, + options.shapeSize, + options.spreadDeg, ), ); } @@ -55,61 +80,93 @@ function Confetti() { ); const render = useCallback(() => { - const canvas = canvasRef.current; - if (!ctxRef.current || !canvas) return; + if (!ctxRef.current) return; let now; let delta; let then = Date.now(); + let effectDelta; + let effectThen = Date.now() - effectInterval; const frame = () => { + const canvas = canvasRef.current; if (!ctxRef.current) return; + if (!canvas) return; animationFrameRef.current = requestAnimationFrame(frame); now = Date.now(); delta = now - then; + effectDelta = now - effectThen; if (delta < INTERVAL) return; ctxRef.current.clearRect(0, 0, canvas.width, canvas.height); - createConfetti({ - x: 0, // 0 ~ 1 - y: 0.5, // 0 ~ 1 - count: 10, - deg: -50, - }); - createConfetti({ - x: 1, // 0 ~ 1 - y: 0.5, // 0 ~ 1 - count: 10, - deg: 230, - }); - - for (let i = particles.length - 1; i >= 0; i--) { + if ( + effectDelta > effectInterval && + effectCountRef.current < effectCount + ) { + createConfetti({ + x, + y, + particleCount, + deg, + shapeSize, + spreadDeg, + colors, + }); + effectThen = now - (effectDelta % effectInterval); + effectCountRef.current += 1; + } + + const particles = particlesRef.current; + for (let i = particles.length - 1; i >= 0; i -= 1) { const p = particles[i]; p.update(); p.draw(ctxRef.current); - if (p.opacity <= 0) particles.splice(particles.indexOf(p), 1); - if (p.y > window.innerHeight) particles.splice(particles.indexOf(p), 1); + + const canvasHeight = canvas?.height || 0; + if (p.opacity <= 0 || p.y > canvasHeight) + particles.splice(particles.indexOf(p), 1); } then = now - (delta % INTERVAL); + + if (effectCountRef.current >= effectCount && particles.length === 0) { + cancelAnimationFrame(animationFrameRef.current); + } }; animationFrameRef.current = requestAnimationFrame(frame); - }, []); + }, [ + x, + y, + particleCount, + deg, + effectInterval, + shapeSize, + effectCount, + spreadDeg, + colors, + createConfetti, + ]); useEffect(() => { init(); render(); return () => { - if (animationFrameRef.current) + if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); + } }; - }, []); + }, [init, render]); + + useEffect(() => { + effectCountRef.current = 0; + // render(); + }, [effectCount]); return ; } -export { Confetti }; +export default Confetti; diff --git a/src/js/Particle.ts b/src/js/Particle.ts index 81645f2..a964554 100644 --- a/src/js/Particle.ts +++ b/src/js/Particle.ts @@ -1,4 +1,4 @@ -import { hexToRgb, randomNumBetween } from './utils'; +import { randomNumBetween, hexToRgb } from './utils'; class Particle { x: number; @@ -18,27 +18,32 @@ class Particle { rotateDelta: number; colors: string[]; color: { r: number; g: number; b: number }; - shapes: string[]; + shapes: readonly ['circle', 'square']; shape: string; + swingOffset: number; + swingSpeed: number; + swingAmplitude: number; constructor( x: number, y: number, deg = 0, - colors: string[] = ['#ff577f', '#ff884b', '#ffd384', '#fff9b0'], - shapes: ('circle' | 'square')[] = ['circle', 'square'], + colors: string[], + shapes = ['circle', 'square'] as const, + shapeSize = 12, spread = 30, ) { - this.x = x * window.innerWidth; - this.y = y * window.innerHeight; - this.width = 12; - this.height = 12; + const DPR = window.devicePixelRatio > 1 ? 2 : 1; + this.x = x * window.innerWidth * DPR; + this.y = y * window.innerHeight * DPR; + this.width = shapeSize; + this.height = shapeSize; this.theta = (Math.PI / 180) * randomNumBetween(deg - spread, deg + spread); - this.radius = randomNumBetween(30, 100); + this.radius = randomNumBetween(20, 70); this.vx = this.radius * Math.cos(this.theta); this.vy = this.radius * Math.sin(this.theta); - this.friction = 0.89; - this.gravity = 0.5; + this.friction = 0.87; + this.gravity = 0.55; this.opacity = 1; this.rotate = randomNumBetween(0, 360); this.widthDelta = randomNumBetween(0, 360); @@ -49,20 +54,24 @@ class Particle { this.colors[Math.floor(randomNumBetween(0, this.colors.length))], ); this.shapes = shapes; - this.shape = - this.shapes[Math.floor(randomNumBetween(0, this.shapes.length))]; + this.shape = this.shapes[Math.floor(randomNumBetween(0, this.shapes.length))]; + this.swingOffset = randomNumBetween(0, Math.PI * 2); + this.swingSpeed = Math.random() * 0.05 + 0.01; + this.swingAmplitude = randomNumBetween(0, 0.4); } update() { - this.vy += this.gravity; this.vx *= this.friction; this.vy *= this.friction; + this.vy += this.gravity; + if (this.vy > 0) this.vx += Math.sin(this.swingOffset) * this.swingAmplitude; this.x += this.vx; this.y += this.vy; - this.opacity -= 0.005; + this.opacity -= 0.004; this.widthDelta += 2; this.heightDelta += 2; this.rotate += this.rotateDelta; + this.swingOffset += this.swingSpeed; } drawSquare(ctx: CanvasRenderingContext2D) { @@ -95,6 +104,7 @@ class Particle { ctx.translate(this.x + translateXAlpha, this.y + translateYAlpha); ctx.rotate((Math.PI / 180) * this.rotate); ctx.translate(-(this.x + translateXAlpha), -(this.y + translateYAlpha)); + // eslint-disable-next-line no-param-reassign ctx.fillStyle = `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.opacity})`; if (this.shape === 'square') this.drawSquare(ctx); diff --git a/src/type.d.ts b/src/type.d.ts index 8802366..5060ece 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -1 +1,7 @@ declare module "*.module.scss"; + +type ConstructorParams = { + [K in keyof T as T[K] extends (...args: any[]) => any + ? never + : K]: T[K]; +};