iT邦幫忙

2024 iThome 鐵人賽

DAY 20
1
Modern Web

p5.js 的環形藝術系列 第 20

[Day 20]p5.js 基礎教學(九) –– 貝茲曲線

  • 分享至 

  • xImage
  •  

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

貝茲曲線繪製原理

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

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

Imgur

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

Imgur

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

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

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

Imgur

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

Imgur

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

Imgur

  1. 然後在這一條粉紅色線段上,標記出線段長度為全長 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 函數畫出軌跡,因為牽涉到遞迴概念有一點困難,讀者可以慢慢理解體會。


上一篇
[Day 19] p5.js 實戰演練(八) –– 煙霧動畫實作(三)
下一篇
[Day 21] p5.js 創作應用(九) –– 貝茲曲線隨機動畫
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言