1

I tried to load image using fetch and async/await and display it on canvas, but it doesn't work. The image isn't drawn on canvas. It loads correctly into img element in HTML. On canvas if I wrap the drawing code in setTimeout it works, but I would prefer to not to do this.

Is there some way to load the image using await and fetch and draw it on canvas without setTimeout?

Here is the code:

async function loadImage(url) {
    let response = await fetch(url);
    let blob = await response.blob();
    return URL.createObjectURL(blob);
}

let canvas = document.body.querySelector("canvas");
let ctx = canvas.getContext("2d");

let tileURL = loadImage("https://avatars.githubusercontent.com/u/92330684?s=120&v=4").then((tileURL) => {
        
    // Displaying image element in HTML works.
    let img = document.body.querySelector("img");
    img.src = tileURL;
    
    // Displaying the image immediately in canvas doesn't work.
    ctx.drawImage(img, 0, 0);
    
    // But it works if I add some delay.
    setTimeout(() => {
         ctx.drawImage(img, 100, 0);
    }, 3000); // 3 second delay.
    
});
canvas {
   border: 1px solid #000;
}
<canvas></canvas>
<img>
Phil
  • 157,677
  • 23
  • 242
  • 245
  • There's no reason to assign `let tileURL` to the promise returned by `loadImage`. You never use it (not an answer, just an observation) – Phil Dec 08 '21 at 22:40
  • You might want to make `loadImage` return an `Image`, not an object url – Bergi Dec 08 '21 at 22:45
  • @Bergi OP would need to [manually create a `Promise`](https://stackoverflow.com/a/46399452/283366) then to handle the loading which IMO won't make the code any nicer – Phil Dec 08 '21 at 22:49
  • @Phil It would make the drawing code nicer - not the `loadImage` code, yes. – Bergi Dec 08 '21 at 22:52
  • TIL about [HTMLImageElement.decode()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode) thanks to [this awesome answer](https://stackoverflow.com/a/64747517/283366) – Phil Dec 08 '21 at 22:55

2 Answers2

2

Loading an image is always asynchronous, whatever the source*. You need to wait for it has loaded before being able to do anything with it.

Now, it's unclear why you are using fetch here, you can very well just set the image's .src to the URL directly:

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

// Displaying image element in HTML works.
const img = document.body.querySelector("img");
img.crossOrigin = "anonymous"; // allow read-back
img.src = "https://avatars.githubusercontent.com/u/92330684?s=120&v=4";
img.onload = (evt) => // or await img.decode() if in async
  ctx.drawImage(img, 0, 0);
canvas {
   border: 1px solid #000;
}
<canvas></canvas>
<img>

However, if you have a Blob (or are actually forced to use fetch), then create an ImageBitmap from this Blob directly. This is the most performant way to produce and store a CanvasImageSource (to be used in drawImage()).
For older browsers that didn't support this method, I got you covered through this polyfill of mine.

async function loadImage(url) {
  const response = await fetch(url);
  const blob = response.ok && await response.blob();
  return createImageBitmap(blob);
}

const canvas = document.body.querySelector("canvas");
const ctx = canvas.getContext("2d");
loadImage("https://avatars.githubusercontent.com/u/92330684?s=120&v=4").then((bitmap) => {
  ctx.drawImage(bitmap, 0, 0);
});
canvas {
   border: 1px solid #000;
}
<!-- createImageBitmap polyfill for old browsers --> <script src="https://cdn.jsdelivr.net/gh/Kaiido/createImageBitmap/dist/createImageBitmap.js"></script>
<canvas></canvas>
<!-- HTMLImage is not needed -->

*It may happen that cached images are ready before the next call to drawImage(), but one should never assume so.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
1

Even though you're using an in-memory blob URL, it still needs to be loaded by the browser.

You aren't waiting for the img to load before adding it to the canvas so all it adds is a blank.

Register a load event handler or use the newer HTMLImageElement.decode() to wait for the image to be ready before adding it to the canvas.

async function loadImage(url) {
  let response = await fetch(url);
  let blob = await response.blob();
  return URL.createObjectURL(blob);
}

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

const url = "https://avatars.githubusercontent.com/u/92330684?s=120&v=4"

loadImage(url).then(async (tileURL) => {

  // Displaying image element in HTML works.
  img.src = tileURL;
  
  // wait for it to load
  await img.decode()
  
  ctx.drawImage(img, 0, 0);
});
canvas {
  border: 1px solid #000;
}
<canvas></canvas>
<img>
Phil
  • 157,677
  • 23
  • 242
  • 245