炫酷粒子表白,雙十一脫單靠它了!

我老婆

​ 雙十一光棍節又要來臨了,每一年這個時候都是本人最苦悶的時刻。日漸消瘦的錢包,愈發乾涸的雙手,雖然變強了,頭卻變涼了。今年必定要搞點事情!javascript

<img src="https://img.alicdn.com/tfs/TB...; alt="fxxking things" width="160">html

​ 最近聽女神說想談戀愛了,✧(≖ ◡ ≖) 嘿嘿,必定不能放過這個機會,給她來個不同的表白。前端

<img src="https://img.alicdn.com/tfs/TB...; alt="我老婆" width="360">java

做爲成天搞可視化的前端攻城獅,最早想到的就是常玩的各類粒子。那麼我們就一塊兒來把這個粒子系統玩出花來吧。
演示地址web

用粒子組成文字

首先,我們想下要如何將一系列的粒子組成一句表白呢?canvas

實現原理其實很簡單,Canvas 中有個 getImageData 的方法,能夠獲得一個矩形範圍全部像素點數據。那麼咱們就試試來獲取一個文字的形狀吧。數組

第一步,用 measureText 的方法來計算出文字適當的尺寸和位置。app

// 建立一個跟畫布等比例的 canvas
const width = 100;
const height = ~~(width * this.height / this.width); // this.width , this.height 說整個畫布的尺寸
const offscreenCanvas = document.createElement('canvas');
const offscreenCanvasCtx = offscreenCanvas.getContext('2d');
offscreenCanvas.setAttribute('width', width);
offscreenCanvas.setAttribute('height', height);

// 在這離屏 canvas 中將咱們想要的文字 textAll 繪製出來後,再計算它合適的尺寸
offscreenCanvasCtx.fillStyle = '#000';
offscreenCanvasCtx.font = 'bold 10px Arial';
const measure = offscreenCanvasCtx.measureText(textAll); // 測量文字,用來獲取寬度
const size = 0.8;
// 寬高分別達到屏幕0.8時的size
const fSize = Math.min(height * size * 10 / lineHeight, width * size * 10 / measure.width);  // 10像素字體行高 lineHeight=7 magic
offscreenCanvasCtx.font = `bold ${fSize}px Arial`;

// 根據計算後的字體大小,在將文字擺放到適合的位置,文字的座標起始位置在左下方
const measureResize = offscreenCanvasCtx.measureText(textAll);
// 文字起始位置在左下方
let left = (width - measureResize.width) / 2;
const bottom = (height + fSize / 10 * lineHeight) / 2;
offscreenCanvasCtx.fillText(textAll, left, bottom);

我們能夠 appendChild 到 body 裏看眼dom

textAll

好的。同窗們注意,我要開始變形了 [推眼鏡] 。性能

getImageData 獲取的像素數據是一個 Uint8ClampedArray (值是 0 - 255 的數組),4 個數一組分別對應一個像素點的 R G B A 值。咱們只須要判斷 i * 4 + 3 不爲 0 就能夠獲得須要的字體形狀數據了。

// texts 全部的單詞分別獲取 data ,上文的 textAll 是 texts 加一塊兒
Object.values(texts).forEach(item => {
    offscreenCanvasCtx.clearRect(0, 0, width, height);
    offscreenCanvasCtx.fillText(item.text, left, bottom);
    left += offscreenCanvasCtx.measureText(item.text).width;
    const data = offscreenCanvasCtx.getImageData(0, 0, width, height);
    const points = [];
    // 判斷第 i * 4 + 3 位是否爲0,得到相對的 x,y 座標(使用時需乘畫布的實際長寬, y 座標也須要取反向)
    for (let i = 0, max = data.width * data.height; i < max; i++) {
        if (data.data[i * 4 + 3]) {
            points.push({
                x: (i % data.width) / data.width,
                y: (i / data.width) / data.height
            });
        }
    }
    // 保存到一個對象,用於後面的繪製
    geometry.push({
        color: item.hsla,
        points
    });
})

制定場景,繪製圖形

文字圖形的獲取方式以及搞定了,那麼我們就能夠把內容總體輸出了。我們定義一個簡單的腳本格式。

// hsla 格式方便之後作色彩變化的擴展
const color1 = {h:197,s:'100%',l:'50%',a:'80%'};
const color2 = {h:197,s:'100%',l:'50%',a:'80%'};
// lifeTime 禎數
const Actions = [
    {lifeTime:60,text:[{text:3,hsla:color1}]},
    {lifeTime:60,text:[{text:2,hsla:color1}]},
    {lifeTime:60,text:[{text:1,hsla:color1}]},
    {lifeTime:120,text:[
        {text:'I',hsla:color1},
        {text:'❤️',hsla:color2},
        {text:'Y',hsla:color1},
        {text:'O',hsla:color1},
        {text:'U',hsla:color1}
    ]},
];

根據預設的腳本解析出每一個場景的圖形,加一個 tick 判斷是否到了 lifeTime 切換到下一個圖形從新繪製圖形。

function draw() {
    this.tick++;
    if (this.tick >= this.actions[this.actionIndex].lifeTime) {
        this.nextAction();
    }
    this.clear();
    this.renderParticles(); // 繪製點
    this.raf = requestAnimationFrame(this.draw);
}

