iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
Modern Web

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

[Day 19] p5.js 實戰演練(八) –– 煙霧動畫實作(三)

  • 分享至 

  • xImage
  •  

今天我們要進行煙霧動畫最後的部分。

昨天 p5.js 實戰演練(七) –– 煙霧動畫實作(二) 提到我們必須解決 noise function 震幅隨著半徑越來越大的問題,這原因其實很好理解,我們用 n * radius 來構成最後的極座標半徑 rn 的影響當然會隨著 radius 數值逐漸加大。

新增傳入參數 variance

解決方法是,我們讓 radius 的變化和變數 n 脫鉤,所以我們可以更改原本的函數參數,現在應該會有四個可控變因:

  • 半徑 radius (隨時間越來越大)
  • 擾動參數 variance (基本上不隨時間改變)
  • 線的個數 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);
    }
}

現在我們試著只讓 radius 隨著時間增長,其他的參數維持不變,看看最後結果如何?

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

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 * variance + 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 * variance + radius);
                vertex(r * cos(theta), r * sin(theta));
            }
        }
        endShape(CLOSE);
    }
}

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

    circular_smoke(-10 + frameCount*0.5, 100, 30, 1);
}

Imgur

導入參數 Object 列表 smoke_args_list

看起來效果好了不少,但現在還有兩個問題:

  1. 當半徑越大,整體形狀越趨近於圓形,失去了煙霧會有的氣流擾動效果
  2. 煙霧應該要隨著半徑加大而逐漸淡出,而且應該在最後完全消失,不再渲染
  3. 要持續有新的煙霧持續出現

第一個問題很好解決,我們只需要讓 variance 隨著時間小幅增長就好了。

第二點和第三點是同一層面的問題,牽涉到煙霧的出現與消失,所以要改變直接呼叫 circular_smoke(-10 + frameCount*0.5, 100, 30, 1); 的方式,我打算維護一個 Object 的陣列 smoke_args_list,每一個 Object 的內容代表要塞入 circular_smoke 的參數表,所以這個陣列的長度代表當下會渲染出的煙霧數量。

let smoke_args_list = [];
function draw() {
    background(0);
    translate(width/2, height/2);
    for (let args of smoke_args_list) {
        circular_smoke(
                args.radius,
                args.variance,
                args.line_num,
                args.alpha
        );
    }
}

我們必須決定在某個固定的時間點增加煙霧的數量,假設要在每 150 幀增加一個煙霧動畫,那我們可以這樣處理:

let smoke_args_list = [];
function draw() {
    background(0);
    translate(width/2, height/2);
    if (frameCount % 150 == 1) {
        smoke_args_list.unshift({
            radius: -10,
            variance: 100,
            line_num: 30,
            alpha: 1
        });
    }
    for (let args of smoke_args_list) {
        circular_smoke(
                args.radius,
                args.variance,
                args.line_num,
                args.alpha
        );
    }
}

然後針對外部參數做出時間上的變化:

  • radius 隨時間放大,讓煙霧暈開(每幀增加 0.5
  • variance 隨時間放大,增加氣流擾動效果(每幀增加 0.1
  • alpha 隨時間減小,製造淡出效果(每幀減小 0.002
let smoke_args_list = [];
function draw() {
    background(0);
    translate(width/2, height/2);
    if (frameCount % 150 == 1) {
        smoke_args_list.unshift({
            radius: -10,
            variance: 100,
            line_num: 30,
            alpha: 1
        });
    }
    for (let args of smoke_args_list) {
        args.radius += 0.5;
        args.variance += 0.1;
        args.alpha -= 0.002;
    }
    for (let args of smoke_args_list) {
        circular_smoke(
                args.radius,
                args.variance,
                args.line_num,
                args.alpha
        );
    }
}

最後我們要逐一檢驗,是否 alpha 已經小於等於 0(已經不會在畫布上顯示),那我們就要將此參數 Object 從 smoke_args_list 中剔除:

let smoke_args_list = [];
function draw() {
    background(0);
    translate(width/2, height/2);
    if (frameCount % 150 == 1) {
        smoke_args_list.unshift({
            radius: -10,
            variance: 100,
            line_num: 30,
            alpha: 1
        });
    }
    for (let args of smoke_args_list) {
        args.radius += 0.5;
        args.variance += 0.1;
        args.alpha -= 0.002;
    }
    while (smoke_args_list.length > 0) {
        if (smoke_args_list[smoke_args_list.length-1].alpha < 0) {
            smoke_args_list.pop();
        } else {
            break;
        }
    }
    for (let args of smoke_args_list) {
        circular_smoke(
                args.radius,
                args.variance,
                args.line_num,
                args.alpha
        );
    }
}

來看看最後渲染的結果:

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

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 * variance + 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 * variance + radius);
                vertex(r * cos(theta), r * sin(theta));
            }
        }
        endShape(CLOSE);
    }
}

