Bootstrap
博客内容搜索
Kaysama's Blog

刚刚在开车群看到一个好玩的动图

正好新年也快到了,给家人做个“过年红包刮刮乐”的页面,增加点年味不也挺好。

说做就做,这里我们用canvas2D来实现效果,核心的API是 CanvasRenderingContext2D.globalCompositeOperation,主要是用来设定图形绘制前后的图层混合模式,详见该页。简单加上自己的理解翻译下:(ps:source 是将要绘制的图形,destination是指画布上已存在的图形)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# 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来重置画笔。

代码很短,就直接放上来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<!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>

完整代码戳这里

在线演示1在线演示2