Canvas 快速入门小结

296 阅读24分钟

MDN Canvas 文档

应用场景

应用场景典型用途示例技术特征与优势替代/对比技术
🎮 游戏开发小游戏、2D 引擎、动画角色高性能像素级绘制、支持复杂物理与碰撞检测WebGL、PixiJS
📊 数据可视化图表、仪表盘、实时数据图支持高频率刷新、大数据量图表比 SVG 性能更优SVG、D3.js
🖌 图形编辑器/白板涂鸦、签名、协同绘图支持路径绘制、事件响应、撤销重做、状态同步SVG、Fabric.js
📹 视频处理/弹幕视频滤镜、美颜、弹幕、水印可从 <video> 读取帧数据并像素处理,结合 captureStream 合成输出WebGL、FFmpeg WASM
🧩 图像处理/导出压缩、裁剪、滤镜、分享图生成像素级处理,toDataURL() 导出图片OffscreenCanvas
🌠 UI 动效/视觉装饰背景特效、粒子轨迹、光影模拟结合 requestAnimationFrame() 实现高帧率视觉表现CSS 动画、SVG
🎯 命中测试/物理模拟像素选区、热点检测、弹跳模拟getImageData() 精确像素检测无直接替代方案

基本框架--最小实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Canvas 入门</title>
  </head>
  <body>
    <canvas id="canvas"> 该浏览器不支持Canvas,请升级浏览器 </canvas>
  </body>
  <script>
    /** @type {HTMLCanvasElement} */
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    // 绘制直线
    ctx.moveTo(10, 10);
    ctx.lineTo(50, 50);
    ctx.stroke();
  </script>
</html>

image.png

注意点

  1. canvas 默认宽300,高150
  2. canvas 标签中内容为浏览器不支持canvas标签的时候显示
  3. 设置 canvas 属性 width、height 的同时使用 Css 设置 canvas 宽高会出现拉伸、模糊的情况,其实就是进行了缩放,所以尽量使用canvas 自带属性来设置宽高。如图: <canvas id="canvas" style="width: 300px; height: 300px"> 会变成下面情况,默认属性值和css值不一样导致拉伸

image.png

4. 使用 `/** @type {HTMLCanvasElement} */` 可以添加代码提示

绘制形状

直线

API讲解

  1. .moveTo(x,y): 将笔触移动到指定的坐标 x 以及 y 上
  2. .lineTo(x,y): 绘制一条从当前位置到指定 x 以及 y 位置的直线
  3. .stroke(): 通过线条来绘制图形轮廓

示例:

ctx.moveTo(10, 10); 
ctx.lineTo(50, 50); 
ctx.stroke();

image.png

三角形

绘制三角形就是在三条直线连接起来

API讲解

  1. .fill(): 通过填充路径的内容区域生成实心的图形

示例:

ctx.moveTo(10, 10);
ctx.lineTo(50, 50);
ctx.lineTo(10, 50);
ctx.lineTo(10, 10);
// 空心三角
ctx.stroke();
// 实心三角
ctx.fill();

image.png

image.png

矩形

矩形一样可以由4条直线连接,但是 Canvas 有直接绘制矩形的API

API讲解

  1. .strokeRect(x,y,width,height): 绘制矩形边框
  2. .fillRect(x,y,width,height): 绘制填充的矩形
  3. .clearRect(x,y,width,height): 清除指定矩形区域,让清除部分完全透明
  4. .rect(x,y,width,height): 创建一个矩形路径,然后选择使用 fill 或 stroke 填充或描边

示例:

ctx.strokeRect(210, 210, 50, 50);
ctx.fillRect(100, 100, 100, 100);
ctx.clearRect(110, 110, 40, 40);

image.png

弧、圆

API讲解:

  1. .beginPath(): 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径
  2. .closePath(): 闭合路径,使图形绘制命令又重新指向到上下文中
  3. .arc(x, y, radius, startAngle, endAngle, anticlockwise): 画一个以(x,y)为圆心的以 radius 为半径的圆弧(圆),从 startAngle 开始到 endAngle 结束,按照 anticlockwise 给定的方向(默认为顺时针)来生成。
  4. .arcTo(x1, y1, x2, y2, radius): 通过给定2个控制点(x1,y1) (x2,y2)和半径radius向当前子路径添加一个圆弧。如果需要,例如起始点和控制点在一条直线上,该圆弧会自动与路径的最后一个点用直线连接。
