上一個單元 p5.js 實戰演練(六) –– 煙霧動畫實作(一) 我們留了一個回家作業給大家,看要怎麼使用 smoothstep 函數來進行緞帶頭尾的接合。

先來上個解答。

作業答案

function setup() {
    createCanvas(600, 600);
    background(0);
}

function draw() { background(0); noFill(); strokeWeight(2); translate(width/2, height/2);

for (let i = 0; i map(value, ori_start, ori_end, target_start, target_end)

簡單來說就是查看 value 在範圍 ori_start ~ ori_end 之間的哪個位置,比如說是在 ori_start ~ ori_end 之間的三分之一段的位置:

Imgur

那輸出就會映射到 target_start ~ target_end 之間同樣三分之一段的數值點:

Imgur

map 函數用在這真的是再適合不過了!我們可以藉由 map 函數的映射,輕鬆得得出目前的 theta 在 11/12<em>2</em>pi ~ 2*pi 中的那一個區段:

Imgur

這裡注意到我們將映射的範圍顛倒過來,變成 1 ~ 0 而不是 0 ~ 1,因為越靠近 11/12<em>2</em>pi,變數 n 的混合比例就要越高(越趨近於 1),越靠近 2*pi,比例就越低(越趨近於 0)。

然後我們要導入 smoothstep 函數,讓混合的比例可以更加平滑。

> ratio = 3ratioratio - 2ratioratio*ratio;

假設我們不這樣做,直接用 1 ~ 0 的線性比例混合,會導致以下結果:

Imgur

可以看到 theta 在 11/12<em>2</em>pi2*pi 有明顯的摺痕出現。

將煙霧動畫抽象化為函數

p5.js 實戰演練(六) –– 煙霧動畫實作(一),我們已經展示了最後的成果,由多個環狀煙霧以同心圓的方式向周圍暈開,所以我們要將環狀煙霧抽象化為一個函數,以便能在程式中重複使用。

先來想想要將煙霧的哪些特性抽出來作為可調整的函數參數:

  • 半徑 radius (隨時間越來越大)
  • 線的個數 line_num (決定煙霧厚度)
  • 透明度 alpha (先淡入再淡出)

function circular_smoke(radius, variance, line_num, alpha) {
noFill();
strokeWeight(2);
for (let i = 0; i < line_num; i++) { stroke(255 i / line_num alpha);
beginShape();
let head_n;
for (let theta = 0; theta < 2PI; theta += 2PI/180) {
if (theta < 2PI 11/12) {
let n = float(noise(theta 0.5, i 0.02, frameCount * 0.005));
if (theta == 0) {
head_n = n;
}
let r = float(n * radius);
vertex(r cos(theta), r sin(theta));
} else {
let n = float(noise(theta 0.5, i 0.02, frameCount * 0.005));
let ratio = map(theta, 2PI 11/12, 2*PI, 1, 0);
ratio = 3ratioratio – 2ratioratio*ratio;
let mixed_n = (1-ratio) head_n + ratio n;
let r = float(mixed_n * radius);
vertex(r cos(theta), r sin(theta));
}
}
endShape(CLOSE);
}
}

抽象化的過程非常簡單,接下來試著讓半徑隨著時間放大,看會有什麼樣的效果:

function setup() {
createCanvas(600, 600);
background(0);
}

function circular_smoke(radius, line_num, alpha) {
noFill();
strokeWeight(2);
for (let i = 0; i < line_num; i++) { stroke(255 i / line_num alpha);
beginShape();
let head_n;
for (let theta = 0; theta < 2PI; theta += 2PI/180) {
if (theta < 2PI 11/12) {
let n = float(noise(theta 0.5, i 0.02, frameCount * 0.005));
if (theta == 0) {
head_n = n;
}
let r = float(n * radius);
vertex(r cos(theta), r sin(theta));
} else {
let n = float(noise(theta 0.5, i 0.02, frameCount * 0.005));
let ratio = map(theta, 2PI 11/12, 2*PI, 1, 0);
ratio = 3ratioratio – 2ratioratio*ratio;
let mixed_n = (1-ratio) head_n + ratio n;
let r = float(mixed_n * radius);
vertex(r cos(theta), r sin(theta));
}
}
endShape(CLOSE);
}
}

function draw() {
background(0);
translate(width/2, height/2);

circular_smoke(200 + frameCount*0.1, 30, 1);
}
“`

Imgur

看起來好像沒有達到理想的效果,當半徑加大,noise function 的振幅也逐漸加大,環狀帶寬也變得越來越大。

根據最終結果我們想要的,應該是煙霧隨時間暈開薄化的效果(振幅應該隨著半徑加大仍然維持不變,甚至是要變小)。

最後的優化流程留到明天繼續解說。