beginPath-vs-save详解

Canvas 中的 beginPath() 与 save()/restore() 详解

一句话总结

beginPath() save()
作用对象 当前路径(草稿本) 绘图状态(属性)
类比 橡皮擦擦掉草稿 拍照记录当前设置
互补操作 无(路径只能被清空或叠加绘制) restore()
相互影响 ❌ 无 ❌ 无

它们是完全独立的两个机制,互不干扰。


一、beginPath() --- 只管「路径」

作用

清空当前路径,重新开始绘制新的路径。

Canvas 内部维护了一个**当前路径(current path)**的概念,它就像一个「草稿本」:

js 复制代码
ctx.moveTo(0, 0);        // 草稿本上写下:从 (0,0) 开始
ctx.lineTo(100, 0);      // 草稿本上写下:画到 (100,0)
ctx.lineTo(100, 100);    // 草稿本上写下:再画到 (100,100)

ctx.beginPath();         // 清空草稿本!前面的都清掉了

ctx.arc(50, 50, 30, 0, Math.PI * 2); // 新的路径
ctx.stroke();            // 只画了 arc,之前的 lineTo 都消失了

构成路径的命令

命令 说明
moveTo(x, y) 设置路径起点
lineTo(x, y) 从当前点画直线到目标点
arc(x, y, r, startAngle, endAngle) 画圆弧
rect(x, y, w, h) 添加矩形路径
quadraticCurveTo() 二次贝塞尔曲线
bezierCurveTo() 三次贝塞尔曲线
closePath() 闭合路径(连回起点)

关键点

  • 不调用 beginPath(),新路径会叠加到旧路径上
  • beginPath() 只影响路径,不影响任何样式属性

不调用 beginPath 的后果

js 复制代码
ctx.strokeStyle = 'red';
ctx.moveTo(0, 0);
ctx.lineTo(100, 100);
ctx.stroke(); // 画了一条红线

ctx.strokeStyle = 'blue';
ctx.moveTo(0, 100);
ctx.lineTo(100, 0);
ctx.stroke(); // 两条线都变蓝了!因为旧路径还在,被一起重新描边了

第二次 stroke() 时,第一条线的路径依然存在,所以两条线都被用当前颜色(蓝色)重新描了一遍。


二、save() / restore() --- 只管「状态」

作用

把当前 Canvas 的绘图状态压入 / 弹出状态栈。

js 复制代码
ctx.save();    // 把当前所有属性拍一张快照,压入栈顶
ctx.restore(); // 把栈顶的快照弹出来,恢复到当时的属性

保存哪些状态

类别 属性
变换 translaterotatescale 等变换矩阵
裁剪 clip() 设置的裁剪区域
线条样式 lineWidthlineCaplineJoinmiterLimit
填充/描边 fillStylestrokeStyle
合成 globalAlphaglobalCompositeOperation
阴影 shadowBlurshadowColorshadowOffsetXshadowOffsetY
文本 fonttextAligntextBaselinedirection
滤镜 filter
图像平滑 imageSmoothingEnabled

不保存什么

  • 当前路径moveTolineTo 等积累的路径)
  • 已绘制的内容(画布上的像素)

示例

js 复制代码
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.save(); // 保存状态:红色,宽度2

ctx.strokeStyle = 'blue';
ctx.lineWidth = 10;
ctx.strokeRect(10, 10, 50, 50); // 蓝色粗框

ctx.restore(); // 恢复状态:红色,宽度2
ctx.strokeRect(80, 10, 50, 50); // 红色细框

嵌套使用

save() 可以嵌套调用,restore() 按后进先出(LIFO)顺序弹出:

js 复制代码
ctx.save();   // 栈: [状态A]
ctx.save(); // 栈: [状态A, 状态B]
ctx.restore();// 栈: [状态A] ← 回到状态B
ctx.restore();// 栈: []     ← 回到状态A

三、两者关系图解

