引言
本文缘起自笔者开发一个基于 PIXI.js 的在线动画编辑器时,想系统学习 Canvas 相关知识,却发现缺少合适的中文入门资料,于是萌生了撰写这份"速通指北"的想法,欢迎感兴趣的朋友订阅我的 《Canvas 指北》专栏。
第10章:状态
在上一章节最后的示例中,你已经见到 save() 和 restore() 的身影------在裁剪图片时,我们用它们来隔离裁剪区域,避免影响后续绘制。这一对方法正是 Canvas 状态管理 的核心。本章将系统讲解什么是 Canvas 状态,以及如何通过 save 和 restore 轻松管理绘图上下文的"快照"。
10.1 什么是 Canvas 状态
Canvas 的绘图上下文(CanvasRenderingContext2D)包含了一系列的属性和当前设置,例如:
-
样式类:
fillStyle、strokeStyle、lineWidth、lineCap、lineJoin等 -
变换类:由
translate、rotate、scale、transform等累积的当前变换矩阵 -
裁剪区域:由
clip()设置的当前裁剪路径 -
其他:
font、textAlign、textBaseline、shadow*、globalAlpha、globalCompositeOperation等
所有这些设置共同构成了 Canvas 的当前状态。当你开始绘制一个新图形时,这些状态决定了图形的外观。
10.2 保存与恢复:save() 和 restore()
Canvas 提供了两个简单的方法来管理状态栈:
-
ctx.save():将当前状态(包括变换矩阵、样式、裁剪区域等)压入栈中,保存起来。 -
ctx.restore():从栈顶弹出一个状态,并将其恢复为当前上下文的状态。
你可以把状态栈想象成一叠便签:每调用一次 save(),就像在便签上写下当前所有设置,然后放到栈顶;每调用一次 restore(),就拿出最上面的便签,把上面的设置全部覆盖到当前上下文。
下面的代码绘制了两个矩形,中间更改了线条样式。由于没有使用状态管理,第三个矩形本想恢复成第一个矩形的样式,但由于忘记重置虚线设置,结果继承了第二个矩形的虚线,导致样式被污染。
html
<canvas id="canvas" width="580" height="160"></canvas>
<script>
const c = document.getElementById('canvas').getContext('2d');
// A
c.fillStyle = '#4ECDC4'; c.strokeStyle = '#2C3E50'; c.lineWidth = 2; c.setLineDash([]);
c.fillRect(30, 40, 140, 80); c.strokeRect(30, 40, 140, 80);
// B(改成粗虚线)
c.fillStyle = '#FF6B6B'; c.strokeStyle = '#C0392B'; c.lineWidth = 4; c.setLineDash([8, 4]);
c.fillRect(230, 40, 140, 80); c.strokeRect(230, 40, 140, 80);
// C(想恢复成 A,但漏了 setLineDash([]) -> 被污染)
c.fillStyle = '#4ECDC4'; c.strokeStyle = '#2C3E50'; c.lineWidth = 2;
c.fillRect(430, 40, 140, 80); c.strokeRect(430, 40, 140, 80);
</script>

下面我们用 save() 和 restore() 来分别设置两个矩形的样式,互不干扰。
html
<canvas id="canvas" width="580" height="160"></canvas>
<script>
const c = document.getElementById('canvas').getContext('2d');
// A
c.save();
c.fillStyle = '#4ECDC4'; c.strokeStyle = '#2C3E50'; c.lineWidth = 2; c.setLineDash([]);
c.fillRect(30, 40, 140, 80); c.strokeRect(30, 40, 140, 80);
c.restore();
// B(改成粗虚线)
c.save();
c.fillStyle = '#FF6B6B'; c.strokeStyle = '#C0392B'; c.lineWidth = 4; c.setLineDash([8, 4]);
c.fillRect(230, 40, 140, 80); c.strokeRect(230, 40, 140, 80);
c.restore();
// C(再次用 A,状态不受 B 影响)
c.save();
c.fillStyle = '#4ECDC4'; c.strokeStyle = '#2C3E50'; c.lineWidth = 2; c.setLineDash([]);
c.fillRect(430, 40, 140, 80); c.strokeRect(430, 40, 140, 80);
c.restore();
</script>

