Composite Operations

globalCompositeOperation decides how new pixels (the source) combine with those already drawn (the destination). Chrome 154 1 accepts 26 values: eight source-* and destination-* Porter-Duff operators (source-over is the default, destination-over draws behind), copy, xor and lighter, and the 15 CSS blend modes from multiply to luminosity.

Eight composite operations: a blue square, then an orange circleHTML
<div id="grid" style="display:flex; gap:6px; font:11px sans-serif; text-align:center"></div>
<script src="hidpi.js"></script>
<script>
  for (const op of ['source-over', 'destination-over', 'source-in', 'source-atop',
                    'destination-out', 'xor', 'multiply', 'screen']) {
    const fig = grid.appendChild(document.createElement('div'));
    const c = document.createElement('canvas');  fig.append(c, op);
    c.style.cssText = 'width:70px; height:64px; background:#eceff1; display:block';
    const ctx = hidpi(c);
    ctx.fillStyle = '#1f5f8b';  ctx.fillRect(6, 6, 40, 40);              // destination
    ctx.globalCompositeOperation = op;
    ctx.fillStyle = '#e09a10';  ctx.beginPath();  ctx.arc(44, 40, 20, 0, 7);  ctx.fill();
  }
</script>
Browser output of Listing 2.12
Browser output of 12

Each operation gets its own canvas because source-in, source-out, destination-in, destination-atop and copy clear every destination pixel outside the new shape, across the whole canvas. destination-out erases, for scratch cards and cut-outs.

A gallery of globalCompositeOperation values, plus a scratch card using destination-outHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; font: 11px system-ui, sans-serif; background: #fafaf7; max-width: 600px; }
  #grid { display: flex; flex-wrap: wrap; gap: 6px; text-align: center; }
  #grid canvas { width: 70px; height: 64px; background: #eceff1; display: block; }
  .card { position: relative; width: 300px; height: 90px; margin-top: 10px; background: #fff3e0;
          font: bold 22px Georgia; color: #b5452f; display: flex; align-items: center; justify-content: center; }
  #scratch { position: absolute; inset: 0; width: 300px; height: 90px; touch-action: none; cursor: crosshair; }
</style>
<div id="grid"></div>
<div class="card">20% off Salt and Saffron<canvas id="scratch"></canvas></div>
<p>Scratch the grey card with the pointer to reveal the discount.</p>
<script>
  function hidpi(c, r = devicePixelRatio) {
    c.width = c.clientWidth * r;  c.height = c.clientHeight * r;
    const ctx = c.getContext('2d');  ctx.scale(r, r);  return ctx;
  }
  // One canvas per operation: source-in and friends clear the whole destination outside the shape
  const ops = ['source-over', 'destination-over', 'source-in', 'source-out', 'source-atop',
               'destination-in', 'destination-out', 'xor', 'lighter', 'multiply', 'screen',
               'overlay', 'difference', 'hue', 'color', 'luminosity'];
  const grid = document.getElementById('grid');
  for (const op of ops) {
    const fig = grid.appendChild(document.createElement('div'));
    const c = document.createElement('canvas');
    fig.append(c, op);
    const ctx = hidpi(c);
    ctx.fillStyle = '#1f5f8b';  ctx.fillRect(6, 6, 40, 40);          // destination
    ctx.globalCompositeOperation = op;
    ctx.fillStyle = '#e09a10';                                        // source
    ctx.beginPath();  ctx.arc(44, 40, 20, 0, 2 * Math.PI);  ctx.fill();
  }

  // Scratch card: the prize is HTML under the canvas; cover it, then erase the cover with destination-out
  const card = document.getElementById('scratch');
  const cctx = hidpi(card);
  cctx.fillStyle = '#9e9e9e';  cctx.fillRect(0, 0, 300, 90);
  cctx.fillStyle = '#eee';  cctx.font = 'bold 16px sans-serif';  cctx.fillText('SCRATCH HERE', 90, 50);
  cctx.globalCompositeOperation = 'destination-out';
  const scratchAt = (x, y) => { cctx.beginPath();  cctx.arc(x, y, 14, 0, 2 * Math.PI);  cctx.fill(); };
  card.addEventListener('pointermove', e => {
    if (e.buttons || e.pointerType === 'mouse') {
      const b = card.getBoundingClientRect();
      scratchAt(e.clientX - b.left, e.clientY - b.top);
    }
  });
  [[40, 30], [60, 45], [80, 40], [100, 55]].forEach(p => scratchAt(...p));  // a first scratch
</script>