iT邦幫忙

2022 iThome 鐵人賽

DAY 2
0
自我挑戰組

向網頁施點魔法粉 framer-motion 系列 第 2

#02 Dancing with Animation and Transition

  • 分享至 

  • xImage
  •  

以能夠做到 CSS 基本的動畫操作為切入點,初步了解 framer motion。

目錄

  1. 安裝環境
  2. Magic Is Happening !
  3. 躍上五線譜 : motion Component
  4. 完美的 Ending Pose : animate props
  5. dj drop a beat ! : initial props
  6. 多層次口感 : transition
  7. 補充 : 2022 年 CSS transform 的大改版

本節資源 :
原始碼 | 網頁展示

安裝環境

這邊我使用 Create React App 快速建立專案

  • create-react-app
npx create-react-app framer-motion-example
  • 加入 framer motion 本體
npm i framer-motion
  • 使用的版本
"dependencies": {
  "framer-motion": "^7.3.5",
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "react-scripts": "5.0.1",
  ...
}

在專案中我會使用資料夾來分隔每一天的範例,程式碼都在 components 資料夾裡面,現在的範例頁有點醜,之後會慢慢進化。

Magic Is Happening !

回到最初的起點,使用 CSS 怎麼做動畫 ?

有兩種簡單的方式 :

  1. 全都靠 CSS 的 className
  2. 在 JS 寫 inline style
  • style.css
.box{
    width: 100px;
    height: 100px;
    background-color: red;
    margin: 10px;
}

.box.anim{
  animation: move 1s ease normal forwards;
}
@keyframes move {
  from{
      transform: translateX(0px);
  }
  to{
      transform: translateX(100px);
  }
}
  • NormalAnimation.js
import React from "react";
import "./style.css"; // CSS 引入

export default function NormalAnimation() {
    return (
        <>
            {/*  全用 className */}
            <div className="box anim">only className</div>
            {/*  只用到 keyframes 字串 */}
            <div
                className="box"
                style={{
                    animation: "move 1s ease normal forwards",
                }}
            >
                inline style animation
            </div>
            {/*  把 animate 給分開寫,用 CamelCase*/}
            <div
                className="box"
                style={{
                    animationName: "move",
                    animationDuration: "1s",
                    animationTimingFunction: "normal",
                    animationFillMode: "forwards",
                }}
            >
                inline style separate animation property
            </div>
        </>
    );
}

那 framer motion 呢 ?

import React from "react";
import { motion } from "framer-motion"; //  1. 引入 motiom Component
import "./style.css";

export default function FramerMotionAnimation() {
    return (
        {/*  2. 把原本的 div 改成 motion.div */}
        <motion.div 
          className="box framer" 
          {/*  3. props animate 是元素最後的動畫狀態 */}
          animate={{ x: 100 }}
        >
            framer motion
        </motion.div>
    );
}

蛤 ! 就這麼簡單? 對,就是這麼簡單。
來源自網路
後面會再說明 animate props 的使用方式,但可以看到只要透過兩步驟,就可以達到動畫。

所有效果 :

添加 motion 的元件在預設的動畫中就會有彈簧動畫 (Spring Animation),具有物理的運動狀態,看起來就沒有那麼死板,如果不想要有 ㄉㄨㄞ ㄉㄨㄞ 這樣的效果,是可以替換的。

躍上五線譜 : motion Component

<motion> 元件是對 DOM 元素進行 60 fps 與各種使用者交互動作 (Gestures) 優化 ,<motion> 可以是任何 HTML 元素或者 SVG 元素。

  • HTML : <motion.div><motion.li> ...
  • SVG : <motion.circle>...

關於動畫與瀏覽器 rendering
請參考 : Rendering Performance 說明為什麼瀏覽器會選擇 16.67 ms 刷新,以此保證使用者體驗。

而 motion 元件表現都跟一般的 Component 所對應標籤 (HTML tag) 都一樣,差在提供多個 props 表現不同的動畫,使用 <motion> 可以做到 :

  • 搭配 animate props ,執行簡單的動畫
  • 加入拖曳 (drag)、畫面平移 (pan) (用於手機滑的操作)、滑鼠移入 (hover) 或手指點擊 (touch) 等交互動作
  • 使用者交互動作後的回饋動畫
  • 透過變體 (variants) 設定更多動畫細節

完美的 Ending Pose : animate props

