将鼠标位置转换为 Canvas 坐标并返回
Convert mouse position to Canvas Coordinates and back
我正在创建一个带有叠加层 div 的 canvas 以在单击时添加标记,我希望标记在平移缩放 canvas 或调整 [= 大小时改变位置30=]。我正在使用 https://github.com/timmywil/panzoom 进行平移缩放。
问题是当我将鼠标位置转换为 canvas 坐标时它工作正常但是当我将其转换回屏幕位置以在叠加 div 上呈现标记时,结果与初始化鼠标位置并重新计算调整大小时标记的位置也不正确。
这个 canvas 是全屏的,没有滚动。
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);
})
}
如何让它与缩放和平移一起工作?如果你知道有什么图书馆可以做这项工作,请推荐,谢谢。
这是一个似乎有效的代码片段,您可以根据自己的目的对其进行调整。
我用的是:
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);
}
我只是从 window
对象获取鼠标位置。我可能弄错了,但我认为这就是 scrollLeft
和 scrollTop
没有出现在 toCanvasCoords
中的原因(因为位置是相对于 [=67= 的客户区的) ] 本身,卷轴不会进入其中)。但是你变回来的时候,你就得考虑了。
这最终只是 returns 鼠标相对于 window(这是输入)的位置,因此如果您只想将元素附加到鼠标指针。但是,如果您想将某些东西附加到 canvas 图像上的某个点(例如,地图上的某个特征),则转换回来很有用 - 我猜这就是您想要的东西,因为你说你想在叠加层上渲染标记 div.
在下面的代码片段中,红色圆圈绘制在 canvas 本身的 toCanvasCoords
返回的位置;你会注意到它与背景一起缩放。
我没有使用覆盖整个地图的叠加层 div,我只是使用绝对定位在顶部放置了几个小的 div。黑色三角形是一个 div (#tracker),基本上可以跟踪鼠标;它被放置在 toScreenCoords
的结果中。它用作检查转换是否正常工作的一种方法。它是一个独立的元素,因此不会随图像缩放。
红色三角形是另一个这样的 div (#feature),并展示了上述“附加到功能”的想法。假设背景是一张地图之类的东西,并且假设您想将“地图图钉”图标附加到上面的某物上,例如特定的十字路口;您可以在地图上获取该位置(这是一个固定值),并将其传递给 toScreenCoords
。在下面的代码片段中,我将它与背景上正方形的一角对齐,以便您可以在更改比例 and/or 滚动时直观地跟踪它。 (点击“运行代码段”后,可以点击“整页”,然后调整window的大小即可得到滚动条)。
现在,根据您的代码中到底发生了什么,您可能已经调整了一些东西,但希望这会对您有所帮助。如果您 运行 遇到问题,请使用 console.log and/or 在页面上放置一些调试元素,这些元素将为您实时显示值(例如鼠标位置、客户端矩形等),因此您可以检查值。一次迈出一步-例如首先让比例正常工作,但忽略滚动,然后尝试让滚动正常工作,但将比例保持在 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
// (e.g. 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>
P.S。不要做 Number.parseInt(x.toFixed(0))
;通常,尽可能长时间地使用浮点数以最大程度地减少错误的累积,并且只在最后一刻才转换为 int。我已经包含了 roundPoint
函数,它将一个点的 (x, y) 坐标舍入到最接近的整数(通过 Math.round
),但最终根本不需要使用它。
注意:下图作为代码段中的背景,作为缩放的参考点;它被包含在这里只是为了将它托管在 Stack Exchange 的 imgur.com 帐户上,这样代码就不会引用(可能不稳定的)第三层源。
我正在创建一个带有叠加层 div 的 canvas 以在单击时添加标记,我希望标记在平移缩放 canvas 或调整 [= 大小时改变位置30=]。我正在使用 https://github.com/timmywil/panzoom 进行平移缩放。
问题是当我将鼠标位置转换为 canvas 坐标时它工作正常但是当我将其转换回屏幕位置以在叠加 div 上呈现标记时,结果与初始化鼠标位置并重新计算调整大小时标记的位置也不正确。
这个 canvas 是全屏的,没有滚动。
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);
})
}
如何让它与缩放和平移一起工作?如果你知道有什么图书馆可以做这项工作,请推荐,谢谢。
这是一个似乎有效的代码片段,您可以根据自己的目的对其进行调整。
我用的是:
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);
}
我只是从 window
对象获取鼠标位置。我可能弄错了,但我认为这就是 scrollLeft
和 scrollTop
没有出现在 toCanvasCoords
中的原因(因为位置是相对于 [=67= 的客户区的) ] 本身,卷轴不会进入其中)。但是你变回来的时候,你就得考虑了。
这最终只是 returns 鼠标相对于 window(这是输入)的位置,因此如果您只想将元素附加到鼠标指针。但是,如果您想将某些东西附加到 canvas 图像上的某个点(例如,地图上的某个特征),则转换回来很有用 - 我猜这就是您想要的东西,因为你说你想在叠加层上渲染标记 div.
在下面的代码片段中,红色圆圈绘制在 canvas 本身的 toCanvasCoords
返回的位置;你会注意到它与背景一起缩放。
我没有使用覆盖整个地图的叠加层 div,我只是使用绝对定位在顶部放置了几个小的 div。黑色三角形是一个 div (#tracker),基本上可以跟踪鼠标;它被放置在 toScreenCoords
的结果中。它用作检查转换是否正常工作的一种方法。它是一个独立的元素,因此不会随图像缩放。
红色三角形是另一个这样的 div (#feature),并展示了上述“附加到功能”的想法。假设背景是一张地图之类的东西,并且假设您想将“地图图钉”图标附加到上面的某物上,例如特定的十字路口;您可以在地图上获取该位置(这是一个固定值),并将其传递给 toScreenCoords
。在下面的代码片段中,我将它与背景上正方形的一角对齐,以便您可以在更改比例 and/or 滚动时直观地跟踪它。 (点击“运行代码段”后,可以点击“整页”,然后调整window的大小即可得到滚动条)。
现在,根据您的代码中到底发生了什么,您可能已经调整了一些东西,但希望这会对您有所帮助。如果您 运行 遇到问题,请使用 console.log and/or 在页面上放置一些调试元素,这些元素将为您实时显示值(例如鼠标位置、客户端矩形等),因此您可以检查值。一次迈出一步-例如首先让比例正常工作,但忽略滚动,然后尝试让滚动正常工作,但将比例保持在 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
// (e.g. 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>
P.S。不要做 Number.parseInt(x.toFixed(0))
;通常,尽可能长时间地使用浮点数以最大程度地减少错误的累积,并且只在最后一刻才转换为 int。我已经包含了 roundPoint
函数,它将一个点的 (x, y) 坐标舍入到最接近的整数(通过 Math.round
),但最终根本不需要使用它。
注意:下图作为代码段中的背景,作为缩放的参考点;它被包含在这里只是为了将它托管在 Stack Exchange 的 imgur.com 帐户上,这样代码就不会引用(可能不稳定的)第三层源。