I have asked a question before: How can I control z-index of canvas objects? and we reached to a solution that may not be a good one for complicated situations.
I found that canvas doesn't have a z-index system, but a simple ordered drawing one. Now there is a new question: how can I simulate z-index system to make this problem fixed in complicated situations?
The good answer can solve a big problem.
It's not that canvas doesn't have a z-index, it's that canvas doesn't keep objects drawn contrary to the HTML page. It just draws on the pixel matrix.
There are basically two types of drawing models :
object ones (usually vector) : objects are kept and managed by the engine. They can usually be removed or changed. They have a z-index
bitmap ones : there are no objects. You just change a pixel matrix
The Canvas model is a bitmap one. To have objects drawn over other ones, you must draw them after. This means you must manage what you draw.
The canvas model is very fast, but if you want a drawing system managing your objects, maybe you need SVG instead.
If you want to use a canvas, then the best is to keep what you draw as objects.
Here's an example I just made : I keep a square list and every second I randomize their zindex and redraw them :
var c = document.getElementById('c').getContext('2d');
function Square(x, y, s, color) {
this.x = x; this.y = y; this.s = s; this.color = color;
this.zindex=0;
}
Square.prototype.draw = function(c) {
c.fillStyle = this.color;
c.fillRect(this.x, this.y, this.s, this.s);
}
var squares = [
new Square(10, 10, 50, 'blue'), new Square(40, 10, 40, 'red'), new Square(30, 50, 30, 'green'),
new Square(60, 30, 40, '#111'), new Square(0, 30, 20, '#444'), new Square(70, 00, 40, '#999')
];
function draw() {
c.fillStyle = "white";
c.fillRect(0, 0, 1000, 500);
for (var i=0; i<squares.length; i++) squares[i].draw(c);
}
setInterval(function(){
// give all squares a random z-index
squares.forEach(function(v){v.zindex=Math.random()});
// sort the list accordingly to zindex
squares.sort(function(a,b){return a.zindex-b.zindex});
draw();
}, 1000);
Demonstration
The idea is that the square array is sorted accordingly to zindex. This could be easily extended to other types of objects.
As dystroy has said, z-index is, at its simplest, just an index to tell you in what order to draw things on the canvas, so that they overlap properly.
If you mean to do more than this, say to replicate the existing workings of a browser, then you would have more work to do. The order in which objects are drawn in a browser is a complicated calculation that is driven by:
The DOM tree
Elements' position attributes
Elements' z-index attributes
The canonical source to this is the Elaborate description of Stacking Contexts, part of the CSS specification.
Related
I have many sprites. Think thousands of sprites that have different textures.
https://threejs.org/examples/webgl_sprites.html
https://threejs.org/docs/#api/en/materials/SpriteMaterial
Now my scene animates, I want to change the text content of these sprites. I do so by setting on each frame:
var animate = function() {
requestAnimationFrame(animate);
for (var sprite in sprites) {
newText = randomText(); // think some uniquely random text or number on each frame
sprite.material.map = changeText(newText);
}
renderer.render(scene, camera);
}
and changeText looks something like this:
function changeText(newText) {
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
canvas.width = 40; canvas.height = 40;
// ... set color etc
ctx.fillRect(0, 0, 40, 40);
// ... set another color then
context.fillText(newText, 0, 0);
var amap = new THREE.Texture(canvas);
amap.needsUpdate = true;
return amap;
}
Now this works but it absolutely crawls if I do this on each frame as opposed to doing it once. Maybe createElement is expensive. When I try to set the canvas as a global variable for some reason it makes the sprites all have the same texture even though I'm redrawing the canvas for each symbol. Regardless, it's still very slow so I didn't investigate further.
I also noticed that if I pregenerate all the texts before hand and then just use a hashmap to assign them then it works fast and good. So it's just the texture generation (in changeText()) that is making this slow. But I need the texture to be completely dynamic so I can't generate all the texts beforehand. Any ideas?
I am wondering how I could alter my Javascript to only clear the falling sprites, and not the entire canvas (as it does currently).
I hope to place multiple other (animated) sprites on the canvas, which do not appear with the way my function animate is structured.
Is there a way so that if there was another image/sprite was on the canvas, it would not be affected by the function animate.
I'm thinking that this line needs to change:
ctx.clearRect(0, 0, canvas.width, canvas.height);
Though I have no idea what parameters I would need to place inside.
The falling sprites draw at a size of 60x60, but as they fall downwards this is where I am a bit stuck with clearing the only the sprite path.
Any help would be appreciated :)
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
canvas.width = 1408;
canvas.height = 640;
canvasWidth = canvas.width;
canvasHeight = canvas.height;
var orangeEnemy = new Image();
orangeEnemy.src = "http://www.catholicsun.org/wp-content/uploads/2016/09/cropped-sun-favicon-512x512-270x270.png";
var yellowEnemy = new Image();
yellowEnemy.src = "http://www.clker.com/cliparts/o/S/R/S/h/9/transparent-red-circle-hi.png";
var srcX;
var srcY;
var enemySpeed = 2.75;
var images = [orangeEnemy, yellowEnemy];
var spawnLineY=-50;
var spawnRate=2500;
var spawnRateOfDescent=1.50;
var lastSpawn=-1;
var objects=[];
var startTime=Date.now();
animate();
function spawnRandomObject() {
var object = {
x: Math.random() * (canvas.width - 15),
y: spawnLineY,
image: images[Math.floor(Math.random() * images.length)]
}
objects.push(object);
}
function animate(){
var time=Date.now();
if(time>(lastSpawn+spawnRate)){
lastSpawn=time;
spawnRandomObject();
}
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// move each object down the canvas
for(var i=0;i<objects.length;i++){
var object=objects[i];
object.y += enemySpeed;
ctx.drawImage(object.image, object.x, object.y, 60, 60);
}
}
<html>
<canvas id="canvas" style="border:3px solid"></canvas>
</html>
The easiest and quickest way would be to overlay another canvas, specifically for your sprites, atop your current canvas (requires a bit of CSS). Put all your sprites in one, everything else in the other. The clearRect() in your animate() function will then only apply to your sprite canvas, and not the other.
Otherwise, you will have to keep track of the positions of the sprites, and clear each programatically with 60x60 rectangles using clearRect(offsetX, offsetY, 60, 60).
P.S. excuse the non-formatted answer... still figuring SO out
Clear once for performance.
You are much better off clearing the whole canvas and redrawing the sprites. Using the previous position, checking for overlap and then clearing each sprite in turn, making sure you don't clear an existing sprite will take many more CPU cycles than clearing the screen once.
The clear screen function is very fast and is done in hardware, the following is the results of a performance test on Firefox (currently the quickest renderer) of clearing 65K pixels using just one call for whole area then 4 calls each a quarter, then 16 calls each clearing a 16th. (µs is 1/1,000,000th second)
Each test clears 256*256 pixels Each sample is 100 tests
'Clear 1/1' Mean time: 213µs ±4µs 1396 samples
'Clear 4/4' Mean time: 1235µs ±14µs 1390 samples
'Clear 16/16' Mean time: 4507µs ±42µs 1405 samples
As you can see clearing 65K pixels is best done in one call with the actual javascript call adding about 100µs to do.
On Firefox the number of pixels to clear does not affect the execution time of the call to clearRect with a call clearRect(0,0,256,256) and clearRect(0,0,16,16) both taking ~2µs
Apart from the speed, trying to clear overlapping animated sprites per sprite becomes extremely complicated and can result in 100s of clear calls with only a dozen sprites. This is due to the overlapping.
What I want to do
I would like to draw a custom shape (for example a simple rectangle) which has different colors for each edge. I can do it with four paths, it works like a charm. BUT, in this way, it seems I can not fill the shape.
Trying the other way, I can draw the shape with one path and fill it, BUT in this case, I can not use different colors for the edges, because the last fillStyle will override the previous ones, even if I stroke the subpaths individually.
Is it possible to mix the two, by coloring subpaths individually, or by filling a shape consisting multiple paths?
Use different "layers" on the canvas, one for the filled with color shape, and a new one for each color path you have, z-index doesn't work on canvas, just make sure you draw what goes underneath first, and just wrap everything on a group <g> tag to make it easier to manipulate
After some experiment, I managed to solve my problem. It is not an ideal solution, because it has some overhead, but it works fine.
In the beginning of the drawing operation, I store the target coordinates in an array, and draw the whole stuff again and again. Each run is a new path. With .globalCompositeOperation = "destination-over" I can draw the lines under the existing ones, so each line can have a different color.
At the end of the drawing operation, the array contains all the coordinates of the shape, so the .fill() method can fill the path.
I hope it can help others:
// get the canvas context
var ctx = document.getElementById("myCanvas").getContext("2d");
// init shape array
var shape = [];
shape.push({
x: 0,
y: 0
}); // or any other starting point
// let's try
draw(20, 20);
draw(40, 40);
draw(60, 60);
// this is how we draw
function draw(x, y) {
// this is important
// see: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
ctx.globalCompositeOperation = "destination-over";
// this is just to be more visible
ctx.lineWidth = 10;
// get a random color
ctx.strokeStyle = myRandomColor();
// save target coordinates
shape.push({
x: x,
y: y
});
// reset the path
ctx.beginPath();
// jump to the start point
ctx.moveTo(shape[0].x, shape[0].y);
// draw the whole stuff
for (var i = 0; i < shape.length; i++) {
ctx.lineTo(shape[i].x, shape[i].y);
}
ctx.stroke();
}
function myRandomColor() {
var colors = ["red", "green", "blue", "yellow", "pink"];
var rand = Math.round(Math.random() * 5);
return colors[rand];
}
<canvas id="myCanvas"></canvas>
)
I have a small web-application which uses jquery and jcanvas (http://calebevans.me/projects/jcanvas/) to print some shapes onto a canvas.
It's basically a map. The user can zoom into it and drag it around and whenever he does so everything has to be drawn again.
As you may see in the code below I remove and recreate layers pretty often (whenever the user drags the map, zooms or resizes his window). I need the layers to handle hover- and click-events. My question is whether there is a big performance impact of this way to handle events in comparison to other solutions. If this is the case, how could I optimize my performance?
var posX = 0, posY = 0;
var zoom = 100;
var points = []; //array of up to 1000 points retrieved by ajax
function draw(){
$("canvas").removeLayers();
$("canvas").clearCanvas();
var xp, yp, ra;
var name;
$.each(points, function(index) {
xp = (this["x"]-posX)/zoom;
yp = (this["y"]-posY)/zoom;
ra = 1000/zoom;
$("#map").drawArc({
layer:true,
fillStyle: "black",
x: xp,
y: yp,
radius: ra,
mouseover: function(layer) {
$(this).animateLayer(layer, {
fillStyle: "#c33",
scale: 1.0
},200);
},
mouseout: function(layer) {
$(this).animateLayer(layer, {
fillStyle: "black",
scale: 1.0
},200);
},
mouseup: function(layer){
context(index,layer.x,layer.y);
}
});
});
}
Thank you for your time :-)
Optimization is in general a case-to-case thing but I'll try to give some general points for this case:
Keep operations to a minimum
Creating and removing "layers" (canvas elements) are costly operations - try to avoid if possible.
Rather move content around using drawImage() on itself or use clearRect() if you need to clear the whole canvas.
The same with a little more details:
You can for example use drawImage() to move the content to one of the sides, if we wanted to move everything to the left by 10 pixels we could do:
context.drawImage(canvas, 10, 0, canvas.width - 10, canvas.height,
0, 0, canvas.width - 10, canvas.height);
This will clip only the part we want to move/scroll and then redraw it in the new position.
Finally draw in the 10px gap at right with new graphics (you would probably want to cache the width and height but frankly, in the modern JavaScript engines this does not matter so much anymore as it did in the past). Test your points to see what points actually needs to be drawn to avoid drawing all of them again which otherwise renders this step pointless.
If you want to clear the canvas, rather than removing the canvas element and create a new canvas use the method clearRect() or use fillRect() if you want a pattern, color etc. Removing and creating elements are a costly operation from a optimization perspective especially if they affect the browser's flow (if it need to reflow the content). It triggers a bunch of operations from layout, css parsing, repaint etc. Reuse if you can.
I have done lots of examples on dragging an object created by Raphael's library. Now I am working with sets and was also able to write a code to drag them.
Now my problem appeared when I rotate an object and then drag it.
Check out this code example: demo
var paper = Raphael('stage', 300, 300);
var r = paper.rect(50,100,30,50).attr({fill:"#FFF"}).rotate(45),
t = paper.text(30, 140, "Hello");
var p = paper.set(r, t);
r.set = p, t.set = p;
p.newTX=0,p.newTY=0,p.fDx=0,p.fDy=0,p.tAddX,p.tAddY,p.reInitialize=false,
start = function () {
},
move = function (dx, dy) {
var a = this.set;
a.tAddX=dx-a.fDx,a.tAddY=dy-a.fDy,a.fDx=dx,a.fDy=dy;
if(a.reInitialize)
{
a.tAddX=0,a.fDx=0,a.tAddY=0;a.fDy=0,a.reInitialize=false;
}
else
{
a.newTX+=a.tAddX,a.newTY+=a.tAddY;
a.attr({transform: "t"+a.newTX+","+a.newTY});
}
},
up = function () {
this.set.reInitialize=true;
};
p.drag(move, start, up);
By examining the DEMO you can see that the set is created with rotated rectangle, but as soon you drag it, it goes back to the 0 degree state. Why? Any solutions?
The problem is that whenever an element is transformed by applying a string containing instructions to move, rotate, scale etc, it resets the transformation object, and hence previous transformations get lost. To avoid this, add "..." at the beginning of the transformation string. Like,
var el = paper.rect(10, 20, 300, 200);
// translate 100, 100, rotate 45°, translate -100, 0
el.transform("t100,100r45t-100,0");
// NOW, to move the element further by 50 px in both directions
el.transform("...t50,50");
If "t50,50" is used instead of "...t50,50", then transformation effect for "t100,100r45t-100,0" is lost and transformation effect for "t50,50" rules.
Raphael reference for further study: http://raphaeljs.com/reference.html#Element.transform
Hope this helps.
I found an easy solution to this problem. Since I need to have a diamond instead of rectangle, I have created a path that represents that diamond. Then this path becomes just like a square 45 degree rotated.
This turned out to be easy because dragging functionality I had for my program works perfectly with paths.