图像滑块对比功能的开发记录

458 阅读4分钟

背景介绍

最近,公司需要开发一款在线图像压缩工具,其中的一个关键功能是让用户直观地比较压缩前后的图像效果。因此,我们设计了一个对比组件,它允许用户通过拖动滑块,动态调整两张图像的显示区域,从而清晰地看到压缩前后的差异。

目标效果

  • 两张图片堆叠放置,一张始终可见,另一张可调整可见范围。
  • 通过滑块拖动,控制上层图片的显示区域。
  • 适配 PC 和移动端,提供流畅的交互体验。

效果如图:

开发思路

结构设计

  1. 创建一个外层容器,用于包裹两张图片和滑块。
  2. 底层图片(原始图像)始终可见。
  3. 顶层图片(优化后图像)放置在上方,并使用 clip-path 控制其显示范围。
  4. 滑块(拖动条) 用于调整顶层图片的可见区域。
<div class="image-change-block">
    <div class="desc-container">
        <div class="before-desc">BEFORE (827 KB)</div>
        <div class="after-desc">AFTER (94 KB)</div>
    </div>
    <img src="img1.jpg" class="old-img" />
    <div class="clip-container" id="clipContainer">
        <img src="img2.jpg" class="new-img" id="newImg" />
    </div>
    <div class="handle-container">
        <div class="bar-btn" id="barBtn"></div>
        <div class="bar-line" id="barLine"></div>
    </div>
</div>

样式布局

  • 两张图片:底层图片 position: absolute; 覆盖整个容器,顶层图片使用 clip-path 或 width 控制显示区域。
  • 滑块样式:自定义 div + 伪元素 作为滑块,并放在 absolute 位置。
.image-change-block {
    position: relative;
    overflow: hidden;
    border-radius: 10px;
    margin: 100px auto 0 auto;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    max-width: 900px;
    background-color: #fff;
}

.desc-container {
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    width: 95%;
    gap: 5px
}

.after-desc,
.before-desc {
    background-color: #000000;
    opacity: 0.6;
    color: #fff;
    border-radius: 5px;
    z-index: 10;
    padding: 5px 15px;
}

.old-img,
.new-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.clip-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    will-change: clip-path;
}

.clip-container img {
    transform: translateZ(0);
    will-change: clip-path;
}

.new-img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    clip-path: inset(0 50% 0 0);
    transform: translateZ(0);
    will-change: clip-path;
}

.handle-container {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    pointer-events: none;
}

.bar-btn {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 10px;
    height: 10px;
    border-radius: 50%;
    pointer-events: all;
    cursor: ew-resize;
    z-index: 2;
}

.bar-line {
    position: absolute;
    top: 0;
    left: 50%;
    height: 100%;
    width: 5px;
    background-color: #000;
    z-index: 1;
}

.bar-btn::before {
    width: 40px;
    height: 40px;
    font-size: 13px;
    content: "拖动";
    color: #fff;
    cursor: ew-resize;
    background: #000;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-45%, -50%);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    z-index: 1;
}

.bar-btn::after {
    content: "";
    background: #000;
    width: 5px;
    height: 100%;
    position: absolute;
    left: 50%;
    top: 0;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

交互逻辑

封装一个兼容 PC 和移动端的拖拽对比函数,通过传入对应的 dom 实现鼠标或手指拖动滑块时图像的对比。

document.addEventListener('DOMContentLoaded', () => {
    /**
     * 初始化通过剪裁实现图像对比的功能
     * @param {Object} doms - 包含所需DOM元素的对象
     *  - barBtn: 滑动按钮元素
     *  - barLine: 滑动线条元素
     *  - clipContainer: 剪裁容器元素
     *  - newImg: 被剪裁的图片元素
     */
    function initClip(doms) {
        // 解构赋值获取所需的DOM元素
        const { barBtn, barLine, clipContainer, newImg } = doms;
        // 定义变量以跟踪鼠标或触摸是否在拖动
        let isDragging = false;
        let isMDragging = false;

        /**
         * 更新图片剪裁位置
         * @param {number} x - 鼠标或触摸在剪裁容器上的x坐标
         */
        function updateImageClip(x) {
            requestAnimationFrame(() => {
                // 计算剪裁容器的宽度
                const containerWidth = clipContainer.offsetWidth;
                // 计算并限制剪裁的百分比
                const percent = Math.min(Math.max(x / containerWidth, 0), 1);
                // 更新图片的剪裁路径
                newImg.style.clipPath = `inset(0 ${100 - percent * 100}% 0 0)`;
                // 更新滑动按钮和线条的位置
                barBtn.style.left = `${percent * 100}%`;
                barLine.style.left = `${percent * 100}%`;
            });
        }

        // 添加鼠标按下事件监听器到滑动按钮
        barBtn.addEventListener("mousedown", (e) => {
            // 开始拖动并阻止默认行为
            isDragging = true;
            e.preventDefault();
        });

        // 添加鼠标抬起事件监听器到文档
        document.addEventListener("mouseup", () => {
            // 结束拖动
            isDragging = false;
        });

        // 添加鼠标移动事件监听器到文档
        document.addEventListener("mousemove", (e) => {
            // 如果正在拖动,则更新图片剪裁
            if (isDragging) {
                const x = e.clientX - clipContainer.getBoundingClientRect().left;
                updateImageClip(x);
            }
        });

        // 添加点击事件监听器到剪裁容器
        clipContainer.addEventListener("click", (e) => {
            // 点击时更新图片剪裁
            const x = e.clientX - clipContainer.getBoundingClientRect().left;
            updateImageClip(x);
        });

        // 添加触摸开始事件监听器到滑动按钮
        barBtn.addEventListener("touchstart", (e) => {
            // 开始拖动并阻止默认行为
            isMDragging = true;
            e.preventDefault();
        });

        // 添加触摸结束事件监听器到文档
        document.addEventListener("touchend", () => {
            // 结束拖动
            isMDragging = false;
        });

        // 添加触摸移动事件监听器到文档
        document.addEventListener("touchmove", (e) => {
            // 如果正在拖动,则更新图片剪裁
            if (isMDragging) {
                const touch = e.touches[0];
                const x = touch.clientX - clipContainer.getBoundingClientRect().left;
                updateImageClip(x);
            }
        });
    }

    initClip({
        barBtn: document.getElementById(`barBtn`),
        barLine: document.getElementById(`barLine`),
        clipContainer: document.getElementById(`clipContainer`),
        newImg: document.getElementById(`newImg`),
    });
});

总结

这个滑块对比组件利用 clip-path 来裁剪图像,并结合鼠标和触摸事件监听,实现了流畅的交互体验。它不仅适用于图像压缩前后的对比,还可以扩展到滤镜效果、照片修复等其他图像对比场景。在实际开发中,我们可以根据 UI 需求,进一步优化滑块的样式、动画效果,以及提升移动端的操作体验。

原文链接

图像滑块对比功能的开发记录