程式設計師的小浪漫----文字粒子效果

王興欣發表於2018-02-26

預覽

程式設計師的小浪漫----文字粒子效果

完整專案預覽----預覽地址;

粒子效果原理

在canvas中,可以通過getImageData()方法來獲取畫素資料。

ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 1, 1);
const imageData = ctx.getImageData(0, 0, 1, 1);
複製程式碼

imageData有三個屬性:

  • data:陣列,包含了畫素資訊,每個畫素會有四個長度,如[255,0,0,255, ... ,255,127,0,255],分別代表該畫素的RGBA值。
  • widthimageData物件的寬。
  • heightimageData物件的高。

首先在canvas寫上某種顏色文字,再去分析畫素資料(比如改畫素是否有透明度等),然後自己記錄下該畫素點的位置

下例是通過改變畫素的資料而重新寫出來的文字。

程式設計師的小浪漫----文字粒子效果

ctx.font = 'bold 40px Arial';
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText('你好啊', 60, 20);

document.querySelector('#button').addEventListener('click', function(){
    const imgData = ctx.getImageData(0, 0, 120, 40);
    for(let i = 0;i < imgData.data.length; i+=4){
    	if(imgData.data[i + 3] == 0) continue;
    	imgData.data[i] = 255;
    	imgData.data[i + 1] = 0;
    	imgData.data[i + 2] = 0;
    	// imgData.data[i + 3] = 255;  這個代表的是透明度 透明度不變 255最高 0最低
    }
    ctx.putImageData(imgData,120,0);
});
複製程式碼

這段程式碼只是示例說明一下,實際上才不會有人這麼腦殘去換顏色吧。

獲取點位置

要獲取點的位置,首先要將字寫在畫布上,但是字又不能讓別人看到。所以可以動態建立一個畫布,這個畫布不會append到任何節點上,只會用於寫字。

const cache = document.createElement('canvas');

將寬高等與展示的畫布設定成一樣的。(不貼這部分的程式碼了)

建立一個物件,用於獲取點的位置

const ShapeBuilder = {
    //初始化字的對齊方式等,我認為middle 與 center比較好計算一點
    init(width, height){
        this.width = width;
        this.height = height;
        this.ctx = cache.getContext('2d');
        this.ctx.textBaseline = 'middle';
        this.ctx.textAlign = 'center';
    },
    //獲取位置之前必須先要寫入文字。 這裡的size=40是預設值
    write(words, x, y, size = 40){
        //清除之前寫的字。
        this.ctx.clearRect(0, 0, this.width, this.height);
        this.font = `bold ${size}px Arial`;
        this.ctx.fillText(words, x, y);
        //記錄當前文字的位置,方便計算獲取畫素的區域
        this.x = x;
        this.y = y;
        this.size = size;
        this.length = words.length;
    },
    getPositions(){
        //因為imgData資料非常的大,所以儘可能的縮小獲取資料的範圍。
        const xStart = this.x - (this.length / 2) * this.size, 
            xEnd = this.x + (this.length / 2) * this.size,
            yStart = this.y - this.size / 2, 
            yEnd = this.y + this.size / 2, 
            
            //getImageData(起點x, 起點y, 寬度, 高度);
            data = this.ctx.getImageData(xStart, yStart, this.size * this.length, this.size).data;
            
        //間隔 (下面有介紹)
        const gap = 4;
        
        let positions = [], x = xStart, y = yStart;
        
        for(var i = 0;i < data.length; i += 4 * gap){
            if(data[i+3] > 0){
                positions.push({x, y});	
            }
            
            x += gap;
            
            if(x >= xEnd){
                x = xStart;
                y += gap;
                i += (gap - 1) * 4 * (xEnd - xStart);
            }
        }
        return positions;
    }
}

ShapeBuilder.init();
複製程式碼

關於gap:在迴圈imgData陣列的時候,資料量太大可能會造成卡頓,所以可以使用間隔來獲取座標點的方法。不過可能會造成文字部分地方缺失。就需要個人來權衡利弊,自己來調整了。

gap的值必須能被xEnd-xStart給整除,不然會造成獲取座標點錯位的後果。

關於canvasmiddlecenter的規則:

this.ctx.font = 'bold 40px Arial';
this.ctx.fillText('你好',40 ,20);
複製程式碼

效果如下圖所示

程式設計師的小浪漫----文字粒子效果

fillText設定的座標點剛好會是整個字的中點,就是圖中middlecenter的交點。其實以其它對齊方式也是可以的,看個人喜好。

更多的對齊規則參考HTML 5 Canvas 參考手冊的文字。

建立微粒類

微粒應該隨機生成,然後移動到指定的位置去。

微粒類的屬性:

自身當前位置(x,y), 目標位置:(xEnd,yEnd),自身大小(size),自身顏色(color),移動快慢(e)

方法:go():每一幀都要移動一段距離,render():渲染出微粒(我用心形的形狀)