let smoke_args_list = [];
function draw() {
    background(0);
    translate(width/2, height/2);

    if (frameCount % 150 == 1) {
        smoke_args_list.unshift({
            radius: -10,
            variance: 100,
            line_num: 30,
            alpha: 1
        });
    }
    for (let args of smoke_args_list) {
        args.radius += 0.5;
        args.variance += 0.1;
        args.alpha -= 0.002;
    }
    while (smoke_args_list.length > 0) {
        if (smoke_args_list[smoke_args_list.length-1].alpha < 0) {
            smoke_args_list.pop();
        } else {
            break;
        }
    }
    for (let args of smoke_args_list) {
        circular_smoke(
                args.radius,
                args.variance,
                args.line_num,
                args.alpha
        );
        //circular_smoke(-10 + frameCount*0.5, 100 + frameCount*0.1, 30, 1);
    }
}

Imgur

增加淡入效果和煙霧的變化性

看起來效果變的相當好了,但每個煙霧的 pattern 看起來都非常一致,我希望每個煙霧都有自己的變化,並且我們還希望在出現時有個淡入的效果,而不是直接冒出來,那我們可以這樣修改:

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

function circular_smoke(radius, variance, line_num, alpha, seed) { // 增加隨機參數 seed
    noFill();
    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) {
							  // 在 noise function 的位置座標上加入隨機變數 seed
                let n = float(noise(theta * 0.5, seed + i * 0.02, frameCount * 0.005));
                if (theta == 0) {
                    head_n = n;
                }
                let r = float(n * variance + radius);
                vertex(r * cos(theta), r * sin(theta));
            } else {
							  // 在 noise function 的位置座標上加入隨機變數 seed
                let n = float(noise(theta * 0.5, seed + 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 * variance + radius);
                vertex(r * cos(theta), r * sin(theta));
            }
        }
        endShape(CLOSE);
    }
}

let smoke_args_list = [];
function draw() {
    background(0);
    translate(width/2, height/2);

    if (frameCount % 150 == 1) {
        smoke_args_list.unshift({
            radius: -10,
            variance: 100,
            line_num: 30,
            alpha: [0, 1], // 第二個參數若是 1 代表淡入階段,若是 0 代表淡出階段
			seed: random()*1000 // 增加隨機參數 seed
        });
    }
    for (let args of smoke_args_list) {
        args.radius += 0.5;
        args.variance += 0.1;
			
			  // alpha 參數增加淡入階段
			  if (args.alpha[1] == 1) {
					args.alpha[0] += 0.02;
					if (args.alpha[0] >= 1) {
						args.alpha[1] = 0; // 切換為淡出階段
					}
				} else {
					args.alpha[0] -= 0.002;
				}
    }
    while (smoke_args_list.length > 0) {
        if (smoke_args_list[smoke_args_list.length-1].alpha[0] < 0) { // 記得加上 [0]
            smoke_args_list.pop();
        } else {
            break;
        }
    }
    for (let args of smoke_args_list) {
        circular_smoke(
                args.radius,
                args.variance,
                args.line_num,
                args.alpha[0],
			    args.seed // 增加隨機參數 seed
        );
    }
}

Imgur

作品網址:
https://openprocessing.org/sketch/2315699

費盡千辛萬苦終於完成了,感謝大家費心閱讀,下個單元我們開始來講貝茲曲線。


上一篇
[Day 18] p5.js 實戰演練(七) –– 煙霧動畫實作(二)
下一篇
[Day 20]p5.js 基礎教學(九) –– 貝茲曲線
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言