├── .gitignore ├── .npmignore ├── package-lock.json ├── package.json ├── readme.md ├── src ├── fireworks.ts └── react.ts ├── test ├── custom.html ├── custom.js ├── images │ └── fireworks.gif ├── index.html ├── index.js ├── react.html └── react.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Temporary files 5 | *.log 6 | .cache 7 | .parcel-cache 8 | .DS_Store 9 | dist 10 | lib 11 | 12 | # Lockfiles 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Temporary files 5 | *.log 6 | .cache 7 | .parcel-cache 8 | .DS_Store 9 | dist 10 | 11 | # Lockfiles 12 | yarn.lock 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fireworks", 3 | "description": "A simple fireworks library with exploding bubbles.", 4 | "version": "2.2.7", 5 | "module": "./lib/fireworks.js", 6 | "types": "./lib/fireworks.d.ts", 7 | "license": "MIT", 8 | "author": "Fouad Matin ", 9 | "files": [ 10 | "lib" 11 | ], 12 | "scripts": { 13 | "build": "tsc", 14 | "watch": "tsc -w", 15 | "clean": "rimraf lib", 16 | "serve": "parcel serve test/*.html" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/fouad/fireworks.git" 21 | }, 22 | "peerDependencies": { 23 | "@types/react": ">=16.8.0", 24 | "react": ">=16.8.0" 25 | }, 26 | "devDependencies": { 27 | "highlight.js": "^10.5.0", 28 | "parcel": "^2.0.0-beta.1", 29 | "rimraf": "^2.6.3", 30 | "typescript": "^3.5.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Demo GIF of fireworks being rendered](./test/images/fireworks.gif) 2 | 3 |
4 |

5 | 🎆 fireworks 6 |

7 | 8 |

9 | Simple, zero-dependency library for
10 | rendering fireworks in JavaScript and React 11 |

12 |
13 | 14 |

15 | 16 | 17 |

18 |
19 | 20 | ```typescript 21 | import fx from 'fireworks' 22 | 23 | fx({ 24 | x: number // required 25 | y: number // required 26 | 27 | // optional 28 | count?: number 29 | colors?: string[] 30 | canvasWidth?: number 31 | canvasHeight?: number 32 | canvasTopOffset?: number 33 | canvasLeftOffset?: number 34 | bubbleSizeMinimum?: number 35 | bubbleSizeMaximum?: number 36 | bubbleSpeedMinimum?: number 37 | bubbleSpeedMaximum?: number 38 | particleTimeout?: number 39 | parentNode?: HTMLElement 40 | }) 41 | ``` 42 | 43 | ## Installation 44 | 45 | Install with npm: 46 | 47 | ``` 48 | $ npm install fireworks --save 49 | ``` 50 | 51 | Or with yarn: 52 | 53 | ``` 54 | $ yarn add fireworks 55 | ``` 56 | 57 | ### Usage 58 | 59 | Each time you call the `fireworks()` function, a canvas gets rendered with fireworks at position `(x,y)` like this: 60 | 61 | ```javascript 62 | const fireworks = require('fireworks') 63 | 64 | fireworks({ 65 | x: window.innerWidth / 2, 66 | y: window.innerHeight / 2, 67 | colors: ['#cc3333', '#4CAF50', '#81C784'] 68 | }) 69 | ``` 70 | 71 | If you want render multiple, you can loop through randomly: 72 | 73 | ```javascript 74 | import fx from 'fireworks' 75 | 76 | let range = n => [...new Array(n)] 77 | 78 | range(6).map(() => 79 | fx({ 80 | x: Math.random(window.innerWidth / 2) + window.innerWidth / 4, 81 | y: Math.random(window.innerWidth / 2) + window.innerWidth / 4, 82 | colors: ['#cc3333', '#4CAF50', '#81C784'] 83 | }) 84 | ) 85 | ``` 86 | 87 | For React apps, you can optionally use the component: 88 | 89 | ```javascript 90 | // You need to install React/React-DOM 91 | import { Fireworks } from 'fireworks/lib/react' 92 | 93 | function App() { 94 | let fxProps = { 95 | count: 3, 96 | interval: 200, 97 | colors: ['#cc3333', '#4CAF50', '#81C784'], 98 | calc: (props, i) => ({ 99 | ...props, 100 | x: (i + 1) * (window.innerWidth / 3) - (i + 1) * 100, 101 | y: 200 + Math.random() * 100 - 50 + (i === 2 ? -80 : 0) 102 | }) 103 | } 104 | 105 | return ( 106 |
107 | 108 |

