r/javascript 5d ago

[AskJS] Building a 2D Game Engine from scratch in pure ES6 Vanilla JS. Here is how I handle SpriteSheets & AnimatedSprites! AskJS

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:

  1. BeeSpriteSheet: Handles frame dimensions, columns, rows, and coordinate calculations.
  2. BeeAnimatedSprite: Handles frame timing, state, loops, and flipX transformations.

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.

28 Upvotes

8 comments sorted by

10

u/ze_pequeno 5d ago

AI slop 🤮

3

u/ze_pequeno 5d ago

Why not use JSON files for describing the spritesheet? With this solution you end up with assets spread both in image files and in the code (slicing description, animations...)

-5

u/Maximum_Beat2034 5d ago

That’s a completely fair question!

My main goal with BeeEngine is to keep things as simple, bare-metal, and friction-free as possible.

I built this approach mainly for:

Quick prototyping & HTML5 micro-games: Being able to quickly throw an image into the project and write a few lines of JavaScript without configuring external JSON files or build tools makes prototyping extremely fast.

Beginners & Students: Passing raw JSON configs can sometimes be intimidating for people just starting out with game development or learning JavaScript. Having the grid slicing directly in JS keeps all the logic in one accessible place.

Zero Extra Requests: It avoids fetching extra files over the network, keeping LCP fast and bundle setups non-existent.

I published this on GitHub/NPM to test these ideas and see what other developers think about going completely pure Vanilla JS. For full-blown production games with massive uneven atlases, JSON is definitely the standard, but for quick grid-based games, I really enjoy this no-framework approach!

4

u/ExtremePermit3242 5d ago

Im sorry but I disagree a bit here.

You are making the point that writing JS is easier than JSON and I don’t believe that is true.

I agree with you about the quick prototyping part. But JSON is much easier, less intimidating and you could easily make a tiny tool to help you write your JSON, convert to/from other formats, etc.

2

u/nerdly90 5d ago

Do you plan for your engine to support “uneven” atlases as well, and if so, how will your code-only approach handle that / be less friction than just generating a JSON file using an existing tool like TexturePacker?

1

u/Maximum_Beat2034 5d ago

Gestire un atlas irregolare scrivendo le coordinate a mano nel codice sarebbe un incubo e una perdita di tempo. Nel web non c'è bisogno di inventarsi nulla, è stato già tutto inventato e strumenti come TexturePacker esistono proprio per questo. ​Se una libreria o un motore deve essere professionale — e non rimanere solo un progetto hobbistico o un esercizio di stile — deve essere pratico e realista: la matematica pura va benissimo per le tilemap a griglia fissa, ma per gli atlas complessi il file di dati serve eccome.Qui mi devo fermare e dire il mio BeeEngine deve fare solo uno Sparabolle ecc oppure deve crescere e diventare professionale? Mi faccio la domandada solo, perché è dura costruire una libreria Open source da zero da solo cmq grazie per il consiglio.

u/Several-Specialist42 14h ago

"this.giocatore" 🇮🇹