变换(平移/旋转/缩放)
Canvas 提供了完整的 2D 仿射变换系统,包括平移(translate)、旋转(rotate)、缩放(scale)以及自定义变换矩阵。变换是 Canvas 实现复杂图形布局的核心机制,结合 save()/restore() 可以构建出层次分明的图形场景。本节将详细介绍 Canvas 变换系统的使用方法。
前置知识
阅读本节前,建议先了解:图像操作
基础概念
Canvas 的变换系统基于 2D 仿射变换矩阵。所有变换操作都是对当前变换矩阵(CTM)进行乘法运算。变换会影响后续所有绘图操作,包括坐标、形状和图像。
变换矩阵原理
Canvas 内部使用 3x3 的变换矩阵:
| a c e | | x | | ax + cy + e |
| b d f | x | y | = | bx + dy + f |
| 0 0 1 | | 1 | | 1 |其中 a, b, c, d, e, f 是变换参数,可以通过 setTransform() 或 transform() 直接设置。
translate() 平移
translate(tx, ty) 将坐标系原点移动到 (tx, ty) 位置。
javascript
ctx.translate(100, 50); // 将原点移动到 (100, 50)
// 现在 (0, 0) 对应画布上的 (100, 50)
ctx.fillRect(0, 0, 50, 50); // 绘制在 (100, 50) 处平移的应用场景
javascript
// 1. 绘制多个相同图案
function drawPattern(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = '#4285f4';
ctx.fillRect(-25, -25, 50, 50);
ctx.restore();
}
drawPattern(ctx, 100, 100);
drawPattern(ctx, 200, 100);
drawPattern(ctx, 150, 200);
// 2. 以图形中心为原点绘制(简化计算)
ctx.translate(250, 250); // 移到画布中心
ctx.fillRect(-50, -50, 100, 100); // 以中心为原点rotate() 旋转
rotate(angle) 围绕当前原点旋转坐标系,角度单位为弧度。
javascript
ctx.rotate(Math.PI / 4); // 顺时针旋转 45 度
ctx.fillRect(50, 0, 100, 50);旋转的应用
javascript
// 围绕图形中心旋转
function drawRotatedRect(ctx, cx, cy, width, height, angle) {
ctx.save();
ctx.translate(cx, cy); // 移到中心
ctx.rotate(angle); // 旋转
ctx.fillRect(-width / 2, -height / 2, width, height); // 以中心绘制
ctx.restore();
}
// 使用
drawRotatedRect(ctx, 200, 200, 100, 60, Math.PI / 6); // 旋转 30 度
drawRotatedRect(ctx, 200, 200, 100, 60, Math.PI / 3); // 旋转 60 度scale() 缩放
scale(sx, sy) 分别在 X 和 Y 方向缩放坐标系。
javascript
ctx.scale(2, 2); // 放大 2 倍
ctx.scale(1, -1); // Y 轴翻转
ctx.scale(-1, 1); // X 轴翻转
ctx.scale(0.5, 0.5); // 缩小到一半缩放的应用
javascript
// 1. 镜像翻转(水平)
ctx.save();
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(img, 0, 0); // 水平镜像
ctx.restore();
// 2. 绘制缩放的图形
const scales = [0.5, 0.75, 1.0, 1.25, 1.5];
scales.forEach((s, i) => {
ctx.save();
ctx.translate(50 + i * 100, 200);
ctx.scale(s, s);
ctx.fillStyle = '#4285f4';
ctx.fillRect(-25, -25, 50, 50);
ctx.restore();
});setTransform() 与 transform()
setTransform()
setTransform(a, b, c, d, e, f) 直接设置变换矩阵(替换而非累加):
javascript
// 重置为单位矩阵(等同于 resetTransform)
ctx.setTransform(1, 0, 0, 1, 0, 0);
// 设置自定义变换
ctx.setTransform(2, 0.5, -0.5, 2, 100, 100);transform()
transform(a, b, c, d, e, f) 将新矩阵与当前矩阵相乘(累加变换):
javascript
ctx.translate(100, 100); // 先平移
ctx.transform(2, 0, 0, 2, 0, 0); // 再放大 2 倍setTransform 与 transform 对比
| 方法 | 行为 | 类比 |
|---|---|---|
setTransform() | 替换当前矩阵 | ctx = newMatrix |
transform() | 与当前矩阵相乘 | ctx = ctx * newMatrix |
resetTransform() | 重置为单位矩阵 | ctx = identity |
变换方向
Canvas 变换遵循"向右累加"规则:先调用的变换后执行(从右到左应用)。
javascript
// 组合变换:先旋转再平移
ctx.translate(200, 200); // 第二步:平移
ctx.rotate(Math.PI / 4); // 第一步:旋转
// 等价于:
ctx.translate(200, 200).rotate(Math.PI / 4)
// 图形先在自己的坐标系中旋转,然后平移到 (200, 200)实战示例
示例 1:变换操作综合演示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Canvas 变换</title>
<style>
canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
</style>
</head>
<body>
<h1>Canvas 变换操作</h1>
<canvas id="transformCanvas" width="600" height="500"></canvas>
<script>
const canvas = document.getElementById('transformCanvas');
const ctx = canvas.getContext('2d');
// 辅助函数:绘制坐标轴
function drawAxes(ctx, length, color) {
ctx.strokeStyle = color || '#999';
ctx.lineWidth = 1;
// X 轴
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(length, 0);
ctx.stroke();
// Y 轴
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, length);
ctx.stroke();
// 原点
ctx.fillStyle = '#ea4335';
ctx.beginPath();
ctx.arc(0, 0, 3, 0, Math.PI * 2);
ctx.fill();
}
// === 1. 平移演示 ===
ctx.fillStyle = '#333';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('1. 平移', 20, 20);
ctx.save();
ctx.translate(80, 80);
drawAxes(ctx, 40);
ctx.fillStyle = '#4285f4';
ctx.fillRect(0, 0, 30, 30);
ctx.restore();
// === 2. 旋转演示 ===
ctx.fillText('2. 旋转', 200, 20);
ctx.save();
ctx.translate(280, 80);
drawAxes(ctx, 40);
ctx.rotate(Math.PI / 6); // 旋转 30 度
ctx.fillStyle = '#34a853';
ctx.fillRect(0, 0, 30, 30);
ctx.restore();
// === 3. 缩放演示 ===
ctx.fillText('3. 缩放', 420, 20);
ctx.save();
ctx.translate(500, 80);
drawAxes(ctx, 40);
ctx.scale(1.5, 0.8);
ctx.fillStyle = '#ea4335';
ctx.fillRect(0, 0, 30, 30);
ctx.restore();
// === 4. 组合变换:绘制花朵 ===
ctx.fillText('4. 组合变换(花朵)', 20, 170);
ctx.save();
ctx.translate(120, 280);
for (let i = 0; i < 12; i++) {
ctx.rotate(Math.PI / 6);
ctx.fillStyle = `hsla(${i * 30}, 70%, 60%, 0.6)`;
ctx.beginPath();
ctx.ellipse(0, -30, 15, 30, 0, 0, Math.PI * 2);
ctx.fill();
}
// 花心
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI * 2);
ctx.fillStyle = '#fbbc05';
ctx.fill();
ctx.restore();
// === 5. Y 轴翻转镜像 ===
ctx.fillText('5. 镜像翻转', 250, 170);
// 原始图形
ctx.save();
ctx.translate(320, 230);
drawAxes(ctx, 30);
ctx.fillStyle = '#4285f4';
ctx.fillRect(-15, -15, 30, 30);
ctx.restore();
// 镜像图形
ctx.save();
ctx.translate(380, 230);
drawAxes(ctx, 30);
ctx.scale(-1, 1);
ctx.fillStyle = '#ea4335';
ctx.fillRect(-15, -15, 30, 30);
ctx.restore();
// 标签
ctx.fillStyle = '#333';
ctx.font = '12px sans-serif';
ctx.fillText('原始', 305, 280);
ctx.fillText('镜像', 365, 280);
// === 6. 变换矩阵动画提示 ===
ctx.fillStyle = '#333';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('6. 动态旋转方块', 470, 170);
ctx.save();
ctx.translate(550, 280);
const time = Date.now() / 1000;
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(time + i * Math.PI / 3);
ctx.fillStyle = `hsla(${i * 60}, 60%, 50%, 0.4)`;
ctx.fillRect(-40, -10, 80, 20);
ctx.restore();
}
ctx.restore();
</script>
</body>
</html>示例 2:时钟绘制
javascript
/**
* 使用变换绘制时钟
*/
function drawClock(ctx, cx, cy, radius) {
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
const millis = now.getMilliseconds();
// 表盘
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.stroke();
// 刻度
ctx.save();
ctx.translate(cx, cy);
for (let i = 0; i < 60; i++) {
ctx.rotate(Math.PI / 30);
ctx.beginPath();
if (i % 5 === 0) {
ctx.moveTo(0, radius - 15);
ctx.lineTo(0, radius - 5);
ctx.lineWidth = 2;
} else {
ctx.moveTo(0, radius - 8);
ctx.lineTo(0, radius - 5);
ctx.lineWidth = 1;
}
ctx.stroke();
}
ctx.restore();
// 时针
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((hours + minutes / 60) * Math.PI / 6);
ctx.beginPath();
ctx.moveTo(0, 10);
ctx.lineTo(0, -radius * 0.5);
ctx.lineWidth = 4;
ctx.strokeStyle = '#333';
ctx.stroke();
ctx.restore();
// 分针
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((minutes + seconds / 60) * Math.PI / 30);
ctx.beginPath();
ctx.moveTo(0, 10);
ctx.lineTo(0, -radius * 0.7);
ctx.lineWidth = 2;
ctx.strokeStyle = '#333';
ctx.stroke();
ctx.restore();
// 秒针
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((seconds + millis / 1000) * Math.PI / 30);
ctx.beginPath();
ctx.moveTo(0, 15);
ctx.lineTo(0, -radius * 0.85);
ctx.lineWidth = 1;
ctx.strokeStyle = '#ea4335';
ctx.stroke();
ctx.restore();
// 中心点
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fillStyle = '#ea4335';
ctx.fill();
}
// 使用
const canvas = document.getElementById('clockCanvas');
const ctx = canvas.getContext('2d');
function updateClock() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawClock(ctx, 200, 200, 150);
requestAnimationFrame(updateClock);
}
updateClock();示例 3:变换矩阵分解
javascript
/**
* 从变换矩阵中提取分解参数
* @param {DOMMatrix} matrix
* @returns {{ translateX, translateY, scaleX, scaleY, rotation, skewX, skewY }}
*/
function decomposeMatrix(matrix) {
return {
translateX: matrix.m41,
translateY: matrix.m42,
scaleX: matrix.m11,
scaleY: matrix.m22,
rotation: Math.atan2(matrix.m12, matrix.m11),
skewX: Math.atan2(matrix.m21, matrix.m11),
skewY: Math.atan2(matrix.m12, matrix.m22)
};
}
// 使用
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.scale(2, 1);
const matrix = ctx.getTransform();
const decomposed = decomposeMatrix(matrix);
console.log(decomposed);注意事项
1. 变换是累加的
javascript
// 错误:变换累积
for (let i = 0; i < 10; i++) {
ctx.rotate(Math.PI / 6); // 每次都累加旋转!
ctx.fillRect(0, 0, 50, 50);
}
// 正确:使用 save/restore 或 setTransform
for (let i = 0; i < 10; i++) {
ctx.save();
ctx.rotate(i * Math.PI / 6);
ctx.fillRect(0, 0, 50, 50);
ctx.restore();
}2. 缩放影响所有属性
javascript
ctx.scale(2, 2);
ctx.lineWidth = 1; // 实际渲染为 2px
ctx.font = '16px sans-serif'; // 实际渲染为 32px3. 旋转中心
旋转总是围绕当前原点进行,不是围绕图形中心:
javascript
// 错误:围绕 (0,0) 旋转,图形会"飞走"
ctx.rotate(Math.PI / 4);
ctx.fillRect(200, 200, 50, 50);
// 正确:先移到图形中心,旋转,再绘制
ctx.translate(225, 225);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-25, -25, 50, 50);最佳实践
1. 变换模式封装
javascript
/**
* 在指定位置、旋转和缩放下执行绘制
*/
function drawAt(ctx, x, y, rotation, scaleX, scaleY, drawFn) {
ctx.save();
ctx.translate(x, y);
if (rotation) ctx.rotate(rotation);
if (scaleX || scaleY) ctx.scale(scaleX || 1, scaleY || 1);
drawFn();
ctx.restore();
}
// 使用
drawAt(ctx, 200, 200, Math.PI / 4, 1, 1, () => {
ctx.fillStyle = '#4285f4';
ctx.fillRect(-25, -25, 50, 50);
});2. 使用 DOMMatrix 进行复杂变换
javascript
// DOMMatrix 提供更直观的变换方式
const matrix = new DOMMatrix();
matrix.translateSelf(200, 200);
matrix.rotateSelf(45);
matrix.scaleSelf(1.5);
// 应用到 Canvas
ctx.setTransform(matrix);
// 或链式调用
ctx.setTransform(
new DOMMatrix()
.translateSelf(200, 200)
.rotateSelf(45)
.scaleSelf(1.5)
);3. 变换组合模式表
| 效果 | 变换顺序 | 代码 |
|---|---|---|
| 绕自身中心旋转 | translate -> rotate -> draw at origin | 见上例 |
| 沿路径放置 | translate -> rotate -> draw | 时钟刻度 |
| 缩放后居中 | translate -> scale -> draw at origin | 同上 |
| 斜切效果 | setTransform(1, skewX, skewY, 1, 0, 0) | 平行四边形 |
下一节
继续学习:合成与裁剪