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);
// 结果:先平移(沿旋转后的坐标轴),再旋转三种基本变换
三、语法与用法
方法列表
transform/setTransform 参数
变换组合公式
代码示例
绕点 (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>五、浏览器兼容性
六、注意事项与最佳实践
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. 变换影响所有绘制操作
变换不仅影响路径,还影响 fillRect、strokeRect、drawImage、fillText 等所有绘制操作。
七、代码规范示例
代码示例
/**
* 变换工具类
* 封装常用变换操作
*/
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/restore 或 setTransform(1,0,0,1,0,0) 重置。
如何获取变换后的坐标?
解决方案:使用 DOMPoint 和 DOMMatrix: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:直接操作变换矩阵
-
实战应用:时钟、万花筒、动画、游戏等
本文涉及AI创作
内容由AI创作,请仔细甄别