So I've gotten pretty excited about the introduction of Canvas Paths as standard objects in contemporary browsers, and have been trying to see how much mileage I can get out of this new-ish feature. However, my understanding of how these objects interact with the isPointInPath() method (and possibly other path-based methods) is apparently somewhat flawed.
As demonstrated in the first two test functions below, I can get the drawn paths to be recognized by the isPointInPath() method. However, when I define the paths as an object, the method ceases to work (even though the path objects can be recognized for other purposes such as filling).
function startGame(){ //Initiating Environment Variables
gamemap = document.getElementById("GameMap")
ctx = gamemap.getContext("2d")
testCircleBounds()
testVarCircleBounds()
testObjCircleBounds()
testMultiObjCircleBounds()
}
function testCircleBounds() { //Minimalist Test of Path Methods
ctx.beginPath()
ctx.arc(250,250,25,0,2*Math.PI)
console.log(ctx.isPointInPath(250,250)) //point in path detected
ctx.closePath()
console.log(ctx.isPointInPath(250,250)) //point in path still detected
ctx.stroke()
ctx.fillStyle = "yellow"
ctx.fill() //fills great
}
function testVarCircleBounds() { //Test of Path Methods with Variables
x_cen = 250; y_cen = 250; rad = 15
ctx.beginPath()
ctx.arc(x_cen,y_cen,rad,0,2*Math.PI)
ctx.closePath()
console.log(ctx.isPointInPath(x_cen,y_cen)) //true yet again
ctx.stroke()
ctx.fillStyle = "orange"
ctx.fill() //also fills great
}
function testObjCircleBounds() { //Test of Path Methods with Single Stored Path Object
x_cen = 250; y_cen = 250; rad = 10
ctx.beginPath()
lonely_node = new Path2D()
lonely_node.arc(x_cen,y_cen,10,0,2*Math.PI)
ctx.closePath()
console.log(ctx.isPointInPath(x_cen,y_cen)) //point in path not found!
ctx.stroke(lonely_node)
ctx.fillStyle = "red"
ctx.fill(lonely_node) //but ctx.fill notices the path just fine
}
function testMultiObjCircleBounds(){ //Test of Paths Methods with Multi-Object Referencing
nodes = [] //initializes set of nodes as array
for (i=0; i<25; i++) { //generates 25 nodes
nodes[i] = new Path2D() //defines each node as Path object in the array
node = nodes[i]
//Places Nodes along the 'horizon' of the map
x_cen = 20*i + 10
y_cen = 100
ctx.beginPath(node) //"node" argument probably not helping?
node.arc(x_cen,y_cen,8,0,2*Math.PI)
console.log(ctx.isPointInPath(x_cen,y_cen)) //still returns false!
ctx.closePath(node)
ctx.stroke(node)
console.log(ctx.isPointInPath(x_cen,y_cen)) //arrgh!!
}
// Fill can also be selectively applied to referenced path objects
for (i=0; i<25; i=i+2) {
ctx.fill(nodes[i])
}
}
<!DOCTYPE html>
<html>
<head>
<title>Wrap Around Beta</title>
<script src="Circuity_PathObjectTest.js"></script>
</head>
<body onload='startGame()'>
<canvas id="GameMap" width="500" height="500" style="border:1px solid #000000"></canvas>
</body>
</html>
Is this fundamentally the wrong way to think about Path2D objects and to record 'hit' areas on a canvas? If so, is there another technique (saving the canvas context for each path drawn or something along that vein) that would produce the desired effect?
You must send a reference to the Path2D being tested into isPointInPath:
ctx.isPointInPath( lonely_node, x_cen, y_cen )
Related
I'm looking for a way to check if two path2D are intersecting but can't find a way...
Exemple :
// My circle
let circlePath = new Path2D();
circlePath.ellipse(x, y, radiusX, radiusY, 0, 0, Math.PI*2, false);
// My rectangle
let rectPath = new Path2D();
rectPath.rect(x, y, width, height);
// Intersect boolean
let intersect = circlePath.intersect(rectPath); // Does not exists
Is there a function to do that ?
I found isPointInPath(path2D, x, y) (that I use with my paths to check intersect with mouse) but can't use it between two paths.
Or maybe a way to get an array of all points in a Path2D to use isPointInPath with all points ?
Edit:
FYI, I want this for a game development, I want to be able to check collision between my entities (an entity is defined by some data and a Path2D). My entities can be squares, circles or more complex shapes.
There currently is nothing in the API to do this.
The Path2D interface is still just an opaque object, from which we can't even extract the path-data. I actually did start working on a future proposal to expose this path-data and add a few more methods to the Path2D object, like a getPointAtLength(), or an export to SVG path commands, which would help doing what you want, but I wouldn't hold my breath until it's part of the API, and I must admit I didn't even consider including such a path-intersect method...
However as part of this effort I did build a prototype of that API that currently exposes only a few of the expected methods: https://github.com/Kaiido/path2D-inspection/
Among these methods there is a toSVGString() that can be used with this project (that I didn't extensively test).
So we could build a Path2D#intersects() by merging both libraries. However note that at least mine (path2d-inspection) hasn't been extensively tested yet and I don't recommend using it in production just yet.
Anyway, here is a very quickly made hack as a proof-of-concept:
const circlePath = new Path2D();
circlePath.ellipse(rand(100, 20), rand(100, 20), rand(80, 10), rand(80, 10), rand(Math.PI*2), 0, Math.PI*2, false);
// My rectangle
const rectPath = new Path2D();
rectPath.rect(rand(150), rand(100), rand(200), rand(200));
// Intersect boolean
const intersect = rectPath.intersects(circlePath);
console.log("intersect:", intersect);
// List of intersection points
const intersections = rectPath.getIntersections(circlePath);
console.log(intersections);
// Render on a canvas to verify
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.strokeStyle = "green";
ctx.stroke(circlePath);
ctx.strokeStyle = "red";
ctx.stroke(rectPath);
function rand(max=1, min=0) {
return Math.random() * (max - min) + min;
}
.as-console-wrapper { max-height: 50px !important }
<script src="https://cdn.jsdelivr.net/gh/Kaiido/path2D-inspection#master/build/path2D-inspection.min.js"></script>
<script>
// a really quick hack to grasp 'intersect' from their esm
globalThis.module = {};
</script>
<script src="https://cdn.jsdelivr.net/gh/bpmn-io/path-intersection#master/intersect.js"></script>
<script>
// part 2 of the hack to grasp 'intersect', let's make it a Path2D method :P
{
const intersect = module.exports;
delete globalThis.module;
Path2D.prototype.getIntersections = function(path2) {
return intersect(this.toSVGString(), path2.toSVGString());
};
Path2D.prototype.intersects = function(path2) {
return this.getIntersections(path2).length > 0;
};
}
</script>
<canvas></canvas>
I am trying to animate a line two lines along a path, one then the other. Basically it will look like one line being drawn, stopping at a point, then another line being drawn somewhere else. So far I have come across promises and callbacks to achieve this, but being a javascript newbie this is confusing
Current animate function:
/*
* Animation function draws a line between every point
*/
var animate = function(p){
return new Promise(function(resolve) {
t = 1;
var runAnimation = function(){
if(t<p.length-1){
context.beginPath();
context.moveTo(p[t-1].x,p[t-1].y);
context.lineTo(p[t].x,p[t].y);
context.stroke();
t++;
requestAnimationFrame(function(){runAnimation()});
} else {
resolve()
}
};
runAnimation();
});
}
Current call to animate function:
animate(points).then(animate(secondary_points));
The points are similar to:
var points = [{x:100, y:200}];
And the paths the lines need to follow are just the multiple coordinates inside points and secondary_points
Ive tried many solutions on SO that were similar, but small differences cause me to either mess up or not understand the solution. The biggest issue I seem to have is calling the SAME animate function, with that animate function needing to be run on different parameters.
Without this solution, using
animate(points);
animate(secondary_points);
the lines are drawn somewhat at the same time, but the result is actually just randomly placed dots along the path instead of smooth lines, I assume because both are running at the same time.
How would I go about fixing this so that one line is drawn along path1 and then the second line is drawn along path2?
It is probably a simple solution, but Ive worked with JS for 3 days and my head is still spinning from getting used to some of the syntax of the old code Ive had to fix
Thank you
EDIT:
The full flow of the animation is as follows:
I have a php file that contains 2 canvases, each containing an image of a map. The php file has a couple <script/> tags, one of which calls the js script I am writing the animation on via drawPath(source,destination,true) or drawPath(source,destination,false)
The drawPath function uses the boolean to determine which canvas to get the context for, and then draw on the path from point A to point B via finding the path and creating the points mentioned above, then drawing using animate(). There are a couple breaks in the maps that require separate lines, which prompted my original question. I was able to fix that thanks to suggestions, but now I am having a larger issue.
If I need to go from point A on map A to point B on map B, ie
drawPath(source, end_point_of_map_A, true); is called then
drawPath(start_point_of_map_B, destination, false);, the lines are drawn only on one map, and they are similar to before where they are 1. random and 2. incomplete/only dots
I am assuming this is due to the animation again, because it worked when just drawing the lines statically, and each animation works when going from point A to B on a single map
Any help is appreciated!
Edit:
DrawPath()
function drawPath(source, desti, flag) {
/*
* Define context
*/
//lower
if(!flag){
var c = document.getElementById("myCanvas");
context = c.getContext("2d");
//upper
} else {
var cUpr = document.getElementById("myCanvasUpr");
context = cUpr.getContext("2d");
}
/*
* Clear the variables
*/
points = [];
secondary_points = [];
vertices = [];
secondary_vertices = [];
t = 1;
done = false;
//check for invalid locations
if (source != "" && desti != "") {
context.lineCap = 'round';
context.beginPath();
/*
* Get the coordinates from source and destination strings
*/
var src = dict[source];
var dst = dict[desti];
/*
* Get the point number of the point on the path that the source and destination connect to
*/
var begin = point_num[source];
var finish = point_num[desti];
/*
* Draw the green and red starting/ending circles (green is start, red is end)
*/
context.beginPath();
context.arc(src[0], src[1], 8, 0, 2 * Math.PI);
context.fillStyle = 'green';
context.fill();
context.beginPath();
context.arc(dst[0], dst[1], 6, 0, 2 * Math.PI);
context.fillStyle = 'red';
context.fill();
/*
* Call the function that draws the entire path
*/
draw_segments(begin, finish, src, dst, flag);
//window.alert(JSON.stringify(vertices, null, 4))
/*
* Edit what the line looks like
*/
context.lineWidth = 5;
context.strokeStyle = "#ff0000";
context.stroke();
}
}
A nice way to handle this is to put your lines into a an array where each element is a set of points of the line. Then you can call reduce() on that triggering each promise in turn. reduce() takes a little getting used to if you're new to javascript, but it basically takes each element of the array c in this case, does something and that something becomes the next a. You start the whole thing off with a resolve promise which will be the initial a. The promise chain will be returned by reduce to you can tack on a final then to know when the whole thing is finished.
For example:
let canvas = document.getElementById('canvas')
let context = canvas.getContext('2d');
var animate = function(p){
return new Promise(function(resolve) {
t = 1;
var runAnimation = function(){
if(t<p.length-1){
context.beginPath();
context.moveTo(p[t-1].x,p[t-1].y);
context.lineTo(p[t].x,p[t].y);
context.stroke();
t++;
requestAnimationFrame(function(){runAnimation()});
} else {
resolve()
}
};
runAnimation();
});
}
// make some points:
let points = Array.from({length: 200}, (_,i) => ({x:i+1, y:i+2}))
let points2 = Array.from({length: 200}, (_,i) => ({x:300-i, y:i+2}))
let points3 = Array.from({length: 200}, (_,i) => ({x:i*2, y:100+100*Math.sin(i/10)}))
// create an array holding each set
let sets = [points, points2, points3]
// use reduce to call each in sequence returning the promise each time
sets.reduce((a, c) => a.then(() => animate(c)), Promise.resolve())
.then(() => console.log("done"))
<canvas id="canvas" height="300" width="500"></canvas>
There are plenty of examples on how to draw lines on canvas, in js.
But for only educational purposes i want to draw line using algorithm. basically method gets two Vector2 points, from them it finds middle point, then it continues like that recursively until minimum distance of 2 pixels is reached.
I have DrawPoint method to basically draw 1 point on canvas, and DrawLine method that does all the job.
For now I have 2 problems:
1: points are not colored red, as they should be.
2:
It doesnt look like a line.
For Vector2 i used "Victor.js" plugin, and it seems to be working well.
this is code i have:
JS:
var point2 = new Victor(100, 100);
var point3 = new Victor(150, 150);
DrawLine(point2, point3);
function DrawLine(vec0, vec1)
{
var point0 = new Victor(vec0.x, vec0.y);
var point1 = new Victor(vec1.x, vec1.y);
var dist = point1.distance(point0);
if (dist < 2)
return;
//this is how it should look like in c# var middlePoint = point0 + (point1 - point0)/2; But looks like i cant just divide by 2 using victor js because i can only divide vector by vector.
var middlePoint = point0.add(point1.subtract(point0).divide(new Victor(2,2)));
DrawPoint(middlePoint);
DrawLine(point0, middlePoint);
DrawLine(middlePoint, point1);
}
function DrawPoint(point){
var c = document.getElementById("screen");
var ctx = c.getContext("2d");
ctx.fillStyle = "FF0000";
ctx.fillRect(point.x, point.y, 3,1);
}
I really appreciate any help you can provide.
The victor.js documentation shows that most functions of Victors do not return new Victors, but operate on the current instance. In a way, v1.add(v2) is semantically more like v1 += v2 and not v1 + v2.
The problem is with calculating the midpoint. You could use the mix() method, which blends two vectors with a weight. You must clone() the Victor first, otherwise point0will be midofied:
var middlePoint = point0.clone().mix(point1, 0.5);
If you don't change the original Vectors, you don't need to create new instances of Victors from the arguments, you can use the arguments directly:
function DrawLine(point0, point1)
{
var dist = point1.distance(point0);
if (dist < 2) return;
var middlePoint = point0.clone().mix(point1, 0.5);
DrawPoint(middlePoint);
DrawLine(point0, middlePoint);
DrawLine(middlePoint, point1);
}
Finally, as Sven the Surfer has already said in a comment, "FF0000" isn't a valid colour. Use "#FF0000", note the hash mark, or one of the named web colours such as "crimson".
Given: an html canvas context:
Example code: http://jsfiddle.net/m1erickson/wJ67M/
This code creates a CanvasGradient object in the gradient variable.
var gradient=context.createLinearGradient(0,0,300,150);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'white');
This code creates a CanvasGradient object in the gradient variable.
var gradient=context.createRadialGradient(150,150,30, 150,150,100);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'white');
This code creates a CanvasPattern object in the pattern variable.
var pattern=context.createPattern(myImage,'repeat');
Is there a programatic way of extracting the properties from these objects after they are created (not by referring back to the JS code that created them)?
Wanted:
The linear gradients line segment (0,0,300,150) and its colorstops (0,red,1,white).
The radial gradients circles (150,150,30, 150,150,100) and its colorstops (0,red,1,white).
The patterns image and repeat properties.
Thanks for any thoughts!
The canvas specs does not grant access to the inner gradient or pattern properties, just like, as you also know, one cannot get access to the transform matrix.
So the only solution is to inject CanvasRenderingContext2D, CanvasGradient and CanvasPattern prototypes to store, in the created objects, the values used to create them.
So for the Canvas, you can write something like :
// save native linear gradient function
var nativeCreateLinearGradient = CanvasRenderingContext2D.prototype.createLinearGradient ;
// redefine createLinearGradient with a function that stores creation data
// new properties : gradientType, x0, y0, x1, y1, colorStops
CanvasRenderingContext2D.prototype.createLinearGradient = function(x0, y0, x1, y1) {
// actually create the gradient
var newGradient = nativeCreateLinearGradient.apply(this,arguments);
// store creation data
newGradient.gradientType = 0 ; // 0 for linear, 1 for radial
newGradient.x0 = x0; newGradient.y0 = y0;
newGradient.x1 = x1; newGradient.y1 = y1;
newGradient.colorStops = [];
return newGradient;
};
And for the Gradient :
var dummyContext = document.createElement('canvas').getContext('2d');
var nativeAddColorStop = CanvasGradient.prototype.addColorStop;
CanvasGradient.prototype.addColorStop = function (offset, color) {
// evaluate offset (to avoid reference issues)
offset = +offset;
// evaluate color (to avoid reference issues)
dummyContext.fillStyle = color;
color = dummyContext.fillStyle ;
// store color stop
this.colorStops.push([offset, color]);
// build the real gradient
nativeAddColorStop.call(this, offset, color);
return this;
};
You can do this in a very similar way for the radial gradient, and for the pattern you might want to copy the image, which type is CanvasImageSource )
Is it possible to set the z-index of a drawn object in HTML5 canvas?
I am trying to get it so one object can be infront of a the "player" and another object is behind the "player"
Yes..kind of yes. You can use globalCompositeOperation to "draw behind" existing pixels.
ctx.globalCompositeOperation='destination-over';
Here's an example and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cx=100;
drawCircle()
cx+=20;
ctx.globalCompositeOperation='destination-over';
$("#test").click(function(){
drawCircle();
cx+=20;
});
function drawCircle(){
ctx.beginPath();
ctx.arc(cx,150,20,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle=randomColor();
ctx.fill();
}
function randomColor(){
return('#'+Math.floor(Math.random()*16777215).toString(16));
}
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>
<button id="test">Draw new circle behind.</button><br>
<canvas id="canvas" width=300 height=300></canvas>
A solution that I've found works for me (and gets rid of flickering, hurrah!) is to use two canvases. (or more)
I will assume you are making a game of some kind (since you mention a player) and use that as an example.
You can take a page from the way windows works and put a canvas first as a background with another canvas over it as your player canvas. You can mark the player canvas as 'dirty' or changed whenever it has been altered and only redraw the changed layer when needed. This means that you only update the second canvas when your player moves or takes an action.
This method can be taken even farther and you can add a third canvas for a HUD with gameplay stats on it that is only changed when the player's stats change.
The html might look something like this:
<canvas id="background-canvas" height="500" width="1000" style="border:1px solid #000000;"></canvas>
<canvas id="player-canvas" height="500" width="1000" style="border:1px solid #000000;"></canvas>
<canvas id="hud-canvas" height="500" width="1000" style="border:1px solid #000000;"></canvas>
Just draw the things behind it first, then the thing, then the other objects.
To do hit testing you may need to iterate backwards over your display list, testing each object. This will work if you know the object boundaries really well.
Or you may want to try some standard graphics tricks like drawing the same objects to another in-memory canvas with unique colours for every object drawn: to hit test this just check the pixel colour on the in-memory canvas. Neat ;-)
Or you could simply use an array containing your objects to be drawn then sort this array using the zIndex property of each child. Then you just iterate over that array and draw childrens.
var canvas = document.querySelector('#game-canvas');
var ctx = canvas.getContext('2d');
// Define our shape "class".
var Shape = function (x, y, z, width, height) {
this.x = x;
this.y = y;
this.zIndex = z;
this.width = width;
this.height = height;
};
// Define the shape draw function.
Shape.prototype.draw = function () {
ctx.fillStyle = 'lime';
ctx.fillRect(this.x, this.y, this.width, this.height);
};
// let's store the objects to be drawn.
var objects = [
new Shape(10, 10, 0, 30, 30), // should be drawn first.
new Shape(50, 10, 2, 30, 30), // should be drawn third.
new Shape(90, 10, 1, 30, 30) // should be drawn second.
];
// For performance reasons, we will first map to a temp array, sort and map the temp array to the objects array.
var map = objects.map(function (el, index) {
return { index : index, value : el.zIndex };
});
// Now we need to sort the array by z index.
map.sort(function (a, b) {
return a.value - b.value;
});
// We finaly rebuilt our sorted objects array.
var objectsSorted = map.map(function (el) {
return objects[el.index];
});
// Now that objects are sorted, we can iterate to draw them.
for (var i = 0; i < objectsSorted.length; i++) {
objectsSorted[i].draw();
}
// Enjoy !
Note that I didn't tested that code and wrote it on my cellphone, so there might be typos, but that should permit to understand the principle, i hope.
Sorry, but nope, the canvas element will have its z-index and anything drawn on it will be on that layer.
If you are referring to different things on the canvas then yes, anything that is drawn is drawn on top of whatever was there before.