实现图片的卷帘对比效果

项目中遇到了一个需求是前台上传原图与对比图,后台进行对比分析,返回一个带有标注的识别结果,然后前台要实现原图与识别结果的卷帘对比效果。

我也不知道这个应该叫什么,感觉像个卷帘门一样,就姑且叫它卷帘效果了。

在学习arcgis的js api时,发现arcgis是有提供这种效果的小部件的,当然这次项目并没有使用arcgis,所以没法使用这个效果。后台问我这种效果好实现不,去找找插件试一下。回想到之前做过图片的截取效果,想着这个也可以用类似的方法实现的吧,所以就用同样的思想实现了这个效果,可能走了弯路,见谅哦。

先看下效果图吧(点击图片跳转到图床页可播放,感谢路过图床):

otW8zR.md.gif

接下来从图片的上传开始,到图片的预览,再到图片的对比效果,一步一步来。

图片上传

因为需求比较简单,每次只需要上传一张图片到后台即可,所以图片的上传我并没有直接使用组件库提供的组件,而是使用了一个div实现上传按钮的样式,然后添加了一个隐藏的input标签,手动触发上传事件。

1
2
3
4
5
6
7
8
9
<input 
style="opacity: 0;width:0;height:0;"
type="file"
value=""
name="file"
ref="inputFile"
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
@change="handleUploadImg($event)"
>

当用户上传了文件触发了input标签的change事件后,我们在这里实现上传的操作。

1
2
3
4
5
6
7
8
function handleUploadImg(event){
// 上传的文件
let file = event.target.files[0]
// 后台通过formData接收,所以使用formData
let formData = new FormData()
formData.append('file', file)
// 将formData传给后台即可完成上传
}

图片预览

在大多情况下,我们将图片文件传给后台之后,后台会返回图片文件的地址,我们将地址渲染到image标签上就可以完成预览了。

如果后台不返回图片的地址,或者我们需要用户选择后先进行预览,待用户再次确认后才进行上传,那么我们就需要对用户上传的图片进行操作实现预览了。

1
2
3
4
5
6
7
8
9
10
function handleUploadImg(event){
let file = event.target.files[0]
let reader = new FileReader()
// 读取图片文件为base64
reader.readAdDataURL(file)
// 读取完成的回调函数
reader.onload = function(evt){
// evt.target.result 就是图片的base64,渲染到image即可
}
}

卷帘的对比效果

图片的上传与预览都是比较简单的工作,难点还是在实现卷帘的对比效果这一块

我的业务需求是上传一张图片进行ai识别,生成识别结果然后做对比效果的,在此处就简化一下直接拿两张图片来实现这个样式效果了。

说一下大概思路:

页面上会有两张图片,一张原图,一张对比图

原图放到底部作为底图,然后将对比图定位到原图的正上方,通过裁切显示指定的区域。

有用但又不常用的clip属性

首先介绍一个可能不是那么常用的CSS样式属性clip,我们就要通过他来裁切我们的对比图。

w3c对于clip的定义是:

clip 属性剪裁绝对定位元素。

这个属性用于定义一个剪裁矩形。对于一个绝对定义元素,在这个矩形内的内容才可见。出了这个剪裁区域的内容会根据 overflow 的值来处理。剪裁区域可能比元素的内容区大,也可能比内容区小。

1
clip: rect(<top>, <right>, <bottom>, <left>)

如果我们要进行裁切的话,clip有且仅有这一种用法,接收的四个值是用于指定矩形四条边的位置,而不是四个点的坐标。

要注意的是:top和bottom是基于盒子的顶部边缘开始算的,left和right是基于盒子的左侧边缘开始算的

所以我们要传入的四个值分别是:矩形上边距离盒子顶部的距离,矩形右边距离盒子左侧的距离,矩形下边距离盒子顶部的距离,矩形左边距离盒子左侧的距离。

因为传入的不是坐标值,可能一下子不太适应,给一个简单的例子吧,比如我们有一个绝对定位的div,其宽度为800px,高度为400px,此时我们要将其裁剪为只显示右边一半区域,应该怎么写呢?

1
clip: rect(0, 800px, 400px, 400px)

确实不太符合常用坐标的直觉,熟悉一下也很容易接受的。

拖动事件

确定好怎么裁切内容,接下来就要开始解决怎么改变裁切区域大小的问题了。

首先,我们需要一个元素供我们拖动,这个看你设计的样式,怎么实现都可以,不过建议是可以放到一个初始裁切区域大小相同位置相同的盒子中,然后将其绝对定位到左侧,这样在拖动过程中只需要改变盒子的大小就可以使拖动条位置跟随变化了。

这里并没有使用更新的drapstart,drag...等事件,而是监听了鼠标的mousedown,mousemove,mouseup事件。

不过绑定事件的时候要注意了,不要把所有的事件都绑定到滚动条上哦,只需要保证是在滚动条上按下鼠标即可。因为拖动过程中很容易把鼠标脱离滚动条,导致事件一致在触发了。

所以我们将mousedown事件绑定到滚动条,其余两个事件绑定到window上就可以了。

我们需要保证鼠标是在滚动条上按下才可以拖动,所以加入一个中间变量,鼠标的按下与抬起事件也就比较简单了。

1
2
3
4
5
6
7
8
9
10
11
12
// 中间变量
let ifKeyDown = false

// mousedown
function handleMouseDown(){
ifKeyDown = true
}

// mouseup
function handleMouseUp(){
ifKeyDown = false
}

接下来我们的拖动事件就可以根据中间变量的状态来决定是否执行了

1
2
3
4
5
function handleMouseMove(){
if(ifKeyDown){
// 拖动逻辑
}
}

计算元素距离实现拖动逻辑

在拖动过程中肯定要实时计算鼠标的位置来改变元素的裁切区域,而且我们需要限制拖动的边界值,我是通过计算元素距离屏幕左侧的位置来计算的。但是offsetLeft只能获取到元素距离其父元素左侧的距离,所以我们有必要写一个函数用于计算。

1
2
3
4
5
6
7
8
9
function getLeftPX(dom){
let curLeft = dom.offsetLeft
let parent = dom.offsetParent
while (parent != null){
curLeft += parent.offsetLeft
parent = parent.offsetLeft
}
return curLeft
}

原图放到底部是不用动的,所以只需要改变对比图的裁切大小就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function handleMouseMove(){
if(ifKeyDown){
// 拖动条
let dragDom = document.getElementById('dragDom')
// 对比图 - 其大小是与原图一致的,只是通过裁切限制了显示的区域
let resultImg = document.getElementById('resultImg')
// 计算左右边界
let minLeft = getLeftPX(resultImg)
let maxLeft = minLeft + 元素的宽度
// 滚动条距离左侧的距离
let drapDomToLeft = getLeftPX(drapDom)
// 滚动条盒子之前的宽度
let widthBefore = dragDom.offsetWidth
// 鼠标的位置
let clientX = event.clientX
// 初始化宽度的变化
let changeWidth;
// 根据向左还是向右拖动执行不同操作
if(clientX < dragDomToLeft){
// 向左拖动
changeWidth = dragDomToLeft - clientX
// 没有超过左边界
if(dragDomToLeft - changeWidth > minLeft){
dragDom.style.width = (changeWidth + widthBefore) + 'px'
// 计算对比图裁切区域左侧的位置
let imgLeft = (元素宽度 - widthBefore) - changeWidth
resultImg.style.clip = `rect(原来的上边界, 原来的右边界, 原来的下边界, ${imgLeft}px)`
}
}else{
// 向右拖动
changeWith = clientX - dragDomToLeft
if(dragDomToLeft + changeWidth < maxLeft){
dragDom.style.width = (widthBefore - changeWidth) + 'px'
let imgLeft = (元素宽度 - widthBefore) + changeWidth
resultImg.style.clip = `rect(原来的上边界, 原来的右边界, 原来的下边界, ${imgLeft}px)`
}
}
}
}

到此为止,效果已经基本实现了,计算可能有点儿绕,自己实际写一些应该就还挺容易理解的。

作者

胡兆磊

发布于

2021-12-02

更新于

2022-10-23

许可协议