天天看點

[譯] 怎樣使用簡單的三角函數來建立更好的加載動畫

最近在研究登入頁面的時候,我偶然進入了一個網站。這個網站對于使用的人而言非常棒也非常有用。這個網站上的一個小細節雖然吸引了我的注意力,但是我卻不那麼輕松。

Nooooo!

注意到這個,圓圈們不太自然的抖動以及不那麼流暢的運動讓我有了寫這篇文章的想法。

這篇文章所要做的一件事就是使用基礎三角函數的概念重新建立一個上方加載動畫的更加流暢的版本。我知道這聽起來可能很奇怪,但是相信我,這将會非常有趣。你會被這個加載動畫工作起來所需要的代碼量之小所驚訝到。而且,弄懂這篇文章根本不需要你是一個數學天才,甚至不需要你懂三角函數,我會解釋所有的一切。

下面是我們要做的事情!

很流暢!

讓我們開始吧

我們所要實作的加載動畫實際上是由三個小圓周期性的上下運動所組成的,每一個的運動都與其它兩個不同步。

讓我們把它分解成多個部分,首先,我們會得到一個小圓流暢地周期性地上下運動。我們稍對剩餘的部分進行分析。

歡迎你随時進行編碼。

1. 給小圓定位

上面的代碼在

<svg>

元素的中間畫了一個小圓。

圖1:SVG 輸出的非實際示意圖

讓我們了解一下它是怎麼實作的。

width

height

屬性使我們想要的實際尺寸。簡單起見,就是我們的

SVG

元素或者是盒子的寬度和高度。

圖二:SVG 盒子的寬度和高度

預設情況下,

SVG

盒子具有傳統坐标系,它的原點在左上角,

x, y

的值分别向右和向下遞增。同樣在預設情況下,每一個機關都對應一個像素,這樣盒子的四個角落根據給定的

width

height

具有适當的坐标。

圖三:SVG 盒子的四個角以及它們的坐标

下一步非常簡單地國小數學知識的運用。盒子中心點的坐标可以通過

(width/2, height/2)

計算出來為

(150, 75)

。我們把這兩個值分别賦給

cx

cy

以便于把小圓圈定位于盒子的中心。

圖四:計算盒子的中心點

2. 讓小圓圈動起來

我們這一節的目的就是使小圓圈動起來。但是不僅僅是無規律的簡單形式的任何運動。我們需要小圓圈做周期性的上下運動。

圖五:預期的運動

2.1 周期性運動中的數學知識

周期性是指事情發生在有規律的時間間隔内。最簡單的例子就是每天的日出和日落。不管現在是什麼時候,比如下午 6:30,24 小時後還是下午 6:30,而且在那個時候的 24 小時之後仍然是下午 6:30。它很有規律,它恰好在 24 小時的時間間隔内發生。

假設現在是中午,太陽位于天空中它一天中的最高點,24 小時候它仍然在那裡。或者假如現在是晚上并且夕陽處在地平線,随時都會落下去,24 小時之後,它又在做着相同的事情。你明白我舉這些例子是為了說明什麼了嗎?

圖六:日出和日落的循環

這是一個非常簡單的示意圖,有些人可能會說在某些層面(科學)上是不準确的,但我認為它仍然表示出了太陽重複位置的點,相當好。

如果我們畫出來一天中太陽在天空中的垂直位置,我們可能會發現其周期性愈發明顯。

為了畫出來一條二維曲線,我們需要兩個值,

x

y

。在我們的例子中是[一天中的]

time

positionOfTheSun

(譯者注:太陽的位置)。我們收集到了一系列的這樣的值,把它們畫在一張圖上就得到了我們想要的。

圖七:把日出和日落的循環畫在一張圖上

垂直坐标軸或者說是

y 軸

就是太陽在天空中的垂直位置;水準坐标軸或者說是

x 軸

代表時間。随着時間的變化,太陽的位置也會發生變化,并且這樣的值在 24 小時之後會重複出現。

現在我們已經得到了有關太陽位置的知識圖譜,這樣即使我們處在黑暗的洞穴裡,我們也可以知道此時此刻太陽在天空中的位置。要想知道我們是如何做到這點的,首先讓我們繼續,給我們的圖表命名為

sunsVerticalPositionAt

一旦我們得到了有關太陽位置的知識圖表,我們可以得到以下公式……

verticalPositionInTheSky = sunsVerticalPositionAt( [time] )

我們隻需要把我們的時間代入圖表(或者從數學的角度說,是函數),然後我們就可以得到太陽在天空中的位置。這就是怎樣得到太陽位置的方法。

