pin_drop当前位置:知识文库 ❯ 图文
多媒体:Canvas裁剪 - 完整教程与代码示例
一、教程简介
Canvas 裁剪(Clipping)允许开发者将绘制区域限制在指定路径内,超出路径的内容不会被显示。通过 clip() 方法,可以创建各种形状的"窗口"效果,如圆形头像、不规则形状遮罩、渐显动画等。本教程将详细介绍 clip() 方法的用法、裁剪路径的创建以及实际应用技巧。
二、核心概念
什么是裁剪
裁剪是将 Canvas 的绘制区域限制在指定路径内的操作。调用 clip() 后,后续所有绘制操作都只在裁剪路径内部可见,超出部分被"裁掉"。
代码示例
┌──────────────────┐
│ │
│ ╭────────╮ │ 裁剪路径(圆形)
│ │ 可见区 │ │ 只有路径内的内容可见
│ │ 域 │ │
│ ╰────────╯ │
│ │ 路径外的内容被裁掉
└──────────────────┘裁剪的特点
clip 与 globalCompositeOperation 的区别
三、语法与用法
方法
fillRule 参数
裁剪的标准流程
代码示例
ctx.save(); // 1. 保存状态
ctx.beginPath(); // 2. 开始路径
// ... 定义裁剪路径
ctx.clip(); // 3. 应用裁剪
// ... 绘制内容(只在裁剪区域内可见)
ctx.restore(); // 4. 恢复状态(取消裁剪)四、代码示例
裁剪基础演示
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas 裁剪基础</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0d1117;
font-family: "Microsoft YaHei", sans-serif;
color: #c9d1d9;
}
h1 { text-align: center; color: #f0883e; }
canvas {
display: block;
margin: 20px auto;
border: 1px solid #30363d;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>Canvas 裁剪基础</h1>
<canvas id="clipCanvas" width="750" height="400"></canvas>
<script>
const canvas = document.getElementById('clipCanvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#161b22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// === 圆形裁剪 ===
ctx.save();
ctx.beginPath();
ctx.arc(120, 120, 80, 0, Math.PI * 2);
ctx.clip();
// 绘制渐变背景(只在圆形内可见)
const grad1 = ctx.createLinearGradient(40, 40, 200, 200);
grad1.addColorStop(0, '#e94560');
grad1.addColorStop(1, '#0f3460');
ctx.fillStyle = grad1;
ctx.fillRect(40, 40, 160, 160);
// 绘制装饰
for (let i = 0; i < 10; i++) {
ctx.beginPath();
ctx.arc(40 + Math.random() * 160, 40 + Math.random() * 160, Math.random() * 15 + 5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.3})`;
ctx.fill();
}
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.font = '13px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.fillText('圆形裁剪', 120, 220);
// === 星形裁剪 ===
ctx.save();
ctx.beginPath();
const starCx = 310, starCy = 120;
for (let i = 0; i < 10; i++) {
const r = i % 2 === 0 ? 80 : 35;
const angle = (Math.PI / 5) * i - Math.PI / 2;
const x = starCx + r * Math.cos(angle);
const y = starCy + r * Math.sin(angle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.clip();
const grad2 = ctx.createRadialGradient(starCx, starCy, 0, starCx, starCy, 80);
grad2.addColorStop(0, '#ffd93d');
grad2.addColorStop(1, '#f0883e');
ctx.fillStyle = grad2;
ctx.fillRect(230, 40, 160, 160);
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.fillText('星形裁剪', 310, 220);
// === 多次裁剪(交集) ===
ctx.save();
ctx.beginPath();
ctx.arc(120, 340, 70, 0, Math.PI * 2);
ctx.clip();
ctx.beginPath();
ctx.arc(160, 340, 60, 0, Math.PI * 2);
ctx.clip();
// 只在两个圆的交集区域可见
const grad3 = ctx.createLinearGradient(50, 270, 220, 410);
grad3.addColorStop(0, '#539bf5');
grad3.addColorStop(1, '#b083f0');
ctx.fillStyle = grad3;
ctx.fillRect(50, 270, 200, 140);
ctx.restore();
// === evenodd 裁剪 ===
ctx.save();
ctx.beginPath();
ctx.arc(350, 340, 70, 0, Math.PI * 2);
ctx.arc(350, 340, 35, 0, Math.PI * 2);
ctx.clip('evenodd'); // 使用奇偶规则,内圆镂空
const grad4 = ctx.createRadialGradient(350, 340, 0, 350, 340, 70);
grad4.addColorStop(0, '#57ab5a');
grad4.addColorStop(1, '#0f3460');
ctx.fillStyle = grad4;
ctx.fillRect(280, 270, 140, 140);
ctx.restore();
</script>
</body>
</html>裁剪实战 - 圆形头像与聚光灯效果
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas 裁剪实战</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
font-family: "Microsoft YaHei", sans-serif;
color: #e0e0e0;
}
h1 { text-align: center; color: #e94560; }
canvas {
display: block;
margin: 20px auto;
border: 1px solid #333;
border-radius: 8px;
cursor: none;
}
.info { text-align: center; color: #888; }
</style>
</head>
<body>
<h1>Canvas 裁剪实战 - 聚光灯效果</h1>
<canvas id="spotlightCanvas" width="600" height="400"></canvas>
<p class="info">移动鼠标查看聚光灯效果</p>
<script>
const canvas = document.getElementById('spotlightCanvas');
const ctx = canvas.getContext('2d');
let mouseX = 300, mouseY = 200;
// 创建背景内容
function createBackground() {
const offscreen = document.createElement('canvas');
offscreen.width = 600;
offscreen.height = 400;
const octx = offscreen.getContext('2d');
const grad = octx.createLinearGradient(0, 0, 600, 400);
grad.addColorStop(0, '#e94560');
grad.addColorStop(0.3, '#0f3460');
grad.addColorStop(0.6, '#533483');
grad.addColorStop(1, '#e94560');
octx.fillStyle = grad;
octx.fillRect(0, 0, 600, 400);
for (let i = 0; i < 30; i++) {
octx.beginPath();
octx.arc(Math.random() * 600, Math.random() * 400, Math.random() * 30 + 10, 0, Math.PI * 2);
octx.fillStyle = `rgba(255,255,255,${Math.random() * 0.2})`;
octx.fill();
}
octx.fillStyle = '#fff';
octx.font = 'bold 36px "Microsoft YaHei"';
octx.textAlign = 'center';
octx.textBaseline = 'middle';
octx.fillText('Canvas 聚光灯效果', 300, 180);
octx.font = '18px "Microsoft YaHei"';
octx.fillText('移动鼠标探索隐藏内容', 300, 230);
return offscreen;
}
const bgCanvas = createBackground();
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
draw();
});
function draw() {
ctx.clearRect(0, 0, 600, 400);
// 暗色背景
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
// 聚光灯裁剪区域
ctx.save();
ctx.beginPath();
ctx.arc(mouseX, mouseY, 80, 0, Math.PI * 2);
ctx.clip();
// 在裁剪区域内绘制亮色内容
ctx.drawImage(bgCanvas, 0, 0);
ctx.restore();
// 聚光灯边框
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(mouseX, mouseY, 80, 0, Math.PI * 2);
ctx.stroke();
// 光晕效果
const glowGrad = ctx.createRadialGradient(mouseX, mouseY, 70, mouseX, mouseY, 100);
glowGrad.addColorStop(0, 'rgba(255, 255, 255, 0.05)');
glowGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(mouseX, mouseY, 100, 0, Math.PI * 2);
ctx.fill();
}
draw();
</script>
</body>
</html>五、浏览器兼容性
六、注意事项与最佳实践
1. 必须使用 save/restore
裁剪是累积的且不可逆,必须使用 save/restore 来管理:
代码示例
// 正确
ctx.save();
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();
// ... 绘制
ctx.restore(); // 裁剪被取消
// 错误:裁剪会一直生效
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();
// ... 之后所有绘制都被裁剪!2. 裁剪是累积的
多次调用 clip() 会取交集:
代码示例
ctx.save();
ctx.beginPath();
ctx.rect(50, 50, 100, 100);
ctx.clip(); // 裁剪为 100x100 矩形
ctx.beginPath();
ctx.rect(80, 80, 100, 100);
ctx.clip(); // 裁剪为两个矩形的交集
// 可绘制区域:80,80 到 150,1503. 裁剪路径需要先定义
clip() 使用当前路径作为裁剪区域,必须在调用前定义路径:
代码示例
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip(); // 使用圆形路径裁剪4. 裁剪与变换
裁剪路径受当前变换的影响:
代码示例
ctx.save();
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.beginPath();
ctx.rect(-50, -50, 100, 100);
ctx.clip(); // 裁剪区域是旋转后的正方形
ctx.restore();5. 性能考虑
复杂的裁剪路径可能影响性能,尽量使用简单路径:
代码示例
// 好:简单圆形裁剪
ctx.arc(cx, cy, r, 0, Math.PI * 2);
// 差:复杂路径裁剪
// 大量点组成的不规则路径七、代码规范示例
代码示例
/**
* 裁剪工具类
*/
class ClipHelper {
constructor(ctx) {
this.ctx = ctx;
}
/**
* 在裁剪区域内绘制
* @param {Function} clipPathFn - 定义裁剪路径
* @param {Function} drawFn - 绘制函数
* @param {string} [fillRule='nonzero'] - 填充规则
*/
clipAndDraw(clipPathFn, drawFn, fillRule = 'nonzero') {
const ctx = this.ctx;
ctx.save();
ctx.beginPath();
clipPathFn(ctx);
ctx.clip(fillRule);
drawFn(ctx);
ctx.restore();
}
/**
* 圆形裁剪
* @param {number} cx - 圆心 X
* @param {number} cy - 圆心 Y
* @param {number} radius - 半径
* @param {Function} drawFn - 绘制函数
*/
circleClip(cx, cy, radius, drawFn) {
this.clipAndDraw(
(ctx) => ctx.arc(cx, cy, radius, 0, Math.PI * 2),
drawFn
);
}
/**
* 圆角矩形裁剪
* @param {number} x - X 坐标
* @param {number} y - Y 坐标
* @param {number} w - 宽度
* @param {number} h - 高度
* @param {number} r - 圆角半径
* @param {Function} drawFn - 绘制函数
*/
roundedRectClip(x, y, w, h, r, drawFn) {
this.clipAndDraw((ctx) => {
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}, drawFn);
}
/**
* 镂空裁剪
* @param {Function} outerPathFn - 外部路径
* @param {Function} innerPathFn - 内部路径(镂空)
* @param {Function} drawFn - 绘制函数
*/
hollowClip(outerPathFn, innerPathFn, drawFn) {
this.clipAndDraw(
(ctx) => {
outerPathFn(ctx);
innerPathFn(ctx);
},
drawFn,
'evenodd'
);
}
}八、常见问题与解决方案
常见问题
裁剪后无法绘制到裁剪区域外怎么办?
原因是裁剪效果持续存在。解决方案是使用 save/restore 包裹裁剪操作,在 restore 后即可恢复正常绘制范围。
clip() 后图形消失是什么原因?
原因是裁剪路径定义不正确,或路径为空。解决方案是确保在 clip() 前正确创建路径,使用 beginPath() 开始新路径并定义有效的裁剪形状。
裁剪区域不是预期的形状怎么排查?
原因是路径未正确闭合,或变换影响了路径。解决方案是检查路径定义,注意变换对裁剪路径的影响,确保在正确的变换状态下定义裁剪路径。
多次裁剪后区域越来越小怎么办?
原因是裁剪是累积的,多次 clip 取交集。解决方案是每次裁剪前 restore 之前的裁剪,或重新 save 开始新的裁剪上下文。
如何创建文字形状的裁剪?
Canvas 的 fillText 不创建路径,可以使用 clip-path CSS 或使用 SVG 文字路径作为替代方案。也可以使用第三方库将文字转为路径后再进行裁剪操作。
九、总结
Canvas 裁剪是限制绘制区域的强大工具:
-
clip():使用当前路径作为裁剪区域
-
save/restore:必须配合使用,防止裁剪持续影响
-
累积性:多次 clip 取交集
-
fillRule:nonzero 和 evenodd 控制镂空效果
-
Path2D:可以使用 Path2D 对象作为裁剪路径
-
变换影响:裁剪路径受当前变换影响
-
实战应用:圆形头像、聚光灯、遮罩、不规则形状显示
-
性能:简单裁剪路径性能好,复杂路径需注意
小贴士
裁剪(clip)与合成模式(destination-in)都可以实现遮罩效果,但裁剪的性能更好,因为它在绘制阶段就限制了渲染区域,而不是先绘制再合成。在需要频繁更新的动画场景中,优先使用 clip() 方法。
本文涉及AI创作
内容由AI创作,请仔细甄别