JavaScript滚动文本透明度:实现0→1→0的渐变效果
问题:AirPods滚动动画中文本无法实现双向淡入淡出
模仿苹果AirPods官网滚动动画时,文本渲染功能仅能实现突然出现后淡出,无法完成透明度从0→1→0的双向渐变,现有代码如下:
原有代码
HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="style.css"> <title>Image Scrolling Animation</title> </head> <body> <canvas class="airpods-scrolling"></canvas> <div id="textie" style="width: 1050px; top: 50%; left: 50%; margin-left: -525px; position: fixed; color: white; z-index: 10;text-align:center"> <h1>This is working OK.</h1> </div> <div id="textietwo" style="width: 1050px; top: 50%; left: 50%; margin-left: -525px; position: fixed; color: white; z-index: 10;text-align:center"> <h1>This is working OK TOO.</h1> </div> <script src="main.js"></script> </body> </html>
CSS
html{ height: 100vh; } body{ background: #000; height: 400vh; } canvas{ position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); max-height: 100vh; max-width: 100vw; } #textie{ display: none; } #textietwo{ display: none; }
JavaScript
const html = document.documentElement; const canvas = document.querySelector('.airpods-scrolling'); const context = canvas.getContext('2d'); const currentFrame = index => ( `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${index.toString().padStart(4, '0')}.jpg` ); const frameCount = 147; canvas.height = 770; canvas.width = 1158; const img = new Image(); img.src = currentFrame(1); img.onload = function(){ context.drawImage(img, 0, 0); } const updateImage = index => { img.src = currentFrame(index); context.drawImage(img, 0, 0); } window.addEventListener('scroll', () =>{ const scrollTop = html.scrollTop; const maxScrollTop = html.scrollHeight - window.innerHeight; const scrollFraction = scrollTop / maxScrollTop; const frameIndex = Math.min(frameCount - 1, Math.floor(scrollFraction * frameCount)); requestAnimationFrame( () => updateImage(frameIndex + 1)); // Text handler if(frameIndex < 10){ var test = document.getElementById('textie'); test.style.display = 'none'; } if(frameIndex > 10 && frameIndex < 50){ var test = document.getElementById('textie'); test.style.display = 'inline-block'; window.addEventListener('scroll', function(e) { var scroll = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; test.style.opacity = Math.max(0, Math.min(1, -scroll / 500 + 2)); }); } // Text 2 handler if(frameIndex < 100){ var test2 = document.getElementById('textietwo'); test2.style.display = 'none'; } if(frameIndex > 70 && frameIndex < 130){ var test2 = document.getElementById('textietwo'); test2.style.display = 'inline-block'; window.addEventListener('scroll', function(e) { var scroll2 = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; test2.style.opacity = Math.max(0, Math.min(1, -scroll2 / 1255 + 2)); }); } });
问题根源
- 重复绑定scroll事件:在scroll回调内再次绑定scroll监听,导致事件叠加、逻辑混乱
- 透明度计算逻辑错误:依赖scrollTop绝对值而非文本对应的帧区间相对进度,无法实现先淡入后淡出
- display切换导致突兀:用
display: none/inline-block切换显示状态,会跳过过渡直接显示,破坏渐变效果
修复方案
修改思路
- 移除重复的scroll事件绑定,所有逻辑在主scroll回调内完成
- 基于文本对应的帧区间计算相对滚动进度,实现0→1→0的透明度曲线
- 用
opacity配合visibility控制显示(避免display导致的突兀)
修改后的代码
HTML(无需改动)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="style.css"> <title>Image Scrolling Animation</title> </head> <body> <canvas class="airpods-scrolling"></canvas> <div id="textie" style="width: 1050px; top: 50%; left: 50%; margin-left: -525px; position: fixed; color: white; z-index: 10;text-align:center"> <h1>This is working OK.</h1> </div> <div id="textietwo" style="width: 1050px; top: 50%; left: 50%; margin-left: -525px; position: fixed; color: white; z-index: 10;text-align:center"> <h1>This is working OK TOO.</h1> </div> <script src="main.js"></script> </body> </html>
CSS
html{ height: 100vh; } body{ background: #000; height: 400vh; } canvas{ position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); max-height: 100vh; max-width: 100vw; } #textie, #textietwo{ opacity: 0; visibility: hidden; transition: opacity 0.1s ease; /* 可选:添加过渡平滑度 */ }
JavaScript
const html = document.documentElement; const canvas = document.querySelector('.airpods-scrolling'); const context = canvas.getContext('2d'); // 预获取文本元素,避免重复DOM查询 const textie = document.getElementById('textie'); const textietwo = document.getElementById('textietwo'); const currentFrame = index => ( `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${index.toString().padStart(4, '0')}.jpg` ); const frameCount = 147; canvas.height = 770; canvas.width = 1158; const img = new Image(); img.src = currentFrame(1); img.onload = function(){ context.drawImage(img, 0, 0); } const updateImage = index => { img.src = currentFrame(index); context.drawImage(img, 0, 0); } // 计算文本透明度的工具函数 const calculateOpacity = (currentFrame, startFrame, midFrame, endFrame) => { let opacity = 0; // 淡入阶段:从startFrame到midFrame,透明度0→1 if(currentFrame >= startFrame && currentFrame <= midFrame){ opacity = (currentFrame - startFrame) / (midFrame - startFrame); } // 淡出阶段:从midFrame到endFrame,透明度1→0 else if(currentFrame > midFrame && currentFrame <= endFrame){ opacity = 1 - (currentFrame - midFrame) / (endFrame - midFrame); } return Math.max(0, Math.min(1, opacity)); } window.addEventListener('scroll', () =>{ const scrollTop = html.scrollTop; const maxScrollTop = html.scrollHeight - window.innerHeight; const scrollFraction = scrollTop / maxScrollTop; const frameIndex = Math.min(frameCount - 1, Math.floor(scrollFraction * frameCount)); requestAnimationFrame( () => updateImage(frameIndex + 1)); // 处理第一个文本:帧区间10-50,中点30(淡入到30,淡出到50) const textieOpacity = calculateOpacity(frameIndex, 10, 30, 50); textie.style.opacity = textieOpacity; textie.style.visibility = textieOpacity > 0 ? 'visible' : 'hidden'; // 处理第二个文本:帧区间70-130,中点100(淡入到100,淡出到130) const textietwoOpacity = calculateOpacity(frameIndex, 70, 100, 130); textietwo.style.opacity = textietwoOpacity; textietwo.style.visibility = textietwoOpacity > 0 ? 'visible' : 'hidden'; });
关键说明
- 工具函数
calculateOpacity:根据当前帧在目标区间的位置,计算对应的透明度值,实现先增后减的曲线 - 避免重复DOM操作:预获取文本元素,减少scroll事件内的DOM查询开销
- 用visibility替代display:在透明度为0时隐藏元素,同时保留元素布局(也可根据需求调整)
- 可选过渡动画:CSS中添加
transition可让透明度变化更平滑,符合AirPods官网的细腻效果
内容的提问来源于stack exchange,提问作者Dannie
相关产品推荐
相关产品推荐

