Svelte Animation 相關
tags: Svelte
category: Front-End
description: Svelte Animation 相關
created_at: 2023/04/17 16:00:00
前言
上一篇講完 Store
後,可以開始來記錄 animation
相關的東西(因為有些地方需要上一篇講到的 $
),會有以下主題:
Transition
Motion
:Tweened
Spring
Animation
Vue
當中有 Transition
、TransitionGroup
可以使用 Transition
,而 React
則需要額外去安裝相關套件。
而補間動畫的部分則只有 Svelte
有提供,其他都需要去安裝額外的套件。
Motion
這個主要是來做值的補間的,上一篇的 Store
設定完之後值會立刻改變,假設希望可以在一段時間以某種規律去從 A
改成 B
,就需要這個功能。
先來看看沒有這種套件的時候要怎麼做,之後才會知道有這種工具真的太棒了
tweened
自幹tweened
功能
tweened
功能<script lang="ts">
import { get, writable, type Writable } from 'svelte/store';
const value = writable(0);
function store_set(store: Writable<number>, value: number, duration = 1000) {
const from = get(store);
const to = value;
// 線性成長函數
const easing = (t: number) => t;
// 計算新的值
const interpolate = (from: number, to: number) => (t: number) => from + (to - from) * t;
// 紀錄開始時間
const start = performance.now();
const tick = (now: number) => {
// 跟 duration 算出已完成幾%
const t = Math.min(1, (now - start) / duration);
// 呼叫 interpolate 與 easing 計算出新的值並設定
store.set(interpolate(from, to)(easing(t)));
// t < 1 代表未完成,則繼續呼叫
if (t < 1) {
requestAnimationFrame(tick);
}
};
requestAnimationFrame(tick);
}
</script>
<div>
<button
on:click={() => {
store_set(value, $value + 100);
}}>+100</button
>
{$value}
</div>
這時可以戳看看按鈕,會看到畫面上 value
的值會在 1000
毫秒從 A
變成 A+100
。
嫌上面那段很複雜也沒關係,Svelte
有提供 tweened
函數,就是做上面那件事,也就是說下面這段是一樣的功能:
<script lang="ts">
import { tweened } from 'svelte/motion';
const value = tweened(0, {
duration: 1000,
easing: (t) => t,
interpolate: (from, to) => (t) => {
return from + (to - from) * t;
}
});
</script>
<div>
<button
on:click={() => {
$value += 100;
}}>+100</button
>
{$value}
</div>
實際上 tweened
還有提供一個 options
是 delay
,可以設定 delay
多久之後再跑(但這個和 duration
一樣應該很好理解就不舉例了)
easing
是定義一個函數,Svelte
有提供很多現成的可以直接用(範例),他主要就是描述從 0 ~ 1
的變化,也就是說你讓 t
乘以二,他過渡期間會跑到趨近於兩倍,但最後結束的時候會回到一倍,不相信的話自己把它改成 t * 2
而 interpolate
則是接收 easing
的值,對過渡期間的畫面上呈現的值做處理,但最後結束還是會回到你當初設定的 to
的值。
貼心提醒(?): 上面文字描述可能比較難懂,還是自己親自下去改改看會比較有感覺。
spring
上面的 tweened
補間比較像是設定一段時間然後從 A
透過函數計算到 B
,而 spring
這個函數則是設定流暢度、彈性然後他幫你補間。
這邊的差異的話可能用一般值顯示不好呈現,所以順便簡單做一個區塊做動畫
<script lang="ts">
import { spring } from 'svelte/motion';
const value = spring(0, {
stiffness: 0.1,
damping: 0.5
});
</script>
<div>
<button
on:click={() => {
$value += 100;
}}>+100</button
>
{$value}
<hr />
<div style="width: {$value}px; height: {$value}px; background: red;" />
</div>
這時去戳戳看按鈕,下面的 div
會被放大。
其中 spring
函數有三個選項(options
):
stiffness
: 硬度: 介於0 ~ 1
,越高越硬damping
: 彈力: 介於0 ~ 1
,越低越有彈性precision
: 精度,越低越精確,越高動畫也會越早結束
上面光看文字可能不太好想像,但可以想像就是在設定一個彈簧,然後稍微改參數去觀察他。
如果沒有要限制特定時間的補間,然後希望自然一些,可以使用 spring
幫你完成,如果你想設定更多細節(像是多久完成、補間函數之類),則可以使用 tweened
。
Transition
一個漸變,就想像是 CSS
提供的 transition
一樣,只是這可以做更細緻的操作。
基本使用
<script lang="ts">
import { fade } from 'svelte/transition';
let show = true;
</script>
<div>
<button on:click={() => (show = !show)}>Toggle</button>
{#if show}
<div transition:fade>Show</div>
{/if}
</div>
Svelte
內建的 transition
有提供這些,需要再去看,或是後面也會寫到可以客製化,所以這邊先以基本的 fade
與 blur
之類的當範例好了。
如果你只下 transition
,那麼出現跟消失都會套用同一個模式,如果你要分開的話可以使用 in
、out
,如下:
<script lang="ts">
import { fade, blur } from 'svelte/transition';
let show = true;
</script>
<div>
<button on:click={() => (show = !show)}>Toggle</button>
{#if show}
<div in:fade out:blur>Show</div>
{/if}
</div>
還可以帶入參數做細節的設定,例如設定 in
的時間,像下面這樣就會以 1000ms
來做 transition
的 fade
<div
in:fade={{
duration: 1000
}}
out:blur
>
Show
</div>
自訂 transition
在自訂之前,要先知道他的函數定義:
transition = (node: HTMLElement, params: any, options: { direction: 'in' | 'out' | 'both' }) => {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
傳入的部分是以下:
node
: 基本上就是這個指令套在誰身上params
: 指令後面帶入的參數,例如上面fade
的例子就是一個物件,裡面有duration: 1000
options
: 就是被套用在in
、out
、還是都有(both
)
回傳的部分都是可選(optional
)的:
delay
:delay
多久再執行duration
: 要用多少時間去做transition
easing
: 這個transition
使用的補間函數css
: 如何改變這個node
的CSS
tick
: 可以直接對node
做操作
如果可以,盡可能使用 css
,而不是 tick
,除非你要的效果必須使用 javascript
才能實現。
直接來看個 css
的例子:
<script lang="ts">
import * as easing from 'svelte/easing';
let length = 0;
function fn(node: HTMLElement, { duration }: { duration: number }) {
return {
duration,
css: (t: number) => {
const scaleY = easing.quadIn(t);
const x = easing.bounceInOut(t);
const y = easing.bounceIn(t);
return `
transform: scaleY(${scaleY}) translateX(${100 - x * 100}px) translateY(${-100 + y * 100}px);
`;
}
};
}
</script>
<button on:click={() => length++}>Add</button>
{#each { length } as _}
<div in:fn={{ duration: 1000 }}>Hello World</div>
{/each}
<style>
div {
font-size: 48px;
}
</style>
上例可以看到修改 css
,他會傳入一個 t
,跟前面補間的範例一樣,那個 t
會是 0 ~ 1
之間的值,可以根據他再去做運算,而且在內部也能再透過內建的補間函數去做計算,再來回傳 CSS String
來用。
還請見諒比較沒有美感,一時想不到好看的漸變效果
再來看看 tick
的範例,使用 css
和 tick
達到一樣的效果,但建議使用 css
,才不會占用 javascript
的主線程(main thread
)
function fn(node: HTMLElement, { duration }: { duration: number }) {
return {
duration,
// css: (t: number) => `font-size: ${48 * easing.cubicOut(t)}px;`
tick: (t: number) => {
node.style.fontSize = `${48 * easing.cubicOut(t)}px`;
}
};
}
可以把上面的程式切換使用看看,效果是一樣的。
再舉一個只有 js
才能達到的例子:
function typewriter(node: HTMLElement, { speed = 1 } = {}) {
const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;
if (!valid) {
throw new Error(`This transition only works on elements with a single text node child`);
}
const text = node.textContent || '';
const duration = text.length / (speed * 0.01);
return {
duration,
tick: (t: number) => {
const i = Math.trunc(text.length * t); // 僅取整數
node.textContent = text.slice(0, i); // 設定為前 i 位
}
};
}
這是一個官方提供的 typewriter
的範例,因為我一時也想不到什麼點子,所以我就把它翻譯成 TypeScript
Transition
的 Events
這邊就相對簡單很多,就只是監聽事件,可以自己在做一些處理(如果需要的話)
<script lang="ts">
import { fade } from 'svelte/transition';
let length = 0;
</script>
<button on:click={() => length++}>Add</button>
<button on:click={() => length--}>Remove</button>
{#each { length } as _}
<div
transition:fade
on:introstart={() => console.log('intro started')}
on:outrostart={() => console.log('outro started')}
on:introend={() => console.log('intro ended')}
on:outroend={() => console.log('outro ended')}
>
Hello World
</div>
{/each}
然後他也有提供事件變數,所以如果你需要也可以寫成 on:introstart={(e) => console.log('intro started', e.target)}
。
local
屬性
有時候可能不希望父層的關係觸發子元素的 transition
,就需要使用 local
這個修飾子。
<script lang="ts">
import { slide } from 'svelte/transition';
let length = 0;
let show = true;
</script>
<button on:click={() => (show = !show)}>Toggle</button>
<button on:click={() => length++}>Add</button>
<button on:click={() => length--}>Remove</button>
{#if show}
{#each { length } as _}
<div transition:slide>Hello World</div>
{/each}
{/if}
如果單純這樣寫,外面的 show
被切換的時候裡面的那堆 div
還會有 transition
的作用。
如果不希望這樣,只要加上 local
這個修飾子就可以。
<div transition:slide|local>Hello World</div>
Q
: 你可能會想說為什麼要這麼做?
A
: 因為父層可能有自己想要的 transition
,如下範例:
<script lang="ts">
import { fade, slide } from 'svelte/transition';
// 略
</script>
<!-- 略 -->
{#if show}
<div transition:fade>
{#each { length } as _}
<div transition:slide|local>Hello World</div>
{/each}
</div>
{/if}
key
區域
在講迴圈的時候有提到帶 key
,當 key
不同,就會重新渲染,這邊可以利用 key
區塊的技巧,讓 transition
重新播放。
<script lang="ts">
import { fly } from 'svelte/transition';
let count = 0;
</script>
<button on:click={() => count++}>Increment</button>
<div>
Count:
{#key count}
<div in:fly={{ y: -20 }}>{count}</div>
{/key}
</div>
也可以設定 out
,只是會很奇怪,如果有想到用途也可以加進去用。
Deferred transitions
到了 transition
當中最複雜的一個,這個的概念有點像用 key
去分組做對稱的 transition
。
使用 crossfade
這個函數,會得到兩個函數,分別是 send
、receive
。
send
會去找對應的 receive
,反之亦然。
簡單的例子:
<script lang="ts">
import { crossfade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
const [send, receive] = crossfade({
duration: 1000,
easing: quintOut
});
let show = true;
</script>
<button on:click={() => (show = !show)}>Toggle</button>
<div>
{#if show}
<h1 in:send={{ key: 1 }} out:receive={{ key: 1 }}>A</h1>
{:else}
<h1 in:send={{ key: 1 }} out:receive={{ key: 1 }}>B</h1>
{/if}
</div>
<style>
div {
position: relative;
}
h1 {
position: absolute;
top: 0;
}
</style>
一個對應可能沒什麼感覺,假設有兩個對應,如下:
<div>
{#if show}
<h1 in:send={{ key: 1 }} out:receive={{ key: 1 }}>A</h1>
{:else}
<h1 in:send={{ key: 2 }} out:receive={{ key: 2 }}>B</h1>
{/if}
{#if show}
<h1 style="top: 60px" in:send={{ key: 2 }} out:receive={{ key: 2 }}>A2</h1>
{:else}
<h1 style="top: 60px" in:send={{ key: 1 }} out:receive={{ key: 1 }}>B2</h1>
{/if}
</div>
這時去戳按鈕,就會發現是上下對調。
而 crossfade
還有一個 option
是 fallback
函數,這個函數當他沒有找到對應要做 transition
的元素就會觸發。
<script lang="ts">
import { crossfade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
const [send, receive] = crossfade({
duration: 1000,
easing: quintOut,
fallback(node, params, intro) {
return {
duration: 600,
easing: quintOut,
css: (t) => `opacity: ${t}; transform: translateY(${-20 + t * 20}px);`
};
}
});
let show = true;
</script>
<button on:click={() => (show = !show)}>Toggle</button>
<div>
{#if show}
<h1 in:send={{ key: 1 }} out:receive={{ key: 1 }}>A</h1>
{:else}
<h1 in:send={{ key: 2 }} out:receive={{ key: 2 }}>B</h1>
{/if}
{#if show}
<h1 style="top: 60px" in:send={{ key: 2 }} out:receive={{ key: 2 }}>A2</h1>
{/if}
</div>
<style>
div {
position: relative;
}
h1 {
position: absolute;
top: 0;
}
</style>
上面的範例刻意讓 A
沒有對應的元素,那麼他就會觸發 fallback
函數,去做不同的 transition
。
而 fallback
有傳入三個參數可以使用,分別是:
node
: 與上方定義相同,就是綁在誰身上
params
: 同上,例如上例帶入的 key: 1
intro
: 當前執行的 fallback node
是不是 intro
,這邊可以自己把他 console
出來觀察變化(我是覺得應該會滿少用到的?)
主要比較常用到的可能是新增或刪除的操作,剛好會導致另一方找不到對應的 key
,而觸發 fallback
行為。
Animation
終於到了最後一個小結,這邊 Svelte
在 Animation
提供一個 flip
的指令來使用,主要是來做 transition
之間的補間。
FLIP
的全寫是: First
, Last
, Invert
, Play
,細節可以看這裡,但主要就是說
First
: 紀錄動畫元素的初始狀態Last
: 讓元素移動到最終狀態並記錄狀態Invert
: 以前從上面知道初始以及最終狀態,接著可以用transform
相關的屬性讓元素移動回初始狀態Play
: 開始跑動畫,從初始狀態跑到最終狀態
我知道文字很難懂,不過核心概念就是因為用 js
去做動畫是很昂貴的事情(可能會因為卡住主線程而掉幀之類),所以這邊的概念是用 js
去算,然後套入 css
去實際跑動畫。
而這邊 Svelte
提供的 animate:flip
主要是幫你補間迭代元素可能被刪除,等待被刪除元素的 out
,結束後下面元素遞補上來的過程。
目前他也只能被放在 each
區域(有帶 key
的情形)才能使用。
簡單的範例
<script lang="ts">
import { fade } from 'svelte/transition';
let stack: number[] = [];
</script>
<h1>Stack</h1>
<div>
<button on:click={() => (stack = [...stack, Math.random()])}>Push</button>
<button on:click={() => (stack = stack.slice(1))}>Pop</button>
</div>
{#each stack as item (item)}
<div transition:fade>{item}</div>
{/each}
先去戳看看 Pop
,會發現下面遞補上來的元素是瞬間上來,再來可以加上 animate:flip
指令:
<script lang="ts">
import { flip } from 'svelte/animate';
// 略...
</script>
<!-- 略... -->
{#each stack as item (item)}
<div transition:fade animate:flip>{item}</div>
{/each}
注意:那個 #each
區塊的 key
是必須有的,否則會編譯錯誤。
這時在去戳戳看 Pop
,會發現變得很流暢。
而 flip
一樣有幾個 options
可以使用:
delay
duration
easing
這幾個都跟上面描述到的一樣用途,所以就不再贅述了。
總結
這一篇一口氣把 Svelte
動畫相關的東西補齊,內容其實滿多的,自己也整理了滿多時間(因為都想不太到很酷炫的範例,只好以簡單為主,未來回來看可以從這些再做延伸)
總之盡可能用 css
去完成動畫,除非真的有必要再去透過 js
處理,效能會比較好。