用原生JS实现一个简易的放大镜功能
附带放大镜相关公式 以及 offsetX,offsetY抖动解决办法
效果如图:

先完成HTML结构:
<div class="zoom">
<!-- 小图 -->
<div class="smallImg">
<!-- 遮罩层 -->
<div class="mask"></div>
</div>
<!-- 放置大图的div -->
<div class="big">
<!-- 大图 -->
<img class="bigImg" src="./image.jpg" alt="">
</div>
</div>
CSS样式:
* {
margin: 0;
padding: 0;
}
.smallImg {
position: absolute;
width: 200px;
height: 200px;
left: 300px;
top: 100px;
border: 1px solid #ccc;
background-image: url('./image.jpg');
background-size: cover;
}
.mask {
display: none;
position: absolute;
width: 50px;
height: 50px;
background-color: red;
opacity: .3;
}
.big {
/* 大图默认隐藏 */
display: none;
position: absolute;
top: 100px;
left: 502px;
width: 300px;
height: 300px;
border: 1px solid #ccc;
/* 让大图溢出部分隐藏 */
overflow: hidden;
}
.bigImg {
position: absolute;
width: 1200px;
height: 1200px;
}
实现放大镜的关键点
- 鼠标移入小图区域时,显示大图和小图上的遮罩层,鼠标移出时,隐藏大图和遮罩层
获取元素,监听 mouseenter 鼠标进入和 mouseleave 鼠标离开事件,设置 display 的值即可
const smallImg = document.querySelector('.smallImg')
const mask = document.querySelector('.mask')
const big = document.querySelector('.big')
const bigImg = document.querySelector('.bigImg')
smallImg.addEventListener('mouseenter', () => {
mask.style.display = 'block'
big.style.display = 'block'
})
smallImg.addEventListener('mouseleave', () => {
mask.style.display = 'none'
big.style.display = 'none'
})
- 监测到鼠标移动距离,控制遮罩层移动
首先要监听 mousemove 事件:mousemove 事件是一个实时响应的事件,当鼠标指针的位置发生变化时(至少移动一个像素),就会触发该事件
在事件回调中:可以通过形参 e 来接收鼠标事件对象,对象中有2个属性:
offsetX:当鼠标事件发生时,鼠标坐标 到 事件源元素左侧的距离
offsetY:当鼠标事件发生时,鼠标坐标 到 事件源元素顶部的距离
由于遮罩层开启了绝对定位,其父元素小图开启了相对定位,所以遮罩层的 left 和 top 是相对于小图的,可以通过设置 left 和 top 的值指定它在小图上的位置:
遮罩层的 left = e.offsetX - 遮罩层 offsetWidth / 2
遮罩层的 top = e.offsetY - 遮罩层 offsetHeight / 2
对于 offsetWidth、offsetHeight :
offsetWidth :元素的自身宽度,包含元素自身设置的 width + padding + border
offsetHeight :元素的自身高度,包含元素自身设置的 height + padding + border
smallImg.addEventListener('mousemove', (e) => {
// 遮罩层移动距离
let left = e.offsetX - mask.offsetWidth / 2
let top = e.offsetY - mask.offsetHeight / 2
mask.style.left = left + 'px'
mask.style.top = top + 'px'
}
这样,遮罩层就可以跟随鼠标移动了
- 限制遮罩层移动范围
但我们会发现一个bug:遮罩层可能会移出小图区域,像这样:

因为我们只想要放大小图,所以遮罩层的活动范围应该完全限制在小图的宽高里面
遮罩层的 left 的最小值 = 0
遮罩层的 left 的最大值 = 小图的 offsetWidth - 遮罩层的 offsetWidth - 2
此处为什么要减 2 ?
因为小图两边有 2px 的 border,offsetWidth 是包含border的,而 left 的最大值应该是小图的 width - 遮罩层的 width,应该减去 border 的宽度
同理:
遮罩层的 top 的最小值 = 0
遮罩层的 top 的最大值 = 小图的 offsetHeight - 遮罩层的 offsetHeight - 2
// 控制遮罩层移动范围
left < 0 ? left = 0 : left
left > smallImg.offsetWidth - mask.offsetWidth - 2 ? left = smallImg.offsetWidth - mask.offsetWidth - 2 : left
top < 0 ? top = 0 : top
top > smallImg.offsetHeight - mask.offsetHeight - 2 ? top = smallImg.offsetHeight - mask.offsetHeight - 2 : top
- 设置大图的移动方向和距离
将放置大图div的样式 overflow: hidden; 去掉,我们可以观察到:遮罩层的 left 值变化和大图的 left 值变化方向是相反的,top 值同理

而移动距离,与放大镜倍数有关:
大图移动距离 = - 遮挡层移动距离 left × 放大镜倍数
放大镜倍数 = 大图宽高 / 小图宽高
示例的放大镜倍数 = 1200px / 200px = 6
// 大图移动距离
bigImg.style.left = -left * 6 + 'px'
bigImg.style.top = -top * 6 + 'px'
需要注意的一点是,我们应当保证:大图宽高 / 放置大图的div宽高 = 小图宽高 / 小图遮罩层宽高,才能实现放大镜效果
在本例中:1200px / 300px = 200px / 50px
- 关于 offsetX,offsetY 抖动的问题
练习过程中发现随着鼠标移动,遮罩层一直抖动,

我们虽然监听的是小图的 mousemove 事件,但小图和其子元素遮罩层都会触发该事件,当鼠标在遮罩层上时,mousemove 事件对象是遮罩层元素,当鼠标在小图上时,事件对象又变成了小图元素,offsetX是鼠标到触发事件对象的位置。所以当事件对象是遮罩层时offsetX的值比较小,计算后会移到左上角,但移动后事件对象又变成小图元素了,计算后又回到了鼠标位置,循环反复,造成了闪动效果。
解决方案:给遮罩层添加css属性:pointer-events: none;
让遮罩层不参与鼠标事件,event.target 一直是小图,就可以解决了
完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
.smallImg {
position: absolute;
width: 200px;
height: 200px;
left: 300px;
top: 100px;
border: 1px solid #ccc;
background-image: url('./image.jpg');
background-size: cover;
}
.mask {
display: none;
position: absolute;
width: 50px;
height: 50px;
background-color: red;
opacity: .3;
pointer-events: none;
}
.big {
/* 大图默认隐藏 */
display: none;
position: absolute;
top: 100px;
left: 502px;
width: 300px;
height: 300px;
border: 1px solid #ccc;
/* 让大图溢出部分隐藏 */
overflow: hidden;
}
.bigImg {
position: absolute;
width: 1200px;
height: 1200px;
}
</style>
</head>
<body>
<div class="zoom">
<div class="smallImg">
<div class="mask"></div>
</div>
<div class="big">
<img class="bigImg" src="./image.jpg" alt="">
</div>
</div>
<script>
const smallImg = document.querySelector('.smallImg')
const mask = document.querySelector('.mask')
const big = document.querySelector('.big')
const bigImg = document.querySelector('.bigImg')
smallImg.addEventListener('mouseenter', () => {
mask.style.display = 'block'
big.style.display = 'block'
})
smallImg.addEventListener('mouseleave', () => {
mask.style.display = 'none'
big.style.display = 'none'
})
smallImg.addEventListener('mousemove', (e) => {
// 遮罩层移动距离
let left = e.offsetX - mask.offsetWidth / 2
let top = e.offsetY - mask.offsetHeight / 2
// 控制遮罩层移动范围
left < 0 ? left = 0 : left
left > smallImg.offsetWidth - mask.offsetWidth - 2 ? left = smallImg.offsetWidth - mask.offsetWidth - 2 : left
top < 0 ? top = 0 : top
top > smallImg.offsetHeight - mask.offsetHeight - 2 ? top = smallImg.offsetHeight - mask.offsetHeight - 2 : top
mask.style.left = left + 'px'
mask.style.top = top + 'px'
// 大图移动距离
bigImg.style.left = -left * 6 + 'px'
bigImg.style.top = -top * 6 + 'px'
})
</script>
</body>
</html>
结束;
2127



被折叠的 条评论
为什么被折叠?



