javascripthtml5-canvasdrawimageimagebitmap

Problem with loading assets into canvas - Uncaught (in promise) DOMException: An attempt was made to use an object that is not, or is no longer usable


I'm trying to load images from my array using the drawImage canvas method. The problem is that it sometimes throws me the following error

Uncaught (in promise) DOMException: An attempt was made to use an object that is not, or is no longer, usable

↑ firefox ↓ google

Uncaught (in promise) DOMException: Failed to execute 'createImageBitmap' on 'Window': The source image width is 0.

Both on index.js:13

Sometimes if I load the page again the error disappears. Does anyone know what could be causing this problem?

My code:

BOE_tile_set.png

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

    let tiles = [];
    let tileWidth = 28;
    let tileHeight = 36;

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

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

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

    const canvas = document.querySelector("canvas");
    canvas.width = 224; // img.width
    canvas.height = 360; // img.height

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

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

})();

Solution

  • createImageBitmap(HTMLImageElement) requires that the HTMLImageElement is fully loaded.

    So either you do wait for its onload event, or, since it's a bitmap image and not an SVG one, you can await a call to its decode() method which does return a Promise for when the image has been fully loaded and decoded.

    (async () => {
      const image = new Image();
      image.src = "https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg?" + Math.random();
      try {
        const bmp = await createImageBitmap(image);
      }
      catch(e) {
        console.error("Before load failed:", e.message);
      }
      await image.decode(); // wait for the image to be fully loaded
      const bmp = await createImageBitmap(image);
      console.log("After load succeeded", bmp);
    })();

    But, still assuming that you are not using an SVG image, and also assuming that you are in a same-origin situation, then the best is to not use an HTMLImageElement at all. Indeed here we just created 2 occurrences of the bitmap data, once for the HTMLImageElement and once for the ImageBitmap. The former isn't going to be used anymore but still consumes computation and memory.

    So the best is to fetch your image as a Blob, and call createImageBitmap on that Blob directly:

    (async () => {
      const resp = await fetch("https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg");
      if (!resp.ok) {
        return console.error("Network error", resp.status);
      }
      const blob = await resp.blob();
      const bmp = await createImageBitmap(blob);
      console.log(bmp);
    })();

    But once again this requires that your image can be fetched through AJAX (which is certainly the case for you since you use a relative path), and that the image is not an SVG image, because createImageBitmap(Blob) doesn't work with SVG images unless using my polyfill.