You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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));
        });
    }
});

问题根源

  1. 重复绑定scroll事件:在scroll回调内再次绑定scroll监听,导致事件叠加、逻辑混乱
  2. 透明度计算逻辑错误:依赖scrollTop绝对值而非文本对应的帧区间相对进度,无法实现先淡入后淡出
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 17:44:50