function nextAction() {
    ....//切換場景 balabala..
    this.setParticle(); // 隨機將點設置到以前獲得的 action.geometry.points 上
}

1

這樣我們基本的功能已經完成了。

能不能再給力一點

說好的粒子系統,如今只是 context.arc 簡單的畫了一點。那我們就來加個粒子系統吧。

class PARTICLE {
    // x,y,z 爲當前的座標,vx,vy,vz 則是3個方向的速度
    constructor(center) {
        this.center = center;
        this.x = 0;
        this.y = 0;
        this.z = 0;
        this.vx = 0;
        this.vy = 0;
        this.vz = 0;
    }
    // 設置這些粒子須要運動到的終點(下一個位置)
    setAxis(axis) {
        this.nextX = axis.x;
        this.nextY = axis.y;
        this.nextZ = axis.z;
        this.color = axis.color;
    }
    step() {
        // 彈力模型 距離目標越遠速度越快
        this.vx += (this.nextX - this.x) * SPRING;
        this.vy += (this.nextY - this.y) * SPRING;
        this.vz += (this.nextZ - this.z) * SPRING;
        // 摩擦係數 讓粒子能夠趨向穩定
        this.vx *= FRICTION;
        this.vy *= FRICTION;
        this.vz *= FRICTION;
        
        this.x += this.vx;
        this.y += this.vy;
        this.z += this.vz;
    }
    getAxis2D() {
        this.step();
        // 3D 座標下的 2D 偏移,暫且只考慮位置,不考慮大小變化
        const scale = FOCUS_POSITION / (FOCUS_POSITION + this.z);
        return {
            x: this.center.x + (this.x * scale),
            y: this.center.y - (this.y * scale),
        };
    }
}

2

大功告成!

既然是 3D 的粒子,其實這上面還有不是文章可作,同窗們能夠發揮想象力來點更酷炫的。

還有什麼好玩的

上面是將粒子擺成文字。那我們固然也能夠直接寫公式擺出個造型。

// Actions 中用 func 代替 texts
{
    lifeTime: 100,
    func: (radius) => {
        const i = Math.random() * 1200;
        let x = (i - 1200 / 2) / 300;
        let y = Math.sqrt(Math.abs(x)) - Math.sqrt(Math.cos(x)) * Math.cos(30 * x);
        return {
            x: x * radius / 2,
            y: y * radius / 2,
            z: ~~(Math.random() * 30),
            color: color3
        };
    }
}

再把剛纔文字轉換形狀的方法用一下

{
    lifeTime: Infinity,
        func: (width, height) => {
            if(!points.length){
                const img = document.getElementById("tulip");
                const offscreenCanvas = document.createElement('canvas');
                const offscreenCanvasCtx = offscreenCanvas.getContext('2d');
                const imgWidth = 200;
                const imgHeight = 200;
                offscreenCanvas.setAttribute('width', imgWidth);
                offscreenCanvas.setAttribute('height', imgHeight);
                offscreenCanvasCtx.drawImage(img, 0, 0, imgWidth, imgHeight);
                let imgData = offscreenCanvasCtx.getImageData(0, 0, imgWidth, imgHeight);
                for (let i = 0, max = imgData.width * imgData.height; i < max; i++) {
                    if (imgData.data[i * 4 + 3]) {
                        points.push({
                            x: (i % imgData.width) / imgData.width,
                            y: (i / imgData.width) / imgData.height
                        });
                    }
                }
            }

            const p = points[~~(Math.random() * points.length)]
            const radius = Math.min(width * 0.8, height * 0.8);
            return {
                x: p.x * radius - radius / 2,
                y: (1 - p.y) * radius - radius / 2,
                z: ~~(Math.random() * 30),
                color: color3
            };
        }
}

3

​完美 😝。

而後咱也能夠用 drawImage 繪製圖片來代替 arc 畫點。

<img src="https://img.alicdn.com/tfs/TB...; width="160">

等等!!前面的效果總以爲哪裏不對勁,好像有些卡 。

穩得一筆

優化小提示

  1. 分層。若是還須要增長一些其餘的內容到 Canvas 中的話,能夠考慮拆出多個 Canvas 來作。
  2. 減小屬性設置。包括 lineWidth、fillStyle 等等。Canvas 上下文是很複雜的一個對象,當你調它的一些屬性設置時消耗的性能仍是很多的。arc 之類的畫圖方法也要減小。
  3. 離屏繪製。不用 arc 畫點,要怎麼辦。上面的延遲其實就是每次畫點時都調用了一遍 fillStyle、arc。可是當咱們繪製圖片 drawImage 時,性能明顯會好上不少。drawImage 除了直接繪圖片外,還能繪製另外一個 Canvas,因此咱們提早將這些點畫到一個不在屏幕上的 Canvas 裏就能夠了。
  4. 減小 js 計算,避免堵塞進程,可使用 web worker。固然我們目前的計算徹底用不上這個。

我這使用了 3000 個粒子,對比下使用離屏繪製先後的幀率。

方不方圓了

總結

如今惟一限制你的就是想象力了。你們一塊兒去征服老闆,征服女神!

這個雙十一脫貧脫單不脫髮!

好了不說了,女神喊我修電腦去了。

參考

文章可隨意轉載,但請保留此 原文連接
很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。
相關文章
相關標籤/搜索