Figure out if point is inside of shape - javascript

I'm hacking on a vector car game in javascript and html canvas.
The point is that you click on of the blue squares and the car goes there - simple!
The outer and inner borders are two arrays of xy points that i've drawn out on the canvas
I've come to the point where i need to figure out if the car is on the course or not.
I've tried a bunch of different things, but just cant get this to work. Its the diagonal lines that makes my head hurt.
Can someone point me in the right direction how i would go about doing this?
You don't need to post any code, just some guidelines on which approach to take and how to calculate.

You can use a Path2D object combined with even-odd fill-rule and isPointInPath(). This allow you to define the test-path once and not have concerns about what you draw on the main context. If your path changes simply redefine the path object accordingly.
First define the two paths on the path object (not context). Separate them using moveTo() for the second path. You may want to use closePath() as well if you plan to stroke (for testing this will be implicit).
Test using the path and the even-odd fill rule:
if (ctx.isPointInPath(path, x, y, "evenodd")) { /* inside */ };
var ctx = c.getContext("2d");
var path = new Path2D();
path.arc(75, 75, 74, 0, 6.28); // replace with the polygons
path.closePath();
path.moveTo(75 + 40, 75);
path.arc(75, 75, 40, 0, 6.28);
path.closePath();
ctx.stroke(path);
ctx.globalCompositeOperation = "copy";
window.onmousemove = function(e) {
ctx.strokeStyle = ctx.isPointInPath(path, e.clientX, e.clientY, "evenodd") ? "red" : "#000";
ctx.stroke(path);
};
html, body {margin:0}
<canvas id=c></canvas>

You could use CanvasRenderingContext2D#isPointInPath() to check wether a Point is on the track or not.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
To do so you might need to refactor your Code in a manner so that the track is drawn as a separate path. Additionally, isPointInPath() must be called right after the path has been drawn and before any furthor path is drawn, because isPointInPath() is applied to the current path on the state-stack.

Related

Drawing Starbursts – basil.js

I am trying to draw a starburst by first drawing a polygon and then adjusting its properties.
on indesignjs.de it says a polygon is any shape that is not a rectangle, ellipse, or graphic line. When you add a polygon, InDesign creates a regular polygon based on the current polygon preferences settings. But I can't for the life of me figure out how to draw the damn thing.
I wrote this:
var myPolygon = polygon(0, 0, 10, 10);
property(myPolygon, "numberOfSides", 8);
property(myPolygon, "insetPercentage", 50);
but I get an error in indesign saying that polygon is not a function. Is is truncated like rectangle is (i.e. poly)?
As fabianmoronzirfas pointed out, polygon() is not a basil.js function (as opposed to rect()). So either you would have to draw the shape yourself using basil commands like this:
beginShape();
vertex(23, 45);
vertex(34, 67);
// draw as many vertices as you need
endShape(CLOSED);
or you would have to use proper (non-basil) InDesign scripting commands to shape your polygon. The thing you want to achieve can be done by using the convertShape() method, which can be used on any shape, so you could create a rect first and then use this method on the rect:
// #include ~/Documents/basiljs/basil.js;
function draw() {
var myPoly = rect(50, 50, 200, 200);
myPoly.convertShape(ConvertShapeOptions.CONVERT_TO_POLYGON, 8, 50);
}

Why,after creating several objects on javascript canvas animation, they are connected?

