iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
Modern Web

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

[Day 18] p5.js 實戰演練(七) –– 煙霧動畫實作(二)

  • 分享至 

  • xImage
  •  

上一個單元 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 < 30; i++) {
        stroke(255 * i / 30);
        beginShape();
        let head_n; // 用來儲存 theta 為 0 的 noise value
        for (let theta = 0; theta < 2*PI; theta += 2*PI/180) {
            // 用 if 判斷式區分是否為最後的 1/12 的段落
            if (theta < 2*PI * 11/12) { // 使用原本邏輯
                let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
                if (theta == 0) {
                    head_n = n; // 儲存 theta 為 0 的 noise value
                }
                let r = float(n * 200);
                vertex(r * cos(theta), r * sin(theta));
            } else { // 用 smoothstep 混合原本的 n 和 head_n
                let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
                let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);
                ratio = 3*ratio*ratio - 2*ratio*ratio*ratio; // 使用 smoothstep 計算新的 ratio
                let mixed_n = head_n + (n-head_n) * ratio;
                let r = float(mixed_n * 200);
                vertex(r * cos(theta), r * sin(theta));
            }
        }
        endShape(CLOSE);
    }
}

呈現出來的結果:

Imgur

程式解說

主要更動的部份已經有用註解去做註記了,我針對一些比較重要的部分進行說明:

    let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
    let r = float(n * 200);
    vertex(r * cos(theta), r * sin(theta)); 

這個是原本的繪製邏輯,但在最後 theta 為 0 和 theta 為 2*pi 的時候,noise value 並不連貫,所以會導致連接的時候出現斷層,所以我們將最後 theta 在 11/12*2*pi ~ 2*pi 之間的部分改成:

    let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
    let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);
    ratio = 3*ratio*ratio - 2*ratio*ratio*ratio;
    let mixed_n = (1-ratio) * head_n + ratio * n;
    let r = float(mixed_n * 200);
    vertex(r * cos(theta), r * sin(theta));

原本的計算半徑 r 的 noise value n,改成變數 mixed_n,這個是由變數 n 和變數 head_n 經由某個比例混合而成的,那我們要如何取這個比例呢?

這裡注意到 let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);,在 p5.js 中 map 函數用來將一個數值從一個範圍重新映射到另一個範圍:

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*2*pi ~ 2*pi 中的那一個區段:

Imgur

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

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

ratio = 3*ratio*ratio - 2*ratio*ratio*ratio;

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

Imgur

可以看到 theta 在 11/12*2*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 < 2*PI; theta += 2*PI/180) {
            if (theta < 2*PI * 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, 2*PI * 11/12, 2*PI, 1, 0);
                ratio = 3*ratio*ratio - 2*ratio*ratio*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 < 2*PI; theta += 2*PI/180) {
            if (theta < 2*PI * 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, 2*PI * 11/12, 2*PI, 1, 0);
                ratio = 3*ratio*ratio - 2*ratio*ratio*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 的振幅也逐漸加大,環狀帶寬也變得越來越大。

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

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


上一篇
[Day 17] p5.js 實戰演練(六) –– 煙霧動畫實作(一)
下一篇
[Day 19] p5.js 實戰演練(八) –– 煙霧動畫實作(三)
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言