一文入坑【Canvas】多图与案例详解

3,095 阅读14分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

输出内容才能更好的理解输入的知识,进阶实践文章已发布:
【Canvas实战】仿明日方舟Logo粒子动画 vue3+ts

前言🎀

如果你想深入图形、可视化等领域,那么肯定离不开 canvas、webgl、three.js 等一系列技术。在这众多技术中,选择canvas2d为基础来入门是一个不错的选择。

canvas在动画、可视化、图片处理等领域有着出人意料的表现,是十分值得学习的一门技术。例如echarts就是通过canvas和svg实现的。

希望通过这篇文章记录Canvas基本的用法和场景,并拓展自己的前端视野。

如果觉得有收获还望大家点赞、收藏🌹

案例

先来看几个案例,你可能就对canvas的应用场景有所了解,也会对它产生兴趣。

当然有些案例不使用canvas一样能实现,但在都能实现目标的情况下如果canvas效果更好或者更方便,那么还是建议大家尝试下canvas的~

点击小球
很简单的一个小球动画,能衍生出许多东西,我为什么想到了合成大西瓜hhhh

截图
还有很多额外操作,如滤镜、调色等就像P图一样。略微修改一下还能当放大镜使用

粒子爆炸

particle

还有特别多场景如 画板、图片水印、视频、进度条、粒子动画...都有很好的效果,但没有研究就不详述了,欢迎了解的同学补充。

Canvas

首先我们学习如何创建canvas元素并获取操作对象

创建画布

<canvas> 是一个可以使用脚本 (通常为JavaScript) 来绘制图形的 HTML 元素 。例如,它可以用于绘制图表、制作图片构图或者制作简单的动画。

<html>
    ....
    <canvas id="canvas" width="300" height="300"></canvas>
    ....
</html>

这样就在页面上创建了一个canvas画布,需要注意的是canvas元素自身只有两个属性 width和height,都只接受数字为属性值,无需设置单位,默认单位为像素。

impicture_20220914_110017.png 如果不设置会初始化宽度为300像素和高度为150像素。

impicture_20220914_110305.png

如果通过css来定义大小,绘制时图像会伸缩以适应框架的尺寸,如果css的尺寸与初始画布比例不一样,内容就容易出现拉伸。

impicture_20220914_112323.png impicture_20220914_112014.png

直线被拉粗

获取上下文

<canvas> 元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,其可以用来绘制和处理要展示的内容。我们进行操作时先根据需要获取渲染上下文,然后在它上面绘制。

这个过程就像我们在进行绘画之前选择了一只画笔。

<script>
    ....
    // 获取canvas对象
    const canvas = document.getElementById('canvas')
    // 获取渲染上下文对象
    const ctx = canvas.getContext('2d')
    ...
</script>

本文主要了解2D渲染上下文,canvas也支持如WebGL的3D上下文,更多详情 MDN-Canvas

Canvas API

在创建完canvas画布并获取到渲染上下文拿到画笔后,我们可以看看canvas支持了哪些操作。

以下内容主要总结自MDN-Canvas-API与部分使用经验,适当精简。如果对具体细节有疑惑的同学可以查看官方文档或评论区交流。

栅格

通常canvas元素默认被网格覆盖,一个网格单元相当于canvas元素中的一像素。
所有元素都相对于原点(0, 0)定位,图中蓝色正方形距离Y轴x像素,距离X轴y像素,所以坐标为(x, y)。 栅格

绘制图形

先学习简单的如何绘制直线、矩形、三角形和圆形等。但在绘制前需要掌握路径,它是图形形成的基础。

路径 Path

图形的基本元素是 路径 。
路径 是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的

主要使用以下四个函数:

  1. beginPath 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
  2. closePath 闭合路径之后图形绘制命令又重新指向到上下文中。
  3. stroke 通过线条来绘制图形轮廓。
  4. fill 通过填充路径的内容区域生成实心的图形。

生成路径主要有三个步骤:

  1. 调用 beginPath() 清空重置路径和子路径列表,以重新绘制图形。
  2. 调用 目标函数 绘制路径。
  3. 非必需,调用 closePath() 绘制一条从当前点到开始点的直线来闭合图形。