ctx.arc(200, 200, 50, 0, Math.PI); // 顺时针
ctx.stroke();
ctx.arc(300, 300, 50, 0, Math.PI / 3, false); // 逆时针
ctx.stroke();

image.png

出现了连笔的情况,这是因为在绘制圆和圆弧的时候,上下文的用了一笔来绘制,当绘制了圆之后连续绘制到点(350,300),然后开始绘制圆弧

解决1: 使用 .beginPath()、.closePath() 新建路径

ctx.arc(200, 200, 50, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(300, 300, 50, 0, Math.PI / 3, false);
ctx.stroke();
ctx.closePath();

image.png

解决2: 使用 .moveTo() 移动到新的起始点开始绘制,也是上面效果

ctx.arc(200, 200, 50, 0, Math.PI * 2);
ctx.stroke();
ctx.moveTo(350, 300);
ctx.arc(300, 300, 50, 0, Math.PI / 3, false);
ctx.stroke();

使用 .arcTo

// 切线段
ctx.beginPath();
ctx.strokeStyle = "gray";
ctx.moveTo(200, 20);
ctx.lineTo(200, 130);
ctx.lineTo(50, 20);
ctx.stroke();

// 圆弧
ctx.beginPath();
ctx.strokeStyle = "black";
ctx.lineWidth = 5;
ctx.moveTo(200, 20);
ctx.arcTo(200, 130, 50, 20, 40);
ctx.stroke();

// 起始点
ctx.beginPath();
ctx.fillStyle = "blue";
ctx.arc(200, 20, 5, 0, 2 * Math.PI);
ctx.fill();
// 控制点
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(200, 130, 5, 0, 2 * Math.PI); // 控制点一
ctx.arc(50, 20, 5, 0, 2 * Math.PI); // 控制点二
ctx.fill();

image.png

椭圆

API讲解:

  1. .ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise):

    x、y:椭圆的圆心位置

    radiusX、radiusY:x轴和y轴的半径

    rotation:椭圆的旋转角度,以弧度表示

    startAngle:开始绘制点

    endAngle:结束绘制点

    anticlockwise:绘制的方向(默认顺时针),可选参数。

示例:

// 圆心(100,100) x轴半径50 y轴半径75 默认顺时针旋转45°(Math.PI / 4)  从0°开始绘制360°
ctx.ellipse(100, 100, 50, 75, Math.PI / 4, 0, 2 * Math.PI);
ctx.stroke();
// 对称轴
ctx.moveTo(0, 200);
ctx.lineTo(200, 0);
ctx.stroke();

image.png

二次贝塞尔曲线

API讲解:

  1. .quadraticCurveTo(cpx, cpy, x, y): cpx和cpy为控制点坐标,x和y为结束点坐标

新增二次贝塞尔曲线路径需要 3 个点。分别是起始点、控制点、终点。起始点是当前路径最新的点,在创建二次贝赛尔曲线之前,可以使用 moveTo() 方法进行改变起始点坐标

示例:

// 二次贝塞尔曲线
ctx.moveTo(50, 20);
ctx.quadraticCurveTo(230, 30, 50, 100);
ctx.stroke();
// 控制点
ctx.beginPath();
ctx.arc(230, 30, 5, 0, 2 * Math.PI);
ctx.fill();
// 起始点和结束点
ctx.beginPath();
ctx.arc(50, 20, 5, 0, 2 * Math.PI); // 起始点
ctx.arc(50, 100, 5, 0, 2 * Math.PI); // 结束点
ctx.fill();

image.png

推荐在线工具: 二次贝塞尔曲线

三次贝塞尔曲线

API讲解:

  1. .bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y): cp1x和cp1y为第一个控制点坐标,cp2x和cp2y为第二个控制点坐标,x和y为结束点坐标

示例:

// 点的坐标
let start = { x: 50, y: 20 }; // 起始点
let cp1 = { x: 230, y: 30 }; // 控制点1
let cp2 = { x: 150, y: 80 }; // 控制点2
let end = { x: 250, y: 100 }; // 终点
// 三次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
ctx.stroke();
// 起点和终点
ctx.beginPath();
ctx.arc(start.x, start.y, 5, 0, 2 * Math.PI); // 起点
ctx.arc(end.x, end.y, 5, 0, 2 * Math.PI); // 终点
ctx.fill();
// 控制点
ctx.beginPath();
ctx.arc(cp1.x, cp1.y, 5, 0, 2 * Math.PI); // 控制点一
ctx.arc(cp2.x, cp2.y, 5, 0, 2 * Math.PI); // 控制点二
ctx.fill();

