实现类似参考网站的平滑滚动追赶动画效果(JavaScript)
嘿,我完全懂你想要的那种滚动质感——就是页面滚动起来像丝滑的流体,元素动画会慢悠悠“追赶”你的滚动节奏,而不是硬邦邦地瞬间到位对吧?你提到的scroll-behavior: smooth确实只能搞定锚点跳转的平滑,完全做不出这种带物理缓动的“追赶”效果。结合你贴的代码,我来帮你改造出这个效果~
核心原理拆解
这种“追赶式”丝滑滚动,本质是模拟物理惯性的缓动动画:我们不直接用浏览器原生的即时滚动,而是自定义两个变量:
targetScrollY:用户想要滚动到的目标位置(由滚轮/触摸事件触发更新)currentScrollY:页面当前的实际滚动位置(通过requestAnimationFrame逐帧向targetScrollY靠近,中间加入缓动系数)
通过这种“追近”的逻辑,就会产生元素慢慢跟上滚动节奏的丝滑感,同时你的图片切割动画也会跟着这个缓动后的滚动位置变化,不再生硬。
代码改造步骤
我会基于你现有的代码结构,一步步调整出效果:
1. 先调整CSS:禁用原生滚动
首先要阻止浏览器的原生滚动,避免和我们自定义的滚动逻辑冲突:
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Unica One', sans-serif; color: #111; z-index: 1; } body { overflow: hidden; /* 禁用原生滚动,完全由JS控制 */ height: 100vh; } header { width: 100%; height: 100vh; background-image: url("Images/Banner(1).png"); background-position: center; background-repeat: no-repeat; background-size: cover; } .container { display: flex; align-items: center; justify-content: center; height: 100%; flex-wrap: wrap; position: absolute; } .parallax { min-height: 1000px; background-position: center; background-repeat: no-repeat; background-size: cover; position: relative; } .parallax.subTopic::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); } .parallax h1 { position: absolute; color: white; } /* 把主内容容器设为相对定位,我们通过transform移动它来模拟滚动 */ .pagecontainer { position: relative; width: 100%; height: 100vh; overflow: visible; }
2. 重写JavaScript:核心滚动逻辑+适配你的现有功能
这部分是重点,我们要把原来依赖原生scrollY的逻辑,改成基于自定义缓动滚动位置的逻辑:
// 核心滚动变量:目标位置、当前位置、缓动系数(越小追赶越慢,0.1-0.2最自然) let targetScrollY = 0; let currentScrollY = 0; const friction = 0.15; // 页面元素引用 const historyContent = document.getElementsByClassName("historyLayout"); const parallaxContent = document.querySelectorAll('section .parallax'); const pageContainer = document.querySelector('.pagecontainer'); const navLinks = document.querySelectorAll('.nav-link'); // 计算页面总高度(用于限制滚动边界,避免滚出页面) function getTotalPageHeight() { return Math.max( document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight ); } // 监听滚轮事件:只更新目标滚动位置,不直接滚动 window.addEventListener('wheel', (e) => { e.preventDefault(); // 阻止原生滚动 const delta = e.deltaY; // 根据滚轮方向调整目标位置(向下滚deltaY为正,所以加delta) targetScrollY += delta; // 限制滚动边界:不能小于0,不能超过页面总高度-视口高度 const maxScroll = getTotalPageHeight() - window.innerHeight; targetScrollY = Math.max(0, Math.min(targetScrollY, maxScroll)); }, { passive: false }); // 适配锚点链接:点击导航时更新目标滚动位置 navLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('href'); const targetElement = document.querySelector(targetId); if (targetElement) { targetScrollY = targetElement.offsetTop; // 同样限制边界 const maxScroll = getTotalPageHeight() - window.innerHeight; targetScrollY = Math.max(0, Math.min(targetScrollY, maxScroll)); } }); }); // 你的图片切割函数:修改为依赖自定义的currentScrollY let cutcounter = 0; function cuttingImages() { let section1 = historyContent[cutcounter]; if (!section1) return; let parallax2 = parallaxContent[cutcounter + 1]; let section2 = historyContent[cutcounter + 1]; if (!parallax2 || !section2) return; // 基于自定义滚动位置计算元素相对位置 const image1Top = section1.offsetTop - currentScrollY; const section2Top = parallax2.offsetTop - currentScrollY; let progress = (image1Top - section2Top) / -966; progress = Math.max(0, Math.min(1, progress)); section1.style.clipPath = `inset(0 0 ${(100 - progress * 100)}% 0)`; section2.style.clipPath = `inset(${progress * 100}% 0 0 0)`; // 原有的计数器逻辑保留 if (progress === 0 && cutcounter === 0) { cutcounter++; } if (progress === 1 && cutcounter > 0) { cutcounter--; } } // 你的历史布局固定定位逻辑:适配currentScrollY function updateHistoryLayoutPosition() { if (historyContent.length === 0) return; const prehistoricContainer = historyContent[0]; const containerRect = prehistoricContainer.getBoundingClientRect(); const containerMiddle = (containerRect.top - containerRect.bottom) / 2; const containerHalf = containerRect.top - containerMiddle; if (containerHalf <= window.innerHeight / 2) { for (const content of historyContent) { content.style.position = "fixed"; content.style.top = "0"; content.style.left = "0"; content.style.right = "0"; content.style.bottom = "0"; if ((currentScrollY - containerHalf - 1000) <= window.innerHeight / 2) { content.style.position = "absolute"; } } } } // 逐帧更新的核心动画循环 function animateScroll() { // 让当前位置向目标位置缓动靠近 currentScrollY += (targetScrollY - currentScrollY) * friction; // 当差距极小时直接对齐,避免无限接近 if (Math.abs(targetScrollY - currentScrollY) < 0.1) { currentScrollY = targetScrollY; } // 通过transform移动页面容器(GPU加速,更丝滑) pageContainer.style.transform = `translateY(${-currentScrollY}px)`; // 同步更新你的图片切割和布局定位 updateHistoryLayoutPosition(); cuttingImages(); // 持续下一帧动画 requestAnimationFrame(animateScroll); } // 启动动画循环 requestAnimationFrame(animateScroll);
关键细节说明
为什么用
transform而不是scrollTop?
transform的变化会触发GPU硬件加速,比直接修改scrollTop更丝滑,减少页面卡顿,尤其是在有大量动画元素的时候。缓动系数
friction怎么调?- 0.1:滚动追赶速度慢,粘性强,适合偏文艺的慢节奏页面
- 0.15:平衡型,接近你参考网站的感觉
- 0.2:追赶速度快,丝滑但粘性弱,适合偏活泼的页面
触摸设备适配(可选)
如果要支持移动端触摸滚动,只需要监听touchstart、touchmove、touchend事件,记录触摸起始位置和移动距离,然后更新targetScrollY,逻辑和滚轮事件类似,你可以自己扩展。
最后
这样修改之后,你应该就能得到那种丝滑的滚动追赶效果了,你的图片切割动画也会跟着滚动节奏慢慢变化,不再生硬。你可以根据自己的页面风格,调整缓动系数、边界计算这些参数,直到达到你想要的质感~
内容来源于stack exchange