现在两个矩形的样式完全独立,线宽互不影响。这就是 save/restore 的威力:它们让你能够临时改变上下文设置,并在改变前后自动恢复原状,无需手动记录每一个属性。
10.3 状态栈的嵌套
save() 和 restore() 是可以嵌套的。每次 save() 压入一个状态,每次 restore() 弹出最近压入的状态。
html
<canvas id="canvas" width="580" height="160"></canvas>
<script>
const c = document.getElementById('canvas').getContext('2d');
// 状态A: red
c.fillStyle = 'red';
c.save();
// 状态B: green
c.fillStyle = 'green';
c.save();
// 当前: blue
c.fillStyle = 'blue';
c.fillRect(40, 40, 140, 80);
// restore -> 回到状态B (green)
c.restore();
c.fillRect(220, 40, 140, 80);
// restore -> 回到状态A (red)
c.restore();
c.fillRect(400, 40, 140, 80);
</script>

你可以利用嵌套来管理复杂的临时设置,例如先保存整体状态,再局部修改变换和样式,最后一次性恢复。
第11章:变形
11.1 变形基础
Canvas 的变形不是修改已经绘制好的图形,而是修改绘图上下文的坐标系 。你可以把画布想象成一张无限大的透明纸,变形操作就是在移动、旋转或缩放这张纸本身。一旦变形被应用,之后所有的绘图命令(如 fillRect、fillText、drawImage 等)都会在新的坐标系下执行。
变形是累积 的:多次变形会叠加效果,例如先平移再旋转,与先旋转再平移的结果完全不同。正因如此,我们通常需要结合 save 和 restore 来隔离变形,避免影响后续绘制(详见第10章)。
11.2 平移:translate(x, y)
translate(x, y) 将坐标原点向右移动 x 像素,向下移动 y 像素。也就是说,之后绘制的图形位置都会基于新的原点计算。
javascript
js
ctx.translate(dx, dy);
dx:水平偏移量(正数向右,负数向左)dy:垂直偏移量(正数向下,负数向上)
下面的代码在画布上绘制两个相同的矩形,但第二个矩形是在平移坐标系之后绘制的。
js
<canvas id="translateDemo" width="400" height="200"></canvas>
<script>
const canvas = document.getElementById('translateDemo');
const ctx = canvas.getContext('2d');
// 第一个矩形:正常绘制(原点在左上角)
ctx.fillStyle = 'red';
ctx.fillRect(30, 30, 80, 50);
// 平移坐标系:向右移动 100px,向下移动 50px
ctx.translate(100, 50);
// 第二个矩形:同样调用 (30,30) 位置,但实际绘制在 (130,80)
ctx.fillStyle = 'blue';
ctx.fillRect(30, 30, 80, 50);
</script>

11.3 旋转:rotate(angle)
rotate(angle) 将坐标系绕当前原点旋转 angle 弧度(顺时针方向)。旋转会影响之后所有图形的朝向。
javascript
js
ctx.rotate(angle); // angle 为弧度制
注意:旋转中心始终是当前坐标系的原点,默认是画布左上角。因此,要实现绕任意点旋转,通常需要先平移将原点移至旋转中心,旋转,然后再平移回去。
面的示例绘制两个矩形:一个直接旋转(绕左上角),另一个先平移再旋转(绕矩形中心)。
js
<canvas id="rotateDemo" width="500" height="250"></canvas>
<script>
const canvas = document.getElementById('rotateDemo');
const ctx = canvas.getContext('2d');
// --- 矩形 A:绕左上角旋转 ---
ctx.save();
ctx.translate(100, 100); // 将原点移到矩形左上角
ctx.rotate(Math.PI / 6); // 旋转 30°
ctx.fillStyle = 'rgba(255,0,0,0.5)';
ctx.fillRect(0, 0, 80, 50); // 相对于新的原点绘制
ctx.restore();
// --- 矩形 B:绕中心旋转 ---
ctx.save();
const rectX = 250, rectY = 100, w = 80, h = 50;
ctx.translate(rectX + w/2, rectY + h/2); // 将原点移到矩形中心
ctx.rotate(-Math.PI / 4); // 旋转 -45°
ctx.fillStyle = 'rgba(0,0,255,0.5)';
ctx.fillRect(-w/2, -h/2, w, h); // 从中心开始绘制
ctx.restore();
</script>

