如何使用GSAP ScrollTrigger优化堆叠面板的平滑固定启动、滚动过渡及粘性头部表现
Hey 👋,我刚好做过类似的滚动堆叠面板需求,结合你给出的代码,咱们来一步步优化平滑固定启动、滚动过渡的流畅度,还有粘性头部的配合逻辑,让整个交互更丝滑:
一、优化固定启动的平滑度
你已经用到了anticipatePin: 1,这是个好起点,但还可以从这几个方向调整:
微调触发位置,让固定时机更自然
原代码的start: "center-=30% 50%"可以根据实际视觉需求调整,比如改成start: "top+=100px 50%",让面板刚进入视口上方一点就开始准备固定,避免突兀的固定动作。给固定动作添加缓动过渡
不要直接依赖ScrollTrigger默认的pin行为,而是配合GSAP的tween来控制面板进入时的透明度和位置过渡,替换原来生硬的gsap.set为带缓动的动画。优化scrub值,平衡平滑度和响应性
原代码的scrub: 2偏慢,改成scrub: 0.6会让滚动和动画的响应更同步,同时保持平滑感(可以根据需求在0.3-1之间调整)。
二、优化面板滚动过渡的流畅性
堆叠面板的显隐和层级管理很关键,这里做几个优化:
给面板显隐添加淡入淡出动画
把原来直接切换透明度的gsap.set改成带缓动的淡入淡出,避免生硬的视觉跳变:gsap.to(panel, { autoAlpha: 1, duration: 0.5, ease: "power2.out" });动态管理z-index,确保堆叠层级正确
取消注释你原来的panel.style.zIndex =${100 - index};,或者在ScrollTrigger的回调里根据激活状态调整层级,让当前固定的面板始终在最上层,避免被其他面板遮挡。
三、优化粘性头部的联动逻辑
粘性头部和ScrollTrigger的配合要避免setTimeout的临时 hack,改用ScrollTrigger的原生回调来精准控制:
移除setTimeout,用原生refresh回调
原代码里的延迟刷新可以替换成在ScrollTrigger创建完成后直接调用ScrollTrigger.refresh(),如果有异步加载的内容,可以在内容加载完成后再调用,确保布局稳定后计算滚动位置。精准控制粘性头部的显示/隐藏
在最后一个面板的回调里,用GSAP的tween控制头部的位移或透明度,而不是直接切换class,让过渡更平滑:gsap.to(stickyHeader, { y: -headerHeight, duration: 0.4, ease: "power2.in" });
四、完整优化后的代码
结合上面的所有优化,修改后的完整代码如下:
ngAfterViewInit(): void { function updateActiveButton(activeIndex: number) { buttons.forEach((btn) => { const index = Number(btn.getAttribute('data-index')); btn.classList.toggle('active', index === activeIndex); }); } gsap.registerPlugin(ScrollTrigger, ScrollToPlugin); const panels = gsap.utils.toArray<HTMLElement>(".panel"); const buttons = document.querySelectorAll('.button-group button'); // menu buttons const header = document.querySelector('.products-header-panel') as HTMLElement; const stickyHeader = document.querySelector(".products-header-panel"); const headerHeight = header?.offsetHeight || 0; panels.forEach((panel, index) => { const isLast = index === panels.length - 1; // 初始化层级,确保面板堆叠顺序正确 panel.style.zIndex = `${90 - index}`; ScrollTrigger.create({ id: `panel-${index}`, // unique ID for each panel trigger: panel, // 调整触发位置,让固定动作更提前启动 start: "top+=100px 50%", end: isLast ? "bottom bottom" : `+=${panel.offsetHeight}`, // 优化scrub值,平衡平滑度和滚动响应速度 scrub: 0.6, pin: panel, pinSpacing: false, // markers: true, // 调试时可以打开查看触发范围 // 增强固定前的预判动画,让启动更丝滑 anticipatePin: 1.2, onEnter: () => { // 淡入当前面板并置顶层级 gsap.to(panel, { autoAlpha: 1, duration: 0.5, ease: "power2.out" }); panel.style.zIndex = "100"; updateActiveButton(index); }, onLeave: () => { // 淡出面版并调整层级 gsap.to(panel, { autoAlpha: 0, duration: 0.3, ease: "power2.in" }); panel.style.zIndex = `${90 - index}`; if (isLast && stickyHeader) { // 平滑隐藏粘性头部 gsap.to(stickyHeader, { y: -headerHeight, duration: 0.4, ease: "power2.in" }); // 最后一个面板离开时保持可见 gsap.set(panel, { autoAlpha: 1 }); } }, onEnterBack: () => { // 淡入当前面板并置顶层级 gsap.to(panel, { autoAlpha: 1, duration: 0.5, ease: "power2.out" }); panel.style.zIndex = "100"; updateActiveButton(index); if (isLast && stickyHeader) { // 平滑显示粘性头部 gsap.to(stickyHeader, { y: 0, duration: 0.4, ease: "power2.out" }); } }, }); }); // Scroll to panel on button click buttons.forEach((btn, i) => { btn.addEventListener("click", () => { const targetPanel = panels[i]; if (!targetPanel) return; // Find trigger by ID const trigger = ScrollTrigger.getById(`panel-${i}`); if (!trigger) return; // 滚动到面板起始位置,适配头部高度避免遮挡 const scrollToY = trigger.start - headerHeight; gsap.to(window, { duration: 0.6, scrollTo: { y: scrollToY }, ease: "power2.out" }); }); }); // 确保布局稳定后刷新ScrollTrigger ScrollTrigger.refresh(); }
额外小技巧
- 如果你想让固定时面板有轻微的视觉层次感,可以在
onEnter里添加scale: 1.02的动画,然后在onLeave里恢复scale: 1。 - 打开
markers: true可以直观看到ScrollTrigger的触发范围,方便微调start和end的位置。
内容来源于stack exchange

