【动手系列】以鼠标为中心对图片进行缩放

在上一家公司开发的时候,看到一个流程图组件,里面有一个拖拽和缩放的功能,缩放很鸡肋,不会以鼠标中心点缩放。所以用户在缩放的时候,还得不停的拖拽。

以用户体验为第一的原则,我就想着把这个功能的体验弄好一点,在网上找了一些资料:

最后选择的是 css3 实现,效果图:

思路

最开始界面应该是有一个 div(400 * 300),如下:

初始化div

然后假设用户进行鼠标放大之后,scale是 1.4:

图片放大

这个时候,transform的值应该是translate(-80px, -60px) scale(1.4)

计算过程:(scale后的高度 - 最开始的高度) * 鼠标在图片高度位置的比例

  1. 图片高度是 300px,假设鼠标在 150px 的位置,得到位置比例是 150 / 300 = 0.5,放大后的高度是 300 1.4 = 420px,向上增加的高度应该是 (420 - 300) 0.5 = 60px。
  2. 不管是缩小还是放大,都把上一次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();每次进行缩放时获取父元素的 topleft 值,用来获取鼠标坐标在图片比例最重要的一步。

代码还有很多缺陷,总会一步步完善的,努力吧。

Last modification:August 8th, 2019 at 03:32 pm
If you think my article is useful to you, please feel free to appreciate

2 comments

  1. repostone

    非技术的路过。

  2. 心灵博客

    不错,用户体验就是在这些细节方面。

Leave a Comment