Canvas 从进阶到退学

本文简介

点赞 + 关注 + 收藏 = 学会了


接着 《Canvas 从入门到劝朋友放弃(图解版)》 ,本文继续补充 canvas 基础知识点。

这次我不手绘了!


本文会涉及到 canvas 的知识包括:变形、像素控制、渐变、阴影、路径



变形

这里说的变形是基于画布,全局进行变形。

变形主要包括:平移 translate缩放 scale旋转操作 rotate

除了对应的方法外,还可以使用 transformsetTransform 对上面三种操作进行配置,这称为“变换矩阵”。


在学习“变形”之前,需要了解 W3C坐标系

箭头所指是各轴自己的正方向,x轴越往右(正方向)值越大,y轴越往下(正方向)值越大。


平移

使用 translate() 方法可以实现平移效果(位移)。

translate(x, y) 接收2个参数,第一个参数代表x轴方向位移距离,第二个参数代表y轴方向位移距离。

正数代表向正方向位移,负数代表向反方向位移。


演示平移效果之前,我先创建一个矩形,长宽都是100,位置在画布的原点 (0, 0) ,也就是紧贴画布的左上角。




如果此时在 fillRect 之前设置 translate 就可以实现整个画布位移的效果。

// 省略部分代码

// 平移,往右平移10,往下平移20
ctx.translate(10, 20)

// 渲染矩形
ctx.fillRect(0, 0, 100, 100)

从上图可以看出,矩形距离画布顶部的距离是20,距离画布左侧的距离是10。


注意:平移 translate() 要写在 “绘制操作(本例是 fillRect)” 之前才有效。


如果在使用 translate 的前后都有渲染操作,画布会多次渲染,并不会自动清屏。

比如这样




再做个明显点的效果,每秒平移一次



可以看出,每次使用 translate() 平移画布,都会基于上一次画布所在的位置进行平移。


上图效果是 canvas 的默认效果,所以在执行 translate 之前可以执行 “清屏操作”


清屏




缩放

缩放画布用到的方法是 scale(x, y) ,接收2个参数,第一个参数是x轴方向的缩放,第二个参数是y轴方向的缩放。

x 或者 y 的值是 0 ~ 1 时代表缩小,比如取值为 0.5 时,表示比原本缩小一半;值为2时,比原本放大一倍。



scale() 方法同样会保留原本已经渲染的内容。

如果不需要保留原本内容,可以使用 “清屏操作”

注意:scale() 会以上一次缩放为基准进行下一次缩放。


副作用:

其实从上面的例子就可以看出 scale() 存在一点副作用的,从图中可以看出,缩放后文本的左上角坐标发生了“位移”,文本描边粗细也发生了变化。

虽然说是副作用,但也很容易理解,整块画布缩放了,对应的坐标比例其实也跟着缩放嘛。


旋转

使用 rotate(angle) 方法可以旋转画布,但默认的旋转原点是画布的左上角,也就是 (0, 0) 坐标。

我计算旋转角度通常是用 角度 * Math.PI / 180 的方式表示。

虽然这样书写代码看上去很长,但习惯后就比较直观的看出要旋转多少度。

rotate(angle) 中的参数 angle 代表角度,angle 的取值范围是 -Math.PI * 2 ~ Math.pi * 2

当旋转角度小于 0 时,画布逆时针旋转;反之顺时针旋转。




修改原点

如果需要修改旋转中心,可以使用 translate() 方法平移画布,通过计算移动到指定位置。




变换矩阵

变换矩阵常用方法有 transform()setTransform() 两个方法。

变换矩阵是一个稍微进阶一点的知识了,别怕!

前面的 平移 translate缩放 scale旋转操作 rotate 可以说都是 transform() 的 “语法糖”。

变换矩阵已经涉及到一点数学知识了,但本文不会讲到这些知识,只会讲讲 transform() 是怎么用的。


transform

transform() 一个方法就可以实现 平移、缩放、旋转 三种功能,它接收6个参数。

transform(a, b, c, d, e, f)

  • a: 水平缩放(x轴方向),默认值是 1;
  • b: 水平倾斜(x轴方向),默认值是 0;
  • c: 垂直倾斜(y轴方向),默认值是 0;
  • d: 垂直缩放(y轴方向),默认值是 1;
  • e: 水平移动(x轴方向),默认值是 0;
  • f: 垂直移动(y轴方向),默认值是 0;


