Canvas mouse click, offsets and parent nodes - javascript

I am new to canvas and I need to know the coordinates where the mouse was clicked. I was successful with:
var canvas = document.getElementById('MainMap');
canvas.addEventListener('mousedown', onMapMouseDown, false);
The problem comes when I try to get the right coordinates. I tried
var x = ev.x - canvas.offsetLeft;
var y = ev.y - canvas.offsetTop;
except that canvas.offsetLeft = 0 and canvas.offsetTop = 40. (Actually it is right since the canvas is placed at that position in a div).
The div is in another div that is in a table that is in a div..... and so on.
The Solution to get the right position would be by adding offsets in a recursive function by getParentNode();
The question is then. How do I now I've reached the Top level node. May be the document or the top level div or table.

You don't need to traverse every node. offsetLeft and offsetTop represent the distance of the element respect to its offset parent. And what is the offsetParent? Well, it's the first ancestor of a node that has a position different from static (this is, relative, absolute or fixed).
Fortunately for you, DOM elements contain a property named offsetParent which indicates what ancestor is the offset parent, so...
var offset = {x: 0, y: 0};
var node = canvas;
while (node) {
offset.x += node.offsetLeft;
offset.y += node.offsetTop;
node = node.offsetParent;
}
This goes on until node is the document, which is placed at (0, 0).
Another, quicker way, is uing jQuery:
var offset = $('#MainMap').offset();

I believe you need to use the 'screen' width and height.. Script libraries like Kinetic.js make this a lot easier for you. Still, if you insist on writing the functionality yourself, I found this tutorial quite helpful:
http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
Or this blog post:
http://miloq.blogspot.be/2011/05/coordinates-mouse-click-canvas.html

Related

Move transformed HTML element to absolute position

I want to write a JavaScript function that moves CSS3 transformed (scaled, skewed, rotated) div elements to an absolute position in the container box. We are developing a canvas like application. I am using getBoundingClientRect() to find an absolute position and bounding-rect of the element. However, when I move this element to specified position it does not work because x and y position has translated.
Move to (0, 0):
// pseudocode I tried!
const matrix = getTransformationMatrix(elm); // get elements transormation matrix.
const pos = matrix.applyToPoint(0, 0); // find related coordiate of zeroth postion by applying element's matrix
tranlatePosition(elm, pos.x, pos.y);

Tracking relative mouse position without a mousemove event