11.4 缩放:scale(x, y)
scale(x, y) 在水平方向缩放 x 倍,垂直方向缩放 y 倍。缩放不仅影响图形的大小,还会影响线宽 、坐标偏移等一切与尺寸相关的属性。
js
ctx.scale(sx, sy);
sx:水平缩放因子(1 表示不变,>1 放大,<1 缩小,负数可实现水平翻转)sy:垂直缩放因子(负数可实现垂直翻转)
js
<canvas id="scaleDemo" width="574" height="160"></canvas>
<script>
const ctx = document.getElementById("scaleDemo").getContext("2d");
const panels = [
{ x: 14, label: "原始图形", color: "#2563eb", sx: 1, sy: 1 },
{ x: 200, label: "scale(1.5, 0.7)", color: "#dc2626", sx: 1.5, sy: 0.7 },
{ x: 387, label: "scale(−1, 1) 水平镜像", color: "#059669", sx: -1, sy: 1 },
];
panels.forEach(({ x, label, color, sx, sy }) => {
ctx.strokeStyle = "#e5e7eb";
ctx.lineWidth = 1;
ctx.strokeRect(x, 14, 173, 133);
ctx.save();
ctx.fillStyle = "#6b7280";
ctx.font = "11px -apple-system, sans-serif";
ctx.textAlign = "center";
ctx.fillText(label, x + 87, 11);
ctx.restore();
ctx.save();
ctx.translate(sx < 0 ? x + 127 : x + 40, 50);
ctx.scale(sx, sy);
ctx.lineWidth = 4;
ctx.strokeStyle = color;
ctx.fillStyle = `${color}24`;
ctx.fillRect(0, 0, 80, 47);
ctx.strokeRect(0, 0, 80, 47);
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(13, 13, 4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
</script>

11.5 变换矩阵:transform() 与 setTransform()
Canvas 的变形本质上是操作一个 2×3 的变换矩阵(实际是 3×3 的齐次矩阵,但只用到前两行)。矩阵参数如下:
text
a c e
b d f
0 0 1
其中:
a:水平缩放b:垂直倾斜c:水平倾斜d:垂直缩放e:水平平移f:垂直平移
11.5.1 transform(a, b, c, d, e, f)
transform() 将当前矩阵与传入的矩阵相乘(叠加),从而实现任意组合的变形。例如,要实现斜切(skew) 效果,可以设置 b 或 c 参数。
11.5.2 setTransform(a, b, c, d, e, f)
setTransform() 会先重置当前矩阵为单位矩阵 ,然后再应用新矩阵。这意味着它会取消之前的所有变形,相当于把坐标系恢复到原始状态(原点在左上角,无旋转缩放),然后再进行指定的变换。
js
<canvas id="transformDemo" width="500" height="200"></canvas>
<script>
const canvas = document.getElementById('transformDemo');
const ctx = canvas.getContext('2d');
// 左侧:原始矩形(无变形)
ctx.fillStyle = 'orange';
ctx.fillRect(30, 30, 80, 50);
// 中间:使用 transform 添加斜切(b 参数控制垂直倾斜)
ctx.save();
ctx.transform(1, 0.5, 0, 1, 150, 30); // a=1, b=0.5, c=0, d=1, e=150, f=30
ctx.fillStyle = 'rgba(0,150,0,0.6)';
ctx.fillRect(0, 0, 80, 50);
ctx.restore();
// 右侧:先用 setTransform 重置,再应用斜切(注意坐标原点已重置)
ctx.setTransform(1, -0.3, 0.3, 1, 300, 50); // a=1, b=-0.3, c=0.3, d=1, e=300, f=50
ctx.fillStyle = 'rgba(0,0,255,0.6)';
ctx.fillRect(0, 0, 80, 50);
// 注意:这里没有用 restore,因为 setTransform 直接替换了矩阵
</script>

左侧为正常矩形,中间是在平移基础上叠加了垂直斜切(b=0.5),右侧则直接设置了一个同时有水平和垂直斜切的变换,且坐标原点被重置到了 (300,50)。
11.6 用 save/restore 隔离变形
由于变形是累积的,并且会影响后续所有绘图,强烈建议在每次需要临时变形时,使用 save() 和 restore() 包裹起来(正如本章所有示例所做的那样)。这可以确保变形不会意外地影响到其他图形。
第12章:渐变与阴影
渐变和阴影是 Canvas 中为图形增添立体感和视觉层次的重要工具。本章将介绍两种渐变类型(线性渐变、径向渐变)以及阴影属性的基本用法。
12.1 线性渐变:createLinearGradient()
线性渐变是指颜色沿一条直线平滑过渡。通过 createLinearGradient(x0, y0, x1, y1) 创建渐变对象,参数定义了渐变的起点 (x0, y0) 和终点 (x1, y1)。然后使用 addColorStop(offset, color) 添加色标,offset 是 0 到 1 之间的位置,color 是 CSS 颜色字符串。最后将渐变对象赋值给 fillStyle 或 strokeStyle。
基本示例:水平渐变
从画布左侧到右侧的红蓝渐变:
html
<canvas id="linear1" width="300" height="100"></canvas>
<script>
const canvas = document.getElementById('linear1');
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); // 水平从左到右
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>
效果:矩形从左(红)到右(蓝)平滑渐变。

多色渐变
可以在任意位置添加多个色标。
html
<canvas id="linear2" width="300" height="100"></canvas>
<script>
const canvas = document.getElementById('linear2');
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.5, 'yellow');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>
效果:红 → 黄 → 蓝三色渐变。

对角线渐变
起点和终点可以任意指定,实现任意方向的渐变。
html
<canvas id="linear3" width="300" height="150"></canvas>
<script>
const canvas = document.getElementById('linear3');
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(20, 20, 280, 130); // 从左上到右下
gradient.addColorStop(0, 'green');
gradient.addColorStop(0.7, 'orange');
gradient.addColorStop(1, 'purple');
ctx.fillStyle = gradient;
ctx.fillRect(20, 20, 260, 110);
</script>
效果:在矩形区域内沿对角线渐变。

透明渐变
色标可使用带透明度的颜色(如 rgba)。
html
<canvas id="linear4" width="300" height="100"></canvas>
<script>
const canvas = document.getElementById('linear4');
const ctx = canvas.getContext('2d');
// 先绘制一个背景图案便于观察透明效果
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, 'rgba(255,0,0,1)'); // 完全不透明红色
gradient.addColorStop(0.5, 'rgba(255,255,0,0.5)'); // 半透明黄色
gradient.addColorStop(1, 'rgba(0,0,255,0)'); // 完全透明蓝色
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>
效果:红色逐渐变为半透明黄,最后完全透明,露出下方的灰色背景。