如果不按照规定步骤来绘制路径可能会导致意外的结果
我们通过绘制圆弧的案例来观察(arc函数用于绘制圆和圆弧等图形)
impicture_20220915_174153.png 两个圆弧被连接在了一起,这是因为一系列操作被canvas理解为了同一次路径的绘制,而我们的本意是两次绘制并分为两个图形
解决方法,每次绘制都重新创建路径并在绘制结束后闭合并重新开始路径: impicture_20220915_174854.png

移动笔触 moveTo

每次绘制总是从一个点开始再到另一个点结束,可以想象一下我们在纸上移动画笔的过程。

而canvas则提供了一个 moveTo函数 让我们在“画布”上“移动”自己画笔的位置(并不是作画,而是设置起始点)。

moveTo(x, y):将笔触移动到指定的坐标 x 以及 y 上

直线 lineTo

直线也是图形的一种,学习了路径和移动笔触后再来看如何绘制出一条直线
主要使用 lineTo(x, y) 函数,绘制一条从当前位置到指定x以及y位置的直线

impicture_20220915_200234.png

<script>
    ....
    ctx.beginPath()
    ctx.moveTo(50, 50)
    ctx.lineTo(200, 50)
    ctx.closePath()
    ctx.stroke()
</script>

三角形 triangle

三角形可以理解为三条直线连接在一起,

impicture_20220915_201018.png

<script>
    ....
    ctx.moveTo(50, 50)
    ctx.lineTo(100, 50)
    ctx.lineTo(100, 100)
    ctx.lineTo(50, 50)
    ctx.stroke()
</script>

也可以是一次路径绘制上的两条直线通过闭合连接头尾 impicture_20220915_200850.png

<script>
    ....
    ctx.beginPath()
    ctx.moveTo(50, 50)
    ctx.lineTo(100, 50)
    ctx.lineTo(100, 100)
    ctx.closePath()
    ctx.stroke()
</script>

矩形 rect

所有的图形都能由单条或者多条路径组合而成,不过canvas也提供了内置的方法让我们快速绘制复杂的图形。

我们先来看矩形的绘制,它可以由四条直线拼接而成,但更多时候我们直接使用内置方法。

主要有以下三个方法,其中 x 和 y 是矩形的起点坐标,width 和 height 设置矩形的尺寸。

  1. fillRect(x, y, width, height) 绘制一个填充的矩形

impicture_20220915_203607.png

  1. strokeRect(x, y, width, height) 绘制一个矩形的边框

impicture_20220915_203707.png

  1. clearRect(x, y, width, height) 清除指定矩形区域,让清除部分完全透明

impicture_20220915_203841.png

圆形 arc

绘制圆形的方法是:arc(x, y, radius, startAngle, endAngle, anticlockwise)

其中(x,y)为圆心,radius 为圆的半径,(startAngle,endAngle) 为开始、结束角度, anticlockwise 为绘制方向(默认false为顺时针)。

需要注意的是:

startAngle,endAngle都以弧度为单位 每1°为Math.PI / 180

所以为便于理解设置值时应参照公式: 弧度 = 目标角度 * (Math.PI / 180)

即 360° = 360 * (Math.PI / 180)

impicture_20220915_210021.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.arc(100, 100, 50, 0, 360 * Math.PI / 180, false)
ctx.stroke()
ctx.closePath()

ctx.beginPath()
ctx.arc(250, 100, 50, 0, 180 * Math.PI / 180, false)
ctx.stroke()
ctx.closePath()

注意:每次绘制完后要闭合路径,否则会导致路径里举例的情况

样式、颜色

在上一节里只用到了默认的线条和填充样式,这一节来学习canvas更多的样式可选项,绘制出更吸引人的内容。

可以想象下我们使用不同色彩、大小的笔和不同的手法在画布上绘画。

色彩 Colors

canvas除了提供绘制内容的方法,也提供了设置图形颜色的属性。

属性值支持符合CSS所有color属性的值:

// 这些 fillStyle 的值均为 '橙色'
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";

主要有以下两个属性:

  1. fillStyle = color 设置图形填充颜色

