diff --git a/app/components/home/HomeHero.vue b/app/components/home/HomeHero.vue new file mode 100644 index 0000000..e51bc7a --- /dev/null +++ b/app/components/home/HomeHero.vue @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + Hero Section + + + + + + diff --git a/app/components/home/HomeIntro.vue b/app/components/home/HomeIntro.vue new file mode 100644 index 0000000..5b95e95 --- /dev/null +++ b/app/components/home/HomeIntro.vue @@ -0,0 +1,88 @@ + + + + + + + + {{ letter }} + + TM + + + + + + diff --git a/app/components/home/HomeLoader.vue b/app/components/home/HomeLoader.vue new file mode 100644 index 0000000..c553fba --- /dev/null +++ b/app/components/home/HomeLoader.vue @@ -0,0 +1,65 @@ + + + + + + Loading + + + + + + + + diff --git a/app/pages/index.vue b/app/pages/index.vue index a553354..b271480 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -5,13 +5,13 @@ import type Lenis from 'lenis' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import { useUserStore } from '~/stores/user' +import HomeLoader from '~/components/home/HomeLoader.vue' +import HomeIntro from '~/components/home/HomeIntro.vue' +import HomeHero from '~/components/home/HomeHero.vue' gsap.registerPlugin(ScrollTrigger) const { t } = useI18n() -const introTitle = 'ArcticNewOne' -const introLetters = introTitle.split('') -const heroPinDistance = 1200 const isIntroActive = ref(true) const userStore = useUserStore() const demoUserName = computed(() => userStore.displayName) @@ -38,39 +38,14 @@ useSeoMeta({ twitterDescription: () => t('home.welcome'), }) -// 背景图是装饰层,左右两列共用同一套模板,只通过 side 区分位置和速度。 -const heroGalleryColumns = [ - { - side: 'left', - images: [ - 'https://picsum.photos/id/1018/900/1200', - 'https://picsum.photos/id/1015/900/1200', - 'https://picsum.photos/id/1024/900/1200', - 'https://picsum.photos/id/1039/900/1200', - ], - }, - { - side: 'right', - images: [ - 'https://picsum.photos/id/1043/900/1200', - 'https://picsum.photos/id/1050/900/1200', - 'https://picsum.photos/id/1067/900/1200', - 'https://picsum.photos/id/1074/900/1200', - ], - }, -] as const const pageRef = ref(null) -const loaderRef = ref(null) -const loaderBarRef = ref(null) -const introRef = ref(null) -const introTitleRef = ref(null) -const heroStageRef = ref(null) -const heroRef = ref(null) -const heroPanelRef = ref(null) +const homeLoaderRef = ref | null>(null) +const homeIntroRef = ref | null>(null) +const homeHeroRef = ref | null>(null) + let heroAnimationContext: gsap.Context | null = null let removeLoadListener: (() => void) | null = null let restorePageScroll: (() => void) | null = null -let cleanupHeroScroll: (() => void) | null = null let hasIntroStarted = false let loaderTween: gsap.core.Tween | null = null let introTimeline: gsap.core.Timeline | null = null @@ -83,13 +58,11 @@ const setHomeIntroActive = (active: boolean) => { onMounted(() => { const pageElement = pageRef.value - const loaderElement = loaderRef.value - const loaderBarElement = loaderBarRef.value - const introElement = introRef.value - const introTitleElement = introTitleRef.value - const heroStageElement = heroStageRef.value - const heroElement = heroRef.value - const heroPanelElement = heroPanelRef.value + const loaderElement = homeLoaderRef.value?.loaderRef + const loaderBarElement = homeLoaderRef.value?.loaderBarRef + const introElement = homeIntroRef.value?.introRef + const introTitleElement = homeIntroRef.value?.introTitleRef + const heroElement = homeHeroRef.value?.heroRef if ( !pageElement @@ -97,78 +70,21 @@ onMounted(() => { || !loaderBarElement || !introElement || !introTitleElement - || !heroStageElement || !heroElement - || !heroPanelElement ) { return } const introCharElements = gsap.utils.toArray('.hero-intro-char', introElement) - const tmElement = introElement.querySelector('.hero-intro-tm') + const tmElement = introElement.querySelector('.hero-intro-tm') - if (introCharElements.length === 0 || !tmElement) { - return - } + if (introCharElements.length === 0 || !tmElement) { + return + } const { $lenis } = useNuxtApp() as ReturnType & { $lenis?: Lenis } - const topHeader = document.querySelector('.site-header-top') heroAnimationContext = gsap.context(() => { - const pinEndScale = 0.65 - const finalScale = 0.18 - const getExitDistance = () => window.innerHeight - - const setupHeroScroll = () => { - cleanupHeroScroll?.() - - gsap.set(heroPanelElement, { - scale: 1, - force3D: true, - transformOrigin: 'center center', - }) - - let renderedScale = -1 - - const setHeroPanelScale = (scale: number) => { - if (Math.abs(renderedScale - scale) < 0.001) { - return - } - - renderedScale = scale - gsap.set(heroPanelElement, { scale }) - } - - const updateHeroPanelScale = () => { - const exitDistance = getExitDistance() - const totalDistance = heroPinDistance + exitDistance - // Lenis 存在时只读 Lenis 的平滑滚动值,避免向上滚时 window.scrollY 和 Lenis 值互相打架。 - const currentScroll = $lenis ? $lenis.scroll : window.scrollY - const currentDistance = gsap.utils.clamp(0, totalDistance, currentScroll - heroStageElement.offsetTop) - - if (currentDistance <= heroPinDistance) { - setHeroPanelScale(gsap.utils.interpolate(1, pinEndScale, currentDistance / heroPinDistance)) - return - } - - setHeroPanelScale( - gsap.utils.interpolate( - pinEndScale, - finalScale, - (currentDistance - heroPinDistance) / exitDistance, - ), - ) - } - - // CSS sticky 负责固定;ticker 每帧读取当前滚动距离,避免依赖 scroll/ScrollTrigger 事件链。 - updateHeroPanelScale() - gsap.ticker.add(updateHeroPanelScale) - - cleanupHeroScroll = () => { - gsap.ticker.remove(updateHeroPanelScale) - } - } - const playIntro = () => { if (hasIntroStarted) { return @@ -265,22 +181,6 @@ onMounted(() => { this._headerTriggered = false } }, 'introOut') - // 取消原本通过 index.vue 直接控制导航栏 yPercent 的行为 - // 因为我们要让 Header 自己执行从屏幕底部(y: 100vh)飞入的高级动画 - // if (topHeader) { - // introTimeline.to(topHeader, { - // yPercent: 0, - // autoAlpha: 1, - // duration: 0.55, - // ease: 'power3.out', - // }, 'introOut+=0.45') - // } - - // 在动画即将完成或者 Hero 入场时触发自定义事件,通知导航栏执行自身的从下往上飞入动画 - // 将触发时机再延后,等白色背景开始渲染的瞬间就触发 - // introTimeline.add(() => { - // window.dispatchEvent(new Event('hero-intro-done')) - // }, 'introOut') } const releasePageScroll = () => { @@ -303,7 +203,6 @@ onMounted(() => { gsap.set(introCharElements, { y: 0, yPercent: 110 }) gsap.set(tmElement, { autoAlpha: 0 }) gsap.set(heroElement, { yPercent: 100 }) - setupHeroScroll() loaderTween = gsap.to(loaderBarElement, { scaleX: 0.82, @@ -344,8 +243,6 @@ onBeforeUnmount(() => { loaderTween = null introTimeline?.kill() introTimeline = null - cleanupHeroScroll?.() - cleanupHeroScroll = null removeLoadListener?.() removeLoadListener = null restorePageScroll?.() @@ -353,7 +250,6 @@ onBeforeUnmount(() => { heroAnimationContext?.revert() heroAnimationContext = null }) - @@ -363,67 +259,10 @@ onBeforeUnmount(() => { :data-demo-user="demoUserName" :data-demo-user-role="demoUserRole" > - - - Loading - - - - - + + + - - - - - {{ letter }} - - TM - - - - - - - - - - - - - - - - - - Hero Section - - - Next Section @@ -434,176 +273,6 @@ onBeforeUnmount(() => {
Hero Section
Loading