image.png

推荐在线工具:三次贝塞尔曲线

Path2D

允许 canvas 中根据需要创建可以保留并重用的路径

Path2D 对象方法:

  1. CanvasRenderingContext2D 接口对象中的相同方法:

.closePath() .moveTo() .lineTo() .bezierCurveTo() .quadraticCurveTo() .arc() .arcTo() .ellipse() .rect()

  1. 实例方法: .addPath(path, transform): 添加一条新路径到对当前路径。path: 添加的 Path2D 路径。transform(可选): DOMMatrix 作为新增路径的变换矩阵。
// 创建 Path2D 路径
const p1 = new Path2D();
p1.rect(0, 0, 100, 150);
const p2 = new Path2D();

// 填充 p1 路径
ctx.fill(p1);

// p1 加入到 p2,并平移
ctx.fillStyle = "red";
p2.addPath(p1);
ctx.translate(120, 0);
ctx.fill(p2);

image.png

绘制样式

strokeStyle--描边颜色等、lineWidth--线条宽度

  1. strokeStyle: 指定用于形状描边(轮廓)的颜色、渐变或图案。默认值是 #000(黑色)。

     color: CSS <color> 值的字符串
     gradient: CanvasGradient 对象(线性或径向渐变)
     pattern: CanvasPattern 对象(重复图像)
    
  2. lineWidth: 指定线条的宽度(以坐标空间单位表示)。值为正数,零、负数、Infinity 和 NaN 值将被忽略。默认值为 1.0

演示

ctx.strokeStyle = "blue"; // 设置颜色
ctx.lineWidth = 5; // 设置线条宽度
ctx.moveTo(20, 20);
ctx.lineTo(130, 130);
ctx.stroke();

image.png

lineCap--绘制线段的末端

lineCap: 指定如何绘制线段的末端。值为

    butt: 线条末端呈正方形。这是默认值
    round: 线条末端呈圆形的
    square: 线条末端呈方形,通过添加一个宽度与线条粗细相同且高度粗细的一半的盒子来形成。
    

演示

// 绘制辅助线
ctx.strokeStyle = "#09f";
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(140, 10);
ctx.moveTo(10, 140);
ctx.lineTo(140, 140);
ctx.stroke();
// 绘制线条
ctx.strokeStyle = "black";
["butt", "round", "square"].forEach((lineCap, i) => {
  ctx.lineWidth = 15;
  ctx.lineCap = lineCap;
  ctx.beginPath();
  ctx.moveTo(25 + i * 50, 10);
  ctx.lineTo(25 + i * 50, 140);
  ctx.stroke();
});

image.png

lineJoin--线段如何连接

lineJoin: 用于设置 2 个线段如何连接在一起,这个属性在两个连接的线段具有相同方向(如两线段组成一个线段)时没有效果,因为在这种情况下不会添加连接区域。长度为零的退化线段(即所有端点和控制点处于完全相同的位置)也会被忽略。默认值:miter

    round: 通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。圆角的半径是线段的宽度
    bevel: 在相连部分的末端填充一个额外的以三角形为底的区域,每个部分都有各自独立的矩形拐角
    miter: 通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。这个设置受到 miterLimit 属性的影响。

演示

ctx.lineWidth = 10;
["round", "bevel", "miter"].forEach((join, i) => {
  ctx.lineJoin = join;
  ctx.beginPath();
  ctx.moveTo(-5, 5 + i * 40);
  ctx.lineTo(35, 45 + i * 40);
  ctx.lineTo(75, 5 + i * 40);
  ctx.lineTo(115, 45 + i * 40);
  ctx.lineTo(155, 5 + i * 40);
  ctx.stroke();
});

image.png

miterLimit--设置斜接限制比例

miterLimit: 用于设置斜接限制比例,于坐标空间单位中。零、负数、Infinity 和 NaN 值将被忽略。默认值为 10.0,所谓斜接长度指线条交接处内角顶点到外角顶点的长度,线段之间夹角比较大时,交点不会太远,但随着夹角变小,交点距离会呈指数级增大;如果交点距离大于miterLimit值,连接效果会变成了 lineJoin = bevel 的效果

演示

ctx.lineWidth = 7;
ctx.miterLimit = 8;
// 绘制线条
ctx.beginPath();
ctx.moveTo(0, 100);
for (let i = 0; i < 24; i++) {
  // 每段折线在 Y 方向上的偏移:偶数索引向下偏移 25,奇数索引向上偏移 25,形成波浪形折线
  const dy = i % 2 === 0 ? 25 : -25;
  // 计算当前点的 X 坐标,使用幂函数 Math.pow(i, 1.5) * 2 使 X 间距逐渐变大
  ctx.lineTo(Math.pow(i, 1.5) * 2, 75 + dy);
}
ctx.stroke();

