刚刚在开车群看到一个好玩的动图
正好新年也快到了,给家人做个“过年红包刮刮乐”的页面,增加点年味不也挺好。
说做就做,这里我们用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>