Files
arcticnewone/app/components/home/HomeIntro.vue
T
virtheart 76e7b4b817 refactor(components): 优化动画组件代码结构并移除冗余样式
style(pages): 使用实用类替换内联样式
refactor(composables): 简化页面标题模板逻辑
docs(components): 添加详细注释说明动画逻辑
2026-04-27 02:28:49 +08:00

77 lines
3.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onBeforeUnmount } from 'vue'
import gsap from 'gsap'
const introTitle = 'ArcticNewOne'
// 将标题拆分为单个字符,用于后续 GSAP 的逐字动画
const introLetters = introTitle.split('')
const introRef = ref<HTMLElement | null>(null)
const introTitleRef = ref<HTMLElement | null>(null)
let ctx: gsap.Context | null = null
// 暴露供外部(如 index.vue)使用的异步播放方法
const play = () => new Promise<void>((resolve) => {
if (!introRef.value || !introTitleRef.value) return resolve()
// 获取所有需要执行动画的字符和 TM 标志 DOM 节点
const chars = gsap.utils.toArray('.hero-intro-char', introRef.value)
const tm = introRef.value.querySelector('.hero-intro-tm')
if (!chars.length || !tm) return resolve()
// 使用 gsap.context 包装时间轴,方便在组件卸载时统一清理所有动画状态(revert)
ctx = gsap.context(() => {
// 1. 初始化 DOM 的初始样式状态,防止闪烁
gsap.set(introRef.value, { display: 'grid', yPercent: 0, autoAlpha: 1 })
gsap.set(introTitleRef.value, { y: 0, autoAlpha: 1 })
// 将每个字符初始放置在视口下方(yPercent: 110),等待升起
gsap.set(chars, { y: 0, yPercent: 110 })
// TM 标志初始透明度为 0
gsap.set(tm, { autoAlpha: 0 })
// 2. 创建并执行时间轴
gsap.timeline({
defaults: { ease: 'power3.out' },
onComplete: () => {
// 动画完全结束时,隐藏整个组件容器并 resolve Promise,通知外部可进行下一步
gsap.set(introRef.value, { display: 'none' })
resolve()
}
})
// 步骤 A:字符从下方逐字平滑升起入场 (stagger 产生阶梯感)
.to(chars, { yPercent: 0, duration: 0.75, stagger: 0.055 })
// 步骤 BTM 标志渐显入场
.to(tm, { autoAlpha: 1, duration: 0.4, ease: 'power2.out' })
// 停顿 0.35s 后,为接下来的离场动画打上时间标签
.addLabel('introOut', '+=0.35')
// 步骤 C:标题整体淡出
.to(introTitleRef.value, { autoAlpha: 0, duration: 1.05, ease: 'power4.inOut' }, 'introOut')
// 步骤 D:同时,整个 Intro 黑色背景容器向上滑出屏幕
.to(introRef.value, { yPercent: -100, duration: 1.05, ease: 'power4.inOut' }, 'introOut')
}, introRef.value)
})
// 组件销毁前清理 GSAP 动画上下文,避免内存泄漏或在 SPA 路由切换时导致状态异常
onBeforeUnmount(() => ctx?.revert())
defineExpose({ play })
</script>
<template>
<div ref="introRef" class="fixed inset-0 z-[80] hidden place-items-center overflow-hidden bg-black text-white opacity-0 invisible will-change-transform" aria-hidden="true">
<h1 ref="introTitleRef" class="m-0 flex whitespace-nowrap font-bold leading-[0.9] tracking-[-0.07em] text-[clamp(36px,6.2vw,104px)]" :aria-label="introTitle" v-once>
<span class="relative inline-flex">
<span
v-for="(letter, index) in introLetters"
:key="`${letter}-${index}`"
class="relative inline-block overflow-hidden"
>
<!-- GSAP 将通过操作此元素的 translateY 来实现逐字浮现 -->
<span class="hero-intro-char inline-block will-change-transform">{{ letter }}</span>
</span>
<span class="hero-intro-tm absolute left-[calc(100%+0.12em)] top-[0.08em] text-[0.13em] font-bold leading-none tracking-[-0.02em] opacity-0 invisible">TM</span>
</span>
</h1>
</div>
</template>