在上一家公司开发的时候,看到一个流程图组件,里面有一个拖拽和缩放的功能,缩放很鸡肋,不会以鼠标中心点缩放。所以用户在缩放的时候,还得不停的拖拽。
以用户体验为第一的原则,我就想着把这个功能的体验弄好一点,在网上找了一些资料:
最后选择的是 css3 实现,效果图:
image思路
最开始界面应该是有一个 div(400 * 300),如下:
初始化div然后假设用户进行鼠标放大之后,scale
是 1.4:
这个时候,transform
的值应该是translate(-80px, -60px) scale(1.4)
。
计算过程:(scale
后的高度 - 最开始的高度) * 鼠标在图片高度位置的比例。
- 图片高度是 300px,假设鼠标在 150px 的位置,得到位置比例是 150 / 300 = 0.5,放大后的高度是 300 * 1.4 = 420px,向上增加的高度应该是 (420 - 300) * 0.5 = 60px。
- 不管是缩小还是放大,都把上一次
translate
对应坐标的值 - 这次得到的值,最后得出translate
属性上y
的值是上一次的值(0) - 60 =-60
鼠标在图片上的比例,也就是 150px 是如何来的,以
y
轴为例:鼠标的位置(event.y
) - 缩放元素的父元素距离屏幕顶部的距离(通过dom.getBoundingClientRect().top
可以获取到)
代码如下:
/**
* 元素缩放、拖拽
* @param {string | HTMLBaseElement} selector 元素选择器或者一个元素
* @param {number} [scale] 初始化的缩放比
* @param {object} [option] 其他选项
* @param {number} [option.interval = 0.1] 每次叠加的间隔数
* @param {number} [option.minScale = 0.5] 最小缩放
* @param {number} [option.maxScale = 3] 最大缩放
* @param {number} [option.disabledZoom = false] 是否禁用缩放,默认 否
* @param {number} [option.disabledDrag = false] 是否禁用拖拽,默认 否
* @param {number} [option.slopOver = true] 是否可以超出父容器边界,默认 是
*/
function zoom (selector, scale = 1, option = {}) {
// 记录 Translate 的坐标值
let prevTranslateMap = {
x: 0,
y: 0
}
let zoomDom = selector,
mx, // 记录鼠标按下时的 x 坐标
my, // 记录鼠标按下时的 y 坐标
tLeft = prevTranslateMap.x, // 最后设置的 translateX 值
tTop = prevTranslateMap.y, // 最后设置的 translateY 值
newsetWidth, // 拖动容器最新的宽度
newsetHeight, // 拖动容器最新的高度
firstMoveFlag = false // 第一次移动标记,防止用户第一次按下和松开鼠标但并未移动,第二次移动时 dom 出现闪现的情况
const { interval = 0.1, minScale = 0.5, maxScale = 3, slopOver = true, disabledZoom = false, disabledDrag = false } = option
if (typeof selector === 'string') {
zoomDom = document.querySelector(selector)
}
zoomDom.style.transformOrigin = '0 0';
// 获取最初始的宽高
const { width: initWidth, height: initHeight } = zoomDom.getBoundingClientRect()
const pDom = zoomDom.parentElement;
// 滚动事件兼容文章(https://www.zhangxinxu.com/wordpress/2013/04/js-mousewheel-dommousescroll-event/)
!disabledZoom && zoomDom.addEventListener('mousewheel', ev => {
const isZoomOut = ev.deltaY < 0; // 缩小
// 鼠标坐标
const { x: mouseX, y: mouseY } = ev;
// 元素当前宽高
const { height, width } = zoomDom.getBoundingClientRect();
const { top: pTop, left: pLeft } = pDom.getBoundingClientRect()
if (isZoomOut) {
// 缩小
scale -= interval;
if (minScale && scale < minScale) {
scale = minScale
}
} else {
// 放大
scale += interval;
if (maxScale && scale > maxScale) {
scale = maxScale
}
}
// 获取比例
let yScale = (mouseY - pTop - prevTranslateMap.y) / height;
let xScale = (mouseX - pLeft - prevTranslateMap.x) / width;
// 放大后的宽高
const ampWidth = initWidth * scale
const ampHeight = initHeight * scale
// 需要重新运算的 translate 坐标
const y = yScale * (ampHeight - height)
const x = xScale * (ampWidth - width)
// 更新
const translateY = prevTranslateMap.y - y
const translateX = prevTranslateMap.x - x
zoomDom.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
// 记录这次的值
prevTranslateMap = {
x: translateX,
y: translateY
}
ev.preventDefault()
})
// 鼠标按下去
!disabledDrag && zoomDom.addEventListener('mousedown', mousedown);
function mousedown(ev) {
mx = ev.x;
my = ev.y;
const clientRect = zoomDom.getBoundingClientRect()
newsetWidth = clientRect.width
newsetHeight = clientRect.height
// 鼠标移动
document.addEventListener('mousemove', mousemove);
// 鼠标松开
document.addEventListener('mouseup', mouseup);
}
function mousemove(ev) {
firstMoveFlag = true
tTop = prevTranslateMap.y + (ev.y - my)
tLeft = prevTranslateMap.x + (ev.x - mx)
if (!slopOver) {
if (tTop < 0) tTop = 0
if (tLeft < 0) tLeft = 0
const rightBoundary = pDom.offsetWidth - newsetWidth // 右边边界
const bottomBoundary = pDom.offsetHeight - newsetHeight // 下边边界
if (tTop > bottomBoundary) tTop = bottomBoundary
if (tLeft > rightBoundary) tLeft = rightBoundary
}
// 设置样式
zoomDom.style.cssText += `transform: translate(${tLeft}px, ${tTop}px) scale(${scale})`;
}
function mouseup() {
if (firstMoveFlag) {
prevTranslateMap = {
x: tLeft,
y: tTop
}
}
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
}
}
zoom('#drag'); // <div><div id='drag'></div></div>
需要注意的几行代码,少了这几行,缩放就达不到想要的效果:
-
zoomDom.style.transformOrigin = '0 0';
要给缩放元素设置该属性。 -
const { top: pTop, left: pLeft } = pDom.getBoundingClientRect();
每次进行缩放时获取父元素的top
和left
值,用来获取鼠标坐标在图片比例最重要的一步。
代码还有很多缺陷,总会一步步完善的,努力吧。
网友评论