iT邦幫忙

2024 iThome 鐵人賽

DAY 14
1

架構設計

在昨天的文章中,我們完成了一個繪圖工具 painter,已經完成渲染的步驟,今天,我們將把演算法的細節補足:

export default function createAlgorithm(){
    //......
	const painter = new createPainter();
    this.transitionRadian = 0;
    this.trasitionOmega = Math.PI / 10000;
	this.update = (ctx, width, height) => {
		this.transitionRadian += this.trasitionOmega * this.speed;
		this.motion(width, height);
		this.addTexture(width, height, ctx);
	}
    this.render = () => {
		clear(ctx);
		ctx.save();
		ctx.translate(ctx.canvas.width * offset, 0);
		painter.render();
		ctx.restore();
    }
    return this;
}

掠食者獵物紋理-addTexture

藉由 addTask 繪圖管理方法,我們就可以輕鬆完成紋理的演算法,完整程式碼如下:

this.addTexture = (width, height, ctx) => {
    this.data.forEach((point) => {
        const x = point.x;
        const y = point.y;
        const ex = x / width;
        const ey = y / height;
        const dx = this.equation1(ex, ey) * width;
        const dy = this.equation2(ex, ey) * height;
        
        const x2 = x + this.dlength * dx;
        const y2 = y + this.dlength * dy;
        
        const blue = Math.abs( y / width * 255 );
        const green = Math.abs( x / width * 255 );
        const red = Math.abs( Math.sin(this.transitionRadian) * 255 );
        const color = `rgb(${red}, ${green}, ${blue})`;
        
        const mypoint = { name: "point", size: 2, ctx, x, y, color };
        const myline = { name: "line", size: 2, ctx, x, y, color, x2, y2 };
        painter.addTask(0, myline, mypoint);
    });
}

還記得前天介紹這個方程式的時候,我們計算出了 dx 和 dy,此時將其等比例放大,可以方便我們看清楚這條切線::

const x2 = x + this.dlength * dx;
const y2 = y + this.dlength * dy;

顏色方面,讓藍綠色通道隨座標變化產生漸層效果,而紅色通道隨週期變化,營造出動態視覺:

const blue = Math.abs( y / width * 255 );
const green = Math.abs( x / width * 255 );
const red = Math.abs( Math.sin(this.transitionRadian) * 255 );

最後,我們填充資料格式,提供畫筆繪圖的依據:

const mypoint = { name: "point", size: 2, ctx, x, y, color };
const myline = { name: "line", size: 2, ctx, x, y, color, x2, y2 };
painter.addTask(0, myline, mypoint);

添加物理引擎-引力效果 motion

這個選題是我非常喜歡的一個,前天我們操作動畫的時候,是直接設定週期、並讓圖形旋轉變換,現在還要加入引力效果,不會互相衝突嗎?這問題非常有趣,我們不仿換個角度想:圖形變換和旋轉的依據是什麼?

回顧程式碼

this.motion = (width, height) => {
    this.data.forEach((point) => {
        const rad = this.transitionRadian;
        const period1 = Math.cos(rad);
        const period2 = Math.sin(rad);

        const angular1 = point.d * period1 * 0.1;
        const angular2 = point.d * period2 * 0.1;
        
        point.r+= Math.PI / 1000;

        point.x = width / 2 + point.d * Math.cos(point.r + angular1);
        point.y = height / 2 + point.d * Math.sin(point.r + angular2);
    })
}

答案是極座標系的角度和距離

這段程式碼通過極座標來計算粒子的位置,我們可以設法將引力效果與旋轉變換區別開來,避免互相衝突。

場域疊加

現在,想像這些粒子像電子一樣,在虛擬磁場中運動。這個影響是由初始值 r 和 d 決定的,不會輕易改變。

const point = {
    "d": Math.sqrt(Math.random()) * width / 2, // distance
    "r": Math.random() * 2 * Math.PI, // radian
};

因此我們會得到一個跟磁場有關的虛擬座標:

point.fakeX = width / 2 + point.d * Math.cos(point.r);
point.fakeY = height / 2 + point.d * Math.sin(point.r);

當磁場發生改變時,我們需要對粒子座標進行修正,而非直接更新:

this.motion = (width, height) => {
    // 1.旋轉
    this.data.forEach((point) => {
        //......
        const newX = width / 2 + point.d * Math.cos(point.r + angular1);
        const newY = height / 2 + point.d * Math.sin(point.r + angular2);

        point.x += newX - point.fakeX;
        point.y += newY - point.fakeY;
        
        point.fakeX = newX;
        point.fakeY = newY;
    });
    // 2.引力
}

這樣我們就得到一個疊加的座標系,正常運行下,就和原本的動畫相同。但我們可以嘗試把起始座標統一設置為中心點,這會有很魔幻的效果,像波一樣。

point.fakeX = width / 2;
point.fakeY = height / 2;

因為太魔幻了,我不知道如何形容,所以,看影片吧!
Yes

