Chapter 52: Canvas Compositing
Canvas Compositing (also called globalCompositeOperation) in the clearest, most patient, step-by-step way possible.
Imagine I’m sitting right next to you with my laptop open. We’re going to build every concept together — slowly, with tiny complete runnable examples you can copy-paste immediately. No skipping steps, no magic, just plain English explanations, visual thinking, common beginner traps, and repeated patterns until compositing feels completely natural and easy to use.
Canvas Compositing – From Zero to Confident
1. The single most important sentence about compositing
Compositing controls what happens when you draw something new on top of pixels that are already on the canvas.
Normally, when you draw a shape or image, the new pixels replace the old ones (or blend with alpha transparency). But with globalCompositeOperation you can change that rule — you can tell Canvas:
- “Only draw where there is already something”
- “Only draw where there is nothing”
- “Subtract colors”
- “Keep the brightest parts”
- “Use the new shape as a mask”
- etc.
This is one of the most powerful (and most underused) features in Canvas — it lets you do things that normally require very complex math or external image editors.
2. The property that controls everything: globalCompositeOperation
You set it on the context:
|
0 1 2 3 4 5 6 |
ctx.globalCompositeOperation = 'source-over'; // default (normal behavior) |
It stays set until you change it again or restore() after save().
There are ~26 possible values in the Canvas spec, but you will use these 8 most often (the “Porter-Duff” operations + a few extras):
| Value | What happens when new shape is drawn on top of existing pixels | Visual intuition | Most common real use case |
|---|---|---|---|
| source-over | Normal: new pixels on top (default) | New paint covers old paint | Everyday drawing (most default use) |
| destination-over | Old pixels on top — new shape is drawn behind existing | New paint goes under old paint | Draw background after foreground |
| source-in | Only keep new shape where it overlaps existing pixels | New shape becomes a mask for old content | Fill inside existing shape |
| source-out | Only keep new shape where it does NOT overlap existing | Cut holes in existing content | Erase / punch holes |
| destination-in | Only keep old pixels where new shape overlaps them | New shape acts as a mask for old content | Clip / mask existing drawing |
| destination-out | Keep old pixels only where new shape does NOT overlap | New shape punches transparent holes | Erase parts of existing drawing |
| lighter | Add colors together (like screen blend) | Bright areas get brighter | Glows, light effects, particle systems |
| xor | Keep pixels that are in one shape but not both | Creates “cut-out” / XOR effect | Interesting patterns, debug visualizations |
Quick rule of thumb (memorize this table):
- Want normal painting? → ‘source-over’ (you almost never need to set it)
- Want to mask / clip with a shape? → ‘destination-in’
- Want to erase / punch holes? → ‘destination-out’ or ‘source-out’
- Want glowing / additive light? → ‘lighter’
3. Minimal complete compositing example (copy → run)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Canvas – Compositing Explained</title> <style> canvas { border: 1px solid #ccc; background: #f8f9fa; display: block; margin: 20px auto; } .container { display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; } </style> </head> <body> <div class="container"> <canvas id="c1" width="300" height="300"></canvas> <canvas id="c2" width="300" height="300"></canvas> <canvas id="c3" width="300" height="300"></canvas> <canvas id="c4" width="300" height="300"></canvas> </div> <script> // Helper to draw background + red circle + blue square function drawBase(ctx) { ctx.fillStyle = '#e3f2fd'; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); // Red circle ctx.beginPath(); ctx.arc(120, 120, 80, 0, Math.PI*2); ctx.fillStyle = '#F44336'; ctx.fill(); // Blue square ctx.fillStyle = '#2196F3'; ctx.fillRect(140, 140, 160, 160); } // Label function function label(ctx, text) { ctx.font = 'bold 20px Arial'; ctx.fillStyle = '#333'; ctx.textAlign = 'center'; ctx.fillText(text, 150, 280); } // Canvas 1: source-over (default) const c1 = document.getElementById('c1').getContext('2d'); drawBase(c1); label(c1, 'source-over (normal)'); c1.globalCompositeOperation = 'source-over'; c1.fillStyle = '#FFEB3B'; c1.fillRect(100, 100, 200, 200); // Canvas 2: destination-in (keep only overlap) const c2 = document.getElementById('c2').getContext('2d'); drawBase(c2); label(c2, 'destination-in (mask)'); c2.globalCompositeOperation = 'destination-in'; c2.fillStyle = '#FFEB3B'; c2.fillRect(100, 100, 200, 200); // Canvas 3: destination-out (erase overlap) const c3 = document.getElementById('c3').getContext('2d'); drawBase(c3); label(c3, 'destination-out (erase)'); c3.globalCompositeOperation = 'destination-out'; c3.fillStyle = '#FFEB3B'; c3.fillRect(100, 100, 200, 200); // Canvas 4: lighter (additive glow) const c4 = document.getElementById('c4').getContext('2d'); drawBase(c4); label(c4, 'lighter (additive)'); c4.globalCompositeOperation = 'lighter'; c4.fillStyle = 'rgba(255, 235, 59, 0.6)'; // semi-transparent yellow c4.fillRect(100, 100, 200, 200); </script> </body> </html> |
What you should see & remember forever:
- source-over → yellow rectangle covers red circle & blue square (normal painting)
- destination-in → only the part of red/blue where yellow overlaps remains → yellow acts as a mask
- destination-out → yellow punches transparent holes in red/blue → yellow acts as an eraser
- lighter → yellow adds to whatever is below → creates glowing/brightening effect
4. Very common real-world patterns
Pattern 1 – Circular mask / avatar
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
ctx.save(); ctx.beginPath(); ctx.arc(100, 100, 80, 0, Math.PI*2); ctx.clip(); // clip to circle ctx.drawImage(photo, 20, 20, 160, 160); // image only visible in circle ctx.restore(); |
Pattern 2 – Erase / punch hole
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Draw background ctx.fillStyle = '#4CAF50'; ctx.fillRect(0,0,canvas.width,canvas.height); // Punch a hole with destination-out ctx.save(); ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.arc(300, 200, 120, 0, Math.PI*2); ctx.fill(); ctx.restore(); |
Pattern 3 – Glow / additive light
|
0 1 2 3 4 5 6 7 8 9 10 |
ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.beginPath(); ctx.arc(cx, cy, 150, 0, Math.PI*2); ctx.fill(); |
5. Your three tiny practice tasks (10–15 min each)
Task 1 – Circular avatar Draw a rectangle filled with any photo/image Clip it to a perfect circle Add a thick white stroke around the circle
Task 2 – Punch hole Fill canvas with green Punch a large transparent circle in the center using destination-out
Task 3 – Glowing orb Draw a purple circle Add a large white radial gradient circle using lighter compositing → create soft glow
Paste any of them here when you finish — I’ll review it like we’re pair-programming together.
Which part still feels a little slippery?
- Why clip() is different from globalCompositeOperation?
- Difference between destination-in vs source-in?
- How destination-out actually erases?
- When to use lighter vs normal blending?
- Why we mustsave() / restore() with compositing?
- Something else?
Tell me — we’ll stay on compositing until you feel super confident using it to mask, erase, glow, or blend anything you draw.
You’re doing really well — compositing is one of the most powerful (and most “wow” inducing) features in Canvas! 🚀
