iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0
Modern Web

[ 重構倒數30天,你的網站不Vue白不Vue ] 系列 第 17

[重構倒數第14天] - Vue3處理動態效果(二)

前言

該系列是為了讓看過Vue官方文件或學過Vue但是卻不知道怎麼下手去重構現在有的網站而去規畫的系列文章,在這邊整理了許多我自己使用Vue重構很多網站的經驗分享給讀者們。

上一個章節我們講了兩個在 Vue 裡面做動態的方式,今天來說另外兩個做法。

第三種:使用第三方動畫工具 (GSAP)

GSAP 是一個功能強大,且 API 簡單,可以使用在一般的網頁或是canvas上面的動畫函式庫,因為它是為了專門做動畫所誕生的函式庫,所以不管是效能還是它所提供的功能,原比 JQuery 的 animate來的更加強大。

基本範例一 : 把一個DOM從左邊移動到右邊

<div class="box-100 bg-red"></div>
.box-100{
    width: 100px;
    height: 100px;
}
.bg-red{
    background-color: red;
}
gsap.to(".box-100", {duration: 1, x: 200, ease: "elastic"});
  • .to: 動畫是從起點到終點的執行
  • duration : 這個動畫執行的秒數
  • x: 水平移動,綁定的元件最後到達的地方
  • ease : 動畫速率

mike vue gsap

codepen範例 : https://codepen.io/MikeCheng1208/pen/ExXgQYg

基本範例二 : 依序執行動畫

<div class="cube1 box-100 bg-red"></div>
<div class="cube2 box-100 bg-blue"></div>
<button id="btn">click</button>
gsap.timeline({ defaults: { duration: 1, ease:"elastic" }})
    .to(".cube1", {x: 100})
    .to(".cube2", {y: 100})
    .to(".cube2", {x: 100, duration: 0,3}) 
  • .timeline : 控制動畫執行的時間線,可以產生補間影格讓動畫依序執行。

先把動畫參數設定完成後,在執行.to,之後所有的 .to 會全部參照 timeline 設定好的去執行,當然你也可以中途去改變,最後的方塊執行速度會變快,不會照 timeline 設定的 defaults 去執行。

mike vue gsap

codepen 範例 : https://codepen.io/MikeCheng1208/pen/jOwMZNj

接下來我們來看怎麼在 Vue 裡面使用 GSAP 吧 !

這是我們要使用GSAP來製作的效果,利用卷軸的捲動,來讓我的圖片依序照不同的方向跟旋轉出現。

mike vue gsap

我們先來看 template 的部分,就是把圖片給放到上面來

<template>
<div id="box">
     <div class="pic pic1"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic2"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic3"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic4"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic5"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic6"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic7"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic8"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic9"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic10"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
     <div class="pic pic11"><img src="https://source.unsplash.com/user/erondu/240x240"></div>
</div>
</template>

然後css的部分

<style>
#box{
    width: 240px;
    height: auto;
    margin: 0 auto;
    padding-top: 250px;
}
.pic{
    width: 240px;
    height: auto;
    overflow: hidden;
    background-color: #fff;
    margin-bottom: 20px;
    opacity: 0;
}
.pic > img{
    display: block;
    width: 240px;
    height: auto;
}
.pic1{
    margin-left: -100px;
}
.pic2{
    margin-left: 100px;
}
.pic3{
    margin-top: 100px;
}
.pic4{
    transform: scale(0);
}
.pic5{
    width: 0;
}
.pic6{
    transform:scaleX(-1.4);
}
.pic7{
    margin-left: -100px;
}
.pic8{
    margin-left: 100px;
}
.pic9{
    transform:scaleY(-1.4);
}
.pic10{
    margin-top: 100px;
}
.pic11{
    width: 0;
}
</style>

在這邊我把所有圖片的透明度全部設定成 0,然後每一張圖片依序的起始位置就是我動畫要出現的地方,你會看到有 transform: scale(0); 或是 margin-left: -100px;之類的,所以接下來就是要透過 GSAP 來把它恢復原狀,中間的過程做補間動畫

