春节不停更,此文正在参加「星光计划-春节更帖活动」
前言
一墩难求,一墩难求,冬奥会都过半了,小包的墩还是没有到位,难受~~~感觉小包暂时是无法得到真墩了。没办法,又得拾起老手艺,想方设法绘制个糊弄糊弄自己吧。
由于存在版权问题,所以小包本文就不全程带大家写冰墩墩的代码了,咱们一起在学习一下实现思路。源码地址
学习本文,你可以收获:
- 理解贝塞尔曲线
- 学会
canvas
中绘制二阶贝塞尔曲线和三阶贝塞尔曲线的方法
- 获得冰墩墩各曲线的数据,成功学会绘制冰墩墩
分析
首先我们整体看一下封面图,大致会有两种实现思路。

- 方案一: 基于
CSS
实现
冰墩墩大部分部位都可以使用 CSS
的椭圆角配合为元素来模拟实现,比如眼睛、防护罩等,但有两个问题。
- 轮廓实现问题: 小包观摩了大佬们的创意,大佬将冰墩墩进行拆分,将耳朵手臂等部位单独拆分出来,形象化一下冰墩墩,实现的墩墩非常可爱。小包仔细的思考了一下,好像很难有更好的创意了。
- 椭圆尺寸问题: 小包找到样板后突然发现,如何获取椭圆的半径是个大问题。
- 方案二: 基于
canvas
实现
canvas stroke()
方法会实际地绘制出通过 moveTo()
和 lineTo()
等方法定义的路径。
我们找到了绘制曲线的方法,因此冰墩墩的绘制难点集中在如何绘制每条曲线上。
二维数学空间中存在贝塞尔曲线,冰墩墩整体就可以用贝塞尔曲线来绘制。**那什么是贝塞尔曲线那?我们又如何求解出贝塞尔曲线?**下面我们来一起学习一下。
贝塞尔曲线
贝塞尔曲线(Bezier curve
),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔曲线工具,如 PhotoShop
等 ———— 百度百科
光看定义感觉这是嘛玩意啊?接下来来看几个案例,了解一下各阶贝塞尔曲线。
贝塞尔曲线根据点的数量可以分为:




跟随着上面的动画,不知道是否对贝塞尔曲线产生了一定的理解?不急下面小包慢慢道来~
贝塞尔曲线中有个重要的参数 t
,取值 [0,1]
,t
参数的值等于线段上某一个点距离起点的长度除以线段长度。现在对 t
的理解有可能有些空洞,下面我们来具体了解一下。
一阶贝塞尔曲线
一阶贝塞尔曲线包括两个控制点 P0、P1,两控制点连接成线段P0P1。

在一阶贝塞尔曲线中,只有一条线段,t=P0P1P01P0,因此随着 t 从 0 -> 1 ,可以绘制出一条直线。
根据几何知识,我们可以求解出一阶贝塞尔曲线的公式为:
B1(t)=P1+(P2−P1)t,t∈[0,1]
一阶贝塞尔曲线总结一下就是随着参数t增大,贝塞尔曲线上的点从线段一端移动到另一端。
二阶贝塞尔曲线
二阶贝塞尔曲线分别有三个控制点,P0、P1、P2,二阶贝塞尔曲线两条线段 P0P1、P1P2。t
参数的值等于线段上某一个点距离起点的长度除以线段长度,且所有的线段上都要满足上述要求,因此 t=P0P1P0P01=P1P2P1P11,P01 和 P11 构成新的线段,所以根据 t=P01P11P01P02 值可以求得位于贝塞尔曲线上的点 P02

根据上面的计算流程,我们可以一步一步推出二阶贝塞尔曲线的公式:
- 利用一阶公式在 P0P1 上求解出 P01,在 P1P2 上求出 P11
P01=(1−t)P0+tP1
P11=(1−t)P1+tP2
- 利用一阶公式,在 P01P11 上求解出 P02,也就是上述公式代入下列公式中:
B2(t)=(1−t)P01+tP11
=(1−t)((1−t)P0+tP1)+t((1−t)P1+tP2)
=(1−t)2P0+2t(1−t)P1+t2P2
求解出二阶贝塞尔曲线的公式如下:
B2(t)=(1−t)2P0+2t(1−t)P1+t2P2,t∈[0,1]
随着参数 t 增大,就可以获得一条图示二阶贝塞尔曲线。
三阶贝塞尔曲线

