Search code examples
javascriptarraysbitmaphtml5-canvascrop

Is there a way to draw pieces of the image on 2d canvas without transparent parts?


I would like to draw this image on canvas without the transparent parts. The principle of my rendering is that I crop small images from a large image using the createImageBitmap method and store them in an array. I then render them on the canvas one by one. The problem is that it also unnecessarily draws the transparent parts. So if I log my array I get this. Since my map is 10x10 tiles it results in 100 images (of which 96 are useless). Instead, I would like to save only 4 images.

Not only that it messes up my performance but I have more reasons why it bothers me. Is there a way to solve this problem?

My code so far:

(async () => {
    const img = new Image();
    img.src = "./img/obstacles.png";
    await img.decode();

    let tiles = [];
    let tileWidth = 32;
    let tileHeight = 32;

    for (let i = 0; i < 100; i++) {
        let x = i % 10;
        let y = Math.floor(i / 10);

        let bmp = await createImageBitmap(img, x * tileWidth, y * tileHeight, tileWidth, tileHeight); // index.js:13

        tiles.push({
            bmp,
            x,
            y
        })
    }

    console.log(tiles)

    const canvas = document.querySelector("canvas");
    canvas.width = 320;
    canvas.height = 320;

    const ctx = canvas.getContext("2d");

    function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        tiles.forEach((tile) => {
            ctx.drawImage(tile.bmp, tile.x * tileWidth, tile.y * tileHeight);
        })

        // requestAnimationFrame(draw)
    }
    draw();

})();

Solution

  • The best is to prepare your asset correctly, so here that would mean having only the 3 unique sprites in your atlas:

    OP's atlas without the transparent slots

    Then you can use your simple loop to get these as separate objects.

    However it's not uncommon to have sprites of different sizes in the same atlas, in such a case, your simple loop won't do. Instead, you should prepare a coordinates dictionary (e.g as a JSON file, or embedded directly in your js), with all the coordinates of each sprites.

    Most spritesheet generating tools will produce this for you.

    Here is an example where I just added one big sprite to the atlas:

    Atlas of 3 small sprites and one big

    (async () => {
        // If you are same-origin, it's better to fetch as a Blob
        // and create your ImageBitmap from the Blob
        // Here we aren't, so we have to go through an <img>
        const img = new Image();
        img.src = "https://i.sstatic.net/h7w1C.png";
        await img.decode();
        document.body.append(img);
        // We hardcode the coords of each sprite
        const map = [
          // [x, y, w, h]
          [0,  0, 32, 32],
          [0, 32, 32, 32],
          [0, 64, 32, 32],
          [32, 0, 96, 96],
        ];
        const tiles = [];
        
        for (const [x, y, w, h] of map) {
            const bmp = await createImageBitmap(img, x, y, w, h);
            tiles.push({
                bmp,
                x,
                y
            })
        }
    
        console.log(tiles)
    
        const canvas = document.querySelector("canvas");
        canvas.width = 320;
        canvas.height = 320;
    
        const ctx = canvas.getContext("2d");
        let m = 0;
        function draw() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            m = (m + 0.005) % 0.5;
            const margin = m + 1;
            tiles.forEach((tile) => {
                // we add some margin to show these are separate bitmaps
                ctx.drawImage(tile.bmp, tile.x * margin, tile.y * margin);
            })
    
            requestAnimationFrame(draw)
        }
        draw();
    
    })().catch(console.error);
    .as-console-wrapper { max-height: 100px !important }
    <canvas></canvas>