- 蘇格團隊
- 作者:Jason
前言
某一天我收到了產品發來的微信訊息。小X,我們的業務現在需要一個類似加入購物車的掉落動畫,經過組織的慎重考慮,這個需求就交給你了。於是便有了這篇文章。本文並沒有描述多少高深的技術,更多的是一些筆者在做動畫時對動畫原理的思考以及如何優化動畫的一些思路。實現效果如下:
技術分析
前端實現動畫的方式有很多。無論是JS動畫,CSS動畫,Canvas動畫還是SVG動畫,哪怕是GIF動畫實現一個簡單的拋物線都是足夠的。但考慮業務場景的需求以及可玩性,最終決定使用JS來實現這個動畫。
實現分析
在筆者看來大多數動畫效果,歸根到底還是 數學公式的應用。所謂拋物線動畫也無非就是讓元素的運動符合拋物線的運動軌跡。
拋物線的方程為: Y = A*X*X + B*X + C
也許大家看到這個公式有點陌生。但曾經物理老師唸唸有詞的 L(距離) = 1/2*A(加速度)*T*T(時間)
想必大家都一定熟記於心。筆者正是利用這個公式來完成拋物線動畫。
具體實現
步驟一 獲取拋物線的起點和終點
由於業務本身的特殊性,需要在使用者點選物品時獲取到該元素在視窗中的絕對位置。即元素相對於瀏覽器可見區域的X, Y的座標。
這裡筆者推薦使用getBoundingClientRect()
函式結合具體業務計算絕對位置。
當然,在一些場景裡你可以直接使用滑鼠點選位置或者其他任意方法獲取動畫的起點。
步驟二 設定拋物線引數
1. 加速度
加速度A決定了元素在設定方向(下文都用垂直方向代替)的速度變化快慢。當動畫的起點和終點都固定時,由公式 L(垂直距離) = 0.5 * A(加速度) * T * T(時間)
可得出此時 加速度A與時間T的平方成反比。
需要注意的是,正的加速度A會一直擴大幀與幀之間的垂直移動距離,所以過大的加速度A可能會導致 動畫的末期小球有閃爍感。
2. 時間 T 與 X 軸初速度
在拋物線的動畫中,一般的我們認為元素的 水平移動速度固定。那麼同樣由公式 L(水平距離) = T * Xspeed(水平速度)
可得出水平速度Xspeed實際上決定了動畫的執行時長。
綜合加速度的概念,我們可以得出以下結論:
當動畫的起始點和結束點一定時,若我們設定X軸的初速度為固定值,則動畫的執行時長被固定,此時為了讓小球達到既定位置。加速度A需要計算生成。 具體計算公式如下:
// 確定動畫起始點和終點
let XStart = 0, YStart = 0, XEnd = 1000, YEnd = 1000;
// 確定關鍵引數
let Xspeed = XX;
// 根據關鍵引數Xpeed計算動畫時間與垂直加速度
let Time = (XEnd - XStart) / Xspeed;
let A = 2 * (YEnd - YStart ) / (Time * Time);
複製程式碼
如果需要動畫執行的時長固定呢?
// 確定關鍵引數
let Time = XX;
// 根據關鍵引數Time計算水平速度與垂直加速度
let Xpeed = (XEnd - XStart) / Time;
let A = 2 * (YEnd - YStart ) / (Time * Time);
複製程式碼
如果需要加速度固定呢?
不,你不需要 .....
3. Y軸初速度
Y軸的初速度,即拋物線丟擲時垂直速度。一般的我會設定 Y軸初速度為負值。 此時會有向上拋然後自然下落的動畫,略生動... 這時加速度A的計算公式變為:
let A = 2 * (YEnd - YStart - Yspeed * Time) / (Time * Time);
複製程式碼
這裡需要注意的是,設定不同比值的Xspeed 與 Yspeed可以 改變曲線的形態。背後原理為:Yspeed和加速度A(有可能受Xspeed控制)共同決定了丟擲小球后小球上升階段能達到的 最高點, 而Xspeed決定了此時的X軸位置。
步驟三 讓它動起來
常規的JS動畫,我們一般使用 setTimeOut 或 requestAnimationFram去實現 。下面我們以requestAnimationFram實現 固定動畫執行時長 為例。
1.首先生成小球並確定動畫起點和終點,以及關鍵引數
// 起點和終點請自由設定
let XStart = 0, YStart = 0, XEnd = 1000, YEnd = 1000;
let Time = T;
let Xpeed = (XEnd - XStart) / Time;
let Ypeed = -YY;
let A = 2 * (YEnd - YStart - Yspeed * Time) / (Time * Time);
// 生成元素
let Node = document.createElement('div');
// 自由控制形體,定位一般設定為Fixed
Node.className = 'myNode';
document.body.appendChild(Node);
Node.style.top = YStart + 'px';
Node.style.left = XStart + 'px';
複製程式碼
2.在requestAnimationFram回撥內改變元素位置
// 記錄元素實時位置
let nowX = XStart;
let nowY = YEnd;
// 單位時間
let loop = 0;
//
let move = () => {
if (nowY >= targetTop) {
// 銷燬例項的判斷可自行設定
Node.remove();
return;
}
// 當前位置等於原始位置 + 單位時間內的位移
nowX += Xspeed;
//
nowY += (A * loop + Yspeed);
requestAnimationFram(() => {
Node.style.top = nowY + 'px';
Node.style.left = nowX + 'px';
loop++;
move();
});
};
複製程式碼
3. 小球可能會超過目的點
根據停止動畫的程式碼邏輯,小球在最後一次位移時,也許會超越我們設定的目的點。在下一次setTimeOut的判斷中我們才會停止動畫和銷燬例項。解決方式如下。
requestAnimationFram(() => {
Node.style.top = Math.min(nowY, XEnd) + 'px';
Node.style.left = Math.min(nowX, YEnd) + 'px';
loop++;
move();
});
複製程式碼
順便一提:這裡利用Math.min()或Math.max()可以實現很多有趣的動畫,自己去發現新大陸吧。
如何實現動態模糊效果
何為動態模糊?
動態模糊,這裡採用百度百科對其的定義 動態模糊或運動模糊(motion blur)是靜態場景或一系列的圖片像電影或是動畫中快速移動的物體造成明顯的模糊拖動痕跡。
筆者理解就是視覺資訊的殘留,即當前時刻的視覺來源(比如圖片,視訊,腦補)中殘留有上一時刻的視覺資訊。
這樣有什麼好處呢?適當的動態模糊會使連續的畫面變化 變得更加流暢和自然。
如何實現動態模糊?
首先我們做個排除法,肯定是不能放電影的...
讓我們在看一遍動態模糊實現的效果造成明顯的模糊拖動痕跡
。 也就是說如果實現了模糊拖動的痕跡就可以模仿動態模糊效果。
那麼模糊拖動又是什麼效果呢?
筆者認為,動態模糊的效果可模擬為 在元素周圍新增數個透明度漸變的相同元素
程式碼實現
在程式碼實現之前,我們在首先要確定我們需要實現的目標。以拋物線動畫中的小球為目標,即 在運動的小球周圍生成數個透明度漸變的小球。具體新增小球的位置呢?筆者的想法是,在小球倆幀位置之間插入殘影小球。
第一步 包裝
將原有實現包裝在一個函式裡
let animat = (初始位置, 結束位置) => {
...引數設定
// 位置變換
nowX += Xspeed;
nowY += (A * loop + Yspeed);
requestAnimationFrame(() => {
Node.style.top = nowY + 'px';
Node.style.left = nowX + 'px';
loop++;
move();
});
}
複製程式碼
目的很簡單,就是生成的殘影的小球也需要和原有小球位置資訊同步。
第二步 生成殘影
思考:每一次殘影小球的位置都要與真實小球相關。(通過相同初始值設定的小球自然軌跡相同) 所以我們不能變動小球的真實位置,那麼translate似乎就是一個不錯的選擇。
let animat = (初始位置, 結束位置, 是否是殘影) => {
...引數設定
// 位置變換
nowX += Xspeed;
nowY += (A * loop + Yspeed);
requestAnimationFrame(() => {
Node.style.top = nowY + 'px';
Node.style.left = nowX + 'px';
if (isShadow) {
item.style.transform = `translate(${(0.5 * Xspeed)}px ,${-(0.5 * (A * loop + Yspeed))}px)`;
item.style.opacity = 0.5;
}
}
loop++;
move();
});
}
複製程式碼
這一步需要注意的是 透明度的變化至關重要。 透明度的取值筆者推薦 0.1至 0.5之間。
第三步 生成多個殘影
如果只是一個生成一個小球的話,動態模糊的效果不會和明顯。所以我們需要新建一個控制小球數量的函式。
createShadow(初始位置, 結束位置, num) {
for (let i = 0; i < num; i++) {
animat(初始位置, 結束位置, true, i / (num + 1));
}
},
複製程式碼
animat函式更改為
let animat = (初始位置, 結束位置, 是否是殘影, num) => {
.....
requestAnimationFrame(() => {
....
if (isShadow) {
item.style.transform = `translate(${-(num * Xspeed)}px ,${-(num * (A * loop + Yspeed))}px)`;
item.style.opacity = (1 - num) * 0.5;
}
}
.....
});
}
複製程式碼
大功告成。
結束語
如果對本文有不解,不贊同之處或你有更好的點子,請在留言區留言。一起交流,共同進步。