Skip to content

逐字轉場

讓每個文字都有進入進出動畫。( •̀ ω •́ )✧

使用範例

基本用法

預設就是經典的淡入淡出。( •̀ ω •́ )✧

🐟

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <base-checkbox
      v-model="visible"
      label="顯示"
      class="border rounded p-4"
    />

    <div class="flex flex-col gap-2">
      <text-characters-transition
        :visible="visible"
        label="我是鱈魚🐟"
        class="text-2xl tracking-wider"
      />

      <text-characters-transition
        :visible="visible"
        label="一段很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長的文字"
        :enter="(i) => ({
          delay: i * 5,
        })"
        :leave="(i) => ({
          delay: i * 5,
        })"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import TextCharactersTransition from '../text-characters-transition.vue'

const visible = ref(false)
</script>

切分文字

可以自行設定文字分割邏輯或是提供分好的文字。

一隻熱愛程式的魚但是沒有手指可以打鍵盤更買不到能在水裡用的電腦。

鱈魚是一種很油很油肥魚

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <base-checkbox
      v-model="visible"
      label="顯示"
      class="border rounded p-4"
    />

    <div class="flex flex-col gap-2">
      <text-characters-transition
        :visible="visible"
        label="一隻熱愛程式的魚,但是沒有手指可以打鍵盤,更買不到能在水裡用的電腦。"
        :splitter="/(,)/"
      />

      <text-characters-transition
        :visible="visible"
        :label="[
          '鱈魚', '是一種', '很油', '很油', '的', '肥魚',
        ]"
        :enter="(i) => ({
          delay: i * 200,
        })"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import TextCharactersTransition from '../text-characters-transition.vue'

const visible = ref(false)
</script>

轉場類型

元件內建了一些簡單的效果,來試試吧。◝( •ω• )◟

(點擊以下任一方塊,開始切換)

clip-right

random-spin

landing

flicker

converge

whirling

gather

