阅读 1649
原生JS图片拖动、缩放、边界等问题总结

原生JS图片拖动、缩放、边界等问题总结

前言

大家好,我是浴室熊~ 本前端萌新时隔半年又开始用原生js写起了图片拖拽功能,虽才半年,也能明显感觉到自己的代码水平有所提升,下面给自己做一些总结并记录一些踩过的坑,希望各位大佬指点~

看见电商的查看商品图片细节的功能,想着自己能不能写出来,随后一发不可收拾~成功打发了一晚上的时间。咳咳言归正传,功能在脑子里转了一下感觉很简单,无非就是图片拖动图片缩放,但其实写的过程中还是有很多坑的,下面来一一细说:

1. 图片拖动

必然少不了事件:mousedown、mousemove、mouseup

<div class="box">
    <div class="avatar">
        <img src="./avatar1.jpg" draggable="false">
    </div>
</div>
复制代码

首先,很简单的结构,唯一需要注意的是img标签的draggable属性,draggable属性在imga标签中默认true,先置成falsedraggable属于浏览器默认拖动,会跟自己写的事件冲突从而导致拖不动、拖动残影等等奇怪的问题。大概就像这样

image.png image.png

当然,还可以取消图片/文字选中进一步增加体验感

document.addEventListener('selectstart', e => { e.preventDefault() })

鼠标按下

获取图片的位置并添加监听鼠标移动、抬起事件,这里本熊使用event.clientX和event.clientY计算

const mouseDown = e => {
    let transf = getTransform(oDiv)
    x = e.clientX - transf.transX // 图片初始位置
    y = e.clientY - transf.transY // 图片初始位置
    document.addEventListener('mousemove', mouseMove)
    document.addEventListener('mouseup', mouseUp)
}
复制代码

图片相对浏览器(有效区域)的位置减去图片的translateXtranslateY的值(图片无lefttop等样式)赋值给变量xy,关于transform怎么获取下面会说到

鼠标拖动

鼠标当前相对浏览器(有效区域)位置减去鼠标按下时的位置 计算出图片移动的距离,通过 DOM.style.transform 更新图片位置

const mouseMove = e => {
    let multiple = getTransform(oDiv).multiple
    let moveX = e.clientX - x // x向移动距离
    let moveY = e.clientY - y // y向移动距离
    let newTransf = limitBorder(oDiv, oBox, moveX, moveY, multiple)
    oDiv.style.transform = `matrix(${multiple}, 0, 0, ${multiple}, ${newTransf.transX}, ${newTransf.transY})`
}
复制代码

因为要写图片缩放的原因,这里一并把图片的scale倍数值multiple获取到,一并更新transformlimitBorder函数通过多层判断给出合理(不超边框等~)的新的translateX和translateY,下面会细说,我们先说鼠标抬起

鼠标抬起

鼠标抬起没有什么特别的,无非就是移除监听器,但要注意的一点是,很多人只移除mousemove的回调函数,其实mouseup也要一并移除,否则在多次拖动之后会有多个mouseup事件(addEventListener可以重复添加同种事件)导致拖动延迟

const mouseUp = () => {
    document.removeEventListener('mousemove', mouseMove)
    document.removeEventListener('mouseup', mouseUp)
}
复制代码

没错,鲁迅曾经说过——我杀我自己~

鼠标滚轮滚动

根据event.deltaY判断放大 / 缩小,将缩放倍数multiple乘 / 除以某个倍数(这里本熊使用的是DELTA = 1.1倍)然后更新transformscale

const zoom = e => {
    let transf = getTransform(oDiv)
    if (e.deltaY < 0) {
        transf.multiple *= DELTA // 放大DELTA倍
    } else {
        transf.multiple /= DELTA // 缩小DELTA倍
    }
    let newTransf = limitBorder(oDiv, oBox, transf.transX, transf.transY, transf.multiple)
    oDiv.style.transform = `matrix(${transf.multiple}, 0, 0, ${transf.multiple}, ${newTransf.transX}, ${newTransf.transY})`
}
复制代码

2. 边界问题

半年前本熊在写边界问题的时候,用了多个if判断上下左右四个边界、如果超出就等于边界值。咳咳,可现在的我不一样了,要想不出界,不就是一个数学的闭区间问题吗x方向移动距离∈[左边界, 右边界]y方向移动距离∈[上边界, 下边界] ,利用Math函数的方法即可

transX = Math.max(Math.min(moveX, right), left)

transY = Math.max(Math.min(moveY, bottom), top)

至于边界值具体怎么算就不做过多解释了、简单的加减法以及缩放倍率的乘法

那么问题来了:上面的情况是图片小于父盒子,如果图片放大到盒子放不下呢?

本熊的做法:上面的情况是图片“靠内边”,如果图片宽或高人一边超过盒子,便让他最多“靠外边”,如图所示

边界.gif

当图片超出时,很简单~此时图片相当于盒子,盒子相当于图片,只不过我们拖动的是盒子,啊 恍然大悟

