fix: 修复所有首页板块 pin 滚动不连贯问题

🐛 问题根源:
每个板块有两套独立的 ScrollTrigger 在同一个元素上:
- 入场动画用独立的 scrollTrigger
- pin 效果用 ScrollTrigger.create
两者互相冲突,导致空白间隙和动画跳变

 修复方案:
将所有板块的入场动画和 pin 滚动合并为一个统一时间线(timeline),
确保动画从 pin 开始到结束完全连续,无空白区

🔧 修复的 6 个板块:
1. HomeTestimonials - 标题入场 + 卡片逐张切换(淡入淡出)
2. HomeServices - 标题入场 + 卡片飞入归位 + 逐张聚焦
3. HomeAchievements - 逐个成就项展示(前一个淡出,后一个淡入)
4. HomeTechStack - 内容入场 + 标签散开 + 球体旋转
5. HomeProcess - 标题入场 + 线条绘制 + 步骤逐个出现 + 横向滚动
6. HomeUpdates - 标题入场 + 卡片分批出现

🎯 效果:
- pin 期间动画完全连续,无空白间隙
- 入场和 pin 共享同一个 timeline
- scrub: 0.5 让滚动更平滑
- 每个板块到下一个板块无缝过渡

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
陈春
2026-06-03 19:53:46 +08:00
parent bb0020b406
commit 02323fd426
6 changed files with 161 additions and 307 deletions
+40 -76
View File
@@ -202,106 +202,70 @@ onMounted(async () => {
if (!rootRef.value || !headerRef.value) return
ctx = gsap.context(() => {
// 标题动画
gsap.fromTo(headerRef.value,
{ autoAlpha: 0, y: 50 },
{
scrollTrigger: {
trigger: headerRef.value,
start: 'top 80%',
toggleActions: 'play none none reverse'
},
autoAlpha: 1,
y: 0,
duration: 1,
ease: 'power3.out'
}
)
// Pin 效果:固定板块,逐张卡片聚焦展示
const totalCards = services.length
const timeline = gsap.timeline()
// 初始:所有卡片从四周飞入并归位
// 初始状态
gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
const positions = [
{ 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 }
]
cardElements.value.forEach((card, index) => {
if (!card) return
const positions = [
{ 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.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)
const p = positions[index]
gsap.set(card, { autoAlpha: 0, x: p.x, y: p.y, scale: 0.8, rotateZ: p.rotate })
})
// 后续屏:每次聚焦一张卡片(放大),其他缩小
for (let focus = 0; focus < totalCards; focus++) {
timeline.addLabel(`focus-${focus}`, focus + 1)
// 统一时间线
const totalCards = services.length
const tl = gsap.timeline()
// 第一屏:标题入场 + 所有卡片飞入归位
tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0)
cardElements.value.forEach((card, index) => {
if (!card) return
tl.to(card, {
autoAlpha: 1, x: 0, y: 0, scale: 1, rotateZ: 0,
duration: 1, ease: 'back.out(1.2)',
}, 0.2 + index * 0.1)
})
// 后续屏:逐张聚焦
for (let focus = 0; focus < totalCards; focus++) {
const pos = 1.5 + focus
cardElements.value.forEach((card, index) => {
if (!card) return
timeline.to(card, {
tl.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}`)
// 聚焦的卡片翻转展示背面
duration: 0.6, ease: 'power2.out',
}, pos)
if (index === focus) {
timeline.call(() => handleCardHover(focus, true), [], `focus-${focus}`)
tl.call(() => handleCardHover(focus, true), [], pos)
} else {
timeline.call(() => handleCardHover(index, false), [], `focus-${focus}`)
tl.call(() => handleCardHover(index, false), [], pos)
}
})
}
// 最后:恢复所有卡片正常状态
timeline.addLabel('reset')
// 最后:恢复正常
const resetPos = 1.5 + totalCards
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')
tl.to(card, { scale: 1, autoAlpha: 1, duration: 0.5, ease: 'power2.out' }, resetPos)
tl.call(() => handleCardHover(index, false), [], resetPos)
})
ScrollTrigger.create({
trigger: rootRef.value,
start: 'top top',
end: () => `+=${(totalCards + 2) * window.innerHeight}`,
end: () => `+=${(totalCards + 3) * window.innerHeight}`,
pin: true,
scrub: true,
scrub: 0.5,
anticipatePin: 1,
animation: timeline,
animation: tl,
})
}, rootRef.value)
})