Congrats!

109 |
110 | ) 111 | } 112 | ``` 113 | 114 | #### NodeConf Fireworks 115 | 116 | Looking for Eran Hammer's fireworks from NodeConf? Check out https://github.com/hueniverse/fireworks 117 | -------------------------------------------------------------------------------- /src/fireworks.ts: -------------------------------------------------------------------------------- 1 | export type FireworksInput = { 2 | x: number 3 | y: number 4 | count?: number 5 | colors?: string[] 6 | canvasWidth?: number 7 | canvasHeight?: number 8 | canvasTopOffset?: number 9 | canvasLeftOffset?: number 10 | bubbleSizeMinimum?: number 11 | bubbleSizeMaximum?: number 12 | bubbleSpeedMinimum?: number 13 | bubbleSpeedMaximum?: number 14 | particleTimeout?: number 15 | parentNode?: HTMLElement 16 | } 17 | 18 | export type Particle = { 19 | x: number 20 | y: number 21 | yVel: number 22 | speed: number 23 | radius: number 24 | opacity: number 25 | gravity: number 26 | rotation: number 27 | friction: number 28 | color: string 29 | } 30 | 31 | export default fireworks 32 | 33 | const defaultColors = ['#2d80ee', '#1b4e8f', '#112e55'] 34 | 35 | export function fireworks(opts: FireworksInput) { 36 | if (!opts) { 37 | throw new Error('Missing options for fireworks') 38 | } 39 | 40 | const { 41 | x, 42 | y, 43 | canvasWidth = 300, 44 | canvasHeight = 300, 45 | particleTimeout = 1000, 46 | colors = defaultColors, 47 | bubbleSizeMinimum = 10, 48 | bubbleSizeMaximum = 25, 49 | bubbleSpeedMinimum = 6, 50 | bubbleSpeedMaximum = 10, 51 | count: bubbleCount = 25, 52 | canvasLeftOffset = canvasWidth / 2, 53 | canvasTopOffset = canvasHeight / 2, 54 | parentNode = document.body 55 | } = opts 56 | 57 | const ratio = window.devicePixelRatio 58 | const cvs = document.createElement('canvas') 59 | const ctx = cvs.getContext('2d') 60 | 61 | if (!ctx) { 62 | console.log(`fireworks: unable to get 2d canvas context`) 63 | return 64 | } 65 | 66 | cvs.style.zIndex = '100' 67 | cvs.style.position = 'absolute' 68 | cvs.style.pointerEvents = 'none' 69 | cvs.style.top = `${y - canvasTopOffset}px` 70 | cvs.style.left = `${x - canvasLeftOffset}px` 71 | cvs.style.height = `${canvasHeight}px` 72 | cvs.style.width = `${canvasWidth}px` 73 | cvs.height = canvasHeight * ratio 74 | cvs.width = canvasWidth * ratio 75 | parentNode.appendChild(cvs) 76 | 77 | let particles = [] 78 | 79 | for (let i = 0; i < bubbleCount; i++) { 80 | particles.push({ 81 | x: cvs.width / 2, 82 | y: cvs.height / 2, 83 | color: colors[Math.floor(Math.random() * colors.length)], 84 | radius: randomRange(bubbleSizeMinimum, bubbleSizeMaximum), 85 | speed: randomRange(bubbleSpeedMinimum, bubbleSpeedMaximum), 86 | rotation: randomRange(0, 360, -1), 87 | opacity: randomRange(0, 0.5, -1), 88 | friction: 0.96, 89 | gravity: 0.05, 90 | yVel: 0, 91 | }) 92 | } 93 | 94 | render(cvs.width, cvs.height, particles, ctx) 95 | 96 | setTimeout(function() { 97 | parentNode.removeChild(cvs) 98 | }, particleTimeout) 99 | } 100 | 101 | function render( 102 | width: number, 103 | height: number, 104 | particles: Particle[], 105 | ctx: CanvasRenderingContext2D 106 | ) { 107 | requestAnimationFrame(() => { 108 | render(width, height, particles, ctx) 109 | }) 110 | 111 | ctx.clearRect(0, 0, width, height) 112 | particles.forEach(function(p: Particle, i: number) { 113 | p.x += p.speed * Math.cos((p.rotation * Math.PI) / 180) 114 | p.y += p.speed * Math.sin((p.rotation * Math.PI) / 180) 115 | p.opacity -= 0.005 116 | p.speed *= p.friction 117 | p.radius *= p.friction 118 | p.yVel += p.gravity 119 | p.y += p.yVel 120 | 121 | if (p.opacity < 0 || p.radius < 0) { 122 | return 123 | } 124 | 125 | ctx.beginPath() 126 | ctx.globalAlpha = p.opacity 127 | ctx.fillStyle = p.color 128 | ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, false) 129 | ctx.fill() 130 | }) 131 | 132 | return ctx 133 | } 134 | 135 | function randomRange(a: number, b: number, c: number = 0) { 136 | return parseFloat( 137 | (Math.random() * ((a ? a : 1) - (b ? b : 0)) + (b ? b : 0)).toFixed( 138 | c > 0 ? c : 0 139 | ) 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /src/react.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import fx, { FireworksInput } from './fireworks' 3 | 4 | export type FireworksProps = { 5 | /** Interval in milliseconds for how often new fireworks get rendered */ 6 | interval?: number 7 | /** Count of the fireworks that are rendered concurrently */ 8 | count?: number 9 | /** Calc is a function that can be evaluated to generate `FireworksInput` */ 10 | calc?: (input: any, index: number) => FireworksInput 11 | } 12 | 13 | export class Fireworks extends React.Component { 14 | // A reference for `setInterval` instance 15 | _ivl = 0 16 | // A check if the browser is idle to decide if fireworks 17 | // re-render should be attempted. 18 | _idle = false 19 | // Reference to the base HTML element 20 | _ref: HTMLElement | null = null 21 | 22 | render() { 23 | return React.createElement('div', { 24 | ref: (ref: HTMLElement) => (this._ref = ref), 25 | className: 'react-fireworks' 26 | }) 27 | } 28 | 29 | componentDidMount() { 30 | let self = this 31 | let { interval } = this.props 32 | 33 | if (interval) { 34 | // TODO: check if tab is idle 35 | // 36 | // window.requestIdleCallback(function() { 37 | // self.onIdle() 38 | // }) 39 | 40 | this._ivl = window.setInterval(function() { 41 | if (self._idle) return 42 | 43 | self.evaluate() 44 | }, interval) 45 | 46 | this.evaluate() 47 | } else { 48 | this.evaluate() 49 | } 50 | } 51 | 52 | onIdle() { 53 | this._idle = true 54 | } 55 | 56 | componentWillUnmount() { 57 | if (this._ivl) { 58 | clearInterval(this._ivl) 59 | } 60 | } 61 | 62 | evaluate() { 63 | let { count, calc, interval, ...props } = this.props 64 | let input = props as FireworksInput 65 | if (!input.parentNode) { 66 | if (this._ref) { 67 | input.parentNode = this._ref 68 | } 69 | } 70 | for (let i = 0; i < (count || 1); i++) { 71 | fx(calc ? calc(props, i) : input) 72 | } 73 | } 74 | } 75 | 76 | // Add type-safe `window.requestIdleCallback` 77 | // 78 | // From https://github.com/Microsoft/TypeScript/issues/21309 79 | type RequestIdleCallbackHandle = any 80 | type RequestIdleCallbackOptions = { 81 | timeout: number 82 | } 83 | type RequestIdleCallbackDeadline = { 84 | readonly didTimeout: boolean 85 | timeRemaining: () => number 86 | } 87 | 88 | declare global { 89 | interface Window { 90 | requestIdleCallback: ( 91 | callback: (deadline: RequestIdleCallbackDeadline) => void, 92 | opts?: RequestIdleCallbackOptions 93 | ) => RequestIdleCallbackHandle 94 | cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void 95 | } 96 | } 97 | 98 | export default Fireworks 99 | -------------------------------------------------------------------------------- /test/custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/custom.js: -------------------------------------------------------------------------------- 1 | import { fireworks } from '../lib/fireworks' 2 | 3 | let range = n => [...new Array(n)].map((_, i) => i) 4 | let r = () => 5 | range(10).map(i => 6 | fireworks({ 7 | canvasWidth: 800, 8 | canvasHeight: 800, 9 | particleTimeout: 1200, 10 | bubbleSizeMinimum: 20, 11 | bubbleSizeMaximum: 30, 12 | bubbleSpeedMinimum: 10, 13 | bubbleSpeedMaximum: 15, 14 | colors: ['#e40624', '#2c2954', '#c0c1d9'], 15 | x: (window.innerWidth / 3) * 2 - ((i + 1) % 5) * 100, 16 | y: 200 + Math.random() * 100 - 50 + (i === 2 ? -80 : 0) 17 | }) 18 | ) 19 | 20 | r() 21 | setInterval(() => r(), 2000) 22 | -------------------------------------------------------------------------------- /test/images/fireworks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fouad/fireworks/e5c5ce667246efbcaeacf39acda1e3ac6f815079/test/images/fireworks.gif -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { fireworks } from '../lib/fireworks' 2 | 3 | let range = n => [...new Array(n)].map((_, i) => i) 4 | let r = () => 5 | range(10).map(i => 6 | fireworks({ 7 | colors: ['#47B881', '#1070CA', '#00783E'], 8 | x: (window.innerWidth / 3) * 2 - ((i + 1) % 5) * 100, 9 | y: 200 + Math.random() * 100 - 50 + (i === 2 ? -80 : 0) 10 | }) 11 | ) 12 | 13 | r() 14 | setTimeout(() => r(), 2000) 15 | -------------------------------------------------------------------------------- /test/react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Fireworks: React Test

4 |
5 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/react.js: -------------------------------------------------------------------------------- 1 | import { Fireworks } from '../lib/react' 2 | 3 | const root = document.querySelector('#root') 4 | 5 | ReactDOM.render( 6 | React.createElement(Fireworks, { 7 | count: 10, 8 | interval: 1200, 9 | colors: ['#47B881', '#1070CA', '#00783E'], 10 | calc: (props, i) => ({ 11 | ...props, 12 | x: (window.innerWidth / 3) * 2 - ((i + 1) % 5) * 100, 13 | y: 200 + Math.random() * 100 - 50 + (i === 2 ? -80 : 0) 14 | }) 15 | }), 16 | root 17 | ) 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "lib", 5 | "target": "es2015", 6 | "module": "commonjs", 7 | "declaration": true, 8 | "removeComments": true, 9 | "esModuleInterop": true, 10 | "noUnusedLocals": false, 11 | "noImplicitReturns": true, 12 | "moduleResolution": "node", 13 | "preserveConstEnums": true, 14 | "noUnusedParameters": false, 15 | "emitDecoratorMetadata": true, 16 | "experimentalDecorators": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "lib": ["es2017", "es7", "es6", "dom"] 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules"] 23 | } 24 | --------------------------------------------------------------------------------