天天看點

使用canvas繪制冰墩墩(貝塞爾曲線)

春節不停更,此文正在參加「星光計劃-春節更帖活動」

前言

一墩難求,一墩難求,冬奧會都過半了,小包的墩還是沒有到位,難受~~~感覺小包暫時是無法得到真墩了。沒辦法,又得拾起老手藝,想方設法繪制個糊弄糊弄自己吧。

由于存在版權問題,是以小包本文就不全程帶大家寫冰墩墩的代碼了,咱們一起在學習一下實作思路。源碼位址

學習本文,你可以收獲:

  • 了解貝塞爾曲線
  • 學會

    canvas

    中繪制二階貝塞爾曲線和三階貝塞爾曲線的方法
  • 獲得冰墩墩各曲線的資料,成功學會繪制冰墩墩

分析

首先我們整體看一下封面圖,大緻會有兩種實作思路。

使用canvas繪制冰墩墩(貝塞爾曲線)
  1. 方案一: 基于

    CSS

    實作

冰墩墩大部分部位都可以使用

CSS

的橢圓角配合為元素來模拟實作,比如眼睛、防護罩等,但有兩個問題。

  • 輪廓實作問題: 小包觀摩了大佬們的創意,大佬将冰墩墩進行拆分,将耳朵手臂等部位單獨拆分出來,形象化一下冰墩墩,實作的墩墩非常可愛。小包仔細的思考了一下,好像很難有更好的創意了。
  • 橢圓尺寸問題: 小包找到樣闆後突然發現,如何擷取橢圓的半徑是個大問題。
  1. 方案二: 基于

    canvas

    實作

canvas stroke()

方法會實際地繪制出通過

moveTo()

lineTo()

等方法定義的路徑。

我們找到了繪制曲線的方法,是以冰墩墩的繪制難點集中在如何繪制每條曲線上。

二維數學空間中存在貝塞爾曲線,冰墩墩整體就可以用貝塞爾曲線來繪制。**那什麼是貝塞爾曲線那?我們又如何求解出貝塞爾曲線?**下面我們來一起學習一下。

貝塞爾曲線

貝塞爾曲線(

Bezier curve

),又稱貝茲曲線或貝濟埃曲線,是應用于二維圖形應用程式的數學曲線。一般的矢量圖形軟體通過它來精确畫出曲線,貝茲曲線由線段與節點組成,節點是可拖動的支點,線段像可伸縮的皮筋,我們在繪圖工具上看到的鋼筆工具就是來做這種矢量曲線的。貝塞爾曲線是計算機圖形學中相當重要的參數曲線,在一些比較成熟的位圖軟體中也有貝塞爾曲線工具,如

PhotoShop

等 ———— 百度百科

光看定義感覺這是嘛玩意啊?接下來來看幾個案例,了解一下各階貝塞爾曲線。

貝塞爾曲線根據點的數量可以分為:

  • 一階貝塞爾曲線 (2 個控制點)
使用canvas繪制冰墩墩(貝塞爾曲線)
  • 二階貝塞爾曲線 (3 個控制點)
使用canvas繪制冰墩墩(貝塞爾曲線)
  • 三階貝塞爾曲線 (4 個控制點)
使用canvas繪制冰墩墩(貝塞爾曲線)
  • 四階貝塞爾曲線 (5 個控制點)
使用canvas繪制冰墩墩(貝塞爾曲線)
  • n階貝塞爾曲線 (n + 1 個控制點)

跟随着上面的動畫,不知道是否對貝塞爾曲線産生了一定的了解?不急下面小包慢慢道來~

貝塞爾曲線中有個重要的參數

t

,取值

[0,1]

t

參數的值等于線段上某一個點距離起點的長度除以線段長度。現在對

t

的了解有可能有些空洞,下面我們來具體了解一下。

一階貝塞爾曲線

一階貝塞爾曲線包括兩個控制點 $P_0$、$P_1$,兩控制點連接配接成線段$P_0$$P_1$。

使用canvas繪制冰墩墩(貝塞爾曲線)

在一階貝塞爾曲線中,隻有一條線段,$t = \frac{P_0^1P_0}{P_0P_1}$,是以随着 t 從 0 -> 1 ,可以繪制出一條直線。

根據幾何知識,我們可以求解出一階貝塞爾曲線的公式為:

$B_1(t) = P_1 + (P_2 - P_1)t, t \in [0,1]$

一階貝塞爾曲線總結一下就是随着參數t增大,貝塞爾曲線上的點從線段一端移動到另一端。

二階貝塞爾曲線

二階貝塞爾曲線分别有三個控制點,$P_0$、$P_1$、$P_2$,二階貝塞爾曲線兩條線段 $P_0P_1$、$P_1P_2$。

t

參數的值等于線段上某一個點距離起點的長度除以線段長度,且所有的線段上都要滿足上述要求,是以 $t = \frac{P_0P_0^1}{P_0P_1} = \frac{P_1P_1^1}{P_1P_2}$,$P_0^1$ 和 $P_1^1$ 構成新的線段,是以根據 $t = \frac{P_0^1P_0^2}{P_0^1P_1^1}$ 值可以求得位于貝塞爾曲線上的點 $P_0^2$

使用canvas繪制冰墩墩(貝塞爾曲線)

根據上面的計算流程,我們可以一步一步推出二階貝塞爾曲線的公式:

  1. 利用一階公式在 $P_0P_1$ 上求解出 $P_0^1$,在 $P_1P_2$ 上求出 $P_1^1$

