雙十一光棍節又要來臨了,每一年這個時候都是本人最苦悶的時刻。日漸消瘦的錢包,愈發乾涸的雙手,雖然變強了,頭卻變涼了。今年必定要搞點事情!javascript
最近聽女神說想談戀愛了,✧(≖ ◡ ≖) 嘿嘿,必定不能放過這個機會,給她來個不同的表白。html
做爲成天搞可視化的前端攻城獅,最早想到的就是常玩的各類粒子。那麼我們就一塊兒來把這個粒子系統玩出花來吧。 演示地址前端
首先,我們想下要如何將一系列的粒子組成一句表白呢?java
實現原理其實很簡單,Canvas 中有個 getImageData 的方法,能夠獲得一個矩形範圍全部像素點數據。那麼咱們就試試來獲取一個文字的形狀吧。web
第一步,用 measureText 的方法來計算出文字適當的尺寸和位置。canvas
// 建立一個跟畫布等比例的 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 裏看眼數組
好的。同窗們注意,我要開始變形了 [推眼鏡] 。app
getImageData 獲取的像素數據是一個 Uint8ClampedArray (值是 0 - 255 的數組),4 個數一組分別對應一個像素點的 R G B A 值。咱們只須要判斷 i * 4 + 3 不爲 0 就能夠獲得須要的字體形狀數據了。dom
// 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");
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
};
}
}
複製代碼
完美 😝。
而後咱也能夠用 drawImage 繪製圖片來代替 arc 畫點。
等等!!前面的效果總以爲哪裏不對勁,好像有些卡 。
我這使用了 3000 個粒子,對比下使用離屏繪製先後的幀率。
如今惟一限制你的就是想象力了。你們一塊兒去征服老闆,征服女神!
這個雙十一脫貧脫單不脫髮!
好了不說了,女神喊我修電腦去了。
y=sqrt(abs(x))-sqrt(cos(x))*cos(40x)
文章可隨意轉載,但請保留此 原文連接。 很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。