I need to track mouse position relative to a <canvas> element in my app. Currently, I have a mousemove event listener attached to the <canvas> that updates my mouse position whenever it fires, using offsetX/offsetY when available, or layerX/layerY when the offsetX/Y is not available. Using offsetX/Y or layerX/Y gives me mouse coordinates relative to my <canvas>, which is exactly what I want. As my app works its magic, various CSS 3d transformations get applied to the <canvas>, and even when <canvas> is very transformed, offsetX/Y still gives me accurate coordinates within the <canvas>'s local, transformed coordinate-space.
That's kind of confusing, so I'll try stating an example. If my <canvas> is 100px in both width and height, and is located at (0,0) relative to the browser viewport, and I click at (50,50) (in viewport coords), that corresponds to (50,50) in my <canvas>, and 50 is the value that is (correctly) returned via offsetX and offsetY. If I then apply transform: translate3d(20px,20px,0px) to my <canvas> and click at (50,50) (in viewport coords), since my canvas has been shifted 20px down and 20px to the right, that actually corresponds to (30,30) relative to the <canvas>, and 30 is the value that is (correctly) returned via offsetX and offsetY.
The problem I'm facing is what to do when the user is not physically moving the mouse, yet the <canvas> is being transformed. I'm only updating the position of the mouse on mousemove events, so what do I do when there is no mousemove?
For example. My mouse is positioned at (50,50) and no transformations are applied to the <canvas>. My this.mouseX and this.mouseY are both equal to 50; they were saved at the final mousemove event when I moved the mouse to (50,50). Without moving the mouse at all, I apply the above transformation (transform: translate3d(20px,20px,0px)) to my <canvas>. Now, I need this.mouseX and this.mouseY to each be equal to 30, as that is my mouse's new position relative to the current transformation of <canvas>. But this.mouseX and this.mouseY are still equal to 50. Since I never moved the mouse, there was no mousemove event fired, and those saved coords never got updated.
How can I deal with this? I thought about creating a new jQuery event, manually assigning some properties (pageX and pageY?) based on my old/previous mouse position, and then triggering that event, but I don't think that's going to cause the browser to recalculate the offsetX and offsetY properties. I've also been thinking about taking the known old/previous mouse position and multiplying it by my transformation matrix, but that's going to get real complicated since my mouse coordinates are in 2d-space, but the transformations I'm applying to <canvas> are all 3d transformations.
I guess really, what I want to do is take my known 2d page position and raycast it into the 3d space and find out where I'm hitting the transformed <canvas>, all in javascript (jQuery is available).
Is this possible? Does this even make sense?
Works in all browsers
var mouseX=0;
var mouseY=0;
var canvas = document.querySelector('#canvas');
var rect = canvas.getBoundingClientRect();
document.onmousemove = function(e) {
mouseX=e.clientX-rect.left;
mouseY=e.clientY-rect.top;
};
function updateCoords() {
mouseX=e.clientX-mouseX;
mouseY=e.clientY-mouseY;
setTimeout(updatecoords,10);
}
Now we can call updateCoords() function once to repeatedly check for new position.
updateCoords();
You can add your code inside the updateCoords() function and it will be executed each 10 milliseconds
Concept: mouseX and mouseY variables get updated on mousemove event, and also get updated when there is any change in the canvas position.
It looks like you want to refresh your mouseposition-values even if you don't move your mouse. You should try something like this:
var event = '';
var counter = 1;
$(function(e){
event = e;
window.setInterval(refresh, 10);
});
$(document).mousemove(function(e){
event = e;
refresh;
});
function refresh(){
counter++;
$('#mousepos').val("event.pageX: " + event.pageX + ", event.pageY: " + event.pageY + ", counter: " + counter)
}
The counter is just for visualisation of the refresh. You can set the interval to everything you want (10 = 10ms = 0.01s) Just move everything from your .mousemove() event into this refresh() function and call it properly and your mouse position should update even if you don't move your mouse.
Look at this fiddle for a life example: http://jsfiddle.net/82cmxw8L/1
EDIT:
Because my fiddle didn't work for the asker, i updated it: http://jsfiddle.net/82cmxw8L/8/
New is, that the mouseposition is now set every 0.1 Second, no matter what, rather than being updated only when the mouse moves.

Mouse position and offset

