這次的基礎教學我們要介紹貝茲曲線,這是一種用數學函數所描述的曲線形式,廣泛應用到計算機圖學以及字型設計上,他好處是可以藉由少少的座標點來精確描述複雜的曲線圖形。

貝茲曲線繪製原理

我們要如何使用少少的幾個點來繪製一個貝茲曲線呢?

我們以畫布上任意標記四個座標點為例,貝茲曲線是用下面的方法畫出來的:

Imgur

接下來我們一步步解釋這個方法的原理,貝茲曲線上的每一個點,都可以標記為 0~1 之間的某一個數字:

Imgur

貝茲曲線就是根據畫布上給定的座標點,用某種方式標記出 0-1 這個區間每個實數對應在畫布上的某個點位置,將這些點全部連接,就會得到貝茲曲線。

用一個範例來解釋這個標記位置的方法,給定一個 0-1 區間的實數 0.3,根據剛剛給定的四個座標點 (50, 300)(150, 100)(250, 300)(350, 50)

  • 先用將這四個座標點按照順序連接起來,得到三條灰色線段:

Imgur

  • 然後在這三條灰色線段上,標記出線段長度為全長 0.3 的點位置,然後再按照順序將這些點連接起來,得到兩條藍色線段:

Imgur

  • 然後在這兩條藍色線段上,標記出線段長度為全長 0.3 的點位置,然後再按照順序將這些點連接起來,得到一條粉紅色線段:

Imgur

  • 然後在這一條粉紅色線段上,標記出線段長度為全長 0.3 的點位置,得到一個綠色點點,這個點就是貝茲曲線對應於實數 0.3 的繪製點:

Imgur

不管最初給定的座標點有多少個,我們可以持續用這種標記點位置、連接線段的方式,得到某個實數對應的曲線繪製點。

五個座標點的範例:

Imgur

六個座標點的範例:

Imgur

貝茲曲線數學形式

同時貝茲曲線也能直接被數學公式表述,這裡也介紹一下貝茲曲線的數學形式,給對數學有興趣的讀者看。

  • 一階貝茲曲線(直線)

一階貝茲曲線就是在兩個點之間的直線段。公式如下:

Imgur

  • 二階貝茲曲線(拋物線)

二階貝茲曲線通過三個控制點定義。公式如下:

Imgur

  • 三階貝茲曲線(常用)

三階貝茲曲線是最常用的貝茲曲線,由四個控制點定義。公式如下:

Imgur

貝茲曲線語法

在 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(); }

Imgur

然後還要提到另一個函數 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(); }

Imgur

那如果是非四個控制點的貝茲曲線要怎麼畫呢?那就必須用程式自己計算出軌跡了,以下給出五個控制點的範例:

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(); }

Imgur

範例中自定義函數 get_bezier_point ,給定 0-1 之間的實數計算出對應的貝茲曲線點位置,然後用之前講過的 vertex 函數畫出軌跡,因為牽涉到遞迴概念有一點困難,讀者可以慢慢理解體會。