剛剛在開車羣看到一個好玩的動圖
正好新年也快到了,給家人做個“過年紅包刮刮樂”的頁面,增加點年味不也挺好。
說做就做,這裏我們用canvas2D來實現效果,核心的API是 CanvasRenderingContext2D.globalCompositeOperation,主要是用來設定圖形繪製前後的圖層混合模式,詳見該頁。簡單加上自己的理解翻譯下:(ps:source 是將要繪製的圖形,destination是指畫布上已存在的圖形)
# source-over
- This is the default setting and draws new shapes on top of the existing canvas content.
- 這是默認值。新圖覆蓋繪製在舊圖上(保留舊圖)
# source-in
- The new shape is drawn only where both the new shape and the destination canvas overlap. Everything else is made transparent.
- 新圖只在與舊圖重疊區域繪製(繪製區域外畫布透明)
# source-out
- The new shape is drawn where it doesn't overlap the existing canvas content.
- 新圖只在與舊圖不重疊區域繪製(繪製區域外畫布透明)
# source-atop
- The new shape is only drawn where it overlaps the existing canvas content.
- 新圖只在與舊圖重疊區域繪製(保留舊圖)
# destination-over
- New shapes are drawn behind the existing canvas content.
- 新圖覆蓋繪製在舊圖底下(保留舊圖)
# destination-in
- The existing canvas content is kept where both the new shape and existing canvas content overlap. Everything else is made transparent.
- 新圖與舊圖的重疊區域作爲蒙版裁剪舊圖(重疊區域外畫布透明)
# destination-out
- The existing content is kept where it doesn't overlap the new shape.
- 新圖與舊圖的非重疊區域作爲蒙版裁剪舊圖(重疊區域外畫布透明)
# destination-atop
- The existing canvas is only kept where it overlaps the new shape. The new shape is drawn behind the canvas content.
- 新圖只在與舊圖的重疊區域繪製且繪製於舊圖下(繪製區域外畫布透明)
# lighter
- Where both shapes overlap the color is determined by adding color values.
- 重疊區域顏色矩陣相加
# copy
- Only the new shape is shown.
- 只顯示新圖
# xor
- Shapes are made transparent where both overlap and drawn normal everywhere else.
- 圖像中,那些重疊和正常繪製之外的其他地方是透明的。
# multiply
- The pixels are of the top layer are multiplied with the corresponding pixel of the bottom layer. A darker picture is the result.
- 重疊區域顏色矩陣相乘
# screen
- The pixels are inverted, multiplied, and inverted again. A lighter picture is the result (opposite of multiply)
- 像素被倒轉,相乘,再倒轉,結果是一幅更明亮的圖片。
# overlay
- A combination of multiply and screen. Dark parts on the base layer become darker, and light parts become lighter.
- multiply和screen的結合,原本暗的地方更暗,原本亮的地方更亮。
# darken
- Retains the darkest pixels of both layers.
- 保留兩個圖層中最暗的像素。
# lighten
- Retains the lightest pixels of both layers.
- 保留兩個圖層中最亮的像素。
# color-dodge
- Divides the bottom layer by the inverted top layer.
- 將底層除以頂層的反置。
# color-burn
- Divides the inverted bottom layer by the top layer, and then inverts the result.
- 將反置的底層除以頂層,然後將結果反過來。
# hard-light
- A combination of multiply and screen like overlay, but with top and bottom layer swapped.
- 屏幕相乘(A combination of multiply and screen)類似於疊加,但上下圖層互換了。
# soft-light
- A softer version of hard-light. Pure black or white does not result in pure black or white.
- 用頂層減去底層或者相反來得到一個正值。
# difference
- Subtracts the bottom layer from the top layer or the other way round to always get a positive value.
- 一個柔和版本的強光(hard-light)。純黑或純白不會導致純黑或純白。
# exclusion
- Like difference, but with lower contrast.
- 和difference相似,但對比度較低。
# hue
- Preserves the luma and chroma of the bottom layer, while adopting the hue of the top layer.
- 保留了底層的亮度(luma)和色度(chroma),同時採用了頂層的色調(hue)。
# saturation
- Preserves the luma and hue of the bottom layer, while adopting the chroma of the top layer.
- 保留底層的亮度(luma)和色調(hue),同時採用頂層的色度(chroma)。
# color
- Preserves the luma of the bottom layer, while adopting the hue and chroma of the top layer.
- 保留了底層的亮度(luma),同時採用了頂層的色調(hue)和色度(chroma)。
# luminosity
- Preserves the hue and chroma of the bottom layer, while adopting the luma of the top layer.
- 保持底層的色調(hue)和色度(chroma),同時採用頂層的亮度(luma)。
看到global前綴大家應該也猜到了,這個屬性是影響整個畫布的,在一次渲染中無論被賦值幾次,最終的效果都取決於本次渲染的前globalCompositeOperation的最終值。
可以在這裏自己修改各個屬性查看效果。
說一下代碼設計的幾個要點:
① 準備兩個canvas,一個是背景,只在圖片載入的時候渲染一遍,一個是前景,用於合成前景圖和繪圖區域(destination-out)。事實上僅用一個canvas也能實現,每次繪圖時先在畫布上繪製前景圖,然後把背景與繪圖區域通過source-atop合成,再講結果繪製到畫布上。相較起來,前者性能顯然會更好。
② 通過lineTo來塗抹繪圖區域,而不是arc畫圓,避免幀率過低時連線不平滑,因此當鼠標按下時,需要調用beginPath來重置畫筆。
代碼很短,就直接放上來了:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>刮刮樂</title>
<style>
* {
padding: 0;
margin: 0;
}
html, body {
width: 100%;
height: 100%;
}
canvas {
position: absolute;
left: 0;
top: 0;
border: 1px dashed black;
}
</style>
</head>
<body>
<script>
(function () {
const imgBg = new Image()
const imgFg = new Image()
let canvasWidth = 0
let canvasHeight = 0
let canvasLeft = 0
let canvasTop = 0
const init = function () {
const $canvasBg = document.createElement('canvas')
$canvasBg.id = 'bg'
const $canvasFg = document.createElement('canvas')
$canvasFg.id = 'fg'
$canvasFg.width = $canvasBg.width = canvasWidth
$canvasFg.height = $canvasBg.height = canvasHeight
$canvasFg.style.cssText = $canvasBg.style.cssText = `left:${canvasLeft}px;top:${canvasTop}px;`
document.body.append($canvasBg)
document.body.append($canvasFg)
const ctxBg = $canvasBg.getContext('2d')
const ctxFg = $canvasFg.getContext('2d')
// 繪製背景
ctxBg.drawImage(imgBg, 0, 0)
ctxFg.lineWidth = 50
ctxFg.lineCap = 'round'
ctxFg.lineJoin = 'round'
ctxFg.strokeStyle = '#000'
ctxFg.drawImage(imgFg, 0, 0)
ctxFg.globalCompositeOperation = 'destination-out'
let posX = 0
let posY = 0
let drawing = false
/**
* 塗抹
* @param start 重置畫筆
*/
const draw = function (start) {
if (start) {
ctxFg.beginPath()
ctxFg.moveTo(posX, posY)
}
ctxFg.lineTo(posX, posY)
ctxFg.stroke()
}
// 按下
const onMouseDown = function (e) {
drawing = true
// 獲得畫筆相對canvas位置
if (e.touches && e.touches.length) {
posX = e.touches[0].pageX - canvasLeft
posY = e.touches[0].pageY - canvasTop
}
else {
posX = e.pageX - canvasLeft
posY = e.pageY - canvasTop
}
draw(true)
}
// 移動
const onMouseMove = function (e) {
if (drawing) {
if (e.touches && e.touches.length) {
posX = e.touches[0].pageX - canvasLeft
posY = e.touches[0].pageY - canvasTop
}
else {
posX = e.pageX - canvasLeft
posY = e.pageY - canvasTop
}
draw()
}
}
// 擡起
const onMouseUp = function (e) {
if (drawing) {
drawing = false
}
}
// 事件監聽
$canvasFg.addEventListener('mousedown', onMouseDown, false)
$canvasFg.addEventListener('touchstart', onMouseDown, false)
window.addEventListener('mousemove', onMouseMove, false)
window.addEventListener('touchmove', onMouseMove, false)
window.addEventListener('mouseup', onMouseUp, false)
window.addEventListener('touchend', onMouseUp, false)
}
// 載入圖片
let loadCount = 0
const onLoad = function () {
loadCount++
if (loadCount === 2) {
canvasWidth = imgBg.width
canvasHeight = imgBg.height
canvasLeft = (window.innerWidth - canvasWidth) * 0.5
canvasTop = (window.innerHeight - canvasHeight) * 0.5
init()
}
}
imgBg.src = 'after.png'
imgBg.complete ? onLoad() : (imgBg.onload = onLoad)
imgFg.src = 'before.png'
imgFg.complete ? onLoad() : (imgFg.onload = onLoad)
})()
</script>
</body>
</html>