圖八:根據圖表計算太陽的位置

我們選一個想要知道太陽位置的時間(假設是 t1),畫一條垂直的線,它會與圖表中的曲線相交,經過這個交點我們再畫一條水準的直線讓它與

y

軸相交。水準直線與

y

軸的交點所代表的數值即為 t1 時刻太陽在天空中的位置。這樣看來我們并不需要離開我們的洞穴就可以知道太陽在天空中的位置了。

我想我已經用了足夠多的比喻來進行解釋,接下來我們講一些數學知識。把圖表中的太陽和其它裝飾都删除掉,就得到了我們所想要的。

圖九:周期曲線

這張圖表很直覺地表示了周期性。一個對象(在我們的例子中是 Sun 的垂直位置)重複其作為另一個對象的值(在我們的例子中是時間)。

數學當中有許許多多周期性函數,但是我們仍然堅持周期函數最基本的特征,我們打算使用

y = sin(x)

函數作為建立最完美的加載動畫的公式,也就是著名的正弦公式。

下面是

y = sin(x)

的曲線圖。

圖十:正弦曲線

你是不是突然發現了什麼?你有沒有發現正弦公式和計算太陽在天空中位置的公式的相似之處?

我們可以傳入一個

x

值然後得到

y

的值。就像我們可以傳入

time

然後計算出太陽在天空中的位置一樣……不用離開我們的洞穴,好吧我再也不開這個洞穴的玩笑了。

如果你在思考什麼是

正弦公式

?好吧,那就是一個函數的名字,就像我們給我們的圖表(或者函數)命名為

sunsVerticalPositionAt

這裡需要注意的是

y

x

。看一下

y

是怎樣随

x

的變化而變化的。(你可以把它和我們太陽在天空中垂直位置随時間變化的例子聯系起來嗎?)

同樣的可以注意到

y

的最大值是 1,最小值是 -1。這隻是正弦函數的一個特征。

y = sin(x)

的值域為 -1 到 +1。

但是這個值域是可以改變的,我們将一點一點的做。但在這之前,讓我們把目前所學的所有知識都運用起來,實作小圓圈的運動。

2.2 從數學知識到代碼

現在我們已經在

<svg>...</svg>

中畫了一個圓圈,并且這個圓圈的 ID 是

c

。讓我們繼續,然後通過 JavaScript 讓它舞動起來!

let c = document.getElementbyId('c');

animate();
function animate() {
  requestAnimationFrame(animate);
}
複制代碼           

上面代碼所做的事情很簡單,一開始我們擷取到了圓圈并且把它存到了一個叫做

c

的變量中。

接下來,我們使用了

requestAnimationFrame

函數和一個叫做

animate

的函數。

animate

通過

requestAnimationFrame

函數遞歸的調用它自己,以 60 FPS 的速度運作其中的任何動畫代碼(盡可能)。在

這裡

擷取更多有關

requestAnimationFrame

的知識。

你所需要知道的是每次

animate

被調用時,其内部的代碼描述了動畫中的單個幀。當它下一次被遞歸地調用的時候,這一幀就發生了一點點的變化。這一變化在高速下(60 FPS)不斷的重複,然後就出現了我們所要的動畫效果。

看一下代碼了解得更清楚一些。

let c = document.getElementById('c');

let currentAnimationTime = 0;
const centreY = 75;

animate();
function animate() {
  c.setAttribute('cy', centreY + (Math.sin(currentAnimationTime)));
  
  currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}
複制代碼           

我們添加了四行代碼。如果你運作這些代碼,你就會看到圓圈會在中心點附近緩慢地移動,就像下面這樣。

下面是代碼的解釋。

一旦我們知道了圓圈中心點的坐标,

cx

cy

,這裡是盒子寬度和高度的一半。首先,我們把

cx

放在一邊,因為我們不想改變小圓圈的水準位置。我們需要定期從

cy

添加或減去相同的數字以使得小圓圈上下移動。這也正是我們在代碼中所做的。

圖十一:改變小圓圈中心點的 y 坐标

centreY

存儲着小圓圈中心點的 Y 坐标的值(75),這樣就可以從

centreY

增加或者減去一定的值 —— 就像已經提到的那樣 —— 改變小圓圈的垂直位置。

currentAnimationTime

是一個被初始化為 0 的值,它決定了動畫變化的快慢,我們在每次調用中給它增加的值越多,動畫變化得越快。我通過嘗試和錯誤選擇了

