feat: 为首页新增板块添加 ScrollTrigger pin 滚动效果

 新增 pin 效果(板块固定,内容随滚动切换):

1. HomeTestimonials - 客户评价
   - 固定板块,滚动自动切换卡片
   - 每屏切换一张评价卡片

2. HomeTechStack - 技术栈
   - 固定板块,3D 球体自动旋转一圈
   - 滚动控制旋转进度

3. HomeProcess - 创意流程
   - 固定板块,横向滚动浏览所有步骤
   - 6 个步骤依次展示

4. HomeServices - 服务能力
   - 固定板块,逐张卡片聚焦展示
   - 聚焦卡片放大,其他缩小变暗
   - 所有卡片飞入归位后逐个聚焦

5. HomeAchievements - 数字成就
   - 固定板块,逐个成就项展示
   - 每个数字滚动计数动画触发

6. HomeUpdates - 最新动态
   - 固定板块,逐批展示卡片
   - 分批出现,每批 3 个卡片

🎯 效果:
- 滚动到板块时自动固定(pin)
- 内部元素随滚动依次展示
- 所有内部元素展示完毕后才继续下一个板块
- 每个板块独特的 pin 动画效果

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
陈春
2026-06-03 19:49:18 +08:00
parent 89c4c4c7b5
commit bb0020b406
6 changed files with 208 additions and 76 deletions
+33 -22
View File
@@ -206,32 +206,43 @@ onMounted(async () => {
const cleanupParticles = initParticles() const cleanupParticles = initParticles()
ctx = gsap.context(() => { ctx = gsap.context(() => {
// Pin 效果:固定板块,逐个成就项展示
const totalItems = achievements.length
const timeline = gsap.timeline()
// 初始隐藏所有
achievementElements.value.forEach((item, index) => { achievementElements.value.forEach((item, index) => {
if (!item) return if (!item) return
gsap.set(item, { autoAlpha: 0, x: index % 2 === 0 ? -100 : 100 })
})
// 成就项入场动画 // 逐个展示每个成就项
gsap.fromTo(item, for (let i = 0; i < totalItems; i++) {
{ autoAlpha: 0, x: index % 2 === 0 ? -100 : 100 }, timeline.addLabel(`item-${i}`)
{
scrollTrigger: { timeline.to(achievementElements.value[i], {
trigger: item, autoAlpha: 1,
start: 'top 80%', x: 0,
toggleActions: 'play none none reverse', duration: 0.8,
onEnter: () => { ease: 'power3.out',
// 触发数字计数 onStart: () => {
const numberEl = numberElements.value[index] // 触发数字计数
if (numberEl) { const numberEl = numberElements.value[i]
animateNumber(numberEl, achievements[index].number, achievements[index].suffix) if (numberEl) {
} animateNumber(numberEl, achievements[i].number, achievements[i].suffix)
} }
},
autoAlpha: 1,
x: 0,
duration: 1,
delay: index * 0.2,
ease: 'power3.out'
} }
) }, `item-${i}`)
}
ScrollTrigger.create({
trigger: rootRef.value,
start: 'top top',
end: () => `+=${totalItems * window.innerHeight}`,
pin: true,
scrub: true,
anticipatePin: 1,
animation: timeline,
}) })
}, rootRef.value) }, rootRef.value)
+21
View File
@@ -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) }, rootRef.value)
}) })
+76 -31
View File
@@ -218,45 +218,90 @@ onMounted(async () => {
} }
) )
// 卡片入场动画 - 从四周飞入 // Pin 效果:固定板块,逐张卡片聚焦展示
const totalCards = services.length
const timeline = gsap.timeline()
// 初始:所有卡片从四周飞入并归位
cardElements.value.forEach((card, index) => { cardElements.value.forEach((card, index) => {
if (!card) return if (!card) return
const positions = [ const positions = [
{ x: -200, y: -200 }, // 左上 { x: -200, y: -200, rotate: -15 },
{ x: 0, y: -200 }, // 上 { x: 0, y: -200, rotate: 10 },
{ x: 200, y: -200 }, // 右上 { x: 200, y: -200, rotate: -15 },
{ x: -200, y: 200 }, // 左下 { x: -200, y: 200, rotate: 15 },
{ x: 0, y: 200 }, // 下 { x: 0, y: 200, rotate: -10 },
{ x: 200, y: 200 } // 右下 { x: 200, y: 200, rotate: 15 }
] ]
const startPos = positions[index] const startPos = positions[index]
gsap.fromTo(card, gsap.set(card, {
{ autoAlpha: 0,
autoAlpha: 0, x: startPos.x,
x: startPos.x, y: startPos.y,
y: startPos.y, scale: 0.8,
scale: 0.8, rotateZ: startPos.rotate,
rotateZ: index % 2 === 0 ? -15 : 15 })
},
{ // 第一屏:所有卡片飞入归位
scrollTrigger: { timeline.to(card, {
trigger: card, autoAlpha: 1,
start: 'top 85%', x: 0,
toggleActions: 'play none none reverse' y: 0,
}, scale: 1,
autoAlpha: 1, rotateZ: 0,
x: 0, duration: 1,
y: 0, ease: 'back.out(1.2)',
scale: 1, }, index * 0.1)
rotateZ: 0, })
duration: 1.2,
delay: index * 0.1, // 后续屏:每次聚焦一张卡片(放大),其他缩小
ease: 'back.out(1.2)' 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) }, rootRef.value)
}) })
+20
View File
@@ -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) }, rootRef.value)
// 启动自动旋转 // 启动自动旋转
+24 -2
View File
@@ -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) }, rootRef.value)
// 启动自动播放 // 启动自动播放,使用滚动控制
startAutoplay() // startAutoplay()
}) })
onUnmounted(() => { onUnmounted(() => {
+34 -21
View File
@@ -180,34 +180,47 @@ onMounted(async () => {
} }
) )
// 卡片错开入场 - 每列不同速度 // Pin 效果:固定板块,逐行展示卡片
cardElements.value.forEach((card, index) => { const totalCards = updates.length
const timeline = gsap.timeline()
// 初始隐藏所有卡片
cardElements.value.forEach((card) => {
if (!card) return if (!card) return
gsap.set(card, { autoAlpha: 0, y: 60, scale: 0.9 })
})
// 根据列位置计算延迟 // 分批展示:每批展示一行(按顺序,每行 2-3 个)
const columnDelay = (index % 3) * 0.1 const batchSize = 3
const rowDelay = Math.floor(index / 3) * 0.15 const batches = Math.ceil(totalCards / batchSize)
gsap.fromTo(card, for (let batch = 0; batch < batches; batch++) {
{ timeline.addLabel(`batch-${batch}`)
autoAlpha: 0,
y: 80, const start = batch * batchSize
scale: 0.9 const end = Math.min(start + batchSize, totalCards)
},
{ for (let i = start; i < end; i++) {
scrollTrigger: { if (!cardElements.value[i]) continue
trigger: card,
start: 'top 85%', timeline.to(cardElements.value[i], {
toggleActions: 'play none none reverse'
},
autoAlpha: 1, autoAlpha: 1,
y: 0, y: 0,
scale: 1, scale: 1,
duration: 0.8, 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) }, rootRef.value)
}) })