Search code examples
javascriptcanvasweb-workeroffscreen-canvas

Firefox cannot drawImage from an offscreen canvas controlled by a worker


I tried to transfer an image drawn to an offscreen canvas by a web worker to a visible canvas. It works in Chrome, but not in Firefox. No error was thrown in the console, but the visible canvas remains blank. Below is my complete program (index.html). Is this an issue with how Firefox handles worker and canvas? Is there a fix to this? Thanks!

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script>
    window.addEventListener('load', function () {
      const w = 600, h = 400;
      const blob = new Blob(['(' + render.toString() + ')()'], {type: 'text/javascript'});
      const worker = new Worker(URL.createObjectURL(blob));
      const offs = document.getElementById('worker').transferControlToOffscreen();
      worker.postMessage({ msg: 'load', canvas: offs }, [offs]);

      // draw image in offscreen canvas
      document.getElementById('render').addEventListener('click', function () {
        worker.postMessage({ msg: 'draw' });
      });

      // transfer image to main canvas
      document.getElementById('transfer').addEventListener('click', function () {
        const img = document.getElementById('worker');
        const ctx = document.getElementById('main').getContext('2d');
        ctx.clearRect(0, 0, w, h);
        ctx.drawImage(img, 0, 0, w, h, 0, 0, w, h);
      });
    });

    // worker for rendering
    function render() {
      let canvas;
      onmessage = function(e) {
        if (e.data.msg === 'load') {
          canvas = e.data.canvas;
        } else if (e.data.msg === 'draw') {
          ctx = canvas.getContext('2d');
          ctx.fillStyle = 'green';
          ctx.fillRect(150, 150, 300, 200);
        }
      }
    }
  </script>
</head>
<body>
  <div>
    <p>
      <canvas id="main" width="600" height="400"></canvas>
      <canvas id="worker" width="600" height="400" style="display: none"></canvas>
    </p>
    <p>
      <button id="render">Render</button>
      <button id="transfer">Transfer</button>
    </p>
  </div>
</body>
</html>


Solution

  • This is indeed a bug, and I filed BUG 1833496.
    For a fix, we'll need to wait a bit, but the last few bugs I opened against their OffscreenCanvas young implementation were fixed rapidly, so there is hope this one gets fixed too.

    If you need a workaround, you could transfer your OffscreenCanvas to an ImageBitmap, transfer that back to your main thread through postMessage() and draw it when needed:

    const w = 600, h = 400;
    const blob = new Blob(['(' + render.toString() + ')()'], {type: 'text/javascript'});
    const worker = new Worker(URL.createObjectURL(blob));
    const offs = document.getElementById('worker').transferControlToOffscreen();
    worker.postMessage({ msg: 'load', canvas: offs }, [offs]);
    
    // We'll store the canvas bitmap when ready
    let source;
    worker.onmessage = ({data}) => {
      if (data.msg === "bitmap") {
        source = data.bmp;
      }
    };
    
    // draw image in offscreen canvas
    document.getElementById('render').addEventListener('click', function () {
      worker.postMessage({ msg: 'draw' });
    });
    
    // transfer image to main canvas
    document.getElementById('transfer').addEventListener('click', function () {
      if (!source) {
        console.log("no source available yet");
        return;
      }
      const ctx = document.getElementById('main').getContext('2d');
      ctx.clearRect(0, 0, w, h);
      ctx.drawImage(source, 0, 0, w, h, 0, 0, w, h);
    });
    
    // worker for rendering
    function render() {
      let canvas;
      onmessage = function(e) {
        if (e.data.msg === 'load') {
          canvas = e.data.canvas;
          // I removed a "draw" step here, up to you to reimplement as you need
          const ctx = canvas.getContext('2d');
          ctx.fillStyle = 'green';
          ctx.fillRect(150, 150, 300, 200);
          // let main thread draw us (https://bugzil.la/1833496)
          const bmp = canvas.transferToImageBitmap();
          postMessage({msg: "bitmap", bmp}, [bmp]);
        }
      }
    }
    <div>
      <p>
        <button id="render">Render</button>
        <button id="transfer">Transfer</button>
      </p>
      <p>
        <canvas id="main" width="600" height="400"></canvas>
        <canvas id="worker" width="600" height="400" style="display: none"></canvas>
      </p>
    </div>