Related
I've been battling with <canvas> for a while. I want to create an animation/game with lots of different units on different layers.
Due to <canvas> limitation to just one context my approach is as follows:
have one <canvas> on the page,
create multiple "layers" using document.createElement("canvas"),
animate/rerender "layers" separately.
But this approach does not seem to work properly due to one quirk - in order to stack "layers" on my main canvas I'm doing realCanvas.drawImage(layerCanvas, 0, 0);. Otherwise the layers are not being rendered.
The issue here is ultimately it does not change a thing as everything is in being drawn on my main <canvas> and when I do clearRect on one of my layers it does nothing as the pixels are also drawn on the main canvas in addition to given layer. If I run clearRect on main canvas then the layers are useless as every layer is on main canvas so I'm back to starting point as I'm clearing the whole canvas and layers are not separated at all.
Is there a way to fix it easily? I feel like I'm missing something obvious.
Here's an example, how do I clear blue ball trail without touching background rectangles here? There should be only one blue ball under your cursor. Note it's a very simplified example, I'll have multiple blue balls and multiple other layers. I just want to know how the heck do I clear only one layer in canvas. Note I don't want to use multiple <canvas> elements and don't want to use any libs/engines as I'm trying to learn canvas by this. I know many apps use just one canvas html element, many layers and somehow animate them separately.
Source: https://jsfiddle.net/rpmf4tsb/
Try adding canvas2ctx.clearRect(0,0, canvas.width, canvas.height); under ctx.clearRect(0,0, canvas.width, canvas.height); and it works as supposed but all the layers are being cleared, not only the one with the ball...
If you look at things from a performance point-of-view, things are better if you use a single visible <canvas> element for your visual output.
Nothing is stopping you from doing things on seperate canvases you stack on top of each other though. Maybe there's just a basic misunderstanding here.
You say:
and when I do clearRect on one of my layers it does nothing as the
pixels are also drawn on the main canvas in addition to given layer
Well that's not true. If you draw the contents of a freshly cleared canvas onto another canvas it won't overwrite the target canvas with 'nothing'.
Take a look at this example:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
Our main canvas contains a green background with a black circle and we're utilizing the drawImage() method to draw a dynamically created, freshly cleared canvas onto, which results in a green background with a black circle as the new canvas element did not contain any data to draw. It did not erase the main canvas.
If we change the example a bit, so the second canvas contains a rectangle things will work as expected:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempContext.strokeRect(tempCanvas.width / 2 - 60, tempCanvas.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
Now if we assume the green background with the circle (tempCanvasA) and the rectangle (tempCanvasB) are two separate canvases we ultimately want to draw to a main canvas it will bring up an important point: the order of drawing.
So this will work:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();
let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
while this fails:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();
let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
The rectangle is missing! Why does it fail? Because we changed the order we draw the canvases onto the main canvas. In the latter example:
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
We first draw tempCanvasB which contains a transparent background & the rectangle and afterwards tempCanvasA with the solid green background - which covers the entire canvas - and the circle. As there are no transparent pixels it will overwrite the rectangle which we've drawn first.
To get to your example with the ball. The problem is that you're drawing the ball to the wrong canvas. Inside your draw function you're doing this:
ctx.clearRect(0, 0, canvas.width, canvas.height);
ball.draw();
ball.x = e.clientX;
ball.y = e.clientY;
ctx.drawImage(canvas2, 0, 0);
So first you clear ctx, afterwards call ball's draw method which draws onto canvas2ctx and finally drawImage onto ctx with the contents of canvas2ctx.
Instead draw the ball onto the main ctx after using drawImage()
e.g.
// helper functions
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
// canvas
let firstRender = true;
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth - 50;
canvas.height = window.innerHeight - 50;
let ctx = canvas.getContext('2d');
// virtual canvas for rectangles layer
let canvas2 = document.createElement("canvas");
canvas2.width = window.innerWidth - 50;
canvas2.height = window.innerHeight - 5;
let canvas2ctx = canvas2.getContext("2d");
let ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function draw(e) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(canvas2, 0, 0);
ball.draw();
ball.x = e.clientX;
ball.y = e.clientY;
if (firstRender) {
drawRandomRectangles()
firstRender = false;
}
}
function drawRandomRectangles() {
for (i = 0; i < 50; i++) {
canvas2ctx.beginPath();
canvas2ctx.rect(randomInt(0, window.innerWidth - 50), randomInt(0, window.innerWidth - 50), randomInt(5, 20), randomInt(5, 20));
canvas2ctx.stroke();
}
}
canvas.addEventListener('mousemove', function(e) {
draw(e);
});
ball.draw();
<canvas id="canvas"></canvas>
Thinking about your approach of multiple canvas stacking above each other sounds like an interesting approach to get things done. I would not recommend doing this in that way and therefore handle multiple layers through JavaScript and then still render every time everything new. Especially if you will use animations, then I believe that multiple not synchronized canvases will give you another sort of headache.
Then you would do the following:
Clear your canvas with clearRect.
Draw in an iteration each layer above each other
I hope this theoretical explanation helps.
Now to your code: At the end of the day your ctx and canvas2ctx are in the very same context, because they are from the same canvas. That makes anyway not much sense.
When creating an HTML canvas I was planning on making these cylinders and animating marbles moving inside them. However, when trying to do so it would just delete everything. After messing around with my code, I discovered the problem was due to the fillStyle which was a CanvasPattern from an image.
This snippet simulates exactly what I am experiencing. The rectangle draws perfectly fine, however, after 1 second, when the interval runs, it disappears and there is no arc or "marble" drawn. There are no errors in console either
With Interval (Not working):
let canv = document.getElementById("canvas");
let ctx = canv.getContext('2d');
let matte = new Image(canv.width, canv.height);
matte.onload = function() {
var pattern = ctx.createPattern(matte, 'repeat');
ctx.globalCompositeOperation = 'source-in';
ctx.rect(0, 0, canv.width, canv.height);
ctx.fillStyle = pattern;
ctx.fill();
};
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
ctx.lineWidth = "5";
ctx.fillRect(0, 0, 50, 50); // This dissapears when the setInterval runs???? Marble doesn't even draw
let x = 60,
y = 20;
var draw = setInterval(function() { // Drawing the marble
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
y += 1;
}, 1 * 1000);
<html>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
When I get rid of the interval it would work, but when the interval is there, nothing is drawn.
I have absolutely no idea why this is happening and I cannot find anything on the internet regarding this problem. Is there a way I can animate this marble while having the image continue to mask its fillStyle??
Without Interval (Working):
let canv = document.getElementById("canvas");
let ctx = canv.getContext('2d');
let matte = new Image(canv.width, canv.height);
matte.onload = function() {
var pattern = ctx.createPattern(matte, 'repeat');
ctx.globalCompositeOperation = 'source-in';
ctx.rect(0, 0, canv.width, canv.height);
ctx.fillStyle = pattern;
ctx.fill();
};
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
ctx.lineWidth = "5";
ctx.fillRect(0, 0, 50, 50); // This dissapears when the setInterval runs???? Marble doesn't even draw
let x = 60,
y = 20;
//var draw = setInterval(function() { // Drawing the marble
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
y += 1;
//}, 1 * 1000);
<html>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
Things I've tried:
Got rid of beginPath and closePath, doesn't make anything disappear but doesn't display arc
Recreating pattern inside the interval
Making the fillstyle a colour for everything (Works)
Making the fillstyle of the marble a colour (Doesnt work)
EDIT: After looking some more, I believe the problem is in the globalCompositeOperation. It's what deals with the pattern intersecting the drawing. When looking at all the types, source-in is the only one that satisfies my expected result, however, it's not working in this situation weirdly.
Thank you in advance
The problem is your ctx.globalCompositeOperation instruction. Using source-in, you're explicitly telling the canvas to make anything that's a different color from the new thing you're drawing (on a per pixel basis) transparent. Since every pixel is different, everything becomes transparent and you're left with what looks like an empty canvas (even if the ImageData will show RGBA data in which the RGB channels have meaningful content, but A is 0).
Remove the globalCompositeOperation rule and you're good to go, but you should probably take some time to rewrite the logic here, so that nothing happens until your image is loaded, because your code is pretty dependent on that pattern existing: wait for the image to load, the build the pattern, assign it to the context, and then start your draw loop.
const canv = document.getElementById("canvas");
const ctx = canv.getContext('2d');
let x = 60, y = 20;
function start() {
const matte = new Image(canv.width, canv.height);
matte.addEventListener(`load`, evt =>
startDrawing(ctx.createPattern(matte, 'repeat'))
);
matte.addEventListener(`load`, evt =>
console.error(`Could not load ${matte.src}...`);
);
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
}
function startDrawing(pattern) {
ctx.strokeStyle = `red`;
ctx.fillStyle = pattern;
setInterval(() => {
draw();
y += 10;
}, 1 * 1000);
}
function draw() {
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// and kick everything off
start();
Although on another note, normally setInterval is not the best choice for animations: you usually want requestAnimationFrame instead, with a "wall time" check (e.g. doing things depending on the actual clock, instead of trusting intervals):
...
function startDrawing(pattern) {
ctx.strokeStyle = `red`;
ctx.fillStyle = pattern;
startAnimation();
}
let playing, lastTime;
function startAnimation() {
playing = true;
lastTime = Date.now();
requestAnimationFrame(nextFrame);
}
function stopAnimation() {
playing = false;
}
function nextFrame() {
let newTime = Date.now();
if (newTime - lastTime >= 1000) {
draw();
}
if (playing) {
lastTime = newTime;
requestAnimationFrame(nextFrame);
}
}
...
https://jsbin.com/wawecedeve/edit?js,output
My JS code:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var mouse = {x:0,y:0}
const times = [];
let fps;
function refreshLoop() {
window.requestAnimationFrame(() => {
const now = performance.now();
while (times.length > 0 && times[0] <= now - 1000) {
times.shift();
}
times.push(now);
fps = times.length;
refreshLoop();
});
}
refreshLoop();
function draw() {
ctx.fillStyle = "black"
ctx.fillRect(0, 0, c.width, c.height);
ctx.strokeStyle = "white"
ctx.beginPath();
var e = window.event;
ctx.arc(mouse.x, mouse.y, 40, 0, 2*Math.PI);
ctx.stroke();
ctx.font = "30px Comic Sans MS";
ctx.fillStyle = "red";
ctx.textAlign = "center";
ctx.fillText(fps, c.width/2, c.height/2);
}
setInterval(draw, 0);
document.addEventListener('mousemove', function(event){
mouse = { x: event.clientX, y: event.clientY }
})
My HTML is just the canvas declaration.
To my understanding, setinterval(x, 0) is supposed to run as fast as possible but it's never exceeding 60fps. I'm trying to hit 240+ fps to reduce input lag.
First, never use setInterval(fn, lessThan10). There is a great possibility that fn will take more than this time to execute, and you may end up stacking a lot of fn calls with no interval at all, which can result* in the same as the well known while(true) browser crasher®.
*Ok, in correct implementations, that shouldn't happen, but you know...
Now, to your question...
Your code is quite flawn.
You are actually running two different loops concurrently, which will not be called at the same interval.
You are checking the fps in a requestAnimationFrame loop, which will be set at the same frequency than your Browser's painting rate (generally 60*fps*).
You are drawing in the setInterval(fn, 0)
Your two loops are not linked and thus, what you are measuring in the first one is not the rate at which your draw is called.
It's a bit like if you did
setInterval(checkRate, 16.6);
setInterval(thefuncIWantToMeasure, 0);
Obviously, your checkRate will not measure thefuncIWantToMeasure correctly
So just to show that a setTimeout(fn, 0) loop will fire at higher rate:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var mouse = {
x: 0,
y: 0
}
const times = [];
let fps;
draw();
function draw() {
const now = performance.now();
while (times.length > 0 && times[0] <= now - 1000) {
times.shift();
}
times.push(now);
fps = times.length;
ctx.fillStyle = "black"
ctx.fillRect(0, 0, c.width, c.height);
ctx.strokeStyle = "white"
ctx.beginPath();
ctx.arc(mouse.x, mouse.y, 40, 0, 2 * Math.PI);
ctx.stroke();
ctx.font = "30px Comic Sans MS";
ctx.fillStyle = "red";
ctx.textAlign = "center";
ctx.fillText(fps, c.width / 2, c.height / 2);
setTimeout(draw, 0);
}
<canvas id="myCanvas"></canvas>
Now, even if a nested setTimeout loop is better than setInterval, what you are doing is a visual animation.
It makes no sense to draw this visual animation faster than the browser's painting rate, because what you will have drawn on this canvas won't be painted to screen.
And as said previously, that's exactly the rate at which an requestAnimationFrame loop will fire. So use this method for all your visual animations (At least if it has to be painted to screen, for some rare case there are other methods I could link you to in comments if needed).
Now to solve your actual problem, which is not to render at higher rate, but to handle user's inputs at such rate, then the solution is to split your code.
Keep your drawing part bound to a requestAniamtionFrame loop, doesn't need to get faster.
Update your object's values that should respond to user's gesture synchronously from user's input. Though, beware some user's gestures actually fire at very high rate (e.g WheelEvent, or window's resize Event). Generally, you don't need to get all the values of such events, so you might want to bind these in rAF throttlers instead.
If you need to do collision detection with moving objects, then perform the Math that will update moving objects from inside the user's gesture, but don't draw it on screen.
There are thousands of moving particles on an HTML5 canvas, and my goal is to draw a short fading trail behind each one. A nice and fast way to do this is to not completely clear the canvas each frame, but overlay it with semi-transparent color. Here is an example with just one particle:
var canvas = document.getElementById('display');
var ctx = canvas.getContext('2d');
var displayHeight = canvas.height;
var backgroundColor = '#000000';
var overlayOpacity = 0.05;
var testParticle = {
pos: 0,
size: 3
};
function render(ctx, particle) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
function update(particle) {
particle.pos += 1;
}
// Fill with initial color
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
function mainLoop() {
update(testParticle);
render(ctx, testParticle);
requestAnimationFrame(mainLoop);
}
mainLoop();
<canvas id="display" width="320" height="240"></canvas>
There is an apparent problem: with low opacity values, the trail never fades away completely. You can see the horizontal line that (almost) does not fade in my single-particle example. I understand why this happens. ColorA overlayed by semi-transparent ColorB is basically a linear interpolation, and ColorA never fully converges to ColorB if we repeatedly do the following:
ColorA = lerp(ColorA, ColorB, opacityOfB)
My question is, what can I do to make it converge to the background color, so that trails don't remain there forever? Using WebGL or drawing trails manually are not valid options (because of compatibility and performance reasons respectively). One possibility is to loop over all canvas pixels and manually set pixels with low brightness to background color, although it may get expensive for large canvases. I wonder if there are better solutions.
As a workaround which could work in some cases is to set the overlayOpacity up to 0.1 (this value converges) but draw it only every x times and not in every render call.
So when drawn only every other time it keeps more or less the same trail length.
var renderCount = 0;
var overlayOpacity = 0.1;
function render(ctx, particle) {
if((renderCount++)%2 == 0) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
Obviously the disadvantage is that it looks more jerked and perhaps this may not be acceptable in your case.
Best solution is to use the composite operation "destination-out" and fade to a transparent background. Works well for fade rates down to globalAlpha = 0.01 and event a little lower 0.006 but it can be troublesome below that. Then if you need even slower fade just doe the fade every 2nd or 3rd frame.
ctx.globalAlpha = 0.01; // fade rate
ctx.globalCompositeOperation = "destination-out" // fade out destination pixels
ctx.fillRect(0,0,w,h)
ctx.globalCompositeOperation = "source-over"
ctx.globalAlpha = 1; // reset alpha
If you want a coloured background you will need to render the animation on an offscreen canvas and render it over the onscreen canvas each frame. Or make the canvas background the colour you want.
If someone struggles with this, here is a workaround that worked for me:
// Do this instead of ctx.fillStyle some alpha value and ctx.fillRect
if(Math.random() > 0.8){
ctx.fillStyle = 'rgba(255, 255, 255, '+getRandomNumber(0.1,0.001)+')';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Define this helper function somewhere in your code
function getRandomNumber(minValue, maxValue) {
return Math.random() * (maxValue - minValue) + minValue;
}
It also works for different colored backgrounds. Adjust trail length by playing around with Math.random() > 0.8 and getRandomNumber(0.1,0.001).
I want to make a simple page that grows circles from its center ad infinitum. I'm almost there, but I can't figure out how to repeatedly grow them (resetting the radius i to 0 at a certain interval and calling the function again). I assume it will require a closure and some recursion, but I can't figure it out.
// Initialize canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.getElementsByTagName('body')[0].appendChild(canvas);
// Grow a circle
var i = 0;
var draw = function() {
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, i, 0, 2 * Math.PI);
ctx.fill();
i += 4;
window.requestAnimationFrame(draw);
}
draw();
Two things I'd do...
First, modify your draw function so that if the circle gets to a certain size, the i variable is reset back to zero. That starts the circle over again.
Second, add a setInterval timer to call your draw function at some time interval. See http://www.w3schools.com/js/js_timing.asp for details.
This setup will cause draw() to be called regularly, and the reset of i to zero makes it repeat.
So this did indeed require a closure. We wrap the initial function in a closure, and call it's wrapper function, which reinitializes I every time when called. draw() grows a single circle, and drawIt()() starts a new circle.
var drawIt = function(color) {
var i = 0;
return function draw() {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, i, 0, 2 * Math.PI);
ctx.fill();
i+=1*growthFactor;
// Growing circles until they are huge
if (i < canvas.width) {
window.requestAnimationFrame(draw);
if (i === spacing) {
circles++
drawIt(nextColor())();
}
}
}
};
drawIt(nextColor())();
})();