-1

EDIT: I have better rewrite the question, I can see was badly worded. Well worthy of a down vote... Apologies.

I'm trying to get x,y coordinates when the user clicks inside a canvas element.

I believe I have a correct solution. I have constructed a unit-test, which my solution passes.

What I'm asking is whether it is possible to construct a unit-test for which my solution fails. Maybe by CSS-scaling the canvas element, enclosing in an iframe, etc. This question is about strengthening the unit test.

As I am newcomer to web development, I'm guessing there are awkward scenarios that I am currently unable to anticipate.

Here is my unit test:

function getNumericStyleProperty(style, prop){
    return parseInt(style.getPropertyValue(prop),10) ;
}

function element_position(e) {
    var x = 0, y = 0;
    var inner = true ;
    do {
        x += e.offsetLeft;
        y += e.offsetTop;
        var style = getComputedStyle(e,null) ;
        var borderTop = getNumericStyleProperty(style,"border-top-width") ;
        var borderLeft = getNumericStyleProperty(style,"border-left-width") ;
        y += borderTop ;
        x += borderLeft ;
        if (inner){
          var paddingTop = getNumericStyleProperty(style,"padding-top") ;
          var paddingLeft = getNumericStyleProperty(style,"padding-left") ;
          y += paddingTop ;
          x += paddingLeft ;
        }
        inner = false ;
    } while (e = e.offsetParent);
    return { x: x, y: y };
}

var c = document.getElementById('c');
var t = c.getContext('2d');
t.font = '10px monospace';
c.addEventListener('click', function(e) {
    t.fillStyle = "white"
    t.fillRect(0, 0, c.width, c.height);
    t.fillStyle = "black"

    t.fillText('page:     ' + e.pageX 
                     + ", " + e.pageY, 16, 16*1 );
    
    var bcr = e.target.getBoundingClientRect();
    t.fillText('BoundingClientRect: ' + bcr.left
                               + ", " + bcr.top , 16, 16*2);
    
    // Method 1
    var style = getComputedStyle(e.target,null) ;

    var borderTop = getNumericStyleProperty(style,"border-top-width") ;
    var borderLeft = getNumericStyleProperty(style,"border-left-width") ;
    var paddingTop = getNumericStyleProperty(style,"padding-top") ;
    var paddingLeft = getNumericStyleProperty(style,"padding-left") ;

    t.fillText('client :  ' + e.clientX
                     + ", " + e.clientY, 16, 16*3);

    t.fillText('bcr.x-client-border-padding:  ' 
               + (e.clientX - bcr.left - borderLeft - paddingLeft)
        + ", " + (e.clientY - bcr.top  - borderTop - paddingTop), 16, 16*4);
    
    // Method 2
    var p = element_position(e.target);
    t.fillText('element_position:  ' + p.x
                              + ", " + p.y, 16, 16*5); 

    t.fillText('e.page - element_position():  ' 
               + (e.pageX - p.x)
        + ", " + (e.pageY - p.y), 16, 16*6);
       
}, false);
body {
    margin:     1em; 
    background: #8888ff; 
    padding:    1em;
}

.myDiv {
    position:   absolute;
    left:       20px; 
    top:        40px; 
    border:     solid #88ff88 10px; 
    background: green; 
    margin:     1em; 
    padding:    0.5em;
}

canvas {
    border:     solid red 20px;
    position:   relative; 
    left:       20px; 
    top:        0px;
    padding:    10px;  
    background: brown;  
    margin:     1em
}
<p style="background:white">body</p>
<div class="myDiv">
    <p style="background:white">myDiv</p>
        
    <canvas id="c" width="300" height="200" style="cursor:crosshair"> </canvas>
</div>

How could this unit-test be improved? I'm trying to construct the simplest "most awkward scenario".

Can anyone adapt it so that it no longer returns the correct click position within the canvas?

EDIT: blurb from the original question moved here:

There are a lot of bad solutions. The Internet seems full of copy-paste coding when I google this one. Although I have solutions I could work with, I would like to examine this problem more closely and see whether a robust generic solution can be achieved.

Here are various solutions I've found:
getting mouse position relative to content area of an element -- this question has an excellent answer (together with live example) which still exhibits the same offset problem.

How do I get the coordinates of a mouse click on a canvas element? <-- this question is hopelessly cluttered.

http://miloq.blogspot.in/2011/05/coordinates-mouse-click-canvas.html <-- also exhibits the same behaviour.

Getting cursor position in a canvas without jQuery <-- uses document.documentElement which might be an alternative to faffling with CSS margin/border/padding, or just an error. Part of the problem is that it is such a common question, everyone has tried to do it, but there is no unique solution. So there is a lot of clutter. Another part of the problem is that the underlying browser APIs keep shifting.

Another part of the problem is that nobody seems to be testing against a setup that evinces failure from inferior methods, and that's what I'm trying to address here.

Community
  • 1
  • 1
P i
  • 29,020
  • 36
  • 159
  • 267
  • WADR, since you're in control of the coding, it's your job to **Avoid the most awkward scenarios!** – markE Apr 15 '15 at 16:42
  • I'm a little confused: *"Can anyone adapt it so that it no longer returns the correct click position within the canvas?"* you ***don't*** want the correct position in canvas? –  Apr 15 '15 at 16:47
  • Please state what you want the click coordinate to be relative to: client, document, screen or canvas –  Apr 15 '15 at 16:52
  • 1
    Sorry, bad communication on my part. I've rewritten the question! – P i Apr 15 '15 at 18:58
  • This type of question might be a better fit on the sister-site, CodeReview: http://codereview.stackexchange.com/ – markE Apr 15 '15 at 19:01

1 Answers1

2

This is because you haven't followed earlier advices given to you.

When you use getBoundingClientRect() it will add border and padding if any. You have to subtract those from the result.

The advice is, instead of doing that, to wrap the canvas in a div and apply border and padding to that instead. Implementing this in your code results in the update below.

PS: Also, the code you marked "works" in the comments, does not work (see console, it neither prints anything to canvas. If it did it would write on top of the previous code...).

function getNumericStyleProperty(style, prop){
    return parseInt(style.getPropertyValue(prop),10) ;
}

function element_position(e) {
    var x = 0, y = 0;
    var inner = true ;
    do {
        x += e.offsetLeft;
        y += e.offsetTop;
        var style = getComputedStyle(e,null) ;
        var borderTop = getNumericStyleProperty(style,"border-top-width") ;
        var borderLeft = getNumericStyleProperty(style,"border-left-width") ;
        y += borderTop ;
        x += borderLeft ;
        if (inner){
          var paddingTop = getNumericStyleProperty(style,"padding-top") ;
          var paddingLeft = getNumericStyleProperty(style,"padding-left") ;
          y += paddingTop ;
          x += paddingLeft ;
        }
        inner = false ;
    } while (e = e.offsetParent);
    return { x: x, y: y };
}

var c = document.getElementById('c');
var t = c.getContext('2d');
t.font = '10px monospace';
c.addEventListener('click', function(e) {
    t.fillStyle = "white"
    t.fillRect(0, 0, c.width, c.height);
    t.fillStyle = "black"

    t.fillText('pageX:     ' + e.pageX, 16, 16);
    t.fillText('pageY:     ' + e.pageX, 16, 16*2);
    
    var bcr = e.target.getBoundingClientRect();
    t.fillText('BoundingClientRect.left: ' + bcr.left, 16, 16*3);
    t.fillText('BoundingClientRect.top:  ' + bcr.top , 16, 16*4);
    
    // fails
    t.fillText('clientX :  ' + e.clientX, 16, 16*5);
    t.fillText('clientY :  ' + e.clientY, 16, 16*6);
    t.fillText('bcr.x-clientX:  ' + (e.clientX - bcr.left), 16, 16*7);
    t.fillText('bcr.y-clientY:  ' + (e.clientY - bcr.top), 16, 16*8);
    
    // works
    var p = element_position(e.eventTarget);
    t.fillText('element_position x:  ' + p.x, 16, 16*9);
    t.fillText('element_position y:  ' + p.y, 16, 16*10); 
    t.fillText('canvas x:  ' + (e.pageX - p.x), 16, 16*11);
    t.fillText('canvas y:  ' + (e.pageY - p.y), 16, 16*12);
       
}, false);
body {
    margin:     1em; 
    background: #8888ff; 
    padding:    1em;
}

.myDiv {
    position:   absolute;
    left:       20px; 
    top:        40px; 
    border:     solid #88ff88 10px; 
    background: green; 
    margin:     1em; 
    padding:    0.5em;
}

.wrapper {
    border:     solid red 20px;
    position:   relative; 
    left:       20px; 
    top:        0px;
    padding:    10px;  
    background: brown;  
    margin:     1em
}
<p style="background:white">body</p>
    <div class="myDiv">
        <p style="background:white">myDiv</p>
        
        <div class="wrapper">
          <canvas id="c" width="200" height="200" style="cursor:crosshair"> </canvas>
        </div>
    </div>
Community
  • 1
  • 1
  • Very sorry about this, I hit the "post" button far too early, and I think I must have been doing major edits at the same time as you were constructing this reply. I must be careful to avoid doing this in future. My idea was to create a function that would work irrespective of any styling. Manually subtracting the border and padding satisfies my unit-test (as demonstrated by my final question code). What I'm asking is whether the unit test is strong enough, or whether a stronger test can be devised. For example, applying a CSS scale of 50% would, I think, throw the results off. – P i Apr 15 '15 at 18:46
  • 1
    @Pi no worries. It can happen. If you apply transforms with CSS (scale etc.) you need to use the difference between the bitmap size and the element size. I answered this for video element, but it will apply for canvas as well, hopefully it can help out (see the third code block): http://stackoverflow.com/questions/29522895/javascript-track-mouse-position-within-video/29527843#29527843 –  Apr 16 '15 at 01:54