前言
去年在公司內部做了一次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/CodeLittleP…
截圖
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,假設影像寬度為2*2,那麼點(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 - 粒子動畫
三要素
- 粒子物件化
- 緩動函式
- 效能
粒子物件化已經介紹過了。 緩動函式,在之前的遊戲也提及過,是為了讓動畫更加的自然生動。 效能是一個很需要關注的問題。因為比如一張500x500的圖片,那資料量就是500x500x4=1000000。動畫藉助了requestAnimationFrame,正常的情況下一般重新整理頻率在60HZ,能展現非常流暢的動畫。但現在要處理這麼大的資料量,瀏覽器抗不過來了,自然造成了降頻,導致動畫卡幀嚴重。
為了效能,粒子動畫往往採用選擇性的選取畫素用來繪製。比如,只繪製原圖x座標為偶數,或能被4等整除的畫素。比如,只繪製原圖對應畫素r色值為155以上的畫素。
結合上面的思路,就可以做出各種強大的例子動畫啦。