🚀AdvancedMedia & Graphics

OffscreenCanvas & Canvas API Advanced

Move canvas rendering to Web Workers with OffscreenCanvas, and master advanced Canvas 2D API techniques for animations and effects.

Overview

The Canvas 2D API is the go-to for dynamic graphics, charts, games, and image processing in the browser. `OffscreenCanvas` extends this by allowing canvas rendering to happen entirely in a Web Worker off the main thread. This is critical for smooth 60fps animations when the main thread is busy with JavaScript logic or UI updates. Transferring a canvas's control to a worker is done once with `canvas.transferControlToOffscreen()`. The worker receives an `OffscreenCanvas` and uses it exactly like a regular canvas. Completed frames are committed to screen automatically or via `commit()`. Beyond basic drawing, the Canvas 2D API supports gradients, patterns, compositing, clipping paths, image filters, and hardware-accelerated `CanvasRenderingContext2D` operations that rival SVG and CSS for complex dynamic graphics.

Key Points

  • •`canvas.transferControlToOffscreen()` transfers canvas rendering to a Worker
  • •`worker.postMessage({ canvas: offscreen }, [offscreen])` pass as transferable to worker
  • •In the worker: `const ctx = offscreenCanvas.getContext("2d")` works identically to regular canvas
  • •`ctx.commit()` in a worker immediately updates the displayed frame
  • •Canvas state: `ctx.save()` and `ctx.restore()` push/pop transforms, styles, and clipping paths
  • •`ctx.requestAnimationFrame` not available in workers use `setTimeout` or `MessageChannel` for animation loops
  • •`createImageBitmap(blob/image)` async, worker-safe image loading for canvas

Examples

Transferring canvas to an OffscreenCanvas Worker

<!-- main.html -->
<canvas id="game-canvas" width="800" height="600"></canvas>

<script>
const canvas = document.getElementById('game-canvas');

// Transfer canvas control to a worker
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('/game-worker.js');

worker.postMessage({ type: 'INIT', canvas: offscreen }, [offscreen]);
// [offscreen] in the transferable list transferred, not copied

// Send game state updates to worker
document.addEventListener('keydown', (e) => {
  worker.postMessage({ type: 'KEYDOWN', key: e.key });
});

worker.onmessage = (e) => {
  if (e.data.type === 'SCORE_UPDATE') {
    document.getElementById('score').textContent = e.data.score;
  }
};
</script>

After transferControlToOffscreen(), the main thread can no longer draw to the canvas.

OffscreenCanvas Worker game loop

// game-worker.js
let ctx, width, height, animating = false;
let playerX = 400, gameScore = 0;

self.onmessage = (e) => {
  switch (e.data.type) {
    case 'INIT':
      ctx = e.data.canvas.getContext('2d');
      width = e.data.canvas.width;
      height = e.data.canvas.height;
      startGameLoop();
      break;
    case 'KEYDOWN':
      if (e.data.key === 'ArrowLeft') playerX -= 10;
      if (e.data.key === 'ArrowRight') playerX += 10;
      break;
  }
};

function startGameLoop() {
  animating = true;
  let lastTime = 0;

  function frame(time) {
    if (!animating) return;

    const dt = time - lastTime;
    lastTime = time;

    // Clear
    ctx.clearRect(0, 0, width, height);

    // Draw background
    ctx.fillStyle = '#1e293b';
    ctx.fillRect(0, 0, width, height);

    // Draw player
    ctx.fillStyle = '#3b82f6';
    ctx.beginPath();
    ctx.arc(playerX, 500, 20, 0, Math.PI * 2);
    ctx.fill();

    // Update score
    gameScore += dt * 0.01;
    self.postMessage({ type: 'SCORE_UPDATE', score: Math.floor(gameScore) });

    // Schedule next frame (no requestAnimationFrame in workers)
    setTimeout(() => frame(performance.now()), 16);
  }

  frame(performance.now());
}

Workers cannot use requestAnimationFrame use setTimeout(fn, 16) for approximately 60fps.

Advanced Canvas 2D: gradients, compositing, and clipping

<canvas id="demo" width="400" height="400"></canvas>

<script>
const canvas = document.getElementById('demo');
const ctx = canvas.getContext('2d');

// 1. Linear gradient
const gradient = ctx.createLinearGradient(0, 0, 400, 0);
gradient.addColorStop(0, '#3b82f6');
gradient.addColorStop(0.5, '#8b5cf6');
gradient.addColorStop(1, '#ec4899');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 400, 400);

// 2. Clipping path only draw inside this shape
ctx.save();
ctx.beginPath();
ctx.arc(200, 200, 150, 0, Math.PI * 2);
ctx.clip();

// 3. Compositing globalCompositeOperation
ctx.globalCompositeOperation = 'screen';
ctx.fillStyle = 'rgba(255, 200, 0, 0.8)';
ctx.fillRect(50, 50, 300, 300);

ctx.restore(); // remove clipping path

// 4. Radial gradient
const radial = ctx.createRadialGradient(200, 200, 0, 200, 200, 100);
radial.addColorStop(0, 'rgba(255,255,255,0.8)');
radial.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = radial;
ctx.beginPath();
ctx.arc(200, 200, 100, 0, Math.PI * 2);
ctx.fill();
</script>

ctx.save/restore stack the canvas state transforms, clips, and style settings enabling safe isolated drawing operations.

Common Mistakes

  • Calling canvas.transferControlToOffscreen() and then trying to draw to the original canvas the main thread loses all access after transfer
  • Using requestAnimationFrame inside a Web Worker it's not available; use setTimeout or MessageChannel with port.onmessage
  • Forgetting ctx.save()/ctx.restore() around clipping paths clips accumulate and affect later drawing operations

Note

For image processing, use `createImageBitmap(file)` to decode images in workers this avoids blocking the main thread for large image decoding. `ImageBitmap` objects are transferable between workers and the main thread.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What happens after canvas.transferControlToOffscreen() is called?