How to avoid permanent particle trails on html5 canvas? - javascript

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).

Related

Clearing canvas "layers" separately?

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.

HTML5 Canvas rotate function

function drawNumbers(){
var rad, num;
cx.font= "30px Arial";
cx.textAlign = "center";
cx.textBaseline = "middle";
//numbers around the inner circumference
for(num=1; num < 13; num++){
rad = num * Math.PI/6; //angle for every number
cx.rotate(rad);
cx.translate(0, -175);
cx.rotate(-rad);
cx.fillText(num.toString(),0,0);
cx.rotate(rad);
cx.translate(0, 175);
cx.rotate(-rad);
}
}
function drawHands(){
//getting the time
var time = new Date();
var hours = time.getHours();
var minutes = time.getMinutes();
var seconds = time.getSeconds();
//setting the radians based on the time
//hour hand
hours %= 12;
hours = (hours * Math.PI/6) + (minutes * Math.PI/360) + (seconds * Math.PI/21600);
hands(hours, radius * 0.04, radius * 0.5);
//minute hand
minutes = (minutes * Math.PI/30) + (seconds * Math.PI/1800);
hands(minutes, radius * 0.03, radius * 0.65);
//second hand
seconds = (seconds * Math.PI/30);
hands(seconds, radius * 0.01, radius * 0.68);
}
function hands(ang, width, length){
cx.beginPath();
cx.lineWidth = width;
cx.lineJoin = "round";
cx.lineCap = "round";
cx.moveTo(0, 0);
cx.rotate(ang);
cx.lineTo(0, -length);
cx.stroke();
cx.rotate(-ang);
}
I was learning the HTML5 canvas in W3Schools and the tutorial was teaching how to make a working clock.
1. I just don't understand how the extra rotates work in the functions.
2. When applying a rotate function, does it always rotate from the center of origin (0, 0) of the canvas?
When you call the rotate function it rotates the entire canvas, imagine holding a painting and then tilting it. It happens around the origin always. The way to rotate around a different point is to translate the entire canvas first, then rotate it.
Going back to the painting analogy, if we have rotated our painting, once we have drawn our line, we need to then restore the painting to being upright. Thus we rotate(-ang). If we had translated we would also have to undo our transation in a similar manner.
Rotating and why we undo rotates
In the code below you can see I'm drawing a base black rectangle, and then calling a function which rotates the canvas by 0.5 radians and draws another rectangle twice. I haven't undone my rotation so the 3rd rectangle is actually rotated at 1 radian.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
function drawRotatedRectangle() {
ctx.rotate(0.5);
ctx.fillRect(0, 0, 60, 50);
}
// Draw base rectangle
ctx.fillRect(0, 0, 60, 50);
// Rotate and draw second rectangle
ctx.fillStyle = "#FF0000";
drawRotatedRectangle();
// rotate and draw third rectangle
ctx.fillStyle = "#00FF00";
drawRotatedRectangle();
<canvas id="canvas"></canvas>
To fix this we modify the drawRotatedRectangle() function to undo all translations and rotations that it made:
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
function drawRotatedRectangle() {
ctx.rotate(0.5);
ctx.fillRect(0, 0, 60, 50);
ctx.rotate(-0.5);
}
// Draw base rectangle
ctx.fillRect(0, 0, 60, 50);
// Rotate and draw second rectangle
ctx.fillStyle = "#FF0000";
drawRotatedRectangle();
// rotate and draw third rectangle
ctx.fillStyle = "#00FF00";
drawRotatedRectangle();
<canvas id="canvas"></canvas>
Now we see the red (hidden) rectangle and the green rectangle are at the same angle.
Rotating around a different point to the origin
To demonstrate how we can rotate around a different location to the origin, we can first translate where our context origin is and then rotate our canvas. Below I move the origin to the center of the base rectangle, rotate the canvas and draw a rotated rectangle ontop of the base rectangle. Again the translations and rotations are restored in order of most recently applied.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
function drawRotatedRectangle() {
// Move origin to center of rectangle
ctx.translate(30, 25);
// Rotate 0.5 radians
ctx.rotate(0.5);
// Draw rectangle where the center of the rectangle is the origin
ctx.fillRect(-30, -25, 60, 50);
// Undo our rotate
ctx.rotate(-0.5);
// Undo our translate
ctx.translate(-30, -25);
}
// Draw base rectangle
ctx.fillRect(0, 0, 60, 50);
// Rotate and draw second rectangle
ctx.fillStyle = "#FF0000";
drawRotatedRectangle();
// rotate and draw third rectangle
ctx.fillStyle = "#00FF00";
drawRotatedRectangle();
<canvas id="canvas"></canvas>
Edit - Handling Rounding Errors
As Kaiido mentioned in the comments, the rotate(a) function will round the input it is given so simply doing the reverse rotate(-a) function will not return you to the original transformation.
The solution they suggested is to set the transformation matrix to the desired location with setTransform(), in these example we are only returning to the original transform of the canvas so we can use the identity matrix:
ctx.setTransform(1,0,0,1,0,0)
Alternitively, you can also use save() and restore() methods. These will act like pushing the current state of the canvas context to a stack and when you restore() it will pop the latest state from the stack returning you to the previous transform. This article by Jakob Jenkov explains this method further with some examples.

