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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
// 启动自动旋转
|
// 启动自动旋转
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user