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》

相關文章
相關標籤/搜索