天天看點

使用 SVG 和 JS 建立一個由星形變心形的動畫

序言:首先,這是一篇學習 SVG 及 JS 動畫不可多得的優秀文章。我非常喜歡 Ana Tudor 寫的教程。在她的教程中有大量使用 SVG 制作的圖解以及實時互動 DEMO,可以說教程的所有細枝末節都可以成為學習 SVG 以及 JS 畫圖的資料。另一方面,這篇教程也非常枯燥,因為教程的主要篇幅是關于幾何圖形的數學計算,不過上過中學的人都能了解。全篇翻譯完,我覺得我幾乎重新溫習了一遍中學的幾何知識,順便學了點英語詞彙。最後還要感歎一下,想要靈活運用 SVG 畫圖,深厚的數學功底是不可或缺的,同時還要有敏銳的思維和牢靠的記憶力。 原文:Creating a Star to Heart Animation with SVG and Vanilla JavaScript 譯者:nzbin

在 我上一篇文章中, 我講解了如何使用純 JavaScript 實作從一個狀态到另一個狀态的平滑過渡。一定要看看這篇文章,因為我會引用一些我詳細解釋過的東西,比如示範示例、各種定時函數公式以及如何從結束狀态傳回初始狀态而不需要反轉定時函數。

最後一個例子展示了一個從悲傷到高興的嘴形,它是通過嘴形 <code>path</code> 的 <code>d</code> 屬性實作的。

利用路徑資料可以獲得更有趣的結果,比如一顆星星變成一個心。

使用 SVG 和 JS 建立一個由星形變心形的動畫

我們即将編寫的星星變心的動畫。

兩個形狀都是使用五條 三次 Bézier 曲線 建立的。下面的互動式示範顯示了各個曲線和這些曲線連接配接的點。單擊任何曲線或點都會高亮顯示,與它對應的另一個形狀的曲線/點也會高亮顯示。

See the Pen star vs. heart: highlight corresponding cubic Bézier curves on click by Ana Tudor (@thebabydino) on CodePen.

注意,所有這些曲線都是三次曲線,不過其中一些曲線的兩個控制點是重合的。

星星和心的形狀都非常簡單,但制作起來還是會有一定難度。

正如在 臉部動畫 中看到的,我經常使用 Pug 生成這樣的形狀,但在這裡,因為我們生成的路徑資料也需要用 JavaScript 來制作路徑動畫,是以全部使用 JavaScript,包括計算坐标并把數值放入 <code>d</code> 屬性中,這似乎是最好的選擇。

這意味着我們不需要寫太多的标簽:

使用 JavaScript 的話, 我們先要擷取 SVG 元素和 <code>path</code> 元素(這是星形到心形來回切換的形狀)。我們在 SVG 元素上添加了 <code>viewBox</code> 屬性,這樣可以保證沿兩軸方向尺寸相等并且 <code>(0,0)</code> 點位于視圖中心。是以左上角的坐标是 <code>(-.5*D,-.5*D)</code>, 其中 <code>D</code> 是 <code>viewBox</code> 尺寸的數值。最後,但并非最不重要的一點是,我們建立一個對象來存儲關于初始狀态和結束狀态的資訊,以及設定 SVG 形狀的的插入值和實際值資訊。

既然已經弄明白了,現在開始讨論有趣的部分!

端點和控制點的初始狀态的坐标用于畫星星,結束狀态的坐标用于畫心形。每個坐标的範圍是它的結束值與其初始值之間的內插補點。在這裡,需要旋轉變形的形狀,因為我們想讓星星的角指向上方,其次我們改變 <code>fill</code> 實作金星到紅心的變化。

但是在這兩種情況下,我們如何得到端點和控制點的坐标呢?

從星形開始,先畫一個正五角星。曲線的端點就是五角星邊的交點,控制點是五角星的頂點。

使用 SVG 和 JS 建立一個由星形變心形的動畫

高亮顯示的正五角星頂點以及邊線交點就是五條三次 Bézier 曲線的控制點及端點 (live).

