Well in Time
練習動畫的好選擇 - 按鈕
05-22-20219 Min Read

製作動畫,人人有責。

前言

疫情升溫,待在家裡救人救己,除了打電動玩健身環外,也是個好機會來培養培養自己的美術能力,然而平常工作沒什麼機會製作動畫的我,即便有了時間,也不知道要從何下手,上了 Dribbble、CodePen 找靈感,的確看到很多有趣的作品,但是大多都很複雜,不像是一個週末午後的休閒良品,例如,Ben Evans 的這個作品:

See the Pen Pure CSS Landscape - An Evening in Southwold by Ben Evans (@ivorjetski) on CodePen.

這張像是照片一樣的圖片,你能想像是單純用 CSS 製作的嗎?作者有放上他製作這作品的縮時影片(影片的音樂還是他自己做的,真有才華),雖然不知道總共花了多少時間,但以他的另一個同樣驚人且至少花費一百小時的作品推斷,時數少不到哪去的。

我知道很多人會覺得,『對呀很酷,但為什麼?』。

我也不懂為什麼,但知道能利用 CSS 做出這種極限真的很令人感到興奮。光是觀察他的程式碼就可以學到不少技巧,像是:

  1. Custom HTML tag

即便是用 CSS 繪圖,相信大部分的人也都是用普通的 divspan 來組裝圖案,但如果你打開剛剛那範例的 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。

  1. 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。

不過我並沒有完全照著原作的動畫製作,主要是想多試試一些不同的動畫組合,接下來我會一一介紹。

動畫實作

我一開始想達到的動畫有四項:

  1. 滑動解鎖
  2. 鎖頭開啟與掉落
  3. 對應開鎖狀態的動畫
  4. 鎖頭拖拉時的 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();
        }
      });
    }
  }
});

中間可以看到,我們指定 typex,表示移動方向為 x 軸,而 boundsbutton DOM 物件,所以最多不會拖移超過 butotn 的範圍。

另外,影片中有一個效果是當你拖拉到前後兩端點的時候,會有一個吸力把拖移中的物件吸過去,這段其實是需要靠額外的兩個動畫效果來達成。

Draggable.create() 可以傳入的 Option 中,能指定 onDragonRelease 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.toonUpdate 中去呼叫另一個 gsap.to,這樣雖然也是可行,但讓程式碼可讀性降低很多,最終我才改成用 Timeline 來串接序列動畫。

接著就是針對每個我們想要觸發動畫的 DOM 物件設定欲變化的值:

先讓整個鎖頭的身體部分往下位移,讓上面鐵環部分保持原地,造出開鎖的效果。

tl.to(lockerBody, {
  y: "120%",
  duration: 0.2
})

demo

接著利用 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
      }
    ]
  })

demo

接著也是差不多的步驟,一步步對其他的 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.togsap.fromTo 外,也能直接指定 class name,非常方便。

就這樣簡單幾行程式碼,就做好了一個套用在按鈕上的動畫,應該還算是不錯吧!

See the Pen Drag to unlock button with locker (final ver.) by Arvin (@arvin0731) on CodePen.

結論

今天簡單練習了一下從 Dribbble 上找靈感然後用前端技術將動畫實作出來的過程,或許沒有什麼新的東西,但希望能給大家帶來點啟發,防疫期間不妨在家做點有趣的動畫或 CSS art,自娛娛人一下!

資料來源

  1. Ben Evans
  2. Aaron Iker
  3. GSAP
  4. frontend.horse
© by Arvin Huang. All rights reserved.
theme inspired by @mhadaily
Last build: 09-08-2024