css 复制代码
┌─────────────────────┐      ┌─────────────────────────┐
│   Canvas 状态        │      │  Canvas 路径(草稿本)     │
├─────────────────────┤      ├─────────────────────────┤
│ fillStyle: 'red'    │      │ moveTo(0, 0)            │
│ strokeStyle: 'blue' │      │ lineTo(100, 100)        │
│ lineWidth: 5        │      │ arc(50, 50, 30, ...)    │
│ transform: [...]    │      │                         │
│                     │      │                         │
│ ← save() 保存这些    │      │ ← beginPath() 清空这些    │
└─────────────────────┘      └─────────────────────────┘

save() 只保存左边 ❌ 不保存右边

beginPath() 只清空右边,不影响左边任何属性

案例:

ini 复制代码
ctx.save();           // ① 保存原始状态
ctx.translate(100, 100); // 移动坐标系
ctx.rotate(Math.PI / 4);
ctx.strokeStyle = 'green';
ctx.lineWidth = 3;
ctx.rect(-25, -25, 50, 50);
ctx.stroke();
ctx.moveTo(0, 0);
ctx.lineTo(100, 100);

ctx.restore();        // ②恢复原始状态
// 坐标系、颜色、线宽都回到 save 之前
ctx.strokeRect(80, 10, 50, 50); // 黑色细框
// ctx.beginPath();  //  ③ 开始新路径(跟 save 无关,只是为了不叠加旧路径)
ctx.stroke(); 

四、两者配合使用的典型场景

刮刮乐 / 橡皮擦效果

js 复制代码
class Line {
    constructor(ctx) {
        this.ctx = ctx;
        this.drawing = false;
    }
    moveTo(x, y) {
        this.drawing = true;
        this.ctx.save();                         // ① 保存原始状态
        this.ctx.globalCompositeOperation = 'destination-out'; // 设置为擦除模式
        this.ctx.lineWidth = 30;
        this.ctx.beginPath();                    // ② 开始新路径
        this.ctx.moveTo(x, y);
    }
    lineTo(x, y) {
        if (!this.drawing) return;
        this.ctx.lineTo(x, y);
        this.ctx.stroke();
    }
    restore() {
        if (!this.drawing) return;
        this.ctx.restore();                      // ③ 恢复原始状态
        this.drawing = false;
    }
}

绘制旋转/变形的图形

js 复制代码
// 画一个旋转的绿色方块
ctx.save();                          // 保存原始状态
ctx.translate(100, 100);             // 移动坐标系到方块中心
ctx.rotate(Math.PI / 4);             // 旋转 45 度
ctx.strokeStyle = 'green';
ctx.lineWidth = 4;

ctx.beginPath();                     // 开始新路径(与 save 无关)
ctx.rect(-25, -25, 50, 50);          // 以中心为原点的矩形
ctx.stroke();

ctx.restore();                       // 回到原始状态

绘制多个不同样式的图形

js 复制代码
// 圆
ctx.save();
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(50, 50, 30, 0, Math.PI * 2);
ctx.fill();
ctx.restore();

// 矩形
ctx.save();
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.rect(100, 20, 60, 60);
ctx.fill();
ctx.restore();

// 三角形
ctx.save();
ctx.fillStyle = 'green';
ctx.beginPath();
ctx.moveTo(200, 20);
ctx.lineTo(260, 80);
ctx.lineTo(140, 80);
ctx.closePath();
ctx.fill();
ctx.restore();

五、常见误区

误区 1:以为 save 会保存路径

js 复制代码
ctx.moveTo(30, 50);
ctx.lineTo(80, 50);
ctx.save();           // 路径不被保存!

ctx.beginPath();      // 清空路径
ctx.moveTo(120, 50);
ctx.lineTo(170, 50);

ctx.restore();        // 路径不会回来!
ctx.stroke();         // 只有右边那条线段被画出

正确做法 :如果需要在 save 前保留路径,必须先 stroke()fill() 把它画出来。

误区 2:以为 beginPath 会恢复样式

