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

JavaScript/CSS圆形虹膜滚动过渡异常:向下滚动正常展开,向上滚动无平滑收缩且页面跳转

JavaScript/CSS圆形虹膜滚动过渡异常:向下滚动正常展开,向上滚动无平滑收缩且页面跳转

我明白你遇到的棘手问题了——向下滚动时圆形遮罩能完美平滑展开,但往上回滚的时候,页面直接跳回Problem 5,遮罩也瞬间闭合,完全没有预期的平滑收缩效果。这问题核心出在你当前的滚动锁定逻辑上:只要进入过渡区就直接钉死滚动位置,切断了向上滚动时通过滚动位置变化计算收缩比例的可能,直接触发了跳转。

下面是修复后的完整实现,我会同时解释关键修改点:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Circular Reveal Scroll</title>
    <style>
        html, body { 
            height: 100%; 
            margin: 0; 
            font-family: sans-serif;
            overflow-x: hidden;
        } 

        /* Problems section: 5 fullscreen cards */
        #problems {
            scroll-snap-type: y mandatory;
            position: relative;
            height: 500vh; /* 对应5个全屏卡片 */
        }

        #problems .card {
            height: 100vh;
            scroll-snap-align: start;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 2rem;
            color: white;
        }

        #problems .card:nth-child(1) { background: #2c3e50; }
        #problems .card:nth-child(2) { background: #34495e; }
        #problems .card:nth-child(3) { background: #16a085; }
        #problems .card:nth-child(4) { background: #27ae60; }
        #problems .card:nth-child(5) { background: #2980b9; }

        /* Solutions section: fixed全屏布局 */
        #solutions {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: #e74c3c;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 2rem;
            color: white;
            clip-path: circle(0px at 50% 50%);
            -webkit-clip-path: circle(0px at 50% 50%);
        }
    </style>
</head>
<body>
    <div id="problems">
        <div class="card">Problem 1</div>
        <div class="card">Problem 2</div>
        <div class="card">Problem 3</div>
        <div class="card">Problem 4</div>
        <div class="card">Problem 5</div>
    </div>
    <div id="solutions">Solution Time!</div>

    <script>
        (() => { 
            const problems = document.getElementById('problems'); 
            const solutions = document.getElementById('solutions'); 
            const body = document.body; 
            const html = document.documentElement; 

            /* -------- 稳健的滚动位置获取函数 -------- */
            const getScrollY = () => window.pageYOffset || html.scrollTop || body.scrollTop || window.visualViewport?.offsetTop || 0; 

            /* -------- 动画边界计算 -------- */
            let viewportHeight = window.innerHeight; 
            let topOfSolutions = problems.offsetTop + problems.offsetHeight; 
            let startReveal = topOfSolutions - viewportHeight; 
            let endReveal = topOfSolutions; 
            let revealRatio = 0; 

            /* 圆形遮罩最大半径:视口对角线的一半 */
            const maxRadius = () => Math.hypot(window.innerWidth / 2, window.innerHeight / 2); 

            /* 更新圆形遮罩的clip-path */
            const setRadius = (r) => { 
                const clip = `circle(${r}px at 50% 50%)`; 
                solutions.style.clipPath = clip; 
                solutions.style.webkitClipPath = clip; 
            }; 

            /* -------- 核心滚动处理逻辑 -------- */
            const handleScroll = () => { 
                const currentY = getScrollY(); 

                // 根据当前滚动位置计算遮罩显示比例
                if (currentY <= startReveal) {
                    revealRatio = 0;
                } else if (currentY >= endReveal) {
                    revealRatio = 1;
                } else {
                    revealRatio = (currentY - startReveal) / (endReveal - startReveal);
                }

                // 应用圆形遮罩
                setRadius(revealRatio * maxRadius());
            }; 

            /* -------- 窗口大小变化时重新计算边界 -------- */
            const handleResize = () => { 
                viewportHeight = window.innerHeight; 
                topOfSolutions = problems.offsetTop + problems.offsetHeight; 
                startReveal = topOfSolutions - viewportHeight; 
                endReveal = topOfSolutions; 

                // 只有当遮罩处于中间状态时才更新半径,避免不必要重绘
                if (revealRatio > 0 && revealRatio < 1) {
                    setRadius(revealRatio * maxRadius());
                }
            }; 

            /* -------- 初始化事件监听 -------- */
            window.addEventListener('scroll', handleScroll, { passive: true }); 
            window.addEventListener('resize', handleResize); 

            // 页面加载时执行一次初始化计算
            handleScroll();
        })();
    </script>
</body>
</html>

关键修改说明

  1. 移除了滚动锁定逻辑:删掉了原代码中的isLocked状态、lockScroll/unlockScroll函数,以及滚动事件中的锁定代码。现在完全通过滚动位置currentY的变化来驱动遮罩比例,彻底解决了跳转问题。
  2. 简化核心逻辑:滚动事件只负责根据当前位置计算遮罩显示比例,不再做任何强制锁定或跳转操作。过渡区内的比例变化完全跟随滚动位置,上下滚动都能平滑响应。
  3. 补充了可运行的基础样式:给Problem卡片和Solution区域添加了全屏布局样式,你可以直接运行代码看到完整效果,无需额外调整。
  4. 优化响应式处理:窗口大小变化时,仅在遮罩处于中间状态时更新半径,减少不必要的重绘操作。

修改后,向下滚动进入Problem 5的最后一个视口高度区域时,圆形遮罩会从中心平滑展开;向上滚动时,遮罩会随着滚动位置上移逐步收缩,完全符合你预期的平滑过渡效果,不会再出现页面跳转。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 08:45:29