在鼠标位置缩放/缩放 [英] Zoom/scale at mouse position

查看:100
本文介绍了在鼠标位置缩放/缩放的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

根据此示例,我正在努力找出并确定如何缩放鼠标位置.( https://stackblitz.com/edit/js-fxnmkm?file=index.js )

I am struggling to figure out and determine how to zoom on my mouse position based on this example. (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 ).目前,我上面的示例仅缩放到视口"的中心,但我希望它成为我的光标所在的位置.

My desired effect is based on this example (https://codepen.io/techslides/pen/zowLd?editors=0010) when zooming in. Currently my example above only scales to the center of the "viewport" but I want it to be where my cursor currently is.

我在高处和低处寻找无法通过画布实现的解决方案.任何帮助将不胜感激!

I have searched high and low for a solution that is not implemented via canvas. Any help would be appreciated!

注意事项我使用wheel事件的原因是为了模仿Figma(设计工具)的平移和缩放交互.

Caveat The reason why I am using the wheel event is to mimic the interaction of Figma (the design tool) panning and zooming.

推荐答案

将画布用于可缩放的内容

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

Use the canvas for zoomable content

Zooming and panning elements is very problematic. It can be done but the list of issues is very long. I would never implement such an interface.

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

Consider using the canvas, via 2D or WebGL to display such content to save your self many many problems.

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

The first part of the answer is implemented using the canvas. The same interface view is used in the second example that pans and zooms an element.

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

As you are only panning and zooming then a very simple method can be used.

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

The example below implements an object called view. This holds the current scale and position (pan)

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

It provides two function for user interaction.

  • 对函数 view.pan(amount)进行平移将按由 amount.x amount.y 保持的距离(以像素为单位)平移视图
  • 缩放功能 view.scaleAt(at,amount)将按 amount (代表比例变化的数字)缩放(缩放)视图. at.x at.y 的像素位置.
  • Panning the function view.pan(amount) will pan the view by distance in pixels held by amount.x, amount.y
  • Zooming the function view.scaleAt(at, amount) will scale (zoom in out) the view by amount (a number representing change in scale), at the position held by at.x, at.y in pixels.

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

In the example the view is applied to the canvas rendering context using view.apply() and a set of random boxes are rendered whenever the view changes. The panning and zooming is via mouse events

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

Use mouse button drag to pan, wheel to zoom

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>

此示例使用元素样式转换属性进行缩放和平移.

This example uses the element style transform property to zoom and pan.

  • 注意,我使用2D矩阵而不是3d矩阵,因为这会带来许多与下面使用的简单缩放和平移不兼容的问题.

  • Note that I use a 2D matrix rather than the 3d matrix as that can introduce many problems not compatible with the simple zoom and pan used below.

注意,并非所有情况下CSS转换都应用于元素的左上方.在下面的示例中,原点位于元素的中心.因此,在缩放时,必须通过减去元素大小的一半来调整在点处缩放.元素大小不受变换的影响.

Note that CSS transforms are not applied to the top left of the element in all cases. In the example below the origin is in the center of the element. Thus when zooming the zoom at point must be adjusted by subtracting half the elements size. The element size is not effected by the transform.

注意,边框,边距和边距也会更改原点的位置.要使用 view.scaleAt(at,amount) at ,必须相对于元素的最左上角像素

Note borders, padding, and margins will also change the location of the origin. To work with view.scaleAt(at, amount) at must be relative to the top left most pixel of the element

注意:缩放元素或平移元素时,您还需要考虑更多的问题和警告,太多了,不足以容纳一个答案.这就是为什么此答案从画布示例开始的原因,因为它是到目前为止管理可缩放可视内容的更安全的方法.

Note there are many more problems and caveats you need to consider when you zoom and pan elements, too many to fit in a single answer. That is why this answer starts with a canvas example as it is by far the safer method to managing zoom-able visual content.

使用鼠标按钮拖动以进行平移,使用滚轮进行缩放.如果您失去位置(将页面放大得太远或平移页面,请重新启动代码段)

Use mouse button drag to pan, wheel to zoom. If you lose your position (zoom too far in out or panned of the page restart the snippet)

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;
}
  

<img id="zoomMe" class="zoomables" src="https://i.stack.imgur.com/C7qq2.png?s=328&g=1">

这篇关于在鼠标位置缩放/缩放的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