Skip to content
JavaScript js canvas 5 min read

Compositing & Clipping

By default, anything you draw on a canvas is painted on top of what is already there. But the 2D context gives you much finer control over how new pixels combine with existing ones. With globalCompositeOperation you can multiply colors, cut holes, keep only overlaps, and reproduce most Photoshop-style blend modes. With clip() you can confine all future drawing to an arbitrary path, which is the foundation of masking, rounded-image avatars, and spotlight effects. This page covers both, plus shadows, which are closely related because they too affect how shapes land on the canvas.

How compositing works

Every drawing operation has a source (the shape you are about to draw) and a destination (the pixels already on the canvas). ctx.globalCompositeOperation decides how the source and destination are blended, pixel by pixel, accounting for the alpha (transparency) of each.

The default value is "source-over": the source is drawn over the destination, respecting alpha. Changing it is a stateful operation, so it stays in effect until you change it again or call restore().

const ctx = canvas.getContext('2d');

ctx.fillStyle = 'crimson';
ctx.fillRect(20, 20, 80, 80);

ctx.globalCompositeOperation = 'multiply'; // affects everything after this
ctx.fillStyle = 'steelblue';
ctx.fillRect(60, 60, 80, 80);

ctx.globalCompositeOperation = 'source-over'; // reset to default

Composite operation reference

The modes fall into two families: Porter–Duff operators that combine shapes based on overlap and alpha, and blend modes that mix colors mathematically.

OperationEffect
source-overDefault. Source painted over destination.
destination-overSource painted behind existing content.
source-inKeep source only where it overlaps destination.
source-outKeep source only where it does not overlap.
destination-outErase destination where source covers it.
xorShow non-overlapping regions of both.
lighterAdd color values (additive glow).
multiplyDarkens; great for shadows/tints.
screenLightens; inverse of multiply.
overlayCombines multiply and screen for contrast.
darken / lightenKeep the darker / lighter channel value.

Tip: destination-out is the canonical way to “erase” — draw a shape with it and you punch a transparent hole through everything beneath. Pair it with source-over afterward to resume normal painting.

The following pen lets you see several modes side by side. Each cell draws a red square, switches the composite mode, then draws an overlapping blue circle.

<!DOCTYPE html>
<canvas id="c" width="480" height="160"></canvas>
<script>
  const ctx = document.getElementById('c').getContext('2d');
  const modes = ['source-over', 'multiply', 'screen', 'xor', 'destination-out'];

  modes.forEach((mode, i) => {
    const x = i * 96;
    ctx.save();
    // red square (destination)
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle = 'crimson';
    ctx.fillRect(x + 20, 30, 50, 50);

    // blue circle (source) with the chosen mode
    ctx.globalCompositeOperation = mode;
    ctx.fillStyle = 'steelblue';
    ctx.beginPath();
    ctx.arc(x + 55, 65, 28, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    ctx.fillStyle = '#333';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(mode, x + 48, 110);
  });
</script>

Shadows

Shadows are applied to whatever you draw next — fills, strokes, text, and images all cast them. They are controlled by four context properties.

PropertyMeaning
shadowColorColor of the shadow (use rgba() for soft edges).
shadowBlurBlur radius in pixels; 0 is a hard shadow.
shadowOffsetXHorizontal displacement.
shadowOffsetYVertical displacement.
ctx.shadowColor = 'rgba(0, 0, 0, 0.45)';
ctx.shadowBlur = 12;
ctx.shadowOffsetX = 4;
ctx.shadowOffsetY = 6;

ctx.fillStyle = '#4caf50';
ctx.fillRect(40, 40, 120, 80);

// Disable shadows for subsequent draws
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;

Warning: Shadows are expensive, especially with a large shadowBlur. In animation loops, prefer pre-rendering a shadowed sprite to an offscreen canvas once, then drawImage it each frame.

Clipping to a path

Calling ctx.clip() intersects the current clipping region with the path you have built. After that, nothing draws outside the path until you restore() a previously saved state. This is how you mask: define a shape, clip to it, then draw freely knowing the overflow is discarded.

ctx.save();

ctx.beginPath();
ctx.arc(100, 100, 70, 0, Math.PI * 2);
ctx.clip(); // future drawing is confined to this circle

// A full-canvas gradient, but only the circle shows through
const g = ctx.createLinearGradient(0, 0, 200, 200);
g.addColorStop(0, '#ff8a00');
g.addColorStop(1, '#e52e71');
ctx.fillStyle = g;
ctx.fillRect(0, 0, 200, 200);

ctx.restore(); // remove the clip

You can pass a fill rule ('nonzero' default or 'evenodd') to clip(), and even clip to a Path2D object: ctx.clip(myPath2D, 'evenodd'). The even-odd rule is handy for creating ring-shaped masks from two concentric circles.

This pen clips to a rounded rectangle and draws an image-like gradient inside it — a common avatar/card masking pattern.

<!DOCTYPE html>
<canvas id="c" width="240" height="240" style="background:#111"></canvas>
<script>
  const ctx = document.getElementById('c').getContext('2d');

  function roundedRect(c, x, y, w, h, r) {
    c.beginPath();
    c.moveTo(x + r, y);
    c.arcTo(x + w, y, x + w, y + h, r);
    c.arcTo(x + w, y + h, x, y + h, r);
    c.arcTo(x, y + h, x, y, r);
    c.arcTo(x, y, x + w, y, r);
    c.closePath();
  }

  ctx.save();
  roundedRect(ctx, 30, 30, 180, 180, 36);
  ctx.clip();

  // Anything drawn now is masked to the rounded rect
  const g = ctx.createLinearGradient(0, 0, 240, 240);
  g.addColorStop(0, '#36d1dc');
  g.addColorStop(1, '#5b86e5');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, 240, 240);

  // Decorative stripes, also clipped automatically
  ctx.fillStyle = 'rgba(255,255,255,0.18)';
  for (let i = -240; i < 240; i += 30) {
    ctx.fillRect(i, 0, 14, 240);
  }
  ctx.restore();
</script>

Verifying state isolation

Because compositing, shadows, and clipping are all stored on the context, wrapping experiments in save()/restore() keeps them from leaking.

ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = 'rgba(255,0,0,0.6)';
ctx.fillRect(0, 0, 50, 50);
ctx.restore();

console.log(ctx.globalCompositeOperation); // back to default after restore

Output:

source-over

Best Practices

  • Always reset globalCompositeOperation to 'source-over' (or use save()/restore()) so a special mode does not silently affect later draws.
  • Use destination-out for erasing and lighter for additive glow effects like fire, sparks, or light blooms.
  • Keep shadowBlur small and avoid applying shadows inside tight animation loops; cache shadowed results offscreen.
  • Wrap every clip() in a save()/restore() pair — there is no “unclip” method, only restoring a prior state.
  • Set shadowColor = 'transparent' to turn shadows off explicitly rather than relying on later code to do it.
  • Prefer Path2D plus clip(path) when you reuse the same mask shape repeatedly; it is clearer and avoids rebuilding the path each frame.
Last updated June 1, 2026
Was this helpful?