擷取正五角星的頂點坐标 非常容易 ,隻要知道它的外接圓半徑 ( 或直徑 ),我們可以從 SVG (為了簡單起見,我們把它看成正方形,不在對它嚴密封裝)的 <code>viewBox</code>尺寸得到。但是我們怎樣才能獲得交叉點坐标呢?

首先,我們先考慮下圖中五角星形中高亮顯示的小五邊形。由于是正五角星形,是以五角星形邊線交叉得到的小五邊形也是正五邊形。它和五角星形有相同的 内切圓 及内切圓半徑。

使用 SVG 和 JS 建立一個由星形變心形的動畫

正五角星形和它裡面的正五邊形有相同的内切圓 (live).

如果我們計算五角星的内切圓半徑,那麼就可以得到内五邊形的半徑,如果再知道正五邊形一條邊所對的 圓心角, 就可以得到五邊形的 外接圓半徑,然後就可以計算出頂點坐标,這些坐标也是五角星形邊線的交點坐标以及三次 Bézier 曲線的坐标。

我們的正五角星形可以用 Schläfli symbol <code>{5/2}</code> 表示,這說明它有 <code>5</code> 頂點,然後将這 <code>5</code> 個頂點平均分布到它的外接圓上,每個點相隔 <code>360°/5 = 72°</code> 。我們從第一個點開始,跳過圓上的相鄰點與第二個點連接配接(這就是符号中的 <code>2</code>;<code>1</code> 表示五邊形,也就是不跳過任何點,與第一個點連接配接)。以此類推,圓上的點依次相隔連接配接。

在下面的互動式示範中,可以選擇五邊形或五角星形,看看它們是怎樣生成的。

See the Pen construct regular pentagon/ pentagram by Ana Tudor (@thebabydino) on CodePen.

這樣,我們得到了正五角星形的中心角,它是正五邊形圓心角的兩倍。其中正五邊形的圓心角是 <code>1·(360°/5) = 1·72° = 72°</code> (弧度 <code>1·(2·π/5)</code>),而正五邊形為 <code>2·(360°/5) = 2·72° = 144°</code> (弧度為 <code>2·(2·π/5)</code>)。通常,給定一個正多邊形(不管是凸多邊形還是星形多邊形),使用 Schläfli symbol <code>{p,q}</code> 表示,與一條邊相對的圓心角就是 <code>q·(360°/p)</code> (弧度為 <code>q·(2·π/p)</code>)。

使用 SVG 和 JS 建立一個由星形變心形的動畫

正多邊形一條邊所對的圓心角: 五角星形 (左, <code>144°</code>) vs. 五邊形 (右, <code>72°</code>) (live).

我們已經知道五角星形的外接圓半徑, 它是正方形 <code>viewBox</code> 尺寸的一部分。這意味着可以通過直角三角形得到五角星形的内切圓半徑(等于它裡面的小五邊形的内切圓半徑),因為我們已經知道斜邊(就是五角星形的外接圓半徑)以及一個銳角(與邊相對的圓心角的一半)。

使用 SVG 和 JS 建立一個由星形變心形的動畫

通過直角三角形計算正五角星形的内切圓半徑,其中斜邊是五角星形的外接圓半徑,銳角是五角星形邊所對的半徑夾角的一半 (live).

圓心角一半的餘弦值就是内切圓半徑除以外接圓半徑,是以内切圓半徑等于外接圓乘以餘弦值。

現在已經知道了五角星形内的小正五邊形的内切圓半徑,我們可以通過相似的直角三角形計算外接圓半徑,直角三角形的斜邊就是外接圓半徑,圓心角的一半是其中一個銳角,與銳角相鄰的中垂線是内切圓半徑。

下圖中,高亮突出顯示的直角三角形就是由正多邊形的外接圓半徑、内切圓半徑以及邊線的一半組成的。從這個三角形中,如果我們知道内切圓半徑以及與多邊形相對的圓心角(兩個半徑之間的銳角等于圓心角的一半),我們就可以計算出外接圓半徑。

使用 SVG 和 JS 建立一個由星形變心形的動畫

通過直角三角形計算正五邊形的外接圓半徑(斜邊), 直角邊是内切圓半徑和五邊形邊長的一半,銳角是五邊形邊所對的半徑夾角的一半  (live).

