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.
| Operation | Effect |
|---|---|
source-over | Default. Source painted over destination. |
destination-over | Source painted behind existing content. |
source-in | Keep source only where it overlaps destination. |
source-out | Keep source only where it does not overlap. |
destination-out | Erase destination where source covers it. |
xor | Show non-overlapping regions of both. |
lighter | Add color values (additive glow). |
multiply | Darkens; great for shadows/tints. |
screen | Lightens; inverse of multiply. |
overlay | Combines multiply and screen for contrast. |
darken / lighten | Keep the darker / lighter channel value. |
Tip:
destination-outis the canonical way to “erase” — draw a shape with it and you punch a transparent hole through everything beneath. Pair it withsource-overafterward 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.
| Property | Meaning |
|---|---|
shadowColor | Color of the shadow (use rgba() for soft edges). |
shadowBlur | Blur radius in pixels; 0 is a hard shadow. |
shadowOffsetX | Horizontal displacement. |
shadowOffsetY | Vertical 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, thendrawImageit 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
globalCompositeOperationto'source-over'(or usesave()/restore()) so a special mode does not silently affect later draws. - Use
destination-outfor erasing andlighterfor additive glow effects like fire, sparks, or light blooms. - Keep
shadowBlursmall and avoid applying shadows inside tight animation loops; cache shadowed results offscreen. - Wrap every
clip()in asave()/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
Path2Dplusclip(path)when you reuse the same mask shape repeatedly; it is clearer and avoids rebuilding the path each frame.