// 1. Load the spritesheet (horizontal strip)
const fxImage = new Image();
fxImage.src = './Runebound Mace Conjure_Sheet.png';
// 2. Spritesheet config
const TOTAL_FRAMES = 15;
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('./Runebound Mace Conjure_Sheet.png');
background-size: 1500% 100%;
animation: vfx-play 0.50s steps(1) infinite;
}
@keyframes vfx-play {
0.00% { background-position: 0.00% 0%; }
6.67% { background-position: 7.14% 0%; }
13.33% { background-position: 14.29% 0%; }
20.00% { background-position: 21.43% 0%; }
26.67% { background-position: 28.57% 0%; }
33.33% { background-position: 35.71% 0%; }
40.00% { background-position: 42.86% 0%; }
46.67% { background-position: 50.00% 0%; }
53.33% { background-position: 57.14% 0%; }
60.00% { background-position: 64.29% 0%; }
66.67% { background-position: 71.43% 0%; }
73.33% { background-position: 78.57% 0%; }
80.00% { background-position: 85.71% 0%; }
86.67% { background-position: 92.86% 0%; }
93.33% { background-position: 100.00% 0%; }
}
/* One-shot variant (plays once then hides) */
.vfx-sprite.once {
animation: vfx-play 0.50s steps(1) forwards;
}
<!-- HTML usage -->
<div class="vfx-sprite"></div>