- 原文地址:How you can use simple Trigonometry to create better loaders
- 原文作者:Nash Vail
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:DM.Zhong
- 校對者:Ruochen Li
最近在研究登入頁面的時候,我偶然進入了一個網站。這個網站對於使用的人而言非常棒也非常有用。這個網站上的一個小細節雖然吸引了我的注意力,但是我卻不那麼輕鬆。
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 有幫助!
最後,當我創作/寫作/教授某些我認為可能對你有幫助的東西時,我可以向你傳送一封電子郵件嗎?讓我知道你的電子郵件地址。沒有垃圾郵件,這是我的承諾。
再次感謝您的閱讀!祝您每天愉快!
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。