前言
大家好,我是浴室熊~ 本前端萌新时隔半年又开始用原生js写起了图片拖拽功能,虽才半年,也能明显感觉到自己的代码水平有所提升,下面给自己做一些总结并记录一些踩过的坑,希望各位大佬指点~
看见电商的查看商品图片细节的功能,想着自己能不能写出来,随后一发不可收拾~成功打发了一晚上的时间。咳咳言归正传,功能在脑子里转了一下感觉很简单,无非就是图片拖动、图片缩放,但其实写的过程中还是有很多坑的,下面来一一细说:
1. 图片拖动
必然少不了事件:mousedown、mousemove、mouseup
<div class="box">
<div class="avatar">
<img src="./avatar1.jpg" draggable="false">
</div>
</div>
首先,很简单的结构,唯一需要注意的是img
标签的draggable
属性,draggable
属性在img
、a
标签中默认true
,先置成false
,draggable
属于浏览器默认拖动,会跟自己写的事件冲突从而导致拖不动、拖动残影等等奇怪的问题。大概就像这样
当然,还可以取消图片/文字选中进一步增加体验感
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)
}
图片相对浏览器(有效区域)的位置减去图片的translateX
和translateY
的值(图片无left
、top
等样式)赋值给变量x
, y
,关于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
获取到,一并更新transform
,limitBorder
函数通过多层判断给出合理(不超边框等~)的新的translateX和translateY
,下面会细说,我们先说鼠标抬起
鼠标抬起
鼠标抬起没有什么特别的,无非就是移除监听器,但要注意的一点是,很多人只移除mousemove
的回调函数,其实mouseup
也要一并移除,否则在多次拖动之后会有多个mouseup
事件(addEventListener可以重复添加同种事件)导致拖动延迟
const mouseUp = () => {
document.removeEventListener('mousemove', mouseMove)
document.removeEventListener('mouseup', mouseUp)
}
没错,鲁迅曾经说过——我杀我自己~
鼠标滚轮滚动
根据event.deltaY
判断放大 / 缩小,将缩放倍数multiple
乘 / 除以某个倍数(这里本熊使用的是DELTA = 1.1
倍)然后更新transform
的scale
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)
至于边界值具体怎么算就不做过多解释了、简单的加减法以及缩放倍率的乘法
那么问题来了:上面的情况是图片小于父盒子,如果图片放大到盒子放不下呢?
本熊的做法:上面的情况是图片“靠内边”,如果图片宽或高人一边超过盒子,便让他最多“靠外边”,如图所示
当图片超出时,很简单~此时图片相当于盒子,盒子相当于图片
,只不过我们拖动的是盒子,啊 恍然大悟
具体代码就只需把Math.max
和Math.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.max
和Math.min
对换就行~
再说一下,之所以要封装这个函数是因为缩放的时候也要考虑边界,请看下面对比图:
原理与拖动相同不再赘述,在鼠标滚轮回调函数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的一天,鲁迅曾经说过——我没说过!