預覽
完整專案預覽----預覽地址;
粒子效果原理
在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值。width
:imageData
物件的寬。height
:imageData
物件的高。
首先在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
給整除,不然會造成獲取座標點錯位的後果。
關於canvas
中middle
與center
的規則:
this.ctx.font = 'bold 40px Arial';
this.ctx.fillText('你好',40 ,20);
複製程式碼
效果如下圖所示
fillText
設定的座標點剛好會是整個字的中點,就是圖中middle
與center
的交點。其實以其它對齊方式也是可以的,看個人喜好。
更多的對齊規則參考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
一個不精準的地方。
這裡只講了粒子效果最基礎的用法,實際上還可以做出很多非常炫酷的效果
比如在粒子到達目的地後還可以抖動什麼的
粒子形狀、顏色的變化等等。
這個專案還可以搞很多事情的,大家也可以自己多來嘗試弄些更加炫酷的效果。
煙花效果可以看一下我的上一篇,程式設計師的小浪漫----煙火
完整專案
如果覺得還不錯,請star一個吧。
參考專案
github上的一個專案---- shape-shifter
這個專案我覺得非常不錯,可惜作者都消失好多年了。
codepen.io 上的一個作品 ---- Love In Hearts