这默认值看上去很乱,但如果这样排列一下是不是就比较容易理解了:

$$ \begin{pmatrix}a & c & e \\\\ b & d & f \\\\ 0 & 0 & 1 \end{pmatrix} $$


随便修改几个值试试效果:




setTransform

setTransform(a, b, c, d, e, f) 同样接收6个参数,和 transform() 一样




transform 和 setTransform 的区别

transform() 每次执行都会参考上一次变换后的结果

比如下面这个多次执行的情况:




setTransform() 每次调用都会基于最原始是状态进行变换。



不管改变多少次,setTransform() 都会参考原始状态进行变换。



控制像素

位图是由像素点组成的,canvas 提供了几个 api 可以操作图片中的像素。

很多工具网站也常用接下来说到的几个 api 做图片滤镜。


需要注意的是,canvas 提供的操作像素的方法,必须使用服务器才能运行起来,不然没有效果的。

可以搭建本地服务器运行本文案例,方法有很多种。

比如你使用 Vue 或者 React 的脚手架搭建的项目,运行后就能跑起本文所有案例。

又或者使用 http-server 启动本地服务。


getImageData()

首先要介绍的是 getImageData() 方法,这个方法可以获取指定区域内的所有像素。


getImageData(x, y, width, height) 接收4个参数,这4个参数表示选区范围。

xy 代表选区的左上角坐标,width 表示选区宽度,height 表示选区高度。


还是举例说明比较清楚。下图渲染到画布上的是我的猫Bubble



打印出来的信息可以点开大图看看

  • data: 图片像素数据集,以数组的形式存放,这是本文要讲的重点,需要关注!
  • colorSpace: 图片使用的色彩标准,这个属性在 Chrome 里有打印出来,Firefox 里没打印。不重要~
  • height: 图片高度
  • width: 图片宽度


通过 getImageData() 获取到的信息中,需要重点关注的是 data ,它是一个一维数组,仔细观察发现里面的值没一个是大于255的,也不会小于0。

其实 data 属性里记录了图片每个像素的 rgba 值分别是多少。

  • r 代表红色
  • g 代表绿色
  • b 代表蓝色
  • a 透明度


这个和 CSS 里的 rgba 是同一个意思。

data 里,4个元素记录1个像素的信息。也就是说,1个像素是由 rgba 4个元素组成。而且每个元素的取值范围是 0 - 255 的整数。

 data: **[r1, g1, b1, a1, r2, g2, b2, a2, ......]** 
像素点 颜色通道
imgData.data[0] 49 红色 r
imgData.data[1] 47 绿色 g
imgData.data[2] 51 蓝色 b
imgData.data[3] 255 透明度 a
…… …… ……
imgData.data[n-4] 206 红色 r
imgData.data[n-2] 200 绿色 g
imgData.data[n-3] 200 蓝色 b
imgData.data[n-1] 255 透明度 a


如果一张图只有10个像素,通过 getImageData() 获取到的 data 信息中就有40个元素。


putImageData()

putImageData(imageData, x, y) 可以将 ImageData 对象的数据(图片像素数据)绘制到画布上。

putImageData(imgData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) 也可以接收更多参数。

  • imageData: 规定要放回画布的 ImageData 对象
  • x: ImageData 对象左上角的 x 坐标,以像素计
  • y: ImageData 对象左上角的 y 坐标,以像素计
  • dirtyX: 可选。水平值(x),以像素计,在画布上放置图像的位置
  • dirtyY: 可选。水平值(y),以像素计,在画布上放置图像的位置
  • dirtyWidth: 可选。在画布上绘制图像所使用的宽度
  • dirtyHeight: 可选。在画布上绘制图像所使用的高度


比如,我要将图片复制到另一个位置



可以实现复制的效果。


透明

知道前面两个 api 就可以实现透明效果了。

前面讲到,通过 getImageData() 获取的是一个数组类型的数据,每4个元素代表1个像素,就是rgba,而 a 表示透明通道,所以只需修改每组像素的最后1个元素的值,就能修改图片的不透明度。




