Skip to content

合成与裁剪

Canvas 的合成(Compositing)和裁剪(Clipping)机制决定了多个图形在画布上重叠时的渲染方式。globalCompositeOperation 定义了新绘制内容与已有内容的混合规则,而 clip() 则将绘制区域限制在指定路径内。本节将详细介绍这两种强大的图形组合技术。

前置知识

阅读本节前,建议先了解:变换

基础概念

合成操作决定了像素的混合方式。当你在画布上绘制新内容时,浏览器需要决定新像素如何与已有像素结合。Canvas 提供了 26 种以上的合成模式,覆盖了常见的图像合成需求。

合成的基本模型

Canvas 合成基于 Porter-Duff 合成模型:

  • Source(源):新绘制的内容
  • Destination(目标):画布上已有的内容
  • Result(结果):合成后的最终像素

globalCompositeOperation

设置合成模式

javascript
ctx.globalCompositeOperation = 'source-over'; // 默认值

常用合成模式

基础模式

模式说明效果
source-over默认,新内容覆盖旧内容源在目标之上
source-in只显示源与目标重叠的部分(非重叠区域透明)用目标裁剪源
source-out只显示源不与目标重叠的部分排除重叠
source-atop源在目标之上,但只在目标范围内显示源被目标裁剪

目标模式

模式说明效果
destination-over目标在源之上目标覆盖源
destination-in只保留目标与源重叠的部分用源裁剪目标
destination-out目标中删除与源重叠的部分橡皮擦效果
destination-atop目标只在源范围内显示目标被源裁剪

特殊模式

模式说明
lighter颜色值相加(变亮)
copy只显示源,清除其他区域
xor排除重叠区域

混合模式

模式说明
multiply正片叠底(变暗)
screen滤色(变亮)
overlay叠加
darken取较暗值
lighten取较亮值
color-dodge颜色减淡
color-burn颜色加深
hard-light强光
soft-light柔光
difference差值
exclusion排除
hue色相
saturation饱和度
color颜色
luminosity亮度

示例:合成模式展示

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>合成模式</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>Canvas 合成模式</h1>
  <canvas id="compCanvas" width="600" height="500"></canvas>

  <script>
    const canvas = document.getElementById('compCanvas');
    const ctx = canvas.getContext('2d');

    const modes = [
      'source-over', 'source-in', 'source-out', 'source-atop',
      'destination-over', 'destination-in', 'destination-out', 'destination-atop',
      'lighter', 'copy', 'xor', 'multiply',
      'screen', 'overlay', 'darken', 'lighten'
    ];

    const cols = 4;
    const cellW = 140;
    const cellH = 120;
    const startX = 20;
    const startY = 40;

    // 标题
    ctx.fillStyle = '#333';
    ctx.font = 'bold 16px sans-serif';
    ctx.fillText('Canvas 合成模式一览', 20, 25);

    modes.forEach((mode, i) => {
      const col = i % cols;
      const row = Math.floor(i / cols);
      const x = startX + col * cellW;
      const y = startY + row * cellH;

      // 绘制目标(蓝色矩形)
      ctx.save();
      ctx.globalCompositeOperation = 'source-over';
      ctx.fillStyle = '#4285f4';
      ctx.fillRect(x + 10, y + 10, 60, 60);

      // 应用合成模式绘制源(红色圆形)
      ctx.globalCompositeOperation = mode;
      ctx.fillStyle = '#ea4335';
      ctx.beginPath();
      ctx.arc(x + 70, y + 40, 30, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();

      // 标签
      ctx.fillStyle = '#333';
      ctx.font = '10px monospace';
      ctx.fillText(mode, x + 5, y + 95);
    });
  </script>
</body>
</html>

globalAlpha

globalAlpha 设置全局透明度,影响所有后续绘制操作。

javascript
// 设置全局透明度
ctx.globalAlpha = 0.5; // 半透明
ctx.fillRect(50, 50, 100, 100); // 以 50% 透明度绘制

ctx.globalAlpha = 1.0; // 恢复不透明
ctx.fillRect(80, 80, 100, 100); // 完全不透明

globalAlpha vs fillStyle 透明度

javascript
// globalAlpha 影响所有绘制操作(包括 stroke、drawImage 等)
ctx.globalAlpha = 0.5;
ctx.fillRect(10, 10, 50, 50);   // 半透明
ctx.strokeRect(70, 10, 50, 50); // 半透明描边

// fillStyle 的透明度只影响 fill 操作
ctx.globalAlpha = 1.0;
ctx.fillStyle = 'rgba(66, 133, 244, 0.5)';
ctx.fillRect(10, 10, 50, 50);   // 半透明
ctx.strokeRect(70, 10, 50, 50); // 不透明描边(不受 fillStyle 影响)

clip() 裁剪路径

clip() 使用当前路径创建裁剪区域,后续绘制操作只在此区域内可见。

基本用法

javascript
ctx.save();

// 定义裁剪路径
ctx.beginPath();
ctx.arc(150, 150, 80, 0, Math.PI * 2);
ctx.clip(); // 应用裁剪

// 后续绘制只在圆形区域内可见
ctx.fillStyle = '#4285f4';
ctx.fillRect(50, 50, 200, 200); // 只有圆形部分可见

ctx.restore(); // 恢复裁剪区域

裁剪路径的特性

javascript
// 1. 裁剪路径是累积的
ctx.beginPath();
ctx.rect(0, 0, 100, 100);
ctx.clip();

ctx.beginPath();
ctx.rect(50, 50, 100, 100);
ctx.clip(); // 两个矩形的交集

// 2. 裁剪是持久的(直到 restore)
ctx.save();
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();

// 所有绘制都受裁剪影响
ctx.fillRect(0, 0, 300, 300);

ctx.restore(); // 恢复裁剪

clip(path, fillRule)

clip() 也接受 Path2D 对象和填充规则参数:

javascript
// 使用 Path2D
const starPath = new Path2D();
starPath.moveTo(100, 20);
// ... 构建星形路径 ...
ctx.clip(starPath);

// 使用填充规则
ctx.beginPath();
ctx.rect(0, 0, 100, 100);
ctx.rect(50, 50, 100, 100);
ctx.clip('evenodd'); // 使用 evenodd 规则

实战示例

示例 1:图片遮罩效果

javascript
/**
 * 使用 clip() 实现图片圆形遮罩
 */
function drawCircleMask(ctx, img, x, y, radius) {
  ctx.save();

  // 创建圆形裁剪区域
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2);
  ctx.clip();

  // 绘制图片(只有圆形区域可见)
  ctx.drawImage(img, x - radius, y - radius, radius * 2, radius * 2);

  ctx.restore();

  // 绘制边框
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2);
  ctx.strokeStyle = '#ccc';
  ctx.lineWidth = 2;
  ctx.stroke();
}

