简体   繁体   中英

How to avoid permanent particle trails on html5 canvas?

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM