两站图片滑动对比效果实现(VUE3)

news2024/11/24 8:33:38

在这里插入图片描述
像这种图片滑动对比的效果,网上还不少见吧,但是网上却不好找到完整现成的实现代码,我找到几个地方有类似的代码,但是都不好直接移植到代码里,因为很多都是使用原生html+css+js实现,太复杂了。反而不好应用到vue3中。
于是我借着他们的思路,自己实现了个。

前置条件

  1. 限制两张图片
  2. 图片大小必须一致,不一致会导致上层图片显示不全
  3. 底层图片必须存在,因为窗口的大小由他的大小决定

实现思路

  1. 将两张图片都看做是背景图,他们属于不同的层次
  2. 底层图片用相对定位,也就可以和其他元素一同正常展示,之后的所有元素都处于其包裹范围内,并且根据图片的大小确定整个样式的大小
  3. 上层图片用绝对定位,因为处于底层图片的包裹中,且大小是一样的,所以他们是完全重叠的,它的高度是和底层图片一致,但是宽度是可变的
  4. 利用input 的滑块模式 <input type=“range” v-model="width" />来改变上层图片的宽度,这里也就是利用了vue3的响应式。这是比原生方式实现的简便之处
  5. 由于input的原生样式无法改变,所以得额外做个滑块来实现自定义样式,然后其位置也受width的控制,实际上并不是点击该滑块来滑动的
  6. input是出于最上层的,但是将其隐藏了,所以点击的时候看起来像是点击那个滑块,实际上点击的是input实现的滑动。
  7. 然后再修饰下其他细节,即可

接下来逐步分解出实现的效果,这样就更好理解文字的意思了

第一步,将两张图片分别呈现

在这里插入图片描述

<template>
    <div id="bottomImg" class="bottomImg" :style="{ height: imgHeigth, width: imgWidth, backgroundImage: 'url(' + bottomImg + ')' }">
        <div class="upperImg" :style="{ backgroundImage: 'url(' + upperImg + ')', width: 100 - upperImgWidth + '%' }"></div>
    </div>
</template>
<script lang="ts" setup>
    import { ref, onMounted } from "vue";
    const imgHeigth = ref("0px"); // 图片高度
    const imgWidth = ref("0px"); // 图片宽度
    const bottomImg = ref(new URL("@/images/bottomImg.jpg", import.meta.url).href); // 底图
    const upperImg = ref(new URL("@/images/upperImg.jpg", import.meta.url).href); // 上层图
    const upperImgWidth = ref(50); // 上层图宽度

    // 首次加载时初始化
    onMounted(() => {
        getImgSize();
    });
    // 获取图片尺寸
    function getImgSize() {
        //加载图片获取图片真实宽度和高度
        var image = new Image();
        image.onload = function () {
            imgHeigth.value = image.height + "px";
            imgWidth.value = image.width + "px";
        };
        image.src = bottomImg.value; //img.src 应该是放在 onload 方法后边的, 因为当image的src发生改变,浏览器就会跑去加载这个src里的资源,先告诉浏览器图片加载完要怎么处理,再让它去加载图片
    }
</script>
<style>
    .bottomImg {
        position: relative; /*  相对定位 */
        overflow: hidden; /*  隐藏超出部分 */
    }
    .upperImg {
        position: absolute; /*  绝对定位 */
        top: 0;
        right: 0; /* 从右边开始铺开图片 */
        height: 100%;
        z-index: 1;
        background-position: right top; /* 改变定位方式,默认是左上角,要改为右上角,这样才能实现底图在左边,上层图在右边的效果*/
        border-left: 2px solid rgb(255, 255, 255, 0.5); /* 显示左边框 */
        background-repeat: no-repeat; /*  不重复 */
    }
</style>

代码有详细注释,现在两张图片分别展示的效果有了,但是还没法滑动。

第二步,添加滑块,图片动起来

在这里插入图片描述
就是加了个滑块,双向绑定宽度:

        <!--滑块控制上层图片的宽度 -->
        <input class="inputRange" type="range" v-model="upperImgWidth" min="0" max="100" />