三阶贝塞尔曲线有四个控制点,类似于二阶贝塞尔曲线的求解过程。
- 分别在线段 AB、BC、CD 上去点 F、G、H,且 t=ABAF=ABAF=BCBG=CDCH
- 点 F、G、H 组成新的线段 FG、GH,在两端线段上分别取点 I、J,且满足 t=FGFI=GHGJ
- 点 I、J 组成线段 IJ,取点 E ,且满足 t=IJIE
- 依次带入公式,可以求得三阶贝塞尔曲线的公式
B3(t)=(1−t)3P0+3t(1−t)2P1+3t2(1−t)P2+t3P3t∈[0,1]
P0,P1,P2,P3 分别代表图中的 A,B,C,D
反推贝塞尔曲线控制点
下面只是小包的个人思考过程,数学大佬轻喷。
通过上面的公式推导,我们可以得出每种贝塞尔曲线都有两个固定的控制点——曲线的起始点和终止点。
那么一阶贝塞尔曲线控制点就是起止点。

二阶贝塞尔曲线控制点分别是起止点及起止点处切线交点。

三阶贝塞尔曲线有四个控制点,其中两个是起止点。另外两个控制点是起止点处切线及曲线极值点处切线相交点。
问题来了,虽然思考出贝塞尔曲线的控制点寻找方法,但小包却没有找到如何求出各段曲线的控制点。那该如何通过上面的算法进行测量那?奈何小包的 PS 水平太差,最终只能参考大佬的测量数据。
膜拜大佬,下面是大佬的测量过程,敬佩大佬。

小包将大佬的数据进行汇总,得出下表格:

canvas 的贝塞尔曲线方法
实现冰墩墩主要使用两个贝塞尔方法: bezierCurveTo
quadraticCurveTo
quadraticCurveTo
quadraticCurveTo
方法用于绘制二阶贝塞尔曲线,使用语法
cpx/cpy
为控制点的坐标,x/y
为结束点坐标。
二阶贝塞尔曲线共需要三个控制点,因此使用 quadraticCurveTo
会基于当前路径的结束点继续绘制。如果不存在路径,需要使用 beginPath()
和 moveTo()
方法来定义起始点。
使用案例:

bezierCurveTo
bezierCurveTo
方法用于绘制三阶贝塞尔曲线,使用语法
cpx1/cpy1,cpx2/cpy2
为控制点的坐标,x/y
为结束点坐标。
与 quadraticCurveTo
方法相同,方法基于当前路径的结束点继续绘制。如果不存在路径,需要使用 beginPath()
和 moveTo()
方法来定义起始点。
使用案例:

冰墩墩绘制
准备工作
由于大佬测量参数太大,因此小包首先对数据做了一下缩放。并封装一下常用函数。
轮廓绘制
轮廓绘制非常简单,我们只需按照测量数据,调用 canvas 的贝塞尔曲线绘制方法即可。
ctx.beginPath();
moveTo(ctx, 497, 462);
bezierCurveTo(ctx, 452, 380, 497, 184, 666, 297);
bezierCurveTo(ctx, 792, 255, 921, 261, 1017, 278);
bezierCurveTo(ctx, 1127, 155, 1227, 305, 1183, 404);
bezierCurveTo(ctx, 1208, 443, 1238, 488, 1254, 544);
bezierCurveTo(ctx, 1251, 421, 1503, 398, 1472, 577);
bezierCurveTo(ctx, 1407, 758, 1336, 789, 1279, 876);
bezierCurveTo(ctx, 1270, 924, 1255, 1044, 1147, 1222);
bezierCurveTo(ctx, 1098, 1372, 1211, 1454, 1031, 1457);
bezierCurveTo(ctx, 877, 1469, 892, 1434, 901, 1376);
bezierCurveTo(ctx, 924, 1313, 783, 1324, 802, 1378);
bezierCurveTo(ctx, 822, 1432, 819, 1467, 691, 1469);
bezierCurveTo(ctx, 571, 1473, 569, 1448, 571, 1332);
bezierCurveTo(ctx, 572, 1218, 530, 1226, 464, 1038);
bezierCurveTo(ctx, 386, 1244, 233, 1115, 272, 1017);
bezierCurveTo(ctx, 306, 916, 365, 845, 407, 777);
bezierCurveTo(ctx, 433, 669, 449, 545, 497, 462);
ctx.stroke();
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
通过上面的代码,我们就可以成功的绘制出墩的轮廓了!!!

绘制耳朵