image.png

setLineDash--虚线样式

  1. .setLineDash(segments): 用于在描线时使用虚线模式。使用正数数据来指定描述模式的线和间隙的交替长度。如果数组元素的数量是奇数,数组的元素会被复制和拼接。例如,[5, 15, 25] 会变成 [5, 15, 25, 5, 15, 25]。如果数组为空,则虚线列表会被清空,线条描边将恢复为实线

  2. .getLineDash(): 返回当前虚线设置样式的非负偶数的数组

  3. lineDashOffset: 设置虚线样式的起始偏移量,值为一个浮点数,默认值为 0.0

演示

let y = 15;
function drawDashedLine(pattern) {
  ctx.beginPath();
  ctx.setLineDash(pattern);
  ctx.moveTo(0, y);
  ctx.lineTo(300, y);
  ctx.stroke();
  y += 20;
  console.log(ctx.getLineDash()); // [15, 3, 8, 3]  [12, 3, 3, 12, 3, 3]
}
drawDashedLine([15, 3, 8, 3]);
drawDashedLine([12, 3, 3]);

ctx.setLineDash([4, 16]);
// 无偏移量的虚线
ctx.beginPath();
ctx.moveTo(0, 80);
ctx.lineTo(300, 80);
ctx.stroke();
// 偏移量为 4 的虚线
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineDashOffset = 4;
ctx.moveTo(0, 100);
ctx.lineTo(300, 100);
ctx.stroke();

image.png

globalAlpha--透明度

globalAlpha: 值是一个在 0.0(完全透明)到 1.0(完全不透明)之间的数字,包括两者在内。默认值是 1.0。超出该范围的值,包括 Infinity 和 NaN,将不会被设置,并且 globalAlpha 将保留其先前的值。

演示

// 不透明
ctx.fillStyle = "red";
ctx.fillRect(160, 50, 100, 100);
// 透明
ctx.globalAlpha = 0.5;
ctx.fillStyle = "red";
ctx.fillRect(50, 50, 100, 100);

image.png

渐变

  1. 线性渐变 .createLinearGradient(x0, y0, x1, y1): 指定四个参数,分别表示渐变线段的起点(x0,y0)和终点(x1,y1),返回一个线段初始化的线性 CanvasGradient

  2. 径向渐变: .createRadialGradient(x0, y0, r0, x1, y1, r1): 指定六个参数,(x0,y0,r0)定义渐变的起始圆,(x1,y1,r1)定义渐变的结束圆,返回一个使用指定两个圆初始化的径向 CanvasGradient

  3. CanvasGradient.addColorStop(offset, color):

     offset: 一个在 0  1 之间(包含边界)的数字,表示色标的位置。0 表示渐变的起始位置,1 表示渐变的结束位置。
     color: 色标的颜色
    

演示:

// 创建线性渐变
// 渐变起点(20,0)  渐变终点(220,0)
const lineGradient = ctx.createLinearGradient(20, 0, 220, 0); // 返回 CanvasGradient
// 使用 CanvasGradient.addColorStop 添加三个色标
lineGradient.addColorStop(0, "green");
lineGradient.addColorStop(0.5, "cyan");
lineGradient.addColorStop(1, "green");
ctx.fillStyle = lineGradient;
ctx.fillRect(20, 220, 200, 100);

// 创建径向渐变
// 起始圆(110,90,30)  结束圆(100,100,70)
const radialGradient = ctx.createRadialGradient(110, 90, 30, 100, 100, 70);
radialGradient.addColorStop(0, "pink");
radialGradient.addColorStop(0.4, "white");
radialGradient.addColorStop(1, "green");
ctx.fillStyle = radialGradient;
ctx.fillRect(20, 20, 160, 160);

image.png

createPattern--图案样式

.createPattern(image, repetition): 用于使用指定的图像或重复创建图案。此方法返回一个 CanvasPattern 对象。这个方法并不直接在画布上绘制任何内容。它所创建的图案必须赋值给fillStylestrokeStyle,之后才会应用于接下来的绘制操作。

