座標處理


p5.js 的畫布,採用的繪圖座標系統是以畫布左上角為原點,單位為像素(Pixel),向右為 x 正方向,向下為 y 正方向。

要取得畫布的寬高可以透過 widthheight 特性,然而對這兩個特性設值,不會改變畫布寬高,要控制畫布的寬高必須透過 createCanvas

例如,使用 point 沿著畫布的對角線逐一畫點:

function setup() {
    createCanvas(200, 200);
    background(220);
    strokeWeight(10); // 筆刷大小
    noLoop();
}

function draw() {
    for(let x = 0; x < width; x += 10) {
        point(x, x);
    }
}

就數學上來說,其實「點」是沒有形狀的,它只有位置資訊,不過繪圖上,經常會將點表現為一個圓,point 就是如此,預設的筆刷大小是 1 個像素,也就是半徑 0.5 的圓點,可以透過 strokeWeight 來改變,繪圖結果如下:

座標處理

直角座標對大多數人應該是沒有問題的,然而繪圖上經常使用極座標,這對於處理圓之類的問題很有用,極座標的簡單說明可以參考〈二維座標系〉。

可以寫個簡單的轉換函式,將極座標轉為直角座標:

function polarToCartesian(r, theta) {
    return {
        x: r * cos(theta),
        y: r * sin(theta)
    };
}

p5.js 提供了 sincos 等三角函式,問題來了,theta 必須是徑度還是角度?預設是徑度模式,然而可以透過 angleMode(DEGREES) 改為角度模式,要改回來可使用 angleMode(RADIANS)

那麼就用點來畫個圓吧!

function setup() {
    createCanvas(200, 200);
    background(220);
    strokeWeight(10);   // 筆刷大小
    angleMode(DEGREES); // 角度模式
    noLoop();
}

function draw() {
    const r = 50;       // 半徑
    // 圓心
    const centerX = width / 2;
    const centerY = height / 2;
    for(let theta = 0; theta < 360; theta += 10) {
        const {x, y} = polarToCartesian(r, theta);
        point(x + centerX, y + centerY);
    }
}

function polarToCartesian(r, theta) {
    return {
        x: r * cos(theta),
        y: r * sin(theta)
    };
}

畫出來的效果如下:

座標處理

上面的範例直接將算出來的 (x, y) 各加上圓心的座標,構成了置中的圓,有時候我們希望繪圖座標的原點,可以就是畫布的中心,這可以透過 translate 來設定:

function setup() {
    createCanvas(200, 200);
    background(220);
    strokeWeight(10);   // 筆刷大小
    angleMode(DEGREES); // 角度模式
    noLoop();
}

function draw() {
    const r = 50;       // 半徑

    const centerX = width / 2;
    const centerY = height / 2;
    translate(centerX, centerY);  // 原點位移
    for(let theta = 0; theta < 360; theta += 10) {
        const {x, y} = polarToCartesian(r, theta);
        point(x, y);
    }
}

function polarToCartesian(r, theta) {
    return {
        x: r * cos(theta),
        y: r * sin(theta)
    };
}

translate 會改變接下來要繪製的像素位置,原本圓會以原點為圓心繪製,每個要繪製的像素位置會透過 translate 指定的位移量計算後再繪製,因此畫出來效果跟方才是相同的,在重複呼叫 draw 的場合,每次都會重置 translate 做過的轉換。

每次的 translate 執行影響了後續的繪製,而且可以累計,例如:

function setup() {
    createCanvas(200, 200);
    background(220);
    strokeWeight(10);   // 筆刷大小
    angleMode(DEGREES); // 角度模式
    noLoop();
}

function draw() {
    const r = 50;       // 半徑

    const centerX = width / 2;
    const centerY = height / 2;
    translate(centerX, centerY);   // 原點位移

    for(let n = 0; n < 3; n++) {
        stroke(random(255), random(255), random(255));   // 隨機顏色
        translate(15, 0);                                // 每次原點的 x 位移 15
        for(let theta = 0; theta < 360; theta += 10) {
            const {x, y} = polarToCartesian(r, theta);
            point(x, y);
        }
    }
}

function polarToCartesian(r, theta) {
    return {
        x: r * cos(theta),
        y: r * sin(theta)
    };
}