記住,在這種情況下,圓心角并不等于五角星形的圓心角,而是它的一半 (<code>360°/5 = 72°</code>).

很好,得到内切圓半徑之後,我們可以得到所有想要的點坐标。它們是在兩個圓上以相等角度分布的點的坐标。外圓(五角星形的外接圓)上有 <code>5</code> 個點,内圓(小五邊形的外接圓)上也有 <code>5</code> 個點。總共有 <code>10</code> 個點,它們所在的徑向線之間的角度為 <code>360°/10 = 36°</code> 。

使用 SVG 和 JS 建立一個由星形變心形的動畫

端點及控制點分别平均分布在内五邊形和五角星的外接圓上 (live).

我們已經知道這兩個圓的半徑。外圓的半徑是正五邊形的外接圓半徑,我們可以取 <code>viewBox</code> 尺寸的任意數值(<code>.5</code> 、 <code>.25</code> 、 <code>.32</code> 或者我們覺得更好的數值)。内圓的半徑是在五角星形内形成的小正五邊形的外接圓半徑,可以通過一條邊相對的圓心角和内切圓半徑計算, 而内切圓半徑等于五角星形的内切圓半徑,可以通過五角星形外接圓半徑和圓心角計算得出。

是以,我們已經可以獲得繪制五角星的路徑資料,所有資料都是已知的。

現在讓我們在代碼中去實作它!

我們先建立一個 <code>getStarPoints(f)</code> 函數,它需要傳遞一個随機因數 (<code>f</code>) ,這個因數乘以 <code>viewBox</code> 尺寸就是五角星形的外接圓半徑。該函數會傳回一個坐标數組,我們之後會用于插入值。

通過這個函數,我們首先計算變換形狀時不會改變的常量,比如五角星形的外接圓半徑(外圓的半徑)、正五角星和正多邊形一條邊所對的圓心角、五角星形和内五邊形(其頂點是五角星形邊的交叉點)共有的内切圓半徑、内五邊形的外接圓半徑、以及需要計算坐标的不同點的總數和平均分布的角度。

之後,使用循環計算我們想要的點的坐标,并把它們放到坐标數組中。

為了計算點的坐标,我們使用它們所在的圓的半徑和與水準軸相連的徑向線的角度,可以看下面的互動式示範(拖動這個點,看看它的笛卡爾坐标是如何變化的):

See the Pen position of point in a plane (drag point) by Ana Tudor (@thebabydino) on CodePen.

在我們的例子中,偶數點 (<code>0</code>, <code>2</code>, ...) 半徑是外圓的半徑(五角星外接圓半徑 <code>RCO</code>),奇數點 (<code>1</code>, <code>3</code>, ...) 半徑是内圓半徑(内五邊形外接圓半徑 <code>RCI</code>),而點的徑向線與端點的夾角就是該點的索引 (<code>i</code>) 乘以平均分布的點的基本角度 (<code>BAD</code>, 在例子中剛好是 <code>36°</code> 或者 <code>π/10</code> )。

是以循環可以這樣寫:

因為我們給 <code>viewBox</code> 尺寸設定的非常大,是以可以放心地将坐标值四舍五入,這樣的話沒有小數點,看起來更簡潔。

在将這些坐标儲存到數組的過程中,外圓的點(偶數點情況下)被儲存了兩次,因為實際上這兩個控制點是重疊的(這種情況隻針對星形),是以我們需要把這些重疊點移動到不同的位置以獲得心形。

接下來,将資料放入對象 <code>O</code> 中。對于路徑資料的(<code>d</code>)屬性,我們将上述函數執行後得到的點數組作為初始數值。我們還建立了一個函數來生成實際的屬性值(也就是路徑資料字元串——在兩對坐标之間插入指令,以便浏覽器處理這些坐标)。最後,我們将存儲資料的每個值設定成前面提到的函數傳回值:

結果可以在下面的 CodePen 中檢視:

See the Pen make SVG star shape by Ana Tudor (@thebabydino) on CodePen.