image 参数值:

    HTMLImageElement(<img>)-- .png 等图片
    SVGImageElement(<image>)-- svg 图
    HTMLVideoElement(<video>,通过使用视频捕获)-- 视频
    HTMLCanvasElement(<canvas>)-- canvas 图案
    ImageBitmap -- 位图图像
    OffscreenCanvas -- 脱离主线程的 Canvas 离屏渲染容器
    VideoFrame -- 视频帧的高效封装

repetition 参数值:

    repeat、null、'' -- 两个方向都重复
    repeat-x -- 仅水平方向重复 
    repeat-y -- 仅垂直方向重复
    no-repeat -- 两个方向都不重复

演示:

const img = new Image();
img.src = "./img.jpg";
// 图像加载完成后再使用
img.onload = () => {
  const pattern = ctx.createPattern(img, "no-repeat");
  // const pattern = ctx.createPattern(img, "repeat");
  // const pattern = ctx.createPattern(img, "repeat-x");
  // const pattern = ctx.createPattern(img, "repeat-y");
  ctx.fillStyle = pattern;
  ctx.fillRect(0, 0, 600, 600);
};

7.png

绘制文本

strokeText、fillText

  1. .strokeText(text, x, y, maxWidth).fillText(text, x, y, maxWidth):

     text: 文本字符串。文本根据 font、textAlign、textBaseline 和 direction 指定的设置进行渲染。
     (x,y): 绘制文本起始点坐标
     maxWidth(可选): 渲染后文本的最大宽度,如果提供了该值,用户代理将调整字距,选择水平方向更紧凑的字体或缩小字体大小,以便在指定宽度内容纳文本。
    
  2. font: 指定绘制文字所使用的当前字体样式,默认字体为 10 像素的无衬线体(sans-serif)

演示:

ctx.font = "bold 48px serif";
ctx.strokeText("strokeText", 50, 90);

ctx.fillText("fillText", 50, 190);
// 设置 maxWidth
ctx.fillText("fillText", 50, 290, 100);

image.png

文本样式

  1. font: 前面提过
  2. textAlign: 文本对齐的方式。可选值为:left、right、center、start()和end。默认值是 start。
  3. direction: 文本的方向。可选值为:ltr(文本方向从左向右)、rtl(文本方向从右向左)、inherit(文字方向从相应的 元素或 Document 继承)。默认值是 inherit。

注意:

  1. 当 direction = 'rtl'的时候,如果字符串以特殊符号结尾,那么特殊符号会显示在 end,也就是左边,例如: Hi! 会显示为 !Hi
  2. direction 属性会对 textAlign 属性产生影响。如果 direction 属性设置为 ltr,则textAlign属性的 left 和 start 的效果相同,right 和 end 的效果相同,如果 direction 属性设置为 rtl,则 textAlign属性的 left 和 end 的效果相同,right 和 start 的效果相同

direction演示:

ctx.font = "48px serif";
ctx.fillText("Hi!", 100, 200); // 默认 inherit ,这里就是 ltl
ctx.direction = "rtl";
ctx.fillText("Hi!", 100, 300);

image.png

textAlign演示:

ctx.moveTo(300, 0);
ctx.lineTo(300, 500);
ctx.stroke();
// 一般文本对齐,值为 left center right
ctx.font = "30px serif";
ctx.textAlign = "left"; // 左对齐
ctx.fillText("left-aligned", 300, 40);
ctx.textAlign = "center"; // 居中
ctx.fillText("center-aligned", 300, 85);
ctx.textAlign = "right"; // 右对齐
ctx.fillText("right-aligned", 300, 130);

// 依赖方向的文本对齐
ctx.direction = "ltr";
ctx.textAlign = "start"; // 相当于 left
ctx.fillText("ltr-Start-aligned", 300, 240);
ctx.textAlign = "end"; // 相当于 right
ctx.fillText("ltr-End-aligned", 300, 240);

ctx.direction = "rtl";
ctx.textAlign = "start"; // 相当于 right
ctx.fillText("rtl-Start-aligned", 300, 380);
ctx.textAlign = "end";
ctx.fillText("rtl-Start-aligned", 300, 380);

image.png

  1. textBaseline: 描述绘制文本时使用的文本基线,默认值:alphabetic

     top: 文本基线在文本块的顶部
     hanging: 文本基线是悬挂基线(用于藏文和其他印度文字)
     middle: 文本基线在文本块的中间
     alphabetic: 文本基线是标准的字母基线
     ideographic: 文字基线是表意字基线;如果字符本身超出了 alphabetic 基线,那么 ideograhpic 基线位置在字符本身的底部(用于中文、日文和韩文)
     bottom: 文本基线在文本块的底部。与 ideographic 基线的区别在于 ideographic 基线不需要考虑下行字母
    