样式

    .inputRange {
        position: absolute;
        height: 100%; /* 这样就能点击任何位置都能实现移动滑块,而不仅仅是滑块所在的位置才有效*/
        z-index: 3; /* 处于最高层次 */
        left: -4px; /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
        touch-action: auto;
        width: calc(100% + 4px); /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
        /* opacity: 0; */ /*隐藏滑块,这里为了演示,就不隐藏先*/
    }

到这一步,效果已经有了,接下来就是样式的优化。

第三步,增加自定义的滑块

在这里插入图片描述
增加一个这样的滑块样式,其实他仅仅是个样式,它并不具备点击滑动的能力,他的位置是靠响应width的值来改变的

        <!-- 这是对外展示的滑块样式,仅仅是展示样式的,不然原生的样式不好看 -->
        <span class="spanHandle" :style="{ left: 'calc(' + upperImgWidth + '% - 24px)' }"></span>

下面的样式不是重点,自己调整都是可以的,只需要注意使用绝对定位,z-index小于input即可。

  .spanHandle {
        position: absolute; /*绝对定位还是一样的*/
        z-index: 2; /* 样式很多,都不是关键的,只有这里,需要注意层次要低于inputRange*/
        height: 48px;
        width: 48px;
        position: center;
        font-size: 24px;
        border: 1px;
        border-radius: 50%;
        top: calc(90% - 24px);
        background-color: rgb(255, 255, 255, 0.5);
    }

    .spanHandle:before {
        left: 5px;
        transform: rotate(-45deg);
    }
    .spanHandle:after {
        right: -5px;
        transform: rotate(135deg);
    }
    .spanHandle:after,
    .spanHandle:before {
        border-left: 2px solid;
        border-top: 2px solid;
        content: "";
        height: 10px;
        position: absolute;
        top: 50%;
        transform-origin: 0 0;
        width: 10px;
    }

然后把上一步的opacity: 0;启用即可隐藏原始的input滑块

第四步,添加label和上层图片为空时的效果,这一步是可选的

效果就不贴了,就跟开头时差不多

完整代码

<template>
    <div id="bottomImg" class="bottomImg" :style="{ height: imgHeigth, width: imgWidth, backgroundImage: 'url(' + bottomImg + ')' }">
        <span class="imgLabel">{{ bottomLabel }}</span>
        <div v-if="upperImg" class="upperImg" :style="{ backgroundImage: 'url(' + upperImg + ')', width: 100 - upperImgWidth + '%' }">
            <span class="imgLabel">{{ upperLabel }}</span>
        </div>
        <div v-else class="upperUndefined" :style="{ width: 100 - upperImgWidth + '%' }">
            <span class="undefinedSpan">暂无结果</span>
        </div>
        <!-- 这是对外展示的滑块样式,仅仅是展示样式的,不然原生的样式不好看 -->
        <span class="spanHandle" :style="{ left: 'calc(' + upperImgWidth + '% - 24px)' }"></span>
        <!--滑块控制上层图片的宽度 -->
        <input class="inputRange" type="range" v-model="upperImgWidth" min="0" max="100" />
    </div>
</template>
<script lang="ts" setup>
    import { ref, onMounted } from "vue";
    const imgHeigth = ref("0px"); // 图片高度
    const imgWidth = ref("0px"); // 图片宽度
    const bottomImg = ref(new URL("@/images/bottomImg.jpg", import.meta.url).href); // 底图
    const upperImg = ref(new URL("@/images/upperImg.jpg", import.meta.url).href); // 上层图
    const upperImgWidth = ref(50); // 上层图宽度

    const bottomLabel = ref("底图"); // 底图标签
    const upperLabel = ref("上层图"); // 上层图标签

    // 首次加载时初始化
    onMounted(() => {
        getImgSize();
    });
    // 获取图片尺寸
    function getImgSize() {
        //加载图片获取图片真实宽度和高度
        var image = new Image();
        image.onload = function () {
            imgHeigth.value = image.height + "px";
            imgWidth.value = image.width + "px";
        };
        image.src = bottomImg.value; //img.src 应该是放在 onload 方法后边的, 因为当image的src发生改变,浏览器就会跑去加载这个src里的资源,先告诉浏览器图片加载完要怎么处理,再让它去加载图片
    }
</script>
<style>
    .bottomImg {
        position: relative; /*  相对定位 */
        overflow: hidden; /*  隐藏超出部分 */
    }
    .upperImg {
        position: absolute; /*  绝对定位 */
        top: 0;
        right: 0; /* 从右边开始铺开图片 */
        height: 100%;
        z-index: 1;
        background-position: right top; /* 改变定位方式,默认是左上角,要改为右上角,这样才能实现底图在左边,上层图在右边的效果*/
        border-left: 2px solid rgb(255, 255, 255, 0.5); /* 显示左边框 */
        background-repeat: no-repeat; /*  不重复 */
    }
    .inputRange {
        position: absolute;
        height: 100%; /* 这样就能点击任何位置都能实现移动滑块,而不仅仅是滑块所在的位置才有效*/
        z-index: 3; /* 处于最高层次 */
        left: -4px; /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
        touch-action: auto;
        width: calc(100% + 4px); /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
        opacity: 0; /*隐藏滑块,这里为了演示,就不隐藏先*/
    }
    .spanHandle {
        position: absolute; /*决定定位还是一样的*/
        z-index: 2; /* 样式很多,都不是关键的,只有这里,需要注意层次要低于inputRange*/
        height: 48px;
        width: 48px;
        position: center;
        font-size: 24px;
        border: 1px;
        border-radius: 50%;
        top: calc(90% - 24px);
        background-color: rgb(255, 255, 255, 0.5);
    }

    .spanHandle:before {
        left: 5px;
        transform: rotate(-45deg);
    }
    .spanHandle:after {
        right: -5px;
        transform: rotate(135deg);
    }
    .spanHandle:after,
    .spanHandle:before {
        border-left: 2px solid;
        border-top: 2px solid;
        content: "";
        height: 10px;
        position: absolute;
        top: 50%;
        transform-origin: 0 0;
        width: 10px;
    }
    .imgLabel {
        font-size: 20px;
        color: aliceblue;
        text-shadow: 1px 1px #533d4a, 2px 2px #533d4a;
    }
    .upperUndefined {
        position: absolute;
        top: 0;
        right: 0;
        height: 100%;
        z-index: 1;
        font-size: 60px;
        background-color: rgb(255, 255, 255, 0.8);
        background-position: right top;
        border-left: 2px solid rgb(255, 255, 255, 0.5);
    }
</style>

封装成组件

上面的示例都是死的,封装成组件,就可以切换图片展示:
在这里插入图片描述

组件代码

<template>
    <div id="bottomImg" class="bottomImg" :style="{ height: imgHeigth, width: imgWidth, backgroundImage: 'url(' + props.bottomImg + ')' }">
        <span class="imgLabel">{{ props.bottomLabel }}</span>
        <div v-if="props.upperImg" class="upperImg" :style="{ backgroundImage: 'url(' + props.upperImg + ')', width: 100 - upperImgWidth + '%' }">
            <span class="imgLabel">{{ props.upperLabel }}</span>
        </div>
        <div v-else class="upperUndefined" :style="{ width: 100 - upperImgWidth + '%' }">
            <span class="undefinedSpan">暂无结果</span>
        </div>
        <span class="spanHandle" :style="{ left: 'calc(' + upperImgWidth + '% - 24px)' }"></span>
        <input class="inputRange" type="range" v-model="upperImgWidth" min="0" max="100" />
    </div>
</template>
<script lang="ts" setup>
    import { ref, watch, onMounted } from "vue";
    const imgHeigth = ref("0px");
    const imgWidth = ref("0px");
    const upperImgWidth = ref(50);
    const props = defineProps({
        bottomImg: {
            type: String,
            default: "",
        },
        upperImg: {
            type: String,
            default: "",
        },
        bottomLabel: {
            type: String,
            default: "原图",
        },
        upperLabel: {
            type: String,
            default: "效果图",
        },
    });
    // 跟踪底层图片的变化,因为底层图片是基础
    watch(
        () => props.bottomImg,
        () => {
            getImgSize();
            upperImgWidth.value = 50;
        }
    );
    // 首次加载时初始化
    onMounted(() => {
        getImgSize();
    });
    function getImgSize() {
        //加载图片获取图片真实宽度和高度
        var image = new Image();
        image.onload = function () {
            imgHeigth.value = image.height + "px";
            imgWidth.value = image.width + "px";
        };
        image.src = props.bottomImg; //img.src 应该是放在 onload 方法后边的, 因为当image的src发生改变,浏览器就会跑去加载这个src里的资源,先告诉浏览器图片加载完要怎么处理,再让它去加载图片
    }
</script>
<style>
    .bottomImg {
        position: relative;
        overflow: hidden;
    }
    .upperImg {
        position: absolute;
        top: 0;
        right: 0;
        height: 100%;
        z-index: 1;
        background-position: right top;
        border-left: 2px solid rgb(255, 255, 255, 0.5);
    }
    .imgLabel {
        font-size: 20px;
        color: aliceblue;
        text-shadow: 1px 1px #533d4a, 2px 2px #533d4a;
    }
    .upperUndefined {
        position: absolute;
        top: 0;
        right: 0;
        height: 100%;
        z-index: 1;
        font-size: 60px;
        background-color: rgb(255, 255, 255, 0.8);
        background-position: right top;
        border-left: 2px solid rgb(255, 255, 255, 0.5);
    }
    .undefinedSpan {
        display: flex;
        width: 100%;
        height: 100%;
        align-items: center;
        justify-content: center;
        color: #999;
        overflow: hidden;
    }
    .inputRange {
        position: absolute;
        height: 100%;
        z-index: 3;
        left: -4px;
        touch-action: auto;
        width: calc(100% + 4px);
        opacity: 0;
    }
    .spanHandle {
        position: absolute;
        z-index: 2;
        height: 48px;
        width: 48px;
        position: center;
        font-size: 24px;
        border: 1px;
        border-radius: 50%;
        top: calc(90% - 24px);
        background-color: rgb(255, 255, 255, 0.5);
    }

    .spanHandle:before {
        left: 5px;
        transform: rotate(-45deg);
    }
    .spanHandle:after {
        right: -5px;
        transform: rotate(135deg);
    }
    .spanHandle:after,
    .spanHandle:before {
        border-left: 2px solid;
        border-top: 2px solid;
        content: "";
        height: 10px;
        position: absolute;
        top: 50%;
        transform-origin: 0 0;
        width: 10px;
    }
</style>

调用示例代码

<template>
    <div>
        <div>
            <button @click="changeBottomImg">切换底图</button>
            <button @click="removeUpperImg">去除上层图</button>
            <button @click="changeUpperImg">切换上层图</button>
        </div>
        <TwoImgCompare :bottom-img="bottomImg" bottom-label="原图" :upper-img="upperImg" upper-label="结果图"></TwoImgCompare>
    </div>
</template>
<script lang="ts" setup>
    import TwoImgCompare from "@/components/twoImgCompare.vue";
    import { ref } from "vue";
    const bottomImg = ref(new URL("@/images/bottomImg.jpg", import.meta.url).href); // 底图
    const upperImg = ref(new URL("@/images/upperImg.jpg", import.meta.url).href); // 上层图

    // 切换底图
    const changeBottomImg = () => {
        bottomImg.value = new URL("@/images/bottomImg2.jpg", import.meta.url).href;
    };

    // 去除上层图
    const removeUpperImg = () => {
        upperImg.value = "";
    };

    // 切换上层图
    const changeUpperImg = () => {
        upperImg.value = new URL("@/images/upperImg2.jpg", import.meta.url).href;
    };
</script>

PS:开发该组件是用于自己开发的网站:极简AI工具箱,欢迎光临!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1797762.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python Cookbook】S02E04 文本模式的匹配和查找 match()、search()、findall() 以及 捕获组和 + 的含义

目录 问题解决方案讨论 问题 本文讨论一些按照特定的文本模式进行的查找和匹配。 解决方案 如果想要匹配的只是简单文字&#xff0c;通常我们使用一些内置的基本字符串方法即可&#xff0c;如&#xff1a;str.find()&#xff0c;str.startwith()&#xff0c;str.endswith() …

MySQL数据库数据恢复方案应对没有where误操作导致的大量数据更新或删除

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

二分答案-acwing-102. 最佳牛围栏

题目传送门&#xff1a;t​​​102. 最佳牛围栏 - AcWing题库高质量的算法题库https://www.acwing.com/problem/content/104/ 解题思路 整体解析 按照题目要求我们要找到一块连续的区域&#xff0c;使其里面每块地里面的平均值最大&#xff0c;且这块区域的长度要大于f 二分处理…

STM32——ADC篇(ADC的使用)

一、ADC的介绍 1.1什么是ADC ADC&#xff08;Analogto-Digital Converter&#xff09;模拟数字转换器&#xff0c;是将模拟信号转换成数字信号的一种外设。比如某一个电阻两端的是一个模拟信号&#xff0c;单片机无法直接采集&#xff0c;此时需要ADC先将短租两端的电…

免费获取云服务器

这几天刚入手了阿贝云的 “免费云服务器 ” &#xff0c;接下来给大家讲讲如何免费注册阿贝云的免费云服务器 如何获取免费云服务器 打开阿贝云官网&#xff0c;注册并认证 即可以领取免费云服务器 阿贝云地址&#xff1a;https://www.abeiyun.com/ 服务器优势 永久免费&…

单点登录(SSO)前端怎么做

单点登录&#xff08;SSO&#xff09;前端怎么做 本文介绍单点登录&#xff08;SSO&#xff09;是什么&#xff0c;还有就是前端怎么做。 单点登录&#xff08;SSO&#xff09;是什么 单点登录&#xff08;SSO&#xff0c;Single Sign On&#xff09;&#xff0c;是在企业内部…

【Java笔记】第9章:三个修饰符

前言1. abstract&#xff08;抽象的&#xff09;2. static&#xff08;静态的&#xff09;3. final&#xff08;最终的&#xff09;结语 上期回顾:【Java笔记】第8章&#xff1a;面向对象的三大特性&#xff08;封装、继承、多态&#xff09; 个人主页&#xff1a;C_GUIQU 归属…

瑞鑫RK3588 画中画 OSD 效果展示

这些功能本来在1126平台都实现过 但是迁移到3588平台之后 发现 API接口变化较大 主要开始的时候会比较费时间 需要找到变动接口对应的新接口 之后 就比较好操作了 经过几天的操作 已实现 效果如下

项目工具|git相关

本博客暂时只作为个人资料&#xff0c;后续会进行完善&#xff0c;主要内容来自&#xff1a; 【【Git第一讲】&#xff1a;git分区与两个盒子的故事】 理解暂存区和未暂存区 git为什么要多一个暂存区&#xff1f;难道不能我把代码写完后就是未暂存区&#xff0c;然后直接提交…

ROS2从入门到精通4-3:全局路径规划插件开发案例(以A*算法为例)

目录 0 专栏介绍1 路径规划插件的意义2 全局规划插件编写模板2.1 构造规划插件类2.2 注册并导出插件2.3 编译与使用插件 3 全局规划插件开发案例(A*算法)常见问题 0 专栏介绍 本专栏旨在通过对ROS2的系统学习&#xff0c;掌握ROS2底层基本分布式原理&#xff0c;并具有机器人建…

docker bash: vi: command not found 修改文件无法使用 vi yum的方法

如题&#xff0c;被入坑很多次。也参考了很多的修复docker 中的vi yum等方法。最终都未解决。 因为要修改 已安装容器中的各类配置信息。无法使用vi yum很麻烦。除去使用docker 挂载文件方法外&#xff0c;还可以使用如下方法直接修改对应的配置文件信息。 如: 修改 logstas…

