Björn Karpenstein
Björn Karpenstein

Reputation: 221

HTML5 Canvas: Use context.isPointInPath(x, y) for complex shapes with more paths

I am drawing a complex shape, that consists of 6 thin lines and two thick line.

In the code i am opening 8 pathes to do this:

        context.save();
    context.lineWidth=2;
    var TAB_ABSTAND=10;
    var TAB_SAITENZAHL=6;
    var TAB_SEITENDICKE=10;
    for(var i=0;i<TAB_SAITENZAHL;i++)
    {
         context.beginPath();
         context.moveTo(this.clickedX, this.clickedY+(i*TAB_ABSTAND));
         context.lineTo(this.clickedX+this.width, this.clickedY+(i*TAB_ABSTAND));
         context.stroke();  
    }



     context.lineWidth=TAB_SEITENDICKE;

     context.beginPath();
     context.moveTo(this.clickedX, this.clickedY-1);
     context.lineTo(this.clickedX, this.clickedY+TAB_ABSTAND*(TAB_SAITENZAHL-1)+1);
     context.stroke();

     context.beginPath();
     context.moveTo(this.clickedX+this.width, this.clickedY-1);
     context.lineTo(this.clickedX+this.width, this.clickedY+TAB_ABSTAND*(TAB_SAITENZAHL-1)+1);
     context.stroke();    

     context.restore();     

In the canvas onmousedown event i want to recognize if the shape (or one of the other shapes in an array) have been clicked to realize a dragging.

Is there a way to use the isPointInPath(x,y) Method to recognize if one of the Lines in the "Shape" has been clicked?

What i want to do is to implement a mechanism that maintains a list of draggable objects.

What i found out yet:

1.) beginPath is the only context method that interrupts the path in a way, that the previous path is not recognized by the isPointInPath method

2.) On a single line with big stroke (i.e. context.lineWidth=10) the isPointInPath method is not returning true when it is just a single line without curves

3.) closePath draws the endpoint of the last line to the beginning point of the first line, but it is not interupting the path, so that a later stroke() always takes effect on the lineTo and moveTo-methods before the closePath

4.) It seems to be impossible to draw a bigger line without calling beginPath() without stroking the rest of the pathes

5.) moveTo(x,y) really jumps to another position, but the other position can be a path that returns true for the isPointInPath method, when it not only consists of one line (see 1.)).

6.) To visualize pathes the fill() method is usefull

So should i always use rectangles to draw lines when i want to recognize if the "line" (that is drawn as rect) is in Path?

Upvotes: 1

Views: 2189

Answers (2)

markE
markE

Reputation: 105015

How to hit-test a shape made of multiple paths

This shape is made up of 2 paths: Path1= 4 thin red lines, Path2= 2 thick blue lines.

enter image description here

First, here is information about Paths that explain why your observations occured

beginPath is the only context method that interrupts the path in a way, that the previous path is not recognized by the isPointInPath method

You can define multiple paths, but isPointInPath will only test the last defined path.

A path consists of a set of path commands formed like this:

  • A path begins with the beginPath command
  • Then a path defines its shape with moveTo, lineTo, etc commands.
  • A path ends with the next beginPath command.

So in your question code, only this last moveTo+lineTo would be tested.

 context.beginPath();
 context.moveTo(this.clickedX+this.width, this.clickedY-1);
 context.lineTo(this.clickedX+this.width, this.clickedY+TAB_ABSTAND*(TAB_SAITENZAHL-1)+1);

On a single line with big stroke (i.e. context.lineWidth=10) the isPointInPath method is not returning true when it is just a single line without curves.

Mathematically, a line occupies no space. Therefore you cannot test if any point is "In" a single line.

In most browsers (notably NOT IE/Edge), you can use the new isPointInStroke method to test if a point is inside a single line. For cross-browser compatibility you would have to "fatten" a single line into a path so that you can hit-test a point within that fattened path.

closePath draws the endpoint of the last line to the beginning point of the first line, but it is not interupting the path, so that a later stroke() always takes effect on the lineTo and moveTo-methods before the closePath

IMHO, closePath is poorly named. It does not close (does not complete) a path. It is not the "closing brace" to beginPath's "opening brace". Instead, it simply draws a line directly from the current point in the path back to the first point in the path.

It seems to be impossible to draw a bigger line without calling beginPath() without stroking the rest of the pathes

You are only allowed one style to apply to one path. So if you define multiple lineWidths inside a set of path commands then the last lineWidth wins and the entire path will be stroked with that last linewidth.

moveTo(x,y) really jumps to another position, but the other position can be a path that returns true for the isPointInPath method, when it not only consists of one line (see 1.)).

moveTo is the equivalent of "picking up your pen" and moving it to a new location on the paper without ending the current set of path commands.

For example: Assume you begin a path with beginPath and then draw 3 separated triangles using moveTo to separate them. Then all 3 triangles will be included in the path and all 3 triangles will be tested with isPointInPath.

