// 1. Load the spritesheet (horizontal strip)
const fxImage = new Image();
fxImage.src = './Sporelight Bloom Node_Sheet.png';
// 2. Spritesheet config
const TOTAL_FRAMES = 16;
const SIZE = 256;
let FRAME_W, FRAME_H;
fxImage.onload = () => {
FRAME_W = fxImage.width / TOTAL_FRAMES; // single horizontal row
FRAME_H = fxImage.height;
};
// 3. Effect class (renders at 256x256)
class VFX {
constructor(x, y) {
this.x = x; this.y = y;
this.frame = 0;
this.done = false;
this.last = 0;
this.fps = 1000 / 30;
}
update(now) {
if (now - this.last > this.fps) {
this.frame++;
this.last = now;
if (this.frame >= TOTAL_FRAMES)
this.done = true;
}
}
draw(ctx) {
if (this.done) return;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.drawImage(fxImage,
this.frame * FRAME_W, 0, FRAME_W, FRAME_H, // source: Nth slot in strip
this.x - SIZE/2, this.y - SIZE/2,
SIZE, SIZE);
ctx.restore();
}
}
// 4. Game loop
const effects = [];
function loop(now) {
ctx.clearRect(0,0,canvas.width,canvas.height);
for (let i = effects.length-1; i>=0; i--)
if (effects[i].done) effects.splice(i,1);
effects.forEach(e => { e.update(now); e.draw(ctx); });
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// 5. Spawn on click
canvas.addEventListener('mousedown', e => {
const r = canvas.getBoundingClientRect();
const x = (e.clientX - r.left) * (canvas.width / r.width);
const y = (e.clientY - r.top) * (canvas.height / r.height);
effects.push(new VFX(x, y));
});
/* Pure CSS Solution (horizontal strip) */
.vfx-sprite {
width: 256px;
height: 256px;
background-image: url('./Sporelight Bloom Node_Sheet.png');
background-size: 1600% 100%;
animation: vfx-play 0.53s steps(1) infinite;
}
@keyframes vfx-play {
0.00% { background-position: 0.00% 0%; }
6.25% { background-position: 6.67% 0%; }
12.50% { background-position: 13.33% 0%; }
18.75% { background-position: 20.00% 0%; }
25.00% { background-position: 26.67% 0%; }
31.25% { background-position: 33.33% 0%; }
37.50% { background-position: 40.00% 0%; }
43.75% { background-position: 46.67% 0%; }
50.00% { background-position: 53.33% 0%; }
56.25% { background-position: 60.00% 0%; }
62.50% { background-position: 66.67% 0%; }
68.75% { background-position: 73.33% 0%; }
75.00% { background-position: 80.00% 0%; }
81.25% { background-position: 86.67% 0%; }
87.50% { background-position: 93.33% 0%; }
93.75% { background-position: 100.00% 0%; }
}
/* One-shot variant (plays once then hides) */
.vfx-sprite.once {
animation: vfx-play 0.53s steps(1) forwards;
}
<!-- HTML usage -->
<div class="vfx-sprite"></div>