Yo folks, I’m bobamilk and I’m messing with a small canvas toy at lunch. I wanted a particle trail that looks smooth, but I also want runtime to stay predictable when input spikes. My failure mode was “looks cool for 10s then CPU climbs” because I kept too much history.
const buckets = 120; // ~2s if step=16ms
const stepMs = 16;
const q = Array.from({ length: buckets }, () => []);
let head = 0;
let last = performance.now();
export function addParticle(p) {
q[head].push(p);
}
export function tick(ctx, now = performance.now()) {
while (now - last >= stepMs) {
head = (head + 1) % buckets;
q[head].length = 0; // drop old bucket in O(1)
last += stepMs;
}
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "rgba(0,0,0,0.12)"; // fade
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "rgba(120,220,255,0.9)";
for (let i = 0; i < buckets; i++) {
for (const p of q[i]) {
ctx.fillRect(p.x | 0, p.y | 0, 1, 1);
}
}
}
Neat part is I can reason about complexity: memory is O(buckets + particles_in_window) and cleanup is O(1) per step, but draw cost is still O(particles_in_window). I’m thinking about sampling or per-bucket pixel stamping next if draw becomes the bottleneck.