impicture_20220916_104404.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
for (let i = 0; i < 6; i++){
  for (let j = 0; j < 6; j++){
    // 设置填充颜色
    ctx.fillStyle = `rgb(${Math.floor(255 - 42.5 * i)}, ${Math.floor(255 - 42.5 * j)}, 0)`
    ctx.fillRect(100 + j * 30, 15 + i * 30, 30, 30)
  }
}
  1. strokeStyle = color 设置图形轮廓颜色 impicture_20220916_105805.png
const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
for (let i=0;i<6;i++){
  for (let j=0;j<6;j++){
    // 设置轮廓颜色
    ctx.strokeStyle = `rgb(0, ${Math.floor(255 - 42.5 * i)}, ${Math.floor(255 - 42.5 * j)})`
    ctx.beginPath();
    ctx.arc(100 + j * 25,30 + i * 25, 10, 0, 360 * Math.PI / 180, true);
    ctx.stroke();
  }
}

透明度 Transparency

除了可以绘制实色图形,我们还可以用 canvas 来绘制半透明的图形。通过设置 globalAlpha 属性或者使用一个半透明颜色作为轮廓或填充的样式。

globalAlpha = transparencyValue

transparencyValue 有效的值范围是 0.0(完全透明)到 1.0(完全不透明),默认是 1.0。

impicture_20220916_115629.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
ctx.fillRect(50, 20, 250, 30)

ctx.beginPath()
ctx.fillStyle = 'rgba(0, 0, 0, 1)'
ctx.globalAlpha = 0.3
ctx.fillRect(50, 50, 250, 30)

ctx.beginPath()
ctx.fillStyle = 'green'
ctx.globalAlpha = 0.4
ctx.fillRect(150, 30, 50, 40)

线型 Line Styles

可以通过以下一系列属性来设置线的样式。

  1. lineWidth = value 设置线条宽度

这个属性设置当前绘线的粗细。属性值必须为正数。默认值是 1.0。

impicture_20220916_202047.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.lineWidth = 1.0
ctx.moveTo(50, 50)
ctx.lineTo(200, 50)
ctx.stroke()

ctx.beginPath()
ctx.lineWidth = 2.0
ctx.moveTo(50, 75)
ctx.lineTo(200, 75)
ctx.stroke()

ctx.beginPath()
ctx.lineWidth = 4.0
ctx.moveTo(50, 100)
ctx.lineTo(200, 100)
ctx.stroke()

ctx.beginPath()
ctx.lineWidth = 10.0
ctx.moveTo(50, 125)
ctx.lineTo(200, 125)
ctx.stroke()
  1. lineCap = type 设置线条末端样式

属性 lineCap 的值决定了线段端点显示的样子。它可以为下面的三种的其中之一:butt无线帽round圆线帽square方形线帽。默认是 butt。

impicture_20220916_204528.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.strokeStyle = '#09f';
ctx.beginPath();
ctx.moveTo(50,30);
ctx.lineTo(50,120);
ctx.moveTo(250,30);
ctx.lineTo(250,120);
ctx.stroke();
// butt
ctx.beginPath();
ctx.strokeStyle = '#000';
ctx.lineWidth = 8.0
ctx.lineCap = 'butt'
ctx.moveTo(50, 50)
ctx.lineTo(250, 50)
ctx.stroke()
// round
ctx.beginPath()
ctx.lineCap = 'round'
ctx.moveTo(50, 75)
ctx.lineTo(250, 75)
ctx.stroke()
ctx.closePath()
// square
ctx.beginPath()
ctx.lineCap = 'square'
ctx.moveTo(50, 100)
ctx.lineTo(250, 100)
ctx.stroke()
  1. lineJoin = type 设定线条与线条间接合处的样式

使用与lineCap类似但作用的位置是线段连接处。

lineJoin 的属性值决定了图形中两线段连接处所显示的样子。它可以是这三种之一:round圆角, bevel斜角miter尖角。默认是 miter。

impicture_20220921_105535.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const lineJoin = ['round', 'bevel', 'miter'];
ctx.lineWidth = 10;
for (var i = 0; i < lineJoin.length; i++) {
  ctx.lineJoin = lineJoin[i];
  ctx.beginPath();
  ctx.moveTo(90, 10 + i * 60)
  ctx.lineTo(180, 10 + i * 60)
  ctx.lineTo(180, 60 + i * 60)
  ctx.stroke()
}
  1. setLineDash(segments) 设置当前虚线样式 / lineDashOffset = value 设置虚线样式的起始偏移量

