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

多媒体:保存与恢复 - 完整教程与代码示例

一、教程简介

Canvas 的 saverestore 方法是管理绘图状态的核心机制。save 将当前绘图状态压入状态栈,restore 从栈中弹出并恢复最近保存的状态。这对于实现复杂的变换、裁剪和样式切换至关重要,可以避免手动重置每个状态属性。本教程将详细讲解状态栈的工作原理、保存与恢复的使用方法和最佳实践。


二、核心概念

绘图状态

save/restore 管理的绘图状态包括:

状态类别 包含属性
变换 当前变换矩阵(translate、rotate、scale 等)
裁剪 当前裁剪区域
样式 strokeStyle、fillStyle、globalAlpha
线条 lineWidth、lineCap、lineJoin、miterLimit
阴影 shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor
文本 font、textAlign、textBaseline、direction
合成 globalCompositeOperation
虚线 setLineDash、lineDashOffset

注意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>

五、浏览器兼容性

方法 Chrome Firefox Safari Edge IE
save 1+ 1.5+ 2+ 12+ 9+
restore 1+ 1.5+ 2+ 12+ 9+

六、注意事项与最佳实践

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 的基本概念。

标签: Canvas状态管理 save方法 restore方法 状态栈 变换管理 绘图状态

本文涉及AI创作

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

list快速访问

上一篇: 多媒体:像素操作 - 完整教程与代码示例 下一篇: 多媒体:SVG简介 - 完整教程与代码示例

poll相关推荐