图片对比组件技

0 阅读4分钟

本组件是一个基于原生 HTML/CSS/JS 开发的交互式图片对比工具(Image Comparison Slider),常用于展示产品渲染前后、照片修图前后或场景变化的效果。

效果如图:

screen_recording_2026-02-26_10-08-26.gif


代码如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Halo 图片对比效果</title>
    <style>
        /* 基础样式复现 */
        body {
            margin: 0;
            padding: 0;
            background: #f5f5f5;
        }

        .sp-ba-wrap {
            max-width: 1440px;
            min-width: 343px;
            margin: 0 auto;
            margin-top: 120px;
            padding: 0 20px;
        }

        .sp-ba {
            position: relative;
            margin: 0 auto;
            max-width: 1200px;
            z-index: 2;
        }

        /* 核心对比滑块样式 */
        .banda-slider {
            display: block;
            overflow: hidden;
            position: relative;
            border-radius: 16px;
            width: 100%;
            line-height: 0;
        }

        .banda-slider img {
            width: 100%;
            height: auto;
            display: block;
            user-select: none;
        }

        .banda-reveal {
            left: 0;
            top: 0;
            bottom: 0;
            overflow: hidden;
            position: absolute;
            right: 50%;
            /* 初始位置 */
            z-index: 1;
            border-right: 2px solid #fff;
        }

        .banda-reveal>img {
            height: 100%;
            width: 200%;
            /* 这里必须是父容器的2倍才能保证内容不拉伸 */
            max-width: none;
            object-fit: cover;
        }

        /* 交互控件:透明滑块 */
        .banda-range {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            opacity: 0;
            cursor: ew-resize;
            z-index: 10;
        }

        /* 装饰用的中间圆圈 */
        .banda-handle {
            background: #000;
            border-radius: 50%;
            color: #fff;
            height: 48px;
            width: 48px;
            left: 50%;
            top: 50%;
            position: absolute;
            transform: translate(-50%, -50%);
            pointer-events: none;
            z-index: 5;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .banda-handle:before,
        .banda-handle:after {
            content: "";
            border: solid white;
            border-width: 0 2px 2px 0;
            display: inline-block;
            padding: 3px;
        }

        .banda-handle:before {
            transform: rotate(135deg);
            margin-right: 4px;
        }

        .banda-handle:after {
            transform: rotate(-45deg);
            margin-left: 4px;
        }

        @media only screen and (max-width: 888px) {
            .sp-ba-wrap {
                margin-top: 40px;
            }

        }
    </style>
</head>
<body>

    <div class="sp-ba-wrap">
        <div class="sp-ba">
            <div class="banda-slider" id="mySlider">
                <img src="https://cdn.shopify.com/s/files/1/0268/7297/1373/files/55379ad14f3eb2af576acdc527686e4e_3840x2000_6af3220c-9331-4d97-bec8-3687cd8745f9.jpg?v=1711012998"
                    alt="Before">

                <div class="banda-reveal" id="revealLayer">
                    <img src="https://cdn.shopify.com/s/files/1/0268/7297/1373/files/83793ec91a3bdd17ce22ed844b4e4aeb_3840x2000_b58d63a4-2a39-4550-b890-ff6519f49952.jpg?v=1711012992"
                        alt="After" id="revealImg">
                </div>

                <input type="range" min="0" max="100" value="50" class="banda-range" id="rangeInput">
                <div class="banda-handle" id="handle"></div>
            </div>
        </div>
    </div>

    <script>
        // 逻辑实现:监听滑动条并实时更新 UI
        const range = document.getElementById('rangeInput');
        const revealLayer = document.getElementById('revealLayer');
        const revealImg = document.getElementById('revealImg');
        const handle = document.getElementById('handle');
        const slider = document.getElementById('mySlider');

        range.addEventListener('input', (e) => {
            const value = e.target.value;

            // 1. 更新遮罩层的宽度(实际上是修改 right 距离)
            // 原理:当 value 增加,左侧显示更多,revealLayer 需要向右移
            revealLayer.style.right = (100 - value) + '%';

            // 2. 更新分隔小圆圈的位置
            handle.style.left = value + '%';

            // 3. 动态调整内部图片的宽度,防止拉伸
            // 因为 revealLayer 的宽度在变,其内部图片需要反向维持比例
            const containerWidth = slider.offsetWidth;
            revealImg.style.width = containerWidth + 'px';
        });

        // 窗口大小改变时重置图片宽度
        window.addEventListener('resize', () => {
            revealImg.style.width = slider.offsetWidth + 'px';
        });
        // 初始化执行一次
        revealImg.style.width = slider.offsetWidth + 'px';
    </script>

</body>
</html>

一、 实现的效果

  • 视觉表现:页面中间展示一张图片,通过一条可移动的垂直分割线将画面分为左右两部分,分别显示不同的内容(如:白昼与黑夜、修图前与修图后)。中心配有一个黑色圆形手柄提示用户可进行操作。

  • 交互表现

    • 手动拖拽:用户点击并左右拖动中间的手柄,即可实时改变两侧图片的显示比例。
    • 移动端适配:支持触摸滑动,在手机或平板上拥有流畅的交互体验。
    • “揭开”感:手柄移动的过程类似于拨开一张蒙版,视觉反馈直观且平滑。

二、 实现思路

  1. 分层堆叠(Layering)

    将两张分辨率完全一致的图片放置在同一个父容器中。底层图片(Bottom Image)作为固定基准,顶层图片(Top Image)嵌套在一个带有遮罩属性的容器中。

  2. 动态裁剪(Clipping)

    给顶层图片容器设置 overflow: hidden。通过改变这个容器的宽度(例如从 50% 变为 30%),它就像一扇“移动的门”,遮挡掉上层图片的一部分,从而露出底层的图片。

  3. 视觉对齐(Alignment Fix)

    • 挑战:默认情况下,如果父容器变窄,内部图片通常会随之缩小或变形。
    • 对策:给上层图片设置一个固定的宽度(通常等于外层大容器的宽度),使其不随遮罩容器的缩放而缩放。这样,上下两张图片的内容就能在视觉上完美重合。
  4. 隐形控制(Invisible Control)

    在整个组件最顶层覆盖一个完全透明的 HTML 滑动条 <input type="range">。这样做可以利用浏览器原生的高性能滑动监听,无需自己写复杂的鼠标位移计算逻辑。


三、 实现原理

1. 核心 CSS 结构

  • 遮罩原理:利用 position: absolute 进行定位。遮罩层 banda-reveal 充当“视口”,通过修改它的 rightwidth 属性来控制露出的比例。
  • 布局优化:使用 line-height: 0display: block 消除图片底部常见的像素间隙,确保容器高度完全由图片撑开。

2. 数值映射

组件通过 JavaScript 实时获取滑块的数据并进行映射:

  • 滑块当前值XX (取值范围 01000 \sim 100)
  • 遮罩层宽度W=X%W = X\% (决定揭开多少内容)
  • 手柄偏移量L=X%L = X\% (确保手柄始终在分割线上)

3. 同步逻辑代码

JavaScript 监听 input 事件,实现数据驱动 UI:

JavaScript

// 核心同步逻辑示例
rangeInput.addEventListener('input', (e) => {
    const sliderValue = e.target.value;
    
    // 1. 改变遮罩层宽度(拨开视觉效果)
    revealLayer.style.width = sliderValue + '%'; 
    
    // 2. 同步移动中间的控制手柄
    handle.style.left = sliderValue + '%';       
});