emerge
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="flex flex-col items-center gap-2 text-3xl font-bold tracking-wider">
      <div
        v-for="(item, i) in list"
        :key="i"
        class="clickable-box relative border px-10 py-4"
        :class="{ 'border-x-4': item.visible }"
        @click="toggleVisible(item)"
      >
        <text-characters-transition
          label="一段展示用的文字"
          v-bind="item"
          class="pointer-events-none"
        />

        <div class="absolute bottom-0 left-0 p-2 px-3 text-sm font-normal tracking-normal opacity-20">
          {{ item.name }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import { addProp, map } from 'remeda'
import { ref } from 'vue'
import TextCharactersTransition from '../text-characters-transition.vue'

type Param = Writable<ExtractComponentProps<typeof TextCharactersTransition>>
type Item = Pick<Param, 'name' | 'visible'>

const list = ref(
  map(
    [
      { name: 'clip-right' },
      { name: 'random-spin' },
      { name: 'landing' },
      { name: 'flicker' },
      { name: 'converge' },
      { name: 'whirling' },
      { name: 'gather' },
      { name: 'emerge' },
    ] satisfies Item[],
    addProp('visible', false),
  ),
)

function toggleVisible(item: Pick<Param, 'visible'>) {
  item.visible = !item.visible
}
</script>

<style scoped lang="sass">
.clickable-box
  cursor: pointer
  transition-duration: 0.4s
  &:active
    transition-duration: 0.1s
    scale: 0.98
</style>

自定義轉場

參數皆可自定義,寫法詳見 anime.js 文檔

來打造各種獨特的轉場效果!(≖‿ゝ≖)✧

(點擊以下任一方塊,開始切換)

程式一個能跑就行

聽說鱈魚體重沒有破百

鱈魚:「那個沒有也太晚出現了吧!Σ(ˊДˋ;)」

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="flex flex-col flex-1 items-center justify-around gap-6 text-xl">
      <div
        v-for="(item, i) in list"
        :key="i"
        class="clickable-box border px-9 py-6"
        :class="{ 'border-x-4': item.visible }"
        @click="toggleVisible(item)"
      >
        <text-characters-transition v-bind="item" />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import anime from 'animejs'
import { sample } from 'remeda'
import { ref } from 'vue'
import TextCharactersTransition from '../text-characters-transition.vue'

type Param = Writable<ExtractComponentProps<typeof TextCharactersTransition>>
type Item = Pick<Param, 'label' | 'enter' | 'leave' | 'visible'> & {
  class?: string;
}

const negativeList = [1, -1] as const
const randomNegative = () => sample(negativeList, 1)[0]

const list = ref<Item[]>([
  {
    visible: false,
    label: '程式如詩寫,美到無人解',
    class: 'font-wenkai tracking-[0.2rem]',
    enter: (i) => ({
      opacity: [0, 1],
      filter: ['blur(20px)', 'blur(0px)'],
      translateX: () => [
        anime.random(40, 60) * randomNegative(),
        0,
      ],
      translateY: () => [
        anime.random(50, 60) * randomNegative(),
        0,
      ],
      delay: i * 100,
      duration: 1600,
      easing: 'easeOutCirc',
    }),
    leave: (i) => ({
      opacity: 0,
      filter: 'blur(20px)',
      delay: i * 50,
      duration: 900,
      easing: 'easeInCirc',
    }),
  },
  {
    visible: false,
    class: 'tracking-[0.2rem] perspective',
    label: ['人', '和', '程式', ',', '一個', '能跑', '就行'],
    enter: (i) => ({
      opacity: [0, 1],
      rotateX: [anime.random(180, 90), 0],
      rotateY: [270, 0],
      rotateZ: [anime.random(-90, 90), 0],
      scaleX: [0.5, 1],
      easing: 'easeOutCirc',
      duration: 1000,
      delay: i * 300,
    }),
    leave: (i) => ({
      opacity: 0,
      rotateX: anime.random(-180, -90),
      rotateY: anime.random(-90, 90),
      rotateZ: anime.random(-90, 90),
      scaleX: 0.5,
      easing: 'easeInExpo',
      duration: 1400,
      delay: i * 100,
    }),
  },
  {
    visible: false,
    class: 'flex flex-nowrap justify-center items-center font-bold tracking-wider w-[18rem] h-[2.8rem]',
    label: [
      {
        value: '聽說鱈魚體重',
        enter: () => ({
          opacity: [0, 1],
        }),
        leave: () => ({
          opacity: 0,
          delay: 500,
        }),
      },
      {
        value: '沒有',
        enter: () => ({
          fontSize: [
            '0rem',
            '1.25rem',
          ],
          delay: 2000,
        }),
        leave: () => ({
          opacity: 0,
          delay: 500,
        }),
      },
      {
        value: '破百',
        enter: () => ({
          opacity: [0, 1],
          fontSize: [
            { value: '3rem' },
            { value: '1.25rem' },
          ],
          color: [
            { value: '#f00' },
            { value: '#000' },
          ],
          rotate: [
            { value: '10deg' },
            { value: '0deg' },
          ],
          delay: 500,
        }),
        leave: () => ({
          opacity: [
            { value: 1 },
            { value: 0 },
          ],
          fontSize: [
            { value: '3rem' },
            { value: '1.25rem' },
          ],
          color: [
            { value: '#f0f' },
            { value: '#000' },
          ],
          rotate: [
            { value: '-10deg' },
            { value: '0deg' },
          ],
        }),
      },
    ],
  },
])

function toggleVisible(item: Pick<Param, 'visible'>) {
  item.visible = !item.visible
}
</script>

<style scoped lang="sass">
@import url('https://fonts.googleapis.com/css2?family=LXGW+WenKai+Mono+TC&family=Noto+Sans:wght@100;200;300;400;500;600;700;800;900&display=swap')

.font-wenkai
  font-family: "LXGW WenKai Mono TC", monospace
  font-weight: 400
  font-style: normal
  text-shadow: 0 0 10px rgba(#111, 0.1)

.perspective
  perspective: 100px
  transform-style: preserve-3d

.clickable-box
  cursor: pointer
  transition-duration: 0.4s
  &:active
    transition-duration: 0.1s
    scale: 0.98
</style>

原理

切分文字後,將每個區塊建立唯一 id,利用 anime.js 實現動畫效果

原始碼

API

Props

interface Props {
  visible?: boolean;
  label: string | string[] | Array<{
    value: string;
    enter: AnimeFuncParam;
    leave: AnimeFuncParam;
  }>;

  /** html tag
   *
   * @default 'p'
   */
  tag?: string;

  /** 如何切割文字
   *
   * 只有在 label 為 string 時有效
   *
   * @default /.*?/u
   */
  splitter?: RegExp | ((label: string) => string[]);

  /** 過場名稱。使用預設內容 */
  name?: `${TransitionName}`;
  /** 進入動畫設定 */
  enter?: AnimeFuncParam;
  /** 離開動畫設定 */
  leave?: AnimeFuncParam;
}

Emits

const emit = defineEmits<{
  'before-enter': [];
  'after-enter': [];
  'before-leave': [];
  'after-leave': [];
}>()

v0.23.1