Created
November 27, 2023 10:21
-
-
Save crashmax-dev/0e686a9f790bd59384272421d9d5c64f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * name: fireworks-js | |
| * version: 2.10.7 | |
| * author: Vitalij Ryndin (https://crashmax.ru) | |
| * homepage: https://fireworks.js.org | |
| * license MIT | |
| */ | |
| function floor(num) { | |
| return Math.abs(Math.floor(num)); | |
| } | |
| function randomFloat(min, max) { | |
| return Math.random() * (max - min) + min; | |
| } | |
| function randomInt(min, max) { | |
| return Math.floor(randomFloat(min, max + 1)); | |
| } | |
| function getDistance(x, y, dx, dy) { | |
| const pow = Math.pow; | |
| return Math.sqrt(pow(x - dx, 2) + pow(y - dy, 2)); | |
| } | |
| function hsla(hue, lightness, alpha = 1) { | |
| if (hue > 360 || hue < 0) { | |
| throw new Error(`Expected hue 0-360 range, got \`${hue}\``); | |
| } | |
| if (lightness > 100 || lightness < 0) { | |
| throw new Error(`Expected lightness 0-100 range, got \`${lightness}\``); | |
| } | |
| if (alpha > 1 || alpha < 0) { | |
| throw new Error(`Expected alpha 0-1 range, got \`${alpha}\``); | |
| } | |
| return `hsla(${hue}, 100%, ${lightness}%, ${alpha})`; | |
| } | |
| const isObject = (obj) => { | |
| if (typeof obj === "object" && obj !== null) { | |
| if (typeof Object.getPrototypeOf === "function") { | |
| const prototype = Object.getPrototypeOf(obj); | |
| return prototype === Object.prototype || prototype === null; | |
| } | |
| return Object.prototype.toString.call(obj) === "[object Object]"; | |
| } | |
| return false; | |
| }; | |
| const PROTECTED_KEYS = [ | |
| "__proto__", | |
| "constructor", | |
| "prototype" | |
| ]; | |
| const deepMerge = (...objects) => { | |
| return objects.reduce((result, current) => { | |
| Object.keys(current).forEach((key) => { | |
| if (PROTECTED_KEYS.includes(key)) { | |
| return; | |
| } | |
| if (Array.isArray(result[key]) && Array.isArray(current[key])) { | |
| result[key] = current[key]; | |
| } else if (isObject(result[key]) && isObject(current[key])) { | |
| result[key] = deepMerge(result[key], current[key]); | |
| } else { | |
| result[key] = current[key]; | |
| } | |
| }); | |
| return result; | |
| }, {}); | |
| }; | |
| function debounce(fn, ms) { | |
| let timeoutId; | |
| return (...args) => { | |
| if (timeoutId) { | |
| clearTimeout(timeoutId); | |
| } | |
| timeoutId = setTimeout(() => fn(...args), ms); | |
| }; | |
| } | |
| class Explosion { | |
| x; | |
| y; | |
| ctx; | |
| hue; | |
| friction; | |
| gravity; | |
| flickering; | |
| lineWidth; | |
| explosionLength; | |
| angle; | |
| speed; | |
| brightness; | |
| coordinates = []; | |
| decay; | |
| alpha = 1; | |
| constructor({ | |
| x, | |
| y, | |
| ctx, | |
| hue, | |
| decay, | |
| gravity, | |
| friction, | |
| brightness, | |
| flickering, | |
| lineWidth, | |
| explosionLength | |
| }) { | |
| this.x = x; | |
| this.y = y; | |
| this.ctx = ctx; | |
| this.hue = hue; | |
| this.gravity = gravity; | |
| this.friction = friction; | |
| this.flickering = flickering; | |
| this.lineWidth = lineWidth; | |
| this.explosionLength = explosionLength; | |
| this.angle = randomFloat(0, Math.PI * 2); | |
| this.speed = randomInt(1, 10); | |
| this.brightness = randomInt(brightness.min, brightness.max); | |
| this.decay = randomFloat(decay.min, decay.max); | |
| while (this.explosionLength--) { | |
| this.coordinates.push([x, y]); | |
| } | |
| } | |
| update(callback) { | |
| this.coordinates.pop(); | |
| this.coordinates.unshift([this.x, this.y]); | |
| this.speed *= this.friction; | |
| this.x += Math.cos(this.angle) * this.speed; | |
| this.y += Math.sin(this.angle) * this.speed + this.gravity; | |
| this.alpha -= this.decay; | |
| if (this.alpha <= this.decay) { | |
| callback(); | |
| } | |
| } | |
| draw() { | |
| const lastIndex = this.coordinates.length - 1; | |
| this.ctx.beginPath(); | |
| this.ctx.lineWidth = this.lineWidth; | |
| this.ctx.fillStyle = hsla(this.hue, this.brightness, this.alpha); | |
| this.ctx.moveTo( | |
| this.coordinates[lastIndex][0], | |
| this.coordinates[lastIndex][1] | |
| ); | |
| this.ctx.lineTo(this.x, this.y); | |
| this.ctx.strokeStyle = hsla( | |
| this.hue, | |
| this.flickering ? randomFloat(0, this.brightness) : this.brightness, | |
| this.alpha | |
| ); | |
| this.ctx.stroke(); | |
| } | |
| } | |
| class Mouse { | |
| constructor(options, canvas) { | |
| this.options = options; | |
| this.canvas = canvas; | |
| this.pointerDown = this.pointerDown.bind(this); | |
| this.pointerUp = this.pointerUp.bind(this); | |
| this.pointerMove = this.pointerMove.bind(this); | |
| } | |
| active = false; | |
| x; | |
| y; | |
| get mouseOptions() { | |
| return this.options.mouse; | |
| } | |
| mount() { | |
| this.canvas.addEventListener("pointerdown", this.pointerDown); | |
| this.canvas.addEventListener("pointerup", this.pointerUp); | |
| this.canvas.addEventListener("pointermove", this.pointerMove); | |
| } | |
| unmount() { | |
| this.canvas.removeEventListener("pointerdown", this.pointerDown); | |
| this.canvas.removeEventListener("pointerup", this.pointerUp); | |
| this.canvas.removeEventListener("pointermove", this.pointerMove); | |
| } | |
| usePointer(event, active) { | |
| const { click, move } = this.mouseOptions; | |
| if (click || move) { | |
| this.x = event.pageX - this.canvas.offsetLeft; | |
| this.y = event.pageY - this.canvas.offsetTop; | |
| this.active = active; | |
| } | |
| } | |
| pointerDown(event) { | |
| this.usePointer(event, this.mouseOptions.click); | |
| } | |
| pointerUp(event) { | |
| this.usePointer(event, false); | |
| } | |
| pointerMove(event) { | |
| this.usePointer(event, this.active); | |
| } | |
| } | |
| class Options { | |
| hue; | |
| rocketsPoint; | |
| opacity; | |
| acceleration; | |
| friction; | |
| gravity; | |
| particles; | |
| explosion; | |
| mouse; | |
| boundaries; | |
| sound; | |
| delay; | |
| brightness; | |
| decay; | |
| flickering; | |
| intensity; | |
| traceLength; | |
| traceSpeed; | |
| lineWidth; | |
| lineStyle; | |
| autoresize; | |
| constructor() { | |
| this.autoresize = true; | |
| this.lineStyle = "round"; | |
| this.flickering = 50; | |
| this.traceLength = 3; | |
| this.traceSpeed = 10; | |
| this.intensity = 30; | |
| this.explosion = 5; | |
| this.gravity = 1.5; | |
| this.opacity = 0.5; | |
| this.particles = 50; | |
| this.friction = 0.95; | |
| this.acceleration = 1.05; | |
| this.hue = { | |
| min: 0, | |
| max: 360 | |
| }; | |
| this.rocketsPoint = { | |
| min: 50, | |
| max: 50 | |
| }; | |
| this.lineWidth = { | |
| explosion: { | |
| min: 1, | |
| max: 3 | |
| }, | |
| trace: { | |
| min: 1, | |
| max: 2 | |
| } | |
| }; | |
| this.mouse = { | |
| click: false, | |
| move: false, | |
| max: 1 | |
| }; | |
| this.delay = { | |
| min: 30, | |
| max: 60 | |
| }; | |
| this.brightness = { | |
| min: 50, | |
| max: 80 | |
| }; | |
| this.decay = { | |
| min: 0.015, | |
| max: 0.03 | |
| }; | |
| this.sound = { | |
| enabled: false, | |
| files: [ | |
| "explosion0.mp3", | |
| "explosion1.mp3", | |
| "explosion2.mp3" | |
| ], | |
| volume: { | |
| min: 4, | |
| max: 8 | |
| } | |
| }; | |
| this.boundaries = { | |
| debug: false, | |
| height: 0, | |
| width: 0, | |
| x: 50, | |
| y: 50 | |
| }; | |
| } | |
| update(options) { | |
| Object.assign(this, deepMerge(this, options)); | |
| } | |
| } | |
| class RequestAnimationFrame { | |
| constructor(options, render) { | |
| this.options = options; | |
| this.render = render; | |
| } | |
| tick = 0; | |
| rafId = 0; | |
| fps = 60; | |
| tolerance = 0.1; | |
| now; | |
| mount() { | |
| this.now = performance.now(); | |
| const interval = 1e3 / this.fps; | |
| const raf = (timestamp) => { | |
| this.rafId = requestAnimationFrame(raf); | |
| const delta = timestamp - this.now; | |
| if (delta >= interval - this.tolerance) { | |
| this.render(); | |
| this.now = timestamp - delta % interval; | |
| this.tick += delta * (this.options.intensity * Math.PI) / 1e3; | |
| } | |
| }; | |
| this.rafId = requestAnimationFrame(raf); | |
| } | |
| unmount() { | |
| cancelAnimationFrame(this.rafId); | |
| } | |
| } | |
| class Resize { | |
| constructor(options, updateSize, container) { | |
| this.options = options; | |
| this.updateSize = updateSize; | |
| this.container = container; | |
| } | |
| resizer; | |
| mount() { | |
| if (!this.resizer) { | |
| const debouncedResize = debounce(() => this.updateSize(), 100); | |
| this.resizer = new ResizeObserver(debouncedResize); | |
| } | |
| if (this.options.autoresize) { | |
| this.resizer.observe(this.container); | |
| } | |
| } | |
| unmount() { | |
| if (this.resizer) { | |
| this.resizer.unobserve(this.container); | |
| } | |
| } | |
| } | |
| class Sound { | |
| constructor(options) { | |
| this.options = options; | |
| this.init(); | |
| } | |
| buffers = []; | |
| audioContext; | |
| onInit = false; | |
| get isEnabled() { | |
| return this.options.sound.enabled; | |
| } | |
| get soundOptions() { | |
| return this.options.sound; | |
| } | |
| init() { | |
| if (!this.onInit && this.isEnabled) { | |
| this.onInit = true; | |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| this.loadSounds(); | |
| } | |
| } | |
| async loadSounds() { | |
| for (const file of this.soundOptions.files) { | |
| const response = await (await fetch(file)).arrayBuffer(); | |
| this.audioContext.decodeAudioData(response).then((buffer) => { | |
| this.buffers.push(buffer); | |
| }).catch((err) => { | |
| throw err; | |
| }); | |
| } | |
| } | |
| play() { | |
| if (this.isEnabled && this.buffers.length) { | |
| const bufferSource = this.audioContext.createBufferSource(); | |
| const soundBuffer = this.buffers[randomInt(0, this.buffers.length - 1)]; | |
| const volume = this.audioContext.createGain(); | |
| bufferSource.buffer = soundBuffer; | |
| volume.gain.value = randomFloat( | |
| this.soundOptions.volume.min / 100, | |
| this.soundOptions.volume.max / 100 | |
| ); | |
| volume.connect(this.audioContext.destination); | |
| bufferSource.connect(volume); | |
| bufferSource.start(0); | |
| } else { | |
| this.init(); | |
| } | |
| } | |
| } | |
| class Trace { | |
| x; | |
| y; | |
| sx; | |
| sy; | |
| dx; | |
| dy; | |
| ctx; | |
| hue; | |
| speed; | |
| acceleration; | |
| traceLength; | |
| totalDistance; | |
| angle; | |
| brightness; | |
| coordinates = []; | |
| currentDistance = 0; | |
| constructor({ | |
| x, | |
| y, | |
| dx, | |
| dy, | |
| ctx, | |
| hue, | |
| speed, | |
| traceLength, | |
| acceleration | |
| }) { | |
| this.x = x; | |
| this.y = y; | |
| this.sx = x; | |
| this.sy = y; | |
| this.dx = dx; | |
| this.dy = dy; | |
| this.ctx = ctx; | |
| this.hue = hue; | |
| this.speed = speed; | |
| this.traceLength = traceLength; | |
| this.acceleration = acceleration; | |
| this.totalDistance = getDistance(x, y, dx, dy); | |
| this.angle = Math.atan2(dy - y, dx - x); | |
| this.brightness = randomInt(50, 70); | |
| while (this.traceLength--) { | |
| this.coordinates.push([x, y]); | |
| } | |
| } | |
| update(callback) { | |
| this.coordinates.pop(); | |
| this.coordinates.unshift([this.x, this.y]); | |
| this.speed *= this.acceleration; | |
| const vx = Math.cos(this.angle) * this.speed; | |
| const vy = Math.sin(this.angle) * this.speed; | |
| this.currentDistance = getDistance( | |
| this.sx, | |
| this.sy, | |
| this.x + vx, | |
| this.y + vy | |
| ); | |
| if (this.currentDistance >= this.totalDistance) { | |
| callback(this.dx, this.dy, this.hue); | |
| } else { | |
| this.x += vx; | |
| this.y += vy; | |
| } | |
| } | |
| draw() { | |
| const lastIndex = this.coordinates.length - 1; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo( | |
| this.coordinates[lastIndex][0], | |
| this.coordinates[lastIndex][1] | |
| ); | |
| this.ctx.lineTo(this.x, this.y); | |
| this.ctx.strokeStyle = hsla(this.hue, this.brightness); | |
| this.ctx.stroke(); | |
| } | |
| } | |
| class Fireworks { | |
| target; | |
| container; | |
| canvas; | |
| ctx; | |
| width; | |
| height; | |
| traces = []; | |
| explosions = []; | |
| waitStopRaf; | |
| running = false; | |
| opts; | |
| sound; | |
| resize; | |
| mouse; | |
| raf; | |
| constructor(container, options = {}) { | |
| this.target = container; | |
| this.container = container; | |
| this.opts = new Options(); | |
| this.createCanvas(this.target); | |
| this.updateOptions(options); | |
| this.sound = new Sound(this.opts); | |
| this.resize = new Resize( | |
| this.opts, | |
| this.updateSize.bind(this), | |
| this.container | |
| ); | |
| this.mouse = new Mouse(this.opts, this.canvas); | |
| this.raf = new RequestAnimationFrame(this.opts, this.render.bind(this)); | |
| } | |
| get isRunning() { | |
| return this.running; | |
| } | |
| get version() { | |
| return "2.10.7"; | |
| } | |
| get currentOptions() { | |
| return this.opts; | |
| } | |
| start() { | |
| if (this.running) | |
| return; | |
| if (!this.canvas.isConnected) { | |
| this.createCanvas(this.target); | |
| } | |
| this.running = true; | |
| this.resize.mount(); | |
| this.mouse.mount(); | |
| this.raf.mount(); | |
| } | |
| stop(dispose = false) { | |
| if (!this.running) | |
| return; | |
| this.running = false; | |
| this.resize.unmount(); | |
| this.mouse.unmount(); | |
| this.raf.unmount(); | |
| this.clear(); | |
| if (dispose) { | |
| this.canvas.remove(); | |
| } | |
| } | |
| async waitStop(dispose) { | |
| if (!this.running) | |
| return; | |
| return new Promise((resolve) => { | |
| this.waitStopRaf = () => { | |
| if (!this.waitStopRaf) | |
| return; | |
| requestAnimationFrame(this.waitStopRaf); | |
| if (!this.traces.length && !this.explosions.length) { | |
| this.waitStopRaf = null; | |
| this.stop(dispose); | |
| resolve(); | |
| } | |
| }; | |
| this.waitStopRaf(); | |
| }); | |
| } | |
| pause() { | |
| this.running = !this.running; | |
| if (this.running) { | |
| this.raf.mount(); | |
| } else { | |
| this.raf.unmount(); | |
| } | |
| } | |
| clear() { | |
| if (!this.ctx) | |
| return; | |
| this.traces = []; | |
| this.explosions = []; | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| } | |
| launch(count = 1) { | |
| for (let i = 0; i < count; i++) { | |
| this.createTrace(); | |
| } | |
| if (!this.waitStopRaf) { | |
| this.start(); | |
| this.waitStop(); | |
| } | |
| } | |
| updateOptions(options) { | |
| this.opts.update(options); | |
| } | |
| updateSize({ | |
| width = this.container.clientWidth, | |
| height = this.container.clientHeight | |
| } = {}) { | |
| this.width = width; | |
| this.height = height; | |
| this.canvas.width = width; | |
| this.canvas.height = height; | |
| this.updateBoundaries({ | |
| ...this.opts.boundaries, | |
| width, | |
| height | |
| }); | |
| } | |
| updateBoundaries(boundaries) { | |
| this.updateOptions({ boundaries }); | |
| } | |
| createCanvas(el) { | |
| if (el instanceof HTMLCanvasElement) { | |
| if (!el.isConnected) { | |
| document.body.append(el); | |
| } | |
| this.canvas = el; | |
| } else { | |
| this.canvas = document.createElement("canvas"); | |
| this.container.append(this.canvas); | |
| } | |
| this.ctx = this.canvas.getContext("2d"); | |
| this.updateSize(); | |
| } | |
| render() { | |
| if (!this.ctx || !this.running) | |
| return; | |
| const { opacity, lineStyle, lineWidth } = this.opts; | |
| this.ctx.globalCompositeOperation = "destination-out"; | |
| this.ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| this.ctx.globalCompositeOperation = "lighter"; | |
| this.ctx.lineCap = lineStyle; | |
| this.ctx.lineJoin = "round"; | |
| this.ctx.lineWidth = randomFloat(lineWidth.trace.min, lineWidth.trace.max); | |
| this.initTrace(); | |
| this.drawTrace(); | |
| this.drawExplosion(); | |
| } | |
| createTrace() { | |
| const { | |
| hue, | |
| rocketsPoint, | |
| boundaries, | |
| traceLength, | |
| traceSpeed, | |
| acceleration, | |
| mouse | |
| } = this.opts; | |
| this.traces.push( | |
| new Trace({ | |
| x: this.width * randomInt(rocketsPoint.min, rocketsPoint.max) / 100, | |
| y: this.height, | |
| dx: this.mouse.x && mouse.move || this.mouse.active ? this.mouse.x : randomInt(boundaries.x, boundaries.width - boundaries.x * 2), | |
| dy: this.mouse.y && mouse.move || this.mouse.active ? this.mouse.y : randomInt(boundaries.y, boundaries.height * 0.5), | |
| ctx: this.ctx, | |
| hue: randomInt(hue.min, hue.max), | |
| speed: traceSpeed, | |
| acceleration, | |
| traceLength: floor(traceLength) | |
| }) | |
| ); | |
| } | |
| initTrace() { | |
| if (this.waitStopRaf) | |
| return; | |
| const { delay, mouse } = this.opts; | |
| if (this.raf.tick > randomInt(delay.min, delay.max) || this.mouse.active && mouse.max > this.traces.length) { | |
| this.createTrace(); | |
| this.raf.tick = 0; | |
| } | |
| } | |
| drawTrace() { | |
| let traceLength = this.traces.length; | |
| while (traceLength--) { | |
| this.traces[traceLength].draw(); | |
| this.traces[traceLength].update((x, y, hue) => { | |
| this.initExplosion(x, y, hue); | |
| this.sound.play(); | |
| this.traces.splice(traceLength, 1); | |
| }); | |
| } | |
| } | |
| initExplosion(x, y, hue) { | |
| const { | |
| particles, | |
| flickering, | |
| lineWidth, | |
| explosion, | |
| brightness, | |
| friction, | |
| gravity, | |
| decay | |
| } = this.opts; | |
| let particlesLength = floor(particles); | |
| while (particlesLength--) { | |
| this.explosions.push( | |
| new Explosion({ | |
| x, | |
| y, | |
| ctx: this.ctx, | |
| hue, | |
| friction, | |
| gravity, | |
| flickering: randomInt(0, 100) <= flickering, | |
| lineWidth: randomFloat( | |
| lineWidth.explosion.min, | |
| lineWidth.explosion.max | |
| ), | |
| explosionLength: floor(explosion), | |
| brightness, | |
| decay | |
| }) | |
| ); | |
| } | |
| } | |
| drawExplosion() { | |
| let length = this.explosions.length; | |
| while (length--) { | |
| this.explosions[length].draw(); | |
| this.explosions[length].update(() => { | |
| this.explosions.splice(length, 1); | |
| }); | |
| } | |
| } | |
| } | |
| export { Fireworks, Fireworks as default }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment