简体   繁体   English

将鼠标位置转换为画布坐标并返回

[英]Convert mouse position to Canvas Coordinates and back

I'm creating a canvas with an overlay div to add markers on click and I want markers to change position when I pan zoom the canvas or resize the window.我正在创建一个带有覆盖 div 的画布,以在单击时添加标记,我希望标记在平移缩放画布或调整窗口大小时改变位置。 I'm using https://github.com/timmywil/panzoom to pan zoom.我正在使用https://github.com/timmywil/panzoom来平移缩放。

The problem is when I convert mouse position to canvas coordinates it worked correctly but when I convert it back to screen position to render markers on overlay div, the result is not as same as initialized mouse position and recalculate marker's position on resize also not correct.问题是当我将鼠标位置转换为画布坐标时它工作正常,但是当我将其转换回屏幕位置以在叠加 div 上呈现标记时,结果与初始化鼠标位置不同,并且在调整大小时重新计算标记位置也不正确。

This canvas is fullscreen with no scroll.这个画布是全屏的,没有滚动。

width = 823; height = 411;
scale = 2; panX = 60; panY = 10;
mouse.pageX = 467; mouse.pageY = 144; 

// {x: 475, y: 184} correct coords when I use ctx.drawImage(..) to test
canvasCoords = getCanvasCoords(mouse.pageX, mouse.pageY, scale);

// {x: 417, y: 124}
screenCoords = toScreenCoords(canvasCoords.x, canvasCoords.y, scale, panX, panY);
------------------------------
but with scale = 1; it worked correctly.
// convert mouse position to canvas coordinates
getCanvasCoords(pageX: number, pageY: number, scale: number) {
    var rect = this.pdfInfo.canvas.getBoundingClientRect();
    let x = (pageX - rect.left + this.scrollElement.scrollTop) / scale;
    let y = (pageY - rect.top + this.scrollElement.scrollLeft) / scale;
    return {
      x: Number.parseInt(x.toFixed(0)),
      y: Number.parseInt(y.toFixed(0)),
    };
}

// convert canvas coords to screen coords 
toScreenCoords(
    x: number,
    y: number,
    scale: number
  ) {
    var rect = this.pdfInfo.canvas.getBoundingClientRect();

    let wx =
      x * scale + rect.left - this.scrollElement.scrollTop / scale;
    let wy =
      y * scale + rect.top - this.scrollElement.scrollLeft / scale;
    return {
      x: Number.parseInt(wx.toFixed(0)),
      y: Number.parseInt(wy.toFixed(0)),
    };
}

getNewPos(x, oldV, newV) {
    return (x * oldV) / newV;
}

// update screen coords with new screen width and height
onResize(old, new) {
   this.screenCoordList.forEach(el => {
      el.x = getNewPos(el.x, old.width, new.width);
      el.y = getNewPos(el.y, old.height, new.height);
   })
}

How to get it worked with scale and pan?如何让它与规模和泛一起工作? if you know any library can do the job please recommend, thank you.如果您知道任何图书馆可以完成这项工作,请推荐,谢谢。

Here's a code snippet that seems to be working, you can probably adapt it for your purposes.这是一个似乎有效的代码片段,您可以根据自己的目的对其进行调整。

What I used was:我用的是:

function toCanvasCoords(pageX, pageY, scale) {
    var rect = canvas.getBoundingClientRect();
    let x = (pageX - rect.left) / scale;
    let y = (pageY - rect.top) / scale;
    return toPoint(x, y);
}

and

function toScreenCoords(x, y, scale) {
    var rect = canvas.getBoundingClientRect();
    let wx = x * scale + rect.left + scrollElement.scrollLeft;
    let wy = y * scale + rect.top + scrollElement.scrollTop;
    return toPoint(wx, wy);
}

I'm just getting the mouse position from the window object.我只是从window对象获取鼠标位置。 I'm may be mistaken, but I think this is why scrollLeft and scrollTop don't appear in toCanvasCoords (since the position is relative to the client area of the window itself, the scroll doesn't come into it).我可能弄错了,但我认为这就是为什么scrollLeftscrollTop没有出现在toCanvasCoords中的toCanvasCoords (因为位置是相对于窗口本身的客户区,滚动没有进入它)。 But then when you transform back, you have to take it into account.但是当你变回时,你必须考虑到它。

This ultimately just returns the mouse position relative to the window (which was the input), so it's not really necessary to do the whole transformation in a roundabout way if you just want to attach an element to the mouse pointer.这最终只是返回相对于窗口(这是输入)的鼠标位置,因此如果您只想将元素附加到鼠标指针,则没有必要以迂回的方式进行整个转换。 But transforming back is useful if you want to have something attached to a certain point on the canvas image (say, a to feature on the map) - which I'm guessing is something that you're going for, since you said that you want to render markers on an overlay div.但是,如果您想将某些东西附加到画布图像上的某个点(例如,地图上的一个 to 特征),则转换回来很有用 - 我猜这就是您想要的东西,因为您说过想要在叠加层 div 上呈现标记。

In the code snippet bellow, the red circle is drawn on the canvas itself at the location returned by toCanvasCoords ;在下面的代码片段中,红色圆圈是在画布本身的toCanvasCoords返回的位置toCanvasCoords you'll notice that it scales together with the background.您会注意到它与背景一起缩放。

I didn't use an overlay div covering the entire map, I just placed a couple of small divs on top using absolute positioning.我没有使用覆盖整个地图的覆盖 div,我只是使用绝对定位在顶部放置了几个小 div。 The black triangle is a div (#tracker) that basically tracks the mouse;黑色三角形是一个div(#tracker),主要是跟踪鼠标; it is placed at the result of toScreenCoords .它被放置在toScreenCoords的结果中。 It serves as a way to check if the transformations work correctly.它是一种检查转换是否正常工作的方法。 It's an independent element, so it doesn't scale with the image.它是一个独立的元素,因此不会随图像缩放。

