1. 程式人生 > >炫酷粒子表白 | 聽說女神都想談戀愛了!

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

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

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

演示地址:

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
		   };
	   }
}

完美