滤镜

要做不同的滤镜效果,其实就是通过不同的算法去操作每个像素的值,我在 《Canvas 10款基础滤镜(原理篇)》 讲到相关知识,有兴趣的工友可以点进去看看



渐变

csssvg 里都有渐变,canvas 肯定也不会缺失这个能力啦。

canvas 提供了 线性渐变 createLinearGradient径向渐变 createRadialGradient


线性渐变 createLinearGradient

canvas 中使用线性渐变步骤如下:

  1. 创建线性渐变对象: createLinearGradient(x1, y1, x2, y2)
  2. 添加渐变颜色: addColorStop(stop, color)
  3. 设置填充色或描边颜色: fillStylestrokeStyle


createLinearGradient(x1, y1, x2, y2)

createLinearGradient(x1, y1, x2, y2) 中,x1, y1 表示渐变的起始位置,x2, y2 表示渐变的结束位置。

比如水平方向的从左往右的线性渐变,此时的 y1y2 的值是一样的。

两个点就可以确定一个渐变方向。


addColorStop(stop, color)

addColorStop(stop, color) 方法可以添加渐变色。

第一个参数 stop 表示渐变色位置的偏移量,取值范围是 0 ~ 1。

第二个参数 color 表示颜色。


填充渐变

实际编码演示一下




如果想修改渐变的方向,只需在使用 createLinearGradient() 时设置好起点和终点坐标即可。


除了填充色,描边渐变和文本渐变同样可以做到。

描边渐变




文本渐变




多色线性渐变

在 0 ~ 1 的范围内,addColorStop 可以设置多个颜色在不同的位置上。

// 省略部分代码
lgrd.addColorStop(0, '#2a9d8f') // 绿色
lgrd.addColorStop(0.5, '#e9c46a') // 黄色
lgrd.addColorStop(1, '#f4a261') // 橙色


径向渐变 createRadialGradient

径向渐变是从一个点到另一个点扩散出去的渐变,是圆形(椭圆也可以)渐变。

直接看效果



createRadialGradient 可以创建一个径向渐变的对象。使用步骤和 createLinearGradient 一样,但参数不同。

createRadialGradient(x1, y1, r1, x2, y2, r2)

  • x1, y1: 渐变开始的圆心坐标
  • r1: 渐变开始的圆心半径
  • x2, y2: 渐变结束的圆心坐标
  • r2: 渐变结束的圆心半径


同样使用 addColorStop 设置渐变颜色,同样支持多色渐变。


渐变的注意事项

渐变的定位坐标是参照画布的,超出定位的部分会使用最临近的那个颜色。

我用线性渐变举例。



上面的例子中,我只创建了一个渐变,然后创建了9个正方形。

此时正方形的填充色取决于出现在画布中的位置。

可以修改一下 createLinearGradient() 的定位数据对照理解。

// 省略部分代码
const lgrd = ctx.createLinearGradient(200, 0, 400, 400)


如果想每个图形都有自己的渐变色,这需要定制化配置,每个创建每个图形之前都单独创建一个渐变色。




所以不管是填充色还是秒变颜色,每个元素最好都自己重新设定一下。不然可能会出现意想不到的效果~



阴影

阴影在前端也是很常用的特效。 依稀记得当年还用 png 做阴影效果

canvas 中,和阴影相关的属性主要有以下4个:

  • shadowOffsetX: 设置或返回阴影与形状的水平距离。默认值是0。大于0时向正方向偏移。
  • shadowOffsetY: 设置或返回阴影与形状的垂直距离。默认值是0。大于0时向正方向偏移。
  • shadowColor: 设置或返回用于阴影的颜色。 默认黑色。
  • shadowBlur: 设置或返回用于阴影的模糊级别。 默认值是0,数值越大模糊度越强。


相信使用过 css 阴影属性的工友,理解起 canvas 阴影也会非常轻松。




除了图形外,文本和图片都可以设置阴影效果。





路径

Canvas 从入门到劝朋友放弃(图解版) —— 新开路径 中我讲到 新开路径关闭路径 的用法,本节会在上篇的基础上丰富更多使用细节。