To visualize pathes the fill() method is useful

.fill and .stroke will command the context to visually draw the current path onto the canvas. They do not complete the path -- only the next beginPath will complete the current path. So if you define 2 sides of a triangle, stroke(), define the 3rd side of the triangle and stroke() again, then the first 2 sides will be stroked twice and the 3rd side will be stroked once.

Important information: You can define a path and test it with isPointInPath without actually stroking (or filling) the test path. This means you can individually redefine + isPointInPath each of your paths without having to redraw them on the canvas. You can also define a single shape that is made up of multiple paths and hit-test that multiple-path-shape using isPointInPath.

So to test which of your shapes (shapes==paths) inside the mousedown event, you can redefine each multi-path and test it with context.isPointInPath(mouseX,mouseY).

To actually draw your shape, you need multiple styles (thin vs thick lines) so you will have to draw the paths individually since you only get 1 styling per path.

Here's example code and a Demo:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
  var BB=canvas.getBoundingClientRect();
  offsetX=BB.left;
  offsetY=BB.top;        
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }

var isDown=false;
var startX,startY;

var shapes=[];
//
var path1=[
  {x:150,y:100},
  {x:50,y:100},
  {x:25,y:75},
  {x:50,y:50},
  {x:150,y:50}
];
path1.linewidth=1;
path1.strokestyle='red';
//
var path2=[
  {x:150,y:50},
  {x:225,y:75},
  {x:150,y:100}
];
path2.linewidth=5;
path2.strokestyle='blue';

var shape1=[path1,path2];
shape1.fill='green';
//
shapes.push(shape1);

// draw both parts of the path onto the canvas
draw(path1);
draw(path2);

$("#canvas").mousedown(function(e){handleMouseDown(e);});

function define(shape){
  ctx.beginPath();
  for(j=0;j<shape.length;j++){
    var p=shape[j];
    ctx.moveTo(p[0].x,p[0].y);
    for(var i=1;i<p.length;i++){
      ctx.lineTo(p[i].x,p[i].y);
    }
  }
}
//
function draw(path){
  ctx.beginPath();
  ctx.moveTo(path[0].x,path[0].y);
  for(var i=1;i<path.length;i++){
    ctx.lineTo(path[i].x,path[i].y);
  }
  ctx.lineWidth=path.linewidth;
  ctx.strokeStyle=path.strokestyle;
  ctx.stroke();
}
//
function dot(x,y,fill){
  ctx.beginPath();
  ctx.arc(x,y,2,0,Math.PI*2);
  ctx.closePath();
  ctx.fillStyle=fill;
  ctx.fill();
}

function handleMouseDown(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  // get mouseX,mouseY
  var mx=parseInt(e.clientX-offsetX);
  var my=parseInt(e.clientY-offsetY);
  //
  var dotcolor='red';
  for(var i=0;i<shapes.length;i++){
    define(shapes[i]);
    if(ctx.isPointInPath(mx,my)){dotcolor=shapes[i].fill;}
  }
  dot(mx,my,dotcolor);
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click inside and outside the multi-path shape<br>Inside clicks become green dots.</h4>
<canvas id="canvas" width=300 height=300></canvas>

Upvotes: 2

Bj&#246;rn Karpenstein
Bj&#246;rn Karpenstein

Reputation: 221

The following code is doing what i expect. I am using only fill() and never use beginPath()

    context.save();
context.lineWidth=1;
var TAB_ABSTAND=10;
var TAB_SAITENZAHL=6;
var TAB_SEITENDICKE=10;

for(var i=0;i<TAB_SAITENZAHL;i++)
{
    context.moveTo(this.clickedX+5, this.clickedY+(i*TAB_ABSTAND));
    context.lineTo(this.clickedX+this.width, this.clickedY+(i*TAB_ABSTAND));
    context.lineTo(this.clickedX+this.width, this.clickedY+(i*TAB_ABSTAND)+1);
    context.lineTo(this.clickedX+5, this.clickedY+(i*TAB_ABSTAND)+1);   
}

context.moveTo(this.clickedX, this.clickedY);
context.lineTo(this.clickedX, this.clickedY+TAB_ABSTAND*(TAB_SAITENZAHL-1)+1);
context.lineTo(this.clickedX+5, this.clickedY+TAB_ABSTAND*(TAB_SAITENZAHL-1)+1);
context.lineTo(this.clickedX+5, this.clickedY);

context.moveTo(this.clickedX+this.width, this.clickedY);
context.lineTo(this.clickedX+this.width, this.clickedY+TAB_ABSTAND*(TAB_SAITENZAHL-1)+1);
context.lineTo(this.clickedX+this.width-5, this.clickedY+TAB_ABSTAND*(TAB_SAITENZAHL-1)+1);
context.lineTo(this.clickedX+this.width-5, this.clickedY);
context.fill();    

context.restore(); 

Upvotes: 0

Related Questions