速通Canvas指北🦮——变形、渐变与阴影篇

引言

本文缘起自笔者开发一个基于 PIXI.js 的在线动画编辑器时,想系统学习 Canvas 相关知识,却发现缺少合适的中文入门资料,于是萌生了撰写这份"速通指北"的想法,欢迎感兴趣的朋友订阅我的 《Canvas 指北》专栏。

第10章:状态

在上一章节最后的示例中,你已经见到 save()restore() 的身影------在裁剪图片时,我们用它们来隔离裁剪区域,避免影响后续绘制。这一对方法正是 Canvas 状态管理 的核心。本章将系统讲解什么是 Canvas 状态,以及如何通过 saverestore 轻松管理绘图上下文的"快照"。

10.1 什么是 Canvas 状态

Canvas 的绘图上下文(CanvasRenderingContext2D)包含了一系列的属性和当前设置,例如:

  • 样式类:fillStylestrokeStylelineWidthlineCaplineJoin

  • 变换类:由 translaterotatescaletransform 等累积的当前变换矩阵

  • 裁剪区域:由 clip() 设置的当前裁剪路径

  • 其他:fonttextAligntextBaselineshadow*globalAlphaglobalCompositeOperation

所有这些设置共同构成了 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 的变形不是修改已经绘制好的图形,而是修改绘图上下文的坐标系 。你可以把画布想象成一张无限大的透明纸,变形操作就是在移动、旋转或缩放这张纸本身。一旦变形被应用,之后所有的绘图命令(如 fillRectfillTextdrawImage 等)都会在新的坐标系下执行。

变形是累积 的:多次变形会叠加效果,例如先平移再旋转,与先旋转再平移的结果完全不同。正因如此,我们通常需要结合 saverestore 来隔离变形,避免影响后续绘制(详见第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) 效果,可以设置 bc 参数。

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 颜色字符串。最后将渐变对象赋值给 fillStylestrokeStyle

基本示例:水平渐变

从画布左侧到右侧的红蓝渐变:

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 阴影:shadowBlurshadowColorshadowOffsetX/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 控制整个图形的透明度,轻松实现淡入淡出与半透明叠加。

掌握这些,你将能直接在浏览器中实现图像处理、创意合成与高级视觉效果。

相关推荐
Neptune12 小时前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript
小星哥哥2 小时前
JavaScript 动态导入 (Dynamic Imports)
javascript
阿懂在掘金2 小时前
Vue 表单避坑(一):为什么 v-model 绑定对象属性会偷偷修改父组件数据?
前端·vue.js
Baihai_IDP2 小时前
在 Anthropic 的这两年,我学会了 13 件事
人工智能·程序员·llm
小码哥_常2 小时前
Android与JS交互:解锁混合开发的魔法之门
前端
leafyyuki2 小时前
如何优雅地上传大文件?分片上传实战指南
前端·音视频开发
Mintopia2 小时前
现代 Vue 3 页面组件文件安排与通信实践
前端
只会cv的前端攻城狮2 小时前
兼容性地狱-Uniapp钉钉小程序环境隔离踩坑实录
前端·uni-app
流水白开2 小时前
前端设计模式
javascript·面试