本节要讲的是

  • beginPath(): 新开路径
  • closePath(): 关闭路径
  • isPointInPath(): 判断某个点是否在当前路径内


beginPath()

beginPath() 方法是用来开辟一条新的路径,这个方法会将当前路径之中的所有子路径都清除掉,以此来重置当前路径。


如果你的画布上有几个基础图形(直线、多边形、圆形、弧、贝塞尔曲线),如果样式相互之间受到影响,那你可以立刻想想在绘制新图形之前是不是忘了使用 beginPath()

先举几个例子说明一下。


污染:颜色、线条粗细受到污染

后面的样式覆盖了前面的样式。




污染:图形路径污染

比如画布上有一条直线和一个圆形,不使用 beginPath() 开辟新路径的话,它们可能会“打架”。



明明直线和圆形是没有交集的,为什么会有一条倾斜的线把两个元素连接起来?


解决办法

除了上面两种情况外,可能还有其他更加奇怪的情况(像极喝醉了假酒),都可以先考虑是不是要使用 beginPath()

比如这样做。



在使用 arc 或者 moveTo 方法之前加上一句 ctx.beginPath() 就可以有效解决以上问题。

这个例子中,如果没用 ctx.beginPath()canvas 就会以为 线 和 圆形 都属于同一个路径,所以在画圆形时,下笔的时候就会把线的“结束点”和圆的“起点”相连起来。


stroke()fill() 都是以最近的 beginPath() 后面所定义的状态样式为基础进行绘制的。


注意事项

前面的样式会覆盖后面元素的默认样式,即使使用了 beginPath()



第一条先设置了 strokeStylelineWidth ,第二条线并没有设置这两个属性,即使在绘制第二条线的开始时使用了 ctx.beginPath() ,第二条线也会使用第一条线的 strokeStylelineWidth 。除非第二条线自己也有设置这两个属性,不然就会沿用之前的配置项。


"特殊情况"

还要补充一个 “特殊情况”。



这个例子中,绘制矩形 rect 前并没有用 beginPath() ,但矩形的红色描边并没有影响直线的粉色描边。

其实还不止 strokeRect() ,还有 fillRect()strokeText()fillText() 都不会影响其他图形,这些方法都只会绘制图形,不会影响原本路径。


closePath()

closePath() 方法可以关闭当前路径,它可以显示封闭某段开放的路径。这个方法常用于关闭圆弧路径或者由圆弧、线段创建出来的开放路径。

closePath() 是关闭路径,并不是结束路径。

关闭路径,指的是连接起点与终点,也就是能够自动封闭图形。

结束路径,指的是开始新的路径。


基础用法

举个例子会更直观



上面的代码通过 moveTolineTo 画了3个点,使用 stroke() 方法把这3个点连起来,就形成了上图效果。

但如果此时在 stroke() 前使用 closePath() 方法,最终出来的路径将自动闭合(将起点和终点连接起来)。




注意事项

看到上面的例子后,可能有些工友会觉得使用 ctx.lineTo(50, 40) 连接回起点也有同样效果。

// 省略部分代码
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.lineTo(50, 40)
ctx.stroke()

确实在描边为1像素时看上去效果是差不多的,但如果此时将 lineWidth 的值设置得大一点,就能看到明显区别。

// 省略部分代码
ctx.lineWidth = 10
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.lineTo(50, 40) // 连接回起点
ctx.stroke()

如果用 closePath() 自动闭合路径的话,会出现以下效果

// 省略部分代码
ctx.lineWidth = 10
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.closePath() // 关闭路径
ctx.stroke()



本文到此就完结了,但 canvas 的知识点还没完,还有很多很多,根本学不完的那种。

接下来 本专栏 的文章会偏向于 知识点 + 案例 的方式讲解 canvas



代码仓库

雷猴 Canvas



推荐阅读

《Canvas 从入门到劝朋友放弃(图解版)》

《Canvas 10款基础滤镜(原理篇)》

《Fabric.js 从入门到膨胀》

《『Three.js』起飞!》

《p5.js 光速入门》

《SVG 从入门到后悔,怎么不早点学起来(图解版)》


点赞 + 关注 + 收藏 = 学会了
代码仓库

你可能感兴趣的:(前端)