多邊形轉場
Motion Graphic 風格的 Transition 元件。
甚麼是 Motion Graphic 轉場效果?可以看看這個影片:
Motion Graphic 轉場在影片製作其實很常見,不過網頁中就沒那麼常見了。
所以我就嘗試把效果搬上網頁了!ˋ( ° ▽、° )
雖然目前稍微有一點點瑕疵,不過基本功能與效果都已經實現,剩下的改天再來慢慢改進吧。(。・∀・)ノ
使用範例
基本用法
基於 Vue 內建的 Transition 元件,不過目前連續切換會壞掉,請先手下留情。 (〃` 3′〃)
即使新舊元素尺寸不同也可以用呦!ˋ( ° ▽、° )
鱈魚
🐟
一隻熱愛程式的魚,但是沒有手指可以打鍵盤,更買不到能在水裡用的電腦。('◉◞⊖◟◉` )
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col items-start gap-4 border border-gray-300 p-6">
<base-btn
class="w-full"
label="更換"
@click="change()"
/>
<div class="w-full flex flex-col gap-4 overflow-hidden border rounded bg-slate-100 p-6">
<div class="flex justify-center">
<transition-shape
:type="imgTransition"
@after-transition="handleReady"
>
<img
:key="index"
:src="profile"
class="h-60 w-60 border-[0.5rem] border-white rounded-full object-cover shadow-md"
>
</transition-shape>
</div>
<div class="flex flex-col items-center justify-center gap-2 text-2xl font-bold">
鱈魚
<transition-shape
:type="fishTransition"
@after-transition="handleReady"
>
<div
:key="index"
class="p-2 px-4 text-3xl"
>
{{ fish }}
</div>
</transition-shape>
</div>
<transition-shape
appear
:type="textTransition"
@after-transition="handleReady"
>
<div
:key="index"
class="p-8"
>
{{ introduction }}
</div>
</transition-shape>
</div>
</div>
</template>
<script setup lang="ts">
import type { TransitionType } from '../type'
import { debounce } from 'lodash-es'
import { hasAtLeast, piped, reverse } from 'remeda'
import { computed, ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import TransitionShape from '../transition-shape.vue'
const index = ref(0)
const fishList = [
'🐟',
'🐋🐋',
'🐠',
'🐡🐡',
]
const fish = computed(() => fishList[index.value % fishList.length])
const profileList = [
'/profile.webp',
'/profile-2.webp',
'/profile-3.webp',
]
const profile = computed(() => profileList[index.value % profileList.length])
const introductionList = [
`一隻熱愛程式的魚,但是沒有手指可以打鍵盤,更買不到能在水裡用的電腦。('◉◞⊖◟◉\` )`,
'最擅長的球類是地瓜球,一打十輕輕鬆鬆。( •̀ ω •́ )✧',
`不知道是不是在水裡躺平躺久了,最近喝水也會胖。\n_(:3」ㄥ)_`,
]
const introduction = computed(() => introductionList[index.value % introductionList.length])
const isReady = ref(false)
const handleReady = debounce(() => {
isReady.value = true
}, 500)
function change() {
if (!isReady.value)
return
isReady.value = false
index.value++
}
const colors: [string, ...string[]] = ['#012030', '#13678A', '#45C4B0', '#9AEBA3', '#DAFDBA']
const reverseColors = piped(
reverse<string[]>(),
(result) => {
if (!hasAtLeast(result, 1)) {
throw new Error('At least one color is required')
}
return result
},
)
const baseOption = {
duration: 800,
delay: 100,
}
const imgTransition: TransitionType = {
name: 'round',
enter: {
action: 'spread-scale',
easing: 'easeOutQuart',
...baseOption,
},
leave: {
action: 'scale',
easing: 'easeInQuart',
...baseOption,
},
colors,
}
const fishTransition: TransitionType = {
name: 'rect',
enter: {
action: 'slide-down',
easing: 'easeOutQuart',
...baseOption,
},
leave: {
action: 'slide-right',
easing: 'easeInQuart',
...baseOption,
},
colors: reverseColors(colors),
}
const textTransition: TransitionType = {
name: 'fence',
enter: {
action: 'spread-left',
easing: 'easeOutQuart',
...baseOption,
},
leave: {
action: 'scale-x',
easing: 'easeInQuart',
...baseOption,
},
colors,
}
</script>
轉場參數
可以微調各類轉場參數。
顏色
進入 action
easing
delay (ms)
duration (ms)
離開 action
easing
delay (ms)
duration (ms)
🐟
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col items-start gap-4 border border-gray-300 p-6">
<div class="w-full flex flex-col gap-2">
<div class="w-full flex items-center gap-3 border p-1">
<div class="flex-1 text-right">
顏色
</div>
<div class="flex flex-1 gap-1 p-2">
<input
v-model="colors[0]"
type="color"
class="flex-1"
>
<input
v-model="colors[1]"
type="color"
class="flex-1"
>
<input
v-model="colors[2]"
type="color"
class="flex-1"
>
</div>
</div>
<!-- 進入 -->
<div class="w-full flex flex-col items-center gap-1 border p-1">
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
進入 action
</div>
<select
v-model="rectAction.enter.action"
class="flex-1 rounded bg-slate-200 p-2"
>
<option
v-for="option in rectActionOptions"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
</div>
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
easing
</div>
<select
v-model="rectAction.enter.easing"
class="flex-1 rounded bg-slate-200 p-2"
>
<option
v-for="option in easingOptions"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
</div>
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
delay (ms)
</div>
<input
v-model.number="rectAction.enter.delay"
type="number"
class="min-w-0 flex-1 rounded bg-slate-200 p-2"
>
</div>
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
duration (ms)
</div>
<input
v-model.number="rectAction.enter.duration"
type="number"
class="min-w-0 flex-1 rounded bg-slate-200 p-2"
>
</div>
</div>
<!-- 離開 -->
<div class="w-full flex flex-col items-center gap-1 border p-1">
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
離開 action
</div>
<select
v-model="rectAction.leave.action"
class="flex-1 rounded bg-slate-200 p-2"
>
<option
v-for="option in rectActionOptions"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
</div>
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
easing
</div>
<select
v-model="rectAction.leave.easing"
class="flex-1 rounded bg-slate-200 p-2"
>
<option
v-for="option in easingOptions"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
</div>
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
delay (ms)
</div>
<input
v-model.number="rectAction.leave.delay"
type="number"
class="min-w-0 flex-1 rounded bg-slate-200 p-2"
>
</div>
<div class="w-full flex items-center gap-3 p-1">
<div class="flex-1 text-right">
duration (ms)
</div>
<input
v-model.number="rectAction.leave.duration"
type="number"
class="min-w-0 flex-1 rounded bg-slate-200 p-2"
>
</div>
</div>
<base-btn
class="w-full"
label="換魚"
@click="changeFish()"
/>
</div>
<transition-shape :type="transitionType">
<div
:key="fishIndex"
class="w-full text-center text-[10rem]"
>
{{ fishList[fishIndex] }}
</div>
</transition-shape>
</div>
</template>
<script setup lang="ts">
import type anime from 'animejs'
import type { TransitionType } from '../type'
import { computed, ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import TransitionShape from '../transition-shape.vue'
import { RectAction } from '../type'
const fishIndex = ref(0)
const fishList = [
'🐟',
'🐋',
'🐠',
'🐡',
]
function changeFish() {
fishIndex.value++
fishIndex.value %= fishList.length
}
const colors = ref<
[string, string, string]
>(['#7DC8FF', '#677580', '#374855'])
const rectAction = ref<
Record<'enter' | 'leave', {
action: `${RectAction}`;
easing: anime.EasingOptions;
delay: number;
duration: number;
}>
>({
enter: {
action: 'slide-right',
easing: 'easeOutQuart',
delay: 100,
duration: 800,
},
leave: {
action: 'slide-right',
easing: 'easeInQuart',
delay: 100,
duration: 800,
},
})
const rectActionOptions = Object.values(RectAction)
const timingList = ['In', 'Out', 'InOut']
const typeList = [
'Quad',
'Cubic',
'Quart',
'Quint',
'Sine',
'Expo',
'Circ',
'Back',
'Elastic',
'Bounce',
]
const easingOptions = [
'linear',
...typeList.flatMap((type) =>
timingList.map((timing) => `ease${timing}${type}`),
),
]
const transitionType = computed<TransitionType>(() => ({
name: 'rect',
enter: rectAction.value.enter,
leave: rectAction.value.leave,
colors: colors.value,
}))
</script>
Round 轉場
由圓形組成。
點擊任意魚,開始轉場。(≧∇≦)ノ
點擊展開
查看範例原始碼
vue
<template>
<div class="w-full flex flex-wrap justify-center gap-4 border border-gray-300 p-6">
<div
v-for="item in list"
:key="item.key"
class="w-full"
>
<transition-shape
:type="item"
@after-transition="handleInit()"
>
<div
:key="fishIndex"
class="w-full cursor-pointer py-6 text-center text-[5rem]"
@click="handleClick()"
>
{{ fishList[fishIndex] }}
</div>
</transition-shape>
</div>
</div>
</template>
<script setup lang="ts">
import type { TransitionType } from '../type'
import { useToggle } from '@vueuse/core'
import { debounce, throttle } from 'lodash-es'
import { hasAtLeast, map, pipe, shuffle } from 'remeda'
import { ref } from 'vue'
import TransitionShape from '../transition-shape.vue'
import { RoundBaseAction, RoundEnterAction } from '../type'
const fishIndex = ref(0)
const fishList = [
'🐟',
'🐋',
'🐠',
'🐡',
]
function changeFish() {
fishIndex.value++
fishIndex.value %= fishList.length
}
const [isReady, toggleReady] = useToggle(false)
const handleInit = debounce(() => {
toggleReady(true)
}, 1000)
const handleClick = throttle(() => {
if (!isReady.value) {
handleClick.cancel()
return
}
changeFish()
}, 4000, {
leading: true,
trailing: false,
})
const leaveActions = Object.values(RoundBaseAction)
const enterActions = [
RoundEnterAction.SPREAD_LEFT,
RoundEnterAction.SPREAD_SCALE,
RoundBaseAction.SCALE,
RoundBaseAction.SCALE_LB,
]
type Item = TransitionType & {
key: string;
}
const list: Item[] = pipe(
enterActions,
map((action, i) => {
const leaveAction = leaveActions[i % leaveActions.length]
if (!leaveAction) {
throw new Error('Leave action is required')
}
const colors = shuffle(['#27A4F2', '#44C1F2', '#85DEF2', '#DCEEF2', '#91E9F2'])
if (!hasAtLeast(colors, 1)) {
throw new Error('At least one color is required')
}
const result: Item = {
key: action,
name: 'round',
enter: {
action,
duration: 900,
delay: 200,
easing: 'easeOutExpo',
},
leave: {
action: leaveAction,
duration: 900,
delay: 200,
easing: 'easeInExpo',
},
colors,
}
return result
}),
)
</script>
Fence 轉場
像柵欄一般,由多個矩形組成。
點擊任意魚,開始轉場。(≧∇≦)ノ
點擊展開
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col justify-center gap-4 border border-gray-300 p-6">
<div
v-for="item in list"
:key="item.key"
class="w-full"
>
<transition-shape
:type="item"
@after-transition="handleInit()"
>
<div
:key="fishIndex"
class="w-full cursor-pointer py-6 text-center text-[6rem]"
@click="handleClick()"
>
{{ fishList[fishIndex] }}
</div>
</transition-shape>
</div>
</div>
</template>
<script setup lang="ts">
import type { TransitionType } from '../type'
import { useToggle } from '@vueuse/core'
import { debounce, shuffle, throttle } from 'lodash-es'
import { hasAtLeast, map, pipe, reverse } from 'remeda'
import { ref } from 'vue'
import TransitionShape from '../transition-shape.vue'
import { FenceAction } from '../type'
const fishIndex = ref(0)
const fishList = [
'🐟',
'🐋',
'🐠',
'🐡',
]
function changeFish() {
fishIndex.value++
fishIndex.value %= fishList.length
}
const [isReady, toggleReady] = useToggle(false)
const handleInit = debounce(() => {
toggleReady(true)
}, 1000)
const handleClick = throttle(() => {
if (!isReady.value) {
handleClick.cancel()
return
}
changeFish()
}, 3000, {
leading: true,
trailing: false,
})
const actions = Object.values(FenceAction)
// const actions = [FenceAction.SPREAD_RIGHT];
const reverseActions = pipe(actions, reverse())
type Item = TransitionType & {
key: string;
}
const list: Item[] = pipe(
actions,
map((action, i) => {
const targetAction = reverseActions[i] ?? action
const colors = shuffle(['#27A4F2', '#44C1F2', '#85DEF2', '#DCEEF2', '#91E9F2'])
if (!hasAtLeast(colors, 1)) {
throw new Error('At least one color is required')
}
const result: Item = {
key: action,
name: 'fence',
enter: {
action,
duration: 800,
delay: 100,
easing: 'easeOutQuart',
},
leave: {
action: targetAction,
duration: 800,
delay: 100,
easing: 'easeInQuart',
},
colors,
}
return result
}),
)
</script>
Converging Rect 轉場
多個矩形向中心匯聚,可以設定傾斜角度。
點擊任意魚,開始轉場。(≧∇≦)ノ
點擊展開
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col justify-center gap-4 border border-gray-300 p-6">
<div
v-for="item in list"
:key="item.key"
class="w-full"
>
<transition-shape
:type="item"
@after-transition="handleInit()"
>
<div
:key="fishIndex"
class="w-full cursor-pointer py-5 text-center text-[5rem]"
@click="handleClick()"
>
{{ fishList[fishIndex] }}
</div>
</transition-shape>
</div>
</div>
</template>
<script setup lang="ts">
import type { TransitionType } from '../type'
import { useToggle } from '@vueuse/core'
import { debounce, throttle } from 'lodash-es'
import { map, pipe } from 'remeda'
import { ref } from 'vue'
import TransitionShape from '../transition-shape.vue'
import { ConvergingRectAction } from '../type'
const fishIndex = ref(0)
const fishList = [
'🐟',
'🐋',
'🐠',
'🐡',
]
function changeFish() {
fishIndex.value++
fishIndex.value %= fishList.length
}
const [isReady, toggleReady] = useToggle(false)
const handleInit = debounce(() => {
toggleReady(true)
}, 1000)
const handleClick = throttle(() => {
if (!isReady.value) {
handleClick.cancel()
return
}
changeFish()
}, 3000, {
leading: true,
trailing: false,
})
type Item = TransitionType & {
key: string;
}
const list: Item[] = pipe(
[0, 10, -30],
map((angle) => {
const action = ConvergingRectAction.SLIDE
const result: Item = {
key: action,
name: 'converging-rect',
enter: {
action,
angle,
duration: 800,
delay: 100,
easing: 'easeOutExpo',
},
leave: {
action,
duration: 800,
delay: 100,
easing: 'easeInExpo',
},
colors: [
'#27A4F2',
'#44C1F2',
'#85DEF2',
'#DCEEF2',
'#91E9F2',
],
}
return result
}),
)
</script>
原理
基於 Vue 內建的 Transition 元件,使用 babylon.js 產生形狀遮罩,接著透過 Transition Event 控制動畫的開始與結束。
可以處理以下情境:
- v-if 條件渲染
- 變更元素 key
暫時不考慮 v-show 的情境。
定位問題
原本使用 fixed 配合頁面滾動事件來定位 canvas 位置,但是在快速滾動畫面時 sharp 還是會稍微偏移。
苦惱之際發現了 CSS Anchor Positioning API 這個神奇酷炫的東西,完美解決此問題!(๑•̀ㅂ•́)و✧
原始碼
API
Props
<template>
<transition
appear
:css="false"
@before-enter="handleBeforeEnter"
@enter="handleEnter"
@after-enter="handleAfterEnter"
@before-leave="handleBeforeLeave"
@leave="handleLeave"
@after-leave="handleAfterLeave"
>
<slot />
</transition>
<shape-mask
ref="maskRef"
class="shape-mask fixed"
:style="maskStyle"
:type="props.type"
:width="currentBounding?.width.value"
:height="currentBounding?.height.value"
@init="() => emit('init')"
@before-transition="() => emit('before-transition')"
@after-transition="() => emit('after-transition')"
/>
</template>
<script lang="ts">
</script>
<script setup lang="ts">
import type { CSSProperties, TransitionProps } from 'vue'
import type { TransitionType } from './type'
import { promiseTimeout, useElementBounding } from '@vueuse/core'
import { nanoid } from 'nanoid'
import { find, pipe } from 'remeda'
import {
computed,
nextTick,
ref,
} from 'vue'
import ShapeMask from './shape-mask.vue'
// #endregion Props
const props = withDefaults(defineProps<Props>(), {
appear: false,
type: () => ({
name: 'rect',
enter: {
action: 'slide-right',
delay: 100,
duration: 1000,
easing: 'easeOutExpo',
},
leave: {
action: 'slide-right',
delay: 100,
duration: 1000,
easing: 'easeInExpo',
},
colors: ['#A5A9AD', '#84888B', '#5D6164'],
}),
})
// #region Emits
const emit = defineEmits<{
(e: 'init'): void;
(e: 'before-transition'): void;
(e: 'after-transition'): void;
}>()
// #endregion Emits
// #region Slots
const slots = defineSlots<{
default?: () => unknown;
}>()
// #region Props
interface Props {
appear?: boolean;
type?: TransitionType;
}
// #endregion Slots
/** 當新舊元素尺寸不同時,會導致 mask 尺寸變化。
*
* 為了防止視覺跳動,使用 CSS transition 過渡,所以 canvas 動畫也要有對應延遲。
*/
const SIZE_CHANGE_DELAY_SEC = 0.6
const maskCssTransitionValue = computed(() => {
return [
`width ${SIZE_CHANGE_DELAY_SEC}s cubic-bezier(0.5, 0, 0, 1.2)`,
`height ${SIZE_CHANGE_DELAY_SEC}s cubic-bezier(0.5, 0, 0, 1.2)`,
].join(', ')
})
/** 如果 appear 為 false,則需快速結束第一次動畫 */
let isFirst = true
const enterElRef = ref<HTMLElement>()
const enterElBounding = useElementBounding(enterElRef)
const leaveElRef = ref<HTMLElement>()
const leaveElBounding = useElementBounding(leaveElRef)
const currentBounding = computed(() => pipe(
[enterElBounding, leaveElBounding],
find(({ width }) => width.value > 0),
))
const maskRef = ref<InstanceType<typeof ShapeMask>>()
const maskVisible = computed(() => !!enterElRef.value || !!leaveElRef.value)
const maskStyle = computed<CSSProperties>(() => pipe(
currentBounding.value,
(bounding) => ({
top: `${bounding?.top.value}px`,
left: `${bounding?.left.value}px`,
width: `${bounding?.width.value}px`,
height: `${bounding?.height.value}px`,
// opacity: maskVisible.value ? 1 : 0,
}),
))
function isSizeChanged(aBounding?: DOMRect, bBounding?: DOMRect) {
if (!aBounding || !bBounding) {
return false
}
return aBounding.width !== bBounding.width
|| aBounding.height !== bBounding.height
}
// 進入事件
const handleBeforeEnter: TransitionProps['onBeforeEnter'] = (el) => {
if (!(el instanceof HTMLElement))
return
el.style.opacity = '0'
el.classList.add('anchor')
enterElRef.value = el
}
const handleEnter: TransitionProps['onEnter'] = async (el, done) => {
// nextTick 才能同時取得 enterElRef 和 leaveElRef
await nextTick()
// console.log(`🚀 ~ handleEnter: `);
if (!(el instanceof HTMLElement)) {
return done()
}
const enterElBounding = el.getBoundingClientRect()
const leaveElBounding = leaveElRef.value?.getBoundingClientRect()
// 初始化 mask
await maskRef.value?.init(enterElBounding)
if (isFirst && !props.appear) {
isFirst = false
el.style.opacity = '1'
emit('after-transition')
return done()
}
// 如果有 leaveElRef,表示為切換動畫
if (leaveElRef.value) {
// 將 enterEl 先脫離佔位
el.style.display = 'none'
}
await maskRef.value?.enter(enterElBounding)
// 如果有 leaveElRef,表示為切換動畫
if (leaveElRef.value) {
el.style.display = ''
// 提早移除 leaveEl 以免影響定位
leaveElRef.value = undefined
if (isSizeChanged(leaveElBounding, enterElBounding)) {
// 等待 canvas 尺寸變化,同 .shape-mask 定義的 transition-duration
await promiseTimeout(SIZE_CHANGE_DELAY_SEC * 1000)
}
}
el.style.opacity = '1'
await maskRef.value?.leave(enterElBounding)
done()
}
const handleAfterEnter: TransitionProps['onAfterEnter'] = (el) => {
enterElRef.value = undefined
}
// 離開事件
const handleBeforeLeave: TransitionProps['onBeforeLeave'] = (el) => {
if (!(el instanceof HTMLElement))
return
el.classList.add('anchor')
leaveElRef.value = el
}
const handleLeave: TransitionProps['onLeave'] = async (el, done) => {
// nextTick 才能同時取得 enterElRef 和 leaveElRef
await nextTick()
// console.log(`🚀 ~ handleLeave: `);
if (!(el instanceof HTMLElement)) {
return done()
}
const enterElBounding = enterElRef.value?.getBoundingClientRect()
const leaveElBounding = el.getBoundingClientRect()
await maskRef.value?.enter(leaveElBounding)
el.style.opacity = '0'
// 如果有 enterElRef,表示為切換動畫
if (enterElRef.value) {
// 將 leaveEl 脫離佔位
el.style.display = 'none'
if (isSizeChanged(leaveElBounding, enterElBounding)) {
await promiseTimeout(SIZE_CHANGE_DELAY_SEC * 1000)
}
}
await maskRef.value?.leave(leaveElBounding)
done()
}
const handleAfterLeave: TransitionProps['onAfterLeave'] = (el) => {
leaveElRef.value = undefined
}
const anchorName = ref(`--${nanoid()}`)
</script>
<style lang="sass">
.shape-mask
transition: v-bind(maskCssTransitionValue) !important
position-anchor: v-bind(anchorName)
top: anchor(top) !important
left: anchor(left) !important
.anchor
anchor-name: v-bind(anchorName)
// @supports (anchor-name: test)
// .shape-mask
// position-anchor: v-bind(anchorName)
// top: anchor(top) !important
// left: anchor(left) !important
</style>
/** 初始化後 name 變更會被忽略,其餘參數皆可動態調整 */
export type TransitionType =
TransitionRect
| TransitionConvergingRect
| TransitionRound
| TransitionFence
TransitionRect
export enum RectAction {
SLIDE_RIGHT = 'slide-right',
SLIDE_LEFT = 'slide-left',
SLIDE_UP = 'slide-up',
SLIDE_DOWN = 'slide-down',
SCALE = 'scale',
SCALE_X = 'scale-x',
SCALE_Y = 'scale-y',
}
interface TransitionRect {
name: 'rect';
enter: {
action: `${RectAction}`;
duration: number;
/** 每個 shape 延遲間距 */
delay: number;
easing: EasingOptions;
};
leave: {
action: `${RectAction}`;
duration: number;
delay: number;
easing: EasingOptions;
};
/** HEX 格式。顏色數量等同 shape 數量,至少需要一個 */
colors: [string, ...string[]];
}
TransitionConvergingRect
export enum ConvergingRectAction {
SLIDE = 'slide',
}
interface TransitionConvergingRect {
name: 'converging-rect';
enter: {
action: `${ConvergingRectAction}`;
angle?: number;
duration: number;
/** 每個 shape 延遲間距 */
delay: number;
easing: EasingOptions;
};
leave: {
action: `${ConvergingRectAction}`;
duration: number;
delay: number;
easing: EasingOptions;
};
/** HEX 格式。顏色數量等同 shape 數量,至少需要一個 */
colors: [string, ...string[]];
}
TransitionRound
export enum RoundEnterAction {
SPREAD_LEFT = 'spread-left',
SPREAD_RIGHT = 'spread-right',
SPREAD_UP = 'spread-up',
SPREAD_DOWN = 'spread-down',
SPREAD_SCALE = 'spread-scale',
}
export enum RoundBaseAction {
SCALE_LT = 'scale-lt',
SCALE_LB = 'scale-lb',
SCALE_RT = 'scale-rt',
SCALE_RB = 'scale-rb',
SCALE = 'scale',
}
interface TransitionRound {
name: 'round';
enter: {
action: `${RoundBaseAction}` | `${RoundEnterAction}`;
duration: number;
/** 每個 shape 延遲間距 */
delay: number;
easing: EasingOptions;
};
leave: {
action: `${RoundBaseAction}`;
duration: number;
delay: number;
easing: EasingOptions;
};
/** HEX 格式。顏色數量等同 shape 數量,至少需要一個 */
colors: [string, ...string[]];
}
TransitionFence
export enum FenceAction {
SPREAD_RIGHT = 'spread-right',
SPREAD_LEFT = 'spread-left',
SCALE_X = 'scale-x',
SCALE_Y = 'scale-y',
}
interface TransitionFence {
name: 'fence';
enter: {
action: `${FenceAction}`;
duration: number;
/** 每個 shape 延遲間距 */
delay: number;
easing: EasingOptions;
};
leave: {
action: `${FenceAction}`;
duration: number;
delay: number;
easing: EasingOptions;
};
/** HEX 格式。顏色數量等同 shape 數量,至少需要一個 */
colors: [string, ...string[]];
}
Emits
const emit = defineEmits<{
(e: 'init'): void;
(e: 'before-transition'): void;
(e: 'after-transition'): void;
}>()
Slots
const slots = defineSlots<{
default?: () => unknown;
}>()
interface Props {
appear?: boolean;
type?: TransitionType;
}