pin_drop当前位置:知识文库 ❯ 图文
多媒体:保存与恢复 - 完整教程与代码示例
一、教程简介
Canvas 的 save 和 restore 方法是管理绘图状态的核心机制。save 将当前绘图状态压入状态栈,restore 从栈中弹出并恢复最近保存的状态。这对于实现复杂的变换、裁剪和样式切换至关重要,可以避免手动重置每个状态属性。本教程将详细讲解状态栈的工作原理、保存与恢复的使用方法和最佳实践。
二、核心概念
绘图状态
save/restore 管理的绘图状态包括:
注意:
save/restore不保存当前路径和当前位图内容。
状态栈
Canvas 维护一个后进先出(LIFO)的状态栈:
代码示例
save() → 状态入栈
save() → 状态入栈
restore() → 状态出栈(恢复到上一个 save 的状态)
restore() → 状态出栈(恢复到更早的 save 的状态)三、语法与用法
save - 保存状态
代码示例
ctx.save();将当前完整绘图状态压入状态栈。
restore - 恢复状态
代码示例
ctx.restore();从状态栈弹出最近保存的状态,并恢复所有绘图属性。如果栈为空则不做任何操作。
四、代码示例
示例1:save/restore 与变换
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>save/restore 与变换</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0d1117;
color: #c9d1d9;
font-family: 'Segoe UI', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
border: 1px solid #30363d;
border-radius: 8px;
background: #161b22;
}
</style>
</head>
<body>
<h1>save/restore 状态管理</h1>
<canvas id="myCanvas" width="750" height="500"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// === 1. 样式保存与恢复 ===
ctx.fillStyle = '#c9d1d9';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('样式保存与恢复', 375, 25);
// 默认样式
ctx.fillStyle = '#58a6ff';
ctx.fillRect(30, 40, 100, 50);
ctx.save(); // 保存当前状态(fillStyle = #58a6ff)
ctx.fillStyle = '#e94560';
ctx.fillRect(150, 40, 100, 50);
ctx.save(); // 保存当前状态(fillStyle = #e94560)
ctx.fillStyle = '#3fb950';
ctx.fillRect(270, 40, 100, 50);
ctx.restore(); // 恢复到 fillStyle = #e94560
ctx.fillRect(390, 40, 100, 50);
ctx.restore(); // 恢复到 fillStyle = #58a6ff
ctx.fillRect(510, 40, 100, 50);
// === 2. 变换保存与恢复 ===
ctx.save();
ctx.translate(400, 200);
ctx.fillStyle = '#3fb950';
ctx.fillRect(-25, -25, 50, 50);
ctx.restore();
ctx.save();
ctx.translate(500, 200);
ctx.fillStyle = '#f8b500';
ctx.fillRect(-25, -25, 50, 50);
ctx.restore();
// === 3. 旋转的矩形 ===
function drawRotatedRect(cx, cy, w, h, angle, color) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.fillStyle = color;
ctx.fillRect(-w/2, -h/2, w, h);
ctx.restore();
}
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const cx = 375 + Math.cos(angle) * 80;
const cy = 380 + Math.sin(angle) * 80;
const color = `hsl(${i * 45}, 70%, 60%)`;
drawRotatedRect(cx, cy, 30, 30, angle, color);
}
</script>
</body>
</html>示例2:save/restore 实际应用 - 绘制时钟
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>save/restore 实际应用 - 时钟</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
color: #eee;
font-family: 'Segoe UI', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
border: 1px solid #333;
border-radius: 8px;
background: #16213e;
}
</style>
</head>
<body>
<h1>save/restore 实际应用 - 模拟时钟</h1>
<canvas id="myCanvas" width="400" height="400"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const cx = 200, cy = 200, r = 150;
function drawClock() {
ctx.clearRect(0, 0, 400, 400);
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// 表盘背景
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
bgGrad.addColorStop(0, '#1e293b');
bgGrad.addColorStop(1, '#0f172a');
ctx.fillStyle = bgGrad;
ctx.fill();
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 3;
ctx.stroke();
ctx.restore();
// 刻度
for (let i = 0; i < 60; i++) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(i * Math.PI / 30);
if (i % 5 === 0) {
// 小时刻度
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, -r + 15);
ctx.lineTo(0, -r + 30);
ctx.stroke();
} else {
// 分钟刻度
ctx.strokeStyle = '#4a5568';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, -r + 20);
ctx.lineTo(0, -r + 28);
ctx.stroke();
}
ctx.restore();
}
// 时针
const hourAngle = (hours + minutes / 60) * Math.PI / 6;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(hourAngle);
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0, 15);
ctx.lineTo(0, -r + 60);
ctx.stroke();
ctx.restore();
// 分针
const minAngle = (minutes + seconds / 60) * Math.PI / 30;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(minAngle);
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0, 15);
ctx.lineTo(0, -r + 40);
ctx.stroke();
ctx.restore();
// 秒针
const secAngle = (seconds) * Math.PI / 30;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(secAngle);
ctx.strokeStyle = '#e94560';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0, 25);
ctx.lineTo(0, -r + 25);
ctx.stroke();
ctx.restore();
// 中心圆点
ctx.beginPath();
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
ctx.fillStyle = '#e94560';
ctx.fill();
requestAnimationFrame(drawClock);
}
drawClock();
</script>
</body>
</html>五、浏览器兼容性
六、注意事项与最佳实践
1. save/restore 必须配对
代码示例
// 正确:配对使用
ctx.save();
ctx.translate(100, 100);
ctx.fillRect(0, 0, 50, 50);
ctx.restore();
// 错误:不配对
ctx.save();
ctx.save();
ctx.restore();
// 还剩一个 save 未 restore,可能导致状态混乱2. 不保存路径和位图
代码示例
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.save(); // 不保存当前路径
ctx.restore(); // 路径仍然是 moveTo(10,10) -> lineTo(100,10)
// 位图内容也不保存
ctx.fillRect(0, 0, 100, 100);
ctx.save(); // 不保存已绘制的内容
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore(); // 画布仍然是空的3. 嵌套层级不宜过深
代码示例
// 不推荐:过深嵌套
ctx.save();
ctx.save();
ctx.save();
ctx.save();
// 难以追踪状态
ctx.restore();
ctx.restore();
ctx.restore();
ctx.restore();
// 推荐:扁平化使用
ctx.save();
ctx.translate(100, 100);
drawObject1();
ctx.restore();
ctx.save();
ctx.translate(200, 200);
drawObject2();
ctx.restore();4. 每个独立变换使用 save/restore
代码示例
// 推荐:每个对象独立 save/restore
function drawScene() {
objects.forEach(obj => {
ctx.save();
ctx.translate(obj.x, obj.y);
ctx.rotate(obj.rotation);
ctx.scale(obj.scale, obj.scale);
drawObject(obj);
ctx.restore();
});
}5. 初始状态保存
代码示例
// 在绘制开始前保存初始状态
ctx.save();
// ... 复杂绘制 ...
ctx.restore(); // 确保恢复到初始状态七、代码规范示例
代码示例
// 推荐:使用 save/restore 包裹每个独立绘制单元
function drawRotatedText(ctx, text, x, y, angle, options) {
const { font = '14px sans-serif', fill = '#000', align = 'center' } = options || {};
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.font = font;
ctx.fillStyle = fill;
ctx.textAlign = align;
ctx.textBaseline = 'middle';
ctx.fillText(text, 0, 0);
ctx.restore();
}
// 推荐:封装变换绘制
function withTransform(ctx, transformFn, drawFn) {
ctx.save();
transformFn(ctx);
drawFn(ctx);
ctx.restore();
}
// 使用示例
withTransform(ctx,
(ctx) => {
ctx.translate(200, 200);
ctx.rotate(Math.PI / 4);
ctx.scale(1.5, 1.5);
},
(ctx) => {
ctx.fillStyle = '#58a6ff';
ctx.fillRect(-25, -25, 50, 50);
}
);八、常见问题与解决方案
常见问题
变换累积导致位置偏移怎么办?
原因是没有使用 save/restore 重置变换。解决方案是每个独立变换使用 ctx.save() 和 ctx.restore() 包裹,确保变换不会累积影响后续绘制。
restore 后样式不对是什么原因?
原因是 save/restore 不配对,或嵌套层级错误。解决方案是确保每个 save 都有对应的 restore,可以通过计数器等调试手段检查配对情况。
如何重置所有 Canvas 状态?
方案1:使用 save/restore 包裹绘制操作。方案2:通过 canvas.width = canvas.width 清空画布并重置所有状态(最彻底)。方案3:手动重置各属性如 setTransform、globalAlpha、fillStyle 等。
save/restore 会影响性能吗?
save/restore 操作非常轻量,性能影响可以忽略。但应避免不必要的深层嵌套,推荐使用扁平化的 save/restore 方式管理状态。
九、总结
save/restore 是 Canvas 状态管理的核心机制,关键要点:
-
save:将当前绘图状态压入状态栈
-
restore:从状态栈弹出并恢复状态
-
保存内容:变换矩阵、裁剪区域、样式属性等
-
不保存内容:当前路径、位图内容
-
必须配对:每个 save 对应一个 restore
-
变换隔离:每个独立变换使用 save/restore 包裹
-
嵌套使用:支持多层嵌套,但不宜过深
下一教程将介绍 SVG 的基本概念。
本文涉及AI创作
内容由AI创作,请仔细甄别