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

多媒体:Canvas变换 - 完整教程与代码示例

一、教程简介

Canvas 变换是图形编程中的核心概念,它允许开发者对坐标系进行平移、旋转和缩放操作,从而简化复杂图形的绘制。通过 translate()rotate()scale() 三种基本变换,配合 save()/restore() 状态管理,可以高效地实现各种图形变换效果。本教程将详细介绍 Canvas 变换的原理、方法和实际应用。


二、核心概念

变换的本质

Canvas 变换不是移动图形,而是变换坐标系。当调用 translate(100, 50) 时,不是把图形移动到 (100, 50),而是将坐标系原点移动到 (100, 50),之后所有绘制操作都基于新的坐标系。

变换矩阵

Canvas 使用 3x3 的仿射变换矩阵来表示所有变换:

代码示例

| a  c  e |   | 缩放X  倾斜Y  平移X |
| b  d  f | = | 倾斜X  缩放Y  平移Y |
| 0  0  1 |   |   0      0      1   |

变换的顺序

变换的顺序非常重要,不同的顺序会产生不同的结果。Canvas 变换是右乘的,即后调用的变换先应用:

代码示例

// 顺序1:先平移后旋转
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
// 结果:先绕原点旋转,再平移

// 顺序2:先旋转后平移
ctx.rotate(Math.PI / 4);
ctx.translate(100, 100);
// 结果:先平移(沿旋转后的坐标轴),再旋转

三种基本变换

变换 方法 效果
平移 translate(x, y) 移动坐标系原点
旋转 rotate(angle) 绕原点旋转坐标系
缩放 scale(x, y) 缩放坐标系的单位长度

三、语法与用法

方法列表

方法 语法 说明
translate() ctx.translate(x, y) 平移坐标系
rotate() ctx.rotate(angle) 旋转坐标系(弧度)
scale() ctx.scale(x, y) 缩放坐标系
transform() ctx.transform(a, b, c, d, e, f) 应用变换矩阵(叠加)
setTransform() ctx.setTransform(a, b, c, d, e, f) 设置变换矩阵(替换)
resetTransform() ctx.resetTransform() 重置为单位矩阵

transform/setTransform 参数

参数 说明
a 水平缩放
b 垂直倾斜
c 水平倾斜
d 垂直缩放
e 水平移动
f 垂直移动

变换组合公式

代码示例

绕点 (cx, cy) 旋转:
1. translate(cx, cy)
2. rotate(angle)
3. translate(-cx, -cy)

绕点 (cx, cy) 缩放:
1. translate(cx, cy)
2. scale(sx, sy)
3. translate(-cx, -cy)

四、代码示例

变换基础演示

代码示例

