作者簡介 Coco 螞蟻金服·資料體驗技術團隊
效果演示
3D粒子動畫效果如下:
本示例所構成並不複雜,主要分成以下幾部分:
- 漸變的背景
- 不斷切換的3D粒子模型(入場的散點也是粒子模型的一種形態)
- 同時切換的文字
實現主要是基於threejs做的,接下來我會分別講解各部分的實現,不過不會介紹基礎。基礎內容可以去官網找到~
漸變的背景
scene的background可以接收Color、Texture或CubeTexture。在本示例中,我們使用Texture就可以達到漸變效果。Texture可以接收canvas或圖片,此處我們使用canvas畫出漸變效果,具體程式碼如下:
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const context = canvas.getContext('2d');
const gradient = context.createLinearGradient(0, 0, width, 0);
gradient.addColorStop(0, '#4e22b7');
gradient.addColorStop(1, '#3292ff');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
const scene = new THREE.Scene();
scene.background = texture;
複製程式碼
減少模型粒子數
接下來給大家介紹的是一個黑科技——減少模型粒子數。通常情況下設計師會給你符合設計稿內所需的模型,粒子數不會比你所需的多,但凡是有例外的時候。另外,粒子的多少是影響效能的關鍵之一,在這個示例中我測試過1W個粒子就使動畫有卡頓感(當然這還跟執行的本本效能有很大的關係)。如果你正好有這方面的煩惱,那這個方法或許對你有用。
實現思路:在X,Y,Z軸上分別計算相鄰點之間的距離,如果大於距離小於設定值則刪除。具體程式碼如下:
// pos為粒子的資料,單項資料分別含x,y,z座標值 {x, y, z}
const all = [];
const yObjs = {};
const xObjs = {};
const zObjs = {};
// 對所有點按照x,y,z值進行分組,分組後的資料分別存在xObjs,yObjs,zObjs
for (var i = pos.length-1; i>=0; i--) {
const p = pos[i];
const yKey = getKey(p.y);
const xKey = getKey(p.x);
const zKey = getKey(p.z);
if (!yObjs[yKey]) { yObjs[yKey] = []; }
if (!xObjs[xKey]) { xObjs[xKey] = []; }
if (!zObjs[zKey]) { zObjs[zKey] = []; }
const item = {x, y, z, r, g, b};
yObjs[yKey].push(item);
xObjs[xKey].push(item);
zObjs[zKey].push(item);
all.push(item);
}
// 對x,y,z值進行排序
const xKeys = orderAscKeys(xObjs);
const yKeys = orderAscKeys(yObjs);
const zKeys = orderAscKeys(zObjs);
// 提取在x,y,z軸上需刪除的座標值
const xDels = getDelKeys(xKeys, 4); // 4為點與點的間距
const yDels = getDelKeys(yKeys, 4);
const zDels = getDelKeys(zKeys, 4);
const result = [];
for (var i = all.length-1; i>=0; i--) {
const item = all[i];
if ( !(xDels.indexOf(getKey(item.x))>-1 || yDels.indexOf(getKey(item.y))>-1 || zDels.indexOf(getKey(item.z))>-1 )) {
result.push(item);
}
}
return result;
function getKey(x) {
let r = x.toFixed(1);
if (r==='-0.0') {
r = '0.0'
}
return r;
}
function orderAscKeys (obj) {
return Object.keys(obj).sort((a, b)=>{
return Number(a) - Number(b);
});
}
function getDelKeys(keys, d) {
const dels = [];
keys.reduce((prev, curr, idx)=>{
let res = curr;
if (curr-prev < d) {
dels.push(curr);
res = prev;
}
return res;
});
return dels;
}
複製程式碼
注:此方法只有對粒子排序比較整齊的模型,如果模型本身的粒子排序混亂使用此方法的效果可能會不理想。
模型切換動畫
建立粒子物件
在整個動畫效果中,粒子通過變換位置流暢的組合著各個模型,如何做到這一點呢?
如果你給每個模型都 new Geometry出來,這回讓你手忙腳亂,而且效果不理想。比較好的處理方式是將這5個模型用一個Geometry,通過改變vertices屬性來達到模型的變換。不過有一點需要注意,vertices屬性可以改變裡面每一項的值,但不能在定義之後再新增項的數量。這就要求我們在最初定義Geometry的vertices的值個個數必須是幾個模型中點數最多的那個。
const config = this.config;
const textureLoader = new THREE.TextureLoader();
textureLoader.crossOrigin = '';
const mapDot = textureLoader.load('img/gradient.png'); // 圓點
this.mapDot = mapDot;
const geometry = new THREE.Geometry();
const count = config.totalCount; // 四個模型中最多個粒子總數值
for (let i = 0; i < count; i++) {
let x = 0, y = 0, z = 0;
x = Math.random() * 2000 - 1000;
y = Math.random() * 2000 - 1000;
z = Math.random() * 8000 - 4000;
geometry.vertices.push(new THREE.Vector3(x, y, z));
geometry.colors.push(new THREE.Color(1, 1, 1));
}
const material = new THREE.PointsMaterial({
size: 20,
map: mapDot,
depthTest: true,
alphaTest: .1,
opacity: 1,
side: THREE.DoubleSide,
transparent: !0,
vertexColors: THREE.VertexColors,
});
const points = new THREE.Points(geometry, material);
// 調整模型姿勢
points.rotation.z = Math.PI;
points.rotation.y = Math.PI;
points.rotation.x = -Math.PI * .3;
points.position.y = 240;
points.position.x = 100;
points.position.z = 240;
this.scene.add(points);
this.points = points;
複製程式碼
webGL採用右手座標系,而canvas二維座標系是x軸往右延伸、y軸往下延伸,因為存在這樣的差異直接通過圖片生成的3維模型會是倒的。在實際處理的時候我們可以通過模型的整理做調整,也可以換算每個點的座標值。具體做法可以根據實際想要的效果來選擇。
座標系的差異,如圖:
狀態切換
整個動畫過程中有以下幾種狀態:
- 入場前奏動畫(即散點狀態),用於資料準備和動畫過渡效果
- 模型切換動畫進行時
- 模型切換間隔休息時
狀態的管理與切換具體程式碼如下:
// 一切模型都已經準備妥當
if (this.objectsData && this.points && this.index !== undefined) {
const config = this.config;
const len = config.objs.length;
const idx = this.index % len; // 當前模型序號
const item = this.objectsData[idx];
const geometry = this.points.geometry;
this.points.material.needsUpdate = true;
geometry.verticesNeedUpdate = true;
geometry.colorsNeedUpdate = true;
// 前奏因資料準備時間不較長,不額外加延遲時間
const delay = (this.index === 0) ? config.delay * 60 * 0 : config.delay * 60;
if (item.waiting < delay) {
// 等待時間,包含了前奏(1)和切換間隔時間(3)
item.waiting++;
} else if (item.waiting >= delay && item.count < config.totalCount - 1) {
// 動畫進行時(2)
let nd;
if (item.count === 0) {
config.onLeave && typeof config.onLeave === 'function' && config.onLeave();
nd = idx;
const prevIdx = (idx - 1 > -1) ? idx - 1 : len - 1;
const prev = this.objectsData[prevIdx];
if (prev) {
// 執行下一個模型切換動畫時,將前一個的執行時計數和休息時計數都歸零
prev.count = 0;
prev.waiting = 0;
}
}
// 執行動畫
this.particleAnimation(this.points, item, nd);
} else if (item.waiting >= delay && item.count >= config.totalCount - 1) {
// 切換到下一個模型
this.index++;
}
}
複製程式碼
執行動畫
模型切換動畫執行時時按批執行的,並不是所有粒子一起開始運動的。
這樣做的好處是:
- 避免出現因單位時間(16.67ms)內因計算量過大而出現畫面卡頓感;單位時間是由requestAnimationFrame執行的頻率決定的。
- 分組動畫會讓動畫更有層次感
/**
* points: 粒子物件
* item: 當前模型資料
* idx: 當前模型序號
*/
particleAnimation(points, item, idx) {
const geometry = points.geometry;
const vertices = geometry.vertices;
const colors = geometry.colors;
const len = vertices.length;
if (item.count >= len) { return; }
if (!item.vTween) {
item.vTween = [];
}
const config = this.config;
let isOdd = this.index % 2===0;
// 每組動畫執行的粒子個數
const cnt = 1000;
for (let j = item.count, l = item.count + cnt; j < l && j < len - 1; j++) {
const n = j % item.length;
const p = item.data[n];
if (!p) { return; }
// TWEEN只new一次,減少多次例項化的開銷
if (!item.vTween[j] && vertices[j]) {
item.vTween.push(new TWEEN.Tween(vertices[j])
.easing(TWEEN.Easing.Exponential.In)
);
}
item.vTween[j].stop();
// 奇偶序號的模型位置不一樣,調整x座標資料
const x = (isOdd) ? p.x : p.x-400;
item.vTween[j].to({ x, y: p.y, z: p.z }, config.speed).start();
}
item.count = item.count + cnt;
}
複製程式碼
TWEEN是一個補間動畫庫,使用方法也很簡單,詳情參見說明文件。TWEEN提供了以下幾種方式,可以自行替換使用,如圖:
結束語
文字部分的動畫是用css3實現的,網路上已經很多有關css3動畫相關的文件就不再詳述,這裡就簡單列出相關樣式程式碼以作參考。
.leaving {
transition: transform .7s, opacity .7s;
transform: translate3d(0px, -205%, 0);
opacity: 0;
}
.entering {
transition: opacity .7s;
opacity: 1;
}
複製程式碼
至此整個動畫效果中可能遇到的問題都已經一一闡述,有興趣的同學可以自動動手嘗試一下。如有更好的實現方式歡迎提出來共享。
感興趣的同學可以關注專欄或者傳送簡歷至'shanshan.hongss####alibaba-inc.com'.replace('####', '@'),歡迎大家加入~~