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(); // 把栈顶的快照弹出来,恢复到当时的属性

保存哪些状态

类别 属性
变换 translate、rotate、scale 等变换矩阵
裁剪 clip() 设置的裁剪区域
线条样式 lineWidth、lineCap、lineJoin、miterLimit
填充/描边 fillStyle、strokeStyle
合成 globalAlpha、globalCompositeOperation
阴影 shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY
文本 font、textAlign、textBaseline、direction
滤镜 filter
图像平滑 imageSmoothingEnabled

不保存什么

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

示例

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>
相关推荐
啾咪orJIUMI33 分钟前
Vue 3 + 高德地图封装实战:点位、区域、Loca 流光与 Vue 信息窗体
前端·vue.js
掘金者阿豪36 分钟前
AI数据库不只是向量检索:从数据语义到库内智能计算的技术演进
前端
梦里寻码39 分钟前
我用 NestJS + Vue3 写了一套完整的研发管理系统(58 表 / 229 API / 130 项断言,全开源
前端·后端·算法
FRing0341 分钟前
vue2干货总结8(路由模块封装、router-link、声明式导航、路由重定向、编程式导航)
开发语言·前端·vue.js·vue2
SameX1 小时前
HarmonyOS ArkUI Dialog 复用 用 @CustomDialog + enum type 做多类型统一管理的做法
前端
AiMan1 小时前
豆包爱学还是不可以导出左侧所有对话,只能单容导出,方法不对吗
前端
AiMan1 小时前
Grok生成的内容能否下载吗?
前端
徐小夕2 小时前
JitArchives,一款基于JitWord协同AI文档打造的企业级电子档案系统
前端·vue.js·github
陈随易2 小时前
Node.js 2026上半年都有哪些性能提升?
前端·后端·程序员
墨林陌2 小时前
AI 热点日报(2026-11-09):苹果代理式Siri曝光,Anthropic推进IPO,编码智能体实测翻车
前端