🏂抢不到冰墩墩?快用canvas画一个谁也抢不走的冰墩墩吧!

冬奥会正在如火如荼的进行,从第一天的冰壶到👦短道速滑队惊心动魄的金牌再到前几天看到🏂谷爱凌做出第一个偏轴1620我直接激动地跳起来,今年冬奥看点十足!不得不提的就是今年冬奥的吉祥物冰墩墩也是成了新网红,网络上全是他的身影,前两天开启的购买我也是一个都没抢到…真是要气死了。

image.png

于是我决定自己画一个,作为一个前端人,曾经也是学生会宣传部骨干成员,怎么说也是有点美术功底在的(自我感觉良好)。

本篇文章会通过画冰墩墩的方式帮助大家复习一下canvas基本的使用及相关方法。

实现效果

image.png

绘制步骤

身体

身体部分我们利用canvas的椭圆和canvas填充未闭合路径的特点来绘制。我们需要两个半椭圆来生成如下图所示的形状

image.png

1
2
3
4
5
6
ctx.beginPath()
ctx.ellipse(400, 400, 150, 160, 0, 0, Math.PI)
ctx.ellipse(400, 310, 160, 150, 0, Math.PI, Math.PI * 2)
ctx.fillStyle = '#fff'
ctx.fill()
ctx.closePath()

椭圆方法:ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)

对应参数:起点x.起点y,半径x,半径y,旋转的角度,起始角,结果角,顺时针或逆时针

耳朵

耳朵很简单,这里用一个半圆和一个矩形组成:

image.png

1
2
3
4
5
6
7
8
ctx.beginPath()
ctx.fillStyle = '#000'
ctx.arc(300, 190, 20, Math.PI, Math.PI * 2)
ctx.fillRect(280, 190, 40, 40)
ctx.arc(500, 190, 20, Math.PI, Math.PI * 2)
ctx.fillRect(480, 190, 40, 40)
ctx.fill()
ctx.closePath()

绘圆方法:arc(x,y,r,sAngle,eAngle,counterclockwise)

对应参数:圆心x,圆心y,半径,起始角,结束角,顺时针或逆时针

耳朵要先于身体绘制,这样身体就会将耳朵多余部分覆盖

手与脚

手脚为了做的生动形象,这里使用贝塞尔曲线来构造对应的形状,首先先来简单了解一下canvas中二阶、三阶贝塞尔曲线的生成方式和对应方法。

贝塞尔曲线图片来自https://www.jianshu.com/p/afccc4642621

  • quadraticCurveTo(二阶贝塞尔曲线)

quadraticCurveTo(cpx,cpy,x,y)

参数:控制点的x,控制点的y,结束点x,结束点y

16075459-cf42c00869beb95e.gif

  • bezierCurveTo(三阶贝塞尔曲线)

bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)

参数:控制点1的x,控制点1的y,控制点2的x,控制点2的y,结束点x,结束点y

16075459-c505dd7f784accfa.gif

贝塞尔曲线原理在动图中已经体现的很明显了,控制点能够使两点之间的折线变得圆滑。接下来我们利用它画手脚,每画完一部分记得closePath()关闭当前路径。

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
//   脚
ctx.beginPath()
ctx.moveTo(300, 523)
ctx.bezierCurveTo(315, 523, 290, 583, 300, 603)
ctx.quadraticCurveTo(325, 613, 370, 603)
ctx.quadraticCurveTo(360, 584, 370, 556)
ctx.quadraticCurveTo(325, 523, 300, 520)
ctx.stroke()
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.moveTo(440, 554)
ctx.quadraticCurveTo(445, 575, 438, 600)
ctx.quadraticCurveTo(465, 610, 510, 600)
ctx.bezierCurveTo(495, 575, 515, 545, 505, 514)
ctx.quadraticCurveTo(470, 546, 440, 553)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()

// 手
// 左手
ctx.beginPath()
ctx.moveTo(250, 400)
ctx.quadraticCurveTo(245, 330, 241, 300)
ctx.quadraticCurveTo(200, 320, 160, 420)
ctx.lineTo(230, 420)
ctx.quadraticCurveTo(245, 400, 250, 400)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(195, 420, 35, 0, Math.PI)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()

// 右手
ctx.beginPath()
ctx.moveTo(559, 300)
ctx.quadraticCurveTo(575, 295, 600, 250)
ctx.quadraticCurveTo(625, 230, 655, 250)
ctx.quadraticCurveTo(680, 330, 550, 400)
ctx.quadraticCurveTo(556, 340, 559, 300)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()

image.png

爱心我利用掘友已经实现了的代码添加,而logo则利用canvas导入图片的方法实现。logo网上一搜就有。

drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

参数:要使用的图像,裁剪x,裁剪y,图像宽度,图像高度,放置点x,放置点y,使用图像宽度,使用图像高度

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
// 爱心
let get_arr = function (a, len) {
let arr = []
for (let i = 0; i < len; i++) {
let step = (i / len) * (Math.PI * 2) //递增的θ
let vector = {
x: a * (16 * Math.pow(Math.sin(step), 3)) + 625,
y:
-a *
(13 * Math.cos(step) -
5 * Math.cos(2 * step) -
2 * Math.cos(3 * step) -
Math.cos(4 * step)) +
275,
}
arr.push(vector)
}
return arr
}
ctx.beginPath()
ctx.strokeStyle = 'red'
ctx.lineWidth = 1
let len = 50
let arr = get_arr(1, 50)
for (let i = 0; i < len; i++) {
ctx.lineTo(arr[i].x, arr[i].y) //心形的点一一被描绘出来
}
ctx.fillStyle = '#f00'
ctx.fill()
ctx.closePath()
// logo
var img = new Image()
img.onload = () => {
ctx.drawImage(img, 0, 0, 900, 900, 360, 440, 100, 100)
}
img.src = './logo.jpg'