0.15

這個值,因為它看起來像是一個足夠好的動畫速度。

currentAnimationTime

是正弦函數的

x

值。當

currentAnimationTime

的值增加以後,我們把它傳給

Math.sin

函數(一個内置的用于計算正弦值的 JavaScript 函數),然後把它經過

Math.sin

函數計算出來的值添加到

centreY

上……

……然後使用

setAttribute

把最後的結果指派給

cy

就像我們知道的那樣,對于任意一個

x

值,都可以使用正弦函數産生一個

-1

1

之間的值。是以,

cy

的值最小為

centreY — 1

,最大為

centreY + 1

。這就導緻小圓圈在垂直方向上的抖動距離為 1 像素。

圖十二

我們想要增加這個抖動的間距。這就意味着我們需要一個比 1 更大的數字。我們該怎麼做呢?我們需要一個新的函數嗎?No!

還記得我們要在 2.2 節開始之前進行一個操作嗎? 這非常簡單,我們需要做的就是将正弦乘以我們想要的邊距。

将函數乘以常數的操作稱為縮放。請注意圖形如何改變其形狀,還有乘法對正弦的最大值和最小值的影響。

圖十三:圖形縮放

現在我們知道該怎麼做了,讓我修改一下代碼。

let c = document.getElementById('c');

let currentAnimationTime = 0;
const centreY = 75;

animate();
function animate() {
  c.setAttribute('cy', 
  centreY + (20 *(Math.sin(currentAnimationTime))));
  
  currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}
複制代碼           

這産生了一個非常流暢的小圓圈上下運動的動畫。很可愛吧?

What we just did is increased the amplitude of the Sine function by multiplying a number to it.

我們所做的隻是通過将函數乘以一個固定數字,增加了正弦函數的振幅。

下一步我們要做的是添加兩個小圓圈到原來小圓圈的兩邊,然後讓它們以同樣的方式動起來。

<svg width="300" height="150">
  <circle id="cLeft" cx="120" cy="75" r="10" />
  <circle id="cCentre" cx="150" cy="75" r="10" />
  <circle id="cRight" cx="180" cy="75" r="10" />
</svg>
複制代碼           

我們已經做了一點改變,這裡的代碼也已經被重構了。首先,請注意到兩行新的粗體代碼。它們是兩個新的小圓圈,一個在原來小圓圈左邊的 30 像素處(150 - 30 = 120),一個在原來小圓圈右邊的 30 像素點處(150 + 30 = 180)

之前,我們給了唯一的那個小圓圈一個 ID 為

c

,它能夠正常運動因為隻有一個小圓圈。但是現在我們已經有了三個小圓圈,最好給它們都取一個描述性很強的 ID。我們已經完成了這個工作,這些小圓圈從左到右 —— ID 為

cLeft

cCentre

cRight

。原來的小圓圈的 ID 已經由

c

變成了

cCentre

運作以上代碼,下面就是我們得到的效果。

很好,但是新添加的小圓圈都沒有動起來!好吧,現在要讓它們動起來了。

let cLeft= document.getElementById('cLeft'),
  cCenter = document.getElementById('cCenter'),
  cRight = document.getElementById('cRight');

let currentAnimationTime = 0;
const centreY = 75;
const amplitude = 20;

animate();
function animate() {

  cLeft.setAttribute('cy', 
  centreY + (amplitude *(Math.sin(currentAnimationTime))));

  cCenter.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime))));

  cRight.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime))));  

  currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}
複制代碼           

隻添加了寥寥幾行代碼就達到了我們的目标,給新的小圓圈都添加了和 ID 為

cCentre

的小圓圈一樣的動畫代碼,下面是我們得到的效果。

哇哦!新的小圓圈也動了起來!但是,我們現在得到的效果,根本不像是一個我們想要做出來的加載動畫。

盡管小圓圈們周期性的動了起來,現在還是有問題,因為它們的動作是同步的。這不是我們想要的。我們希望每個連續的小圓圈在運動時都有一些延遲。是以看起來,除了第一個小圓圈之外,後面的小圓圈看起來像循環之前的小圓圈的運動。就像下面這樣。

你注意到了嗎?每個小圓圈的運動都比它左邊的小圓圈慢一步。如果你用手遮掉兩個小圓圈,你會發現你看到的那個小圓圈的上下運動仍然跟我們在 2.2 節中實作的動畫一樣。

現在為了讓小圓圈不同步,對其進行幹擾,我們隻需要對我們的代碼做一個微小的改變。但了解這種微小變化如何起作用很重要。讓我們來看看。

