写点什么

为冬奥加油——利用贝塞尔曲线实现冰墩墩

作者:战场小包
  • 2022 年 2 月 18 日
  • 本文字数:5720 字

    阅读完需:约 19 分钟

为冬奥加油——利用贝塞尔曲线实现冰墩墩

前言

冬奥会即将马上结束,我国体育健儿发挥优异,创造了我国参加冬奥会的新纪录。在冬奥即将结束之际,我们一起来绘制火出天际的冰墩墩,为我国体育健儿喝彩,当然也可以为自己收获一只可爱的冰墩墩。


可爱冰墩墩源码

可爱冰墩墩在线版


学习本文,你可以收获:


  • 理解贝塞尔曲线

  • 学会 canvas 中绘制二阶贝塞尔曲线和三阶贝塞尔曲线的方法

  • 获得墩各曲线的数据,成功拥有你的墩。

分析

首先我们整体看一下封面图,大致会有两种实现思路。


  1. 方案一: 基于 CSS 实现


冰墩墩大部分部位都可以使用 CSS 的椭圆角配合为元素来模拟实现,比如眼睛、能量圈等,但有两个问题。


  • 轮廓实现问题: 小包观摩了大佬们的创意,大佬将冰墩墩进行拆分,将耳朵手臂等部位单独拆分出来,形象化一下冰墩墩,实现的冰墩墩非常可爱。小包仔细的思考了一下,好像也没有什么创新点。

  • 椭圆尺寸问题: 小包找到一张比较简单的简笔画,但是测量各个椭圆的尺寸也是个大麻烦。


  1. 方案二: 基于 canvas 实现


canvas stroke() 方法会实际地绘制出通过 moveTo()lineTo() 等方法定义的路径。


绘制曲线的方法找到了,接下来我们需要找到这种曲线的类别是什么?


二维数学空间中存在贝塞尔曲线,贝塞尔曲线就可以胜任整个冰墩墩的绘制。那什么是贝塞尔曲线那?我们又如何求解出贝塞尔曲线? 下面我们来一起学习一下。

贝塞尔曲线

贝塞尔曲线(Bezier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔曲线工具,如 PhotoShop 等 ———— 百度百科


定义这是嘛玩意啊?一头雾水。接下来来看几个案例,深入了解一下各阶贝塞尔曲线。


贝塞尔曲线根据控制点的数量可以分为:


  • 一阶贝塞尔曲线 (2 个控制点)



  • 二阶贝塞尔曲线 (3 个控制点)



  • 三阶贝塞尔曲线 (4 个控制点)



  • 四阶贝塞尔曲线 (5 个控制点)



  • n 阶贝塞尔曲线 (n + 1 个控制点)


跟随着上面的动画,不知道是否对贝塞尔曲线产生了一定的理解?不急下面小包慢慢道来~


贝塞尔曲线中有个重要的参数 t ,取值 [0,1]t 参数的值等于线段上某一个点距离起点的长度除以线段长度。现在对 t 的理解有可能有些空洞,下面我们通过具体案例深入理解一下。

一阶贝塞尔曲线

一阶贝塞尔曲线包括两个控制点 ,两控制点连接成线段



在一阶贝塞尔曲线中,只有一条线段,,对于每一个 t 值,都可以获得一个贝塞尔曲线上的点。因此随着 t0 -> 1 ,就可以绘制出一阶贝塞尔曲线。


根据几何知识,我们可以求解出一阶贝塞尔曲线的公式为:


一阶贝塞尔曲线总结一下就是随着参数 t 增大,贝塞尔曲线上的点从线段一端移动到另一端

二阶贝塞尔曲线

二阶贝塞尔曲线分别有三个控制点,,二阶贝塞尔曲线两条线段 t 参数的值等于线段上某一个点距离起点的长度除以线段长度,且所有的线段上都要满足上述要求,因此在两个线段上分别取点 ,且满足 构成新的线段,仍要满足 t 值,因此通过 值可以求得位于贝塞尔曲线上的点 。随着 t0 -> 1 ,就可以绘制出二阶贝塞尔曲线。



根据上面的计算流程,我们可以一步一步推出二阶贝塞尔曲线的公式:


  1. 利用一阶公式可以求解出



  2. 然后求解出 ,也就是第一步求解结果代入一阶共公式中:



二阶贝塞尔曲线的公式如下:


三阶贝塞尔曲线



三阶贝塞尔曲线有四个控制点,类似于二阶贝塞尔曲线的求解过程。


  1. 分别在线段 上取点 ,且

  2. 组成新的线段 ,在两端线段上分别取点 ,且满足

  3. 组成线段 ,取点 ,且满足

  4. 依次带入公式,可以求得三阶贝塞尔曲线的公式


分别代表图中的


其余多阶贝塞尔曲线是类似的原理,利用贝塞尔曲线我们可以创造很多可爱、炫酷的图形。

反推贝塞尔曲线控制点

下面只是小包的个人思考过程,数学大佬轻喷。


目前我们拥有了墩的样板,因此我们需要反推墩上每段贝塞尔曲线的控制点。求解出控制点后,我们就可以利用 canvas 绘制每段曲线,最终组成可爱的墩。


首先各阶贝塞尔曲线都有两个固定的控制点——曲线的起始点和终止点。


那么一阶贝塞尔曲线控制点就是起止点



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



三阶贝塞尔曲线有四个控制点,其中两个是起止点。另外两个控制点是起止点处切线及曲线极值点处切线相交点


问题来了,虽然思考出贝塞尔曲线的控制点寻找方法,但小包却没有找到如何求出各段曲线的控制点。那该如何通过上面的算法进行测量那?奈何小包的 PS 水平太差,没找到合适的测量手段,所幸有大佬测量过。


膜拜大佬,下面是大佬的测量过程。



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



我们得到了每段贝塞尔曲线的控制点,下面就开始绘制吧。

canvas 的贝塞尔曲线方法

实现冰墩墩主要使用两个贝塞尔方法: bezierCurveTo quadraticCurveTo

quadraticCurveTo

quadraticCurveTo 方法用于绘制二阶贝塞尔曲线,使用语法


context.quadraticCurveTo(cpx,cpy,x,y);
复制代码


cpx/cpy 为控制点的坐标,x/y 为结束点坐标。


二阶贝塞尔曲线共需要三个控制点,因此使用 quadraticCurveTo 会基于当前路径的结束点继续绘制。如果不存在路径,需要使用 beginPath()moveTo() 方法来定义起始点。


使用案例:


// ctx 代表当前 canvasctx.moveTo(20,20)ctx.quadraticCurveTo(20,100,200,20)
复制代码



bezierCurveTo

bezierCurveTo 方法用于绘制三阶贝塞尔曲线,使用语法


context.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y);
复制代码