image.png

面部

面部相对就比较简单了,都是规则图形或曲线,新知识点就是canvas的渐变填充方法。

线性渐变:createLinearGradient(x0,y0,x1,y1)

参数:渐变开始点x,渐变开始点y,渐变结束点x,渐变结束点y

径向渐变:createRadialGradient(x0,y0,r0,x1,y1,r1)

参数:渐变开始圆心x,渐变开始圆心y,半径,渐变结束圆心x,渐变结束圆心y,半径

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
// 彩虹🌈圈
ctx.beginPath()
ctx.ellipse(400, 300, 125, 95, -0.06, 0, Math.PI * 2)
var grd = ctx.createRadialGradient(400, 300, 95, 400, 300, 130)
grd.addColorStop(0, '#dddddd20')
grd.addColorStop(1, '#0000FF')
ctx.strokeStyle = grd
ctx.lineWidth = 6
ctx.stroke()
ctx.closePath()
ctx.beginPath()
ctx.ellipse(400, 300, 130, 95, -0.12, 0, Math.PI * 2)
var grd = ctx.createRadialGradient(400, 300, 95, 400, 300, 130)
grd.addColorStop(0, '#dddddd20')
grd.addColorStop(1, '#00FFFF66')
ctx.strokeStyle = grd
ctx.lineWidth = 2
ctx.stroke()
ctx.closePath()
ctx.beginPath()
ctx.ellipse(400, 300, 130, 95, 0.06, 0, Math.PI * 2)
var grd = ctx.createRadialGradient(400, 300, 95, 400, 300, 130)
grd.addColorStop(0, '#dddddd20')
grd.addColorStop(1, '#FFFF0066')
ctx.strokeStyle = grd
ctx.lineWidth = 3
ctx.stroke()
ctx.closePath()
ctx.beginPath()
ctx.ellipse(400, 300, 130, 95, 0, 0, Math.PI * 2)
var grd = ctx.createRadialGradient(400, 300, 95, 400, 300, 130)
grd.addColorStop(0, '#dddddd20')
grd.addColorStop(1, '#00FF0066')
ctx.strokeStyle = grd
ctx.lineWidth = 3
ctx.stroke()
ctx.closePath()
ctx.beginPath()
ctx.ellipse(400, 300, 130, 95, 0.1, 0, Math.PI * 2)
var grd = ctx.createRadialGradient(400, 300, 95, 400, 300, 130)
grd.addColorStop(0, '#dddddd20')
grd.addColorStop(1, '#8B00FF66')
ctx.strokeStyle = grd
ctx.lineWidth = 4
ctx.stroke()
ctx.closePath()

image.png

眼鼻嘴

btw:眼鼻嘴这个标题一写出来我就没忍住开始唱

미안해 미안해 하지마

내가 초라해지잖아

好了收!

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
// 眼
ctx.beginPath()
ctx.ellipse(465, 275, 38, 26, 1, 0, Math.PI * 2)
ctx.ellipse(335, 275, 43, 29, -0.8, 0, Math.PI * 2)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(344, 272, 22, 0, Math.PI * 2)
ctx.arc(458, 270, 20, 0, Math.PI * 2)
ctx.fillStyle = '#fff'
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(344, 272, 17, 0, Math.PI * 2)
ctx.arc(458, 270, 15, 0, Math.PI * 2)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(344, 272, 16, 0, Math.PI * 2)
var grd = ctx.createRadialGradient(344, 272, 0, 344, 272, 20)
grd.addColorStop(0, '#444')
grd.addColorStop(1, '#111')
ctx.fillStyle = grd
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(458, 270, 14, 0, Math.PI * 2)
var grd = ctx.createRadialGradient(458, 270, 0, 458, 270, 20)
grd.addColorStop(0, '#444')
grd.addColorStop(1, '#111')
ctx.fillStyle = grd
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(344, 272, 12, 0, Math.PI * 2)
ctx.arc(458, 270, 10, 0, Math.PI * 2)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(350, 266, 4, 0, Math.PI * 2)
ctx.arc(452, 266, 3, 0, Math.PI * 2)
ctx.fillStyle = '#fff'
ctx.fill()
ctx.closePath()
// 鼻子
ctx.beginPath()
ctx.moveTo(380, 280)
ctx.lineTo(420, 280)
ctx.lineTo(400, 295)
ctx.lineTo(380, 280)
ctx.fillStyle = '#000'
ctx.fill()
ctx.closePath()
// 嘴
ctx.beginPath()
ctx.moveTo(370, 323)
ctx.lineTo(400, 335)
ctx.lineTo(430, 323)
ctx.quadraticCurveTo(400, 330, 370, 323)
ctx.strokeStyle = '#000'
ctx.lineWidth = 2
ctx.fillStyle = '#f00'
ctx.fill()
ctx.stroke()
ctx.closePath()

image.png

最后

我本来想要给整个冰墩墩描个黑边,画出透明外套的感觉,因为觉得透明外套是冰墩墩的灵魂,但是我一直没找到一个好的方式来绘,如果有大神能够指导或提点一下老弟的话我实在是感激不尽!


🏂抢不到冰墩墩?快用canvas画一个谁也抢不走的冰墩墩吧!
https://moewang0321.github.io/2022/02/10/🏂抢不到冰墩墩?快用canvas画一个谁也抢不走的冰墩墩吧!/
作者
Moe Wang
发布于
2022年2月10日
许可协议