這是一個好的開始。然而,我們希望生成的五角星第一個角朝下,而最終的星形第一個角朝上。目前,他們都指向右。這是因為星形是從 <code>0°</code> 度(三點鐘方向)開始繪制的。是以為了将六點鐘方向作為起點,我們在 <code>getStarPoints()</code> 函數中給所有角度添加 <code>90°</code> (<code>π/2</code>弧度)。

現在生成的五角星和最終的星形的第一角都朝下。為了旋轉星形,我們需要在 <code>transform</code> 屬性中設定半個圓的角度。為了做到這一點,我們首先将初始旋轉角度設定為 <code>-180</code> 。然後,我們設定一個生成實際屬性值的函數,這個函數可以通過函數名和參數生成字元串:

我們也用同樣的方式給星形填充金色。将 RGB 數組設定為 <code>fill</code> 的初始值,并使用同樣的函數生成實際的屬性值:

現在,我們有了一個使用三次 Bézier 曲線及 SVG 繪制的漂亮的金色星星:

See the Pen make SVG star shape #2 by Ana Tudor (@thebabydino) on CodePen.

既然已經有了星形,接下來看看如何才能得到心形!

我們從兩個等徑的相交圓開始畫,半徑都是  <code>viewBox</code> 尺寸的一部分(暫時為 <code>.25</code> )。在這種情況下,兩個相交圓的中心點連線位于 x 軸,交點連線位于 y 軸。而且這兩部分是相等的。

使用 SVG 和 JS 建立一個由星形變心形的動畫

從兩個半徑相等的圓開始畫,它的圓心位于橫軸,交線位于豎軸 (live).

接下來,我們畫出通過上方交點的直徑,然後畫出通過直徑另一點的切線。這些切線相交于 y 軸。

使用 SVG 和 JS 建立一個由星形變心形的動畫

畫出經過上方交點的直徑,以及經過直徑與圓相交的另一端點的切線,切線的交點位于豎軸 (live).

上方的交點和切點正好是我們需要的五個端點中的三個。另外兩個端點将半圓弧分成了兩個相等的部分,進而可以得到四個四分之一圓弧。

使用 SVG 和 JS 建立一個由星形變心形的動畫

高亮顯示的三次 Bézier 曲線構成了心形, 下方曲線的控制點重合 (live).

下方的曲線的控制點正好和之前兩切線的交點重合。但是其他四條曲線呢?如何用三次 Bézier 曲線得到圓弧?

我們無法直接通過三次 Bézier 曲線畫出四分之一圓弧,但我們可以找到近似的方法,詳見 這篇文章 。

我們從一個半徑為 <code>R</code> 的四分之一圓弧開始,畫出圓弧端點 ( N and Q ) 的切線。切線相交于 P 點。四邊形 ONPQ 的所有角都等于 <code>90°</code> ( 或者 <code>π/2</code>),其中三個是建立出來的(O 所對的是 <code>90°</code> 圓弧,是以通過圓弧端點的切線必然與通過該點的半徑垂直) ,最後一個是計算出來的(四邊形的内角和是 <code>360°</code> ,而另外三個角的和為 <code>270°</code>)。是以 ONPQ 是一個矩形。但是 ONPQ 也有兩個相等的鄰邊(OQ 和 ON 是半徑,長度等于 <code>R</code> ),是以它是邊長為 <code>R</code> 的正方形。是以 NP 和 QP 的長度也等于 <code>R</code> 。

使用 SVG 和 JS 建立一個由星形變心形的動畫

三次 Bézier 曲線畫出的近似四分之一圓弧 (live).

與圓弧近似的三次曲線的控制點在切線 NP 和 QP 上,與端點的距離為 <code>C·R</code> ,其中 <code>C</code> 是之前介紹的文章中所計算出的常量 <code>.551915</code> 。

知道這些條件之後,現在開始計算建立出星形的端點和控制點坐标。