大部分的動畫都是使用 animate props,animate 是一個物件,就跟寫 inline style 一樣,也要保持駝峰式 (CamelCase) 的寫法。animate props 用來表示 元件 mount 之後最終的動畫狀態

<motion.div 
 animate={{ 
    x: 100 
  }}
/>

與一般的 CSS 屬性有點不同,位移是使用座標 x ,等同於 transform: translateX() ,而且 motion 的位移預設單位是 px ,因此透過 x: 100 ,motion 元件是知道 "向右移動 100 px",以此類推 y 也是。

基本的單位 motion 是隱式的,像是 deg (degree) 、 px使用其他單位轉成字串 就可以了,此外也可以計算 (calc) 或者使用 CSS 變數 variable。

<motion.div
  animate={{
    x : 100, // 水平位移
    y : 50, // 垂直位移
    rotateZ: 90,  // 旋轉
    fontSize : 50, // 字體到 50 px
    width : '100%',
    backgroundColor: '#ff0',
    marginTop : "var(--mTop)", // 自定義的 CSS 變數
    height: "calc(100vh -  64px)" // 計算
  }}
/>

123 牽著手,456 抬起頭 ─ keyframe 影格

上面只提到單一個動畫怎麼操作,那怎麼做到連貫性的動畫呢 ?

CSS 擁有強大 keyframes 可以針對不同時刻用逐格動畫,而 motion 也可以,並且 用陣列來表示

  • 來走個正方形吧,想像自己在座標軸上走
<motion.div
  animate={{
      x : [0,100,100,0]
      y : [0,0,100,100]
  }}
/>

風騷走位
以此類推其他的屬性也是如此。

另外在 CSS 我們可以在 keyframes 切秒數的 % 數決定,而在 animate 的預設 keyframes duration 則是 0.8s

The duration of the tween animation. Set to 0.3 by default, 0r 0.8 if animating a series of keyframes.

相關設定是在 transition props 而不是在 animate 本身。我把 animate 當做腳本,是一步步要元素怎麼演; transition 是剪輯階段的節奏安排。

另一個 animate ?

motion API 還提供另一個 animate function ,這在 Utilities。為了不混淆,我不會提到它,它是進階的操作 animate。可以想一下 setState 有兩種賦值的方法,直接輸入值跟 updater function, animate 也是大概的概念。

dj drop a beat ! : initial props

initial 是一個物件,顧名思義就是動畫的起點,定義元件在 mount 之前所在的動畫狀態,也就是可以讓元素不限於 CSS 布局流 (Flow Layout) ,更像是暫時添加了 position : absolute ,移動時也不會去擠壓其他元素。

比如說從左邊飛過來的動畫,原本只有 animate 只能在正常的區塊布局 (Normal Flow) 作為起點,initial 則是可以讓元素跳脫來指定位置 :

<motion.div
  className="box"
  initial={{
    x : -100
  }}
  animate={{
    x : 100
  }}
/>

如此一來可以做到大部分的 PPT 式的陽春的動畫 :

// ? 先訂好所有的動畫
const direction = [
    {
        name: "左飛入",
        initial: {
            x: "-100%",
        },
        animate: {
            x: "0%",
        },
    },
    {
        name: "右飛入",
        initial: {
            x: "100%",
        },
        animate: {
            x: "0%",
        },
    },
    {
        name: "上飛入",
        initial: {
            y: "100%",
        },
        animate: {
            y: "0%",
        },
    },
    {
        name: "下飛入",
        initial: {
            y: "-100%",
        },
        animate: {
            y: "0%",
        },
    },
];
export default function PPTAnimation() {
  return (
      <div className="section">
         {/* 用 map 展開 */}
          {direction.map(({ name, initial, animate }) => (
              <div
                  key={name}
                  style={{
                      marginLeft: "50px",
                  }}
                >
                  <h3>{name}</h3>

                  <motion.div
                      className="box"
                      initial={{ ...initial, opacity: 0 }}
                      animate={{ ...animate, opacity: 1 }}
                  />
              </div>
          ))}
      </div>
  );
}

效果 :
漸進飛入

initial 可以是 false

如果等於 false ,起始位置就是等於 animate,就變不會動了。

