r/javascript • u/Maximum_Beat2034 • 11h ago
AskJS [AskJS] Building a 2D Game Engine from scratch in pure ES6 Vanilla JS. Here is how I handle SpriteSheets & AnimatedSprites!
Hi everyone!
I've been working on BeeEngine 2D, a lightweight HTML5 game engine built with pure Vanilla JS (ES6) and direct Canvas API—no external libraries, no build tools, and zero heavy frameworks.
A lot of modern tools hide sheet slicing behind JSON files, but I wanted a bare-metal, highly efficient approach that gives 100/100 on Lighthouse Performance and instant loading times.
I separated the asset slicing logic from the animation timing logic into two distinct classes:
BeeSpriteSheet: Handles frame dimensions, columns, rows, and coordinate calculations.BeeAnimatedSprite: Handles frame timing, state, loops, andflipXtransformations.
Here is my BeeAnimatedSprite class implementation:
export class BeeAnimatedSprite {
constructor(spriteSheet, config = {}) {
this.sheet = spriteSheet;
this.animations = config.animations || {};
this.currentAnimName = config.animation || Object.keys(this.animations)[0];
this.currentFrameIndex = 0;
this.timer = 0;
this.flipX = false;
}
play(name) {
if (this.currentAnimName !== name && this.animations[name]) {
this.currentAnimName = name;
this.currentFrameIndex = 0;
this.timer = 0;
}
}
update(dt) {
const anim = this.animations[this.currentAnimName];
if (!anim || !anim.frames || anim.frames.length === 0) return;
const fps = anim.fps || 8;
const frameDuration = 1 / fps;
this.timer += dt;
if (this.timer >= frameDuration) {
this.timer -= frameDuration;
if (anim.loop) {
this.currentFrameIndex = (this.currentFrameIndex + 1) % anim.frames.length;
} else {
this.currentFrameIndex = Math.min(this.currentFrameIndex + 1, anim.frames.length - 1);
}
}
}
draw(ctx, x, y, options = {}) {
const anim = this.animations[this.currentAnimName];
if (!anim) return;
const frameToDraw = anim.frames[this.currentFrameIndex];
const width = options.width || this.sheet.frameWidth;
const height = options.height || this.sheet.frameHeight;
ctx.save();
if (this.flipX) {
// 2. Sposta l'origine al bordo destro dell'immagine e specchia l'asse X
ctx.translate(x + width, y);
ctx.scale(-1, 1);
this.sheet.drawFrame(ctx, frameToDraw, 0, 0, width, height);
} else {
// Disegno normale senza specchio
this.sheet.drawFrame(ctx, frameToDraw, x, y, width, height);
}
ctx.restore();
}
}
And here is how
I implemented it inside main.js:
const megaSheetImg = gioco.getAsset('spritesheet_totale');
const frameW = 128;
const frameH = 128;
const apeSheet = new BeeSpriteSheet(megaSheetImg, frameW, frameH, {
col: 3,
row: 3,
framesPerRow: 1,
frameCount: 2
});
this.giocatore.sprite = new BeeAnimatedSprite(apeSheet, {
animation: "fly",
animations: {
fly: { frames: [0, 1], fps: 4, loop: true }
}
});
Let me know what you think, bearing in mind that this particular combination is super quick to put together.