演示:

const baselines = [
  "top",
  "hanging",
  "middle",
  "alphabetic",
  "ideographic",
  "bottom",
];
ctx.font = "20px serif";
ctx.strokeStyle = "red";

ctx.moveTo(0, 100);
ctx.lineTo(840, 100);
ctx.moveTo(0, 55);
ctx.stroke();

baselines.forEach((baseline, index) => {
  ctx.save(); // 用于将当前状态放入栈中,保存 canvas 的状态
  ctx.textBaseline = baseline;
  let x = index * 120 + 10;
  ctx.fillText("Abcdefghijk", x, 100);
  ctx.restore(); // 恢复到最近一次调用 save() 时保存的状态
  ctx.fillText(baseline, x + 5, 50);
});

image.png

measureText -- 测量文本

.measureText(text): 该方法返回一个包含被测量文本相关信息(例如它的宽度)的 TextMetrics 对象,text为需要测量的文本字符串

注意:

演示:返回的 TextMetrics 对象的属性都是只读

const text = ctx.measureText("Hello world");
console.log(text.width); // 53.729736328125

阴影

  1. shadowOffsetXshadowOffsetY属性: 用来设定阴影在 X 和 Y 轴的延伸距离,不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,默认为 0。

  2. shadowBlur属性: 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为 0

  3. shadowColor属性: 用于设定阴影颜色效果,默认是全透明的黑色

// 设置阴影
ctx.shadowColor = "rgba(255, 0, 0, 0.8)";
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 30;
ctx.shadowOffsetY = 20;
    
// 矩形
ctx.fillStyle = "rgba(0, 255, 0, 0.2)";
ctx.fillRect(10, 10, 150, 100);
// 矩形的边
ctx.lineWidth = 10;
ctx.strokeStyle = "rgba(0, 0, 255, 0.6)";
ctx.strokeRect(10, 10, 150, 100); 

image.png

绘制图像 -- drawImage

和 createPattern() 可以用来绘制图像,但使用方式和场景有区别

功能项drawImage()createPattern()
目的绘制图像创建基于图像的重复图案
用法ctx.drawImage(image, x, y)ctx.fillStyle = ctx.createPattern(img, repeat)
作用对象直接绘制图像到 canvas设置图案填充样式,需配合 fillRect() 使用
图像是否平铺❌ 否,只绘制一次✅ 是,可以设置为重复铺满
剪裁/缩放支持✅ 支持(有多个重载)❌ 不支持剪裁,整图平铺
性能更适合单次绘制更适合大面积图案填充
示例用途渲染一张图片、贴图、角色等背景纹理、墙面砖块、地板图案等
  1. .drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight): 用于直接绘制一张图片到指定位置

注意: drawImage()方法根据传参个数实现不同功能,有三种传参方式

  • drawImage(image, dx, dy)
  • drawImage(image, dx, dy, dWidth, dHeight)
  • drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

参数说明:

    image: 和 createPattern() 的image 参数值一样
    sx、sy:裁剪框的左上角(相对于源 image )X轴坐标和Y轴坐标,可以使用 3 参数或 5 参数语法来省略这个参数
    sWidth、sHeight:裁剪框的宽度和高度
    dx、dy:源 image 的左上角在目标画布上 X 轴坐标、 y 轴坐标
    dWidth、dHeight:image 在目标画布上绘制的宽度。允许对绘制的图像进行缩放。如果不指定,在绘制时 image 宽度不会缩放
const image = new Image();
image.src = "./img.jpg";
image.onload = function () {
    ctx.drawImage(image, 50, 50); // drawImage(image, dx, dy)
};

image.png

    ctx.drawImage(image, 50, 50, 200, 100); 

image.png

ctx.drawImage(image, 50, 50);
ctx.drawImage(image, 60, 60, 100, 100, 50, 550, 200, 100); 

image.png

变换状态

save、restore -- 状态的保存和恢复

  1. .save(): 用于通过将当前状态放入栈中,以保存 canvas 的完整状态

     保存到栈中的绘制状态有下面部分组成:
         当前的变换矩阵
         当前的剪切区域
         当前的虚线列表
         以下属性当前的值:strokeStyle、fillStyle、globalAlpha、lineWidth、
             lineCap、lineJoin、miterLimit、lineDashOffset、shadowOffsetX、
             shadowOffsetY、shadowBlur、shadowColor、globalCompositeOperation、
             font、textAlign、textBaseline、direction、imageSmoothingEnabled。
    
  2. .restore(): 用于通过在绘制状态栈中弹出顶部的条目,将 canvas 恢复到最近的保存状态。如果没有保存状态,此方法不做任何改变

