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.
<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>
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.
<!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>