引言
本文缘起自笔者开发一个基于 PIXI.js 的在线动画编辑器时,想系统学习 Canvas 相关知识,却发现缺少合适的中文入门资料,于是萌生了撰写这份“速通指北”的想法,欢迎感兴趣的朋友订阅我的 《Canvas 指北》专栏。
第7章:曲线
在进入具体的曲线绘制方法之前,我们先理清一个关键概念:弧线(Arc) 和 曲线(Curve) 在 Canvas 中并不是同一回事。
-
弧线(上一章的主角)是圆的一部分。它由圆心、半径、起始角和结束角定义,具有固定的曲率(半径恒定)。你可以把它想象成用圆规画出来的一段圆弧——它的弯曲程度是均匀的。
-
曲线(本章的主角,即贝塞尔曲线)则不局限于圆。它通过起点、终点和一个或多个控制点来“拉伸”形状,可以产生从抛物线到 S 形、波浪形等千变万化的路径。它的曲率是连续变化的,没有固定的半径。
在 Canvas 中,弧线使用 arc 和 arcTo 绘制,而曲线则使用 quadraticCurveTo(二次贝塞尔)和 bezierCurveTo(三次贝塞尔)。理解了这一点,我们就能更准确地选择工具来绘制想要的图形。
7.1 二次贝塞尔曲线:quadraticCurveTo
二次贝塞尔曲线由一个控制点定义,其数学本质是一段抛物线弧。虽然抛物线通常有对称轴,但在画布上,随着控制点位置的变化,抛物线弧可以朝向任意方向,看起来可能并不对称。
quadraticCurveTo(cpx, cpy, x, y)
-
(cpx, cpy):控制点坐标 -
(x, y):终点坐标 -
起点 需通过
moveTo事先指定。
下面的代码在同一个画布上绘制三条二次贝塞尔曲线,起点均为 (30,100),终点均为 (270,100),但控制点的高度不同。你可以清楚地看到控制点越靠上,曲线被“拉”得越高。
<canvas id="quadraticDemo" width="320" height="200" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('quadraticDemo');
const ctx = canvas.getContext('2d');
// 第一条:控制点在 (150, 30) —— 向上拉
ctx.beginPath();
ctx.moveTo(30, 100);
ctx.quadraticCurveTo(150, 30, 270, 100);
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.stroke();
// 第二条:控制点在 (150, 100) —— 与起点终点同高,变成直线
ctx.beginPath();
ctx.moveTo(30, 100);
ctx.quadraticCurveTo(150, 100, 270, 100);
ctx.strokeStyle = 'green';
ctx.stroke();
// 第三条:控制点在 (150, 170) —— 向下拉
ctx.beginPath();
ctx.moveTo(30, 100);
ctx.quadraticCurveTo(150, 170, 270, 100);
ctx.strokeStyle = 'blue';
ctx.stroke();
</script>
效果:红线上拱,绿线平直,蓝线下凹。控制点的 y 坐标直接决定了曲线的弯曲方向。
7.2 三次贝塞尔曲线:bezierCurveTo
三次贝塞尔曲线有两个控制点,能塑造出 S 形、波浪等更复杂的形状。
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
-
(cp1x, cp1y):第一个控制点(影响曲线起始段的走向) -
(cp2x, cp2y):第二个控制点(影响曲线结束段的走向) -
(x, y):终点坐标 -
起点 同样由
moveTo指定。
以下代码从 (50,100) 到 (250,100) 绘制一条三次贝塞尔曲线。第一个控制点 (100,20) 将曲线拉向上方,第二个控制点 (200,180) 将曲线拉向下方,形成优美的 S 形。
<canvas id="bezierDemo" width="320" height="200" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('bezierDemo');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(50, 100);
ctx.bezierCurveTo(100, 20, 200, 180, 250, 100);
ctx.strokeStyle = 'purple';
ctx.lineWidth = 3;
ctx.stroke();
</script>
理论上通过贝塞尔曲线,你可以绘制出任何图形。如果你对控制点如何影响曲线仍感到抽象,强烈推荐 cubic-bezier.com/ 这个网站。
第8章:文本
Canvas 不仅可以绘制图形,还能直接渲染文本。本章将介绍如何控制文本的字体、位置、对齐方式,以及如何测量文本尺寸,为你在画布上添加文字标签、设计动态文本效果打下基础。
8.1 文本绘制基础
Canvas 提供了两种绘制文本的方法:
-
fillText(text, x, y, maxWidth):在指定位置绘制实心字符。 -
strokeText(text, x, y, maxWidth):在指定位置绘制空心(仅描边)字符。
x 和 y 是文本绘制的起始坐标(具体位置受 textAlign 和 textBaseline 影响,后文会讲)。可选的 maxWidth 参数用于限制文本的最大宽度。
<canvas id="textBasic" width="400" height="150" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('textBasic');
const ctx = canvas.getContext('2d');
// 填充文本
ctx.font = '20px Arial';
ctx.fillStyle = 'blue';
ctx.fillText('实心文本', 50, 50);
// 描边文本
ctx.font = '20px Arial';
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;
ctx.strokeText('空心文本', 50, 100);
</script>
这里 font 属性与 CSS 的 font 简写语法一致,用于设置文本的字体大小、字体系列等。默认值为 "10px sans-serif"。
8.2 文本布局
8.2.1 水平对齐:textAlign
textAlign 属性决定文本在水平方向上相对于绘图点的对齐方式。可选值:
-
start(默认):在从左到右的语言中左对齐,从右到左的语言中右对齐。 -
end:与start相反。 -
left:总是左对齐。 -
right:总是右对齐。 -
center:文本中心与绘图点对齐。
为了直观理解,我们可以绘制一个参考点,然后分别用不同对齐方式绘制文本,观察它们相对于该点的位置。
<canvas id="textAlignDemo" width="400" height="200" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('textAlignDemo');
const ctx = canvas.getContext('2d');
// 绘制参考竖线(x = 200)
ctx.beginPath();
ctx.strokeStyle = 'gray';
ctx.setLineDash([5, 5]);
ctx.moveTo(200, 0);
ctx.lineTo(200, 200);
ctx.stroke();
ctx.setLineDash([]); // 恢复实线
ctx.font = '16px Arial';
ctx.fillStyle = 'black';
ctx.textBaseline = 'middle'; // 让文本垂直居中,便于观察水平对齐
// 在 x=200 处用不同 textAlign 绘制文本
ctx.textAlign = 'start';
ctx.fillText('start', 200, 30);
ctx.textAlign = 'center';
ctx.fillText('center', 200, 70);
ctx.textAlign = 'end';
ctx.fillText('end', 200, 110);
ctx.textAlign = 'left';
ctx.fillText('left', 200, 150);
ctx.textAlign = 'right';
ctx.fillText('right', 200, 190);
</script>
8.2.2 垂直对齐:textBaseline
textBaseline 属性控制文本在垂直方向上相对于绘图点的对齐方式。可选值:
-
top:文本的顶部(em 方格的上沿)对齐到 y 坐标。 -
hanging:悬挂基线(某些印度字体使用),通常比 top 稍低。 -
middle:文本的中间对齐到 y 坐标。 -
alphabetic(默认):字母基线(拉丁字母的底部,如 a、x 的下沿)。 -
ideographic:表意文字基线(汉字、日文字符的底部),通常比 alphabetic 稍低。 -
bottom:文本的底部(em 方格的下沿)对齐到 y 坐标。
同样,我们可以绘制一条参考横线,观察不同基线相对于该线的位置。
<canvas id="baselineDemo" width="500" height="250" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('baselineDemo');
const ctx = canvas.getContext('2d');
// 绘制参考横线(y = 120)
ctx.beginPath();
ctx.strokeStyle = 'gray';
ctx.setLineDash([5, 5]);
ctx.moveTo(0, 120);
ctx.lineTo(500, 120);
ctx.stroke();
ctx.setLineDash([]);
ctx.font = '20px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'left';
// 绘制不同 baseline 的文本,y 坐标统一为 120
ctx.textBaseline = 'top';
ctx.fillText('top', 30, 120);
ctx.textBaseline = 'hanging';
ctx.fillText('hanging', 120, 120);
ctx.textBaseline = 'middle';
ctx.fillText('middle', 240, 120);
ctx.textBaseline = 'alphabetic';
ctx.fillText('alphabetic', 350, 120);
ctx.textBaseline = 'ideographic';
ctx.fillText('ideographic', 30, 200);
ctx.textBaseline = 'bottom';
ctx.fillText('bottom', 180, 200);
</script>
8.2.3 文本方向:direction
direction 属性设置文本的方向,影响 start 和 end 的对齐行为。可选值:
-
ltr:从左到右 -
rtl:从右到左 -
inherit(默认):继承 canvas 或文档的设置
通常用于多语言混合场景。下面简单示例:
ctx.direction = 'rtl';
ctx.textAlign = 'start';
ctx.fillText('نص عربي', 200, 50); // 阿拉伯语文本,从右向左显示
8.3 限制文本宽度:maxWidth 参数
fillText 和 strokeText都支持可选的第四个参数 maxWidth。当文本的原始宽度超过 maxWidth 时,浏览器会水平压缩字体(而不是截断或换行)以适应指定宽度。
<canvas id="maxWidthDemo" width="400" height="150" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('maxWidthDemo');
const ctx = canvas.getContext('2d');
ctx.font = '24px Arial';
ctx.fillStyle = 'blue';
// 无限制
ctx.fillText('无限制文本', 20, 40);
// 限制宽度为 100px
ctx.fillText('压缩文本', 20, 90, 100);
</script>
8.4 文本度量:measureText()
measureText(text) 方法返回一个 TextMetrics 对象,包含指定文本在当前字体下的尺寸信息。最常用的属性是 width(文本的宽度,单位像素)。
<canvas id="measureDemo" width="400" height="150" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('measureDemo');
const ctx = canvas.getContext('2d');
ctx.font = '24px Arial';
const text = 'Hello Canvas';
const metrics = ctx.measureText(text);
// 在画布中央绘制文本
const x = (canvas.width - metrics.width) / 2;
const y = 80;
ctx.fillStyle = 'black';
ctx.fillText(text, x, y);
// 绘制一个矩形框表示文本宽度
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;
ctx.strokeRect(x, y - 24, metrics.width, 24); // 粗略框选,基线为 alphabetic
</script>
第9章:图片
Canvas 不仅能绘制图形和文字,还能直接操作图片。本章将介绍如何在画布上绘制图片、用图片填充形状,以及如何通过裁剪实现有趣的视觉效果。
在 Canvas 中绘制图片,首先需要有一个图片源。通常我们使用 JavaScript 的 Image 对象来加载图片。
<canvas id="canvas1" width="400" height="300"></canvas>
<script>
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://via.placeholder.com/150'; // 示例图片
img.onload = () => {
ctx.drawImage(img, 50, 50);
};
</script>
关键点: 一定要在图片的 load 事件之后调用 drawImage,否则画布上不会有任何内容。
最简单的 drawImage 用法是传入图片对象以及目标坐标 (x, y),图片会按照原始尺寸绘制。
9.2 drawImage 的三种形式
drawImage 方法有三种调用形式,可以满足不同的绘制需求。下面我们用一个完整的示例来对比展示这三种形式。
基础绘制:drawImage(img, x, y) —— 图片保持原始尺寸。
缩放绘制:drawImage(img, x, y, width, height) —— 图片缩放到指定尺寸。
裁剪+缩放绘制:drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) —— 从源图片中裁剪一块区域,再绘制到画布上并缩放。
示例:在同一个画布上,分别用三种方式绘制同一张图片:
<canvas id="compareCanvas" width="600" height="400" style="border:1px solid #ccc"></canvas>
<script>
const canvas = document.getElementById('compareCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://picsum.photos/200'; // 一张 200x150 的示例图
img.onload = () => {
// 1. 基础绘制(原始尺寸)
ctx.drawImage(img, 30, 30);
ctx.fillStyle = 'black';
ctx.font = '12px sans-serif';
ctx.fillText('基础绘制 (原始尺寸)', 30, 20);
// 2. 缩放绘制(宽120,高90)
ctx.drawImage(img, 250, 30, 120, 90);
ctx.fillText('缩放绘制 (120x90)', 250, 20);
// 3. 裁剪+缩放绘制:从原图 (50,30) 位置裁剪 100x80 区域,绘制到 (400,30) 处并缩放到 150x120
ctx.drawImage(img, 50, 30, 100, 80, 400, 30, 150, 120);
ctx.fillText('裁剪+缩放', 400, 20);
};
</script>
9.3 用图片填充形状:createPattern
createPattern 方法可以基于图片创建一个平铺图案(类似于 CSS 的 background-repeat),然后将这个图案赋值给 fillStyle,之后绘制的所有形状(矩形、圆形、任意路径)都会用该图片平铺填充。
const pattern = ctx.createPattern(img, repetition);
repetition取值:"repeat"(默认,双向平铺)、"repeat-x"(水平平铺)、"repeat-y"(垂直平铺)、"no-repeat"(不平铺)。
<canvas id="patternCanvas" width="400" height="200"></canvas>
<script>
const canvas = document.getElementById('patternCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://picsum.photos/30/30';
img.onload = () => {
const pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
// 填充矩形
ctx.fillRect(20, 20, 150, 100);
// 填充圆形
ctx.beginPath();
ctx.arc(280, 80, 50, 0, Math.PI * 2);
ctx.fill();
};
</script>
9.4 图片与文字结合:图案文字
同样的技巧也可以用在文字上:将图案赋值给 fillStyle,然后使用 fillText 绘制文字,文字内部就会显示图片平铺效果,形成纹理字。
<canvas id="textureTextCanvas" width="400" height="150"></canvas>
<script>
const canvas = document.getElementById('textureTextCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://picsum.photos/100';
img.onload = () => {
const pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.font = 'bold 60px Arial';
ctx.fillText('纹理', 50, 100);
};
</script>
9.5 图片与形状结合:裁剪(clip)入门
clip()方法是 Canvas 中一个非常实用的功能:它基于当前路径设置一个裁剪区域,之后所有绘制的内容只显示在该区域内部。超出部分会被隐藏。
使用方法:
先用 beginPath 和绘图命令(如矩形、圆形、任意路径)定义一个路径。
调用 clip(),将当前路径设置为裁剪区域。
执行绘制(图片、图形、文字等),它们只会出现在裁剪区域内。
通常配合 save() 和 restore() 使用,避免裁剪影响后续绘制。
示例:将图片裁剪成圆形头像
<canvas id="clipCanvas" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('clipCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://via.placeholder.com/200/ff9900/ffffff?text=头像';
img.onload = () => {
ctx.save(); // 保存当前状态(无裁剪)
// 绘制圆形路径
ctx.beginPath();
ctx.arc(100, 100, 80, 0, Math.PI * 2);
ctx.clip(); // 设置裁剪区域为圆形内部
// 绘制图片,图片只会在圆形区域内显示
ctx.drawImage(img, 20, 20, 160, 160);
ctx.restore(); // 恢复状态,移除裁剪区域
// 绘制一个边框参考
ctx.strokeStyle = 'gray';
ctx.beginPath();
ctx.arc(100, 100, 80, 0, Math.PI * 2);
ctx.stroke();
};
</script>
🚀下篇预告:高级操作与动画篇
在下一篇中,你将学到:
- 坐标变换(平移、旋转、缩放)的灵活运用,轻松实现复杂的图形变换;
- 像素级操作:读取、修改像素数据,打造自定义滤镜和图像特效;
- 绘图状态的管理与保存,高效切换样式;
- 动画循环的实现原理,结合性能优化技巧,打造流畅的交互式动画;
- 更多实用技巧,助你独立开发完整的动画和图像应用。
学完本篇,你将真正掌握 Canvas 的高级玩法,开启创意编程的大门。