setLineDash 方法接受一个数组,来指定线段与间隙的交替;
impicture_20220917_191727.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.setLineDash([4, 2]);
ctx.strokeRect(10,10, 100, 100);

lineDashOffset 属性设置起始偏移量。

渐变 Gradients

就好像一般的绘图软件一样,我们可以用线性径向的渐变来填充描边,主要使用canvasGradient对象,并且赋值给图形的 fillStyle 或 strokeStyle 属性。

addColorStop

可以使用addColorStop方法对canvasGradient渐变对象上色。

addColorStop 每次调用会为渐变对象添加一个色标,渐变内容会从一个色标的颜色渐变为下一个色标的颜色,可以存在多个色标。

addColorStop(position, color)
接收两个参数,分别表示:
1.position 渐变中颜色所在相对位置, 如0代表颜色出现在开头 1代表出现在末尾
2.color 色标值,必须是一个有效的CSS颜色值,如#fff rgba(0, 0, 0, 1)等

canvasGradient

使用以下两个方法创建 canvasGradient渐变对象,并使用addColorStop方法对其上色

  1. createLinearGradient(x1, y1, x2, y2) 创建线性渐变对象,参数分别表示 渐变的起点坐标(x1, y1) 和 渐变的终点坐标(x2, y2)。如果两个坐标x/y轴坐标不同会出现沿x/y轴的倾斜

impicture_20220919_212111.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const gradient1 = ctx.createLinearGradient(20, 40, 280, 40)
gradient1.addColorStop(0, '#03a9f4')
gradient1.addColorStop(0.33, '#f441a5')
gradient1.addColorStop(0.66, '#ffeb3b')
gradient1.addColorStop(1, '#03a9f4')
ctx.beginPath()
ctx.fillStyle = gradient1
ctx.fillRect(20, 40, 260, 40)

const gradient2 = ctx.createLinearGradient(20, 120, 280, 200)
gradient2.addColorStop(0, '#03a9f4')
gradient2.addColorStop(0.33, '#f441a5')
gradient2.addColorStop(0.66, '#ffeb3b')
gradient2.addColorStop(1, '#03a9f4')
ctx.beginPath()
ctx.fillStyle = gradient2
ctx.fillRect(20, 120, 260, 40)
  1. createRadialGradient(x1, y1, r1, x2, y2, r2) 创建径向渐变对象,参数分别表示 开始圆的坐标 (x1, y1) 和 半径r1 ,以及 结束圆的坐标(x2, y2) 和 半径r2。
    想象一个圆1沿着路径逐渐变为另一个圆2,或者以圆2为中心向外围的圆1扩张。

impicture_20220921_115132.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const radgrad1 = ctx.createRadialGradient(45,45,10,52,50,30);
radgrad1.addColorStop(0, '#A7D30C');
radgrad1.addColorStop(0.9, '#019F62');
radgrad1.addColorStop(1, 'rgba(1,159,98,0)');

const radgrad2 = ctx.createRadialGradient(105,105,20,112,120,50);
radgrad2.addColorStop(0, '#FF5F98');
radgrad2.addColorStop(0.75, '#FF0188');
radgrad2.addColorStop(1, 'rgba(255,1,136,0)');

const radgrad3 = ctx.createRadialGradient(95,15,15,102,20,40);
radgrad3.addColorStop(0, '#00C9FF');
radgrad3.addColorStop(0.8, '#00B5E2');
radgrad3.addColorStop(1, 'rgba(0,201,255,0)');

ctx.beginPath()
ctx.fillStyle = radgrad1
ctx.fillRect(0, 0, 300, 200)
ctx.fillStyle = radgrad2
ctx.fillRect(0, 0, 300, 200)
ctx.fillStyle = radgrad3
ctx.fillRect(0, 0, 300, 200)

阴影 Shadows

