在上一家公司開(kāi)發(fā)的時(shí)候肤京,看到一個(gè)流程圖組件,里面有一個(gè)拖拽和縮放的功能,縮放很雞肋忘分,不會(huì)以鼠標(biāo)中心點(diǎn)縮放棋枕。所以用戶在縮放的時(shí)候,還得不停的拖拽妒峦。
以用戶體驗(yàn)為第一的原則重斑,我就想著把這個(gè)功能的體驗(yàn)弄好一點(diǎn),在網(wǎng)上找了一些資料:
最后選擇的是 css3 實(shí)現(xiàn)窥浪,效果圖:
image
思路
最開(kāi)始界面應(yīng)該是有一個(gè) div(400 * 300),如下:
初始化div
然后假設(shè)用戶進(jìn)行鼠標(biāo)放大之后笛丙,scale
是 1.4:
圖片放大
這個(gè)時(shí)候漾脂,transform
的值應(yīng)該是translate(-80px, -60px) scale(1.4)
。
計(jì)算過(guò)程:(scale
后的高度 - 最開(kāi)始的高度) * 鼠標(biāo)在圖片高度位置的比例胚鸯。
- 圖片高度是 300px骨稿,假設(shè)鼠標(biāo)在 150px 的位置,得到位置比例是 150 / 300 = 0.5姜钳,放大后的高度是 300 * 1.4 = 420px坦冠,向上增加的高度應(yīng)該是 (420 - 300) * 0.5 = 60px。
- 不管是縮小還是放大哥桥,都把上一次
translate
對(duì)應(yīng)坐標(biāo)的值 - 這次得到的值辙浑,最后得出translate
屬性上y
的值是上一次的值(0) - 60 =-60
鼠標(biāo)在圖片上的比例,也就是 150px 是如何來(lái)的拟糕,以
y
軸為例:鼠標(biāo)的位置(event.y
) - 縮放元素的父元素距離屏幕頂部的距離(通過(guò)dom.getBoundingClientRect().top
可以獲取到)
代碼如下:
/**
* 元素縮放判呕、拖拽
* @param {string | HTMLBaseElement} selector 元素選擇器或者一個(gè)元素
* @param {number} [scale] 初始化的縮放比
* @param {object} [option] 其他選項(xiàng)
* @param {number} [option.interval = 0.1] 每次疊加的間隔數(shù)
* @param {number} [option.minScale = 0.5] 最小縮放
* @param {number} [option.maxScale = 3] 最大縮放
* @param {number} [option.disabledZoom = false] 是否禁用縮放,默認(rèn) 否
* @param {number} [option.disabledDrag = false] 是否禁用拖拽已卸,默認(rèn) 否
* @param {number} [option.slopOver = true] 是否可以超出父容器邊界佛玄,默認(rèn) 是
*/
function zoom (selector, scale = 1, option = {}) {
// 記錄 Translate 的坐標(biāo)值
let prevTranslateMap = {
x: 0,
y: 0
}
let zoomDom = selector,
mx, // 記錄鼠標(biāo)按下時(shí)的 x 坐標(biāo)
my, // 記錄鼠標(biāo)按下時(shí)的 y 坐標(biāo)
tLeft = prevTranslateMap.x, // 最后設(shè)置的 translateX 值
tTop = prevTranslateMap.y, // 最后設(shè)置的 translateY 值
newsetWidth, // 拖動(dòng)容器最新的寬度
newsetHeight, // 拖動(dòng)容器最新的高度
firstMoveFlag = false // 第一次移動(dòng)標(biāo)記,防止用戶第一次按下和松開(kāi)鼠標(biāo)但并未移動(dòng)累澡,第二次移動(dòng)時(shí) dom 出現(xiàn)閃現(xiàn)的情況
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;
// 滾動(dòng)事件兼容文章(https://www.zhangxinxu.com/wordpress/2013/04/js-mousewheel-dommousescroll-event/)
!disabledZoom && zoomDom.addEventListener('mousewheel', ev => {
const isZoomOut = ev.deltaY < 0; // 縮小
// 鼠標(biāo)坐標(biāo)
const { x: mouseX, y: mouseY } = ev;
// 元素當(dāng)前寬高
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
// 需要重新運(yùn)算的 translate 坐標(biāo)
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()
})
// 鼠標(biāo)按下去
!disabledDrag && zoomDom.addEventListener('mousedown', mousedown);
function mousedown(ev) {
mx = ev.x;
my = ev.y;
const clientRect = zoomDom.getBoundingClientRect()
newsetWidth = clientRect.width
newsetHeight = clientRect.height
// 鼠標(biāo)移動(dòng)
document.addEventListener('mousemove', mousemove);
// 鼠標(biāo)松開(kāi)
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
}
// 設(shè)置樣式
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>
需要注意的幾行代碼梦抢,少了這幾行,縮放就達(dá)不到想要的效果:
-
zoomDom.style.transformOrigin = '0 0';
要給縮放元素設(shè)置該屬性愧哟。 -
const { top: pTop, left: pLeft } = pDom.getBoundingClientRect();
每次進(jìn)行縮放時(shí)獲取父元素的top
和left
值奥吩,用來(lái)獲取鼠標(biāo)坐標(biāo)在圖片比例最重要的一步。
代碼還有很多缺陷蕊梧,總會(huì)一步步完善的霞赫,努力吧。