jev-8ball/public/js/particles.js

104 lines
2.9 KiB
JavaScript

/**
* Ambient particle field behind the interface.
*
* Cheap by construction: a small fixed particle count, device-pixel-ratio aware,
* paused whenever the tab is hidden, and drawn as plain rects so there is no
* per-particle path allocation.
*/
export class ParticleField {
constructor(canvas, { count = 46, tone = '#38bdf8' } = {}) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.count = count;
this.tone = tone;
this.particles = [];
this.running = false;
this.raf = 0;
this.dpr = 1;
this.width = 0;
this.height = 0;
this.clock = 0;
this.last = 0;
this.resize();
window.addEventListener('resize', this.resize);
}
resize = () => {
const width = window.innerWidth;
const height = window.innerHeight;
this.dpr = Math.min(window.devicePixelRatio || 1, 2);
this.width = width;
this.height = height;
this.canvas.width = Math.round(width * this.dpr);
this.canvas.height = Math.round(height * this.dpr);
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
this.seed();
};
seed() {
this.particles = Array.from({ length: this.count }, () => ({
x: Math.random() * this.width,
y: Math.random() * this.height,
size: Math.random() * 1.6 + 0.5,
vx: (Math.random() - 0.5) * 12,
vy: (Math.random() - 0.5) * 12,
alpha: Math.random() * 0.3 + 0.08,
phase: Math.random() * Math.PI * 2
}));
}
setTone(tone) {
this.tone = tone;
}
start() {
if (this.running) return;
this.running = true;
this.last = performance.now();
this.raf = requestAnimationFrame(this.frame);
}
stop() {
this.running = false;
cancelAnimationFrame(this.raf);
}
frame = (now) => {
if (!this.running) return;
const dt = Math.min((now - this.last) / 1000, 0.05);
this.last = now;
if (!document.hidden) {
this.clock += dt;
this.update(dt);
this.draw();
}
this.raf = requestAnimationFrame(this.frame);
};
update(dt) {
for (const particle of this.particles) {
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
if (particle.x < -4) particle.x = this.width + 4;
if (particle.x > this.width + 4) particle.x = -4;
if (particle.y < -4) particle.y = this.height + 4;
if (particle.y > this.height + 4) particle.y = -4;
}
}
draw() {
const ctx = this.ctx;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
ctx.fillStyle = this.tone;
for (const particle of this.particles) {
const twinkle = 0.72 + Math.sin(this.clock * 0.9 + particle.phase) * 0.28;
ctx.globalAlpha = particle.alpha * twinkle;
ctx.fillRect(particle.x, particle.y, particle.size, particle.size);
}
ctx.globalAlpha = 1;
}
}