Skip to content

多邊形轉場

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 這個神奇酷炫的東西,完美解決此問題!(๑•̀ㅂ•́)و✧

📚 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;
}

v0.21.2