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>
相关推荐
泽_浪里白条1 小时前
我在 Superset 6.x 做自定义图表 + Embedded SDK 集成的实战复盘(附踩坑清单)
前端·数据可视化
幽络源小助理2 小时前
小六壬排盘工具源码 自适应双端 纯原生HTML+JS
前端·javascript·html
Championship.23.243 小时前
Open Source Pipeline Skill深度解析:自动化开源贡献全流程
前端·javascript·html
Bigger3 小时前
🧠 前端岗位的"结构性调整":现象背后的冷思考
前端·程序员·ai编程
薯老板3 小时前
vue组件之间的通信
前端·vue.js
迪菲赫尔曼3 小时前
从 0 到 1 打造工业级推理控制台:UltraConsole(Ultralytics + FastAPI + React)开源啦!
前端·yolo·react.js·计算机视觉·开源·fastapi
万邦科技Lafite3 小时前
京东开放API接口:item_get返回参数指南
java·前端·javascript·api·电商开放平台
梦想CAD控件3 小时前
网页CAD协同设计平台-生产级在线实时协同CAD引擎
前端·javascript·架构
Highcharts.js3 小时前
React 开发实战:如何使用 useEffect 为 Highcharts 注入实时数据
前端·javascript·react.js·开发实战·实时数据·highcharts·轮询数据