以能夠做到 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 上討論問題。