基于我們選擇的建立心形的方式,TO0SO1 (如以下圖形所示) 是 一個正方形 ,因為它的所有邊都相等(都等于兩個相等圓的半徑)并且對角線也相等(我們說過中心點之間的距離等于交點之間的距離)。其中, O 是對角線的交點,OT 是對角線 ST 的一半。T 和 S 都位于 y 軸,是以它們的 x 坐标為 <code>0</code> 。它們的 y 坐标的絕對值等于 OT 線段的長度,也是對角線(OS 線段)的一半。

使用 SVG 和 JS 建立一個由星形變心形的動畫

正方形 TO0SO1 (live).

我們将所有的正方形分解成邊長為 <code>l</code> 的兩個等腰三角形,其中直角邊等于正方形邊長,斜邊等于對角線長度。

使用 SVG 和 JS 建立一個由星形變心形的動畫

任何正方形都可以分成兩個全等的等腰直角三角形 (live).

通過這些直角三角形,我們可以使用畢達哥拉斯定理( <code>d² = l² + l²</code> )計算出斜邊。通過邊長計算正方形對角線的公式為 <code>d = √(2∙l) = l∙√2</code> ( 相反地, 通過對角線計算邊長的公式為 <code>l = d/√2</code>)。同樣地,對角線的一半為 <code>d/2 = (l∙√2)/2 = l/√2</code>.

把這些公式應用到邊長為 <code>R</code> 的正方形 TO0SO1 上,可以得到 T 的 y 坐标是 <code>-R/√2</code> (絕對值等于正方形對角線的一半),S 的 y 坐标是 <code>R/√2</code> 。

使用 SVG 和 JS 建立一個由星形變心形的動畫

正方形 TO0SO1 的所有點坐标(live).

同樣的,Ok 點位于 x 軸,是以它們的 y 坐标是 <code>0</code> ,它們的 x 坐标是對角線 OOk 長度的一半: <code>±R/√2</code> 。

TO0SO1 是一個正方形,是以它的所有角度都是 <code>90°</code>(弧度為 <code>π/2</code> ) 。

使用 SVG 和 JS 建立一個由星形變心形的動畫

四邊形 TAkBkS  (live).

上圖中, TBk 線段是直徑,是以 TBk 所對的弧是半圓弧,也就是 <code>180°</code> 弧,并且 Ak 将它分成了相等的兩部分 TAk 和 AkBk,每一部分是 <code>90°</code> 弧,它所對的是 <code>90°</code> 角, ∠TOkAk 和 ∠AkOkBk 。

因為 ∠TOkS 是 <code>90°</code> 角而且 ∠TOkAk 也是 <code>90°</code> 角,是以 SAk 線段也是直徑。是以在四邊形 TAkBkS 中,對角線 TBk 和 SAk 是垂直且相等,并且相交于中點 (TOk, OkBk, SOk 和 OkAk 相等,都是初始圓的半徑 <code>R</code>)。這說明四邊形 TAkBkS 是正方形并且對角線長為 <code>2∙R</code> 。

現在我們可以獲得四邊形 TAkBkS 的邊長為 <code>2∙R/√2 = R∙√2</code> 。因為所有角都是 <code>90°</code> 并且 TS 與豎軸重合,是以 TAk 和 SBk 邊是水準的,平行于 x 軸并且它們的長度是 Ak 和 Bk 點的 x 坐标: <code>±R∙√2</code>.

因為 TAk 和 SBk 是水準線,是以 Ak 和 Bk 點的 y 坐标是相等的,分别等于 T (<code>-R/√2</code>) 和 S (<code>R/√2</code>) 點坐标。

使用 SVG 和 JS 建立一個由星形變心形的動畫

正方形 TAkBkS 的所有點坐标(live).

我們還可以知道的一點是,因為 TAkBkS 是正方形, AkBk 平行于 TS,TS 位于 y (垂直) 軸,是以線段 AkBk 是垂直的。另外, 因為 x 軸平行于線段 TAk 和 SBk ,并且平分 TS,是以它也平分線段 AkBk 。

現在讓我們轉到控制點。

我們從底部曲線的重疊控制點開始。

使用 SVG 和 JS 建立一個由星形變心形的動畫

四邊形 TB0CB1 (live).

