p5.js 的畫布,採用的繪圖座標系統是以畫布左上角為原點,單位為像素(Pixel),向右為 x 正方向,向下為 y 正方向。
要取得畫布的寬高可以透過 width
、height
特性,然而對這兩個特性設值,不會改變畫布寬高,要控制畫布的寬高必須透過 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 提供了 sin
、cos
等三角函式,問題來了,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
會改變畫布的座標系統,畫布中每個要繪製的像素,會基於新的座標系統繪製,因此畫出來效果跟方才是相同的,在重複呼叫 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〉中的 rotate
、scale
等也會累計,其原因在於這些轉換操作,背後都是矩陣運算,這之後再會談到。
有時候,你可能不需要累計,每次想基於某個座標系統開始進行轉換,例如,你想以畫布為大圓中心,在大圓的圓周線上畫出一些小圓,單靠 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〉中可以看到,還有 rotate
、scale
、shear
等轉換操作函式可以使用,若每次轉換後都還得要設回原本狀態,會是件麻煩事。
你可以透過 push
、pop
來簡化,push
會將當前的座標系統狀態、繪圖等設定等置入堆疊(先入後出),無論之後你做了什麼轉換操作或繪圖設定,都可以透過 pop
回復至上一次的設定。
例如,方才的範例可以使用 push
、pop
改寫如下:
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);
}
}