這次的基礎教學我們要介紹貝茲曲線,這是一種用數學函數所描述的曲線形式,廣泛應用到計算機圖學以及字型設計上,他好處是可以藉由少少的座標點來精確描述複雜的曲線圖形。
我們要如何使用少少的幾個點來繪製一個貝茲曲線呢?
我們以畫布上任意標記四個座標點為例,貝茲曲線是用下面的方法畫出來的:
接下來我們一步步解釋這個方法的原理,貝茲曲線上的每一個點,都可以標記為 0~1
之間的某一個數字:
貝茲曲線就是根據畫布上給定的座標點,用某種方式標記出 0-1
這個區間每個實數對應在畫布上的某個點位置,將這些點全部連接,就會得到貝茲曲線。
用一個範例來解釋這個標記位置的方法,給定一個 0-1
區間的實數 0.3
,根據剛剛給定的四個座標點 (50, 300)
、(150, 100)
、(250, 300)
、(350, 50)
:
0.3
的點位置,然後再按照順序將這些點連接起來,得到兩條藍色線段:0.3
的點位置,然後再按照順序將這些點連接起來,得到一條粉紅色線段:0.3
的點位置,得到一個綠色點點,這個點就是貝茲曲線對應於實數 0.3
的繪製點:不管最初給定的座標點有多少個,我們可以持續用這種標記點位置、連接線段的方式,得到某個實數對應的曲線繪製點。
五個座標點的範例:
六個座標點的範例:
同時貝茲曲線也能直接被數學公式表述,這裡也介紹一下貝茲曲線的數學形式,給對數學有興趣的讀者看。
一階貝茲曲線就是在兩個點之間的直線段。公式如下:
二階貝茲曲線通過三個控制點定義。公式如下:
三階貝茲曲線是最常用的貝茲曲線,由四個控制點定義。公式如下:
在 p5.js 中,描述貝茲曲線的函數如下:
bezier(x1, y1, x2, y2, x3, y3, x4, y4)
bezier
限定就是接收八個參數,也就是四個控制點的貝茲曲線,以下是繪製貝茲曲線的程式範例:
let point_list;
function setup() {
frameRate(20);
createCanvas(400, 400);
background(255);
point_list = [
[50, 300],
[150, 100],
[250, 300],
[350, 50],
];
}
function draw_base () {
fill(0);
stroke(200);
let prev_point;
point_list.forEach((point, index) => {
ellipse(point[0], point[1], 10, 10);
textSize(12);
textAlign(CENTER, CENTER);
text(`(${point[0]}, ${point[1]})`, point[0], point[1] + 15 * (1 - 2 * (index%2)));
if (index > 0) {
line(prev_point[0], prev_point[1], point[0], point[1]);
}
prev_point = point;
});
}
function draw() {
background(255);
draw_base();
noFill();
stroke("#FF3E3E");
strokeWeight(2);
bezier(
point_list[0][0],
point_list[0][1],
point_list[1][0],
point_list[1][1],
point_list[2][0],
point_list[2][1],
point_list[3][0],
point_list[3][1]
);
noLoop();
}
然後還要提到另一個函數 bezierPoint
:
bezierPoint(a, b, c, d, t)
相對於 bezier
用來繪製貝茲曲線,bezierPoint
用來取得 0-1
之間的實數 t
所對應的貝茲曲線描點,只是以單一座標為主,因此若要取得描點座標 (result_x, result_y)
,必須呼叫兩次 bezierPoint
:
let result_x = bezierPoint(x1, x2, x3, x4, t);
let result_y = bezierPoint(y1, y2, y3, y4, t);
以下是程式範例:
let point_list;
function setup() {
frameRate(20);
createCanvas(400, 400);
background(255);
point_list = [
[50, 300],
[150, 100],
[250, 300],
[350, 50],
];
}
function draw_base () {
fill(0);
stroke(200);
let prev_point;
point_list.forEach((point, index) => {
ellipse(point[0], point[1], 10, 10);
textSize(12);
textAlign(CENTER, CENTER);
text(`(${point[0]}, ${point[1]})`, point[0], point[1] + 15 * (1 - 2 * (index%2)));
if (index > 0) {
line(prev_point[0], prev_point[1], point[0], point[1]);
}
prev_point = point;
});
}
function draw() {
background(255);
draw_base();
noFill();
stroke("#FF3E3E");
strokeWeight(2);
bezier(
point_list[0][0],
point_list[0][1],
point_list[1][0],
point_list[1][1],
point_list[2][0],
point_list[2][1],
point_list[3][0],
point_list[3][1]
);
fill("#FF3E3E");
noStroke();
[0.1, 0.3, 0.5, 0.7, 0.9].forEach(t => {
let x = bezierPoint(
point_list[0][0],
point_list[1][0],
point_list[2][0],
point_list[3][0],
t
);
let y = bezierPoint(
point_list[0][1],
point_list[1][1],
point_list[2][1],
point_list[3][1],
t
);
ellipse(x, y, 8, 8);
textSize(12);
textAlign(CENTER, CENTER);
text(t.toFixed(1), x, y - 15);
});
noLoop();
}
那如果是非四個控制點的貝茲曲線要怎麼畫呢?那就必須用程式自己計算出軌跡了,以下給出五個控制點的範例:
let point_list;
function setup() {
frameRate(20);
createCanvas(400, 400);
background(255);
point_list = [
[50, 300],
[150, 100],
[250, 300],
[380, 200],
[350, 50],
];
}
function draw_base () {
fill(0);
stroke(200);
let prev_point;
point_list.forEach((point, index) => {
ellipse(point[0], point[1], 10, 10);
textSize(12);
textAlign(CENTER, CENTER);
text(`(${point[0]}, ${point[1]})`, point[0], point[1] + 15 * (1 - 2 * (index%2)));
if (index > 0) {
line(prev_point[0], prev_point[1], point[0], point[1]);
}
prev_point = point;
});
}
function get_bezier_point(t) {
let iter_count = 0;
let now_list = point_list;
let next_list = [];
while (now_list.length > 1) {
let prev_point;
now_list.forEach((point, index) => {
if (index > 0) {
let inter_point = [
map(t, 0, 1, prev_point[0], point[0]),
map(t, 0, 1, prev_point[1], point[1])
];
next_list.push(inter_point);
}
prev_point = point;
});
now_list = next_list;
next_list = [];
iter_count += 1;
}
return now_list[0];
}
function draw() {
background(255);
draw_base();
noFill();
stroke("#FF3E3E");
strokeWeight(2);
beginShape();
for (let t = 0; t <= 1; t += 0.01) {
let [x, y] = get_bezier_point(t);
vertex(x, y);
}
endShape();
noLoop();
}
範例中自定義函數 get_bezier_point
,給定 0-1
之間的實數計算出對應的貝茲曲線點位置,然後用之前講過的 vertex
函數畫出軌跡,因為牽涉到遞迴概念有一點困難,讀者可以慢慢理解體會。