$P_0^1 = (1 - t)P_0 + tP_1$

$P_1^1 = (1 - t)P_1 + tP_2$

  1. 利用一階公式,在 $P_0^1P_1^1$ 上求解出 $P_0^2$,也就是上述公式代入下列公式中:

$B_2(t) = (1-t)P_0^1 + tP_1^1$

$= (1-t)((1 - t)P_0 + tP_1) + t((1 - t)P_1 + tP_2)$

$= (1-t)^2P_0+2t(1-t)P_1+t^2P_2$

求解出二階貝塞爾曲線的公式如下:

$B_2(t) = (1-t)^2P_0+2t(1-t)P_1+t^2P_2, t\in [0,1]$

随着參數 t 增大,就可以獲得一條圖示二階貝塞爾曲線。

三階貝塞爾曲線

使用canvas繪制冰墩墩(貝塞爾曲線)

三階貝塞爾曲線有四個控制點,類似于二階貝塞爾曲線的求解過程。

  1. 分别線上段 $AB$、$BC$、$CD$ 上去點 $F、G、H$,且 $t = \frac{AF}{AB} = \frac{AF}{AB} = \frac{BG}{BC} = \frac{CH}{CD}$
  2. 點 $F、G、H$ 組成新的線段 $FG、GH$,在兩端線段上分别取點 $I、J$,且滿足 $t = \frac{FI}{FG} = \frac{GJ}{GH}$
  3. 點 $I、J$ 組成線段 $IJ$,取點 $E$ ,且滿足 $t = \frac{IE}{IJ}$
  4. 依次帶入公式,可以求得三階貝塞爾曲線的公式

$B_3(t) = (1-t)^3P_0 + 3t(1-t)^2P_1+3t^2(1-t)P_2+t^3P_3 t \in [0,1]$

$P_0,P_1,P_2,P_3$ 分别代表圖中的 $A,B,C,D$

反推貝塞爾曲線控制點

下面隻是小包的個人思考過程,數學大佬輕噴。

通過上面的公式推導,我們可以得出每種貝塞爾曲線都有兩個固定的控制點——曲線的起始點和終止點。

那麼一階貝塞爾曲線控制點就是起止點。

使用canvas繪制冰墩墩(貝塞爾曲線)

二階貝塞爾曲線控制點分别是起止點及起止點處切線交點。

使用canvas繪制冰墩墩(貝塞爾曲線)

三階貝塞爾曲線有四個控制點,其中兩個是起止點。另外兩個控制點是起止點處切線及曲線極值點處切線相交點。

問題來了,雖然思考出貝塞爾曲線的控制點尋找方法,但小包卻沒有找到如何求出各段曲線的控制點。那該如何通過上面的算法進行測量那?奈何小包的 PS 水準太差,最終隻能參考大佬的測量資料。

膜拜大佬,下面是大佬的測量過程,敬佩大佬。

使用canvas繪制冰墩墩(貝塞爾曲線)

小包将大佬的資料進行彙總,得出下表格:

使用canvas繪制冰墩墩(貝塞爾曲線)

canvas 的貝塞爾曲線方法

實作冰墩墩主要使用兩個貝塞爾方法:

bezierCurveTo

quadraticCurveTo

quadraticCurveTo

quadraticCurveTo

方法用于繪制二階貝塞爾曲線,使用文法

context.quadraticCurveTo(cpx,cpy,x,y);
           

cpx/cpy

為控制點的坐标,

x/y

為結束點坐标。

二階貝塞爾曲線共需要三個控制點,是以使用

quadraticCurveTo

會基于目前路徑的結束點繼續繪制。如果不存在路徑,需要使用

beginPath()

moveTo()

方法來定義起始點。

使用案例:

// ctx 代表目前 canvas
ctx.moveTo(20,20)
ctx.quadraticCurveTo(20,100,200,20)
           
使用canvas繪制冰墩墩(貝塞爾曲線)

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)
           
使用canvas繪制冰墩墩(貝塞爾曲線)

冰墩墩繪制

準備工作

由于大佬測量參數太大,是以小包首先對資料做了一下縮放。并封裝一下常用函數。
// 縮放參數為3
const 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();
           

通過上面的代碼,我們就可以成功的繪制出墩的輪廓了!!!

使用canvas繪制冰墩墩(貝塞爾曲線)

繪制耳朵

// 冰墩墩左耳朵
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();
           
使用canvas繪制冰墩墩(貝塞爾曲線)

繪制冰墩墩小手小腳

// 冰墩墩左手
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();
           
使用canvas繪制冰墩墩(貝塞爾曲線)

繪制黑眼圈

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

![dweneyeborder.png](https://s2.51cto.com/images/blog/202210/13233955_6348314b2cbe228053.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)

### 繪制能量圈
```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();
           
使用canvas繪制冰墩墩(貝塞爾曲線)

其餘冰墩墩部分繪制都是調用 

canvas

 的 

bezierCurveTo

 及 

quadraticCurveTo

 方法,與上文實作類似,小包就不在文章中重複了,具體可以參考源碼或者添加小包索要彙總資料表。

冰墩墩曲線資料來源于: Bulbul

附件連結:https://harmonyos.51cto.com/resource/1725

想了解更多關于鴻蒙的内容,請通路:

51CTO和華為官方合作共建的鴻蒙技術社群

https://ost.51cto.com/#bkwz