在鼠标位置缩放/缩放

时间:2020-02-12 14:48:29

标签: javascript html canvas html5-canvas

根据此示例,我正在努力找出并确定如何缩放鼠标位置。 (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(设计工具)的平移和缩放交互。

4 个答案:

答案 0 :(得分:5)

将画布用于可缩放的内容

缩放和平移元素非常成问题。可以做到,但问题清单很长。我永远不会实现这样的接口。

考虑通过2D或WebGL使用画布来显示此类内容,以节省许多麻烦。

答案的第一部分是使用画布实现的。第二个示例使用相同的界面view来平移和缩放元素。

一个简单的2D视图。

由于您只是平移和缩放,因此可以使用非常简单的方法。

以下示例实现了一个名为view的对象。这会保留当前的比例和位置(平移)

它提供了两个用于用户交互的功能。

  • 平移功能view.pan(amount)将按amount.xamount.y所保持的距离(以像素为单位)平移视图
  • 缩放功能view.scaleAt(at, amount)将在amount,{{1}所保持的位置上按at.x(表示比例变化的数字)缩放(放大)视图。 }(以像素为单位)。

在示例中,使用at.y将视图应用于画布渲染上下文,并且每当视图更改时,都会渲染一组随机框。 平移和缩放是通过鼠标事件

使用画布2D上下文的示例

使用鼠标按钮拖动以进行平移,滚轮进行缩放

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