四邊形 TB0CB1 所有角度都是 <code>90°</code> (因為 TO0SO1 是正方形,是以 ∠T 是直角;因為線段 BkC 在 Bk 點與圓相切,是以與半徑 OkBk 垂直,是以 ∠Bk 是直角;最後,因為四邊形内角和是 <code>360°</code> 而其它三個角是<code>270°</code> ,是以 ∠C 也是 <code>90°</code>  ), 是以它是矩形。又因為 TB0 和 TB1 相等,都是初始圓的直徑,是以都等于 <code>2∙R</code> 。是以它是邊長為 <code>2∙R</code> 的正方形。

現在,我們可以得出對角線 TC 等于 <code>2∙R∙√2</code> 。因為 C 位于 y 軸,它的 x 坐标是 <code>0</code> 。它的 y 坐标等于線段 OC 的長度。線段 OC 等于線段 TC 減去線段 OT : <code>2∙R∙√2 - R/√2 = 4∙R/√2 - R/√2 = 3∙R/√2</code> 。

使用 SVG 和 JS 建立一個由星形變心形的動畫

正方形 TB0CB1 的頂點坐标 (live).

是以我們得到了底部曲線兩個相似控制點的坐标 <code>(0,3∙R/√2)</code>.

為了獲得其它曲線控制點的坐标,我們需要畫出經過端點的切線,它們的交點是 Dk 和 Ek 。

使用 SVG 和 JS 建立一個由星形變心形的動畫

四邊形 TOkAkDk 和 AkOkBkEk  (live).

在四邊形 TOkAkDk 中,所有角都是 <code>90°</code> (直角),其中三個是已知的(∠DkTOk 和 ∠DkAkOk 是半徑分别在 T 和 Ak 點與切線的夾角,而 ∠TOkAk 是四分之一圓弧 TAk 所對的角),第四個角是計算出來的(所有角的和是 <code>360°</code> 而另外三個的和是  <code>270°</code>)。是以 TOkAkDk 是矩形。又因為兩個相鄰邊相等(線段OkT 和 OkAk 都是半徑的長 <code>R</code>), 是以它們都是正方形。

是以對角線 TAk 和 OkDk 等于 <code>R∙√2</code> 。已知 TAk 是水準的,又因為正方形對角線垂直,是以線段 OkDk 是垂直的。是以 Ok 和 Dk 點的 x 坐标相等,我們已經計算過 Ok 點坐标是 <code>±R/√2</code> 。因為已知 OkDk 的長度,是以也可以求出 y 坐标,等于對角線長度 (<code>R∙√2</code>) ,前面有負号。

同樣的,在四邊形 AkOkBkEk 中,所有角也都是 <code>90°</code> (直角), 其中三個是已知的(∠EkAkOk 和 ∠EkBkOk 是半徑分别在 Ak 和 Bk 點與切線的夾角,而 ∠AkOkBk 是四分之一圓弧 AkBk 所對的角),第四個角是計算出來的(所有角的和是 <code>360°</code> 而另外三個的和是  <code>270°</code>), 是以 AkOkBkEk 是矩形。又因為兩個相鄰邊相等(線段OkT 和 OkAk 都是半徑的長 <code>R</code>), 是以它們都是正方形。

現在,我們知道了對角線 AkBk 和 OkEk 的長度是 <code>R∙√2</code> 。已知線段 AkBk 是垂直的,而且被水準軸平分,是以線段 OkEk 位于 x 軸,是以 Ek 點的 y 坐标是 <code>0</code> 。又因為Ok 點的 x 坐标是 <code>±R/√2</code> 而且線段 OkEk 等于 <code>R∙√2</code>, 是以可以計算出 Ek 點坐标等于 <code>±3∙R/√2</code> 。

使用 SVG 和 JS 建立一個由星形變心形的動畫

正方形 TOₖAₖDₖ 和 AₖOₖBₖEₖ 上新計算的點的坐标 (live).

但是,這些切線交點并不是我們想要獲得的近似圓弧的控制點。我們需要的控制點位于線段 TDk, AkDk, AkEk 和 BkEk 上,與(T, Ak, Bk)相聚大約 <code>55%</code> 的位置(這個數值是通過之前文章中的 <code>C</code> 計算出來的) 。是以端點到控制點的線段長為 <code>C∙R</code> 。

