diff --git a/app/components/home/HomeAchievements.vue b/app/components/home/HomeAchievements.vue index 48dcb2f..10e4212 100644 --- a/app/components/home/HomeAchievements.vue +++ b/app/components/home/HomeAchievements.vue @@ -206,32 +206,43 @@ onMounted(async () => { const cleanupParticles = initParticles() ctx = gsap.context(() => { + // Pin 效果:固定板块,逐个成就项展示 + const totalItems = achievements.length + const timeline = gsap.timeline() + + // 初始隐藏所有 achievementElements.value.forEach((item, index) => { if (!item) return + gsap.set(item, { autoAlpha: 0, x: index % 2 === 0 ? -100 : 100 }) + }) - // 成就项入场动画 - gsap.fromTo(item, - { autoAlpha: 0, x: index % 2 === 0 ? -100 : 100 }, - { - scrollTrigger: { - trigger: item, - start: 'top 80%', - toggleActions: 'play none none reverse', - onEnter: () => { - // 触发数字计数 - const numberEl = numberElements.value[index] - if (numberEl) { - animateNumber(numberEl, achievements[index].number, achievements[index].suffix) - } - } - }, - autoAlpha: 1, - x: 0, - duration: 1, - delay: index * 0.2, - ease: 'power3.out' + // 逐个展示每个成就项 + for (let i = 0; i < totalItems; i++) { + timeline.addLabel(`item-${i}`) + + timeline.to(achievementElements.value[i], { + autoAlpha: 1, + x: 0, + duration: 0.8, + ease: 'power3.out', + onStart: () => { + // 触发数字计数 + const numberEl = numberElements.value[i] + if (numberEl) { + animateNumber(numberEl, achievements[i].number, achievements[i].suffix) + } } - ) + }, `item-${i}`) + } + + ScrollTrigger.create({ + trigger: rootRef.value, + start: 'top top', + end: () => `+=${totalItems * window.innerHeight}`, + pin: true, + scrub: true, + anticipatePin: 1, + animation: timeline, }) }, rootRef.value) diff --git a/app/components/home/HomeProcess.vue b/app/components/home/HomeProcess.vue index 277f793..5396de7 100644 --- a/app/components/home/HomeProcess.vue +++ b/app/components/home/HomeProcess.vue @@ -289,6 +289,27 @@ onMounted(async () => { } ) }) + + // Pin 滚动效果:固定板块,横向滚动浏览所有步骤 + if (scrollContainerRef.value) { + const container = scrollContainerRef.value.querySelector('.flex') + if (container) { + const scrollWidth = container.scrollWidth - window.innerWidth + + gsap.to(container, { + x: -scrollWidth, + ease: 'none', + scrollTrigger: { + trigger: rootRef.value, + start: 'top top', + end: () => `+=${stepElements.value.length * window.innerHeight}`, + pin: true, + scrub: 1, + anticipatePin: 1, + } + }) + } + } }, rootRef.value) }) diff --git a/app/components/home/HomeServices.vue b/app/components/home/HomeServices.vue index 83dbce9..5a76fdf 100644 --- a/app/components/home/HomeServices.vue +++ b/app/components/home/HomeServices.vue @@ -218,45 +218,90 @@ onMounted(async () => { } ) - // 卡片入场动画 - 从四周飞入 + // Pin 效果:固定板块,逐张卡片聚焦展示 + const totalCards = services.length + const timeline = gsap.timeline() + + // 初始:所有卡片从四周飞入并归位 cardElements.value.forEach((card, index) => { if (!card) return const positions = [ - { x: -200, y: -200 }, // 左上 - { x: 0, y: -200 }, // 上 - { x: 200, y: -200 }, // 右上 - { x: -200, y: 200 }, // 左下 - { x: 0, y: 200 }, // 下 - { x: 200, y: 200 } // 右下 + { x: -200, y: -200, rotate: -15 }, + { x: 0, y: -200, rotate: 10 }, + { x: 200, y: -200, rotate: -15 }, + { x: -200, y: 200, rotate: 15 }, + { x: 0, y: 200, rotate: -10 }, + { x: 200, y: 200, rotate: 15 } ] - const startPos = positions[index] - gsap.fromTo(card, - { - autoAlpha: 0, - x: startPos.x, - y: startPos.y, - scale: 0.8, - rotateZ: index % 2 === 0 ? -15 : 15 - }, - { - scrollTrigger: { - trigger: card, - start: 'top 85%', - toggleActions: 'play none none reverse' - }, - autoAlpha: 1, - x: 0, - y: 0, - scale: 1, - rotateZ: 0, - duration: 1.2, - delay: index * 0.1, - ease: 'back.out(1.2)' + gsap.set(card, { + autoAlpha: 0, + x: startPos.x, + y: startPos.y, + scale: 0.8, + rotateZ: startPos.rotate, + }) + + // 第一屏:所有卡片飞入归位 + timeline.to(card, { + autoAlpha: 1, + x: 0, + y: 0, + scale: 1, + rotateZ: 0, + duration: 1, + ease: 'back.out(1.2)', + }, index * 0.1) + }) + + // 后续屏:每次聚焦一张卡片(放大),其他缩小 + for (let focus = 0; focus < totalCards; focus++) { + timeline.addLabel(`focus-${focus}`, focus + 1) + + cardElements.value.forEach((card, index) => { + if (!card) return + + timeline.to(card, { + scale: index === focus ? 1.05 : 0.85, + autoAlpha: index === focus ? 1 : 0.4, + zIndex: index === focus ? 10 : 1, + duration: 0.6, + ease: 'power2.out', + }, `focus-${focus}`) + + // 聚焦的卡片翻转展示背面 + if (index === focus) { + timeline.call(() => handleCardHover(focus, true), [], `focus-${focus}`) + } else { + timeline.call(() => handleCardHover(index, false), [], `focus-${focus}`) } - ) + }) + } + + // 最后:恢复所有卡片正常状态 + timeline.addLabel('reset') + cardElements.value.forEach((card, index) => { + if (!card) return + timeline.to(card, { + scale: 1, + autoAlpha: 1, + zIndex: 1, + duration: 0.5, + ease: 'power2.out', + }, 'reset') + timeline.call(() => handleCardHover(index, false), [], 'reset') + }) + + ScrollTrigger.create({ + trigger: rootRef.value, + start: 'top top', + end: () => `+=${(totalCards + 2) * window.innerHeight}`, + pin: true, + scrub: true, + anticipatePin: 1, + animation: timeline, }) }, rootRef.value) }) diff --git a/app/components/home/HomeTechStack.vue b/app/components/home/HomeTechStack.vue index a4a34eb..ad0e2a9 100644 --- a/app/components/home/HomeTechStack.vue +++ b/app/components/home/HomeTechStack.vue @@ -229,6 +229,26 @@ onMounted(async () => { } ) }) + + // Pin 滚动效果:固定板块,球体旋转一圈展示所有技术 + const rotationTimeline = gsap.timeline() + + rotationTimeline.to(autoRotation.value, { + y: 360, + duration: 3, + ease: 'none', + onUpdate: updatePositions + }) + + ScrollTrigger.create({ + trigger: rootRef.value, + start: 'top top', + end: () => `+=${window.innerHeight * 2}`, + pin: true, + scrub: 1, + anticipatePin: 1, + animation: rotationTimeline, + }) }, rootRef.value) // 启动自动旋转 diff --git a/app/components/home/HomeTestimonials.vue b/app/components/home/HomeTestimonials.vue index 20fef7e..693e97f 100644 --- a/app/components/home/HomeTestimonials.vue +++ b/app/components/home/HomeTestimonials.vue @@ -267,10 +267,32 @@ onMounted(async () => { } ) }) + + // Pin 滚动效果:固定板块,滚动切换卡片 + const totalCards = testimonials.length + const timeline = gsap.timeline() + + // 为每张卡片创建切换动画(每屏切换一张) + for (let i = 0; i < totalCards - 1; i++) { + timeline.to({}, { duration: 1 }, i) + timeline.call(() => { + goToSlide(i + 1) + }, [], i + 0.5) + } + + ScrollTrigger.create({ + trigger: rootRef.value, + start: 'top top', + end: () => `+=${totalCards * window.innerHeight}`, + pin: true, + scrub: true, + anticipatePin: 1, + animation: timeline, + }) }, rootRef.value) - // 启动自动播放 - startAutoplay() + // 不启动自动播放,使用滚动控制 + // startAutoplay() }) onUnmounted(() => { diff --git a/app/components/home/HomeUpdates.vue b/app/components/home/HomeUpdates.vue index 12d88e8..d5a6fa7 100644 --- a/app/components/home/HomeUpdates.vue +++ b/app/components/home/HomeUpdates.vue @@ -180,34 +180,47 @@ onMounted(async () => { } ) - // 卡片错开入场 - 每列不同速度 - cardElements.value.forEach((card, index) => { + // Pin 效果:固定板块,逐行展示卡片 + const totalCards = updates.length + const timeline = gsap.timeline() + + // 初始隐藏所有卡片 + cardElements.value.forEach((card) => { if (!card) return + gsap.set(card, { autoAlpha: 0, y: 60, scale: 0.9 }) + }) - // 根据列位置计算延迟 - const columnDelay = (index % 3) * 0.1 - const rowDelay = Math.floor(index / 3) * 0.15 + // 分批展示:每批展示一行(按顺序,每行 2-3 个) + const batchSize = 3 + const batches = Math.ceil(totalCards / batchSize) - gsap.fromTo(card, - { - autoAlpha: 0, - y: 80, - scale: 0.9 - }, - { - scrollTrigger: { - trigger: card, - start: 'top 85%', - toggleActions: 'play none none reverse' - }, + for (let batch = 0; batch < batches; batch++) { + timeline.addLabel(`batch-${batch}`) + + const start = batch * batchSize + const end = Math.min(start + batchSize, totalCards) + + for (let i = start; i < end; i++) { + if (!cardElements.value[i]) continue + + timeline.to(cardElements.value[i], { autoAlpha: 1, y: 0, scale: 1, duration: 0.8, - delay: columnDelay + rowDelay, - ease: 'power3.out' - } - ) + ease: 'power3.out', + }, `batch-${batch}+=${(i - start) * 0.15}`) + } + } + + ScrollTrigger.create({ + trigger: rootRef.value, + start: 'top top', + end: () => `+=${batches * window.innerHeight}`, + pin: true, + scrub: true, + anticipatePin: 1, + animation: timeline, }) }, rootRef.value) })