可以给文字或图形添加阴影,主要使用以下几个属性:

  1. shadowOffsetX = float / shadowOffsetY = float 用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0。
  2. shadowBlur = float 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为 0。
  3. shadowColor = color 是标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色。

impicture_20220921_142226.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const radgrad1 = ctx.createRadialGradient(45,45,10,52,50,30);

ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 2;
ctx.shadowColor = "rgba(0, 0, 0, 0.5)";

ctx.font = "20px Times New Roman";
ctx.fillStyle = "Black";
ctx.fillText("Sample String", 80, 30)

ctx.shadowOffsetX = -10;
ctx.shadowOffsetY = -10;
ctx.shadowBlur = 3;
ctx.shadowColor = "orange";

ctx.font = "20px Times New Roman";
ctx.fillStyle = "Black";
ctx.fillText("Sample String", 80, 80)

绘制文本

在了解样式、颜色后再来看如何在canvas中绘制文本。

绘制方式

canvas提供了两种方式来渲染文本:

  1. fillText(text, x, y, maxWidth) 在 (x,y) 位置以填充的方式绘制指定的文本,绘制的最大宽度是可选的。

  2. strokeText(text, x, y, maxWidth) 在 (x,y) 位置以描边的方式绘制指定的文本,绘制的最大宽度是可选的。

impicture_20220922_101041.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

ctx.font = '40px serif'
ctx.fillText('hello world', 40, 80)
ctx.strokeText('hello world', 40, 120)

文本样式

之前的例子中已经有使用文本样式了,例如font可以调整文本的字体大小和类型。还有以下更多的属性可以改变canvas显示文本的方式:

  1. font = value 设置文本样式的属性,value值与CSS font属性相同,默认为"10px sans-serif"
// canvans font支持CSS font所有属性
cxt.font = 'font-style font-variant font-weight font-size/line-height font-family'
// font-family需要与font-size同时设置,否则无效
ctx.font = '20px serif'

impicture_20220922_103306.png

2.textAlign = value 文本对齐选项,可选值为:start 坐标x处开始、 end 坐标x处结束left 左对齐、 right 右对齐 和 center 居中。默认值是start。

impicture_20220922_104942.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.moveTo(200, 0)
ctx.lineTo(200, 200)
ctx.stroke()

ctx.font = '20px serif'
const aligns = ['center', 'left', 'right', 'start', 'end']
aligns.forEach((align, index) => {
  ctx.textAlign = align
  ctx.fillText('文本 ' + align, 200, 30 + 30 * index)
})
  1. textBaseline = value 基线对齐选项,可选值为:top、 middle、 bottom、alphabetic、 hanging 和 ideographic。默认值是alphabetic。

impicture_20220922_111924.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

ctx.font = '18px serif'
const baslines = ['top', 'middle', 'bottom', 'alphabetic', 'hanging''ideographic']
baslines.forEach((basline, index) => {
  // 绘制对比线
  ctx.moveTo(0, 30.5 + 40 * index)
  ctx.lineTo(300, 30.5 + 40 * index)
  ctx.stroke()
  // 调整基准线
  ctx.textBaseline = basline
  // 绘制文本
  ctx.fillText('文本 ' + basline, 100, 30 + 40 * index)
})
  1. direction = value 描述文本方向的属性。可能的值包括:ltr 从左向右、 rtl 从右向左inherit 继承父元素。默认值是inherit。

impicture_20220922_113445.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

ctx.font = '18px serif'
const directions = ['ltr', 'rtl', 'inherit']
directions.forEach((direction, index) => {
  ctx.beginPath()
  ctx.direction = direction
  // 绘制文本
  ctx.fillText('canvas ' + direction + '!', 140, 30 + 40 * index)
})

这里不知为什么rtl只反序了符号,还望大佬指点

操作图片

canvas对图片(包括但不限于 图片、视频,甚至是另一个canvas)各种操作的支持是一大亮点,这也是非常让人感兴趣的一个地方,感觉终于摆脱了条条框框的线条和图形。

canvas提供了 绘制图片 和 获取图片像素 的方法

获取图片

在绘制图片或者获取图片信息用于操作之前,首先要获取目标图片源

浏览器支持的任意格式的外部图片都可以使用,比如 PNG、GIF 或 JPEG。

