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

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

Nooooo!android

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

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

下面是咱們要作的事情!github

很流暢!後端

讓咱們開始吧

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

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

歡迎你隨時進行編碼。函數

1. 給小圓定位

上面的代碼在 <svg> 元素的中間畫了一個小圓。post

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

讓咱們理解一下它是怎麼實現的。

widthheight 屬性使咱們想要的實際尺寸。簡單起見,就是咱們的 SVG 元素或者是盒子的寬度和高度。

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

默認狀況下,SVG 盒子具備傳統座標系,它的原點在左上角, x, y 的值分別向右和向下遞增。一樣在默認狀況下,每個單位都對應一個像素,這樣盒子的四個角落根據給定的 widthheight 具備適當的座標。

圖三:SVG 盒子的四個角以及它們的座標

下一步很是簡單地小學數學知識的運用。盒子中心點的座標能夠經過 (width/2, height/2) 計算出來爲 (150, 75)。咱們把這兩個值分別賦給 cxcy 以便於把小圓圈定位於盒子的中心。

圖四:計算盒子的中心點

2. 讓小圓圈動起來

咱們這一節的目的就是使小圓圈動起來。可是不只僅是無規律的簡單形式的任何運動。咱們須要小圓圈作週期性的上下運動

圖五:預期的運動

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

週期性是指事情發生在有規律的時間間隔內。最簡單的例子就是天天的日出和日落。無論如今是何時,好比下午 6:30,24 小時後仍是下午 6:30,並且在那個時候的 24 小時以後仍然是下午 6:30。它頗有規律,它剛好在 24 小時的時間間隔內發生。

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

圖六:日出和日落的循環

這是一個很是簡單的示意圖,有些人可能會說在某些層面(科學)上是不許確的,但我認爲它仍然表示出了太陽重複位置的點,至關好。

若是咱們畫出來一天中太陽在天空中的垂直位置,咱們可能會發現其週期性愈發明顯。

爲了畫出來一條二維曲線,咱們須要兩個值,xy。在咱們的例子中是[一天中的] timepositionOfTheSun(譯者注:太陽的位置)。咱們收集到了一系列的這樣的值,把它們畫在一張圖上就獲得了咱們想要的。

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

垂直座標軸或者說是 y 軸就是太陽在天空中的垂直位置;水平座標軸或者說是 x 軸表明時間。隨着時間的變化,太陽的位置也會發生變化,而且這樣的值在 24 小時以後會重複出現。

如今咱們已經獲得了有關太陽位置的知識圖譜,這樣即便咱們處在黑暗的洞穴裏,咱們也能夠知道此時此刻太陽在天空中的位置。要想知道咱們是如何作到這點的,首先讓咱們繼續,給咱們的圖表命名爲 sunsVerticalPositionAt

一旦咱們獲得了有關太陽位置的知識圖表,咱們能夠獲得如下公式……

verticalPositionInTheSky = sunsVerticalPositionAt( [time] )

咱們只須要把咱們的時間代入圖表(或者從數學的角度說,是函數),而後咱們就能夠獲得太陽在天空中的位置。這就是怎樣獲得太陽位置的方法。

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

咱們選一個想要知道太陽位置的時間(假設是 t1),畫一條垂直的線,它會與圖表中的曲線相交,通過這個交點咱們再畫一條水平的直線讓它與 y 軸相交。水平直線與 y 軸的交點所表明的數值即爲 t1 時刻太陽在天空中的位置。這樣看來咱們並不須要離開咱們的洞穴就能夠知道太陽在天空中的位置了。

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

圖九:週期曲線

這張圖表很直觀地表示了週期性。一個對象(在咱們的例子中是 Sun 的垂直位置)重複其做爲另外一個對象的值(在咱們的例子中是時間)。

數學當中有許許多多週期性函數,可是咱們仍然堅持周期函數最基本的特徵,咱們打算使用 y = sin(x) 函數做爲建立最完美的加載動畫的公式,也就是著名的正弦公式。

下面是 y = sin(x) 的曲線圖。

圖十:正弦曲線

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

咱們能夠傳入一個 x 值而後獲得 y 的值。就像咱們能夠傳入 time 而後計算出太陽在天空中的位置同樣……不用離開咱們的洞穴,好吧我不再開這個洞穴的玩笑了。

若是你在思考什麼是正弦公式?好吧,那就是一個函數的名字,就像咱們給咱們的圖表(或者函數)命名爲 sunsVerticalPositionAt

這裏須要注意的是 yx。看一下 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);
}
複製代碼

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

下面是代碼的解釋。

一旦咱們知道了圓圈中心點的座標, cxcy,這裏是盒子寬度和高度的一半。首先,咱們把 cx 放在一邊,由於咱們不想改變小圓圈的水平位置。咱們須要按期從 cy 添加或減去相同的數字以使得小圓圈上下移動。這也正是咱們在代碼中所作的。

圖十一:改變小圓圈中心點的 y 座標

centreY 存儲着小圓圈中心點的 Y 座標的值(75),這樣就能夠從 centreY 增長或者減去必定的值 —— 就像已經提到的那樣 —— 改變小圓圈的垂直位置。

currentAnimationTime 是一個被初始化爲 0 的值,它決定了動畫變化的快慢,咱們在每次調用中給它增長的值越多,動畫變化得越快。我經過嘗試和錯誤選擇了 0.15 這個值,由於它看起來像是一個足夠好的動畫速度。

currentAnimationTime 是正弦函數的 x 值。當 currentAnimationTime 的值增長之後,咱們把它傳給 Math.sin 函數(一個內置的用於計算正弦值的 JavaScript 函數),而後把它通過 Math.sin 函數計算出來的值添加到 centreY 上……

……而後使用 setAttribute 把最後的結果賦值給 cy

就像咱們知道的那樣,對於任意一個 x 值,均可以使用正弦函數產生一個 -11 之間的值。所以,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 爲 cLeftcCentrecRight。原來的小圓圈的 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);
}
複製代碼

如今就對了,咱們平移了圖象,使得 cCentercRight 表明的小圓圈符合要求地動了起來。

上圖就是!咱們加載動畫的小圓圈按照絕對的數學精度運動。值得慶祝一下!你能夠隨時使用不一樣的值,例如增長 currentAnimationFrame 的值以控制動畫速度或幅度來控制偏移量,並使加載動畫按照您但願的方式進行動畫運動。

納什,你寫這麼長的文章解釋一個簡單的加載動畫的錯綜複雜,你瘋了嗎?不!你爲了閱讀它而瘋狂。讓咱們成爲朋友!在你點擊以前,我還有幾個更新共享:)


我有個個人第一個在線課程用於講授 Git 和 GitHub 的使用技巧!你可使用這個連接得到免費的2個月Skillshare會員資格(須要信用卡支付來支持一下我😸),或者使用這個連接來查看免費課程


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

簽出 Wireframe.sketch.


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

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

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索