javascript. how to get the exact touch position on an image [duplicate] - javascript

This question already has an answer here:
How can I translate between client coordinates of a HTML video element and picture coordinates?
(1 answer)
Closed 23 days ago.
I have an image. for this i would like to get the touch events start, move and end.
I need the exact image-pixel of the touch.
The Problem is that the actual image is smaller than the HTMLImageElement.
(css style object-fit: scale-down)
I don't need the touch events for the HTMLImageElement but the actual image itself.
How do I archive this?
btw. I use React (if this helps) but I can (hopefully) adapt the answer myself.
Current Attempt:
<img ...
onTouchStart={(event) => {
const touch = event.changedTouches[0];
const rect = event.currentTarget.getBoundingClientRect();
const pxlX = touch.screenX - rect.left;
const pxlY = touch.screenY - rect.top;
// other stuff with (pxlX, pxlY)
}} />
My Solution in the end
import { useState } from "react";
import ImageSrc from "./assets/image.png";
export default function App() {
const [text, setText] = useState("undefined");
return <div className="w-screen h-screen overflow-hidden">
<img src={ImageSrc} alt="" className="object-scale-down w-full h-full" onMouseMove={(event) => {
const {x, y} = getXY(event);
setText(`${x}/${y}`);
}} />
<span className="fixed top-0 left-0 pointer-events-none">{text}</span>
</div>;
}
function getXY(event: React.MouseEvent<HTMLImageElement, MouseEvent>): {x: number, y: number} {
const img = event.currentTarget;
const [imgWidth, imgHeight] = [img.naturalWidth, img.naturalHeight];
const [boxWidth, boxHeight] = [img.width, img.height];
const imgRatio = imgWidth / imgHeight;
const boxRatio = boxWidth / boxHeight;
if (imgRatio < boxRatio) {
const x0 = (boxWidth - boxHeight * imgWidth/imgHeight) / 2;
const x = Math.round(event.nativeEvent.offsetX - x0);
return {x: x, y: event.nativeEvent.offsetY};
} else {
const y0 = (boxHeight - boxWidth * imgHeight/imgWidth) / 2;
const y = Math.round(event.nativeEvent.offsetY - y0);
return {x: event.nativeEvent.offsetX, y: y};
}
}

Use img.naturalHeight and img.naturalWidth to determine the position at which the effective image starts.
Briefly, the following quantity will be negative if the Y of the event is relative to a point above the image, so capture it in your event and check:
y0 = (400 - 400*mainImg.naturalHeight/mainImg.naturalWidth)/2;
var effectiveY = event.offsetY - y0;
A little math is involved here, feel free to ask if you need a general description when the image can be a portrait, or if you still need help detecting events below the image.
var y0;
window.addEventListener("load", function(ev) {
y0 = (400 - 400*mainImg.naturalHeight/mainImg.naturalWidth)/2;
});
mainImg.addEventListener("mousemove", function(ev) {
infoDiv.innerText = ev.offsetY - y0;
});
img {
object-fit: scale-down;
border: 1px solid #AAA;
}
effectiveY is:
<span id="infoDiv"></span>
<br>
<img id="mainImg" src="https://st.hzcdn.com/simgs/pictures/gardens/garden-with-oval-lawns-fenton-roberts-garden-design-img~1971dc9307c80c73_4-8900-1-60a5647.jpg" width=400 height=400>

Related

How to add a marker to canvas with a position that recalculates when zooming in or out

I am loading a map which can be dragged and zoomed in and out with HTML5 canvas. This works perfect. I can also place a marker i.e. at pixel 500x 600y. But when I zoom the map in or out, the pixels obviously change and the 500x and 600y are not correct anymore. Therefore the marker will shift a little bit and its position is not correct anymore.
If I would zoom out once, and console log the map size which originally was 4097 x 2142 it now is 3687.3 x 1927.8. I think this also means that the original marker position of [500, 500] has to be recalculated to fix my issue... but I have no clue how to.
Been trying for days now to figure out how to fix this but I have no clue... can anyone please help me out on how to solve this matter?
I am using Vue (Nuxt) and HTML5 canvas. I can provide a zip with my project if that helps ;)
Below is the most important piece of code I recon:
// Initial position when the map is loaded (map size 4097x2142)
ICO_POS.value = [500, 500] // Icon will be displayed at 500x 6000y
// This function will check if we zoomed in or out
const zoom = (e) => {
if (e.wheelDelta > 0) {
zoomIn()
} else {
zoomOut()
}
}
// Zoom in function
const zoomIn = (e) => {
if (DEFAULT_ZOOM.value < MAX_ZOOM) {
DEFAULT_ZOOM.value += ZOOM_STEP
}
}
// Zoom out function
const zoomOut = (e) => {
if (DEFAULT_ZOOM.value > MIN_ZOOM) {
DEFAULT_ZOOM.value -= ZOOM_STEP
}
}
// This function will draw the map and icons
const drawImage = () => {
// Draw the map
const w = image.width * DEFAULT_ZOOM.value
const h = image.height * DEFAULT_ZOOM.value
const x = DRAW_POS.value[0] - (w / 2)
const y = DRAW_POS.value[1] - (h / 2)
context.drawImage(image, x, y, w, h)
// Draw the icon
const w2 = (image2.width * 3) * DEFAULT_ZOOM.value
const h2 = (image2.height * 3) * DEFAULT_ZOOM.value
const x2 = ICO_POS.value[0] - (w2 / 2)
const y2 = ICO_POS.value[1] - (h2 / 2)
context.drawImage(image2, x2, y2, w2, h2)
}