<!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="transformCanvas" width="750" height="500"></canvas>

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

        ctx.fillStyle = '#161b22';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // === translate 平移 ===
        ctx.fillStyle = '#8b949e';
        ctx.font = 'bold 16px "Microsoft YaHei"';
        ctx.textAlign = 'center';
        ctx.fillText('translate 平移', 125, 25);

        // 原始位置
        ctx.fillStyle = 'rgba(240, 136, 62, 0.3)';
        ctx.fillRect(25, 40, 80, 50);
        ctx.fillStyle = '#666';
        ctx.font = '11px Arial';
        ctx.fillText('原始', 65, 70);

        // 平移后
        ctx.save();
        ctx.translate(80, 40);
        ctx.fillStyle = '#f0883e';
        ctx.fillRect(25, 0, 80, 50);
        ctx.restore();
        ctx.fillStyle = '#666';
        ctx.fillText('translate(80,40)', 125, 110);

        // === rotate 旋转 ===
        ctx.fillStyle = '#8b949e';
        ctx.font = 'bold 16px "Microsoft YaHei"';
        ctx.fillText('rotate 旋转', 375, 25);

        // 原始
        ctx.fillStyle = 'rgba(83, 155, 245, 0.3)';
        ctx.fillRect(325, 40, 80, 50);

        // 旋转 30 度
        ctx.save();
        ctx.translate(365, 65);
        ctx.rotate(Math.PI / 6);
        ctx.fillStyle = '#539bf5';
        ctx.fillRect(-40, -25, 80, 50);
        ctx.restore();

        ctx.fillStyle = '#666';
        ctx.font = '11px Arial';
        ctx.fillText('rotate(π/6)', 375, 110);

        // === scale 缩放 ===
        ctx.fillStyle = '#8b949e';
        ctx.font = 'bold 16px "Microsoft YaHei"';
        ctx.fillText('scale 缩放', 625, 25);

        // 原始
        ctx.fillStyle = 'rgba(87, 171, 90, 0.3)';
        ctx.fillRect(585, 40, 40, 30);

        // 放大 2 倍
        ctx.save();
        ctx.translate(585, 40);
        ctx.scale(2, 2);
        ctx.fillStyle = '#57ab5a';
        ctx.fillRect(0, 0, 40, 30);
        ctx.restore();

        ctx.fillStyle = '#666';
        ctx.font = '11px Arial';
        ctx.fillText('scale(2,2)', 625, 110);
    </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: #0a0e27;
            font-family: "Microsoft YaHei", sans-serif;
            color: #e0e0e0;
        }
        h1 { text-align: center; color: #64ffda; }
        canvas {
            display: block;
            margin: 20px auto;
            border: 1px solid #1e3a5f;
            border-radius: 8px;
        }
    </style>
</head>
<body>
    <h1>Canvas 变换实战 - 模拟时钟</h1>
    <canvas id="clockCanvas" width="400" height="400"></canvas>

    <script>
        const canvas = document.getElementById('clockCanvas');
        const ctx = canvas.getContext('2d');
        const cx = 200, cy = 200, radius = 160;

        function drawClock() {
            ctx.clearRect(0, 0, 400, 400);

            // 背景
            ctx.fillStyle = '#0a0e27';
            ctx.fillRect(0, 0, 400, 400);

            const now = new Date();
            const hours = now.getHours() % 12;
            const minutes = now.getMinutes();
            const seconds = now.getSeconds();

            // 表盘外圈
            ctx.save();
            ctx.translate(cx, cy);

            // 外圈渐变
            const outerGrad = ctx.createRadialGradient(0, 0, radius - 10, 0, 0, radius);
            outerGrad.addColorStop(0, '#1e3a5f');
            outerGrad.addColorStop(1, '#0a0e27');
            ctx.beginPath();
            ctx.arc(0, 0, radius, 0, Math.PI * 2);
            ctx.fillStyle = outerGrad;
            ctx.fill();

            // 表盘边框
            ctx.beginPath();
            ctx.arc(0, 0, radius, 0, Math.PI * 2);
            ctx.strokeStyle = '#64ffda';
            ctx.lineWidth = 2;
            ctx.stroke();

            // 刻度
            for (let i = 0; i < 60; i++) {
                ctx.save();
                ctx.rotate((Math.PI * 2 / 60) * i);

                if (i % 5 === 0) {
                    // 小时刻度
                    ctx.beginPath();
                    ctx.moveTo(0, -radius + 15);
                    ctx.lineTo(0, -radius + 35);
                    ctx.strokeStyle = '#64ffda';
                    ctx.lineWidth = 3;
                    ctx.lineCap = 'round';
                    ctx.stroke();
                } else {
                    // 分钟刻度
                    ctx.beginPath();
                    ctx.moveTo(0, -radius + 20);
                    ctx.lineTo(0, -radius + 28);
                    ctx.strokeStyle = 'rgba(100, 255, 218, 0.3)';
                    ctx.lineWidth = 1;
                    ctx.stroke();
                }
                ctx.restore();
            }

            // 时针
            const hourAngle = (Math.PI * 2 / 12) * hours + (Math.PI * 2 / 720) * minutes;
            ctx.save();
            ctx.rotate(hourAngle);
            ctx.beginPath();
            ctx.moveTo(-4, 15);
            ctx.lineTo(0, -radius * 0.5);
            ctx.lineTo(4, 15);
            ctx.closePath();
            ctx.fillStyle = '#c9d1d9';
            ctx.fill();
            ctx.restore();

            // 分针
            const minuteAngle = (Math.PI * 2 / 60) * minutes + (Math.PI * 2 / 3600) * seconds;
            ctx.save();
            ctx.rotate(minuteAngle);
            ctx.beginPath();
            ctx.moveTo(-3, 20);
            ctx.lineTo(0, -radius * 0.7);
            ctx.lineTo(3, 20);
            ctx.closePath();
            ctx.fillStyle = '#64ffda';
            ctx.fill();
            ctx.restore();

            // 秒针
            const secondAngle = (Math.PI * 2 / 60) * seconds;
            ctx.save();
            ctx.rotate(secondAngle);
            ctx.beginPath();
            ctx.moveTo(-1, 25);
            ctx.lineTo(0, -radius * 0.85);
            ctx.lineTo(1, 25);
            ctx.closePath();
            ctx.fillStyle = '#f47067';
            ctx.fill();
            ctx.restore();

            // 中心圆
            ctx.beginPath();
            ctx.arc(0, 0, 6, 0, Math.PI * 2);
            ctx.fillStyle = '#f47067';
            ctx.fill();

            ctx.restore();

            requestAnimationFrame(drawClock);
        }

        drawClock();
    </script>
</body>
</html>

五、浏览器兼容性

浏览器 支持版本 备注
Chrome 1+ 完全支持
Firefox 1.5+ 完全支持
Safari 2+ 完全支持
Edge 12+ 完全支持
IE 9+ 完全支持,resetTransform 不支持

六、注意事项与最佳实践

1. 变换是累积的

每次调用变换方法都会在当前变换基础上叠加,而不是替换:

代码示例

ctx.translate(50, 50);  // 原点移到 (50, 50)
ctx.translate(50, 50);  // 原点移到 (100, 100),不是 (50, 50)

2. 使用 save/restore 管理变换

代码示例

ctx.save();
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
// ... 绘制
ctx.restore(); // 恢复变换前的状态

3. 绕指定点旋转的正确方式

代码示例

// 绕 (cx, cy) 旋转 angle 弧度
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.translate(-cx, -cy);
// ... 绘制(使用原始坐标)
ctx.restore();

// 或者更简洁:先平移到原点,旋转后绘制
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
// ... 绘制(以 (0,0) 为中心)
ctx.restore();

4. scale 的负值实现镜像

代码示例

// 水平镜像
ctx.scale(-1, 1);

// 垂直镜像
ctx.scale(1, -1);

// 180度旋转
ctx.scale(-1, -1);

5. setTransform 重置变换

setTransform() 会替换当前变换矩阵,而不是叠加:

代码示例

// 重置为单位矩阵
ctx.setTransform(1, 0, 0, 1, 0, 0);

// 等同于
ctx.resetTransform();

6. 变换影响所有绘制操作

变换不仅影响路径,还影响 fillRectstrokeRectdrawImagefillText 等所有绘制操作。


七、代码规范示例

代码示例

/**
 * 变换工具类
 * 封装常用变换操作
 */
class TransformHelper {
    constructor(ctx) {
        this.ctx = ctx;
    }

    /**
     * 绕指定点旋转
     * @param {number} cx - 中心 X
     * @param {number} cy - 中心 Y
     * @param {number} angle - 旋转角度(弧度)
     * @param {Function} drawFn - 绘制函数
     */
    rotateAround(cx, cy, angle, drawFn) {
        const ctx = this.ctx;
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate(angle);
        ctx.translate(-cx, -cy);
        drawFn(ctx);
        ctx.restore();
    }

    /**
     * 绕指定点缩放
     * @param {number} cx - 中心 X
     * @param {number} cy - 中心 Y
     * @param {number} sx - X 缩放
     * @param {number} sy - Y 缩放
     * @param {Function} drawFn - 绘制函数
     */
    scaleAround(cx, cy, sx, sy, drawFn) {
        const ctx = this.ctx;
        ctx.save();
        ctx.translate(cx, cy);
        ctx.scale(sx, sy);
        ctx.translate(-cx, -cy);
        drawFn(ctx);
        ctx.restore();
    }

    /**
     * 水平镜像
     * @param {number} x - 镜像轴 X 坐标
     * @param {Function} drawFn - 绘制函数
     */
    flipHorizontal(x, drawFn) {
        const ctx = this.ctx;
        ctx.save();
        ctx.translate(x, 0);
        ctx.scale(-1, 1);
        ctx.translate(-x, 0);
        drawFn(ctx);
        ctx.restore();
    }

    /**
     * 垂直镜像
     * @param {number} y - 镜像轴 Y 坐标
     * @param {Function} drawFn - 绘制函数
     */
    flipVertical(y, drawFn) {
        const ctx = this.ctx;
        ctx.save();
        ctx.translate(0, y);
        ctx.scale(1, -1);
        ctx.translate(0, -y);
        drawFn(ctx);
        ctx.restore();
    }

    /**
     * 倾斜变换
     * @param {number} skewX - 水平倾斜
     * @param {number} skewY - 垂直倾斜
     * @param {Function} drawFn - 绘制函数
     */
    skew(skewX, skewY, drawFn) {
        const ctx = this.ctx;
        ctx.save();
        ctx.transform(1, skewY, skewX, 1, 0, 0);
        drawFn(ctx);
        ctx.restore();
    }
}

八、常见问题与解决方案

常见问题

旋转后图形位置不对?

原因rotate() 绕原点旋转,如果图形不在原点,旋转后会偏移。
解决方案:先平移到原点,旋转后再平移回去。

缩放后线条变粗?

原因scale() 会影响 lineWidth
解决方案:在缩放后调整 lineWidth,或使用 save/restore 隔离。

变换累积导致意外结果?

原因:忘记重置变换。
解决方案:使用 save/restoresetTransform(1,0,0,1,0,0) 重置。

如何获取变换后的坐标?

解决方案:使用 DOMPointDOMMatrix
const matrix = ctx.getTransform();
const point = new DOMPoint(x, y);
const transformed = matrix.transformPoint(point);

多次变换后如何恢复?

解决方案:使用 save/restore 配对,或 resetTransform()


九、总结

Canvas 变换是简化复杂图形绘制的强大工具:

  • translate(x, y):平移坐标系原点

  • rotate(angle):绕原点旋转坐标系(弧度制)

  • scale(x, y):缩放坐标系,负值实现镜像

  • 变换顺序:后调用的变换先应用,顺序很重要

  • 绕点变换:translate → rotate/scale → translate(-x, -y)

  • save/restore:管理变换状态,防止累积

  • transform/setTransform:直接操作变换矩阵

  • 实战应用:时钟、万花筒、动画、游戏等

标签: Canvas变换 translate rotate scale 坐标变换 图形编程

本文涉及AI创作

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

list快速访问

上一篇: 多媒体:Canvas图像处理 - 完整教程与代码示例 下一篇: 多媒体:Canvas合成 - 完整教程与代码示例

poll相关推荐