根据此示例,我正在努力找出并确定如何缩放鼠标位置。 (https://stackblitz.com/edit/js-fxnmkm?file=index.js)
let node,
scale = 1,
posX = 0,
posY = 0,
node = document.querySelector('.frame');
const render = () => {
window.requestAnimationFrame(() => {
let val = `translate3D(${posX}px, ${posY}px, 0px) scale(${scale})`
node.style.transform = val
})
}
window.addEventListener('wheel', (e) => {
e.preventDefault();
// Zooming happens here
if (e.ctrlKey) {
scale -= e.deltaY * 0.01;
} else {
posX -= e.deltaX * 2;
posY -= e.deltaY * 2;
}
render();
});
我期望的效果基于放大时的该示例(https://codepen.io/techslides/pen/zowLd?editors=0010)。目前,我上面的示例仅缩放到“视口”的中心,但我希望它成为当前光标所在的位置。
我一直在寻找一个无法通过画布实现的解决方案。任何帮助将不胜感激!
注意事项我使用wheel事件的原因是为了模仿Figma(设计工具)的平移和缩放交互。
答案 0 :(得分:5)
缩放和平移元素非常成问题。可以做到,但问题清单很长。我永远不会实现这样的接口。
考虑通过2D或WebGL使用画布来显示此类内容,以节省许多麻烦。
答案的第一部分是使用画布实现的。第二个示例使用相同的界面view
来平移和缩放元素。
由于您只是平移和缩放,因此可以使用非常简单的方法。
以下示例实现了一个名为view的对象。这会保留当前的比例和位置(平移)
它提供了两个用于用户交互的功能。
view.pan(amount)
将按amount.x
,amount.y
所保持的距离(以像素为单位)平移视图view.scaleAt(at, amount)
将在amount
,{{1}所保持的位置上按at.x
(表示比例变化的数字)缩放(放大)视图。 }(以像素为单位)。在示例中,使用at.y
将视图应用于画布渲染上下文,并且每当视图更改时,都会渲染一组随机框。
平移和缩放是通过鼠标事件
使用鼠标按钮拖动以进行平移,滚轮进行缩放
view.apply()
const ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
const rand = (m = 255, M = m + (m = 0)) => (Math.random() * (M - m) + m) | 0;
const objects = [];
for (let i = 0; i < 100; i++) {
objects.push({x: rand(canvas.width), y: rand(canvas.height),w: rand(40),h: rand(40), col: `rgb(${rand()},${rand()},${rand()})`});
}
requestAnimationFrame(drawCanvas);
const view = (() => {
const matrix = [1, 0, 0, 1, 0, 0]; // current view transform
var m = matrix; // alias
var scale = 1; // current scale
var ctx; // reference to the 2D context
const pos = { x: 0, y: 0 }; // current position of origin
var dirty = true;
const API = {
set context(_ctx) { ctx = _ctx; dirty = true },
apply() {
if (dirty) { this.update() }
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
},
get scale() { return scale },
get position() { return pos },
isDirty() { return dirty },
update() {
dirty = false;
m[3] = m[0] = scale;
m[2] = m[1] = 0;
m[4] = pos.x;
m[5] = pos.y;
},
pan(amount) {
if (dirty) { this.update() }
pos.x += amount.x;
pos.y += amount.y;
dirty = true;
},
scaleAt(at, amount) { // at in screen coords
if (dirty) { this.update() }
scale *= amount;
pos.x = at.x - (at.x - pos.x) * amount;
pos.y = at.y - (at.y - pos.y) * amount;
dirty = true;
},
};
return API;
})();
view.context = ctx;
function drawCanvas() {
if (view.isDirty()) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
view.apply(); // set the 2D context transform to the view
for (i = 0; i < objects.length; i++) {
var obj = objects[i];
ctx.fillStyle = obj.col;
ctx.fillRect(obj.x, obj.y, obj.h, obj.h);
}
}
requestAnimationFrame(drawCanvas);
}
canvas.addEventListener("mousemove", mouseEvent, {passive: true});
canvas.addEventListener("mousedown", mouseEvent, {passive: true});
canvas.addEventListener("mouseup", mouseEvent, {passive: true});
canvas.addEventListener("mouseout", mouseEvent, {passive: true});
canvas.addEventListener("wheel", mouseWheelEvent, {passive: false});
const mouse = {x: 0, y: 0, oldX: 0, oldY: 0, button: false};
function mouseEvent(event) {
if (event.type === "mousedown") { mouse.button = true }
if (event.type === "mouseup" || event.type === "mouseout") { mouse.button = false }
mouse.oldX = mouse.x;
mouse.oldY = mouse.y;
mouse.x = event.offsetX;
mouse.y = event.offsetY
if(mouse.button) { // pan
view.pan({x: mouse.x - mouse.oldX, y: mouse.y - mouse.oldY});
}
}
function mouseWheelEvent(event) {
var x = event.offsetX;
var y = event.offsetY;
if (event.deltaY < 0) { view.scaleAt({x, y}, 1.1) }
else { view.scaleAt({x, y}, 1 / 1.1) }
event.preventDefault();
}
body {
background: gainsboro;
margin: 0;
}
canvas {
background: white;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>
此示例使用元素样式转换属性进行缩放和平移。
注意,我使用2D矩阵而不是3d矩阵,因为这会带来许多与下面使用的简单缩放和平移不兼容的问题。
注意,并非所有情况下CSS转换都应用于元素的左上方。在下面的示例中,原点位于元素的中心。因此,缩放时,必须通过减去元素大小的一半来调整在点处缩放。元素大小不受变换的影响。
注意,边框,内边距和边距也会更改原点的位置。要使用element.style.transform
view.scaleAt(at, amount)
,必须相对于元素的最左上角像素
注意:缩放元素或平移元素时,您还需要考虑更多的问题和注意事项,太多了,不足以容纳一个答案。这就是为什么此答案从画布示例开始的原因,因为它是到目前为止管理可缩放可视内容的更安全的方法。
使用鼠标按钮拖动以进行平移,滚轮进行缩放。如果您失去位置(将页面放大得太远或平移页面,请重新启动代码段)
at
const view = (() => {
const matrix = [1, 0, 0, 1, 0, 0]; // current view transform
var m = matrix; // alias
var scale = 1; // current scale
const pos = { x: 0, y: 0 }; // current position of origin
var dirty = true;
const API = {
applyTo(el) {
if (dirty) { this.update() }
el.style.transform = `matrix(${m[0]},${m[1]},${m[2]},${m[3]},${m[4]},${m[5]})`;
},
update() {
dirty = false;
m[3] = m[0] = scale;
m[2] = m[1] = 0;
m[4] = pos.x;
m[5] = pos.y;
},
pan(amount) {
if (dirty) { this.update() }
pos.x += amount.x;
pos.y += amount.y;
dirty = true;
},
scaleAt(at, amount) { // at in screen coords
if (dirty) { this.update() }
scale *= amount;
pos.x = at.x - (at.x - pos.x) * amount;
pos.y = at.y - (at.y - pos.y) * amount;
dirty = true;
},
};
return API;
})();
document.addEventListener("mousemove", mouseEvent, {passive: false});
document.addEventListener("mousedown", mouseEvent, {passive: false});
document.addEventListener("mouseup", mouseEvent, {passive: false});
document.addEventListener("mouseout", mouseEvent, {passive: false});
document.addEventListener("wheel", mouseWheelEvent, {passive: false});
const mouse = {x: 0, y: 0, oldX: 0, oldY: 0, button: false};
function mouseEvent(event) {
if (event.type === "mousedown") { mouse.button = true }
if (event.type === "mouseup" || event.type === "mouseout") { mouse.button = false }
mouse.oldX = mouse.x;
mouse.oldY = mouse.y;
mouse.x = event.pageX;
mouse.y = event.pageY;
if(mouse.button) { // pan
view.pan({x: mouse.x - mouse.oldX, y: mouse.y - mouse.oldY});
view.applyTo(zoomMe);
}
event.preventDefault();
}
function mouseWheelEvent(event) {
const x = event.pageX - (zoomMe.width / 2);
const y = event.pageY - (zoomMe.height / 2);
if (event.deltaY < 0) {
view.scaleAt({x, y}, 1.1);
view.applyTo(zoomMe);
} else {
view.scaleAt({x, y}, 1 / 1.1);
view.applyTo(zoomMe);
}
event.preventDefault();
}
body {
user-select: none;
-moz-user-select: none;
}
.zoomables {
pointer-events: none;
border: 1px solid black;
}
#zoomMe {
position: absolute;
top: 0px;
left: 0px;
}
答案 1 :(得分:2)
这是我的版本,支持平移和缩放(按住CTRL键)。
let editor = document.getElementById("editor");
let editorCanvas = editor.querySelector(".canvas");
let scale = 1.0;
const minScale = 0.1;
const maxScale = 8;
const scaleStep = 0.003;
let ctrlDown = false;
let dragging = false;
let dragStartX = 0;
let dragStartY = 0;
let previousScrollLeft = 0;
let previousScrollTop = 0;
window.addEventListener("keydown", (e) => {
if (e.ctrlKey) {
ctrlDown = true;
editorCanvas.style.cursor = "move";
}
});
window.addEventListener("keyup", (e) => {
ctrlDown = false;
editorCanvas.style.cursor = "default";
});
editor.addEventListener("mousedown", (e) => {
dragging = true;
dragStartX = e.x - editor.offsetLeft;
dragStartY = e.y - editor.offsetTop;
previousScrollLeft = editor.scrollLeft;
previousScrollTop = editor.scrollTop;
});
editor.addEventListener("mouseup", (e) => {
dragging = false;
});
editor.addEventListener("mousemove", (e) => {
if (ctrlDown && dragging) {
requestAnimationFrame(() => {
let currentX = e.x - editor.offsetLeft;
let currentY = e.y - editor.offsetTop;
let scrollX = previousScrollLeft + (dragStartX - currentX)
let scrollY = previousScrollTop + (dragStartY - currentY);
editor.scroll(scrollX, scrollY);
});
}
});
editor.addEventListener("wheel", (e) => {
e.preventDefault();
requestAnimationFrame(() => {
if (e.ctrlKey) {
scale -= e.deltaY * scaleStep;
if (scale < minScale) {
scale = minScale;
}
if (scale > maxScale) {
scale = maxScale;
}
if (scale < 1) {
editorCanvas.style.transformOrigin = "50% 50% 0";
} else {
editorCanvas.style.transformOrigin = "0 0 0";
}
editorCanvas.style.transform = `matrix(${scale}, 0, 0, ${scale}, 0, 0)`;
let rect = editorCanvas.getBoundingClientRect();
let ew = rect.width;
let eh = rect.height;
let mx = e.x - editor.offsetLeft;
let my = e.y - editor.offsetTop;
editor.scroll((ew - editor.offsetWidth) * (mx / editor.clientWidth), (eh - editor.offsetHeight) * (my / editor.clientHeight));
} else {
editor.scrollTop += e.deltaY;
editor.scrollLeft += e.deltaX;
}
});
}, { passive: false });
body {
background-color: lightgray;
}
#editor {
position: relative;
width: 1024px;
height: 768px;
box-sizing: border-box;
border: 1px solid darkgray;
background-color: gray;
overflow: auto;
}
.canvas {
position: relative;
width: 100%;
height: 100%;
background-color: white;
}
.frame {
position: absolute;
box-sizing: border-box;
border: 1px solid darkslategrey;
transition: all 0.25s;
}
.frame.one {
top: 80px;
left: 400px;
width: 300px;
height: 250px;
background-color: pink;
}
.frame.two {
top: 350px;
left: 150px;
width: 200px;
height: 150px;
background-color: gold;
}
.frame.three {
top: 130px;
left: 70px;
width: 100px;
height: 150px;
background-color: cyan;
}
.frame.four {
top: 368px;
left: 496px;
width: 32px;
height: 32px;
background-color: lime;
}
.frame:hover {
cursor: pointer;
border: 3px solid darkslategrey;
}
.frame:active {
filter: invert();
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Zoom Editor</title>
</head>
<body>
<div id="editor">
<div class="canvas">
<div class="frame one"></div>
<div class="frame two"></div>
<div class="frame three"></div>
<div class="frame four"></div>
</div>
</div>
</body>
</html>
答案 2 :(得分:1)
第二个链接的缩放有点极端,因此我尝试添加一些约束。您可以取消注释它们并玩更多游戏。就目前而言,外观和工作方式与恕我直言完全相同。
const container = document.querySelector('.container');
const image = document.querySelector('.image');
const speed = 0.5;
let size = {
w: image.offsetWidth,
h: image.offsetHeight
};
let pos = { x: 0, y: 0 };
let target = { x: 0, y: 0 };
let pointer = { x: 0, y: 0 };
let scale = 1;
window.addEventListener('wheel', event => {
event.preventDefault();
pointer.x = event.pageX - container.offsetLeft;
pointer.y = event.pageY - container.offsetTop;
target.x = (pointer.x - pos.x) / scale;
target.y = (pointer.y - pos.y) / scale;
scale += -1 * Math.max(-1, Math.min(1, event.deltaY)) * speed * scale;
// Uncomment to constrain scale
// const max_scale = 4;
// const min_scale = 1;
// scale = Math.max(min_scale, Math.min(max_scale, scale));
pos.x = -target.x * scale + pointer.x;
pos.y = -target.y * scale + pointer.y;
// Uncomment for keeping the image within area (works with min scale = 1)
// if (pos.x > 0) pos.x = 0;
// if (pos.x + size.w * scale < size.w) pos.x = -size.w * (scale - 1);
// if (pos.y > 0) pos.y = 0;
// if (pos.y + size.h * scale < size.h) pos.y = -size.h * (scale - 1);
image.style.transform = `translate(${pos.x}px,${pos.y}px) scale(${scale},${scale})`;
}, { passive: false });
.container {
width: 400px;
height: 400px;
overflow: hidden;
outline: 1px solid gray;
}
.image {
width: 100%;
height: 100%;
transition: transform .3s;
transform-origin: 0 0;
}
img {
width: auto;
height: auto;
max-width: 100%;
}
<div class="container">
<div class="image">
<img src="https://picsum.photos/400/400" />
</div>
</div>
答案 3 :(得分:0)
我认为您需要使用外部jquery插件来实现此目标:
js文件: https://ariutta.github.io/svg-pan-zoom/dist/svg-pan-zoom.js
工作演示: https://ariutta.github.io/svg-pan-zoom/
有关更多说明,请访问: https://github.com/ariutta/svg-pan-zoom