页面滚动时全屏区块淡出淡入的Vanilla JS数学逻辑问题
解决方案:基于滚动位置的区块淡入淡出逻辑
原代码的问题在于透明度计算未针对每个区块的滚动区间做精准判断,导致多区块过渡时逻辑混乱。以下是修正后的实现,完全基于滚动Y值计算透明度,无滚动劫持:
修正后的JavaScript代码
(function () { const viewportHeight = window.innerHeight; const headerHeight = document.querySelector('header').offsetHeight; const rows = document.querySelectorAll('.row'); // 初始化第一个区块的透明度 rows[0].style.opacity = 1; window.addEventListener('scroll', () => { const scrollY = window.scrollY; // 修正滚动位置,抵消fixed header的遮挡影响 const adjustedScrollY = scrollY + headerHeight; rows.forEach((row, index) => { // 计算当前区块的可视区间:从(index * 视口高度)到((index+1)*视口高度) const sectionStart = index * viewportHeight; const sectionEnd = (index + 1) * viewportHeight; let opacity = 0; // 1. 当前区块进入视野的淡入阶段 if (adjustedScrollY > sectionStart - viewportHeight && adjustedScrollY <= sectionStart) { opacity = (adjustedScrollY - (sectionStart - viewportHeight)) / viewportHeight; } // 2. 当前区块完全处于视野内 else if (adjustedScrollY > sectionStart && adjustedScrollY <= sectionEnd) { opacity = 1; } // 3. 当前区块离开视野的淡出阶段 else if (adjustedScrollY > sectionEnd && adjustedScrollY <= sectionEnd + viewportHeight) { opacity = 1 - ((adjustedScrollY - sectionEnd) / viewportHeight); } // 确保透明度始终在0-1的有效范围 opacity = Math.max(0, Math.min(1, opacity)); row.style.opacity = opacity; }); }); })();
关键逻辑说明
- 视口基准与偏移修正:以
window.innerHeight作为每个区块的高度基准,同时修正滚动位置抵消fixed header的遮挡,保证计算精准。 - 三段式透明度计算:
- 淡入阶段:滚动位置进入当前区块的前一个视口区间时,透明度从0线性提升到1
- 完全可见:滚动位置在当前区块的视口范围内时,保持透明度为1
- 淡出阶段:滚动位置离开当前区块进入下一个视口区间时,透明度从1线性降到0
- 边界值控制:通过
Math.max(0, Math.min(1, opacity))避免透明度出现负数或大于1的异常值。
配套CSS调整(可选)
为了让过渡更平滑,给.row添加过渡动画:
.row { opacity: 0; height: 100vh; display: flex; flex-direction:column; transition: opacity 0.2s ease-out; /* 添加平滑过渡 */ }
同时调整.row-inner的内边距,避免内容被fixed header遮挡:
.row-inner { display: grid; grid-template-columns: 1fr 1fr; position: sticky; top: 0; padding: 0; height: 100vh; padding-top: calc(4em + 20px); /* 适配header高度 */ }
完整代码示例
HTML
<!DOCTYPE html> <html> <head> <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Manrope&family=Unbounded&family=Lack&display=swap" rel="stylesheet"> </head> <body> <header> <h1>My App</h1> </header> <!-- First row --> <div class="row-container"> <div class="row section-1"> <div class="row-inner"> <div class="column left-column"> <div class="column-inner"> <h1>Headline 1</h1> <h2>Subhead 1</h2> <p>Lorem Ipsum dolar Gamet</p> </div> </div> <div class="column right-column"> <div class="column-inner"> <img src="https://avatars.dicebear.com/api/adventurer/team-member-1.jpg" alt="Image 1"> </div> </div> </div> </div> <!-- Second row --> <div class="row section-2"> <div class="row-inner"> <div class="column left-column"> <div class="column-inner"> <h1>Headline 2</h1> <h2>Subhead 2</h2> <p>Lorem Ipsum dolar Gamet</p> </div> </div> <div class="column right-column"> <div class="column-inner"> <img src="https://avatars.dicebear.com/api/adventurer/team-member-2.jpg" alt="Image 2"> </div> </div> </div> </div> <!-- Third row --> <div class="row section-3"> <div class="row-inner"> <div class="column left-column"> <div class="column-inner"> <h1>Headline 3</h1> <h2>Subhead 3</h2> <p>Lorem Ipsum dolar Gamet</p> </div> </div> <div class="column right-column"> <div class="column-inner"> <img src="https://avatars.dicebear.com/api/adventurer/team-member-3.jpg" alt="Image 3"> </div> </div> </div> </div> <!-- Fourth row --> <div class="row section-4"> <div class="row-inner"> <div class="column left-column"> <div class="column-inner"> <h1>Headline 4</h1> <h2>Subhead 4</h2> <p>Lorem Ipsum dolar Gamet</p> </div> </div> <div class="column right-column"> <div class="column-inner"> <img src="https://avatars.dicebear.com/api/adventurer/team-member-4.jpg" alt="Image 4"> </div> </div> </div> </div> </div> </body> </html>
CSS
* { box-sizing: border-box; } body { margin: 0; padding: 0; font-family: 'Manrope', helvetiva, sans-serif; transition: background-color 1s; color:#fff; background: linear-gradient(0deg, #100521 0%, #50357c 50%, #878290 100%); } header { position: fixed; top: 0; width: 100%; background: #fff; z-index: 999; } header h1 { font-family: 'Lack'; font-size: 4em; text-align: center; padding: 10px; color: #100521; } .row { opacity: 0; height: 100vh; display: flex; flex-direction:column; transition: opacity 0.2s ease-out; } .row-inner { display: grid; grid-template-columns: 1fr 1fr; position: sticky; top: 0; padding: 0; height: 100vh; padding-top: calc(4em + 20px); } .column { display: flex; flex-direction: column; justify-content: center; align-content: center; flex: 50%; padding: 10px; } .left-column { background-color: transparent; } .right-column { background-color: transparent; } h1 { font-family: 'Unbounded', helvetica, sans-serif; } h1 { margin: 0; font-size: 36px; } h2 { margin: 0; font-size: 24px; } p { margin: 10px 0; font-size: 18px; line-height: 1.5; } img { width: 100%; height: auto; }
JavaScript
(function () { const viewportHeight = window.innerHeight; const headerHeight = document.querySelector('header').offsetHeight; const rows = document.querySelectorAll('.row'); // 初始化第一个区块的透明度 rows[0].style.opacity = 1; window.addEventListener('scroll', () => { const scrollY = window.scrollY; // 修正滚动位置,抵消fixed header的遮挡影响 const adjustedScrollY = scrollY + headerHeight; rows.forEach((row, index) => { // 计算当前区块的可视区间:从(index * 视口高度)到((index+1)*视口高度) const sectionStart = index * viewportHeight; const sectionEnd = (index + 1) * viewportHeight; let opacity = 0; // 1. 当前区块进入视野的淡入阶段 if (adjustedScrollY > sectionStart - viewportHeight && adjustedScrollY <= sectionStart) { opacity = (adjustedScrollY - (sectionStart - viewportHeight)) / viewportHeight; } // 2. 当前区块完全处于视野内 else if (adjustedScrollY > sectionStart && adjustedScrollY <= sectionEnd) { opacity = 1; } // 3. 当前区块离开视野的淡出阶段 else if (adjustedScrollY > sectionEnd && adjustedScrollY <= sectionEnd + viewportHeight) { opacity = 1 - ((adjustedScrollY - sectionEnd) / viewportHeight); } // 确保透明度始终在0-1的有效范围 opacity = Math.max(0, Math.min(1, opacity)); row.style.opacity = opacity; }); }); })();
内容的提问来源于stack exchange,提问作者Oscar Godson
相关产品推荐
相关产品推荐

