76

前言

去年在公司内部做了一次canvas的分享,或者说canvas总结会更为贴切,但由于一直都因为公事或者私事,一直没有把东西总结成文章分享给大家,实在抱歉~
分享这篇文章的目的是为了让同学们对canvas有一个全面的认识,废话不多说,开拔!

原文出处

《canvas-深入与应用秘籍》

介绍

Canvas是一个可以使用脚本(通常为Javascript,其它比如 Java Applets or JavaFX/JavaFX Script)来绘制图形,默认大小为300像素×150像素的HTML元素。

<canvas style="background: purple;"></canvas>

clipboard.png

小试牛刀

<!-- 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>

clipboard.png

经过了以上地狱般的学习,我相信同学们现在已精通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>

clipboard.png

游戏

三要素

个人做游戏总结的三要素:

  • 对象抽象
  • 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

clipboard.png

有兴趣的同学可以看我以前写的小游戏。
项目地址:(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 的区别

1551971556019
1551971506031
把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)

clipboard.png

合成图

原理

回看之前的例子,我们知道了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
  )

clipboard.png

如图,背景是一深夜无人后院,然后去网上搜两张背景透明的角色图片,再将两张图一次画到画布上就成了合成图啦。

分享网页截图

原理

拿比较出名的html2canvas为例,实现方式就是遍历整个dom,然后挨个拉取样式,在canvas上一个个地画出来。

Demo

clipboard.png

滤镜

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))
})

它会打印出如下数据:
1551975836754 1

有点迷?不慌,接下去看。

数据类型介绍

Uint8ClampedArray

8位无符号整型固定数组) 类型化数组表示一个由值固定在0-255区间的8位无符号整型组成的数组;如果你指定一个在 [0,255] 区间外的值,它将被替换为0或255;如果你指定一个非整数,那么它将被设置为最接近它的整数。(数组)内容被初始化为0。一旦(数组)被创建,你可以使用对象的方法引用数组里的元素,或使用标准的数组索引语法(即使用方括号标记)。
回看这张图:
1551975836754 1
data里其实就是像素,按每4个为一组成为一个像素。
4个一组,难道是rgba?
(o゜▽゜)o☆[BINGO!]
这样的话,图片的宽x高x4(w h 4 )就是所有像素的总和,刚好就死data的length。

数学推导

已知:924160 = 640 x 316 x 4

clipboard.png

可知:数组的长度为length = canvas.width x canvas.height x 4

知道了这种关系,我们不妨把这个一维数组想象成二维数组,想象它是一个平面图,如图:

clipboard.png

一个格子代表一个像素
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

核心代码:
1551977094061 1
最终效果:
1551977197249 1 1 1

抠图

对于纯背景抠图,其实还是比较简单的。上面我们已经说过,我们可以拿到整个canvas的每个像素点的值了。所以,只需要把纯色的色值转为透明就好了。
但这种场景不多,因为,背景很少有纯色的情况,而且即使背景纯色,不保证被扣对象的身上没有和背景同色值的情况。
所以,如果要处理复杂的情况,还是建议后端来做比较好,后端早已有了成熟的图像处理解决方案,比如opencv等。像美图的话,有专门的图像算法团队,天天研究这方面。
接下来,我将介绍下美图人像抠图的思路。

属性介绍

globalCompositeOperation

控制drawImage的绘制图层先后顺序。

clipboard.png

思路

我们将使用souce-in这个属性。如上图所示,这个属性的作用是,两图叠加,只取叠加的部分。
为什么这样搞?不是说好了,美图是让后端算法大佬们处理吗?
因为,为了人像抠图适应更多的场景,算法大佬们只会把人物图像处理成一个蒙版图并返给前端,之后让前端自己处理。
我们看下原图:

clipboard.png

再看下后端返给的蒙版图:

clipboard.png

得到以上的蒙版图以后,先把黑色处理成透明;
先在canvas上draw原图;
再把globalCompositeOperation 设置为 'source-in';
然后再draw处理后的蒙版图;
得到的就是最后的抠图啦!
这个方案是咨询前美图大佬@xd-tayde的,感谢~

Demo

处理结果:

clipboard.png

旋转、缩放、位移、形变

对于旋转、缩放、位移、形变,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})`
      })
    }

效果:

clipboard.png

Demo - 粒子动画

三要素

  • 粒子对象化
  • 缓动函数
  • 性能

粒子对象化已经介绍过了。
缓动函数,在之前的游戏也提及过,是为了让动画更加的自然生动。
性能是一个很需要关注的问题。因为比如一张500x500的图片,那数据量就是500x500x4=1000000。动画借助了requestAnimationFrame,正常的情况下一般刷新频率在60HZ,能展现非常流畅的动画。但现在要处理这么大的数据量,浏览器抗不过来了,自然造成了降频,导致动画卡帧严重。

为了性能,粒子动画往往采用选择性的选取像素用来绘制。比如,只绘制原图x坐标为偶数,或能被4等整除的像素。比如,只绘制原图对应像素r色值为155以上的像素。

结合上面的思路,就可以做出各种强大的例子动画啦。

Demo

particle

所有Demo项目地址

github.com/CodeLittlePrince/canvas-tutorial

参考文章

《打造高大上的 Canvas 粒子动画 - 腾讯 ISUX》


岁月是把杀猪刀
1.6k 声望1.4k 粉丝

もっと遠くにあるはずの、とこか、僕はそこに行きたいんだ