<script>
import { onMounted, onUnmounted } from 'vue';
import gsap from 'gsap';
export default {
  setup(){
      
      let tl = null;
      
      // 抓 document 的 高
      const getDocumentHeight = () => {
        const body = document.body;
        const html = document.documentElement;
        return Math.max(
          body.offsetHeight,
          body.scrollHeight,
          html.clientHeight,
          html.offsetHeight,
          html.scrollHeight
        )
      }
    
      // 抓卷軸的位置
      const documentTop = () => (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop
      
      // 動畫初始化
      const gsapInit = () => {
        tl = gsap.timeline({defaults: { duration: 1 }});
        tl.to(".pic1", {"margin-left": 0, "opacity":1});
        tl.to(".pic2", {"margin-left": 0, "opacity":1});
        tl.to(".pic3", {"margin-top": 0, "opacity":1});
        tl.to(".pic4", {"transform": "scale(1)", "opacity":1, ease: Bounce.easeOut});
        tl.to(".pic5", {"width": "240px", "opacity":1});
        tl.to(".pic6", {"transform":"scaleX(1)", "opacity":1});
        tl.to(".pic7", {"margin-left": 0, "opacity":1});
        tl.to(".pic8", {"margin-left": 0, "opacity":1});
        tl.to(".pic9", {"transform":"scaleY(1)","opacity":1});
        tl.to(".pic10", {"margin-top": 0, "opacity":1});
        tl.to(".pic11", {"width": "240px", "opacity":1});
        tl.pause();
      }
    
      const handleWinScroll = () =>{
          const scrollTop = documentTop();
          const docHeight = getDocumentHeight();
          const winHeight = window.innerHeight;
          const scrollPercent = (scrollTop) / (docHeight - winHeight);
          tl.progress(scrollPercent);
      }
    
      onMounted(()=> {
          gsapInit();
          window.addEventListener("scroll", handleWinScroll);
      })
      
      onUnmounted(()=> {
          window.removeEventListener("scroll" ,handleWinScroll);
      })
    
      return {}
  }
};
</script>

這邊要注意我宣告了一個 tl但是我並沒有用 refreactive 包起來,這是因為這個是要給 gsap 的實體存放的,不需要透過 Vue 包起來,所以不需要用 refreactive

tl = gsap.timeline({defaults: { duration: 1 }});
tl.to(".pic1", {"margin-left": 0, "opacity":1});
tl.to(".pic2", {"margin-left": 0, "opacity":1});
tl.to(".pic3", {"margin-top": 0, "opacity":1});
tl.to(".pic4", {"transform": "scale(1)", "opacity":1, ease: Bounce.easeOut});
tl.to(".pic5", {"width": "240px", "opacity":1});
tl.to(".pic6", {"transform":"scaleX(1)", "opacity":1});
tl.to(".pic7", {"margin-left": 0, "opacity":1});
tl.to(".pic8", {"margin-left": 0, "opacity":1});
tl.to(".pic9", {"transform":"scaleY(1)","opacity":1});
tl.to(".pic10", {"margin-top": 0, "opacity":1});
tl.to(".pic11", {"width": "240px", "opacity":1});
tl.pause();

這邊你會看到我依序地把圖片一個一個恢復原狀的位置,所以一開始的時候我會用 CSS 把元件的位置給移開,再透過GSAP給補回來。

這邊要特別注意一下 tl.pause();這個函式,它是把這個動畫整個先暫停下來,然後在使用 scroll事件來跑我們動畫的補間進度,透過換算,我們可以得出一個卷軸從最上面滑到最下面的百分比,然後把這個百分比塞入 tl.progress();這個函式內,就可以依照卷軸百分比來顯示你的圖片。

  • tl.progress() : 是百分比是從 0 ~ 1 ;

透過 GSAP 的方式我們可以很簡單的完成這樣子的卷軸控制物件出來的動態,只是要注意,抓取 DOM 的時候需要等 DOM 都 render 到畫面上,所以必須要在 onMounted 的時候再去執行,我也是在 onMounted才去執行 gsapInit 這個函式的。

codepen範例 : https://codepen.io/MikeCheng1208/pen/JjJRpXJ

GSAP補充教學

我有錄製了一份 GSAP3 的免費教學放在 Youtube 上面,如果對 GSAP 有興趣的可以看一下,或是上 GSAP 的官網看看。

GSAP 官網 : https://greensock.com/gsap/
youtube 連結 : https://www.youtube.com/playlist?list=PLbOfcOk7bN40WfzgRMLEQzkayS8pjWO_e

第四種:Vue 的 transition + 第三方動畫工具 (GSAP)

在上一篇教學中,有提到 transition 有提供動畫的 Lifecycle Hooks,所以我們可以針對每個動畫所執行的階段去做一些操作,不過同樣的我們也可以在這些 Lifecycle Hooks 裡面去做動畫的操作,捨棄掉原本的 CSS,透過第三方動畫工具整合,我們可以做出更多 CSS 難以做到的動畫效果,這邊我一樣選用 GSAP 來做整合,GSAP 可是連 Vue 都指令拿來做文件的範例用的,你說這可有多好用呢~

我們先來看一下 transition 的 Lifecycle Hooks。

<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false"
>
</transition>

進場

  • @before-enter:開始的動畫開始前。

  • @enter:動畫開始的執行過程。

  • @after-enter:開始的動畫結束時。

  • @enter-cancelled:開始的動畫被取消。

離開

  • @before-leave:離開的動畫開始前。
  • @leave:離開動畫的執行過程。
  • @after-leave:離開動畫的結束時。
  • @leave-cancelled:離開的動畫被取消。

額外設置

  • v-bind:css:設定 false 可以讓動畫執行的過程中不會受到 css的影響。

關於 Transition 的 Lifecycle Hooks 官方文件 : https://v3.vuejs.org/guide/transitions-enterleave.html#javascript-hooks

其實大部分的 Lifecycle Hooks 就跟我們的 css class 是一樣的,只是多了一兩個東西而已,所以接下來我們就要來看怎麼把 Vue 跟 GSAP 做一個整合。

vue mike gsap

我們拿上一個章節所用的這個輪播範例來改,我們拿掉了 name,加上了 Hooks 。

<transition-group
    @before-enter="beforeEnter"
    @enter="enter"
    @leave="leave"
    :css="false"
>
   <img 
        v-for="(item, idx) in slidList" 
        v-show="imgIdx === idx" 
        :key="item.id" 
        :src="item.src"
   />
</transition-group>
// --------
// ENTERING
// --------
const beforeEnter = (el) =>  {
    gsap.set(el, {
        scaleX: 0,
        scaleY: 0,
        x: 0,
        y: 0,
        opacity: 0,
    })
}
const  enter = (el, done) =>  {
    gsap.to(el, {
        duration: 1,
        scaleX: 1,
        scaleY: 1,
        opacity: 1,
        ease: 'elastic.inOut(1,2, 1)',
        onComplete: done
    })
}

// --------
// LEAVING
// --------
const leave = (el, done) => {
    gsap.to(el, {
        duration: 0.7,
        scaleX: 3,
        scaleY: 0.05,
        x: 0,
        opacity: 0,
        ease: 'elastic.inOut(2.5, 1)'
    })
    gsap.to(el, {
        duration: 0,
        delay: 0.5,
        onComplete: done
    })
}

Lifecycle Hooks 使用的時候會回傳當前執行動畫的 DOM 實體,所以我們就要拿這個實體直接來套入 GSAP 中,就不需要直接寫 class 的名稱在裡面。

首先我們透過 beforeEnter 先用gsap.set來設定動畫開始以前的樣式,然後當動畫在執行的時候在改變回來,然後當動畫執行到離開(leave) 的時候,再去做離開的動畫樣式處理,只是要特別注意,在 enterleave hooks 中,必須要在動畫結束後調用 done這個函式,不然的話當你動畫還沒有執行完你在執行下一段的時候,補間就會直接完成動畫,沒有中間的動畫效果,所以 GSAP 在動畫完成之後有提供自己的 lifecycle,動畫完成之後執行的 lifecycle 叫 onComplete,所以我就可以把, enterleave 回傳的done函式放入 onComplete 去執行它。

codepen 範例 : https://codepen.io/MikeCheng1208/pen/OJgRQBK

先告一個段落

關於 Vue 的動畫上面的處理就先告一個段落,其實網路上面也是還有其他的套件或是整合方式,只是在工作上面來說,這四種動畫的處理方式已經可以解決大部分的問題了,我都有附上 codepen 的範例,有興趣的朋友可以自己玩玩看。

Mike vue

那如果對於Vue3不夠熟的話呢?

Ps. 購買的時候請登入或註冊該平台的會員,然後再使用下面連結進入網站點擊「立即購課」,這樣才可以讓我獲得更多的課程分潤,還可以幫助我完成更多豐富的內容給各位。

我有開設了一堂專門針對Vue3從零開始教學的課程,如果你覺得不錯的話,可以購買我課程來學習
https://hiskio.com/packages/AYR5m7VR3

那如果對於JS基礎不熟的朋友,我也有開設JS的入門課程,可以參考這個課程
https://hiskio.com/packages/Q9R4OYoyD

訂閱Mike的頻道享受精彩的教學與分享

Mike 的 Youtube 頻道
Mike的medium
MIke 的官方 line 帳號,好友搜尋 @mike_cheng


上一篇
[重構倒數第15天] - Vue3處理動態效果(一)
下一篇
[重構倒數第13天] - Vue3定義自己的模板語法
系列文
[ 重構倒數30天,你的網站不Vue白不Vue ] 31

尚未有邦友留言

立即登入留言