pin_drop当前位置:知识文库 ❯ 图文

多媒体:Canvas裁剪 - 完整教程与代码示例

一、教程简介

Canvas 裁剪(Clipping)允许开发者将绘制区域限制在指定路径内,超出路径的内容不会被显示。通过 clip() 方法,可以创建各种形状的"窗口"效果,如圆形头像、不规则形状遮罩、渐显动画等。本教程将详细介绍 clip() 方法的用法、裁剪路径的创建以及实际应用技巧。


二、核心概念

什么是裁剪

裁剪是将 Canvas 的绘制区域限制在指定路径内的操作。调用 clip() 后,后续所有绘制操作都只在裁剪路径内部可见,超出部分被"裁掉"。

代码示例

┌──────────────────┐
│                  │
│   ╭────────╮     │  裁剪路径(圆形)
│   │ 可见区 │     │  只有路径内的内容可见
│   │   域   │     │
│   ╰────────╯     │
│                  │  路径外的内容被裁掉
└──────────────────┘

裁剪的特点

特点 说明
基于路径 裁剪区域由当前路径定义
累积性 多次调用 clip() 会取交集
持久性 裁剪效果持续到 restore() 恢复
不可逆 无法直接"扩大"裁剪区域,只能恢复

clip 与 globalCompositeOperation 的区别

特性 clip() destination-in
操作方式 定义绘制区域 合成已有内容
性能 较好 较差
灵活性 路径定义 像素级
适用场景 限制绘制区域 创建遮罩效果

三、语法与用法

方法

方法 语法 说明
clip() ctx.clip() 使用当前路径作为裁剪区域
clip(fillRule) ctx.clip(fillRule) 使用指定填充规则裁剪
clip(path) ctx.clip(path) 使用 Path2D 作为裁剪区域
clip(path, fillRule) ctx.clip(path, fillRule) 使用 Path2D 和填充规则裁剪

fillRule 参数

说明
nonzero 非零环绕规则(默认)
evenodd 奇偶规则

裁剪的标准流程

代码示例

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>

五、浏览器兼容性

浏览器 支持版本 Path2D 参数 fillRule 参数
Chrome 1+ 36+ 23+
Firefox 1.5+ 31+ 10+
Safari 2+ 8+ 7+
Edge 12+ 14+ 12+
IE 9+ 不支持 不支持

六、注意事项与最佳实践

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,150

3. 裁剪路径需要先定义

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() 方法。

标签: Canvas裁剪 clip方法 圆形头像 聚光灯效果 Canvas教程 裁剪路径

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 多媒体:Canvas合成 - 完整教程与代码示例 下一篇: 多媒体:动画 - 完整教程与代码示例

poll相关推荐