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 -83
View File
@@ -216,100 +216,57 @@ onMounted(async () => {
}, 100)
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'
}
)
// 连接线绘制动画
if (lineRef.value && linePathRef.value) {
const pathLength = linePathRef.value.getTotalLength()
gsap.set(linePathRef.value, {
strokeDasharray: pathLength,
strokeDashoffset: pathLength
})
gsap.fromTo(lineRef.value,
{ autoAlpha: 0 },
{
scrollTrigger: {
trigger: scrollContainerRef.value,
start: 'top 75%',
toggleActions: 'play none none reverse'
},
autoAlpha: 1,
duration: 0.5
}
)
gsap.to(linePathRef.value,
{
scrollTrigger: {
trigger: scrollContainerRef.value,
start: 'top 75%',
toggleActions: 'play none none reverse'
},
strokeDashoffset: 0,
duration: 2,
ease: 'power2.inOut'
}
)
}
// 步骤节点弹跳入场
stepElements.value.forEach((step, index) => {
// 初始状态
gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
stepElements.value.forEach((step) => {
if (!step) return
gsap.fromTo(step,
{ autoAlpha: 0, y: 100, scale: 0.8 },
{
scrollTrigger: {
trigger: step,
start: 'top 90%',
toggleActions: 'play none none reverse'
},
autoAlpha: 1,
y: 0,
scale: 1,
duration: 0.8,
delay: index * 0.15,
ease: 'back.out(1.5)'
}
)
gsap.set(step, { autoAlpha: 0, y: 60, scale: 0.85 })
})
// Pin 滚动效果:固定板块,横向滚动浏览所有步骤
// 统一时间线
const tl = gsap.timeline()
// 第一屏:标题入场
tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0)
// 连接线绘制
if (lineRef.value && linePathRef.value) {
const pathLength = linePathRef.value.getTotalLength()
gsap.set(linePathRef.value, { strokeDasharray: pathLength, strokeDashoffset: pathLength })
tl.to(lineRef.value, { autoAlpha: 1, duration: 0.3 }, 0.5)
tl.to(linePathRef.value, { strokeDashoffset: 0, duration: 2, ease: 'power2.inOut' }, 0.5)
}
// 横向滚动:步骤卡片逐个出现
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,
}
// 逐个展示步骤
stepElements.value.forEach((step, index) => {
if (!step) return
tl.to(step, {
autoAlpha: 1, y: 0, scale: 1,
duration: 0.6, ease: 'back.out(1.5)',
}, 1 + index * 0.3)
})
// 横向滚动
tl.to(container, { x: -scrollWidth, duration: 3, ease: 'none' }, 1)
}
}
const totalSteps = stepElements.value.length || 6
ScrollTrigger.create({
trigger: rootRef.value,
start: 'top top',
end: () => `+=${(totalSteps + 2) * window.innerHeight}`,
pin: true,
scrub: 0.5,
anticipatePin: 1,
animation: tl,
})
}, rootRef.value)
})