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.
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>
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 )
Canvas {
id: canvas
onPaint: {
if (personalInfo.count === 0) {
return
}
var ctx = canvas.getContext("2d");
ctx.globalCompositeOperation = "source-over";
var points = []
for (var i = 0; i < personalInfoModel.dataCount(); i++) {
var temp = personalInfoModel.get(i)
points.push({
date: temp.date,
heartRate: temp.heartRate,
temprature: temp.temprature,
pressure: temp.bloodPressure
}
)
}
drawAxis(ctx)
drawGridLineAndUnitNum(ctx, chart.activeChart, points, "x", 15);
}
}
I have two button. If button A is clicked, then set chart.activeChart to 7 and call cavas.requestPaint() on A::onClicked, on cavas.drawGridLineAndUnitNum draw seven vertical line. If button B is clicked besides set chart.activeChart to 30, all same to A::onClicked. I hope that when A is clicked, canvas wipe the drawn line which product by B is clicked and vice versa. But in fact, it always reserve the line draw by last time.
A Context2D, associated to a specific Canvas, provides two useful functions:
fillRect
clearRect
In most cases, it could be possible to "clear" a Canvassimply by filling it with the background color, i.e. by using fillRect. That's the approach of the StocQt example, which has a white background.
However, if the background is transparent, filling it does not remove other strokes and thus does not make much sense. In this case, the only possible way to clear the Canvas is by removing all the strokes, i.e. by using clearRect.
I use a transparent background and thus clearRect is the way to go for me.
I see this question and I dont know how I can set id for each circles and access them from javascript codes and css codes? (e.g. click)
You can solve this by defining click objects when drawing the circles. Inside the loop drawing the circles (ref. the fiddle made by #MonicaOlejniczak):
...
// push circle info as objects:
circles.push({
id: i + "," + j, // some ID
x: x,
y: y,
radius: radius
});
...
Then:
add a click handler to canvas
correct mouse position
loop through the objects finding if (x,y) is inside the circle:
Function example:
canvas.onclick = function(e) {
// correct mouse coordinates:
var rect = canvas.getBoundingClientRect(), // make x/y relative to canvas
x = e.clientX - rect.left,
y = e.clientY - rect.top,
i = 0, circle;
// check which circle:
while(circle = circles[i++]) {
context.beginPath(); // we build a path to check with, but not to draw
context.arc(circle.x, circle.y, circle.radius, 0, 2*Math.PI);
if (context.isPointInPath(x, y)) {
alert("Clicked circle: " + circle.id);
break;
}
}
};
You can optionally use math instead of the isPointInPath(), but the latter is simpler and is fast enough for this purpose.
Modified version of the same fiddle
You can't set an ID on something that has been drawn to a canvas.
The element on its own is just a bitmap and does not provide information about any drawn objects.
If you need to interact with the items inside the canvas you need to manually keep a reference to where everything is drawn, or use a system like "object picking" or using the built in hit regions.
Okay so i'm embarrassingly unknowledgeable of javascript. I know my ruby and rails pretty solid but have never used js as extensively as i am in my current project.
i have a map that's been drawn on a canvas. on that map, i want drawn multiple position markers (via the provided function). i've given it a list (via rails) of locations to mark. for some reason, it's only drawing the last coordinate.
the javascript is improvised from another source, not mine. this is just the problematic portion:
function plotPosition(long,lat) {
// Grab a handle to the canvas
var canvas = document.getElementById('map'),
ctx;
// Canvas supported?
if (canvas.getContext) {
// Grab the context
ctx = canvas.getContext('2d');
ctx.beginPath();
// Draw a arc that represent the geo-location of the request
ctx.arc(
degreesOfLongitudeToScreenX(long),
degreesOfLatitudeToScreenY(lat),
5,
0,
2 * Math.PI,
false
);
// Point style
ctx.fillStyle = 'rgb(0,0,0)';
ctx.fill();
ctx.stroke();
}
}
function draw() {
// Main entry point got the map canvas example
var canvas = document.getElementById('map'),
ctx;
// Canvas supported?
if (canvas.getContext) {
ctx = canvas.getContext('2d');
// Draw the background
drawBackground(ctx);
// Draw the map background
drawMapBackground(ctx);
// Draw the map background
// drawGraticule(ctx);
// Draw the land
drawLandMass(ctx);
<% #events.each do |e| %>
plotPosition('<%= e.longitude %>','<%= e.latitude %>');
<% end %>
} else {
alert("Canvas not supported!");
}
}
so my thinking is that the plotPosition function is drawing each coordinate, but every time it's given a new coordinate, the old one is erased/moved and replaced with the new one. probably a simple fix here, but i've been at this for hours and to no avail. banging my head.
any ideas as to the problem?
From the code example it seems as you are clearing the background too - you're not showing the calls to these methods but I will assume you do this before drawing the new point(s) (?) somewhere else in the code.
If so -
You need to redraw all the points you have drawn before you clear the canvas.
One way to do this is to store the points you already have in an array and then call a redraw function to draw everything in there. Or store the points from server directly to the array with converted values and when all points has been converted redraw everything.
Simplified example based on latter approach (which will require some re-factoring):
/// GLOBALS (put ctx here instead as well..)
var points = [],
canvas = document.getElementById('map'),
ctx = canvas.getContext ? canvas.getContext('2d') : null;
if (ctx === null) {...sorry...};
...
/// somewhere comes the logic to get the points themselves.
...
<% #events.each do |e| %>
storePosition('<%= e.longitude %>','<%= e.latitude %>');
<% end %>
...
/// after new points are added, render them all
renderAll();
function storePosition(long, lat) {
points.push{
x: degreesOfLongitudeToScreenX(long)
y: degreesOfLatitudeToScreenY(lat),
long: long,
lat: lat
}
}
function renderAll();
/// clear and redraw background of canvas
drawBackground(ctx);
// Draw the map background
drawMapBackground(ctx);
// Draw the map background
// drawGraticule(ctx);
// Draw the land
drawLandMass(ctx);
/// now plot all the stored points
ctx.fillStyle = 'rgb(0,0,0)';
for(var i = 0, point; point = points[i]; i++) {
ctx.beginPath();
ctx.arc(
point.x,
point.y,
5,
0,
2 * Math.PI
);
ctx.closePath();
ctx.fill();
}
}
You can implement a check of x and y to see that they are inside canvas but this is strictly not necessary as canvas will clip them for you and the internal clipping do faster checking than JavaScript will. But for debugging purposes you could always do a check if you suspect this. But you write that the point is plotted but erased when a new point is drawn so I don't think this is the problem here.