canvas 粒子線條

引言

本來是想學(xué)習(xí)一下 Pixi.js 的粒子效果,結(jié)果做出來的效果粗粗糙糙的,想要做出好看的效果粒子運(yùn)動(dòng)軌跡離不開算法,還需要些時(shí)間研究= =。于是在網(wǎng)上看了一篇用 canvas 做的粒子 demo 博客,感覺也挺不錯(cuò)的,實(shí)現(xiàn)思路簡單,于是自己也做了一個(gè),但效果跟前者是不一樣的,是在前者的基礎(chǔ)上做了些改變。


目錄

  1. 實(shí)現(xiàn)原理

    1.1 讓粒子動(dòng)起來

    1.2 比對(duì)

    1.3 畫線

  2. 代碼

  3. 另一種效果

  4. 整合封裝

  5. 總結(jié)

  6. 了解更多


1、實(shí)現(xiàn)原理

先看下效果圖:

屏幕錄制2021-04-22 上午11

1.1 讓粒子動(dòng)起來

效果還是挺炫酷的,實(shí)現(xiàn)起來其實(shí)也并不難,就是在 canvas 畫布上隨機(jī)生成點(diǎn)位,用 requestAnimationFrame 方法不斷更新每個(gè)粒子的 x,y 坐標(biāo)值,就做到了簡單的粒子運(yùn)動(dòng)效果。

在粒子的 x 或 y 值大于 canvas 寬高或小于 0 時(shí)做反向運(yùn)動(dòng):定義兩個(gè)變量 x, y 軸的運(yùn)動(dòng)速度 sx sy 設(shè)置速度為 Math.random() * 1, 0~1 區(qū)間,正常情況下 粒子的 x,y 值等于 x += sx; y += sy; 在大于 canvas 寬高或小于 0 時(shí)將速度值賦值為負(fù)數(shù) sx = - sx; sy = -sy; 做反向運(yùn)動(dòng)。

1.2 比對(duì)

用一個(gè)變量數(shù)組存儲(chǔ)需要繪制的每個(gè)粒子信息,在定義一個(gè)鼠標(biāo)粒子對(duì)象加入數(shù)組當(dāng)做一個(gè)粒子比對(duì),用雙重 for 循環(huán)讓當(dāng)前粒子跟其他粒子位逐個(gè)比對(duì),第一層循環(huán)讓每個(gè)粒子開始移動(dòng),判斷如果是鼠標(biāo)粒子則把鼠標(biāo)移動(dòng)的 x,y 坐標(biāo)傳給鼠標(biāo)粒子對(duì)象的 x,y 值,第二層循環(huán)計(jì)算當(dāng)前粒子與其他粒子距離,如果粒子與粒子的距離小于某值則進(jìn)行畫線。

1.3 畫線

在每次 requestAnimationFrame 更新粒子 x,y 位置前先清空上一次粒子的位置,定義一個(gè) max 值用于判斷粒子與粒子之間距離是否小于 max 值如果小于 max 值,在他們之間畫線,畫線可以用 hsla(色相, 飽和度, 亮度, 透明度) 函數(shù),色值用一個(gè)變量做累加就好了,透明度也需要一個(gè)變量,可以看到效果上在粒子與粒子距離逐漸大于 max 值時(shí)線條是一個(gè)逐漸弱化的效果:(max - 兩點(diǎn)距離) / max;。

效果圖上可以看到粒子會(huì)跟著鼠標(biāo)走,判斷如果比對(duì)粒子是鼠標(biāo)點(diǎn)粒子,并且距離小于 max / 2 時(shí)讓與鼠標(biāo)粒子比對(duì)的其他粒子的 x,y 值遞減,比如粒子與粒子間距離小于 100 就給他畫線,此時(shí)粒子 x,y 值還在做速度遞增運(yùn)動(dòng)(現(xiàn)在的遞增是逐漸靠近鼠標(biāo)點(diǎn)位),在小于 50 的時(shí)候做反方向運(yùn)動(dòng)(逐漸遠(yuǎn)離鼠標(biāo)點(diǎn)),大于50靠近鼠標(biāo)點(diǎn),小于50遠(yuǎn)離鼠標(biāo)點(diǎn),這樣就行成了一個(gè)看似吸附點(diǎn)效果。


2、代碼

let mons = {
  x: -100,
  y: -100,
  r: 1
};
class Point {
        constructor (max, name) {
            this.x = Math.random() * canvas.width; // x坐標(biāo)
            this.y = Math.random() * canvas.width; // y坐標(biāo)
            this.r = 1;
            this.max = max;
            this.name = name;
            this.sx = Math.random() * 2 - 1; // 速度
            this.sy = Math.random() * 2 - 1; // 速度
            this.hue = random( 60, 100 );
            this.brightness = random(0, 250);
        }