在這種情況下,控制點坐标為 <code>1 - C</code> 乘以 (T, Ak and Bk) 點坐标,再加上 <code>C</code> 乘以這些點的切線交點坐标 (Dk 和 Ek)。

趕快編寫 JavaScript 代碼吧!

和編寫星形代碼一樣,先寫一個 <code>getStarPoints(f)</code> 函數,需要傳一個任意因子參數 (<code>f</code>) ,用于從 <code>viewBox</code> 的尺寸中擷取輔助圓的半徑。這個方法也會傳回之後用到的插入點坐标數組。

在函數内部,我們計算那些在整個函數中不會改變的常量。首先是輔助圓的半徑。其次是小正方形的對角線,它的長度等于輔助圓半徑,對角線一半也是它的外接圓半徑。然後是三次曲線的端點坐标 ( T, Ak, Bk 點),沿水準方軸方向的絕對值。最後計算通過端點的切線交點坐标 ( C, Dk, Ek 點)。這些點要麼是與控制點一緻 (C),要麼可以幫助我們獲得控制點 (可以參考計算 Dk 和 Ek 點的方法)。

在下面的互動式示範中,可以點選檢視這些點的坐标:

See the Pen heart structure - end and intersection points by Ana Tudor (@thebabydino) on CodePen.

現在我們可以通過端點得到控制點以及切線交點:

接下來,需要将這些點放到數組中,并傳回數組。在制作星形的時候,我們從底部曲線開始,然後順時針旋轉,現在同樣如此。對于每條曲線,都要寫兩組控制點坐标以及一組端點坐标。

See the Pen star vs. heart: corresponding cubic Bézier curves (annotated, highlight on click) by Ana Tudor (@thebabydino) on CodePen.

注意第一條曲線(底部)曲線,兩條控制點是重合的,是以同一個坐标寫了兩次 。這段代碼看上去不如星形的代碼,但已經足夠了:

我們可以參考星形的例子,同樣使用 <code>getHeartPoints()</code> 函數獲得初始狀态,沒有旋轉,使用紅色 <code>fill</code> 填充。然後,我們将目前狀态設定為最終的形狀,這樣我們就能看到心形了:

我們有了一個漂亮的心:

See the Pen make SVG heart shape by Ana Tudor (@thebabydino) on CodePen.

但是如果将兩個形狀放到一起,不使用 <code>fill</code> 或者 <code>transform</code>,隻有 <code>stroke</code>, 可以看到兩個形狀并沒有對齊:

See the Pen SVG star vs. heart alignment by Ana Tudor (@thebabydino) on CodePen.

解決這個問題最簡單的方法是讓心形根據輔助圓半徑的大小縮放:

現在可以很好的對齊了, 不管怎樣調整 <code>f</code> 因數。在星形中,這個因數決定了相對于 <code>viewBox</code> 尺寸的五角星外接圓半徑 (預設是 <code>.5</code>) ;在心形中,它決定了同樣相對于 <code>viewBox</code> 尺寸的輔助圓半徑 (預設是 <code>.25</code>)。

See the Pen star-heart alignment for various f factors by Ana Tudor (@thebabydino) on CodePen.

我們希望點選時從一個形狀變到另一個形狀。為了做出這種效果,設定一個方向變量 <code>dir</code>,星形變心形的時候值為 <code>1</code> ,心形變星形的時候值為 <code>-1</code> 。初始值為 <code>-1</code>,好像剛從心形變到星形。

在 <code>_SHAPE</code> 元素上添加一個 <code>'click'</code> 事件監聽器并編寫這個狀态下的代碼,我們改變了方向變量 (<code>dir</code>) 以及形狀的屬性,這樣就可以實作從金星變紅心或者紅心變金星:

現在,點選可以切換兩個形狀:

See the Pen toggle between star and heart on click by Ana Tudor (@thebabydino) on CodePen.

我們并不希望一個形狀突變到另一個形狀,而是過渡變化的。是以我們使用之前文章中使用的插入值技術去實作。