还可以将*<video>中的视频帧*作为图片源。

甚至可以将同一个页面中其他 canvas 元素生成的图片作为图片源。(例如案例中的截图

外部图片主要通过两种方式:1.获取页面上的DOM-img元素 2.在JS里创建Image对象 onload回调时读取

<html>
    <div id="app">
      <img id="img1" src="https://p3-passport.byteimg.com/img/user-avatar/edcdbbde0d6f5cb89d7c28187ed55480~180x180.awebp" />
      <canvas id="canvas" width="300" height="200"></canvas>
    </div>
</html>
<script>
    const canvs = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')

    const img1 = document.getElementById('img1')

    const img2 = new Image()
    img2.src = 'https://p3-passport.byteimg.com/img/user-avatar/edcdbbde0d6f5cb89d7c28187ed55480~180x180.awebp'
    img2.onload(() => {
      // ...
    })
</script>

引用其它canvas元素主要通过:
1.document.getElementsByTagName
2.document.getElementById

但你引入的应该是已经准备好的canvas。

一个常用的应用就是将第二个 canvas 作为另一个大的 canvas 的缩略图。

绘制图片

一旦获得了源图对象,我们就可以使用 drawImage 方法将它渲染到 canvas 里。drawImage 方法有三种形态:

drawImage(image, dx, dy) 绘制

参数
1. image:图片对象
2. dx:图片左上角在canvas中的x轴坐标
3. dy:图片左上角在canvas中的y轴坐标

效果

最普通的渲染图片方式,将目标图片渲染到canvas的(x, y)坐标上。超出canvas的部分会被隐藏,重叠的部分新的图片在上层。

impicture_20220924_160837.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

const img = new Image()
img.src = 'https://p3-passport.byteimg.com/img/user-avatar/edcdbbde0d6f5cb89d7c28187ed55480~180x180.awebp'
img.onload = () => {
  ctx.drawImage(img, 10, 10)
  ctx.drawImage(img, 40, 10)
  ctx.drawImage(img, 300 + img.width, 10)
  ctx.drawImage(img, 10, 100 + img.height)
}
drawImage(image, dx, dy, dWidth, dHeight) 缩放

新增了两个参数用于控制图像在canvas中的宽高,两个参数都是可选的,但不能单独选择,如设置了width则必须设置height。

参数
1. image:图片对象
2. dx:图片左上角在canvas中的x轴坐标
3. dy:图片左上角在canvas中的y轴坐标
4. dWidth:图片在canvas中的宽度
5. dHeight:图片在canvas中的高度

效果

将目标图片以dWidth的宽度和dHeight的高度渲染到canvas的(x, y)坐标上,dWidth/dHeight !== img.width/img.height 时会拉伸图片。

impicture_20220924_165334.png

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

新增了四个参数用于控制在原图片上进行切片的位置和大小,需要注意的是这四个参数是插入在前面的。

参数
1. image:图片对象
2. sx:裁剪位置在图片对象中的x轴坐标
3. sy:裁剪位置在图片对象中的y轴坐标
4. sWidth:裁剪出的切片宽度
5. sHeight:裁剪出的切片高度
// 此处是由图片裁剪出的切片
6. dx:切片左上角在canvas中的x轴坐标
7. dy:切片左上角在canvas中的y轴坐标
8. dWidth:切片在canvas中的宽度
9. dHeight:切片在canvas中的高度

效果
首先在图片(sx, sy)位置裁剪出一个sWidth宽和sHeight高的切片,然后将切片以dWidth的宽度和dHeight的高度渲染到canvas的(x, y)坐标上。

impicture_20220925_200421.png

const canvs = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

const img = new Image()
img.src = 'https://p3-passport.byteimg.com/img/user-avatar/edcdbbde0d6f5cb89d7c28187ed55480~180x180.awebp'
img.onload = () => {
  ctx.drawImage(img, 10, 10)
  const { width, height } = img

  const sWidth = 0.5 * width
  const sHeight = 0.5 * height
  const dWidth = 0.8 * width
  const dHeight = 0.8 * height
  const positions = [[0, 0], [0.5, 0], [0, 0.5], [0.5, 0.5]]
  for (let [sx, sy] of positions) {
    const dx = 50 + width + 2.1 * sx * dWidth
    const dy = 10 + 2.1 * sy * dHeight
    ctx.drawImage(img, sx * width,  sy * height, sWidth, sHeight, dx, dy, dWidth, dHeight)
  }
}

像素操作

canvas除了支持对图片缩放、裁剪外,还提供了获取图片像素的功能,这衍生出了更多对图片的操作。

通过canvas的getImageData方法可以获得ImageData对象,而ImageData对象中存储着canvas对象真实的像素数据。

getImageData(left, top, width, height)

作用
该方法会解析canvas中 以(left, top)为坐标点 宽width、高height 的画布区域,并返回一个ImageData对象,它代表了画布指定区域的数据。

备注
任何在画布以外的元素都会被返回成一个透明黑的 ImageData 对像。

ImageData

作用
可以通过ImageData对象操纵像素数据,直接读取或将数据数组写入该对象中。

属性
1.width 图片宽度,单位像素。
2.height 图片高度,单位像素。
3.data Uint8ClampedArray类型的一维数组,包含了指定区域里每个像素点的RGBA格式的整型数据,范围在0至255之间(包括255)。

Uint8ClampedArray 8位无符号整型(即值区间在[0, 255] = [0, (2 ^ 8 - 1)]的正整数)固定数组,区间外的值会被替换成0/255,非整数的值会被替换成最接近它的整数。

∵ 每一个像素点有4个值占据data数组4个索引位置,对应像素rgba(R, G, B, A)的四个值
∴ 第n个位置像素的R、G、B、A信息索引分别为:

Rn = (n - 1) * 4 
Gn = (n - 1) * 4 + 1
Bn = (n - 1) * 4 + 2
An = (n - 1) * 4 + 3

imageData概念图.png

∵ 图片的像素并不是一维数组一样平铺的,而是一个包含行和列的二维矩形
∴ 我们在寻找目标像素点时一般不直接用n索引,而是用 i列j行 / [i, j] 来计算目标索引:

const width = img.width  // 获取图片宽度 = 二维矩形的宽度
Rij = [(j - 1) * width + (i - 1)] * 4 
Gij = [(j - 1) * width + (i - 1)] * 4 + 1
Bij = [(j - 1) * width + (i - 1)] * 4 + 2
Aij = [(j - 1) * width + (i - 1)] * 4 + 3

imageData二维概念图.png

动画

canvas的动画主要是通过 在一些定时方法中去执行重绘操作实现的。

但为什么要重绘呢?

canvas的图像一旦绘制出来,它就是一直保持那样了。

∴ 如果需要移动它,我们不得不对所有东西(包括之前的)进行重绘

而动画是一帧一帧连续绘制形成的,所以canvas实现动画的过程通常是 清理->绘制->清理->绘制... 不断重复的过程。

又为什么要在定时方法执行呢?

∵ 参考浏览器的事件循环机制,同步代码仅仅在脚本执行结束后才能看见结果,比如在for循环里完成动画是不太可能的。

∴ 我们需要通过一些定时执行的方法去调用重绘,实现动画的操控。如 setTimeOut、setInterval、requestAnimationFrame。

关于事件循环和定时器,可以看我之前的文章或者其他大佬的文章。

实现动画的步骤

可以通过以下步骤实现动画的一帧:
1.清空 canvas 除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。最简单的做法就是用 clearRect 方法。
2.保存 canvas 状态 如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。
3.绘制动画图形(animated shapes)  这一步才是重绘动画帧。
4.恢复 canvas 状态 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。

结语🎉

canvas在图形、动画、可视化等领域有出色的表现,入门即可轻松实现许多传统开发难以实现或实现复杂的场景,是前端扩展技术栈的不二之选。

第一次写基础入门文章,感觉对结构和节奏的把握还是不够😥。写到中途感到内容过多难以掌控,故省略了部分过于精细、乏味或偏僻的内容。

后续会持续更新canvas的进阶使用,欢迎大家关注第一时间收到更新消息哦😊

写作不易,如果觉得有收获还望大家点赞、收藏🌹

才疏学浅,如果对canvas的用法和知识有更多理解欢迎大家指教。