通过抑制治疗上调的环氧化酶-2来改善光动力性能的肿瘤归巢嵌合肽菱形体

引用信息 文 章&#xff1a;Tumor Homing Chimeric Peptide Rhomboids to Improve Photodynamic Performance by Inhibiting Therapy‐Upregulated Cyclooxygenase-2. 期 刊&#xff1a;Smal&#xff08;影响因子&#xff1a;13.3&#xff09; 发表时间&#xff1a…

赢单有秘诀,大模型智能陪练更懂你

随着数字化技术在营销场景的加速应用&#xff0c;产品营销节奏不断加快&#xff0c;消费者需求日益多元化、个性化&#xff0c;市场竞争日趋激烈。面对复杂多变的市场环境&#xff0c;企业新产品、新服务的推出速度大幅提升&#xff0c;产品知识更新愈加频繁&#xff0c;传统的…

神经网络 torch.nn---优化器的使用

torch.optim - PyTorch中文文档 (pytorch-cn.readthedocs.io) torch.optim — PyTorch 2.3 documentation 反向传播可以求出神经网路中每个需要调节参数的梯度(grad)&#xff0c;优化器可以根据梯度进行调整&#xff0c;达到降低整体误差的作用。下面我们对优化器进行介绍。 …

通过龙讯旷腾PWmat发《The Journal of Chemical Physics》 :基于第一性原理分子动力学热力学积分的离子溶剂化自由能计算

背景导读 离子溶解是电化学中一个重要的过程。电化学反应中许多重要的参数&#xff0c;例如电化学还原电位、无限稀释活度系数、亨利定律溶解常数和离子溶解度等&#xff0c;都与离子的溶剂化能有关。然而&#xff0c;由于测量技术和数据处理的困难&#xff0c;离子溶剂化能的…

LabVIEW与Arm控制器之间的通讯

LabVIEW是一个强大的图形化编程环境&#xff0c;广泛应用于自动化控制、数据采集和测试测量等领域。而Arm控制器则是嵌入式系统中常用的处理器架构&#xff0c;广泛用于各种控制和计算任务。将LabVIEW与Arm控制器进行通讯控制&#xff0c;可以结合二者的优势&#xff0c;实现高…

笔记96:前馈控制 + 航向误差

1. 回顾 对于一个 系统而言&#xff0c;结构可以画作&#xff1a; 如果采用 这样的控制策略&#xff0c;结构可以画作&#xff1a;&#xff08;这就是LQR控制&#xff09; 使用LQR控制器&#xff0c;可以通过公式 和 构建一个完美的负反馈系统&#xff1b; a a 但是有上…

学习笔记——网络参考模型——TCP/IP模型(网络层)

三、TCP/IP模型-网络层 1、IPV4报头 (1)IPV4报文格式 IP Packet(IP数据包)&#xff0c;其包头主要内容如下∶ Version版本∶4 bit&#xff0c;4∶表示为IPv4; 6∶表示为IPv6。 Header Length首部长度∶4 bit&#xff0c;代表IP报头的长度(首部长度)&#xff0c;如果不带Opt…

安卓手机平板使用JuiceSSH无公网IP远程连接本地服务器详细流程

文章目录 前言1. Linux安装cpolar2. 创建公网SSH连接地址3. JuiceSSH公网远程连接4. 固定连接SSH公网地址5. SSH固定地址连接测试 前言 处于内网的虚拟机如何被外网访问呢?如何手机就能访问虚拟机呢? 本文就和大家分享一下如何使用 cpolarJuiceSSH 实现手机端远程连接Linux…

conntrack如何限制您的k8s网关

1.1 conntrack 介绍 对于那些不熟悉的人来说,conntrack简单来说是Linux内核的一个子系统,它跟踪所有进入、出去或通过系统的网络连接,允许它监控和管理每个连接的状态,这对于诸如NAT(网络地址转换)、防火墙和保持会话连续性等任务至关重要。它作为Netfilter的一部分运行,…