refactor(ui-transitions): better structure
Signed-off-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
@@ -1,46 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
// Define route configurations
|
||||
const routes = [
|
||||
{ number: 1, path: '/' },
|
||||
{ number: 2, path: '/transition-2' },
|
||||
{ number: 3, path: '/transition-3' },
|
||||
{ number: 4, path: '/transition-4' },
|
||||
{ number: 5, path: '/transition-5' },
|
||||
{ number: 6, path: '/transition-6' },
|
||||
{ number: 7, path: '/transition-7' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ row gap-2" h-full items-center justify-center>
|
||||
<RouterLink
|
||||
rounded-lg px-7 py-3 transition="all duration-250 ease-in-out" to="/"
|
||||
:class="[route.path === '/' ? 'bg-neutral-200 dark:bg-neutral-500' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
v-for="route in routes" :key="route.number" :to="route.path" rounded-lg px-7 py-3
|
||||
transition="all duration-250 ease-in-out"
|
||||
class="bg-neutral-100 [&.router-link-active]:bg-neutral-300 dark:bg-neutral-700 hover:bg-neutral-200 [&.router-link-active]:dark:bg-neutral-600 [&.router-link-active]:hover:bg-neutral-400 hover:dark:bg-neutral-600 [&.router-link-active]:hover:dark:bg-neutral-500"
|
||||
>
|
||||
1
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
rounded-lg px-7 py-3 transition="all duration-250 ease-in-out" to="/transition-2"
|
||||
:class="[route.path === '/transition-2' ? 'bg-neutral-200 dark:bg-neutral-500' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
>
|
||||
2
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
rounded-lg px-7 py-3 transition="all duration-250 ease-in-out" to="/transition-3"
|
||||
:class="[route.path === '/transition-3' ? 'bg-neutral-200 dark:bg-neutral-500' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
>
|
||||
3
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
rounded-lg px-7 py-3 transition="all duration-250 ease-in-out" to="/transition-4"
|
||||
:class="[route.path === '/transition-4' ? 'bg-neutral-200 dark:bg-neutral-500' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
>
|
||||
4
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
rounded-lg px-7 py-3 transition="all duration-250 ease-in-out" to="/transition-5"
|
||||
:class="[route.path === '/transition-5' ? 'bg-neutral-200 dark:bg-neutral-500' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
>
|
||||
5
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
rounded-lg px-7 py-3 transition="all duration-250 ease-in-out" to="/transition-6"
|
||||
:class="[route.path === '/transition-6' ? 'bg-neutral-200 dark:bg-neutral-500' : 'bg-neutral-100 dark:bg-neutral-700']"
|
||||
>
|
||||
6
|
||||
{{ route.number }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { definePage } from 'unplugin-vue-router/runtime'
|
||||
|
||||
import Buttons from '../components/Buttons.vue'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
stageTransition: 'bubble-wave-out',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Buttons />
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
colors?: string[]
|
||||
delay?: number
|
||||
duration?: number
|
||||
}>(), {
|
||||
colors: () => ['#eee', '#ebcb8b', '#c56370', '#3f3b52'],
|
||||
delay: 0,
|
||||
duration: 0.4,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.documentElement.style.setProperty('--circle-expansion-delay', `${props.delay}s`)
|
||||
document.documentElement.style.setProperty('--circle-expansion-duration', `${props.duration}s`)
|
||||
props.colors.forEach((color, index) => {
|
||||
document.documentElement.style.setProperty(`--circle-expansion-color-${index + 1}`, color)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="circle-expansion-transition">
|
||||
<div v-for="(_, index) in props.colors" :key="index" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.circle-expansion-transition {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.circle-expansion-transition div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.circle-expansion-transition div:nth-child(1) {
|
||||
background-color: var(--circle-expansion-color-1);
|
||||
animation: circleExpand var(--circle-expansion-duration) ease-in calc(var(--circle-expansion-delay) + 0s) forwards;
|
||||
}
|
||||
|
||||
.circle-expansion-transition div:nth-child(2) {
|
||||
background-color: var(--circle-expansion-color-2);
|
||||
animation: circleExpand var(--circle-expansion-duration) ease-in calc(var(--circle-expansion-delay) + 0.15s) forwards;
|
||||
}
|
||||
|
||||
.circle-expansion-transition div:nth-child(3) {
|
||||
background-color: var(--circle-expansion-color-3);
|
||||
animation: circleExpand var(--circle-expansion-duration) ease-in calc(var(--circle-expansion-delay) + 0.3s) forwards;
|
||||
}
|
||||
|
||||
.circle-expansion-transition div:nth-child(4) {
|
||||
background-color: var(--circle-expansion-color-4);
|
||||
animation: circleExpand var(--circle-expansion-duration) ease-in calc(var(--circle-expansion-delay) + 0.45s) forwards;
|
||||
}
|
||||
|
||||
@keyframes circleExpand {
|
||||
from {
|
||||
transform: scale(0);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,19 +5,29 @@ const props = withDefaults(defineProps<{
|
||||
primaryColor?: string
|
||||
duration?: number
|
||||
delay?: number
|
||||
borderRadius?: number
|
||||
borderRadius?: {
|
||||
sm?: string
|
||||
md?: string
|
||||
lg?: string
|
||||
}
|
||||
}>(), {
|
||||
primaryColor: '#eee',
|
||||
duration: 0.6,
|
||||
delay: 0,
|
||||
borderRadius: 50,
|
||||
borderRadius: () => ({
|
||||
sm: '14rem',
|
||||
md: '14rem',
|
||||
lg: '50%',
|
||||
}),
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.documentElement.style.setProperty('--fantasy-fall-color', props.primaryColor)
|
||||
document.documentElement.style.setProperty('--fantasy-fall-duration', `${props.duration}s`)
|
||||
document.documentElement.style.setProperty('--fantasy-fall-delay', `${props.delay}s`)
|
||||
document.documentElement.style.setProperty('--fantasy-fall-radius', `${props.borderRadius}%`)
|
||||
document.documentElement.style.setProperty('--fantasy-fall-radius-sm', `${props.borderRadius.sm}`)
|
||||
document.documentElement.style.setProperty('--fantasy-fall-radius-md', `${props.borderRadius.md}`)
|
||||
document.documentElement.style.setProperty('--fantasy-fall-radius-lg', `${props.borderRadius.lg}`)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -41,8 +51,8 @@ onMounted(() => {
|
||||
height: 100%;
|
||||
background-color: var(--fantasy-fall-color);
|
||||
transform: translateY(-100%);
|
||||
border-bottom-left-radius: var(--fantasy-fall-radius);
|
||||
border-bottom-right-radius: var(--fantasy-fall-radius);
|
||||
border-bottom-left-radius: var(--fantasy-fall-radius-sm);
|
||||
border-bottom-right-radius: var(--fantasy-fall-radius-sm);
|
||||
animation: fantasy-fall var(--fantasy-fall-duration) ease-out var(--fantasy-fall-delay) forwards;
|
||||
}
|
||||
}
|
||||
@@ -62,4 +72,22 @@ onMounted(() => {
|
||||
border-bottom-left-radius: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.fantasy-fall-transition {
|
||||
&::before {
|
||||
border-bottom-left-radius: var(--fantasy-fall-radius-md);
|
||||
border-bottom-right-radius: var(--fantasy-fall-radius-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.fantasy-fall-transition {
|
||||
&::before {
|
||||
border-bottom-left-radius: var(--fantasy-fall-radius-lg);
|
||||
border-bottom-right-radius: var(--fantasy-fall-radius-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, shallowRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ArrowTransition from './ArrowTransition.vue'
|
||||
import BubbleWaveOutTransition from './BubbleWaveOutTransition.vue'
|
||||
import FantasyFallTransition from './FantasyFallTransition.vue'
|
||||
import MultipleBlocksRevealTransition from './MultipleBlocksRevealTransition.vue'
|
||||
import RectanglesRotateTransition from './RectanglesRotateTransition.vue'
|
||||
@@ -24,15 +25,35 @@ type TransitionComponent =
|
||||
| typeof MultipleBlocksRevealTransition
|
||||
| typeof FantasyFallTransition
|
||||
| typeof RectanglesRotateTransition
|
||||
| typeof BubbleWaveOutTransition
|
||||
|
||||
const router = useRouter()
|
||||
const showTransition = ref(false)
|
||||
const activeTransitionName = ref('')
|
||||
const transitionStage = ref<TransitionStage | null>(null)
|
||||
|
||||
const transitions = shallowRef<Record<string, {
|
||||
// Define transition lifecycle events
|
||||
export type TransitionStage =
|
||||
| 'before-enter' // Just before animation starts
|
||||
| 'enter-active' // Animation has started
|
||||
| 'navigation' // Time to navigate (component still decides when)
|
||||
| 'after-enter' // Entry animation completed
|
||||
| 'before-leave' // Before exit animation starts
|
||||
| 'leave-active' // Exit animation has started
|
||||
| 'after-leave' // Complete animation cycle finished
|
||||
|
||||
// Define hook types
|
||||
type TransitionHook = (stage: TransitionStage, data: any) => void | Promise<void>
|
||||
type NavigationCallback = () => void
|
||||
|
||||
interface TransitionOptions {
|
||||
component: TransitionComponent
|
||||
duration: number
|
||||
}>>({
|
||||
exitDuration?: number
|
||||
nextDelay?: number
|
||||
}
|
||||
|
||||
const transitions = shallowRef<Record<string, TransitionOptions>>({
|
||||
'slide': {
|
||||
component: SlideTransition,
|
||||
duration: 2700,
|
||||
@@ -57,47 +78,154 @@ const transitions = shallowRef<Record<string, {
|
||||
component: RectanglesRotateTransition,
|
||||
duration: 2700,
|
||||
},
|
||||
'bubble-wave-out': {
|
||||
component: BubbleWaveOutTransition,
|
||||
duration: 2700,
|
||||
},
|
||||
})
|
||||
|
||||
function triggerTransition(name: string) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const transition = transitions.value[name]
|
||||
if (!transition) {
|
||||
console.error(`Transition ${name} not found`)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
// Hook system
|
||||
const lifecycleHooks = ref<TransitionHook[]>([])
|
||||
|
||||
// Add a hook
|
||||
function addTransitionHook(hook: TransitionHook): () => void {
|
||||
lifecycleHooks.value.push(hook)
|
||||
|
||||
// Return function to remove the hook
|
||||
return () => {
|
||||
const index = lifecycleHooks.value.indexOf(hook)
|
||||
if (index >= 0) {
|
||||
lifecycleHooks.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger all hooks for a stage
|
||||
async function triggerHooks(stage: TransitionStage, data: any = {}) {
|
||||
transitionStage.value = stage
|
||||
|
||||
// Execute all hooks and wait for them to complete
|
||||
for (const hook of lifecycleHooks.value) {
|
||||
try {
|
||||
await Promise.resolve(hook(stage, data))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error in transition hook at stage "${stage}":`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerTransitionAsyncFn(name: string, next: NavigationCallback, resolve: (value: void | PromiseLike<void>) => void) {
|
||||
const transition = transitions.value[name]
|
||||
if (!transition) {
|
||||
console.error(`Transition ${name} not found`)
|
||||
next()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// Get navigation timing from configuration, with fallback
|
||||
const navTiming = transition.nextDelay !== undefined
|
||||
? transition.nextDelay
|
||||
: transition.duration / 3 // Fallback, but configurable
|
||||
|
||||
let hasNavigated = false
|
||||
|
||||
// Hook specifically for determining when to navigate
|
||||
const navigationHook = (stage: TransitionStage) => {
|
||||
if (stage === 'navigation' && !hasNavigated) {
|
||||
hasNavigated = true
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
// Add this hook temporarily
|
||||
const removeNavHook = addTransitionHook(navigationHook)
|
||||
|
||||
try {
|
||||
// Before enter
|
||||
await triggerHooks('before-enter', { transitionName: name })
|
||||
|
||||
// Handle resetting current transition if needed
|
||||
if (showTransition.value) {
|
||||
// Fast track to exit if a transition is already active
|
||||
await triggerHooks('before-leave', { transitionName: activeTransitionName.value })
|
||||
activeTransitionName.value = ''
|
||||
showTransition.value = false
|
||||
await triggerHooks('after-leave', { transitionName: activeTransitionName.value })
|
||||
|
||||
setTimeout(() => {
|
||||
activeTransitionName.value = name
|
||||
showTransition.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
showTransition.value = false
|
||||
activeTransitionName.value = ''
|
||||
resolve()
|
||||
}, transition.duration)
|
||||
}, 50)
|
||||
// Short delay before starting new transition
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
}
|
||||
else {
|
||||
activeTransitionName.value = name
|
||||
showTransition.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
showTransition.value = false
|
||||
activeTransitionName.value = ''
|
||||
resolve()
|
||||
}, transition.duration)
|
||||
}
|
||||
// Start new transition
|
||||
activeTransitionName.value = name
|
||||
showTransition.value = true
|
||||
|
||||
// Enter active phase
|
||||
await triggerHooks('enter-active', { transitionName: name })
|
||||
|
||||
// Trigger navigation phase at the configured time
|
||||
setTimeout(async () => {
|
||||
await triggerHooks('navigation', {
|
||||
transitionName: name,
|
||||
config: transition, // Pass full config to hooks
|
||||
})
|
||||
|
||||
// Ensure navigation happens even if no hook handled it
|
||||
if (!hasNavigated) {
|
||||
hasNavigated = true
|
||||
next()
|
||||
}
|
||||
}, navTiming)
|
||||
|
||||
// After enter - entry phase complete
|
||||
setTimeout(async () => {
|
||||
await triggerHooks('after-enter', { transitionName: name })
|
||||
}, transition.duration)
|
||||
|
||||
// Before leave - start exit phase
|
||||
setTimeout(async () => {
|
||||
await triggerHooks('before-leave', { transitionName: name })
|
||||
}, transition.duration + 10) // Small offset after enter completes
|
||||
|
||||
// Leave active - exit animation running
|
||||
setTimeout(async () => {
|
||||
await triggerHooks('leave-active', { transitionName: name })
|
||||
}, transition.duration + 20) // Small offset
|
||||
|
||||
// After leave - completely finished
|
||||
const totalDuration = transition.exitDuration ?? 0
|
||||
setTimeout(async () => {
|
||||
showTransition.value = false
|
||||
activeTransitionName.value = ''
|
||||
await triggerHooks('after-leave', { transitionName: name })
|
||||
resolve()
|
||||
}, transition.duration + totalDuration)
|
||||
}
|
||||
finally {
|
||||
// Always remove the navigation hook
|
||||
removeNavHook()
|
||||
|
||||
// Safety - ensure navigation happens no matter what
|
||||
setTimeout(() => {
|
||||
if (!hasNavigated) {
|
||||
hasNavigated = true
|
||||
next()
|
||||
}
|
||||
}, transition.duration * 2) // Conservative timeout
|
||||
}
|
||||
}
|
||||
|
||||
// Improved transition trigger with navigation callback
|
||||
function triggerTransition(name: string, next: NavigationCallback) {
|
||||
return new Promise<void>((resolve) => {
|
||||
triggerTransitionAsyncFn(name, next, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
triggerTransition(to.meta.stageTransition as string).then(next)
|
||||
triggerTransition(to.meta.stageTransition as string, next)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as ArrowTransition } from './ArrowTransition.vue'
|
||||
export { default as CircleExpansionTransition } from './CircleExpansionTransition.vue'
|
||||
export { default as FantasyFallTransition } from './FantasyFallTransition.vue'
|
||||
export { default as MultipleBlocksRevealTransition } from './MultipleBlocksRevealTransition.vue'
|
||||
export { default as SlideTransition } from './SlideTransition.vue'
|
||||
|
||||
Reference in New Issue
Block a user