业务需求
用户上传一张户型图,并且在图片描很多测试点。
- 鼠标放大缩小、拖拽
- 触屏手势放大缩小、拖拽
- 测试点需要跟随图片放大缩小、移动
先看最终完成的效果
基本页面结构
<div class="popup">
<img src="https://picsum.photos/800/600" alt="户型图">
<div class="site-numbers">
<div class="site-number" style="left: 100px; top: 100px;">A</div>
<div class="site-number" style="left: 200px; top: 200px;">B</div>
<div class="site-number" style="left: 300px; top: 300px;">C</div>
<div class="site-number" style="left: 400px; top: 400px;">D</div>
</div>
</div>
这里比较简单,一个div 跟一堆的站点编号定位点
.popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 800px;
height: 600px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.popup img {
width: 100%;
height: 100%;
object-fit: contain;
cursor: move;
}
.popup .site-numbers {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; // 重点
transform-origin: top left; // 重点
}
.popup .site-number {
position: absolute;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background-color: #f00;
color: #fff;
border-radius: 50%;
pointer-events: auto; // 重点
cursor: pointer;
}
开始js表演
鼠标滑轮放大缩小
const popup = document.querySelector('.popup');
const img = popup.querySelector('img');
const siteNumbers = popup.querySelector('.site-numbers');
let scale = 1;
// 缩放
const handlePinch = (event) => {
event.preventDefault();
// 根据deltaY正负值决定放大还是缩小
scale *= event.deltaY > 0 ? 0.9 : 1.1;
// 设置放大倍数最大值最小值
scale = Math.min(Math.max(0.5, scale), 2);
// 使用css对元素直接放大缩小
img.style.transform = `scale(${scale})`;
siteNumbers.style.transform = `scale(${scale}`;
};
// 注册事件
img.addEventListener('wheel', handlePinch);
鼠标拖拽
// 拖拽
const popup = document.querySelector('.popup');
const img = popup.querySelector('img');
const siteNumbers = popup.querySelector('.site-numbers');
const siteNumberList = siteNumbers.querySelectorAll('.site-number');
let lastX = 0;
let lastY = 0;
let isDragging = false;
let imgOffsetX = 0;
let imgOffsetY = 0;
const rectBefore = img.getBoundingClientRect();
// 记录鼠标按下的位置并标记拖拽状态
const handleDragStart = (event) => {
event.preventDefault();
lastX = event.clientX || event.touches[0].clientX;
lastY = event.clientY || event.touches[0].clientY;
isDragging = true;
};
const handleDragMove = (event) => {
event.preventDefault();
if (!isDragging) {
return;
}
const deltaX = (event.clientX || event.touches[0].clientX) - lastX;
const deltaY = (event.clientY || event.touches[0].clientY) - lastY;
// 移动图片
imgOffsetX += deltaX;
imgOffsetY += deltaY;
img.style.transform = `translate(${imgOffsetX}px, ${imgOffsetY}px) scale(${scale})`;
// 这里很重要,因为img放大缩小后,其位置是会发生改变的
const rectAfter = img.getBoundingClientRect();
const offsetX = rectAfter.left - rectBefore.left;
const offsetY = rectAfter.top - rectBefore.top;
siteNumbers.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
// 记录上次移动xy
lastX = event.clientX || event.touches[0].clientX;
lastY = event.clientY || event.touches[0].clientY;
};
const handleDragEnd = (event) => {
event.preventDefault();
isDragging = false;
};
// 注册事件
img.addEventListener('mousedown', handleDragStart);
// 鼠标移动和弹起绑定在document操作更流畅
document.addEventListener('mousemove', handleDragMove);
document.addEventListener('mouseup', handleDragEnd);
移动端
// 移动端手势事件
let lastDistance = 0;
let isPinching = false;
const handleTouchStart = (event) => {
// 判断双指操作
if (event.touches.length === 2) {
// 勾股定理求斜边
lastDistance = Math.sqrt(
(event.touches[0].clientX - event.touches[1].clientX) ** 2 +
(event.touches[0].clientY - event.touches[1].clientY) ** 2
);
isPinching = true;
} else if (event.touches.length === 1) {
handleDragStart(event);
}
};
const handleTouchMove = (event) => {
if (isPinching) {
const distance = Math.sqrt(
(event.touches[0].clientX - event.touches[1].clientX) ** 2 +
(event.touches[0].clientY - event.touches[1].clientY) ** 2
);
// 计算放大比例
scale *= distance / lastDistance;
scale = Math.min(Math.max(0.5, scale), 2);
img.style.transform = `translate(${imgOffsetX}px, ${imgOffsetY}px) scale(${scale})`;
const rectAfter = img.getBoundingClientRect();
const offsetX = rectAfter.left - rectBefore.left;
const offsetY = rectAfter.top - rectBefore.top;
siteNumbers.style.transform = `translate(${imgOffsetX}px, ${imgOffsetY}px) scale(${scale})`;
lastDistance = distance;
} else {
handleDragMove(event);
}
};
const handleTouchEnd = (event) => {
if (isPinching) {
isPinching = false;
} else {
handleDragEnd(event);
}
};
img.addEventListener('touchstart', handleTouchStart);
img.addEventListener('touchmove', handleTouchMove);
img.addEventListener('touchend', handleTouchEnd);
整体代码
总结
- 使用pointer-events来阻止元素成为鼠标事件
- 使用transform 改变大小,还不使用改变长宽,优点是啥?使用了GPU加速、不影响其它元素布局,减少重绘提高性能