前言
疫情升溫,待在家裡救人救己,除了打電動玩健身環外,也是個好機會來培養培養自己的美術能力,然而平常工作沒什麼機會製作動畫的我,即便有了時間,也不知道要從何下手,上了 Dribbble、CodePen 找靈感,的確看到很多有趣的作品,但是大多都很複雜,不像是一個週末午後的休閒良品,例如,Ben Evans 的這個作品:
See the Pen Pure CSS Landscape - An Evening in Southwold by Ben Evans (@ivorjetski) on CodePen.
這張像是照片一樣的圖片,你能想像是單純用 CSS 製作的嗎?作者有放上他製作這作品的縮時影片(影片的音樂還是他自己做的,真有才華),雖然不知道總共花了多少時間,但以他的另一個同樣驚人且至少花費一百小時的作品推斷,時數少不到哪去的。
我知道很多人會覺得,『對呀很酷,但為什麼?』。
我也不懂為什麼,但知道能利用 CSS 做出這種極限真的很令人感到興奮。光是觀察他的程式碼就可以學到不少技巧,像是:
- Custom HTML tag
即便是用 CSS 繪圖,相信大部分的人也都是用普通的 div
、span
來組裝圖案,但如果你打開剛剛那範例的 HTML,會看到這樣的結構:
<landscape>
<sky>
<x>
<x></x>
<x></x>
</x>
<x>
<x></x>
<x></x>
...</x
></sky
></landscape
>
全都是 custom element,以為是他自製的 web component 但他又沒有對應的 Javascript?🤔
其實現今瀏覽器對這種 invalid 的 HTML tag 容忍度很高,只要有給定 CSS,瀏覽器還是能正常渲染出來。實際專案上當然不建議這樣做,但在製作 CSS 繪圖或藝術動畫這類通常擁有複雜 HTML 結構的作品上時,就能讓程式碼看起來簡潔許多,等同於讓 tag name 取代 class name。
- Responsive rem
我們都知道 rem
會隨著 root element 的 font-size 自動調整大小,所以若是我們也能動態調整 root element 的大小,並用 rem
來設定所有元素的 size,那就能讓頁面輕鬆 responsive。要做到這點可以利用 vmin
:
html {
font-size: 1vmin;
}
vmin
對應 viewport 的短邊,意即螢幕縮小時,該值也會隨之變小,這樣就能達到我們要的效果。
其實還有其他技巧,但已經扯夠遠了😅。
雖然試著理解高手如何做到是能吸收不少經驗,但還是會想要自己動手做點什麼,好在我又發現了另一個稍微平易近人的高手 - Aaron Iker,他大多的作品都圍繞在一個網頁上不可缺乏,但鮮少被人拿來做文章的元件 - "按鈕"上。
按鈕,幾乎所有網頁都會用到它,但就是拿來觸發一些動作,被觸發的動作才是我們在意的,很少會在上頭多作著墨,頂多加個 Hover 變色或位移就很差不多了。
但看看下面這個實例:
一點小巧思,瞬間就讓按鈕活了起來。
而且因為範圍限縮在了按鈕的大小,就算動畫稍微華麗一些也不會對整體頁面造成太多干擾。
受到 Aaron 啟發,趁著空閒時間我也試著做了一個按鈕動畫,今天這篇文章就分享一下過程中使用到的工具與眉角!
靈感來源
這次的按鈕動畫主要修改自 Dribbble 上 YorKun 的作品 - Button Lock Animation,感謝作者還有附上 Figma 檔案,讓我能更輕鬆的參照 Style。
不過我並沒有完全照著原作的動畫製作,主要是想多試試一些不同的動畫組合,接下來我會一一介紹。
動畫實作
我一開始想達到的動畫有四項:
- 滑動解鎖
- 鎖頭開啟與掉落
- 對應開鎖狀態的動畫
- 鎖頭拖拉時的 2D 物理效果
理論上應該是很快就能完成,但因為對 GSAP 不熟,花了些冤枉路,導致最後只完成了前三項的效果,算是差強人意。
用到的工具主要是 GSAP 與 GSAP 的 Draggable plugin
滑動解鎖
GSAP 的 Draggable plugin 真的有夠簡單好用,只要給定想要啟動 Draggable 的 DOM 物件,並指定要拖拉的方向(type)與範圍(bounds),就能瞬間完成這樣的效果(demo 由此去):
// 註冊 gsap 的 draggable plugin
gsap.registerPlugin(Draggable);
// 把需要互動的 DOM 用 querySelector 選出來
const button = document.querySelector(".unlock-btn");
const lockerArea = button.querySelector(".locker");
const dropArea = button.querySelector(".drop");
// 主要的 Draggable instance
Draggable.create(lockerArea, {
type: "x",
bounds: button,
onDrag(e) {},
onRelease(e) {
if (!this.hitTest(dropArea)) {
gsap.to(lockerArea, {
x: 0,
duration: 0.6,
ease: "elastic.out(1, .75)"
});
} else {
// this.disable();
gsap.to(lockerArea, {
x: dropArea.offsetLeft - 9,
duration: 0.6,
ease: "elastic.out(1, .8)",
onUpdate(e) {
tl.restart();
}
});
}
}
});
中間可以看到,我們指定 type
為 x
,表示移動方向為 x 軸,而 bounds
為 button
DOM 物件,所以最多不會拖移超過 butotn 的範圍。
另外,影片中有一個效果是當你拖拉到前後兩端點的時候,會有一個吸力把拖移中的物件吸過去,這段其實是需要靠額外的兩個動畫效果來達成。
Draggable.create()
可以傳入的 Option 中,能指定 onDrag
與 onRelease
handler,在 onRelease
的時候我們可以透過 this.hitTest(dropArea)
這個 Draggable 內建的函式判斷拖拉中的物件是否觸碰到另一個指定的 DOM 物件,若還沒碰到,我們就拉回到起點,也就是這段所做的事:
gsap.to(lockerArea, {
x: 0,
duration: 0.6,
ease: "elastic.out(1, .75)"
});
透過 gsap.to
可以讓指定的 DOM 物件變換到我們傳入的 property 狀態,以此例子來說就是位移到原點,等同於 apply transform:translateX(0)
。
而若觸碰到指定物件,則可以調整 x
來將拖移物件直接拉到指定物件,這樣就能製造出吸力的效果。
此外,在觸碰到物件後的 gsap.to
函式中,我們也傳入了 onUpdate
handler,該 handler 會在動畫完成後被觸發,剛好讓我們能接著下一階段的動畫 - 鎖頭開啟與掉落。
鎖頭開啟與掉落
當拖移物件觸碰到指定物件時,onUpdate
會被觸發:
onUpdate(e) {
tl.restart();
}
onUpdate
中我們放的是一個 Timeline
物件,它能讓我們進行序列動畫,一步步指定各個物件該如何依序執行動畫。
由於我是將整個 timeline 動畫定義在別處,所以當 onUpdate
被觸發時是呼叫 tl.restart()
,你也可以直接定義在 handler 裡面。
Timeline 使用方法一樣簡單:
let tl = gsap.timeline({ paused: true }); //create the timeline
先創建一個 timeline 物件,這邊傳入 { paused: true }
是因為我希望在之後才觸發他(上述所說,在拖移物件移動到指定區域後才觸發),所以先預設讓他暫停,這樣我們在 onUpdate
時再呼叫 restart()
即可。
題外話,一開始我並不是用 Timeline 而是在每個 gsap.to
的 onUpdate
中去呼叫另一個 gsap.to
,這樣雖然也是可行,但讓程式碼可讀性降低很多,最終我才改成用 Timeline 來串接序列動畫。
接著就是針對每個我們想要觸發動畫的 DOM 物件設定欲變化的值:
先讓整個鎖頭的身體部分往下位移,讓上面鐵環部分保持原地,造出開鎖的效果。
tl.to(lockerBody, {
y: "120%",
duration: 0.2
})
接著利用 keyframes
針對單一物件進行一連串較為細緻的動畫,這邊主要是要將整個鎖頭(包含身體與鐵環部分)進行位移與旋轉,營造出鎖頭打開並從鎖上拿掉的動畫:
tl.to(lockerBody, { /*...略*/ })
.to(locker, {
keyframes: [
{
rotation: -45,
x: -8,
transformOrigin: "center",
duration: 0.2
},
{
x: -15,
y: -1,
duration: 0.2
},
{
x: -30,
y: 10,
duration: 0.2
},
{
y: 100,
opacity: 0,
duration: 0.2
}
]
})
接著也是差不多的步驟,一步步對其他的 DOM 物件加上最後的 - 對應開鎖狀態的動畫,替換掉 UNLOCK 字樣:
tl.to(lockerBody, { /*...略*/ })
.to(locker, { /*...略*/ })
.to(lockerArea, {
rotation: -90,
duration: 0.3
})
.to(".message,.drop,.locker-area", {
y: 30,
opacity: 0,
duration: 0.1
})
.fromTo(
".read-ok, .unlock-msg",
{
y: -30,
opacity: 0
},
{
opacity: 1,
y: 0,
duration: 0.2
}
);
注意到的是我們除了傳入 DOM object 給 gsap.to
與 gsap.fromTo
外,也能直接指定 class name,非常方便。
就這樣簡單幾行程式碼,就做好了一個套用在按鈕上的動畫,應該還算是不錯吧!
See the Pen Drag to unlock button with locker (final ver.) by Arvin (@arvin0731) on CodePen.
結論
今天簡單練習了一下從 Dribbble 上找靈感然後用前端技術將動畫實作出來的過程,或許沒有什麼新的東西,但希望能給大家帶來點啟發,防疫期間不妨在家做點有趣的動畫或 CSS art,自娛娛人一下!