逐字轉場
讓每個文字都有進入進出動畫。( •̀ ω •́ )✧
使用範例
基本用法
預設就是經典的淡入淡出。( •̀ ω •́ )✧
我是鱈魚🐟
一段很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長的文字
查看範例原始碼
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': [];
}>()