10 分鐘 svg 動畫 (3) — 波浪動畫

xxlee (Ching Hung Lee)
Practicode
Published in
5 min readJul 22, 2017

--

本篇預計產出之結果:

這次的作品是透過 Snap.svg 完成,之前沒有用過的朋友可以花個十分鐘玩一下官方提供的教學(標題騙人!),大概了解一下基本的用法即可,我做這個作品也只是第三次用這個 library,不會用到較進階的功能。

另外,這次也會用到簡單的 svg 路徑寫法,有需要的朋友可以來這裡複習一下每個指令代表的意思。

準備作業完成,我們開始吧!

首先,巧婦難為無米之炊,我們需要一塊畫布才能開始做畫:

body{
margin: 0;
}
.nav-wrapper{
height: 100vh;
width: 300px;
}
<div class="nav-wrapper">
<svg id="svg" width="100%" height="100%" viewbox="0 0 250 300" preserveAspectRatio="none">
<path d="M0 0 L200 0 Q300 150,200 300 L0 300Z"></path>
</svg>
</div>

由於這次的作品我預設是會運用在側邊的導航列,因此我將 wrapper 的高度設為 100vh,並且設定固定的寬度方便後面的操作。接著將 svg 的長寬都設成和 wrapper 一樣,但由於每個使用者的畫面長寬比階不同,因此我將 viewbox 先隨便抓個長方形,並預留寬度給觸發動畫時的鮪魚肚(原本寬度是 200,再多留 50 變成 250)。最後再用 preserveAspectRation="none" 將 viewbox 拉長至填滿 svg 畫布(原本長方形的比例會跑掉,但是看不出來)。對 viewbox 不熟悉的人可以參考這裡

我們先手動加一段之後預計要使用的路徑來測試一下,可以看到我 x 軸的頂點只有用到 200,留 50 的空間給鮪魚肚,而 Q300 150,200 300 則是將上下兩點貝茲曲線的控制點都設在 300 150 ,在中間拉出一個完美的肚子。

接下來請把這個測試用的 path 刪掉,我們使用 Snap.svg 來產生以利後續的操作。

let s = Snap('#svg');let controlPointX = 200;
let controlPointY = 150;
let originalPath = `M0 0 L200 0 Q${controlPointX} ${controlPointY},200 300 L0 300Z`;
let sideNav = s.path(originalPath);
sideNav.attr({
id: 'side_nav',
fill: '#30678f',
});

我們先抓取 #svg 並建立 Snap 物件,接著將關鍵的貝茲曲線控制點的座標設為變數,最後建立路徑並進行簡單的 attribute 設定。

圖形畫好後,只差最後的動態效果了!我們先將接下來的策略訂好:當游標出現在圖形的範圍內時,依據游標位置分別對 x 軸和 y 軸做出等比例變動。

let side_nav = document.querySelector('#side_nav');
let xMax = side_nav.getBoundingClientRect().width + 50;
let yMax = side_nav.getBoundingClientRect().height;

先將觸發動畫的範圍定義出來,個人習慣用 underscore 變數名代表 DOM 物件。由於我們的上界和左界都是畫面的邊緣,因此只定義各自的最大值。這裡要注意的是由於 side_nav 是 SVG 物件,無法用 offsetWidthoffsetHeight 取得元素的寬度和長度,必需使用 getBoundingClientRect() (到底是誰訂這麼長的名子…)。

let xBase = sideNav.getBBox().width;
let xOffset = 100;
let yBase = sideNav.getBBox().y;
let yOffset = sideNav.getBBox().height;

再來定義動畫的比例,值得一提的是這裡的 getBBox()是 Snap.svg 的方法,回傳的值比 SVG 的 getBBox()更多。只看這幾行有點難解釋,我們直接看接下來的 code:

document.addEventListener('mousemove', (event) => {
if(event.pageX < xMax && event.pageY < yMax){
controlPointX = xBase + xOffset*(event.pageX / xMax);
controlPointY = yBase + yOffset*(event.pageY / yMax);
let newPath = `M0 0 L200 0 Q${controlPointX} ${controlPointY},200 300 L0 300Z`;

sideNav.attr({
d: newPath
});
}
else{
sideNav.animate({
d: originalPath
}, 200, mina.bounce)
}
});

先判斷游標是否在範圍內,接著計算出新的貝茲曲線控制點座標, xBase 是原本圖形的寬度, xOffset 可以增加的最大值,用 event.PageX / xMax 計算出游標在圖形串的相對位置,進而算出符合比例的新座標,y 的算法大同小異,在此不贅述。最後我們將計算好的路徑更新至 svg 元素。

最後我們再加一個回彈的動畫,當游標不在範圍內時,則回到原本的路徑,並且用 mina.bounce 做出震盪的效果。

附上完整程式碼的 codepen

除了 Snap.svg 外,還有許多優秀的動畫函式庫可以選擇,像是 GreenSockVelocity.jsSVG.js 等,各自有不同的優缺點,自己用得順手就好,重要的是 svg 的基本觀念,和自己的想像力!

--

--