Fastest way of fading out entire contents of a canvas to transparency, not other color

I want to create visualization, where whatever is drawn fades away slowly. This should be continuous, so that content added later will be less faded than old content, like in this picture:
I don't want to clear entire canvas, I would like to just make everything on it more transparent - then draw any new objects.
My instinct would be to do this in each frame:
ctx.fillStyle = "rgba(0,0,0,0)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
However the problem is that canvas doesn't blend alpha - it uses it for blending the other colors.
How can I get canvas to continuously decrease opacity of everything drawn in it?
You're almost definitely looking for canvas compositing operations! It looks like "destination-in" should achieve what you're looking for.
let canvas = document.getElementsByTagName('canvas')[0];
let ctx = canvas.getContext('2d');
let rand = n => Math.floor(Math.random() * n);
setInterval(() => {
ctx.beginPath();
ctx.arc(rand(300), rand(120), rand(60), Math.PI * 2, 0);
ctx.fillStyle = `rgba(${rand(256)}, ${rand(256)}, ${rand(256)}, 1)`;
ctx.globalCompositeOperation = 'source-over';
ctx.fill();
}, 150);
let fadeOut = () => {
let fadeAmount = 0.002;
// Note that the colour here doesn't matter! Only the alpha matters.
// The colour here is red, but you'll see no red appear
ctx.fillStyle = `rgba(255, 0, 0, ${1 - fadeAmount})`;
ctx.globalCompositeOperation = 'destination-in';
ctx.fillRect(0, 0, 300, 120);
requestAnimationFrame(fadeOut);
};
requestAnimationFrame(fadeOut);
canvas { border: 3px solid #808080; background-color: #000000; }
<canvas width="300" height="120"></canvas>
Note: When you set ctx.globalCompositeOperation, it will apply until you either do ctx.restore(), or set ctx.globalCompositeOperation to a new value! This is a bit of a gotcha.
why you don't decrease the opacity of the canvas and when it reach 0 you erase everything from the canvas then reset the opacity value to 1

Javascript canvas flickering

Seems like there are other questions like this and I'd like to avoid a buffer and/or requestAnimationFrame().
In a recent project the player is flickering but I cannot find out the reason. You can find the project on JSFiddle: https://jsfiddle.net/90wjetLa/
function gameEngine() {
timer += 1;
timer = Math.round(timer);
// NEWSHOOT?
player.canShoot -= 1;
// MOVE:
movePlayer();
shootEngine(); // Schussbewegung & Treffer-Abfrage
// DRAW:
ctx.beginPath();
canvas.width = canvas.width;
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fill();
drawField();
drawPlayer();
drawShoots();
setTimeout(gameEngine, 1000 / 30);
}
Each time you write to a visible canvas the browser want's to update the display. Your drawing routines might be out of sync with the browsers display update. The requestAnimationFrame function allows you to run all your drawing routines before the display refreshes. Your other friend is using an invisible buffer canvas. Draw everything to the buffer canvas and then draw the buffer to the visible canvas. The gameEngine function should only run once per frame and if it runs multiple times you could see flicker. Try the following to clear multiple runs in the same frame.
(edit): You might also want to clear the canvas instead of setting width.
(edit2): You can combine the clearRect, rect, and fill to one command ctx.fillRect(0, 0, canvas.width, canvas.height);.
var gameEngineTimeout = null;
function gameEngine() {
// clear pending gameEngine timeout if it exists.
clearTimeout(gameEngineTimeout);
timer += 1;
timer = Math.round(timer);
// NEWSHOOT?
player.canShoot -= 1;
// MOVE:
movePlayer();
shootEngine(); // Schussbewegung & Treffer-Abfrage
// DRAW:
ctx.beginPath();
//canvas.width = canvas.width;
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//ctx.beginPath();
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
//ctx.fill();
drawField();
drawPlayer();
drawShoots();
gameEngineTimeout = setTimeout(gameEngine, 1000 / 30);
}

Canvas Painting Over with Apha Value

A red rectangle, that I've drawn should smoothly disappear.
As you can see here, it works, but it does not completely disappear. Why?
(function init() {
var canvas = document.getElementById('canvas'), ctx;
if (!canvas.getContext) return;
ctx = canvas.getContext('2d');
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.1)";
setInterval(function() {
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fill();
}, 100);
}());
It should also do the job with lots of different colors and alpha values at the same time.
Thank you :D
It's due to rounding errors in canvas. The value when multiplied with the alpha channel will have to cut the fraction to fit the integer nature of the bitmap.
In all cases here the value will never become full alpha.
The work-around is to track the current alpha level and at the last one clear manually.
Example here
var tracker = 0,
timer;
ctx.fillStyle = "rgba(255,255,255,0.1)";
timer = setInterval(function() {
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fill();
tracker++;
if (tracker > 43) {
clearTimeout(timer);
ctx.fillStyle = "rgb(255,255,255)";
ctx.fillRect(0,0,canvas.width,canvas.height);
}
}, 100);

Categories