Adding Tags to Coordinates of Images with Correct Position, Using React and Next.js

I'm trying to get the coordinates of the image so I can add tags to it. I'm able to add the tag, but when I expand the browser, the tag doesn't follow to the correct position of the image. When browser gets expanded, or scaled down, the image scales correctly with the aspect ratio, however, the tag stays in the current place of the browser (not at the correct place of the image). However, if I scroll, it does move to the correct area, but that's only if scrolling occurs.
const initialPosition = {
x: "",
y: "",
};
const MediaPage = ({ user, mediaId }) => {
const [tagPosition, setTagPosition] = useState(initialPosition);
const imageWrapRef = createRef();
const handleTagClick = (e) => {
const { left, top } = imageWrapRef.current.getBoundingClientRect();
const x = e.pageX - left;
const y = e.pageY - top;
setTagPosition((prev) => ({ ...prev, x, y }));
};
return (
<div
onClick={(e) => handleTagClick(e)}
ref={imageWrapRef}
>
<div
style={{
left: `${tagPosition.x}px`,
top: `${tagPosition.y}px`,
zIndex: 2,
padding: 10,
position: "absolute",
}}
>
1
</div>
<Image
onLoad={handleLoadingComplete}
className="w-full"
alt=""
src="URL"
layout="responsive"
width={700}
height={800}
/>
</div>
}
So essentially, I'm trying to click on an image, and then a tag will appear. However, I need to be able to set the tag on the correct position of the image, even if it scales.
Found a solution, and it was an easy fix. Had to set parent div to relative position, and then update x and y for %
const x = ((e.clientX - left) / width) * 100;
const y = ((e.clientY - top) / height) * 100;
Then in the styling:
left: `${tagPosition.x}%`,
top: `${tagPosition.y}%`,

Svg scaling rotated elements on mouse drag

I'm playing around with svgs and matrixes for a project I'd like to work on and I'm trying to implement a free transform with vanilla javascript. As of now I am able to do all transformations with javascript and this library https://www.npmjs.com/package/transformation-matrix#transform ,to help me dealing with matrixes.
My code for now is only working with rect elements, but I assume most of the code did will be of use for other elements with some changes.
The problem arise when I try to scale a rotated rect.
In my code, when I click on an element I give it a path around the element, some circles that represent the direction you can scale the rect, 1 for the center and 1 for the rotation.
As I've said I don't have any problem with whatever transformation, be it to translate a rotated/scaled shape or anything else, the only thing i can't figure out how to do is how to scale a rotated shape keeping it's rotation.
If a rect has 0 rotation, depending on which direction i decide to scale i just do a mix of translate and scale after i get the new width/height based on mouse pointer. This works great and as intended.
If a rect has rotation this breaks and i tought maybe using the euclidean distance between the point from which i scale and the mouse pointer would work but it doesn't. so i am kinda lost on this.
const elem = document.getElementById('rect2');
const x = elem.x.baseVal.value;
const y = elem.y.baseVal.value;
const width = elem.width.baseVal.value;
const height = elem.height.baseVal.value;
// this is a function that return the mouse coords on the canvas-
// svg
const m = this.getCanvasMousePos(e);
if(type === 'bottom-center'){
const newH = (m.y - y) / height;
const s = scale(1,newH);
const form = toSVG(s);
elem.setAttribute('transform', form);
}else if (type === 'top-center'){
const newH = (y+height - m.y) / height;
const s = scale(1,newH);
const t = translate(0,m.y-y)
const comp = compose(t,s);
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'middle-left') {
const newW = (x+width - m.x) / width;
const s = scale(newW,1);
const t = translate(m.x-x, 0);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'middle-right') {
const newW = (m.x - x) / width;
const s = scale(newW,1);
const form = toSVG(s);
elem.setAttribute('transform', form);
}else if (type === 'bottom-right') {
const newW = (m.x - x) / width;
const newH = (m.y - y) / height;
const s = scale(newW,newH);
const form = toSVG(s);
elem.setAttribute('transform', form);
}else if (type === 'bottom-left'){
const newW = ((x + width) - m.x) / width;
const newH = (m.y - y) / height;
const t = translate(m.x - x,0);
const s = scale(newW,newH);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'top-left') {
const newW = ((x + width) - m.x) / width;
const newH = ((y + height) - m.y) / height;
const t = translate(m.x-x,m.y-y);
const s = scale(newW,newH);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'top-right') {
const newW = (m.x - x) / width;
const newH = ((y + height) - m.y) / height;
const t = translate(0,m.y-y);
const s = scale(newW,newH);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
};
This is the code for the non-rotated svgs.
Hopefully someone can help, thank you very much
I have found a solution.
Whenever a shape is rotated you should translate your mouse pointer and the shape as if they are not rotated, do your scaling normally then translating back and rotating at the end. My problem was that i was trying to rotate before translating back!
If you use this method the order should be transform(rotate translate scale) because the last one you put is the first to happen!

SVG.js: why does rotate behave in a weird way

I'm trying to get a bus with its compass move along the line. However, I can't get the compass rotating properly, it rotates and also move in a very strange way.
Could you please help me find what has gone wrong?
Here is the link to Fiddle: https://jsfiddle.net/ugvapdrj (I've not fixed the CSS yet so it overflows to the right.)
Thanks!
Code:
<div id="drawing"></div>
let canvas = SVG('drawing').size(2000, 2000)
let compassSize = 42;
let lineColour = '#f00';
let Rsc2muVkt = canvas
.path(
'M736 96V72c0-13-11-24-24-24h-75c-18 0-35 7-48 18l-104 94c-23 21-54 32-85 32H0'
)
.stroke({
color: lineColour
})
.fill('none');
// Buses
let bus = canvas.image(
'https://s3-ap-southeast-1.amazonaws.com/viabus-develop-media-resource-ap-southeast/Vehicle+Images/entity/mu/veh_core_mu_06.png',
compassSize
);
let busCompass = canvas.image(
'https://s3-ap-southeast-1.amazonaws.com/viabus-develop-media-resource-ap-southeast/Network+Images/Azimuth/Public/veh_ring_white.png',
compassSize
);
moveBus(bus, busCompass, Rsc2muVkt, 0);
for (let i = 0; i <= 100; i++) {
setTimeout(() => {
moveBus(bus, busCompass, Rsc2muVkt, i);
}, 1000 + i * 200);
}
function moveBus(bus, busCompass, path, to) {
const p = path.pointAt(to / 100 * path.length());
const newPosition = {
x: p.x,
y: p.y
};
const oldX = bus.cx();
const oldY = bus.cy();
bus.center(newPosition.x, newPosition.y);
busCompass.center(newPosition.x, newPosition.y);
const newX = bus.cx();
const newY = bus.cy();
const dx = newX - oldX;
const dy = newY - oldY;
const rotatingAngle = Math.atan2(dy, dx) * 180 / Math.PI;
busCompass.rotate(rotatingAngle + 90);
}
The compass is still rotated when you try to center it on the next frame of animation. This causes the busCompass.center(...) call to use the rotated coordinate system.
You should first call busCompass.rotate(0); to reset the rotation, then busCompass.center(...) will work as expected and the final rotation will complete.

How to get accurate co-ordinates from mouse clicks in javascript [duplicate]

This question already has answers here:
Real mouse position in canvas
(5 answers)
Closed 1 year ago.
What's the simplest way to add a click event handler to a canvas element that will return the x and y coordinates of the click (relative to the canvas element)?
No legacy browser compatibility required, Safari, Opera and Firefox will do.
If you like simplicity but still want cross-browser functionality I found this solution worked best for me. This is a simplification of #AldekeinĀ“s solution but without jQuery.
function getCursorPosition(canvas, event) {
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
console.log("x: " + x + " y: " + y)
}
const canvas = document.querySelector('canvas')
canvas.addEventListener('mousedown', function(e) {
getCursorPosition(canvas, e)
})
Update (5/5/16): patriques' answer should be used instead, as it's both simpler and more reliable.
Since the canvas isn't always styled relative to the entire page, the canvas.offsetLeft/Top doesn't always return what you need. It will return the number of pixels it is offset relative to its offsetParent element, which can be something like a div element containing the canvas with a position: relative style applied. To account for this you need to loop through the chain of offsetParents, beginning with the canvas element itself. This code works perfectly for me, tested in Firefox and Safari but should work for all.
function relMouseCoords(event){
var totalOffsetX = 0;
var totalOffsetY = 0;
var canvasX = 0;
var canvasY = 0;
var currentElement = this;
do{
totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft;
totalOffsetY += currentElement.offsetTop - currentElement.scrollTop;
}
while(currentElement = currentElement.offsetParent)
canvasX = event.pageX - totalOffsetX;
canvasY = event.pageY - totalOffsetY;
return {x:canvasX, y:canvasY}
}
HTMLCanvasElement.prototype.relMouseCoords = relMouseCoords;
The last line makes things convenient for getting the mouse coordinates relative to a canvas element. All that's needed to get the useful coordinates is
coords = canvas.relMouseCoords(event);
canvasX = coords.x;
canvasY = coords.y;
Edit 2018: This answer is pretty old and it uses checks for old browsers that are not necessary anymore, as the clientX and clientY properties work in all current browsers. You might want to check out Patriques Answer for a simpler, more recent solution.
Original Answer:
As described in an article i found back then but exists no longer:
var x;
var y;
if (e.pageX || e.pageY) {
x = e.pageX;
y = e.pageY;
}
else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
x -= gCanvasElement.offsetLeft;
y -= gCanvasElement.offsetTop;
Worked perfectly fine for me.
Modern browser's now handle this for you. Chrome, IE9, and Firefox support the offsetX/Y like this, passing in the event from the click handler.
function getRelativeCoords(event) {
return { x: event.offsetX, y: event.offsetY };
}
Most modern browsers also support layerX/Y, however Chrome and IE use layerX/Y for the absolute offset of the click on the page including margin, padding, etc. In Firefox, layerX/Y and offsetX/Y are equivalent, but offset didn't previously exist. So, for compatibility with slightly older browsers, you can use:
function getRelativeCoords(event) {
return { x: event.offsetX || event.layerX, y: event.offsetY || event.layerY };
}
According to fresh Quirksmode the clientX and clientY methods are supported in all major browsers.
So, here it goes - the good, working code that works in a scrolling div on a page with scrollbars:
function getCursorPosition(canvas, event) {
var x, y;
canoffset = $(canvas).offset();
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft - Math.floor(canoffset.left);
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop - Math.floor(canoffset.top) + 1;
return [x,y];
}
This also requires jQuery for $(canvas).offset().
So this is both simple but a slightly more complicated topic than it seems.
First off there are usually to conflated questions here
How to get element relative mouse coordinates
How to get canvas pixel mouse coordinates for the 2D Canvas API or WebGL
so, answers
How to get element relative mouse coordinates
Whether or not the element is a canvas getting element relative mouse coordinates is the same for all elements.
There are 2 simple answers to the question "How to get canvas relative mouse coordinates"
Simple answer #1 use offsetX and offsetY
canvas.addEventListner('mousemove', (e) => {
const x = e.offsetX;
const y = e.offsetY;
});
This answer works in Chrome, Firefox, and Safari. Unlike all the other event values offsetX and offsetY take CSS transforms into account.
The biggest problem with offsetX and offsetY is as of 2019/05 they don't exist on touch events and so can't be used with iOS Safari. They do exist on Pointer Events which exist in Chrome and Firefox but not Safari although apparently Safari is working on it.
Another issue is the events must be on the canvas itself. If you put them on some other element or the window you can not later choose the canvas to be your point of reference.
Simple answer #2 use clientX, clientY and canvas.getBoundingClientRect
If you don't care about CSS transforms the next simplest answer is to call canvas. getBoundingClientRect() and subtract the left from clientX and top from clientY as in
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
});
This will work as long as there are no CSS transforms. It also works with touch events and so will work with Safari iOS
canvas.addEventListener('touchmove', (e) => {
const rect = canvas. getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
});
How to get canvas pixel mouse coordinates for the 2D Canvas API
For this we need to take the values we got above and convert from the size the canvas is displayed to the number of pixels in the canvas itself
with canvas.getBoundingClientRect and clientX and clientY
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const elementRelativeX = e.clientX - rect.left;
const elementRelativeY = e.clientY - rect.top;
const canvasRelativeX = elementRelativeX * canvas.width / rect.width;
const canvasRelativeY = elementRelativeY * canvas.height / rect.height;
});
or with offsetX and offsetY
canvas.addEventListener('mousemove', (e) => {
const elementRelativeX = e.offsetX;
const elementRelativeY = e.offsetY;
const canvasRelativeX = elementRelativeX * canvas.width / canvas.clientWidth;
const canvasRelativeY = elementRelativeY * canvas.height / canvas.clientHeight;
});
Note: In all cases do not add padding or borders to the canvas. Doing so will massively complicate the code. Instead of you want a border or padding surround the canvas in some other element and add the padding and or border to the outer element.
Working example using event.offsetX, event.offsetY
[...document.querySelectorAll('canvas')].forEach((canvas) => {
const ctx = canvas.getContext('2d');
ctx.canvas.width = ctx.canvas.clientWidth;
ctx.canvas.height = ctx.canvas.clientHeight;
let count = 0;
function draw(e, radius = 1) {
const pos = {
x: e.offsetX * canvas.width / canvas.clientWidth,
y: e.offsetY * canvas.height / canvas.clientHeight,
};
document.querySelector('#debug').textContent = count;
ctx.beginPath();
ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
ctx.fillStyle = hsl((count++ % 100) / 100, 1, 0.5);
ctx.fill();
}
function preventDefault(e) {
e.preventDefault();
}
if (window.PointerEvent) {
canvas.addEventListener('pointermove', (e) => {
draw(e, Math.max(Math.max(e.width, e.height) / 2, 1));
});
canvas.addEventListener('touchstart', preventDefault, {passive: false});
canvas.addEventListener('touchmove', preventDefault, {passive: false});
} else {
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mousedown', preventDefault);
}
});
function hsl(h, s, l) {
return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
}
.scene {
width: 200px;
height: 200px;
perspective: 600px;
}
.cube {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
animation-duration: 16s;
animation-name: rotate;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
#keyframes rotate {
from { transform: translateZ(-100px) rotateX( 0deg) rotateY( 0deg); }
to { transform: translateZ(-100px) rotateX(360deg) rotateY(720deg); }
}
.cube__face {
position: absolute;
width: 200px;
height: 200px;
display: block;
}
.cube__face--front { background: rgba(255, 0, 0, 0.2); transform: rotateY( 0deg) translateZ(100px); }
.cube__face--right { background: rgba(0, 255, 0, 0.2); transform: rotateY( 90deg) translateZ(100px); }
.cube__face--back { background: rgba(0, 0, 255, 0.2); transform: rotateY(180deg) translateZ(100px); }
.cube__face--left { background: rgba(255, 255, 0, 0.2); transform: rotateY(-90deg) translateZ(100px); }
.cube__face--top { background: rgba(0, 255, 255, 0.2); transform: rotateX( 90deg) translateZ(100px); }
.cube__face--bottom { background: rgba(255, 0, 255, 0.2); transform: rotateX(-90deg) translateZ(100px); }
<div class="scene">
<div class="cube">
<canvas class="cube__face cube__face--front"></canvas>
<canvas class="cube__face cube__face--back"></canvas>
<canvas class="cube__face cube__face--right"></canvas>
<canvas class="cube__face cube__face--left"></canvas>
<canvas class="cube__face cube__face--top"></canvas>
<canvas class="cube__face cube__face--bottom"></canvas>
</div>
</div>
<pre id="debug"></pre>
Working example using canvas.getBoundingClientRect and event.clientX and event.clientY
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.canvas.width = ctx.canvas.clientWidth;
ctx.canvas.height = ctx.canvas.clientHeight;
let count = 0;
function draw(e, radius = 1) {
const rect = canvas.getBoundingClientRect();
const pos = {
x: (e.clientX - rect.left) * canvas.width / canvas.clientWidth,
y: (e.clientY - rect.top) * canvas.height / canvas.clientHeight,
};
ctx.beginPath();
ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
ctx.fillStyle = hsl((count++ % 100) / 100, 1, 0.5);
ctx.fill();
}
function preventDefault(e) {
e.preventDefault();
}
if (window.PointerEvent) {
canvas.addEventListener('pointermove', (e) => {
draw(e, Math.max(Math.max(e.width, e.height) / 2, 1));
});
canvas.addEventListener('touchstart', preventDefault, {passive: false});
canvas.addEventListener('touchmove', preventDefault, {passive: false});
} else {
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mousedown', preventDefault);
}
function hsl(h, s, l) {
return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
}
canvas { background: #FED; }
<canvas width="400" height="100" style="width: 300px; height: 200px"></canvas>
<div>canvas deliberately has differnt CSS size vs drawingbuffer size</div>
I made a full demostration that works in every browser with the full source code of the solution of this problem: Coordinates of a mouse click on Canvas in Javascript. To try the demo, copy the code and paste it into a text editor. Then save it as example.html and, finally, open the file with a browser.
Here is a small modification to Ryan Artecona's answer for canvases with a variable (%) width:
HTMLCanvasElement.prototype.relMouseCoords = function (event) {
var totalOffsetX = 0;
var totalOffsetY = 0;
var canvasX = 0;
var canvasY = 0;
var currentElement = this;
do {
totalOffsetX += currentElement.offsetLeft;
totalOffsetY += currentElement.offsetTop;
}
while (currentElement = currentElement.offsetParent)
canvasX = event.pageX - totalOffsetX;
canvasY = event.pageY - totalOffsetY;
// Fix for variable canvas width
canvasX = Math.round( canvasX * (this.width / this.offsetWidth) );
canvasY = Math.round( canvasY * (this.height / this.offsetHeight) );
return {x:canvasX, y:canvasY}
}
Be wary while doing the coordinate conversion; there are multiple non-cross-browser values returned in a click event. Using clientX and clientY alone are not sufficient if the browser window is scrolled (verified in Firefox 3.5 and Chrome 3.0).
This quirks mode article provides a more correct function that can use either pageX or pageY or a combination of clientX with document.body.scrollLeft and clientY with document.body.scrollTop to calculate the click coordinate relative to the document origin.
UPDATE: Additionally, offsetLeft and offsetTop are relative to the padded size of the element, not the interior size. A canvas with the padding: style applied will not report the top-left of its content region as offsetLeft. There are various solutions to this problem; the simplest one may be to clear all border, padding, etc. styles on the canvas itself and instead apply them to a box containing the canvas.
I'm not sure what's the point of all these answers that loop through parent elements and do all kinds of weird stuff.
The HTMLElement.getBoundingClientRect method is designed to to handle actual screen position of any element. This includes scrolling, so stuff like scrollTop is not needed:
(from MDN) The amount of scrolling that has been done of the viewport area (or
any other scrollable element) is taken into account when computing the
bounding rectangle
Normal image
The very simplest approach was already posted here. This is correct as long as no wild CSS rules are involved.
Handling stretched canvas/image
When image pixel width isn't matched by it's CSS width, you'll need to apply some ratio on pixel values:
/* Returns pixel coordinates according to the pixel that's under the mouse cursor**/
HTMLCanvasElement.prototype.relativeCoords = function(event) {
var x,y;
//This is the current screen rectangle of canvas
var rect = this.getBoundingClientRect();
var top = rect.top;
var bottom = rect.bottom;
var left = rect.left;
var right = rect.right;
//Recalculate mouse offsets to relative offsets
x = event.clientX - left;
y = event.clientY - top;
//Also recalculate offsets of canvas is stretched
var width = right - left;
//I use this to reduce number of calculations for images that have normal size
if(this.width!=width) {
var height = bottom - top;
//changes coordinates by ratio
x = x*(this.width/width);
y = y*(this.height/height);
}
//Return as an array
return [x,y];
}
As long as the canvas has no border, it works for stretched images (jsFiddle).
Handling CSS borders
If the canvas has thick border, the things get little complicated. You'll literally need to subtract the border from the bounding rectangle. This can be done using .getComputedStyle. This answer describes the process.
The function then grows up a little:
/* Returns pixel coordinates according to the pixel that's under the mouse cursor**/
HTMLCanvasElement.prototype.relativeCoords = function(event) {
var x,y;
//This is the current screen rectangle of canvas
var rect = this.getBoundingClientRect();
var top = rect.top;
var bottom = rect.bottom;
var left = rect.left;
var right = rect.right;
//Subtract border size
// Get computed style
var styling=getComputedStyle(this,null);
// Turn the border widths in integers
var topBorder=parseInt(styling.getPropertyValue('border-top-width'),10);
var rightBorder=parseInt(styling.getPropertyValue('border-right-width'),10);
var bottomBorder=parseInt(styling.getPropertyValue('border-bottom-width'),10);
var leftBorder=parseInt(styling.getPropertyValue('border-left-width'),10);
//Subtract border from rectangle
left+=leftBorder;
right-=rightBorder;
top+=topBorder;
bottom-=bottomBorder;
//Proceed as usual
...
}
I can't think of anything that would confuse this final function. See yourself at JsFiddle.
Notes
If you don't like modifying the native prototypes, just change the function and call it with (canvas, event) (and replace any this with canvas).
Here is a very nice tutorial-
http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
<canvas id="myCanvas" width="578" height="200"></canvas>
<script>
function writeMessage(canvas, message) {
var context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
context.font = '18pt Calibri';
context.fillStyle = 'black';
context.fillText(message, 10, 25);
}
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
canvas.addEventListener('mousemove', function(evt) {
var mousePos = getMousePos(canvas, evt);
var message = 'Mouse position: ' + mousePos.x + ',' + mousePos.y;
writeMessage(canvas, message);
}, false);
hope this helps!
Using jQuery in 2016, to get click coordinates relative to the canvas, I do:
$(canvas).click(function(jqEvent) {
var coords = {
x: jqEvent.pageX - $(canvas).offset().left,
y: jqEvent.pageY - $(canvas).offset().top
};
});
This works since both canvas offset() and jqEvent.pageX/Y are relative to the document regardless of scroll position.
Note that if your canvas is scaled then these coordinates are not the same as canvas logical coordinates. To get those, you would also do:
var logicalCoords = {
x: coords.x * (canvas.width / $(canvas).width()),
y: coords.y * (canvas.height / $(canvas).height())
}
I recommend this link-
http://miloq.blogspot.in/2011/05/coordinates-mouse-click-canvas.html
<style type="text/css">
#canvas{background-color: #000;}
</style>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", init, false);
function init()
{
var canvas = document.getElementById("canvas");
canvas.addEventListener("mousedown", getPosition, false);
}
function getPosition(event)
{
var x = new Number();
var y = new Number();
var canvas = document.getElementById("canvas");
if (event.x != undefined && event.y != undefined)
{
x = event.x;
y = event.y;
}
else // Firefox method to get the position
{
x = event.clientX + document.body.scrollLeft +
document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop +
document.documentElement.scrollTop;
}
x -= canvas.offsetLeft;
y -= canvas.offsetTop;
alert("x: " + x + " y: " + y);
}
</script>
In Prototype, use cumulativeOffset() to do the recursive summation as mentioned by Ryan Artecona above.
http://www.prototypejs.org/api/element/cumulativeoffset
You could just do:
var canvas = yourCanvasElement;
var mouseX = (event.clientX - (canvas.offsetLeft - canvas.scrollLeft)) - 2;
var mouseY = (event.clientY - (canvas.offsetTop - canvas.scrollTop)) - 2;
This will give you the exact position of the mouse pointer.
See demo at http://jsbin.com/ApuJOSA/1/edit?html,output .
function mousePositionOnCanvas(e) {
var el=e.target, c=el;
var scaleX = c.width/c.offsetWidth || 1;
var scaleY = c.height/c.offsetHeight || 1;
if (!isNaN(e.offsetX))
return { x:e.offsetX*scaleX, y:e.offsetY*scaleY };
var x=e.pageX, y=e.pageY;
do {
x -= el.offsetLeft;
y -= el.offsetTop;
el = el.offsetParent;
} while (el);
return { x: x*scaleX, y: y*scaleY };
}
I was creating an application having a canvas over a pdf, that involved a lot of resizes of canvas like Zooming the pdf-in and out, and in turn on every zoom-in/out of PDF I had to resize the canvas to adapt the size of the pdf, I went through lot of answers in stackOverflow, and didn't found a perfect solution that will eventually solve the problem.
I was using rxjs and angular 6, and didn't found any answer specific to the newest version.
Here is the entire code snippet that would be helpful, to anyone leveraging rxjs to draw on top of canvas.
private captureEvents(canvasEl: HTMLCanvasElement) {
this.drawingSubscription = fromEvent(canvasEl, 'mousedown')
.pipe(
switchMap((e: any) => {
return fromEvent(canvasEl, 'mousemove')
.pipe(
takeUntil(fromEvent(canvasEl, 'mouseup').do((event: WheelEvent) => {
const prevPos = {
x: null,
y: null
};
})),
takeUntil(fromEvent(canvasEl, 'mouseleave')),
pairwise()
)
})
)
.subscribe((res: [MouseEvent, MouseEvent]) => {
const rect = this.cx.canvas.getBoundingClientRect();
const prevPos = {
x: Math.floor( ( res[0].clientX - rect.left ) / ( rect.right - rect.left ) * this.cx.canvas.width ),
y: Math.floor( ( res[0].clientY - rect.top ) / ( rect.bottom - rect.top ) * this.cx.canvas.height )
};
const currentPos = {
x: Math.floor( ( res[1].clientX - rect.left ) / ( rect.right - rect.left ) * this.cx.canvas.width ),
y: Math.floor( ( res[1].clientY - rect.top ) / ( rect.bottom - rect.top ) * this.cx.canvas.height )
};
this.coordinatesArray[this.file.current_slide - 1].push(prevPos);
this.drawOnCanvas(prevPos, currentPos);
});
}
And here is the snippet that fixes, mouse coordinates relative to size of the canvas, irrespective of how you zoom-in/out the canvas.
const prevPos = {
x: Math.floor( ( res[0].clientX - rect.left ) / ( rect.right - rect.left ) * this.cx.canvas.width ),
y: Math.floor( ( res[0].clientY - rect.top ) / ( rect.bottom - rect.top ) * this.cx.canvas.height )
};
const currentPos = {
x: Math.floor( ( res[1].clientX - rect.left ) / ( rect.right - rect.left ) * this.cx.canvas.width ),
y: Math.floor( ( res[1].clientY - rect.top ) / ( rect.bottom - rect.top ) * this.cx.canvas.height )
};
Here is some modifications of the above Ryan Artecona's solution.
function myGetPxStyle(e,p)
{
var r=window.getComputedStyle?window.getComputedStyle(e,null)[p]:"";
return parseFloat(r);
}
function myGetClick=function(ev)
{
// {x:ev.layerX,y:ev.layerY} doesn't work when zooming with mac chrome 27
// {x:ev.clientX,y:ev.clientY} not supported by mac firefox 21
// document.body.scrollLeft and document.body.scrollTop seem required when scrolling on iPad
// html is not an offsetParent of body but can have non null offsetX or offsetY (case of wordpress 3.5.1 admin pages for instance)
// html.offsetX and html.offsetY don't work with mac firefox 21
var offsetX=0,offsetY=0,e=this,x,y;
var htmls=document.getElementsByTagName("html"),html=(htmls?htmls[0]:0);
do
{
offsetX+=e.offsetLeft-e.scrollLeft;
offsetY+=e.offsetTop-e.scrollTop;
} while (e=e.offsetParent);
if (html)
{
offsetX+=myGetPxStyle(html,"marginLeft");
offsetY+=myGetPxStyle(html,"marginTop");
}
x=ev.pageX-offsetX-document.body.scrollLeft;
y=ev.pageY-offsetY-document.body.scrollTop;
return {x:x,y:y};
}
First, as others have said, you need a function to get the position of the canvas element. Here's a method that's a little more elegant than some of the others on this page (IMHO). You can pass it any element and get its position in the document:
function findPos(obj) {
var curleft = 0, curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return { x: curleft, y: curtop };
}
return undefined;
}
Now calculate the current position of the cursor relative to that:
$('#canvas').mousemove(function(e) {
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
var coordinateDisplay = "x=" + x + ", y=" + y;
writeCoordinateDisplay(coordinateDisplay);
});
Notice that I've separated the generic findPos function from the event handling code. (As it should be. We should try to keep our functions to one task each.)
The values of offsetLeft and offsetTop are relative to offsetParent, which could be some wrapper div node (or anything else, for that matter). When there is no element wrapping the canvas they're relative to the body, so there is no offset to subtract. This is why we need to determine the position of the canvas before we can do anything else.
Similary, e.pageX and e.pageY give the position of the cursor relative to the document. That's why we subtract the canvas's offset from those values to arrive at the true position.
An alternative for positioned elements is to directly use the values of e.layerX and e.layerY. This is less reliable than the method above for two reasons:
These values are also relative to the entire document when the event does not take place inside a positioned element
They are not part of any standard
ThreeJS r77
var x = event.offsetX == undefined ? event.layerX : event.offsetX;
var y = event.offsetY == undefined ? event.layerY : event.offsetY;
mouse2D.x = ( x / renderer.domElement.width ) * 2 - 1;
mouse2D.y = - ( y / renderer.domElement.height ) * 2 + 1;
After trying many solutions. This worked for me. Might help someone else hence posting. Got it from here
Here is a simplified solution (this doesn't work with borders/scrolling):
function click(event) {
const bound = event.target.getBoundingClientRect();
const xMult = bound.width / can.width;
const yMult = bound.height / can.height;
return {
x: Math.floor(event.offsetX / xMult),
y: Math.floor(event.offsetY / yMult),
};
}
Hey, this is in dojo, just cause it's what I had the code in already for a project.
It should be fairly Obvious how to convert it back to non dojo vanilla JavaScript.
function onMouseClick(e) {
var x = e.clientX;
var y = e.clientY;
}
var canvas = dojo.byId(canvasId);
dojo.connect(canvas,"click",onMouseClick);
Hope that helps.

Categories