        // 隨機(jī)移動(dòng)
        move (x, y) {
            this.x += this.sx;
            this.y += this.sy;
            this.hue += 1;

            if (this.name) {
                this.x = x;
                this.y = y;
            }
           
            if (this.x > canvas.width || this.x < 0) this.sx = -this.sx;
            if (this.y > canvas.height || this.y < 0) this.sy = -this.sy;
        }

        // 畫線
        drawLine (ctx, p, isDraw){
            let dx = this.x -p.x;
            let dy = this.y -p.y;
            let d = dx * dx + dy * dy;
            if (d < p.max) {
                if (p === mons.point && d > (p.max / 2)) {
                    this.x -= dx * 0.03;
                    this.y -= dy * 0.03;
                }
                let alpha = (p.max - d) / p.max;
                ctx.beginPath();
                ctx.strokeStyle ='hsla(' + this.hue + ', 100%, ' + this.brightness + '%, ' + alpha + ')'
                ctx.strokeWidth = 1;
                ctx.moveTo(this.x, this.y);
                ctx.lineTo(p.x, p.y);
                ctx.stroke();
            }
        }
    }

    let points = [];
    for (let i = 0; i < 200; i++) {
        points.push(new Point(6000));
    }
    mons.point = new Point(20000, 'touch');
    points.push(mons.point);

    const paint = () => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        for (let i = 0; i < points.length; i++) {
            points[i].move();
            if (points[i].name) {
                points[i].move(mons.x, mons.y);
            }
            for (let j = i + 1; j < points.length; j++){
                if (points[j] === mons.point) {
                    points[i].drawLine(ctx, points[j], true);
                }
                points[i].drawLine(ctx, points[j]);
            }
        }
    }

    function random( min, max ) {
        return Math.random() * ( max - min ) + min;
    }

    function loop(params) {
        requestAnimationFrame(loop);
        paint();
    }
    loop();


3、另一種效果

效果圖:

屏幕錄制2021-04-22 下午5

這個(gè)效果也是挺不錯(cuò)的,看起來很絲滑。

這個(gè)效果是在上一個(gè)效果的基礎(chǔ)上做了些修改,改成了類似鼠標(biāo)拖尾的效果,實(shí)現(xiàn)起來比上一個(gè)效果也簡單一些。

for 循環(huán) new Point 類添加到 points 粒子數(shù)組中,requestAnimationFrame 執(zhí)行粒子運(yùn)動(dòng)與畫線,去掉了鼠標(biāo)粒子與其他粒子距離的判斷只在鼠標(biāo)移動(dòng)時(shí) new Point 類添加到粒子數(shù)組把鼠標(biāo)的 x,y 值傳入 Point 類: points.push(new Point(x, y)) 在粒子數(shù)組長度大于 80 shift() 移除粒子數(shù)組第一項(xiàng)。

canvas.addEventListener('mousemove', function(e) {
        e.preventDefault();
        // 根據(jù)鼠標(biāo)位置添加點(diǎn)進(jìn)數(shù)組
        points.push(new Point(e.clientX,e.clientY));
        // 多余80個(gè)點(diǎn)移除數(shù)組第一個(gè)點(diǎn)
        if (points.length > 80) {
            points.shift();
        }
    });


4、整合封裝

做出兩個(gè)效果后就嘗試著整合封裝了一下,通過傳參 isBg 判斷展現(xiàn)的是哪種效果 true 為第一種效果 false 為第二種,pc端上兩種效果都能呈現(xiàn),但移動(dòng)端上目前設(shè)置只能展現(xiàn)第二種,第一種效果在手機(jī)上體驗(yàn)感比較差 100 個(gè)粒子以上運(yùn)動(dòng)就會(huì)比較卡了。

使用方法:

引入 js <script src="./particle-line.js"></script> js 源碼地址

let point = new particleLine({
  canvas: canvas, // canvas
  width: 寬, // canvas寬
  height: 高, // canvas高
  num: 粒子數(shù), // 粒子數(shù)
  isBg: true // true: 第一種效果 false: 第二種效果
});
point.play(); // 執(zhí)行

demo 鏈接:demo

參考博客:

html canvas粒子線條組合動(dòng)畫背景特效

5、總結(jié)

這個(gè) demo 實(shí)現(xiàn)起來還是比較簡單的,效果也挺不錯(cuò)的。難點(diǎn)應(yīng)該在于鼠標(biāo)粒子和普通粒子間的計(jì)算。canvas 粒子還有很多其他炫酷的效果,比如粒子變換特定形狀,用getImageData 方法獲取畫布指定矩形像素,通過獲取到的點(diǎn)位跟 RGBA 值用粒子繪畫出來。


6、了解更多

原文鏈接:canvas 粒子線條

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容