具体代码就只需把Math.maxMath.min调换一下即可,下面贴一下本熊的代码

const limitBorder = (innerDOM, outerDOM, moveX, moveY, multiple) => {
    let { clientWidth: innerWidth, clientHeight: innerHeight, offsetLeft: innerLeft, offsetTop: innerTop } = innerDOM
    let { clientWidth: outerWidth, clientHeight: outerHeight } = outerDOM
    let transX
    let transY
    // 放大的图片超出box时 图片最多拖动到与边框对齐
    if (innerWidth * multiple > outerWidth || innerHeight * multiple > outerHeight) {
        if (innerWidth * multiple > outerWidth && innerWidth * multiple > outerHeight) {
            transX = Math.min(Math.max(moveX, outerWidth - innerWidth * (multiple + 1) / 2 - innerLeft), -innerLeft + innerWidth * (multiple - 1) / 2)
            transY = Math.min(Math.max(moveY, outerHeight - innerHeight * (multiple + 1) / 2 - innerTop), -innerTop + innerHeight * (multiple - 1) / 2)
        } else if (innerWidth * multiple > outerWidth && !(innerWidth * multiple > outerHeight)) {
            transX = Math.min(Math.max(moveX, outerWidth - innerWidth * (multiple + 1) / 2 - innerLeft), -innerLeft + innerWidth * (multiple - 1) / 2)
            transY = Math.max(Math.min(moveY, outerHeight - innerHeight * (multiple + 1) / 2 - innerTop), -innerTop + innerHeight * (multiple - 1) / 2)
        } else if (!(innerWidth * multiple > outerWidth) && innerWidth * multiple > outerHeight) {
            transX = Math.max(Math.min(moveX, outerWidth - innerWidth * (multiple + 1) / 2 - innerLeft), -innerLeft + innerWidth * (multiple - 1) / 2)
            transY = Math.min(Math.max(moveY, outerHeight - innerHeight * (multiple + 1) / 2 - innerTop), -innerTop + innerHeight * (multiple - 1) / 2)
        }
    }
    // 图片小于box大小时 图片不能拖出边框
    else {
        transX = Math.max(Math.min(moveX, outerWidth - innerWidth * (multiple + 1) / 2 - innerLeft), -innerLeft + innerWidth * (multiple - 1) / 2)
        transY = Math.max(Math.min(moveY, outerHeight - innerHeight * (multiple + 1) / 2 - innerTop), -innerTop + innerHeight * (multiple - 1) / 2)
    }
    return { transX, transY }
}
复制代码

本熊这里之所以写了辣么辣么多的if-else因为上面我们分析的情况只是图片和盒子都是正方形啦,实际还得考虑单边超出的情况,也很简单啦,超出的一边就把Math.maxMath.min对换就行~

再说一下,之所以要封装这个函数是因为缩放的时候也要考虑边界,请看下面对比图:

缩放边界1.gif 缩放边界2.gif

原理与拖动相同不再赘述,在鼠标滚轮回调函数zoom中更新transform前调用limitBorder方法即可

3. 获取transform

半年前刚毕业还是新手的我,曾一度使用DOM.style.transform来获取tranform

但这只是获取的是同css代码一样的字符串 translate(100px, 100px) 这种的话,如何获取x,y方向的偏移量呢?可能你会觉得使用split就可以,但如果我的属性是translate(100px, 100px) scale(2) 又或者是 translate(100px, 100px) scale(2) rotate(30deg)好像不是很好获取呢~

getComputedStyle 以及 matrix 矩阵

Window.getComputedStyle()方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有CSS属性的值。 私有的CSS属性值可以通过对象提供的API或通过简单地使用CSS属性名称进行索引来访问。

MDN官方的解释比较难懂,其实简单来说 getComputedStyle(DOM).transform 会得到一个矩阵 matrix(1,0,0,1,100,100),在没有旋转rotate、拉伸skew的情况下,我们只需要关注括号里的1、4、5、6位,分别代表x向缩放倍数、y向缩放倍数、x向偏移量和y向偏移量。这种统一形式的字符串,我们完全可以用split分隔为数组再依次取到

const getTransform = DOM => {
    let arr = getComputedStyle(DOM).transform.split(',')
    return {
        transX: isNaN(+arr[arr.length - 2]) ? 0 : +arr[arr.length - 2], // 获取translateX
        transY: isNaN(+arr[arr.length - 1].split(')')[0]) ? 0 : +arr[arr.length - 1].split(')')[0], // 获取translateX
        multiple: +arr[3] // 获取图片缩放比例
    }
}
复制代码

需要注意的是,当该DOM没有transform属性时,getComputedStyle(DOM).transform返回的是字符串 'none',因此我们需要使用isNaN判断一下

以上~第一次认真写博客,不对的地方请大佬们慷慨指正!感谢!

愿明天也是没有BUG的一天,鲁迅曾经说过——我没说过!

文章分类
前端
文章标签