// 保存 fillStyle
ctx.save();

ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100);

// 恢复到最近一次调用 save() 保存的状态
ctx.restore();
ctx.fillRect(150, 40, 100, 100);

image.png

移动、旋转和缩放

translate(x,y)

用于对当前网格添加平移变换,x 是左右偏移量,y 是上下偏移量

ctx.fillRect(150, 40, 100, 100); // 未平移
ctx.translate(120, 0);
ctx.fillRect(150, 40, 100, 100); // 平移

image.png

.rotate(angle)

用于在变换矩阵中增加旋转, angle 是顺时针旋转弧度。旋转中心是 canvas 的原点,如果想改变中心点,使用 translate() 移动画布

1. 以原点旋转
// canvas 原点
ctx.arc(0, 0, 10, 0, 2 * Math.PI);
ctx.fillStyle = "red";
ctx.fill();

// 未旋转的矩形
ctx.fillStyle = "gray";
ctx.fillRect(100, 0, 80, 20);

// 旋转的矩形
ctx.rotate((45 * Math.PI) / 180);
ctx.fillStyle = "red";
ctx.fillRect(100, 0, 80, 20); 

image.png

2. 围绕中心旋转
// 未旋转的矩形
ctx.fillStyle = "gray";
ctx.fillRect(80, 60, 140, 30);

// 矩阵变换 (150,75)是矩形中心点
ctx.translate(150, 75); // 使用tranlate把旋转中心由 canvas 原点变矩形中心点
ctx.rotate(Math.PI / 2); // 旋转 90°
ctx.translate(-150, -75); // 使用tranlate把旋转中心改回去

// 旋转后的矩形
ctx.fillStyle = "red";
ctx.fillRect(80, 60, 140, 30); 

image.png

.scale(x, y)

用于根据水平和垂直方向,为 canvas 单位添加缩放变换,x和y的值小于1则为缩小,大于1则为放大。默认值为 1

// 未缩放的矩形
ctx.fillStyle = "gray";
ctx.fillRect(10, 10, 8, 20);

// 缩放后的矩形
ctx.scale(9, 3);
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 8, 20); 

image.png

transform setTransform resetTransform

  1. .transform(a, b, c, d, e, f): 用于将由该方法的参数所描述的矩阵与当前的变换相乘
  2. .setTransform(a, b, c, d, e, f): 该方法会将当前变形矩阵重置为单位矩阵,然后用相同的参数调用 transform
  3. .resetTransform(): 该方法可以重置当前变形为单位矩阵。等同于调用 setTransform(1, 0, 0, 1, 0, 0)
  4. .getTransform(): 检索矩阵

如果一个点原始坐标为 (x,y),经过变换后,其坐标将变为 (ax+cy+e,bx+dy+f)。 参数说明:

    a: 水平方向的缩放
    b: 竖直方向的倾斜偏移
    c: 水平方向的倾斜偏移
    d: 竖直方向的缩放
    e: 水平方向的移动
    f: 竖直方向的移动
     b  c  0 时,a  d 控制上下文的水平和垂直缩放
     a  d  1 时,b  c 控制上下文的水平和垂直倾斜
// 进行垂直(.2)和水平(.8)方向的倾斜,同时保持缩放和平移不变
ctx.transform(1, 0.2, 0.8, 1, 0, 0);
ctx.fillRect(0, 0, 100, 100);

ctx.setTransform(1, 0.2, 0.8, 1, 0, 0);
ctx.fillRect(0, 120, 100, 100);
console.log(ctx.getTransform());// DOMMatrix: {a:1,b:0.2 ...}
ctx.resetTransform();
console.log(ctx.getTransform()); // DOMMatrix: {a:1,b:0 ...}

image.png

合成 -- globalCompositeOperation

案例推荐: MDN-globalCompositeOperation

设置要在绘制新形状时应用的合成操作的类型

