合成与裁剪
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. 合成模式的性能影响
某些合成模式(特别是 lighter、screen 等)可能影响渲染性能:
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 |
下一节
继续学习:像素操作