I have problem with javascript canvas.
I'm trying to put on canvas several object which are moving and bouncing on edges of canvas. If i create one object, there is no problem with it, but if i want to create more objects, they become connected.
Fiddle example:
https://jsfiddle.net/mwbgwa39/
I will be very thankfull if someone could help me :)
Important thing is to call beginPath() at the begining of each object drawing and closePath() when you are done drawing that object. If you don't call it canvas thinks that you are trying to continue drawing last object. You can read more on MDN
for(i in obj_t)
{
ctx.beginPath();
obj_t[i].xPos = obj_t[i].xPos + obj_t[i].xVel;
obj_t[i].yPos = obj_t[i].yPos + obj_t[i].yVel;
ctx.arc(obj_t[i].xPos, obj_t[i].yPos, obj_t[i].r, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
ctx.closePath(); // optional
}
EDIT: As Kaiido noticed calling closePath() in this example is not necessary. More info on when you should call closePath() can be found in this SO question.
You're drawing a path. Each time you draw something it continues the path you have made so far. You can move beginPath into the for loop to start a new path for each circle:
ctx.clearRect(0, 0, 400, 400);
for(i in obj_t)
{
ctx.beginPath();
// draw here
}
ctx.closePath();
See in action: https://jsfiddle.net/mwbgwa39/4/

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?
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.
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>
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();

HTML Canvas fill not working

Html:
HTML:
<canvas id="main" height="1000" width="1500"></canvas>
JS
var c = document.getElementById("main");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.moveTo(200,300);
ctx.lineTo(800,0);
ctx.moveTo(800,0);
ctx.lineTo(1500,300);
ctx.moveTo(1500,300);
ctx.lineTo(200,300);
ctx.closePath();
ctx.stroke();
ctx.fillStyle="#8ED6FF";
ctx.fill();
JSFiddle
Fiddle
Updated code
var c = document.getElementById("main");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.moveTo(200,300);
ctx.lineTo(800,0);
ctx.lineTo(1500,300);
ctx.lineTo(200,300);
ctx.closePath();
ctx.stroke();
ctx.fillStyle="#8ED6FF";
ctx.fill();
You dont need to move to same point again as you are already on that point .. so thats why it wasnt working
Sub-paths
This will create (clear) a new main path, so far so good:
ctx.beginPath();
The moveTo() call will create a new sub-path on the main path, here:
ctx.moveTo(200,300);
ctx.lineTo(800,0);
and here:
ctx.moveTo(800,0);
ctx.lineTo(1500,300);
and here:
ctx.moveTo(1500,300);
ctx.lineTo(200,300);
And finally, this will connect first point in path with the last point (in this case they are already overlapping):
ctx.closePath();
Since you now have three sub-paths which represents three unconnected lines, as they all have their own sub-paths, there is no way to fill them as a single shape. And they will not connect simply by having their points overlapping.
Create a continuous line with a single sub-path
You need to make a continuous line on the current sub-path. The lineTo() method will continue from the last point in the current path/sub-path to the coordinate it specifies, so to make a single shape using a single sub-path, you just add a new point to the sub-path by doing:
ctx.beginPath(); // new main path
ctx.moveTo(200,300); // creates a new sub-path, start point (200, 300)
ctx.lineTo(800,0); // line from (200, 300) to (800, 0), current point now: (800,0)
ctx.lineTo(1500,300); // line from (800, 0) to (1500, 300)
//ctx.lineTo(200,300); // not needed because:
ctx.closePath(); // will connect (1500,300) to (200,300) on the current sub-path
Using fill() will also close the path, internally and non-permanent, as it's impossible to fill an open path assuming it has > 2 points (it saves you a line of code, but not very important, just note that stoke() does not close the path for you internally).
Also, a tip not many are aware of: if you intended to draw more closed shapes in the same fillStyle, you could continue with moveTo() after the closePath() to create a new sub-path without the need of using fill()/beginPath() first..

KineticJS click detection inside animated shapes

OK, I admit I tried to be clever: I thought if I overrode Shape's drawFunc property I could simply draw whatever inside a rectangle and still use KineticJS's click detection. Here's my attempt:
var shape = new Kinetic.Shape({
drawFunc: function(context) {
var id = 26; // Id of a region inside composite image.
context.beginPath();
context.rect(0, 0, w, h);
context.closePath();
this.fill(context);
this.stroke(context);
context.drawImage(copyCanvas, (id % 8) * w, flr(id / 8) * h,
w, h, 0, 0, w / 2, h / 2);
},
draggable: true
});
So, the idea was to draw a rectangle, and use drawImage() to draw something on top of the rectangle (like a texture, except it changes from time to time because copyCanvas itself changes). All the meanwhile, I expected event handling (drag-n-drop, in particular) to still 'just work'. Well, here's what happens: the part of the rectangle not covered by my drawImage() correctly detects clicks. However, the one fourth of the rectangle that is covered by the image refuses to respond to clicks! Now, my question is why? I dug into the KineticJS code, and looked to me that click detection simply means drawing to a buffer and seeing if a given x, y point has non-zero alpha. I can't see how this could be affected by my drawing an image on top of my rectangle.
Any ideas what's going on?
OK, so I went ahead and looked at the source code. Here's the definitive answer:
KineticJS assigns a random and unique RGB color to each shape that's created using a global map from RGB colors to shape objects. The draw() function of the shape is called twice: once with the 'real' canvas, and once with a 'buffer' canvas used for hit detection. When using the 'buffer' canvas, KineticJS switches the stroke and fill colors to the unique RGB color of the given shape. The same 'buffer' canvas is used for all shapes on a layer. Thus hit detection simply becomes reading the RGB value of a given point and looking up the corresponding shape in the global map. Now, in my example I drew an image in a way that circumvented KineticJS's juggling of colors used for hit detection. Thus, when I clicked on the image area, KineticJS saw some unknown RGB color on the buffer canvas with no known shape assigned to it.
The solution is not to draw the image for the 'buffer' (or 'hit detection') phase: a simple rectangle will do. In case you're wondering, here's the correct code for the drawFunc:
var width = 200;
var height = 100;
var myShape = new Kinetic.Shape({
drawFunc: function(context) {
if (layer.bufferCanvas.context == context) {
context.beginPath();
context.rect(0, 0, width, height);
context.closePath();
this.fill(context);
this.stroke(context);
} else {
context.drawImage(someCanvasWithAnythingOnIt, 0, 0, width, height,
0, 0, width, height);
}
}});
Can I collect my own reward?
I think your problem lies in the order. There is a depth associated with each object that you draw and the default ordering is like a stack, last drawn is on top.
Now that you have modified the code, making 2 draws inside the shape draw function, I still think the ordering is preserved and hence, the object is not able to detect the input. Try changing the order, i.e. draw image first and then the rectangle and see if the problem is solved.
Else, share a jsFiddle for an example.

Categories