The red triangle is another such div (#feature), and demonstrates the aforementioned "attach to feature" idea.红色三角形是另一个这样的 div (#feature),它展示了前面提到的“附加到特征”的想法。 Suppose the background is a something like a map, and suppose you want to attach a "map pin" icon to something on it, like to a particular intersection;假设背景是一个类似于地图的东西,并且假设您想将“地图图钉”图标附加到其上的某物上,例如特定的交叉点; you can take that location on the map (which is a fixed value), and pass it to toScreenCoords .您可以在地图上获取该位置(这是一个固定值),并将其传递给toScreenCoords In the code snippet below, I've aligned it with a corner of a square on the background, so that you can track it visually as you change scale and/or scroll.在下面的代码片段中,我将它与背景上的一个正方形的角对齐,以便您可以在更改比例和/或滚动时直观地跟踪它。 (After you click "Run code snippet", you can click "Full page", and then resize the window to get the scroll bars). (单击“运行代码片段”后,您可以单击“整页”,然后调整窗口大小以获取滚动条)。

Now, depending on what exactly is going on in your code, you may have tweak a few things, but hopefully, this will help you.现在,根据您的代码究竟发生了什么,您可能已经调整了一些东西,但希望这会对您有所帮助。 If you run into problems, make use of console.log and/or place some debug elements on the page that will display values live for you (eg mouse position, client rectangle, etc.), so that you can examine values.如果遇到问题,请使用 console.log 和/或在页面上放置一些调试元素,这些元素将为您实时显示值(例如鼠标位置、客户端矩形等),以便您可以检查值。 And take things one step at the time - eg first get the scale to work, but ignore scrolling, then try to get scrolling to work, but keep the scale at 1, etc.并在当时采取一步 - 例如,首先让比例起作用,但忽略滚动,然后尝试让滚动起作用,但将比例保持在 1,等等。

 const canvas = document.getElementById('canvas'); const context = canvas.getContext("2d"); const tracker = document.getElementById('tracker'); const feature = document.getElementById('feature'); const slider = document.getElementById("scale-slider"); const scaleDisplay = document.getElementById("scale-display"); const scrollElement = document.querySelector('html'); const bgImage = new Image(); bgImage.src = "https://i.stack.imgur.com/yxtqw.jpg" var bgImageLoaded = false; bgImage.onload = () => { bgImageLoaded = true; }; var mousePosition = toPoint(0, 0); var scale = 1; function updateMousePosition(evt) { mousePosition = toPoint(evt.clientX, evt.clientY); } function getScale(evt) { scale = evt.target.value; scaleDisplay.textContent = scale; } function toCanvasCoords(pageX, pageY, scale) { var rect = canvas.getBoundingClientRect(); let x = (pageX - rect.left) / scale; let y = (pageY - rect.top) / scale; return toPoint(x, y); } function toScreenCoords(x, y, scale) { var rect = canvas.getBoundingClientRect(); let wx = x * scale + rect.left + scrollElement.scrollLeft; let wy = y * scale + rect.top + scrollElement.scrollTop; return toPoint(wx, wy); } function toPoint(x, y) { return { x: x, y: y } } function roundPoint(point) { return { x: Math.round(point.x), y: Math.round(point.y) } } function update() { context.clearRect(0, 0, 500, 500); context.save(); context.scale(scale, scale); if (bgImageLoaded) context.drawImage(bgImage, 0, 0); const canvasCoords = toCanvasCoords(mousePosition.x, mousePosition.y, scale); drawTarget(canvasCoords); const trackerCoords = toScreenCoords(canvasCoords.x, canvasCoords.y, scale); updateTrackerLocation(trackerCoords); updateFeatureLocation() context.restore(); requestAnimationFrame(update); } function drawTarget(location) { context.fillStyle = "rgba(255, 128, 128, 0.8)"; context.beginPath(); context.arc(location.x, location.y, 8.5, 0, 2*Math.PI); context.fill(); } function updateTrackerLocation(location) { const canvasRectangle = offsetRectangle(canvas.getBoundingClientRect(), scrollElement.scrollLeft, scrollElement.scrollTop); if (rectContains(canvasRectangle, location)) { tracker.style.left = location.x + 'px'; tracker.style.top = location.y + 'px'; } } function updateFeatureLocation() { // suppose the background is a map, and suppose there's a feature of interest // (eg a road intersection) that you want to place the #feature div over // (I roughly aligned it with a corner of a square). const featureLoc = toScreenCoords(84, 85, scale); feature.style.left = featureLoc.x + 'px'; feature.style.top = featureLoc.y + 'px'; } function offsetRectangle(rect, offsetX, offsetY) { // copying an object via the spread syntax or // using Object.assign() doesn't work for some reason const result = JSON.parse(JSON.stringify(rect)); result.left += offsetX; result.right += offsetX; result.top += offsetY; result.bottom += offsetY; result.x = result.left; result.y = result.top; return result; } function rectContains(rect, point) { const inHorizontalRange = rect.left <= point.x && point.x <= rect.right; const inVerticalRange = rect.top <= point.y && point.y <= rect.bottom; return inHorizontalRange && inVerticalRange; } window.addEventListener('mousemove', (e) => updateMousePosition(e), false); slider.addEventListener('input', (e) => getScale(e), false); requestAnimationFrame(update);
 #canvas { border: 1px solid gray; } #tracker, #feature { position: absolute; left: 0; top: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 10px solid black; transform: translate(-4px, 0); } #feature { border-bottom: 10px solid red; }
 <div> <label for="scale-slider">Scale:</label> <input type="range" id="scale-slider" name="scale-slider" min="0.5" max="2" step="0.02" value="1"> <span id="scale-display">1</span> </div> <canvas id="canvas" width="500" height="500"></canvas> <div id="tracker"></div> <div id="feature"></div>

PS Don't do Number.parseInt(x.toFixed(0)) ; PS 不要做Number.parseInt(x.toFixed(0)) ; generally, work with floating point for as long as possible to minimize accumulation of errors, and only convert to int at the last minute.通常,尽可能长时间使用浮点数以最大程度地减少错误累积,并且只在最后一分钟转换为 int。 I've included the roundPoint function that rounds the (x, y) coordinates of a point to the nearest integer (via Math.round ), but ended up not needing to use it at all.我已经包含了roundPoint函数,该函数将一个点的 (x, y) 坐标roundPoint入到最近的整数(通过Math.round ),但最终根本不需要使用它。

Note: The image below is used as the background in the code snippet, to serve as a reference point for scaling;注意:下图作为代码片段中的背景,作为缩放的参考点; it is included here just so that it is hosted on Stack Exchange's imgur.com account, so that the code is not referencing a (potentially volatile) 3rd-pary source.它包含在此处只是为了将它托管在 Stack Exchange 的 imgur.com 帐户上,以便代码不引用(可能不稳定的)第三方源。
在此处输入图片说明

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM