smooth scroll

在 window 物件當中,提供了 scrollTo(x, y) 的方法,我們可以透過 scrollTo 來操作滾動。但, scrollTo 方法是直接讓滾動軸移到指定的位置,並沒有動畫的效果,如果要藉由動畫的幫助達到良好的使用者體驗,我們必須自己設計。

基本動畫知識

在開始之前先來讓我們了解一下動畫的基礎吧!

動畫最基本的要素有這些:

  • 時間 = 距離(位移)/ 速度
  • 位移 = 速度 * 時間
  • 速度 = 距離 / 時間

有了這些基礎知識就可以來製作簡單的動畫效果了。

第一次嘗試

我們要做的事是要讓 scroll 能夠用動畫的方式滑到自己想要的地方。所以,在本例當中,scrollTo(x, y) 將會是我們的位移。速度的話,我們先暫定是 200ms 吧!

1
2
3
4
5
6
/* */
function moveScrollY(targetY, speed) {
const speed = 200;
const scrollY = window.scrollY || window.pageYOffset;
scrollTo(0, scrollY + (targetY - scrollY) * t);
}

咦?這個 t 是 undefined 吧?

在定義 t(time) 之前,我們先來思考 t 應該要是什麼。根據剛剛的公式,時間 = 距離 / 速度,所以在本例當中,t 為 targetY - scrollY / speed。這樣寫的話如果 scrollY 大於 targetY 的話時間就會為負了,所以這邊我們要取絕對值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* */
function moveScrollY(targetY, speed) {
const scrollY = window.scrollY || window.pageYOffset;
const t = Math.abs((targetY - scrollY) / speed);
let currentTime = 0;
function move() {
currentTime += 1 / 60;
p = currentTime / t;
scrollTo(0, scrollY + (targetY - scrollY) * p);
setTimeout(move, 1000/60);
}
move();
}

到目前為止,我們的 scroll 動畫雛形已經出來,不過存在一些問題:

  • 距離太遠的時候,動畫的時間顯得有點長
  • 這個動畫不會停

現在我們來改善一下 scroll 的動畫。

距離太遠

顯然如果距離太遠時,動畫完成的時間會變得更長,所以我們需要限制一下 t 的範圍。

1
2
3
4
function moveScrollY(targetY, speed) {
const scrollY = window.scrollY || window.pageYOffset;
const t = Math.min(0.5, Math.abs((targetY - scrollY) / speed)); // 將時間限制在 0.5(500ms) 以下
}

這樣子好多了,在距離太遠時,動畫不會顯得太慢。

動畫不會停

在程式碼當中,因為沒有設定停止條件,所以會無止盡的延續下去。芝諾悖論

怎樣才算是完成了呢?這邊的終止條件是目前的時間(禎數)等於 t 的時候,就算終止了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function moveScrollY(targetY, speed) {
const scrollY = window.scrollY || window.pageYOffset;
const t = Math.min(0.5, Math.abs((targetY - scrollY) / speed)); // 將時間限制在 0.5(500ms) 以下
let currentTime = 0;
function move() {
currentTime += 1 / 60;
p = currentTime / t;
if (p < 1) {
scrollTo(0, scrollY + (targetY - scrollY) * p);
requestAnimationFrame(move); // 採用 requestAnimationFrame
} else {
scrollTo(0, targetY);
alert('done');
}
}
move();
}

這邊我們將 setTimeout 取代成 requestAnimationFrame,requestAnimation 跟 setTimeout 的差別在於使用 requestAnimationFrame 時,瀏覽器會幫我們做最佳化,在不必要的時候不會進行重繪,達到節省資源的效果。

目前主流瀏覽器都已經支援了(主流當然不包含 IE8 囉!)

但是,為什麼感覺動畫那麼不自然呢?

有沒有發現,時間與距離是完全呈現線性變化的,這代表我們假設這個物體在所有的時間點,速度都是相同的。真實生活中通常不會有這樣的事情發生,物體一定都是從靜止狀態逐漸加速,再從移動的狀態中逐漸停止。而上一篇的範例當中,動畫是突然開始,突然停止。在現實生活中,物體的移動速度並非成線性變化,這是造成我們動畫看起來不自然的主要原因。

知道了原理之後,就可以馬上來實作了:

  • 計算距離
  • 重新計算每一次的位移 = 距離 * 比例係數(easing)

把我們原本的 function 改寫成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function moveScrollY(targetY, speed) {
const scrollY = window.scrollY || window.pageYOffset;
const t = Math.min(0.5, Math.abs((targetY - scrollY) / speed)); // 將時間限制在 0.5(500ms)
let currentTime = 0;
function move() {
currentTime += 1 / 60;
var p = currentTime / t;
var d = Math.cos(Math.PI * pos) - 1); // 利用 cos 函數,重新計算移動的位置。
if (p < 1) {
scrollTo(0, scrollY + (targetY - scrollY) * d);
requestAnimationFrame(move); // 採用 requestAnimationFrame
} else {
scrollTo(0, targetY);
alert('done');
}
}
move();
}

這邊我們加上了一個簡單的 cos 函數,重新計算位移的位置,達到 easing 的效果。詳細 easing 效果,可以到 easing.js 看看,這裡蒐藏了很多 ease 效果。

結論

這邊很簡略地用 scroll 的 API 當作範例,介紹了動畫的基礎。當然還動畫的原理跟使用,又是另外一門深奧的學問了。

到目前爲止,我們的 scroll 動畫就算完成了,但因為位移是線性移動,看起來比較不自然一點,下一篇文章,再來介紹 ease 的概念,讓我們 scroller 變得更 smooth。

原本以為篇幅很長,不過基本的動畫原理的確只有這樣而已,加上了 easing 之後,動畫的效果看起來自然很多,當然,你也可以依照自己的經驗調整參數,達到更完美的使用者體驗。

不過,如果可以,盡量不要綁架使用者預設的滾動效果,一來可能會造成效能的問題,二來你綁定的滾動效果如果沒有依照使用者預期反應的話,很容易造成非常差的體驗。

分享到