- 蘇格團隊
- 做者:Jason
某一天我收到了產品發來的微信消息。小X,咱們的業務如今須要一個相似加入購物車的掉落動畫,通過組織的慎重考慮,這個需求就交給你了。因而便有了這篇文章。本文並無描述多少高深的技術,更多的是一些筆者在作動畫時對動畫原理的思考以及如何優化動畫的一些思路。實現效果以下:前端
前端實現動畫的方式有不少。不管是JS動畫,CSS動畫,Canvas動畫仍是SVG動畫,哪怕是GIF動畫實現一個簡單的拋物線都是足夠的。但考慮業務場景的需求以及可玩性,最終決定使用JS來實現這個動畫。瀏覽器
在筆者看來大多數動畫效果,歸根到底仍是 數學公式的應用。所謂拋物線動畫也無非就是讓元素的運動符合拋物線的運動軌跡。
拋物線的方程爲: Y = A*X*X + B*X + C
也許你們看到這個公式有點陌生。但曾經物理老師唸唸有詞的 L(距離) = 1/2*A(加速度)*T*T(時間)
想必你們都必定熟記於心。筆者正是利用這個公式來完成拋物線動畫。bash
因爲業務自己的特殊性,須要在用戶點擊物品時獲取到該元素在窗口中的絕對位置。即元素相對於瀏覽器可見區域的X, Y的座標。 這裏筆者推薦使用getBoundingClientRect()
函數結合具體業務計算絕對位置。 固然,在一些場景裏你能夠直接使用鼠標點擊位置或者其餘任意方法獲取動畫的起點。微信
加速度A決定了元素在設定方向(下文都用垂直方向代替)的速度變化快慢。當動畫的起點和終點都固定時,由公式 L(垂直距離) = 0.5 * A(加速度) * T * T(時間)
可得出此時 加速度A與時間T的平方成反比。
須要注意的是,正的加速度A會一直擴大幀與幀之間的垂直移動距離,因此過大的加速度A可能會致使 動畫的末期小球有閃爍感。app
在拋物線的動畫中,通常的咱們認爲元素的 水平移動速度固定。那麼一樣由公式 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);
複製代碼
若是須要動畫執行的時長固定呢?oop
// 肯定關鍵參數
let Time = XX;
// 根據關鍵參數Time計算水平速度與垂直加速度
let Xpeed = (XEnd - XStart) / Time;
let A = 2 * (YEnd - YStart ) / (Time * Time);
複製代碼
若是須要加速度固定呢?
不,你不須要 .....優化
Y軸的初速度,即拋物線拋出時垂直速度。通常的我會設置 Y軸初速度爲負值。 此時會有向上拋而後天然下落的動畫,略生動... 這時加速度A的計算公式變爲:動畫
let A = 2 * (YEnd - YStart - Yspeed * Time) / (Time * Time);
複製代碼
這裏須要注意的是,設定不一樣比值的Xspeed 與 Yspeed能夠 改變曲線的形態。背後原理爲:Yspeed和加速度A(有可能受Xspeed控制)共同決定了拋出小球后小球上升階段能達到的 最高點, 而Xspeed決定了此時的X軸位置。ui
常規的JS動畫,咱們通常使用 setTimeOut 或 requestAnimationFram去實現 。下面咱們以requestAnimationFram實現 固定動畫執行時長 爲例。
// 起點和終點請自由設定
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';
複製代碼
// 記錄元素實時位置
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();
});
};
複製代碼
根據中止動畫的代碼邏輯,小球在最後一次位移時,也許會超越咱們設定的目的點。在下一次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;
}
}
.....
});
}
複製代碼
大功告成。
若是對本文有不解,不贊同之處或你有更好的點子,請在留言區留言。一塊兒交流,共同進步。