我們首先确定過渡的總幀數 (<code>NF</code>) ,然後選擇合适的時間函數類型,從星形變心形的 <code>path</code> 形狀過渡使用 <code>ease-in-out</code> 類型,旋轉使用 <code>bounce-ini-fin</code> 類型,而 <code>fill</code> 使用 <code>ease-out</code> 類型。暫時就這些,或許以後我們改變主意或者想探索其它參數的時候再添加其它類型。

然後,為每個過渡屬性指定一個時間函數:

繼續添加請求 ID (<code>rID</code>) 以及目前幀 (<code>cf</code>) 變量,點選時首先調用 <code>update()</code> 函數,然後重新整理每次顯示直到過渡結束,調用 <code>stopAni()</code> 函數來結束動畫循環。通過 <code>update()</code> 函數,可以更新目前幀 <code>cf</code>,計算進度 <code>k</code> 以及在過渡結束時決定是否結束動畫循環。

我們還添加了一個乘數變量 <code>m</code> ,當結束狀态(心形)傳回初始狀态(星形)時不需要反轉事件函數 。

然後需要改變點選時的操作:

在 <code>update()</code> 函數中,我們想将過渡屬性設定成一些中間值 (取決于進度 <code>k</code>) 。正如在之前文章中看到的, 在剛開始甚至設定監聽器之前就計算結束值與初始值之間的範圍會比較好,是以接下來: 建立一個計算數字(或者數組中的,無論層級多深)範圍的函數,然後使用這個函數設定過渡屬性值的範圍。

現在剩下的就是 update() 函數的插值部分。使用循環,我們可以将所有屬性從一個狀态平滑過渡到另一個狀态。在這個循環中,我們将目前值設定成插值函數的傳回值,該函數需要傳入初始值(s), 目前屬性(<code>ini</code> 和 <code>rng</code>) 的範圍(s) ,時間函數 (<code>tfn</code>) 以及進度 (<code>k</code>):

最後一步是編寫這個插值函數。它和之前求範圍值的函數非常類似:

最終我們得到了一個形狀,點選時從星心變心形,再次點選從心形變星形!

See the Pen SVG + plain JS: star to heart &amp; back (click) by Ana Tudor (@thebabydino) on CodePen.

這幾乎是我們想要的結果——但還有一點小問題。對于角度這樣的循環值,我們不希望在第二次點選時反方向轉半個圓,而是繼續朝同一個方向轉半個圓。在第一次點選轉半個圓之後,第二次點選時再加上半個圓,就可以得到一個完整的圓,這樣我們就可以回到起始位置了。

我們可以添加一個可變的連續性屬性,隻需要稍微修改一下更新函數和插值函數:

現在我們得到了想要的結果:一個從金星過渡成紅心的形狀,每次點選它會按順時針方向旋轉半圈,從一個狀态變化到另一個狀态:

See the Pen #CodeVember #15 - no library star or heart this? by Ana Tudor (@thebabydino) on CodePen.

感謝您的閱讀,如果您對我的文章感興趣,可以關注我的部落格,我是叙帝利,下篇文章再見!

開發低代碼平台的必備拖拽庫 https://github.com/ng-dnd/ng-dnd

基于 Angular Material 的中背景管理架構 https://github.com/ng-matero/ng-matero

Angular Material Extensions 擴充元件庫 https://github.com/ng-matero/extensions

仿 Windows 照片檢視器插件 https://github.com/nzbin/photoviewer

仿 Windows 照片檢視器插件 jQuery 版 https://github.com/nzbin/magnify

完美替代 jQuery 的子產品化 DOM 庫 https://github.com/nzbin/domq

簡化類名的輕量級 CSS 架構 https://github.com/nzbin/snack

與任意 UI 架構搭配使用的通用輔助類 https://github.com/nzbin/snack-helper

單元素純 CSS 加載動畫 https://github.com/nzbin/three-dots

有趣的 jQuery 卡片抽獎插件 https://github.com/nzbin/CardShow

懸疑科幻電影推薦 https://github.com/nzbin/movie-gallery

鍛煉記憶力的小程式 https://github.com/nzbin/memory-stake