canvas-深刻与应用秘籍

前言

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

原文出处

《canvas-深刻与应用秘籍》html

介绍

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。
接下来,我将介绍不少案例,把本身能想到的都列举出来,而且,结合其原理,为同窗们一一介绍。java

应用案例

案例以下:git

  • 动画
  • 游戏
  • 视频(由于生产环境还不成熟,略)
  • 截图
  • 合成图
  • 分享网页截图
  • 滤镜
  • 抠图
  • 旋转、缩放、位移、形变
  • 粒子

动画

API介绍

requestAnimationFrame

该方法告诉浏览器您但愿执行动画并请求浏览器在下一次重绘以前调用指定的函数来更新动画。
该方法使用一个回调函数做为参数,这个回调函数会在浏览器重绘以前调用。github

requestAnimationFrame 优势

1.避免掉帧
彻底依赖浏览器的绘制频率,从而避免过分绘制,影响电池寿命。
2.提高性能
当Tab或隐藏的iframe里,暂停调用。算法

Demo

方块移动canvas

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

相关文章
相关标签/搜索