炫酷粒子表白 | 聽說女神都想談戀愛了!

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

那麼咱們就一起來把這個粒子系統玩出花來吧

演示地址:

https://es2049.studio/work-show/textPraticle/

如何將一系列的粒子組成一句表白呢?

實現原理其實很簡單,Canvas 中有個 getImageData 的方法,可以得到一個矩形範圍所有像素點數據。那麼我們就試試來獲取一個文字的形狀吧。

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

	// 創建一個跟畫布等比例的 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';
	constmeasure=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 裏看眼

同學們注意,我要開始變形了 [推眼鏡] 。 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 上
	}

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

能不能再給力一點

說好的粒子系統,現在只是 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),
	   };
   }
}

大功告成!

既然是 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");
					constoffscreenCanvas = document.createElement('canvas');
					constoffscreenCanvasCtx = 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
		   };
	   }
}

完美 😝

然後咱也可以用 drawImage 繪製圖片來代替 arc 畫點。

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

優化小提示

1、分層。如果還需要增加一些其他的內容到 Canvas 中的話,可以考慮拆出多個 Canvas 來做。

2、減少屬性設置。包括 lineWidth、fillStyle 等等。Canvas 上下文是很複雜的一個對象,當你調它的一些屬性設置時消耗的性能還是不少的。arc 之類的畫圖方法也要減少。

3、離屏繪製。不用 arc 畫點,要怎麼辦。上面的延遲其實就是每次畫點時都調用了一遍 fillStyle、arc。但是當我們繪製圖片 drawImage 時,性能明顯會好上很多。drawImage 除了直接繪圖片外,還能繪製另一個 Canvas,所以我們提前將這些點畫到一個不在屏幕上的 Canvas 裏就可以了。

4、減少 js 計算,避免堵塞進程,可以使用 web worker。當然咱們目前的計算完全用不上這個 我這使用了 3000 個粒子,對比下使用離屏繪製前後的幀率。

總結

現在唯一限制你的就是想象力了。大家一起去征服老闆,征服女神!

參考:

deformable particles

https://codepen.io/airen/pen/owRMoY

Canvas 參考手冊

http://www.w3school.com.cn/tags/html_ref_canvas.asp

y=sqrt(abs(x))-sqrt(cos(x))*cos(40x)

作者:ES2049

來源:https://juejin.im/post/5bdfe1dbe51d45054771f9d4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章