引力效果

我們現在可以回過頭來設計引力效果了,引力大小和粒子在空間中的相對位置、距離有關,我有準備一個基本的模型,但直接實現有一定的挑戰。

範本程式碼展示了完整的引力計算,但它的時間複雜度是 O(n²),這意味著對於 2000 個粒子而言,我們需要計算 2000² / 2 次,這對於動畫來說會對效能造成很大的負擔,不適合我們做動畫。

範本程式碼如下:

for (let i = 0; i < this.data.length; i++) {
    const p1 = this.data[i];
    let vx = 0, vy = 0;
    for (let j = i + 1; j < this.data.length; j++) {
        const p2 = this.data[j];
        const d = getDistance(p1.x, p1.y, p2.x, p2.y);
        const MAXD = 10;
        if(d < MAXD){
            let force;
            if((d) < MAXD * 0.1) force = -1;
            if((d) < MAXD * 0.55) force = 1 * ((d)- MAXD * 0.1) / (MAXD * 0.45);
            if((d) < MAXD) force = 1 * (MAXD - (d)) / (MAXD * 0.45);
            const directionX = (p2.x > p1.x) ? 1 : -1;
            const directionY = (p2.y > p1.y) ? 1 : -1;
            vx+= directionX * force;
            p2.vx[0]-= directionX * force;
            vy+= directionY * force;
            p2.vy[0]-= directionY * force;
        }
    }
    p1.x+= caluVelocity(p1.vx);
    p1.y+= caluVelocity(p1.vy);

    const GRAVITY = 10;

    vx+= (width*0.5 + 50 > p1.x) ? 1 : -1 * GRAVITY;
    vx-= (width*0.5 - 50 < p1.x) ? 1 : -1 * GRAVITY;
    vy+= (height/2 + 50 > p1.y) ? 1 : -1 * GRAVITY;
    vy-= (height/2 - 50 < p1.y) ? 1 : -1 * GRAVITY;
    
    addVelocity(p1.vx, vx);
    addVelocity(p1.vy, vy);
}

重力效果

接下來,我們將實作一個只包含重力的基礎版本。首先,在初始化時,我們將所有粒子的速度設為 0,並通過緩衝陣列來控制速度變化的平滑性。

const point = {
    "vx": new Array(60).fill(0),
    "vy": new Array(60).fill(0),
    //......
};

這裡,我們使用一個長度為 60 的陣列來保存每個粒子的速度數據,這樣可以避免速度變化過於劇烈,並保持動畫的連貫性。

每當速度變化時,我們將新速度插入陣列的開頭,並移除尾端的舊數據,以此保持速度緩慢改變。計算速度時,我們會取這些速度值的平均數:

function addVelocity(arr, v){
    arr.unshift(v); // 在開頭插入 v
    arr.pop(); // 刪除尾端元素
}
function caluVelocity(arr){
	const avg = arr.reduce((sum, value) => sum + value, 0) / arr.length;
	return avg;
}

這樣的方式讓速度變化平滑且自然,使粒子的運動更加流暢。想像它是一顆行星,我們還設計了一個反直覺的重力模型:離中心越遠的粒子,受到的吸引力越強。而在中心區域則設置一個閥值,在範圍內不受引力影響。

this.motion = (width, height) => {
    // 1.旋轉
    this.data.forEach((point) => {
        //......
    });
    // 2.引力
    this.data.forEach((point) => {
		point.x+= caluVelocity(point.vx);
		point.y+= caluVelocity(point.vy);
		let vx = 0, vy = 0;
		const GRAVITY = 10;
		const GAP = 50;
		vx+= (width * 0.5 + GAP > point.x) ? 1 : -1 * GRAVITY;
		vx-= (width * 0.5 - GAP < point.x) ? 1 : -1 * GRAVITY;
		vy+= (height * 0.5 + GAP > point.y) ? 1 : -1 * GRAVITY;
		vy-= (height * 0.5 - GAP < point.y) ? 1 : -1 * GRAVITY;
		addVelocity(point.vx, vx);
		addVelocity(point.vy, vy);
    });
}

這樣一來,整個引力效果就能夠自然地與粒子運動結合,並且對效能十分友好。此版本不僅運行流暢,也能表現出複雜的引力互動效果。
Yes

結論

透過這篇文章,我們展示了掠食者獵物方程作為基底紋理的潛力,並嘗試在單純的動畫中,添加粒子系統和引力計算。當中,我們實作全套的引力模型,嘗試在計算效能與物理真實性之間取得平衡。最終,我們選擇了一種基於緩衝的重力實作方法,確保動畫的流暢運行,同時保留了引力效果的核心概念。

gh-pages
github Repo


上一篇
B5 掌握分層渲染:製作高效 Canvas 繪圖工具
下一篇
B7 與 Web Worker 互動:多執行緒渲染真的能提升效能嗎?
系列文
讓演算法起舞:前端特效應用的探索之旅35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言