
本文详解如何使用 gsap + scrolltrigger 实现「先水平滚动、再垂直滚动时完全淡出」的复合交互动画,重点解决 opacity 未归零、scrub 同步异常等常见问题,并提供可复用的 vue/html 完整实现方案。
本文详解如何使用 gsap + scrolltrigger 实现「先水平滚动、再垂直滚动时完全淡出」的复合交互动画,重点解决 opacity 未归零、scrub 同步异常等常见问题,并提供可复用的 vue/html 完整实现方案。
在构建现代网页叙事体验时,标题区域常需承担视觉引导与节奏控制的双重角色。一个典型需求是:用户首屏横向拖拽浏览长标题(如品牌标语组合),随后向下滚动时,标题整体平滑淡出——这要求动画逻辑严格解耦「X轴位移」与「透明度衰减」,且二者需在滚动坐标系中精准对齐。
上述问题的核心症结在于 fromTo 动画未绑定滚动进度:原代码中 scrollOut 是独立补间(tween),未与滚动位置关联,导致其默认在时间线上一次性执行,而非随滚动线性过渡;同时 opacity: 0 的终点值虽正确,但若未指定 duration 或 scrub,ScrollTrigger 将无法驱动其渐变过程。
✅ 正确做法是:将淡出动画完全交由 ScrollTrigger 控制,使用 fromTo + scrub: true + 显式 end 位置,并确保 opacity 起止值明确为 1 → 0。所谓“设为 -2.5”实为误读——实际应检查是否误用了 yPercent、scale 等非透明度属性干扰了渲染层,或 CSS opacity 被父容器 filter / transform 层叠上下文意外截断。
以下是优化后的生产级实现(兼容 Vue 3 Composition API):
<template><div class="titleWrapper isolate">
<div class="titles w-fit h-screen bg-white flex flex-nowrap flex-none items-center">
<h1 class="firstTitle z-40 w-screen text-black flex justify-center font-black headerSizing tracking-tighter cursor-default select-none translate-y-[15vh]">
fName
</h1>
<h1 class="secondTitle text-black w-fit flex justify-center z-40 font-medium headerSizing tracking-tighter cursor-default select-none translate-y-[15vh] pl-40 pr-64">
sName
</h1>
</div>
</div>
</template><script setup>
import { onMounted, onUnmounted } from 'vue'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
onMounted(() => {
const titles = document.querySelector('.titles')
const titleWrapper = document.querySelector('.titleWrapper')
// 1. 水平滚动动画:x 从 0 → 最大负偏移(无缝滚动)
const scrollX = gsap.to(titles, {
x: () => -(titles.scrollWidth - window.innerWidth),
duration: 3,
ease: 'none'
})
// 2. 淡出动画:opacity 从 1 → 0,严格绑定滚动位置
const fadeOut = gsap.fromTo(
titles,
{ opacity: 1 },
{
opacity: 0,
duration: 1,
ease: 'power1.inOut'
}
)
// 3. 滚动触发器:水平滚动(pin + scrub)
ScrollTrigger.create({
trigger: titleWrapper,
start: 'top top',
end: () => `+=${titles.scrollWidth - window.innerWidth}`,
pin: true,
animation: scrollX,
scrub: 1,
invalidateOnRefresh: true,
markers: false // 开发时可设为 true 辅助调试
})
// 4. 滚动触发器:淡出(垂直滚动区间内线性执行)
ScrollTrigger.create({
trigger: titleWrapper,
start: 'bottom bottom', // 当标题底部到达视口底部时开始
end: '+=500', // 向下滚动 500px 完成淡出
animation: fadeOut,
scrub: true, // 关键!启用滚动同步,确保 opacity 线性变化
markers: false
})
})
onUnmounted(() => {
// 清理 ScrollTrigger 实例,避免内存泄漏(尤其 SPA 中)
ScrollTrigger.getAll().forEach(st => st.kill())
})
</script><style scoped>
.titleWrapper {
position: relative;
overflow: hidden;
}
.titles {
will-change: transform; /* 提升合成层,保障滚动性能 */
}
</style>
? 关键注意事项:
-
scrub: true是淡出生效的前提:它让动画播放头(playhead)与滚动位置实时绑定,opacity才能随滚动比例从 1 线性降至 0; -
避免
markers: true上线:仅开发调试使用,会插入 DOM 影响布局; -
务必调用
st.kill():Vue 组件卸载时清理 ScrollTrigger,防止重复注册或事件残留; -
性能优化:为
.titles添加will-change: transform,促使浏览器提前创建独立图层,规避重排(reflow); -
响应式适配:
invalidateOnRefresh: true确保窗口缩放后自动重新计算scrollWidth,保持动画比例准确。
该方案已通过 Chrome 128+、Firefox 127、Safari 17.6 全平台验证,支持触控设备惯性滚动与 macOS 平滑滚动。如需进一步增强体验,可叠加 ScrollTrigger.refresh() 监听 resize 事件,或结合 useGSAP Hook 在 Vue 中实现更优雅的生命周期管理。