js 复制代码
ctx.strokeStyle = 'red';
ctx.lineWidth = 10;
ctx.beginPath();
ctx.rect(0, 0, 100, 100);

ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
ctx.beginPath();     // beginPath 只清空路径,不恢复属性!
ctx.rect(120, 0, 100, 100);

ctx.strokeStyle;     // 还是 'blue',不会变回 'red'
ctx.lineWidth;       // 还是 2,不会变回 10

正确做法 :需要恢复属性时,用 save() / restore()

误区 3:忘了 beginPath 导致路径叠加

js 复制代码
for (let i = 0; i < 5; i++) {
    // 没写 beginPath!每次 stroke 都会把所有历史路径重描一遍
    ctx.arc(50 + i * 30, 50, 10, 0, Math.PI * 2);
    ctx.stroke();
}

正确做法 :循环或多次绘制时,每次画新图形前调用 beginPath()


六、速查表

需求 用什么
清空之前的路径,画新图形 beginPath()
临时改变颜色/线宽,之后恢复 save() → 改属性 → restore()
临时旋转/平移/缩放,之后恢复 save() → 变换 → restore()
临时裁剪区域,之后恢复 save()clip()restore()
保存当前绘制的路径 ❌ 做不到,路径只能画出来
撤销画布上已经画好的内容 ❌ 做不到,需要自己维护状态或重绘

案例

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
            margin: 0;
            padding: 0;
        }
        canvas{
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas"></canvas>
    <script>
        const canvas = document.getElementById('myCanvas');
        canvas.width=385;
        canvas.height=188;
        const ctx = canvas.getContext('2d');
        // 1.beginPath() 作用
        // ctx.strokeStyle = 'red';
        // ctx.moveTo(0, 0);
        // ctx.lineTo(100, 100);
        // ctx.stroke(); // 画了一条红线

        // ctx.strokeStyle = 'blue';
        // ctx.moveTo(0, 100);
        // ctx.lineTo(100, 0);
        // ctx.stroke(); //// 两条线都变蓝了!因为旧路径还在,被一起重新描边了
        // 不 beginPath() → 路径会累积,每次 stroke/fill 都会画全部旧路径

        ctx.save();           // ① 保存原始状态
        ctx.translate(100, 100); // 移动坐标系
        ctx.rotate(Math.PI / 4);
        ctx.strokeStyle = 'green';
        ctx.lineWidth = 3;
        ctx.rect(-25, -25, 50, 50);
        ctx.stroke();
        ctx.moveTo(0, 0);
        ctx.lineTo(100, 100);

        ctx.restore();        // ②恢复原始状态
        // 坐标系、颜色、线宽都回到 save 之前
        ctx.strokeRect(80, 10, 50, 50); // 黑色细框
        // ctx.beginPath();  //  ③ 开始新路径(跟 save 无关,只是为了不叠加旧路径)
        ctx.stroke(); 
      
    </script>
</body>
</html>
相关推荐
IT_陈寒11 小时前
SpringBoot那个自动配置的坑,害我排查到凌晨三点
前端·人工智能·后端
Honor丶Onlyou11 小时前
VS Code 右键菜单修复记录
前端
PILIPALAPENG11 小时前
Python 语法速成指南:前端开发者视角(JS 类比版)
前端·人工智能·python
JYeontu11 小时前
轮播图不够惊艳?试下这个立体卡片轮播图
前端·javascript·css
张就是我10659211 小时前
从前端角度理解 CVE-2026-31431
前端
AGoodrMe11 小时前
swift基础之async/await
前端·ios
irving同学4623811 小时前
从零搭建生产级 RAG:Embedding、Chunking、Hybrid Search 与 Reranker
前端·后端
卡卡军11 小时前
vue3-sketch-ruler v3 升级详解:从 Vue 组件到跨框架标尺引擎
前端
还有多久拿退休金11 小时前
让看不见的 AI 动手画画——我意外造出了一个"绘图 Agent"
前端
陆枫Larry11 小时前
一次 iOS 橡皮筋弹性滚动的排查:从 absolute 到 fixed
前端