项目里使用到这个功能, 所以就写了个.
刚接触到需求时的思路
第一个想法就是每次将缩放的基点设置为当前鼠标所在的位置, 可是后来发现在一个地方缩放的时候好像没有问题,可是移动鼠标后再次缩放, 图片会有一个明显的平移现象, 显然这个不是想要的结果, 先上这个方法的代码
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<img
width="560"
src="https://t7.baidu.com/it/u=1653814446,2847580380&fm=193&f=GIF"
alt=""
/>
<script>
// 每次缩放的时候, 修改缩放的基点
const img = document.querySelector("img");
const useZoom = (callback = () => {}) => {
let scale = 1;
const zoom = (e) => {
const { offsetX, offsetY } = e;
let _scale = scale;
_scale += e.deltaY / 1000;
if (_scale < 0.3) return;
scale = _scale;
callback({ x: offsetX, y: offsetY }, scale);
};
return {
zoom,
};
};
const update = (position, scale) => {
img.style.transformOrigin = `${position.x}px ${position.y}px`;
img.style.transform = `scale(${scale})`;
};
const { zoom } = useZoom(update);
img.onwheel = zoom;
</script>
</body>
</html>
调整过的思路
然后通过调整思路, 其实以鼠标为中心缩放, 要做的就是先缩小, 在把鼠标对应的点移动到鼠标所在的位置, 也就是每次触发事件的时候,之前偏移的位置 + 这次偏移,鼠标对应点移动的距离就可以
有一下两种实现方式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="log"></div>
<img
width="560"
src="https://t7.baidu.com/it/u=1653814446,2847580380&fm=193&f=GIF"
alt=""
/>
<script>
const useZoom = (el, callback = () => {}) => {
const { width, height } = el.getBoundingClientRect();
let scale = 1;
const transform = {
x: 0,
y: 0,
};
const wheelZoom = (e) => {
e.preventDefault();
let ratio = e.deltaY / 1000;
// 需要缩放的比例
const _scale = scale * ratio + scale;
scale = _scale;
// 图片缩放以中心为基点, 上下各缩放多少
const origin = {
x: ratio * width * 0.5,
y: ratio * height * 0.5,
};
// 鼠标在图片上的坐标
const positionX = e.clientX - transform.x;
const positionY = e.clientY - transform.y;
// 鼠标所在点在图片上缩放后移动的坐标
const ratioX = ratio * positionX;
const ratioY = ratio * positionY;
// 鼠标坐在点移动的位置 - 图片移动的位置 = 需要移动的位置
transform.x -= ratioX - origin.x;
transform.y -= ratioY - origin.y;
callback({ ...transform }, scale);
};
return {
wheelZoom,
};
};
</script>
<script>
const img = document.querySelector("img");
const log = document.querySelector(".log");
img.onload = () => {
const { wheelZoom } = useZoom(img, (transform, scale) => {
// translate 和 scale 的顺序影响最终效果
img.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${scale})`;
log.innerHTML = `x = ${transform.x.toFixed(0)}<br>
y = ${transform.y.toFixed(0)}<br>
scale = ${scale.toFixed(5)}`;
});
img.addEventListener("wheel", wheelZoom);
};
</script>
<style>
* {
margin: 0;
padding: 0;
}
body {
height: 100vh;
background: #000;
overflow: hidden;
}
.wrap {
margin: 200px;
}
img {
touch-action: none;
}
.log {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
padding: 5px;
color: #fff;
font-size: 12px;
line-height: 18px;
pointer-events: none;
}
</style>
</body>
</html>
第二种
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="log"></div>
<div class="wrap">
<img
width="560"
src="https://t7.baidu.com/it/u=1653814446,2847580380&fm=193&f=GIF"
alt=""
/>
</div>
<script>
const useZoom = (el, callback = () => {}) => {
el.style.transformOrigin = "0 0";
/** 缩放的比例 */
let scale = 1;
/** 平移的距离 */
const translateData = { x: 0, y: 0 };
const { width, height } = el.getBoundingClientRect();
/** 重置数据, 并触发回调更新元素 */
const reset = () => {
scale = 1;
translateData = { x: 0, y: 0 };
callback(translateData, scale);
};
const wheelZoom = (event) => {
let _scale = scale;
_scale += event.deltaY > 0 ? -0.09 : 0.1;
if (_scale < 0.3) return;
let _translateData = distanceMovedZoom(event, scale, _scale);
scale = _scale;
// 需要移动的距离 = 已经移动的距离 + 需要再次移动的距离
translateData.x += _translateData.x;
translateData.y += _translateData.y;
callback(translateData, scale);
};
/** 计算每次需要移动的距离 */
const distanceMovedZoom = (
{ offsetX, offsetY },
oldScale,
newScale
) => {
const newWidth = width * newScale;
const newHeight = height * newScale;
const diffWidth = width * oldScale - newWidth;
const diffHeight = height * oldScale - newHeight;
// 鼠标在图片上坐标比例, offsetX 是取原始大小的值, 所用要除 widht
const xRatio = offsetX / width;
const yRatio = offsetY / height;
// 需要再次移动的距离 x = (新的宽度 - 旧的宽度) * 鼠标在旧的宽度的比例
return { x: diffWidth * xRatio, y: diffHeight * yRatio };
};
return {
wheelZoom,
reset,
};
};
</script>
<script>
const img = document.querySelector("img");
const log = document.querySelector(".log");
img.onload = () => {
const { wheelZoom } = useZoom(img, (transform, scale) => {
// translate 和 scale 的顺序影响最终效果
img.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${scale})`;
log.innerHTML = `x = ${transform.x.toFixed(0)}<br>
y = ${transform.y.toFixed(0)}<br>
scale = ${scale.toFixed(5)}`;
});
img.addEventListener("wheel", wheelZoom);
};
</script>
<style>
* {
margin: 0;
padding: 0;
}
body {
height: 100vh;
background: #000;
overflow: hidden;
}
.wrap {
margin: 200px;
}
img {
touch-action: none;
}
.log {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
padding: 5px;
color: #fff;
font-size: 12px;
line-height: 18px;
pointer-events: none;
}
</style>
</body>
</html>
踩的坑
- 项目里是调整宽度, 所以每次获取鼠标在图片中的坐标都是当前大小对应的值, 现在使用的是缩放, 获取的坐标都是相对于原本的宽度
- 缩放后的图片宽度, 获取的是原本的宽度, 缩放对应的宽度需要通过
getBoundingClientRect获取或者计算出来 - transform 里面属性的顺序影响最终效果