前言
去年在公司内部做了一次 canvas 的分享,或者说 canvas 总结会更为贴切,但由于一直都因为公事或者私事,一直没有把东西总结成文章分享给大家,实在抱歉~ 分享这篇文章的目的是为了让同学们对 canvas 有一个全面的认识,废话不多说,开拔!
原文出处
《canvas- 深入与应用秘籍》
介绍
Canvas 是一个可以使用脚本(通常为 Javascript,其它比如 Java Applets or JavaFX/JavaFX Script)来绘制图形,默认大小为 300 像素×150 像素的 HTML 元素。
<canvas style=”background: purple;”></canvas>
小试牛刀
<!– canvas –>
<canvas id=”canvas”></canvas>
<!– javascript –>
<script>
const canvas = document.getElementById(‘canvas’)
const ctx = canvas.getContext(‘2d’)
ctx.fillStyle = ‘purple’
ctx.fillRect(0, 0, 300, 150)
</script>
经过了以上地狱般的学习,我相信同学们现在已精通 canvas。接下来,我将介绍很多案例,把自己能想到的都列举出来,并且,结合其原理,为同学们一一介绍。
应用案例
案例如下:
动画
游戏
视频(因为生产环境还不成熟,略)
截图
合成图
分享网页截图
滤镜
抠图
旋转、缩放、位移、形变
粒子
动画
API 介绍
requestAnimationFrame
该方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。
requestAnimationFrame 优点
1. 避免掉帧完全依赖浏览器的绘制频率,从而避免过度绘制,影响电池寿命。2. 提升性能当 Tab 或隐藏的 iframe 里,暂停调用。
Demo
方块移动
<!– canvas –>
<canvas id=”canvas” width=”600″ height=”600″></canvas>
<!– javascript –>
<script>
const canvas = document.getElementById(‘canvas’)
const ctx = canvas.getContext(‘2d’)
ctx.fillStyle = ‘purple’
const step = 1 // 每步的长度
let xPosition = 0 // x 坐标
move() // call move
function move() {
ctx.clearRect(0, 0, 600, 600)
ctx.fillRect(xPosition, 0, 300, 150)
xPosition += step
if (xPosition <= 300) {
requestAnimationFrame(() => {
move()
})
}
}
</script>
游戏
三要素
个人做游戏总结的三要素:
对象抽象
requestAnimationFrame
缓动函数
对象抽象:即对游戏中角色的抽象,面向对象的思维在游戏中非常地普遍。举个例子,我们来抽象一个《勇者斗恶龙》里的史莱姆:
class Slime {
constructor(hp, mp, level, attack, defence) {
this.hp = hp
this.mp = mp
this.level = level
this.attack = attack
this.defence = defence
}
bite() {
return this.attack
}
fire() {
return this.attack * 2
}
}
requestAnimationFrame:之前我们已经接触过这个 API 了,结合上面动画的例子,我们很容易自然的就能想到,游戏动起来的原理了。
缓动函数:我们知道,匀速运动的动画会显得非常不自然,要变得自然就得时而加速,时而减速,那样动画就会变得更加灵活,不再生硬。
Demo
有兴趣的同学可以看我以前写的小游戏。项目地址:(github.com/CodeLittlePrince/FishHeart)[https://github.com/CodeLittle…]
截图
API 介绍
drawImage(image, sx, sy [, sWidth, sHeight [, dx, dy, dWidth, dHeight]])
绘制图像方法。
toDataURL(type, encoderOptions)
方法返回一个包含图片展示的 data URI。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为 96dpi。注意:
该方法必须在 http 服务下
非同源的图片需要 CORS 支持,图片设置 crossOrigin =“”(只要 crossOrigin 的属性值不是 use-credentials,全部都会解析为 anonymous,包括空字符串,包括类似 ’abc’ 这样的字符)
canvas.style.width 和 canvas.width 的区别
把 canvas 元素比作画框:canvas.width 则是控制画框尺寸的方式。canvas.style.width 则是控制在画框中的画尺寸的方式。
Demo
核心代码
const captureResultBox = document.getElementById(‘captureResultBox’)
const captureRect = document.getElementById(‘captureRect’)
const style = window.getComputedStyle(captureRect)
// 设置 canvas 画布大小
canvas.width = parseInt(style.width)
canvas.height = parseInt(style.height)
// 画图
const x = parseInt(style.left)
const y = parseInt(style.top)
const w = parseInt(img.width)
const h = parseInt(img.height)
ctx.drawImage(img, x, y, w, h, 0, 0, w, h)
// 将图片 append 到 html 中
const resultImg = document.createElement(‘img’)
// toDataURL 必须在 http 服务中
resultImg.src = canvas.toDataURL(‘image/png’, 0.92)
合成图
原理
回看之前的例子,我们知道了 drawImage 可以自己画图画,也可以画图片。canvas 完全就是个画板,可任由我们发挥。合成的思路其实就是把多张图片都画在同一个画布 (cavans) 里。是不是一下子就知道接下来怎么做啦?
Demo
核心代码
// 设置画布大小
canvas.width = bg.width
canvas.height = bg.height
// 画背景
ctx.drawImage(bg, 0, 0)
// 画第一个角色
ctx.drawImage(
character1, 100, 200,
character1.width / 2,
character1.height / 2
)
// 画第二个角色
ctx.drawImage(
character2, 500, 200,
character2.width / 2,
character2.height / 2
)
如图,背景是一深夜无人后院,然后去网上搜两张背景透明的角色图片,再将两张图一次画到画布上就成了合成图啦。
分享网页截图
原理
拿比较出名的 html2canvas 为例,实现方式就是遍历整个 dom,然后挨个拉取样式,在 canvas 上一个个地画出来。
Demo
滤镜
API 介绍
getImageData(sx, sy, sw, sh)
返回一个 ImageData 对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为 sw、高为 sh。看段代码:
const img = document.createElement(‘img’)
img.src = ‘./filter.jpg’
img.addEventListener(‘load’, () => {
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
console.log(ctx.getImageData(0, 0, canvas.width, canvas.height))
})
它会打印出如下数据:
有点迷?不慌,接下去看。
数据类型介绍
Uint8ClampedArray
8 位无符号整型固定数组)类型化数组表示一个由值固定在 0 -255 区间的 8 位无符号整型组成的数组;如果你指定一个在 [0,255] 区间外的值,它将被替换为 0 或 255;如果你指定一个非整数,那么它将被设置为最接近它的整数。(数组)内容被初始化为 0。一旦(数组)被创建,你可以使用对象的方法引用数组里的元素,或使用标准的数组索引语法(即使用方括号标记)。回看这张图:data 里其实就是像素,按每 4 个为一组成为一个像素。4 个一组,难道是 rgba?(o ゜▽゜)o☆[BINGO!]这样的话,图片的宽 x 高 x4(w h 4)就是所有像素的总和,刚好就死 data 的 length。
数学推导
已知:924160 = 640 x 316 x 4
可知:数组的长度为 length = canvas.width x canvas.height x 4
知道了这种关系,我们不妨把这个一维数组想象成二维数组,想象它是一个平面图,如图:
一个格子代表一个像素 w = 图像宽度 h = 图像高度这样,我们可以很容易得到点 (x, y) 在一维数组中对应的位置。我们想一想,点 (1, 1) 坐标对应的是数组下标为 0,点 (2, 1) 对应的是数组下标 4,假设图像宽度为 22,那么点 (1,2) 对应下标就是 index=((2 – 1)w + (1 – 1))*4 = 8。推导出公式:index = [(y – 1) w + (x – 1) ] 4
继续 API 介绍
createImageData(width, height)
createImageData 是在 canvas 在取渲染上下文为 2D(即 canvas.getContext(‘2d’))的时候提供的接口。作用是创建一个新的、空的、特定尺寸的 ImageData 对象。其中所有的像素点初始都为黑色透明。并返回该 ImageData 对象。
putImageData
putImageData 方法作为 canvas 2D API 以给定的 ImageData 对象绘制数据进位图。如果提供了脏矩形,将只有矩形的像素会被绘制。这个方法不会影响 canvas 的形变矩阵。
这小节我们学了好几个新 API,然后重新理了理数学知识。同学们好好消化完以后,就进 Demo 阶段吧。
Demo
核心代码: 最终效果:
抠图
对于纯背景抠图,其实还是比较简单的。上面我们已经说过,我们可以拿到整个 canvas 的每个像素点的值了。所以,只需要把纯色的色值转为透明就好了。但这种场景不多,因为,背景很少有纯色的情况,而且即使背景纯色,不保证被扣对象的身上没有和背景同色值的情况。所以,如果要处理复杂的情况,还是建议后端来做比较好,后端早已有了成熟的图像处理解决方案,比如 opencv 等。像美图的话,有专门的图像算法团队,天天研究这方面。接下来,我将介绍下美图人像抠图的思路。
属性介绍
globalCompositeOperation
控制 drawImage 的绘制图层先后顺序。
思路
我们将使用 souce-in 这个属性。如上图所示,这个属性的作用是,两图叠加,只取叠加的部分。为什么这样搞?不是说好了,美图是让后端算法大佬们处理吗?因为,为了人像抠图适应更多的场景,算法大佬们只会把人物图像处理成一个蒙版图并返给前端,之后让前端自己处理。我们看下原图:
再看下后端返给的蒙版图:
得到以上的蒙版图以后,先把黑色处理成透明;先在 canvas 上 draw 原图;再把 globalCompositeOperation 设置为 ‘source-in’;然后再 draw 处理后的蒙版图;得到的就是最后的抠图啦!这个方案是咨询前美图大佬 @xd-tayde 的,感谢~
Demo
处理结果:
旋转、缩放、位移、形变
对于旋转、缩放、位移、形变,canvas 的上下文 ctx 有对应的 API 可以调用,也可以用 martrix 方式做更高级的变化。因为涉及的内容很多,如果全写这的话,篇幅太大。所以,我这里直接推荐一篇文章给同学们学习 ——《canvas 图像旋转与翻转姿势解锁》
粒子
抽象
之前我们就知道了,我们可以获取 canvas 上的每个像素点。所谓的粒子,其实算是对一个像素的抽象。它具有自己坐标,自己的色值,可以通过改变自身的属性“动”起来。因此我们不妨将粒子作为一个对象来看待,它有坐标和色值,如:
let particle = {
x: 0,
y: 0,
rgba: ‘(1, 1, 1, 1)’
}
Demo – 小试牛刀
我将把一张网易支付的 logo 图,用散落的粒子重新画出来。核心代码:
// 获取像素颜色信息
const originImageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const originImageDataValue = originImageData.data
const w = canvas.width
const h = canvas.height
let colors = []
let index = 0
for (let y = 1; y <= h; y++) {
for (let x = 1; x <= w ; x++) {
const r = originImageDataValue[index]
const g = originImageDataValue[index + 1]
const b = originImageDataValue[index + 2]
const a = originImageDataValue[index + 3]
index += 4
// 将像素位置打乱,保存进返回数据中
colors.push({
x: x + getRandomArbitrary(-OFFSET, OFFSET),
y: y + getRandomArbitrary(-OFFSET, OFFSET),
color: `rgba(${r}, ${g}, ${b}, ${a})`
})
}
效果:
Demo – 粒子动画
三要素
粒子对象化
缓动函数
性能
粒子对象化已经介绍过了。缓动函数,在之前的游戏也提及过,是为了让动画更加的自然生动。性能是一个很需要关注的问题。因为比如一张 500×500 的图片,那数据量就是 500x500x4=1000000。动画借助了 requestAnimationFrame,正常的情况下一般刷新频率在 60HZ,能展现非常流畅的动画。但现在要处理这么大的数据量,浏览器抗不过来了,自然造成了降频,导致动画卡帧严重。
为了性能,粒子动画往往采用选择性的选取像素用来绘制。比如,只绘制原图 x 坐标为偶数,或能被 4 等整除的像素。比如,只绘制原图对应像素 r 色值为 155 以上的像素。
结合上面的思路,就可以做出各种强大的例子动画啦。
Demo
所有 Demo 项目地址
github.com/CodeLittlePrince/canvas-tutorial
参考文章
《打造高大上的 Canvas 粒子动画 – 腾讯 ISUX》