属性值:

  • source-over: 默认值,在现有画布上绘制新图形
  • source-in: 仅在新形状和目标画布重叠的地方绘制新形状。其他的都是透明的
  • source-out: 在不与现有画布内容重叠的地方绘制新图形
  • source-atop: 只在与现有画布内容重叠的地方绘制新图形
  • destination-over: 在现有画布内容的后面绘制新的图形
  • destination-in: 仅保留现有画布内容和新形状重叠的部分。其他的都是透明的
  • destination-out: 仅保留现有画布内容和新形状不重叠的部分
  • destination-atop: 仅保留现有画布内容和新形状重叠的部分。新形状是在现有画布内容的后面绘制的
  • lighter: 两个重叠图形的颜色是通过颜色值相加来确定的
  • copy: 只显示新图形
  • xor: 形状在重叠处变为透明,在其他地方正常绘制
  • multiply: 将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片
  • screen: 像素被倒转、相乘、再倒转,结果是一幅更明亮的图片(与 multiply 相反)
  • overlay: multiply 和 screen 的结合。原本暗的地方更暗,原本亮的地方更亮
  • darken: 保留两个图层中最暗的像素
  • lighten: 保留两个图层中最亮的像素
  • color-dodge: 将底层除以顶层的反置
  • color-burn: 将反置的底层除以顶层,然后将结果反过来
  • hard-light: 类似于 overlay,multiply 和 screen 的结合——但上下图层互换了
  • soft-light: 柔和版本的 hard-light。纯黑或纯白不会导致纯黑或纯白
  • difference: 从顶层减去底层(或反之亦然),始终得到正值
  • exclusion: 与 difference 类似,但对比度较低
  • hue: 保留底层的亮度(luma)和色度(chroma),同时采用顶层的色调(hue)
  • saturation: 保留底层的亮度和色调,同时采用顶层的色度
  • color: 保留了底层的亮度,同时采用了顶层的色调和色度
  • luminosity: 保持底层的色调和色度,同时采用顶层的亮度

裁剪 -- clip

用于将当前或给定路径转换为当前裁剪区域。前面的裁剪区域(如果有的话)将与当前路径或给定路径相交,从而创建新的裁剪区域

source-in source-atop 的区别

功能项clip()source-in / source-atop
控制方式空间裁剪(路径)像素混合(像素叠加关系)
绘制区域仅限裁剪路径内部基于已有像素的重叠部分
是否可嵌套叠加✅ 可连续调用形成复合裁剪路径❌ 不支持组合,后续操作会替代前一个混合方式
典型用途动态遮罩、圆形头像、局部区域绘图限制图像蒙版、图层合成、遮罩转场动画
性能表现较高(路径裁剪硬件加速)中等(像素级运算,复杂度较高)
图像平铺是否受影响✅ 可控制路径内平铺❌ 图像实际绘制但只在像素合成中显示处理

语法

  • .clip() 使用上下文中当前路径裁剪
  • .clip(path) path: 需要用作裁剪区域的 Path2D 路径
  • .clip(fillRule) fillRule(暂未理解): 一个判断是在路径内还是在路径外的算法,允许的值有 nonzero(默认值):非零环绕原则,evenodd:奇偶环绕原则
  • .clip(path, fillRule)
// 裁剪一个圆
ctx.arc(100, 75, 50, 0, Math.PI * 2);
ctx.clip();
ctx.fillRect(0, 0, canvas.width, canvas.height); 

image.png

动画 -- window.requestAnimationFrame(callback)

canvas 实现动画可以依靠 window 的三个方法

  • window.setInterval(function, delay)

  • window.setTimeout(function, delay)

  • window.requestAnimationFrame(callback) (推荐) 告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数,若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 requestAnimationFrame(),因为requestAnimationFrame()是一次性的。

绘制动画的基本步骤

  • 获取绘图上下文
  • 定义动画开始状态
  • 清空画布(每帧都要做),避免残影
  • 保存 canvas 状态,避免每帧都是以原始状态绘制
  • 绘制当前帧的内容
  • 更新状态(位置、角度等)
  • 恢复 canvas 状态
  • 使用 requestAnimationFrame() 循环绘制下一帧
// 1. 获取绘图上下文
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

// 2. 定义动画开始状态
const ball = {
  x: 25,
  y: 25,
  radius: 25,
  speed: 3,
};

function startAnimation() {
  // 3. 清空画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // 4. 保存 canvas 状态,避免每帧都是以原始状态绘制
  ctx.save();
  // 5. 绘制当前帧的内容
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
  ctx.fill();
  // 6. 更新状态(位置、角度等)
  if (ball.x > canvas.width + ball.radius) ball.x = 0;
  ball.x += ball.speed;
  // 7. 恢复 canvas 状态
  ctx.restore();
  // 8. 使用 requestAnimationFrame() 循环绘制下一帧
  requestAnimationFrame(startAnimation);
}
startAnimation();    

sad1.gif