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

XXLee
XXLee
Jul 22, 2017 · 5 min read

本篇預計產出之結果:

這次的作品是透過 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 的基本觀念,和自己的想像力!

Practicode

sharing technology, programming skills and experience

    XXLee

    Written by

    XXLee

    Practicode

    sharing technology, programming skills and experience

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade