本文简介
点赞 + 关注 + 收藏 = 学会了
接着 《Canvas 从入门到劝朋友放弃(图解版)》 ,本文继续补充 canvas
基础知识点。
这次我不手绘了!
本文会涉及到 canvas
的知识包括:变形、像素控制、渐变、阴影、路径
变形
这里说的变形是基于画布,全局进行变形。
变形主要包括:平移 translate
、缩放 scale
、旋转操作 rotate
。
除了对应的方法外,还可以使用 transform
和 setTransform
对上面三种操作进行配置,这称为“变换矩阵”。
在学习“变形”之前,需要了解 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个参数表示选区范围。
x
和 y
代表选区的左上角坐标,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个像素是由 r
、g
、b
、a
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款基础滤镜(原理篇)》 讲到相关知识,有兴趣的工友可以点进去看看
渐变
在 css
和 svg
里都有渐变,canvas
肯定也不会缺失这个能力啦。
canvas
提供了 线性渐变 createLinearGradient
和 径向渐变 createRadialGradient
。
线性渐变 createLinearGradient
在 canvas
中使用线性渐变步骤如下:
- 创建线性渐变对象:
createLinearGradient(x1, y1, x2, y2)
- 添加渐变颜色:
addColorStop(stop, color)
- 设置填充色或描边颜色:
fillStyle
或strokeStyle
createLinearGradient(x1, y1, x2, y2)
在 createLinearGradient(x1, y1, x2, y2)
中,x1, y1
表示渐变的起始位置,x2, y2
表示渐变的结束位置。
比如水平方向的从左往右的线性渐变,此时的 y1
和 y2
的值是一样的。
两个点就可以确定一个渐变方向。
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()
。
第一条先设置了 strokeStyle
和 lineWidth
,第二条线并没有设置这两个属性,即使在绘制第二条线的开始时使用了 ctx.beginPath()
,第二条线也会使用第一条线的 strokeStyle
和 lineWidth
。除非第二条线自己也有设置这两个属性,不然就会沿用之前的配置项。
"特殊情况"
还要补充一个 “特殊情况”。
这个例子中,绘制矩形 rect
前并没有用 beginPath()
,但矩形的红色描边并没有影响直线的粉色描边。
其实还不止 strokeRect()
,还有 fillRect()
、strokeText()
、fillText()
都不会影响其他图形,这些方法都只会绘制图形,不会影响原本路径。
closePath()
closePath()
方法可以关闭当前路径,它可以显示封闭某段开放的路径。这个方法常用于关闭圆弧路径或者由圆弧、线段创建出来的开放路径。
closePath()
是关闭路径,并不是结束路径。
关闭路径,指的是连接起点与终点,也就是能够自动封闭图形。
结束路径,指的是开始新的路径。
基础用法
举个例子会更直观
上面的代码通过 moveTo
和 lineTo
画了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
。
代码仓库
推荐阅读
点赞 + 关注 + 收藏 = 学会了
代码仓库