cpx1/cpy1,cpx2/cpy2 为控制点的坐标,x/y 为结束点坐标。


quadraticCurveTo 方法相同,方法基于当前路径的结束点继续绘制。如果不存在路径,需要使用 beginPath()moveTo() 方法来定义起始点。


使用案例:


ctx.moveTo(20,20)ctx.bezierCurveTo(20,100,200,100,200,20)
复制代码



冰墩墩绘制

准备工作

由于大佬测量参数太大,因此小包首先对数据做了一下缩放。并封装一下常用函数。


// 缩放参数为3const SCALE = 3;// 对测量参数进行缩放function scaleParam(x) {  return x / SCALE;}
// 根据缩放参数封装一下几个函数function bezierCurveTo(ctx, ...args) { ctx.bezierCurveTo(...args.map(scaleParam));}function moveTo(ctx, ...args) { ctx.moveTo(...args.map(scaleParam));}function quadraticCurveTo(ctx, ...args) { ctx.quadraticCurveTo(...args.map(scaleParam));}
复制代码


轮廓绘制

轮廓绘制非常简单,我们只需按照测量数据,调用 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();
复制代码


通过上面的代码,我们就可以成功的绘制出墩的轮廓了!!!

绘制耳朵

// 冰墩墩左耳朵ctx.beginPath();moveTo(ctx, 526, 437);bezierCurveTo(ctx, 498, 263, 667, 325, 641, 329);quadraticCurveTo(ctx, 600, 343, 526, 437);ctx.fillStyle = "#000000";ctx.fill();ctx.stroke();
// 冰墩墩右耳朵ctx.beginPath();moveTo(ctx, 1050, 285);bezierCurveTo(ctx, 1144, 232, 1167, 342, 1162, 387);quadraticCurveTo(ctx, 1119, 317, 1050, 285);ctx.fillStyle = "#000000";ctx.fill();ctx.stroke();
复制代码



绘制冰墩墩小手小脚


// 冰墩墩左手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();
复制代码



其余部分的墩绘制都是调用 canvasbezierCurveToquadraticCurveTo 方法,小包就不在文章中重复了,具体可以参考源码或者添加小包索要汇总数据表。


绘制黑眼圈


// 左黑眼圈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();
复制代码



绘制能量圈

// 能量圈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();
复制代码


其余冰墩墩部分绘制都是调用 canvas 的 bezierCurveTo 及 quadraticCurveTo 方法,与上文实现类似,小包就不在文章中重复了,具体可以参考源码或者添加小包索要汇总数据表。

源码仓库

源码地址: 可爱冰墩墩源码

体验地址:可爱冰墩墩在线版

如果感觉有帮助的话,别忘了给小包点个 ⭐ 。

参考链接

后语

我是 战场小包 ,一个快速成长中的小前端,希望可以和大家一起进步。

如果喜欢小包,可以在 infoQ 关注我,也可以在我的小小公众号——小包学前端关注小包的日常分享。

一路顺利,冲向未来!!!

冰墩墩绘制只用作文章发布,为冬奥喝彩。不要用作其他途径

发布于: 刚刚阅读数: 2
用户头像

战场小包

关注

还未添加个人签名 2021.09.23 加入

还未添加个人简介

评论

发布
暂无评论
为冬奥加油——利用贝塞尔曲线实现冰墩墩