速通Canvas指北🦮——曲线、文本与图片篇

135 阅读11分钟

引言

本文缘起自笔者开发一个基于 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>

image.png

效果:红线上拱,绿线平直,蓝线下凹。控制点的 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>

image.png

理论上通过贝塞尔曲线,你可以绘制出任何图形。如果你对控制点如何影响曲线仍感到抽象,强烈推荐 cubic-bezier.com/ 这个网站。

第8章:文本

Canvas 不仅可以绘制图形,还能直接渲染文本。本章将介绍如何控制文本的字体、位置、对齐方式,以及如何测量文本尺寸,为你在画布上添加文字标签、设计动态文本效果打下基础。

8.1 文本绘制基础

Canvas 提供了两种绘制文本的方法:

  • fillText(text, x, y, maxWidth):在指定位置绘制实心字符。

  • strokeText(text, x, y, maxWidth):在指定位置绘制空心(仅描边)字符。

xy 是文本绘制的起始坐标(具体位置受 textAligntextBaseline 影响,后文会讲)。可选的 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>

image.png

这里 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>

1.webp

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>

2.webp

8.2.3 文本方向:direction

direction 属性设置文本的方向,影响 startend 的对齐行为。可选值:

  • ltr:从左到右

  • rtl:从右到左

  • inherit(默认):继承 canvas 或文档的设置

通常用于多语言混合场景。下面简单示例:

ctx.direction = 'rtl';
ctx.textAlign = 'start';
ctx.fillText('نص عربي', 200, 50); // 阿拉伯语文本,从右向左显示

8.3 限制文本宽度:maxWidth 参数

fillTextstrokeText都支持可选的第四个参数 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>

image.png

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>

image.png

第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),图片会按照原始尺寸绘制。

image.png

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>

3.webp

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>

4.webp

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>

image.png

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>

5.webp

🚀下篇预告:高级操作与动画篇

在下一篇中,你将学到:

  • 坐标变换(平移、旋转、缩放)的灵活运用,轻松实现复杂的图形变换;
  • 像素级操作:读取、修改像素数据,打造自定义滤镜和图像特效;
  • 绘图状态的管理与保存,高效切换样式;
  • 动画循环的实现原理,结合性能优化技巧,打造流畅的交互式动画;
  • 更多实用技巧,助你独立开发完整的动画和图像应用。

学完本篇,你将真正掌握 Canvas 的高级玩法,开启创意编程的大门。