示例 2:合成模式实现发光效果

javascript
/**
 * 使用合成模式绘制发光圆
 */
function drawGlow(ctx, x, y, radius, color) {
  ctx.save();

  // 1. 绘制发光层
  ctx.globalCompositeOperation = 'lighter';

  for (let i = 5; i >= 1; i--) {
    const alpha = 0.05 * (6 - i);
    ctx.fillStyle = `rgba(${hexToRgb(color)}, ${alpha})`;
    ctx.beginPath();
    ctx.arc(x, y, radius + i * 15, 0, Math.PI * 2);
    ctx.fill();
  }

  // 2. 绘制实心圆
  ctx.globalCompositeOperation = 'source-over';
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2);
  ctx.fill();

  ctx.restore();
}

function hexToRgb(hex) {
  const num = parseInt(hex.replace('#', ''), 16);
  return `${(num >> 16) & 255}, ${(num >> 8) & 255}, ${num & 255}`;
}

示例 3:橡皮擦效果

javascript
/**
 * 使用 destination-out 实现橡皮擦
 */
function drawWithEraser() {
  // 绘制背景
  ctx.fillStyle = '#4285f4';
  ctx.fillRect(0, 0, 400, 300);

  // 绘制一些图形
  ctx.fillStyle = '#34a853';
  ctx.fillRect(50, 50, 100, 100);
  ctx.fillStyle = '#ea4335';
  ctx.beginPath();
  ctx.arc(250, 150, 60, 0, Math.PI * 2);
  ctx.fill();

  // 使用 destination-out 模式"擦除"
  ctx.globalCompositeOperation = 'destination-out';

  // 擦除线条
  ctx.strokeStyle = 'rgba(0, 0, 0, 1)';
  ctx.lineWidth = 20;
  ctx.lineCap = 'round';

  ctx.beginPath();
  ctx.moveTo(30, 30);
  ctx.quadraticCurveTo(200, 250, 380, 280);
  ctx.stroke();

  // 恢复默认合成模式
  ctx.globalCompositeOperation = 'source-over';
}

示例 4:多区域裁剪展示

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>裁剪效果</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
    img { display: none; }
  </style>