如果我們用之前的時間 - 位置曲線圖繪制每個圓圈的運動,如下圖所示,這就是圖形的樣子。

圖十四:三個小圓圈的運動圖

這裡沒有驚喜,因為我們知道每個小圓圈都以相同的方式運動。了解一下它,因為我們使用正弦函數來實作這個動畫,是以上面的所有曲線都隻是正弦函數的圖形。現在為了讓這些圖不同步,我們需要了解圖象平移/圖象變換的數學概念。

平移是一種嚴格的變換,因為它不會改變函數曲線的形狀或大小。所有這些轉變将會改變曲線的位置。平移可以是水準或垂直的。對于我們的目的而言,我們對水準平移感興趣(如您所見)。

注意一下 Gif 中

a

值發生變化時,

y=sin(x)

的曲線圖是怎麼水準移動的。

圖十五:圖象變換(示例)

為了了解其中的原理,讓我重新回到日出和日落的比喻當中。

我們的函數又是哪個?

sunsVerticalPositionAt(t)

。那就對了!好的,是以我們可以給函數傳入時間參數,并在特定的時間獲得太陽在天空中的垂直位置。是以,為了在上午9點得到太陽的位置,我們可以寫

sunsVerticalPositionAt(9)

現在看一下

sunsVerticalPositionAt(t — 3)

。認真注意一下,不管我們傳入了什麼時間(t)到函數中(這裡使用 t - 3 代替 t),我們都會得到比 t 時刻早三個小時的時候,太陽在天空中的位置。

圖十六

這意味着 t = 9 的時候,我們得到的是 6 時刻的結果,而在 t = 12 的時候,我們得到的也是 9 時刻的結果。我們用這種方式連接配接函數,換句話說,函數傳回的值比

t

傳遞的時刻更早。

我們也可以說,我們将函數的圖象在 x 軸向右進行了平移。注意到下面圖象中,變換之前的圖象在

t = 6

時刻的值為

B

。當圖象被平移後,

B

會作為

t = 9

時刻的結果傳回。

圖十七:變換之後的圖象

同樣的,如果我們給參數加 3 而不是減三,

sunsVerticalPosition(t + 3)

的圖象會向左平移,或者換句話說,函數傳回的值會比原來傳入的時刻晚 3 小時。你明白這是為什麼嗎?

随着這個知識的概念在我們頭腦中的形成,我們現在可以做的就是進行圖象變換以使得決定最後兩個小圓圈動畫的圖形像下面這樣。

圖十八

為了完成這個效果,我們需要小小地修改一下代碼。

let cLeft= document.getElementById('cLeft'),
  cCenter = document.getElementById('cCenter'),
  cRight = document.getElementById('cRight');

let currentAnimationTime = 0;
const centreY = 75;
const amplitude = 20;

animate();
function animate() {

cLeft.setAttribute('cy', 
  centreY + (amplitude *(Math.sin(currentAnimationTime))));

cCenter.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime - 1))));

cRight.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime - 2))));

currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}
複制代碼           

現在就對了,我們平移了圖象,使得

cCenter

cRight

代表的小圓圈符合要求地動了起來。

上圖就是!我們加載動畫的小圓圈按照絕對的數學精度運動。值得慶祝一下!你可以随時使用不同的值,例如增加

currentAnimationFrame

的值以控制動畫速度或

幅度

來控制偏移量,并使加載動畫按照您希望的方式進行動畫運動。

納什,你寫這麼長的文章解釋一個簡單的加載動畫的錯綜複雜,你瘋了嗎?不!你為了閱讀它而瘋狂。

讓我們成為朋友!

在你點選之前,我還有幾個更新共享:)

我有個我的第一個線上課程用于講授 Git 和 GitHub 的使用技巧!你可以使用

這個連結獲得免費的2個月Skillshare會員資格

(需要信用卡支付來支援一下我),或者使用

這個連結來檢視免費課程

你使用過 Sketch 嗎?如果是的話那麼你可能會發現我建立的這個庫對 wire-framing 有幫助!

簽出 Wireframe.sketch.

最後,當我創作/寫作/教授某些我認為可能對你有幫助的東西時,我可以向你發送一封電子郵件嗎?讓我知道你的電子郵件位址。沒有垃圾郵件,這是我的承諾。

再次感謝您的閱讀!祝您每天愉快!

作者:DM.Zhong

連結:https://juejin.im/post/5b33055f518825748871c590

來源:掘金

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。