I get mouse coordinates on some web page and save them.
$("div#container").mousemove( function(e) {
client_x = e.pageX;
client_y = e.pageY;
// save x,y
});
Now other person load that same page and i want to show them the same coordinates (x,x position).
How can I get the same point if I have to take in consideration that the div#container is not at same position as it was in my browser (considering screen resolution and scroll)?
I would use $.offset().top and $.offset().left of the parent div container, and calculate the difference from that to the current X and Y coordinates of the mouse cursor.
.offset() always refers to the document and not to the parent of the element.
For example:
$('#element').mousemove(function(e){
var client_x = e.pageX;
var client_y = e.pageY;
var elementOffset = $(this).offset();
client_x -= elementOffset.left;
client_y -= elementOffset.top;
// save x, y.
});
Then, on the other users display, show the coordinates after adding them to his offsets.
This doesn't seem possible because of the variables you mentioned in the question. Screen resolution is the main reason, but, also, it depends on how big their window is. At first, you might think that you could compute the mouse's position relative to fixed points, like divs shown (take Stack Overflow, for example, where the main container of the site doesn't resize with the browser window). But if their window is smaller than the container, you would be making some false assumptions about what they see.
That being said, you can always just compute the mouse position relative to fixed elements you know will be on the screenusing $.offset() and just assume they have their screen showing everything (or check $(window) size) and are using "normal" viewing conditions.
You can use the values returned by offset(), in your example:
$("div#container").offset().left
and
$("div#container").offset().top
to substract them to e.pageXand e.pageY.
offset() function gives you the matched element's position relative to the document (see the docs), so there's no problem if the users scroll down.
Example: http://jsfiddle.net/3jMRS/

HTML5/Canvas Mouse Position off when placed in actual Site

I'm capturing mouse position like this
mouse_move: function(e)
{
mousePos.x = e.pageX - vr.o.context.canvas.offsetLeft;
mousePos.y = e.pageY - vr.o.context.canvas.offsetTop;
},
and it has worked like a dream in all modern browsers while in development, Even tested Wrapping the <canvas/> in a basic dom structure to make sure mouse position adjusted...
obviously now I'm putting it in the actual site it's not working...
You can see here http://jondavidjohn.com/projects/
the mouse position ends up quite a ways south of the actual cursor, anything specifically that could be causing this?
SOLUTION
mouse_move: function(e)
{
mousePos.x = e.offsetX;
mousePos.y = e.offsetY;
},
COPIED FROM: http://simonsarris.com/blog/510-making-html5-canvas-useful
Getting mouse coordinates on Canvas
Getting good mouse coordinates is a little tricky on Canvas. You could use offsetX/Y and LayerX/Y, but LayerX/Y is deprecated in webkit (Chrome and Safari) and Firefox does not have offsetX/Y.
The most bulletproof way to get the correct mouse position is shown below. You have to walk up the tree adding the offsets together. Then you must add any padding or border to the offset. Finally, to fix coordinate problems when you have fixed-position elements on the page (like the wordpress admin bar or a stumbleupon bar) you must add the ’s offsetTop and offsetLeft.
Then you simply subtract that offset from the e.pageX/Y values and you’ll get perfect coordinates in almost every possible situation.
// Creates an object with x and y defined,
// set to the mouse position relative to the state's canvas
// If you wanna be super-correct this can be tricky,
// we have to worry about padding and borders
CanvasState.prototype.getMouse = function(e) {
var element = this.canvas, offsetX = 0, offsetY = 0, mx, my;
// Compute the total offset
if (element.offsetParent !== undefined) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
// Add padding and border style widths to offset
// Also add the <html> offsets in case there's a position:fixed bar
offsetX += this.stylePaddingLeft + this.styleBorderLeft + this.htmlLeft;
offsetY += this.stylePaddingTop + this.styleBorderTop + this.htmlTop;
mx = e.pageX - offsetX;
my = e.pageY - offsetY;
// We return a simple javascript object (a hash) with x and y defined
return {x: mx, y: my};
}
Use e.offsetX and e.offsetY for now instead.
It actually gets more complicated when you introduce some other things, like margins and padding, but offsetX and offsetY will be much more accurate than what you've got to say the least.
I don't have my new "bulletproof-works-in-every-situation" mouse code on me right now, I can get that later for you if you think you'll need it.
edit: Derp! Thanks chopperdave for finally providing the code I forgot to add!

How to get the MouseEvent coordinates for an element that has CSS3 Transform?

I want to detect where a MouseEvent has occurred, in coordinates relative to the clicked element. Why? Because I want to add an absolutely positioned child element at the clicked location.
I know how to detect it when no CSS3 transformations exist (see description below). However, when I add a CSS3 Transform, then my algorithm breaks, and I don't know how to fix it.
I'm not using any JavaScript library, and I want to understand how things work in plain JavaScript. So, please, don't answer with "just use jQuery".
By the way, I want a solution that works for all MouseEvents, not just "click". Not that it matters, because I believe all mouse events share the same properties, thus the same solution should work for all of them.
Background information
According to DOM Level 2 specification, a MouseEvent has few properties related to getting the event coordinates:
screenX and screenY return the screen coordinates (the origin is the top-left corner of user's monitor)
clientX and clientY return the coordinates relative the document viewport.
Thus, in order to find the position of the MouseEvent relative to the clicked element content, I must do this math:
ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
ev.clientX is the coordinate relative to the document viewport
this.getBoundingClientRect().left is the position of the element relative to the document viewport
this.clientLeft is the amount of border (and scrollbar) between the element boundary and the inner coordinates
this.scrollLeft is the amount of scrolling inside the element
getBoundingClientRect(), clientLeft and scrollLeft are specified at CSSOM View Module.
Experiment without CSS Transform (it works)
Confusing? Try the following piece of JavaScript and HTML. Upon clicking, a red dot should appear exactly where the click has happened. This version is "quite simple" and works as expected.
function click_handler(ev) {
var rect = this.getBoundingClientRect();
var left = ev.clientX - rect.left - this.clientLeft + this.scrollLeft;
var top = ev.clientY - rect.top - this.clientTop + this.scrollTop;
var dot = document.createElement('div');
dot.setAttribute('style', 'position:absolute; width: 2px; height: 2px; top: '+top+'px; left: '+left+'px; background: red;');
this.appendChild(dot);
}
document.getElementById("experiment").addEventListener('click', click_handler, false);
<div id="experiment" style="border: 5px inset #AAA; background: #CCC; height: 400px; position: relative; overflow: auto;">
<div style="width: 900px; height: 2px;"></div>
<div style="height: 900px; width: 2px;"></div>
</div>
Experiment adding a CSS Transform (it fails)
Now, try adding a CSS transform:
#experiment {
transform: scale(0.5);
-moz-transform: scale(0.5);
-o-transform: scale(0.5);
-webkit-transform: scale(0.5);
/* Note that this is a very simple transformation. */
/* Remember to also think about more complex ones, as described below. */
}
The algorithm doesn't know about the transformations, and thus calculates a wrong position. What's more, the results are different between Firefox 3.6 and Chrome 12. Opera 11.50 behaves just like Chrome.
In this example, the only transformation was scaling, so I could multiply the scaling factor to calculate the correct coordinate. However, if we think about arbitrary transformations (scale, rotate, skew, translate, matrix), and even nested transformations (a transformed element inside another transformed element), then we really need a better way to calculate the coordinates.
The behaviour you are experiencing is correct, and your algorithm isn't breaking. Firstly CSS3 Transforms are designed not to interfere with the box model.
To try and explain...
When you apply a CSS3 Transform on an element. the Element assumes a kind of relative positioning. In that the surrounding elements are not effected by the transformed element.
e.g. imagine three div's in a horizontal row. If you apply a scale transform to decrease the size of the centre div. The surrounding div's will not move inwards to occupy the space that was once occupied the transformed element.
example: http://jsfiddle.net/AshMokhberi/bWwkC/
So in the box model, the element does not actually change size. Only it's rendered size changes.
You also have to keep in mind that you are applying a scale Transform, so your elements "real" size is actually the same as it's original size. You are only changing it's perceived size.
To explain..
Imagine you create a div with a width of 1000px and scale it down to 1/2 the size. The internal size of the div is still 1000px, not 500px.
So the position of your dots are correct relative to the div's "real" size.
I modified your example to illustrate.
Instructions
Click the div and keep you mouse in the same position.
Find the dot in the wrong position.
Press Q, the div will become the correct size.
Move your mouse to find the dot in the correct position to where you clicked.
http://jsfiddle.net/AshMokhberi/EwQLX/
So in order to make the mouse clicks co-ordinates match the visible location on the div, you need to understand that the mouse is giving back co-ordinates based on the window, and your div offsets are also based on its "real" size.
As your object size is relative to the window the only solution is to scale the offset co-ordinates by the same scale value as your div.
However this can get tricky based on where you set the Transform-origin property of your div. As that is going to effect the offsets.
See here.
http://jsfiddle.net/AshMokhberi/KmDxj/
Hope this helps.
if element is container and positioned absolute or relative,
you can place inside of it element,
position it relative to parent and
width = 1px, height = 1px, and move to inside of container,
and after each move use document.elementFromPoint(event.clientX, event.clientY) =))))
You can use binary search to make it faster.
looks terrible, but it works
http://jsfiddle.net/3VT5N/3/ - demo
BY FAR the fastest. The accepted answer takes about 40-70 ms on my 3d transforms site, this usually takes less than 20 (fiddle):
function getOffset(event,elt){
var st=new Date().getTime();
var iterations=0;
//if we have webkit, then use webkitConvertPointFromPageToNode instead
if(webkitConvertPointFromPageToNode){
var webkitPoint=webkitConvertPointFromPageToNode(elt,new WebKitPoint(event.clientX,event.clientY));
//if it is off-element, return null
if(webkitPoint.x<0||webkitPoint.y<0)
return null;
return {
x: webkitPoint.x,
y: webkitPoint.y,
time: new Date().getTime()-st
}
}
//make full-size element on top of specified element
var cover=document.createElement('div');
//add styling
cover.style.cssText='height:100%;width:100%;opacity:0;position:absolute;z-index:5000;';
//and add it to the document
elt.appendChild(cover);
//make sure the event is in the element given
if(document.elementFromPoint(event.clientX,event.clientY)!==cover){
//remove the cover
cover.parentNode.removeChild(cover);
//we've got nothing to show, so return null
return null;
}
//array of all places for rects
var rectPlaces=['topleft','topcenter','topright','centerleft','centercenter','centerright','bottomleft','bottomcenter','bottomright'];
//function that adds 9 rects to element
function addChildren(elt){
iterations++;
//loop through all places for rects
rectPlaces.forEach(function(curRect){
//create the element for this rect
var curElt=document.createElement('div');
//add class and id
curElt.setAttribute('class','offsetrect');
curElt.setAttribute('id',curRect+'offset');
//add it to element
elt.appendChild(curElt);
});
//get the element form point and its styling
var eltFromPoint=document.elementFromPoint(event.clientX,event.clientY);
var eltFromPointStyle=getComputedStyle(eltFromPoint);
//Either return the element smaller than 1 pixel that the event was in, or recurse until we do find it, and return the result of the recursement
return Math.max(parseFloat(eltFromPointStyle.getPropertyValue('height')),parseFloat(eltFromPointStyle.getPropertyValue('width')))<=1?eltFromPoint:addChildren(eltFromPoint);
}
//this is the innermost element
var correctElt=addChildren(cover);
//find the element's top and left value by going through all of its parents and adding up the values, as top and left are relative to the parent but we want relative to teh wall
for(var curElt=correctElt,correctTop=0,correctLeft=0;curElt!==cover;curElt=curElt.parentNode){
//get the style for the current element
var curEltStyle=getComputedStyle(curElt);
//add the top and left for the current element to the total
correctTop+=parseFloat(curEltStyle.getPropertyValue('top'));
correctLeft+=parseFloat(curEltStyle.getPropertyValue('left'));
}
//remove all of the elements used for testing
cover.parentNode.removeChild(cover);
//the returned object
var returnObj={
x: correctLeft,
y: correctTop,
time: new Date().getTime()-st,
iterations: iterations
}
return returnObj;
}
and also include the following CSS in the same page:
.offsetrect{
position: absolute;
opacity: 0;
height: 33.333%;
width: 33.333%;
}
#topleftoffset{
top: 0;
left: 0;
}
#topcenteroffset{
top: 0;
left: 33.333%;
}
#toprightoffset{
top: 0;
left: 66.666%;
}
#centerleftoffset{
top: 33.333%;
left: 0;
}
#centercenteroffset{
top: 33.333%;
left: 33.333%;
}
#centerrightoffset{
top: 33.333%;
left: 66.666%;
}
#bottomleftoffset{
top: 66.666%;
left: 0;
}
#bottomcenteroffset{
top: 66.666%;
left: 33.333%;
}
#bottomrightoffset{
top: 66.666%;
left: 66.666%;
}
It essentially splits the element into 9 squares, determines which one the click was in via document.elementFromPoint. It then splits that into 9 smaller squares, etc until it is accurate to within a pixel. I know I over-commented it. The accepted answer is several times slower than this.
EDIT: It is now even faster, and if the user is in Chrome or Safari it will use a native function designed for this instead of the 9 sectors thingy and can do it consistently in LESS THAN 2 MILLISECONDS!
another way is place 3 divs in corners of that element,
than find transform matrix ... but is also works only for positioned containerable
elements – 4esn0k
demo: http://jsfiddle.net/dAwfF/3/
Also, for Webkit webkitConvertPointFromPageToNode method can be used:
var div = document.createElement('div'), scale, point;
div.style.cssText = 'position:absolute;left:-1000px;top:-1000px';
document.body.appendChild(div);
scale = webkitConvertPointFromNodeToPage(div, new WebKitPoint(0, 0));
div.parentNode.removeChild(div);
scale.x = -scale.x / 1000;
scale.y = -scale.y / 1000;
point = webkitConvertPointFromPageToNode(element, new WebKitPoint(event.pageX * scale.x, event.pageY * scale.y));
point.x = point.x / scale.x;
point.y = point.y / scale.x;
To get the coordinates of a MouseEvent relative to the clicked element, use offsetX / layerX.
Have you tried using ev.layerX or ev.offsetX?
var offsetX = (typeof ev.offsetX == "number") ? ev.offsetX : ev.layerX || 0;
See also:
CSSOM View Module: 9 Extensions to the MouseEvent Interface
IE 8 measures clientX from the element's padding edge instead of the content edge: GTalbot MSIE 8 Browser Bugs: event.offsetX, event.offsetY as mouse coordinates inside element target's padding-box
MSDN: offsetX Property
This seems to work really well for me
var elementNewXPosition = (event.offsetX != null) ? event.offsetX : event.originalEvent.layerX;
var elementNewYPosition = (event.offsetY != null) ? event.offsetY : event.originalEvent.layerY;
EDIT: my answer is untested, WIP, I will update when I get it working.
I'm implementing a polyfill of the geomtetry-interfaces. The DOMPoint.matrixTransform method I will make next, which means we should be able to write something like the following in order to map a click coordinate onto a transformed (possiblly nested) DOM element:
// target is the element nested somewhere inside the scene.
function multiply(target) {
let result = new DOMMatrix;
while (target && /* insert your criteria for knowing when you arrive at the root node of the 3D scene*/) {
const m = new DOMMatrix(target.style.transform)
result.preMultiplySelf(m) // see w3c DOMMatrix (geometry-interfaces)
target = target.parentNode
}
return result
}
// inside click handler
// traverse from nested node to root node and multiply on the way
const matrix = multiply(node)
const relativePoint = DOMPoint(clickX, clickY, 0, 800).matrixTransform(matrix)
relativePoint will be the point relative to the element's surface that you clicked on.
A w3c DOMMatrix can be constructed with a CSS transform string argument, which makes it super easy to use in JavaScript.
Unfortunately, this isn't working yet (only Firefox has a geometry-interfaces implementation, and my polyfill does not yet accept a CSS transform string). See: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18
I will update this once I implement that and have a working example. Pull requests welcome!
EDIT: the value 800 is the scene's perspective, I'm not sure if this is what the fourth value for the DOMPoint constructor should be when we intend to do something like this. Also, I'm not sure if I should use preMultiplySelf or postMultiplySelf. I'll find out once I get it at least working (values may be incorrect at first) and will update my answer.
I am working on a polyfill to transfrom DOM coordinates. The GeometryUtils api is not available yet (#see https://drafts.csswg.org/cssom-view/). I created a "simple" code in 2014 to transform coordinates, like localToGlobal, globalToLocal and localToLocal. Its not finished yet, but its working :) I think I will finish it in the coming months (05.07.2017), so if you still need a API to accomplish coordinate transformation give it a try: https://github.com/jsidea/jsidea jsidea core library. Its not stable yet (pre alpha).
You can use it like that:
Create your transform instance:
var transformer = jsidea.geom.Transform.create(yourElement);
The box model you want to transform to (default:"border", will be replaced by ENUM's later on):
var toBoxModel = "border";
The box model where your input coordinates coming from (default:"border"):
var fromBoxModel = "border";
Transform your global coordinates (here {x:50, y:100, z: 0}) to local space. The resulting point has 4 components: x, y, z and w.
var local = transformer.globalToLocal(50, 100, 0, toBoxModel, fromBoxModel);
I have implemented some other functions like localToGlobal and localToLocal.
If you want to give a try, just download the release build and use the jsidea.min.js.
Download the first release here: Download TypeScript code
Feel free to change the code, I never put it under any license :)
I have this issue and started trying to compute the matrix.
I started a library around it: https://github.com/ombr/referentiel
$('.referentiel').each ->
ref = new Referentiel(this)
$(this).on 'click', (e)->
input = [e.pageX, e.pageY]
p = ref.global_to_local(input)
$pointer = $('.pointer', this)
$pointer.css('left', p[0])
$pointer.css('top', p[1])
What do you think ?
Works fine whether relative or absolute :) simple solution
var p = $( '.divName' );
var position = p.position();
var left = (position.left / 0.5);
var top = (position.top / 0.5);

Categories

Resources