</head>
<body>
  <h1>Canvas 裁剪效果</h1>
  <canvas id="clipCanvas" width="500" height="400"></canvas>

  <script>
    const canvas = document.getElementById('clipCanvas');
    const ctx = canvas.getContext('2d');

    // 创建渐变背景
    const bgGrad = ctx.createLinearGradient(0, 0, 500, 400);
    bgGrad.addColorStop(0, '#4285f4');
    bgGrad.addColorStop(0.5, '#ea4335');
    bgGrad.addColorStop(1, '#fbbc05');
    ctx.fillStyle = bgGrad;
    ctx.fillRect(0, 0, 500, 400);

    // === 圆形裁剪 ===
    ctx.save();
    ctx.beginPath();
    ctx.arc(80, 100, 60, 0, Math.PI * 2);
    ctx.clip();

    const grad1 = ctx.createLinearGradient(20, 40, 140, 160);
    grad1.addColorStop(0, '#34a853');
    grad1.addColorStop(1, '#9c27b0');
    ctx.fillStyle = grad1;
    ctx.fillRect(20, 40, 120, 120);
    ctx.restore();

    ctx.fillStyle = '#fff';
    ctx.font = '12px sans-serif';
    ctx.fillText('圆形裁剪', 40, 185);

    // === 星形裁剪 ===
    ctx.save();
    const cx = 250, cy = 100, outerR = 60, innerR = 25;
    ctx.beginPath();
    for (let i = 0; i < 10; i++) {
      const r = i % 2 === 0 ? outerR : innerR;
      const angle = (Math.PI / 5) * i - Math.PI / 2;
      const x = cx + r * Math.cos(angle);
      const y = cy + r * Math.sin(angle);
      if (i === 0) ctx.moveTo(x, y);
      else ctx.lineTo(x, y);
    }
    ctx.closePath();
    ctx.clip();

    ctx.fillStyle = '#1a237e';
    ctx.fillRect(180, 40, 140, 120);
    ctx.restore();

    ctx.fillStyle = '#fff';
    ctx.fillText('星形裁剪', 215, 185);

    // === 圆角矩形裁剪 ===
    ctx.save();
    ctx.beginPath();
    const rx = 380, ry = 40, rw = 90, rh = 120, rr = 20;
    ctx.moveTo(rx + rr, ry);
    ctx.arcTo(rx + rw, ry, rx + rw, ry + rh, rr);
    ctx.arcTo(rx + rw, ry + rh, rx, ry + rh, rr);
    ctx.arcTo(rx, ry + rh, rx, ry, rr);
    ctx.arcTo(rx, ry, rx + rw, ry, rr);
    ctx.closePath();
    ctx.clip();

    const grad2 = ctx.createRadialGradient(425, 100, 5, 425, 100, 60);
    grad2.addColorStop(0, '#ff5722');
    grad2.addColorStop(1, '#e91e63');
    ctx.fillStyle = grad2;
    ctx.fillRect(380, 40, 90, 120);
    ctx.restore();

    ctx.fillStyle = '#fff';
    ctx.fillText('圆角裁剪', 395, 185);

    // === 合成模式展示区 ===
    ctx.fillStyle = '#333';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('合成模式:lighter(叠加发光)', 20, 230);

    ctx.save();
    ctx.globalCompositeOperation = 'lighter';

    ctx.fillStyle = 'rgba(66, 133, 244, 0.6)';
    ctx.beginPath();
    ctx.arc(100, 310, 60, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = 'rgba(234, 67, 53, 0.6)';
    ctx.beginPath();
    ctx.arc(160, 310, 60, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = 'rgba(52, 168, 83, 0.6)';
    ctx.beginPath();
    ctx.arc(130, 360, 60, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();

    // === 橡皮擦文字效果 ===
    ctx.fillStyle = '#333';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('合成模式:destination-out(擦除)', 300, 230);

    ctx.save();
    ctx.fillStyle = '#4285f4';
    ctx.fillRect(300, 250, 180, 100);

    ctx.globalCompositeOperation = 'destination-out';
    ctx.font = 'bold 40px sans-serif';
    ctx.fillText('ERASE', 310, 325);
    ctx.restore();
  </script>
</body>
</html>

注意事项

1. clip() 的不可逆性

javascript
// 裁剪一旦应用,无法取消(只能通过 restore)
ctx.beginPath();
ctx.rect(0, 0, 100, 100);
ctx.clip();

// 后续所有绘制都受裁剪影响
ctx.fillRect(0, 0, 500, 500); // 只有 100x100 可见

// 必须用 save/restore 配合
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, 100, 100);
ctx.clip();
// ... 绘制 ...
ctx.restore(); // 裁剪被恢复

2. 合成模式的性能影响

某些合成模式(特别是 lighterscreen 等)可能影响渲染性能:

javascript
// 在动画循环中,尽量使用默认的 source-over
// 只在需要特殊效果时切换合成模式
ctx.save();
ctx.globalCompositeOperation = 'lighter';
drawGlowEffect(ctx);
ctx.restore(); // 立即恢复

3. globalAlpha 的累积效应

javascript
// globalAlpha 不会自动重置
ctx.globalAlpha = 0.5;
ctx.fillRect(10, 10, 50, 50); // 50% 透明

// 后续绘制仍然是 50% 透明!
ctx.fillRect(70, 10, 50, 50); // 还是 50% 透明

// 需要手动重置
ctx.globalAlpha = 1.0;

最佳实践

1. 安全的合成操作封装

javascript
/**
 * 安全执行合成操作
 */
function withComposite(ctx, mode, drawFn) {
  ctx.save();
  ctx.globalCompositeOperation = mode;
  drawFn();
  ctx.restore();
}

// 使用
withComposite(ctx, 'lighter', () => {
  ctx.fillStyle = 'rgba(66, 133, 244, 0.5)';
  ctx.fillRect(50, 50, 100, 100);
});

withComposite(ctx, 'destination-out', () => {
  ctx.fillStyle = '#000';
  ctx.fillRect(60, 60, 80, 80);
});

2. 裁剪与合成模式选择指南

需求推荐方案
隐藏部分内容clip()
透明遮罩source-in
擦除效果destination-out
发光效果lighter
高光效果screen
阴影效果multiply
纹理叠加overlay

下一节

继续学习:像素操作

参考链接