class Particle {
    constructor({x, y, size = 2, color, xEnd, yEnd, e = 60} = {}){
        this.x = x;
        this.y = y;
        this.size = size;
        this.color = color ||  `hsla(${Math.random() * 360}, 90%, 65%, 1)`;
        this.xEnd = xEnd;
        this.yEnd = yEnd;
        
        //經過e幀之後到達目標地點
        this.e = e;
        //計算每一幀走過的距離
        this.dx = (xEnd - x) / e;
		this.dy = (yEnd - y) / e;
    }
    go(){
        //到目的後保持不動 (其實這裡也可以搞點事情的)
        if(--this.e <= 0) {
            this.x = this.xEnd;
            this.y = this.yEnd;
            return ;
        }
        this.x += this.dx;
        this.y += this.dy;
    }
    render(ctx){
        this.go();
        //下面是畫出心型的貝塞爾曲線
        ctx.beginPath();
        ctx.fillStyle = this.color;
        ctx.moveTo(this.x + 0.5 * this.size, this.y + 0.3 * this.size);
        ctx.bezierCurveTo(this.x + 0.1 * this.size, this.y, this.x, 
                        this.y + 0.6 * this.size, this.x + 0.5 * 
                        this.size, this.y + 0.9 * this.size);
        ctx.bezierCurveTo(this.x + 1 * this.size, this.y + 0.6 * 
                        this.size, this.x + 0.9 * this.size, this.y, 
                        this.x + 0.5 * this.size,
                        this.y + 0.3 * this.size);
        ctx.closePath();
        ctx.fill();
        return true;
    }
}

複製程式碼

微粒類最基本的屬性與方法就是這些,如果要讓粒子更好看一點,或者更生動一點,可以自己新增一些屬性與方法。

具體流程

const canvas = {
    init(){
        //設定一些屬性
        this.setProperty();
        //建立微粒
        this.createParticles();
        //canvas的迴圈
        this.loop();
    },
    setProperty(){
        this.ctx = studio.getContext('2d');
        this.width = document.body.clientWidth;
        this.height = document.body.clientHeight;
        this.particles = [];
    },
    createParticles(){
        let dots;
        //ShapeBuilder.write(words, x, y, size)
        ShapeBuilder.write('每個字都是',this.width / 2, this.height / 3, 120);
        dots = ShapeBuilder.getPositions(6);
        ShapeBuilder.write('愛你的模樣', this.width / 2, this.height * 2 / 3, 120);
        dots = dots.concat(ShapeBuilder.getPositions(6));
        //dots已經獲取到了字的座標點 
        //每一個微粒的目標地點都是dots的座標
        //每一個微粒都隨機出生在畫布的某個位置
        for(let i = 0; i < dots.length; i++){
            this.particles.push(new Particle({
                xEnd:dots[i].x, 
                yEnd:dots[i].y , 
                x: Math.random() * this.width, 
                y: Math.random() * this.height, 
                size:6, 
                color:'hsla(360, 90%, 65%, 1)'
            }));
        }
    },
    loop(){
        //每一幀清除畫布,然後再渲染微粒就可以了
        requestAnimationFrame(this.loop.bind(this));
        this.ctx.clearRect(0, 0, this.width, this.height);
        for(var i = 0; i < this.particles.length; i++){
            this.particles[i].render(this.ctx);
        }
    }
}

canvas.init();
複製程式碼

程式設計師的小浪漫----文字粒子效果

如果想要給每個粒子加上小尾巴的話,那麼在每一幀的時候,就不要清除畫布,而且覆蓋一層有透明度的底色。

//修改loop方法
//this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.fillStyle = 'rgba(0,0,0,0.2)';
this.ctx.fillRect(0, 0, this.width, this.height);
複製程式碼

這樣的話會變成如下效果

程式設計師的小浪漫----文字粒子效果

最後

在這這篇文章的時候,並沒有注意太多細節,比如gap應該是可以被設定的,或者是一個被特殊標註的常量,而不應該隨便寫在方法中。對於本例的程式碼,切勿生搬硬套,重要的是要理解原理,以及自己親自動手嘗試

我也是在寫這篇文章的過程中,才發現了之前獲取position一個不精準的地方。

這裡只講了粒子效果最基礎的用法,實際上還可以做出很多非常炫酷的效果

比如在粒子到達目的地後還可以抖動什麼的

粒子形狀、顏色的變化等等。

這個專案還可以搞很多事情的,大家也可以自己多來嘗試弄些更加炫酷的效果。

煙花效果可以看一下我的上一篇程式設計師的小浪漫----煙火

完整專案

github專案地址

如果覺得還不錯,請star一個吧。

參考專案

github上的一個專案---- shape-shifter

這個專案我覺得非常不錯,可惜作者都消失好多年了。

codepen.io 上的一個作品 ---- Love In Hearts

相關文章