绘制冰墩墩小手小脚
ctx.beginPath();
moveTo(ctx, 417, 804);
bezierCurveTo(ctx, 430, 837, 435, 914, 457, 968);
bezierCurveTo(ctx, 445, 1016, 440, 1022, 428, 1053);
bezierCurveTo(ctx, 396, 1142, 307, 1112, 304, 1048);
quadraticCurveTo(ctx, 300, 987, 418, 803);
ctx.fillStyle = "#000000";
ctx.fill();
ctx.stroke();
ctx.beginPath();
moveTo(ctx, 1267, 593);
bezierCurveTo(ctx, 1275, 584, 1279, 574, 1280, 555);
bezierCurveTo(ctx, 1282, 448, 1480, 477, 1429, 575);
bezierCurveTo(ctx, 1403, 621, 1374, 689, 1287, 757);
quadraticCurveTo(ctx, 1291, 693, 1267, 594);
ctx.fillStyle = "#000000";
ctx.fill();
ctx.stroke();
ctx.beginPath();
moveTo(ctx, 585, 1231);
bezierCurveTo(ctx, 626, 1261, 776, 1297, 792, 1336);
bezierCurveTo(ctx, 756, 1387, 838, 1427, 710, 1428);
bezierCurveTo(ctx, 505, 1431, 644, 1381, 585, 1231);
ctx.fillStyle = "#000000";
ctx.fill();
ctx.stroke();
ctx.beginPath();
moveTo(ctx, 910, 1342);
bezierCurveTo(ctx, 981, 1318, 938, 1293, 1125, 1226);
bezierCurveTo(ctx, 1087, 1370, 1172, 1404, 1014, 1420);
bezierCurveTo(ctx, 875, 1425, 959, 1403, 910, 1342);
ctx.fillStyle = "#000000";
ctx.fill();
ctx.stroke();
- 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.

绘制黑眼圈
ctx.beginPath();
moveTo(ctx, 806, 552);
bezierCurveTo(ctx, 706, 492, 512, 681, 603, 777);
bezierCurveTo(ctx, 738, 882, 896, 600, 806, 552);
ctx.fillStyle = "#000000";
ctx.fill();
ctx.stroke();
ctx.beginPath();
moveTo(ctx, 989, 541);
bezierCurveTo(ctx, 1080, 477, 1251, 684, 1168, 768);
bezierCurveTo(ctx, 1077, 837, 893, 607, 989, 541);
ctx.fillStyle = "#000000";
ctx.fill();
ctx.stroke();

### 绘制能量圈
```js
ctx.beginPath();
ctx.lineWidth = 7;
ctx.strokeStyle = "#73fd94";
moveTo(ctx, 497, 772);
bezierCurveTo(ctx, 425, 371, 1145, 80, 1262, 699);
bezierCurveTo(ctx, 1294, 945, 1105, 1031, 907, 1040);
bezierCurveTo(ctx, 716, 1049, 519, 962, 497, 772);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 5;
ctx.strokeStyle = "#f97dfe";
moveTo(ctx, 515, 794);
bezierCurveTo(ctx, 405, 421, 1093, 119, 1242, 646);
bezierCurveTo(ctx, 1316, 881, 1130, 1001, 898, 1003);
bezierCurveTo(ctx, 732, 1005, 562, 961, 515, 794);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 9;
ctx.strokeStyle = "#ecea87";
moveTo(ctx, 611, 909);
bezierCurveTo(ctx, 301, 602, 878, 185, 1137, 487);
bezierCurveTo(ctx, 1495, 981, 840, 1066, 611, 909);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 7;
ctx.strokeStyle = "#9ad6ff";
moveTo(ctx, 611, 909);
bezierCurveTo(ctx, 281, 592, 878, 200, 1137, 487);
bezierCurveTo(ctx, 1495, 1001, 840, 1076, 611, 909);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 5;
ctx.strokeStyle = "#9ad6ff";
moveTo(ctx, 515, 794);
bezierCurveTo(ctx, 405, 421, 1053, 109, 1242, 646);
bezierCurveTo(ctx, 1316, 911, 1150, 1001, 898, 1023);
bezierCurveTo(ctx, 732, 1025, 562, 971, 515, 794);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 7;
ctx.strokeStyle = "#d2fbe5";
moveTo(ctx, 545, 674);
bezierCurveTo(ctx, 673, 289, 1265, 370, 1215, 773);
bezierCurveTo(ctx, 1177, 1083, 453, 1010, 545, 674);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 7;
ctx.strokeStyle = "#4a46be";
moveTo(ctx, 549, 752);
bezierCurveTo(ctx, 548, 421, 1037, 320, 1191, 640);
bezierCurveTo(ctx, 1309, 1058, 597, 1021, 549, 752);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 5;
ctx.strokeStyle = "#b5e7fe";
moveTo(ctx, 549, 752);
bezierCurveTo(ctx, 548, 441, 1057, 300, 1191, 640);
bezierCurveTo(ctx, 1319, 1048, 567, 1021, 549, 752);
ctx.stroke();
- 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.

其余冰墩墩部分绘制都是调用 canvas
的 bezierCurveTo
及 quadraticCurveTo
方法,与上文实现类似,小包就不在文章中重复了,具体可以参考源码或者添加小包索要汇总数据表。
冰墩墩曲线数据来源于: Bulbul
刚刚就看到一个讲如何画冰墩墩的帖子,看来大佬们对"贝塞尔曲线"都挺情有独钟的。
哈哈哈,贝塞尔绘制出的效果比较好看,我去看一下这位大佬的学习一下
贝塞尔曲线,唔,很有知识点的样子。hhh
好多大佬下载了,谢谢大佬们