translate(centerX, centerY) 影響接下來的繪製,第一次 translate(15, 0) 執行時,相當於總共位移 (centerX + 15, centerY + 14),因此以點畫的小圓,圓心就是 (centerX + 15, centerY + 15),第二次 translate(15, 0) 執行時,相當於總共位移 (centerX + 30, centerY + 30),因此以點畫的小圓,圓心就是 (centerX + 30, centerY + 30),依此類推。

可以將迴圈展開來比較清楚:

...
    translate(centerX, centerY);   // 原點位移

    stroke(random(255), random(255), random(255));   
    translate(15, 0);                               // 位移 15
    for(let theta = 0; theta < 360; theta += 10) {
        const {x, y} = polarToCartesian(r, theta);
        point(x, y);
    }

    stroke(random(255), random(255), random(255));   
    translate(15, 0);                              // 再位移 15  
    for(let theta = 0; theta < 360; theta += 10) {
        const {x, y} = polarToCartesian(r, theta);
        point(x, y);
    }

    stroke(random(255), random(255), random(255));   
    translate(15, 0);                             // 再位移 15 
    for(let theta = 0; theta < 360; theta += 10) {
        const {x, y} = polarToCartesian(r, theta);
        point(x, y);
    }

stroke 可指定畫筆顏色,這邊透過 random 產生 0 ~ 255 的隨機值,因此完成的效果如下:

座標處理

不單是 translate 操作會累計,〈Transform〉中的 rotatescale 等也會累計,其原因在於這些轉換操作,背後都是矩陣運算,這之後再會談到。

有時候,你可能不需要累計,例如,你想以畫布為大圓中心,在大圓的圓周線上畫出一些小圓,單靠 translate 轉換像素雖然也可以,例如:

function setup() {
    createCanvas(200, 200);
    background(220);
    strokeWeight(5);    // 筆刷大小
    angleMode(DEGREES); // 角度模式
    noLoop();
}

function draw() {
    const r1 = 10; // 小圓半徑
    const r2 = 50; // 大圓半徑

    const centerX = width / 2;
    const centerY = height / 2;
    translate(centerX, centerY); // 原點位移

    for(let theta = 0; theta < 360; theta += 30) {
        const {x, y} = polarToCartesian(r2, theta);
        translate(x, y);     // 小圓圓心
        stroke(random(255), random(255), random(255)); // 隨機顏色
        dotCircle(r1);
        translate(-x, -y);   // 相當於復位
    }
}

function polarToCartesian(r, theta) {
    return {
        x: r * cos(theta),
        y: r * sin(theta)
    };
}

function dotCircle(r) {
    for(let theta = 0; theta < 360; theta += 30) {
        const {x, y} = polarToCartesian(r, theta);
        point(x, y);
    }
}

這可以完成以下的效果:

座標處理

然而在更複雜的情境時,你要回復的狀態可能不只有位移,也許還有其他的轉換操作或繪圖設定,例如,在〈Transform〉中可以看到,還有 rotatescaleshear 等轉換操作函式可以使用,若每次轉換後都還得要設回原本狀態,會是件麻煩事。

你可以透過 pushpop 來簡化,push 會將當前的轉換操作、繪圖等設定等置入堆疊(先入後出),無論之後你做了什麼轉換操作或繪圖設定,都可以透過 pop 回復至上一次的設定。

例如,方才的範例可以使用 pushpop 改寫如下:

function setup() {
    createCanvas(200, 200);
    background(220);
    strokeWeight(5);    // 筆刷大小
    angleMode(DEGREES); // 角度模式
    noLoop();
}

function draw() {
    const r1 = 10; // 小圓半徑
    const r2 = 50; // 大圓半徑

    const centerX = width / 2;
    const centerY = height / 2;
    translate(centerX, centerY); // 原點位移

    for(let theta = 0; theta < 360; theta += 30) {
        push();   // 在堆疊儲存當前繪圖設定

        const {x, y} = polarToCartesian(r2, theta);
        translate(x, y);
        stroke(random(255), random(255), random(255));
        dotCircle(r1);

        pop();   // 從堆疊回復繪圖設定
    }
}

function polarToCartesian(r, theta) {
    return {
        x: r * cos(theta),
        y: r * sin(theta)
    };
}

function dotCircle(r) {
    for(let theta = 0; theta < 360; theta += 30) {
        const {x, y} = polarToCartesian(r, theta);
        point(x, y);
    }
}