12.2 径向渐变:createRadialGradient()
径向渐变是两个圆之间的渐变。语法为 createRadialGradient(x0, y0, r0, x1, y1, r1),其中 (x0, y0, r0) 是起始圆,(x1, y1, r1) 是结束圆。颜色从起始圆边缘向结束圆边缘过渡。
基本示例:同心圆渐变
两个圆圆心相同,半径不同,形成圆形渐变。
html
<canvas id="radial1" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('radial1');
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(100, 100, 20, 100, 100, 80);
gradient.addColorStop(0, 'white');
gradient.addColorStop(0.5, 'lightblue');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>
效果:中心亮白色,向外逐渐变为蓝色,类似光晕。

偏移圆心的渐变
两个圆圆心不同,可产生立体感。
html
<canvas id="radial2" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('radial2');
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(70, 70, 20, 100, 100, 80);
gradient.addColorStop(0, 'yellow');
gradient.addColorStop(1, 'green');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>
效果:亮区偏向 (70,70),暗区偏向 (100,100),模拟光照。

多色径向渐变
同样可以添加多个色标。
html
<canvas id="radial3" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('radial3');
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(100, 100, 10, 100, 100, 90);
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.3, 'orange');
gradient.addColorStop(0.6, 'yellow');
gradient.addColorStop(1, 'green');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>
效果:由内向外呈现红橙黄绿的彩色环。

