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:
@@ -206,43 +206,41 @@ onMounted(async () => {
|
|||||||
const cleanupParticles = initParticles()
|
const cleanupParticles = initParticles()
|
||||||
|
|
||||||
ctx = gsap.context(() => {
|
ctx = gsap.context(() => {
|
||||||
// Pin 效果:固定板块,逐个成就项展示
|
// 初始状态:全部隐藏
|
||||||
const totalItems = achievements.length
|
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.set(item, { autoAlpha: 0, x: index % 2 === 0 ? -100 : 100 })
|
||||||
})
|
})
|
||||||
|
|
||||||
// 逐个展示每个成就项
|
// 统一时间线:逐个展示
|
||||||
for (let i = 0; i < totalItems; i++) {
|
const tl = gsap.timeline()
|
||||||
timeline.addLabel(`item-${i}`)
|
|
||||||
|
|
||||||
timeline.to(achievementElements.value[i], {
|
for (let i = 0; i < totalItems; i++) {
|
||||||
autoAlpha: 1,
|
// 每个成就项入场,前一个淡出
|
||||||
x: 0,
|
if (i > 0) {
|
||||||
duration: 0.8,
|
tl.to(achievementElements.value[i - 1], {
|
||||||
ease: 'power3.out',
|
autoAlpha: 0, x: i % 2 === 0 ? 100 : -100,
|
||||||
|
duration: 0.5, ease: 'power2.in',
|
||||||
|
}, i)
|
||||||
|
}
|
||||||
|
tl.to(achievementElements.value[i], {
|
||||||
|
autoAlpha: 1, x: 0, duration: 0.8, ease: 'power3.out',
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
// 触发数字计数
|
|
||||||
const numberEl = numberElements.value[i]
|
const numberEl = numberElements.value[i]
|
||||||
if (numberEl) {
|
if (numberEl) animateNumber(numberEl, achievements[i].number, achievements[i].suffix)
|
||||||
animateNumber(numberEl, achievements[i].number, achievements[i].suffix)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, `item-${i}`)
|
}, i + 0.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollTrigger.create({
|
ScrollTrigger.create({
|
||||||
trigger: rootRef.value,
|
trigger: rootRef.value,
|
||||||
start: 'top top',
|
start: 'top top',
|
||||||
end: () => `+=${totalItems * window.innerHeight}`,
|
end: () => `+=${(totalItems + 1) * window.innerHeight}`,
|
||||||
pin: true,
|
pin: true,
|
||||||
scrub: true,
|
scrub: 0.5,
|
||||||
anticipatePin: 1,
|
anticipatePin: 1,
|
||||||
animation: timeline,
|
animation: tl,
|
||||||
})
|
})
|
||||||
}, rootRef.value)
|
}, rootRef.value)
|
||||||
|
|
||||||
|
|||||||
@@ -216,100 +216,57 @@ onMounted(async () => {
|
|||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
ctx = gsap.context(() => {
|
ctx = gsap.context(() => {
|
||||||
// 标题动画
|
// 初始状态
|
||||||
gsap.fromTo(headerRef.value,
|
gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
|
||||||
{ autoAlpha: 0, y: 50 },
|
stepElements.value.forEach((step) => {
|
||||||
{
|
|
||||||
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) => {
|
|
||||||
if (!step) return
|
if (!step) return
|
||||||
|
gsap.set(step, { autoAlpha: 0, y: 60, scale: 0.85 })
|
||||||
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)'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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) {
|
if (scrollContainerRef.value) {
|
||||||
const container = scrollContainerRef.value.querySelector('.flex')
|
const container = scrollContainerRef.value.querySelector('.flex')
|
||||||
if (container) {
|
if (container) {
|
||||||
const scrollWidth = container.scrollWidth - window.innerWidth
|
const scrollWidth = container.scrollWidth - window.innerWidth
|
||||||
|
|
||||||
gsap.to(container, {
|
// 逐个展示步骤
|
||||||
x: -scrollWidth,
|
stepElements.value.forEach((step, index) => {
|
||||||
ease: 'none',
|
if (!step) return
|
||||||
scrollTrigger: {
|
tl.to(step, {
|
||||||
trigger: rootRef.value,
|
autoAlpha: 1, y: 0, scale: 1,
|
||||||
start: 'top top',
|
duration: 0.6, ease: 'back.out(1.5)',
|
||||||
end: () => `+=${stepElements.value.length * window.innerHeight}`,
|
}, 1 + index * 0.3)
|
||||||
pin: true,
|
|
||||||
scrub: 1,
|
|
||||||
anticipatePin: 1,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 横向滚动
|
||||||
|
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)
|
}, rootRef.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -202,106 +202,70 @@ onMounted(async () => {
|
|||||||
if (!rootRef.value || !headerRef.value) return
|
if (!rootRef.value || !headerRef.value) return
|
||||||
|
|
||||||
ctx = gsap.context(() => {
|
ctx = gsap.context(() => {
|
||||||
// 标题动画
|
// 初始状态
|
||||||
gsap.fromTo(headerRef.value,
|
gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
|
||||||
{ autoAlpha: 0, y: 50 },
|
const positions = [
|
||||||
{
|
{ x: -200, y: -200, rotate: -15 },
|
||||||
scrollTrigger: {
|
{ x: 0, y: -200, rotate: 10 },
|
||||||
trigger: headerRef.value,
|
{ x: 200, y: -200, rotate: -15 },
|
||||||
start: 'top 80%',
|
{ x: -200, y: 200, rotate: 15 },
|
||||||
toggleActions: 'play none none reverse'
|
{ x: 0, y: 200, rotate: -10 },
|
||||||
},
|
{ x: 200, y: 200, rotate: 15 }
|
||||||
autoAlpha: 1,
|
]
|
||||||
y: 0,
|
|
||||||
duration: 1,
|
|
||||||
ease: 'power3.out'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 p = positions[index]
|
||||||
const positions = [
|
gsap.set(card, { autoAlpha: 0, x: p.x, y: p.y, scale: 0.8, rotateZ: p.rotate })
|
||||||
{ 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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 后续屏:每次聚焦一张卡片(放大),其他缩小
|
// 统一时间线
|
||||||
for (let focus = 0; focus < totalCards; focus++) {
|
const totalCards = services.length
|
||||||
timeline.addLabel(`focus-${focus}`, focus + 1)
|
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) => {
|
cardElements.value.forEach((card, index) => {
|
||||||
if (!card) return
|
if (!card) return
|
||||||
|
tl.to(card, {
|
||||||
timeline.to(card, {
|
|
||||||
scale: index === focus ? 1.05 : 0.85,
|
scale: index === focus ? 1.05 : 0.85,
|
||||||
autoAlpha: index === focus ? 1 : 0.4,
|
autoAlpha: index === focus ? 1 : 0.4,
|
||||||
zIndex: index === focus ? 10 : 1,
|
duration: 0.6, ease: 'power2.out',
|
||||||
duration: 0.6,
|
}, pos)
|
||||||
ease: 'power2.out',
|
|
||||||
}, `focus-${focus}`)
|
|
||||||
|
|
||||||
// 聚焦的卡片翻转展示背面
|
|
||||||
if (index === focus) {
|
if (index === focus) {
|
||||||
timeline.call(() => handleCardHover(focus, true), [], `focus-${focus}`)
|
tl.call(() => handleCardHover(focus, true), [], pos)
|
||||||
} else {
|
} 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) => {
|
cardElements.value.forEach((card, index) => {
|
||||||
if (!card) return
|
if (!card) return
|
||||||
timeline.to(card, {
|
tl.to(card, { scale: 1, autoAlpha: 1, duration: 0.5, ease: 'power2.out' }, resetPos)
|
||||||
scale: 1,
|
tl.call(() => handleCardHover(index, false), [], resetPos)
|
||||||
autoAlpha: 1,
|
|
||||||
zIndex: 1,
|
|
||||||
duration: 0.5,
|
|
||||||
ease: 'power2.out',
|
|
||||||
}, 'reset')
|
|
||||||
timeline.call(() => handleCardHover(index, false), [], 'reset')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ScrollTrigger.create({
|
ScrollTrigger.create({
|
||||||
trigger: rootRef.value,
|
trigger: rootRef.value,
|
||||||
start: 'top top',
|
start: 'top top',
|
||||||
end: () => `+=${(totalCards + 2) * window.innerHeight}`,
|
end: () => `+=${(totalCards + 3) * window.innerHeight}`,
|
||||||
pin: true,
|
pin: true,
|
||||||
scrub: true,
|
scrub: 0.5,
|
||||||
anticipatePin: 1,
|
anticipatePin: 1,
|
||||||
animation: timeline,
|
animation: tl,
|
||||||
})
|
})
|
||||||
}, rootRef.value)
|
}, rootRef.value)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -193,61 +193,38 @@ onMounted(async () => {
|
|||||||
if (!rootRef.value || !contentRef.value) return
|
if (!rootRef.value || !contentRef.value) return
|
||||||
|
|
||||||
ctx = gsap.context(() => {
|
ctx = gsap.context(() => {
|
||||||
// 右侧内容动画
|
// 初始状态
|
||||||
gsap.fromTo(contentRef.value,
|
gsap.set(contentRef.value, { autoAlpha: 0, x: 100 })
|
||||||
{ autoAlpha: 0, x: 100 },
|
techElements.value.forEach((el) => {
|
||||||
{
|
if (!el) return
|
||||||
scrollTrigger: {
|
gsap.set(el, { autoAlpha: 0, scale: 0 })
|
||||||
trigger: contentRef.value,
|
})
|
||||||
start: 'top 80%',
|
|
||||||
toggleActions: 'play none none reverse'
|
|
||||||
},
|
|
||||||
autoAlpha: 1,
|
|
||||||
x: 0,
|
|
||||||
duration: 1,
|
|
||||||
ease: 'power3.out'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 技术标签从中心爆炸散开
|
// 统一时间线:入场 + pin 旋转
|
||||||
|
const tl = gsap.timeline()
|
||||||
|
|
||||||
|
// 第一屏:内容入场 + 技术标签散开
|
||||||
|
tl.to(contentRef.value, { autoAlpha: 1, x: 0, duration: 0.8, ease: 'power3.out' }, 0)
|
||||||
techElements.value.forEach((el, index) => {
|
techElements.value.forEach((el, index) => {
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
tl.to(el, {
|
||||||
gsap.fromTo(el,
|
autoAlpha: 1, scale: 1, duration: 0.8, ease: 'back.out(2)',
|
||||||
{ autoAlpha: 0, scale: 0 },
|
}, 0.3 + index * 0.05)
|
||||||
{
|
|
||||||
scrollTrigger: {
|
|
||||||
trigger: sphereRef.value,
|
|
||||||
start: 'top 75%',
|
|
||||||
toggleActions: 'play none none reverse'
|
|
||||||
},
|
|
||||||
autoAlpha: 1,
|
|
||||||
scale: 1,
|
|
||||||
duration: 1,
|
|
||||||
delay: index * 0.05,
|
|
||||||
ease: 'back.out(2)'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pin 滚动效果:固定板块,球体旋转一圈展示所有技术
|
// 后续屏:球体旋转一圈
|
||||||
const rotationTimeline = gsap.timeline()
|
tl.to(autoRotation.value, {
|
||||||
|
y: 360, duration: 3, ease: 'none', onUpdate: updatePositions,
|
||||||
rotationTimeline.to(autoRotation.value, {
|
}, 1.5)
|
||||||
y: 360,
|
|
||||||
duration: 3,
|
|
||||||
ease: 'none',
|
|
||||||
onUpdate: updatePositions
|
|
||||||
})
|
|
||||||
|
|
||||||
ScrollTrigger.create({
|
ScrollTrigger.create({
|
||||||
trigger: rootRef.value,
|
trigger: rootRef.value,
|
||||||
start: 'top top',
|
start: 'top top',
|
||||||
end: () => `+=${window.innerHeight * 2}`,
|
end: () => `+=${window.innerHeight * 3}`,
|
||||||
pin: true,
|
pin: true,
|
||||||
scrub: 1,
|
scrub: 1,
|
||||||
anticipatePin: 1,
|
anticipatePin: 1,
|
||||||
animation: rotationTimeline,
|
animation: tl,
|
||||||
})
|
})
|
||||||
}, rootRef.value)
|
}, rootRef.value)
|
||||||
|
|
||||||
|
|||||||
@@ -226,58 +226,34 @@ onMounted(async () => {
|
|||||||
if (!rootRef.value || !headerRef.value) return
|
if (!rootRef.value || !headerRef.value) return
|
||||||
|
|
||||||
ctx = gsap.context(() => {
|
ctx = gsap.context(() => {
|
||||||
// 标题动画
|
// 初始状态:标题隐藏,卡片居中缩小
|
||||||
gsap.fromTo(headerRef.value,
|
gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
|
||||||
{ 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'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 卡片从中心扩散入场
|
|
||||||
cardElements.value.forEach((card, index) => {
|
cardElements.value.forEach((card, index) => {
|
||||||
if (!card) return
|
if (!card) return
|
||||||
|
gsap.set(card, { autoAlpha: 0, scale: 0.5 })
|
||||||
gsap.fromTo(card,
|
|
||||||
{ autoAlpha: 0, scale: 0.5 },
|
|
||||||
{
|
|
||||||
scrollTrigger: {
|
|
||||||
trigger: rootRef.value,
|
|
||||||
start: 'top 70%',
|
|
||||||
toggleActions: 'play none none reverse'
|
|
||||||
},
|
|
||||||
autoAlpha: 1,
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.8,
|
|
||||||
delay: index * 0.1,
|
|
||||||
ease: 'back.out(1.5)',
|
|
||||||
onComplete: () => {
|
|
||||||
if (index === cardElements.value.length - 1) {
|
|
||||||
updateCards()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pin 滚动效果:固定板块,滚动切换卡片
|
// 统一时间线:入场 + pin 滚动切换
|
||||||
const totalCards = testimonials.length
|
const totalCards = testimonials.length
|
||||||
const timeline = gsap.timeline()
|
const tl = gsap.timeline()
|
||||||
|
|
||||||
// 为每张卡片创建切换动画(每屏切换一张)
|
// 第一屏:标题入场 + 第一张卡片出现
|
||||||
for (let i = 0; i < totalCards - 1; i++) {
|
tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0)
|
||||||
timeline.to({}, { duration: 1 }, i)
|
tl.to(cardElements.value[0], { autoAlpha: 1, scale: 1, duration: 0.8, ease: 'back.out(1.5)' }, 0.3)
|
||||||
timeline.call(() => {
|
tl.call(() => { currentIndex.value = 0 }, [], 0.3)
|
||||||
goToSlide(i + 1)
|
|
||||||
}, [], i + 0.5)
|
// 后续屏:滚动切换卡片,每屏切一张
|
||||||
|
for (let i = 1; i < totalCards; i++) {
|
||||||
|
const pos = i
|
||||||
|
// 当前卡片缩小淡出
|
||||||
|
tl.to(cardElements.value[i - 1], {
|
||||||
|
autoAlpha: 0, scale: 0.5, duration: 0.5, ease: 'power2.in'
|
||||||
|
}, pos)
|
||||||
|
// 新卡片放大淡入
|
||||||
|
tl.to(cardElements.value[i], {
|
||||||
|
autoAlpha: 1, scale: 1, duration: 0.6, ease: 'back.out(1.5)'
|
||||||
|
}, pos + 0.3)
|
||||||
|
tl.call(() => { currentIndex.value = i }, [], pos + 0.3)
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollTrigger.create({
|
ScrollTrigger.create({
|
||||||
@@ -285,9 +261,9 @@ onMounted(async () => {
|
|||||||
start: 'top top',
|
start: 'top top',
|
||||||
end: () => `+=${totalCards * window.innerHeight}`,
|
end: () => `+=${totalCards * window.innerHeight}`,
|
||||||
pin: true,
|
pin: true,
|
||||||
scrub: true,
|
scrub: 0.5,
|
||||||
anticipatePin: 1,
|
anticipatePin: 1,
|
||||||
animation: timeline,
|
animation: tl,
|
||||||
})
|
})
|
||||||
}, rootRef.value)
|
}, rootRef.value)
|
||||||
|
|
||||||
|
|||||||
@@ -164,63 +164,45 @@ onMounted(async () => {
|
|||||||
if (!rootRef.value || !headerRef.value) return
|
if (!rootRef.value || !headerRef.value) return
|
||||||
|
|
||||||
ctx = gsap.context(() => {
|
ctx = gsap.context(() => {
|
||||||
// 标题动画
|
// 初始状态
|
||||||
gsap.fromTo(headerRef.value,
|
gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
|
||||||
{ 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 = updates.length
|
|
||||||
const timeline = gsap.timeline()
|
|
||||||
|
|
||||||
// 初始隐藏所有卡片
|
|
||||||
cardElements.value.forEach((card) => {
|
cardElements.value.forEach((card) => {
|
||||||
if (!card) return
|
if (!card) return
|
||||||
gsap.set(card, { autoAlpha: 0, y: 60, scale: 0.9 })
|
gsap.set(card, { autoAlpha: 0, y: 60, scale: 0.9 })
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分批展示:每批展示一行(按顺序,每行 2-3 个)
|
// 统一时间线
|
||||||
const batchSize = 3
|
const tl = gsap.timeline()
|
||||||
|
|
||||||
|
// 第一屏:标题入场
|
||||||
|
tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0)
|
||||||
|
|
||||||
|
// 分批展示卡片:每批 2 个,逐批出现
|
||||||
|
const totalCards = updates.length
|
||||||
|
const batchSize = 2
|
||||||
const batches = Math.ceil(totalCards / batchSize)
|
const batches = Math.ceil(totalCards / batchSize)
|
||||||
|
|
||||||
for (let batch = 0; batch < batches; batch++) {
|
for (let batch = 0; batch < batches; batch++) {
|
||||||
timeline.addLabel(`batch-${batch}`)
|
const pos = 0.8 + batch
|
||||||
|
|
||||||
const start = batch * batchSize
|
const start = batch * batchSize
|
||||||
const end = Math.min(start + batchSize, totalCards)
|
const end = Math.min(start + batchSize, totalCards)
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
if (!cardElements.value[i]) continue
|
tl.to(cardElements.value[i], {
|
||||||
|
autoAlpha: 1, y: 0, scale: 1,
|
||||||
timeline.to(cardElements.value[i], {
|
duration: 0.7, ease: 'power3.out',
|
||||||
autoAlpha: 1,
|
}, pos + (i - start) * 0.12)
|
||||||
y: 0,
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.8,
|
|
||||||
ease: 'power3.out',
|
|
||||||
}, `batch-${batch}+=${(i - start) * 0.15}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollTrigger.create({
|
ScrollTrigger.create({
|
||||||
trigger: rootRef.value,
|
trigger: rootRef.value,
|
||||||
start: 'top top',
|
start: 'top top',
|
||||||
end: () => `+=${batches * window.innerHeight}`,
|
end: () => `+=${(batches + 1) * window.innerHeight}`,
|
||||||
pin: true,
|
pin: true,
|
||||||
scrub: true,
|
scrub: 0.5,
|
||||||
anticipatePin: 1,
|
anticipatePin: 1,
|
||||||
animation: timeline,
|
animation: tl,
|
||||||
})
|
})
|
||||||
}, rootRef.value)
|
}, rootRef.value)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user