Created
November 27, 2023 10:21
-
-
Save crashmax-dev/0e686a9f790bd59384272421d9d5c64f to your computer and use it in GitHub Desktop.
This file contains 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