12.3 阴影:shadowBlur、shadowColor、shadowOffsetX/Y
Canvas 的阴影效果通过以下四个属性控制,它们会影响之后所有绘制的图形(形状、文字、图片等)。
shadowColor:阴影颜色(默认black,支持透明色)shadowBlur:模糊程度(像素,默认 0,数值越大越模糊)shadowOffsetX:阴影水平偏移(正数向右,负数向左,默认 0)shadowOffsetY:阴影垂直偏移(正数向下,负数向上,默认 0)
示例:为矩形添加阴影
html
<canvas id="shadow1" width="300" height="150"></canvas>
<script>
const canvas = document.getElementById('shadow1');
const ctx = canvas.getContext('2d');
ctx.shadowColor = 'rgba(0,0,0,0.5)'; // 半透明黑色
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = 'blue';
ctx.fillRect(50, 30, 100, 80);
</script>
效果:蓝色矩形右下方出现模糊的半透明阴影。

示例:为文字添加阴影
html
<canvas id="shadow2" width="300" height="150"></canvas>
<script>
const canvas = document.getElementById('shadow2');
const ctx = canvas.getContext('2d');
ctx.shadowColor = 'red';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = -4;
ctx.shadowOffsetY = -4;
ctx.font = 'bold 40px Arial';
ctx.fillStyle = 'black';
ctx.fillText('阴影', 50, 100);
</script>
效果:黑色文字左上方出现红色阴影。

多个图形共用阴影
阴影设置是全局的,直到被改变。若只想部分图形有阴影,可用 save/restore 隔离。
html
<canvas id="shadow3" width="400" height="150" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('shadow3');
const ctx = canvas.getContext('2d');
// 第一个矩形无阴影(默认)
ctx.fillStyle = 'green';
ctx.fillRect(20, 20, 80, 50);
// 第二个矩形有阴影
ctx.save();
ctx.shadowColor = 'purple';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
ctx.fillStyle = 'orange';
ctx.fillRect(120, 20, 80, 50);
ctx.restore();
// 第三个矩形无阴影(已恢复)
ctx.fillStyle = 'blue';
ctx.fillRect(240, 20, 80, 50);
</script>
效果:中间矩形有紫色阴影,两侧无阴影。

阴影与渐变结合
阴影可与渐变一起使用,增强立体感。
html
<canvas id="shadowGradient" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('shadowGradient');
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(100, 100, 20, 100, 100, 80);
gradient.addColorStop(0, 'white');
gradient.addColorStop(1, 'blue');
ctx.shadowColor = 'black';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(100, 100, 70, 0, Math.PI * 2);
ctx.fill();
</script>
效果:带径向渐变的蓝色球体,右下方有模糊阴影,更显立体。

🚀 下篇预告:像素操作与合成篇
在下一篇中,你将学到:
- 像素级读取与写入 :通过
getImageData获取像素数据,用putImageData写回画布,为滤镜、颜色调整打下基础; - 自定义图像滤镜:遍历像素矩阵实现灰度、反色、模糊、边缘检测等特效;
- 合成与混合模式 :掌握
globalCompositeOperation的多种模式(如源叠加、遮罩、抠图),实现图层混合效果; - 全局透明度 :利用
globalAlpha控制整个图形的透明度,轻松实现淡入淡出与半透明叠加。
掌握这些,你将能直接在浏览器中实现图像处理、创意合成与高级视觉效果。