export default function InitialFalse() {
  const [toggle, setToggle] = useState(false);

  return (
      <>
          <input
              type="checkbox"
              onChange={() => setToggle((boolean) => !boolean)}
          />
          <motion.div
              key={toggle}
              className="box"
              initial={toggle ?? { x: 0 }}
              animate={{
                  x: 100,
              }}
>
              Initial{toggle ? "True" : "False"}
          </motion.div>
      </>
  );
}

效果 :
初始化會沒效果是因為 initial 等於 false

多層次口感 : transition

transition 有很多不同的屬性,除了影響元素本身,也可以影響底下所有的子元素。

  • 複習一下 CSS transition 的內容
// transition : <property> <duration> <delay> <timing-function>;

.box{
  transition : all 3s 1s ease-in-out;
}

motion 也是一樣,只是把屬性分開寫

<motion.div
  transition={{
      ease: 'easeInOut'
      duration: 2
      delay : 1
  }}
/>

上面有提到不想要彈簧動畫怎麼辦 ? 改變 transition 的 type !

  • type : 改變物理狀態。motion 提供三種物理狀態,每一種對應的 type 又有更細節的物理表現設定 :
    1. spring : 預設,模擬彈簧。
    2. tween : 補間動畫,電腦幫你補動作。
    3. inertia : 慣性 (沒錯 ! 就是不討喜的牛頓運動定律 3 部曲),很常用於拖曳的動畫上。
<motion.div
  transition={{
      ease: 'ease-in-out',
      duration: 2,
      delay : 1,
      type : "inertia"  // 慣性動畫
      velocity: 50 // 按照 type 不同才會有衍生的屬性
  }}
/>

由於設定太長了,就看效果吧 :
三種不同效果

大家可以自己添加不同屬性試試看,在後面章節會再來細談每一種 type 更詳細的效果。

總結

  • <motion.x> : 讓 HTML 或 SVG 可以擁有動畫狀態的 props
  • animate : mount 之後的元素狀態
  • initial : mount 之前的元素狀態,可以是 false animate 就會變成 initial
  • transition : CSS transition 基本屬性、根據不同物理類型對應不同動畫狀態

明天會講到跟使用者互動有關的 Gestures。

補充 : CSS transform 的大改版

在 2022 年 CSS transform 屬性有個大突破 ,把底下的屬性都拆分開來,改善 transform 每次都只能全部屬性覆蓋過去,分開之後就可以不影響其他屬性改變值,達到更高細微度的操作,感覺搭配 OOCSS 就能很方便搭配出各種動畫效果。

更棒的是,主流瀏覽器都有支援

來源自文章圖片

  • 以前
.target {
  transform: translateX(50%) rotate(30deg) scale(1.2);
}
.target:hover {
  // 當你只想改變 scale 必須全部複製過去
  transform: translateX(50%) rotate(30deg) scale(2);
  // ❌ 你不能這樣,因為這樣會全部蓋過去
  transform: scale(2);
}
  • 現在
.target {
  transform: translateX(50%) rotate(30deg);
  scale : 1.2
}
.target:hover {
  scale : 2 // 控制 scale 同時不影響其他值
}

或者到 codepen 玩玩看

參考資料

  1. 官方文件 : Introduction | Framer for Developers
  2. 瀏覽器更新率 : Rendering Performance
  3. CSS Transform 更細微度的操作 : Finer grained control over CSS transforms with individual transform properties

雜談

這裡會放置一些每日寫作心得跟主題以外的想法,我說過目標也是提升寫作、敘述的能力,算是自 言自語 我檢討,不一定每天都有。

鐵人賽規定 300 字,我想雜談不會是拿來湊字數的 :P。我很容易寫又臭又長的文章,不知道大家會不會排斥長文 ? 或者類似長時數的教學影片 (像是 freeCodeCamp) ? 由於我本身有數位學習科系的背景,蠻好奇實際上學習者狀況。

  • 第一眼遇到 "長" 類型會有意願繼續下去嗎 ?
  • 背後動力是什麼,如何維持動力 :o ?
  • 教學啃不下去,花時間看劇卻可以,在理想教學中怎樣的「戲劇橋段」 會吸引你 ?

歡迎留言理性討論 QQ, 賽後有時間我會在自己的 blog 上討論問題。


上一篇
#01 Who & Why framer-motion ?
下一篇
#03 Put your Gestures up - Hover, Focus & Tap
系列文
向網頁施點魔法粉 framer-motion 15
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言