
以能夠做到 CSS 基本的動畫操作為切入點,初步了解 framer motion。
這邊我使用 Create React App 快速建立專案
npx create-react-app framer-motion-example
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 資料夾裡面,現在的範例頁有點醜,之後會慢慢進化。
回到最初的起點,使用 CSS 怎麼做動畫 ?
有兩種簡單的方式 :
className
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>  元件是對 DOM 元素進行 60 fps 與各種使用者交互動作 (Gestures) 優化  ,<motion> 可以是任何 HTML 元素或者 SVG 元素。
<motion.div> 、<motion.li> ...<motion.circle>...關於動畫與瀏覽器 rendering
請參考 : Rendering Performance 說明為什麼瀏覽器會選擇 16.67 ms 刷新,以此保證使用者體驗。
而 motion 元件表現都跟一般的 Component 所對應標籤 (HTML tag) 都一樣,差在提供多個 props 表現不同的動畫,使用 <motion> 可以做到 :
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)" // 計算
  }}
/>
上面只提到單一個動畫怎麼操作,那怎麼做到連貫性的動畫呢 ?
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 是剪輯階段的節奏安排。
motion API 還提供另一個 animate function ,這在 Utilities。為了不混淆,我不會提到它,它是進階的操作 animate。可以想一下 setState 有兩種賦值的方法,直接輸入值跟 updater function, animate 也是大概的概念。
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>
  );
}
效果 :
如果等於 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>
      </>
  );
}
效果 :
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 又有更細節的物理表現設定 :
spring  : 預設,模擬彈簧。tween : 補間動畫,電腦幫你補動作。inertia : 慣性 (沒錯 ! 就是不討喜的牛頓運動定律 3 部曲),很常用於拖曳的動畫上。<motion.div
  transition={{
      ease: 'ease-in-out',
      duration: 2,
      delay : 1,
      type : "inertia"  // 慣性動畫
      velocity: 50 // 按照 type 不同才會有衍生的屬性
  }}
/>
由於設定太長了,就看效果吧 :
大家可以自己添加不同屬性試試看,在後面章節會再來細談每一種 type 更詳細的效果。
<motion.x> : 讓 HTML 或 SVG 可以擁有動畫狀態的 propsfalse  animate 就會變成 initial
明天會講到跟使用者互動有關的 Gestures。
在 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 玩玩看
這裡會放置一些每日寫作心得跟主題以外的想法,我說過目標也是提升寫作、敘述的能力,算是自 言自語 我檢討,不一定每天都有。
鐵人賽規定 300 字,我想雜談不會是拿來湊字數的 :P。我很容易寫又臭又長的文章,不知道大家會不會排斥長文 ? 或者類似長時數的教學影片 (像是 freeCodeCamp) ? 由於我本身有數位學習科系的背景,蠻好奇實際上學習者狀況。
歡迎留言理性討論 QQ, 賽後有時間我會在自己的 blog 上討論問題。