之前我有一個作品 結晶(Crystallization)
,就是以前一單元 p5.js 基礎教學(七) –– 繪製不規則形狀 學習到的 vertex
、beginShape
、endShape
函數為基礎,並搭配迴圈、隨機函數等等技巧組合而成:
所以這次的實戰演練單元,我們就來詳細講解該作品的實作思路與脈絡。
我們可以先觀察,若多個頂點均勻分佈在某個半徑固定的圓周上,連接後會呈現出怎樣的視覺效果:
function setup() {
createCanvas(windowWidth, windowHeight);
stroke(0);
}
function draw() {
background(255, 30);
translate(width/2, height/2);
var point_num = 24;
var radius = 150;
beginShape(TRIANGLES);
for (let i = 0; i < point_num; i++) {
vertex(
radius * cos(i * 2 * PI / point_num),
radius * sin(i * 2 * PI / point_num)
);
}
endShape();
}
看起來非常顯而易見,就跟我們上一個單元的範例是差不多的結果。
這個範例我們的頂點是沿著圓周按照順序標記的,那如果我們是跳著標記頂點,會呈現怎樣的結果呢?
function setup() {
createCanvas(windowWidth, windowHeight);
stroke(0);
}
function draw() {
background(255, 30);
translate(width/2, height/2);
var point_num = 24;
var radius = 150;
beginShape(TRIANGLES);
var j = 0; // 從角度 0 開始標記頂點
var d = 5; // 每次跳 5 格標記頂點
for (let i = 0; i < point_num; i++) {
// i 變成單純計數的變數
j += d;
j %= point_num;
vertex(
radius * cos(j * 2 * PI / point_num),
radius * sin(j * 2 * PI / point_num)
);
}
endShape();
}
這個視覺效果看起來就有趣不少,然後我們再添加一些顏色並設置適當的透明度:
function setup() {
createCanvas(windowWidth, windowHeight);
stroke(0);
}
function draw() {
background(255, 30);
translate(width/2, height/2);
var point_num = 24;
var radius = 150;
colorMode(HSB, 360, 100, 100, 100);
stroke(90, 30, 80);
fill(90, 30, 80, 10); // 將透明度設為 10,讓顏色有覆蓋疊加的效果
colorMode(RGB);
beginShape(TRIANGLES);
var j = 0; // 從角度 0 開始標記頂點
var d = 5; // 每次跳 5 格標記頂點
for (let i = 0; i < point_num; i++) {
// i 變成單純計數的變數
j += d;
j %= point_num;
vertex(
radius * cos(j * 2 * PI / point_num),
radius * sin(j * 2 * PI / point_num)
);
}
endShape();
}
可以觀察到如果我們分別將變數 d
設為 2
、3
、4
,因為跳的格數和總頂點數 point_num
(24
),並不互質,所以我們跳不到所有的頂點,因此建議還是將 point_num
設為一個質數。
接下來我們要引用隨機函數,讓起始標記點 j
以及跳躍的格數 d
隨機化,看看有什麼樣的效果。
function setup() {
createCanvas(windowWidth, windowHeight);
stroke(0);
}
function draw() {
background(255, 30);
translate(width/2, height/2);
var point_num = 29; // 將 point_num 設為質數
var radius = 150;
colorMode(HSB, 360, 100, 100, 100);
stroke(90, 30, 80);
fill(90, 30, 80, 10); // 將透明度設為 10,讓顏色有覆蓋疊加的效果
colorMode(RGB);
beginShape(TRIANGLES);
var j = int(random() * point_num); // 將 j 隨機化
var d = int(random() * point_num); // 將 d 隨機化
for (let i = 0; i < point_num; i++) {
// i 變成單純計數的變數
j += d;
j %= point_num;
vertex(
radius * cos(j * 2 * PI / point_num),
radius * sin(j * 2 * PI / point_num)
);
}
endShape();
}
因為每一幀 random
函數都會任意生成一個數值,所以每一幀的連結方式都會有快速的變動,這樣的動畫實在是太快了,不是我們想要的方式。
假設我們想要將速度放慢,改成每 12 幀再取一個隨機值,應該要怎麼做呢?
會發現 random()
和 randomSeed()
好像無法直接解決這個問題(針對每 12 幀再取一次隨機值),有一個解決方案是我們可以再設一個計數器每數到 12 再去取一個隨機值,但這樣好像有點醜陋,所以這裡引入一個特殊的技巧,我們可以設計一個隨機函數 my_random
:
function my_random(x) {
return fract(sin(x*425.121)*437.53123);
}
我們嘗試設計一個隨機函數,給一個輸入,函數會給我們一個「看似」隨機的輸出,反正人腦解析不出規律就好了,不需要真的隨機輸出。
原理是建構一個 sin 函數,並且讓這個函數的頻率和震幅拉的非常大,最後再取其正小數部分,這樣我給定一個 x
值,我們就不太能預測其 y
值到底在 0~1
的哪一個部分,那就相當於建構出一個「看似」隨機的函數了。
為什麼我們要建構一個必須給定 x
參數的隨機函數呢?因爲這樣我們就可以傳 int(frameCount/12)
進去了,這個數值每過 12 幀才會變動一次,也就是說,過 12 幀才會變動一次隨機值:
function setup() {
createCanvas(windowWidth, windowHeight);
stroke(0);
}
function my_random(x) { // 將 sin 函數的頻率和震幅拉大,再取其正小數部分
return fract(sin(x*425.121)*437.53123);
}
function draw() {
background(255, 30);
translate(width/2, height/2);
var point_num = 29; // 將 point_num 設為質數
var radius = 150;
colorMode(HSB, 360, 100, 100, 100);
stroke(90, 30, 80);
fill(90, 30, 80, 10); // 將透明度設為 10,讓顏色有覆蓋疊加的效果
colorMode(RGB);
beginShape(TRIANGLES);
var random_seed = int(frameCount/12); // 每過 12 幀數字才會變化
var j = int(my_random(random_seed) * point_num); // 將 j 隨機化
var d = int(my_random(random_seed) * point_num); // 將 d 隨機化
for (let i = 0; i < point_num; i++) {
// i 變成單純計數的變數
j += d;
j %= point_num;
vertex(
radius * cos(j * 2 * PI / point_num),
radius * sin(j * 2 * PI / point_num)
);
}
endShape();
}
現在連接方式的隨機變動已經完成,剩下的等下一個單元在慢慢說明。