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

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

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

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

image

思路

最开始界面应该是有一个 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 值,用来获取鼠标坐标在图片比例最重要的一步。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • 1、属性选择器:id选择器 # 通过id 来选择类名选择器 . 通过类名来选择属性选择器 ...
    Yuann阅读 1,621评论 0 7
  • 在介绍有关transform相关的知识之前,先来讲一下transform-origin的用法以及关于角度的几种取值...
    跪键盘的小泰迪阅读 1,219评论 0 2
  • CSS参考手册 一、初识CSS3 1.1 CSS是什么 CSS3在CSS2.1的基础上增加了很多强大的新功能。目前...
    没汁帅阅读 3,564评论 1 13
  • 一、CSS入门 1、css选择器 选择器的作用是“用于确定(选定)要进行样式设定的标签(元素)”。 有若干种形式的...
    宠辱不惊丶岁月静好阅读 1,589评论 0 6
  • 看了很多视频、文章,最后却通通忘记了,别人的知识依旧是别人的,自己却什么都没获得。此系列文章旨在加深自己的印象,因...
    DCbryant阅读 1,857评论 0 4