Skip to content

Instantly share code, notes, and snippets.

@crashmax-dev
Created November 27, 2023 10:21
Show Gist options
  • Save crashmax-dev/0e686a9f790bd59384272421d9d5c64f to your computer and use it in GitHub Desktop.
Save crashmax-dev/0e686a9f790bd59384272421d9d5c64f to your computer and use it in GitHub Desktop.
/**
* 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