How can I animate a gradient with large pixels?
Asked Answered
S

1

6

I am working on a project where I would like to have darkness covering the screen and the character glowing in the darkness. I tried to animate the scene then draw darkness over it using this code:

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;

var pixelSize = 30;
var width = canvasWidth/pixelSize;
var height = canvasHeight/pixelSize;

var lightX = canvasWidth/2;
var lightY = canvasHeight/2;

var lightDiameter = 100;
var a = lightDiameter*pixelSize;

for(var x = 0; x < width; x++) {
  for(var y = 0; y < height; y++) {
    var alpha = 1.25 - a/(Math.pow(x*30 - lightX, 2) + Math.pow(y*30 - 
lightY, 2));
    ctx.fillStyle = "rgba( 25, 25, 30," + alpha + ")";
    ctx.fillRect(x*pixelSize, y*pixelSize, pixelSize, pixelSize);
  }
}

This worked pretty well and I liked the way it looked, but when this was repeatedly animated alongside the other code it slowed the rest down significantly. I think a possible solution may be to somehow draw a gradient with a lower "quality?", another solution I have considered is to save this drawing in a separate canvas and drawing it translated to the players location but that would make it impossible to add multiple sources of light, which I would like to do by simply adding their effect. I may just have to deal with the lag and I'm a noob at this stuff, but if anyone can help me that would be wonderful.

To clarify, I am using this code in the drawing loop, and also it is re-calculated in every iteration. I would prefer to recalculate this way so I can have multiple moving sources of light.

Selfsupporting answered 1/3, 2019 at 3:52 Comment(5)
I don’t know much about Canvas, but you can use CSS, potentially: rgraph.net/canvas/docs/…Teplitz
It would be easier to answer the question if we could see how you integrate it with your rendering loop. For example: If the gradient image isn't changing you could cache it in a virtual canvas and use drawimage to put it on the canvas. If you are calling document.getElementById inside your rendering loop, that could slow down your code and you could speed things up by doing that outside the loop.Carlile
Thanks for the advice. Hopefully that edit fixes the problem, sorry about that.Selfsupporting
You'll likely want to move to WebGL, and for this I suggest using Pixi.jsBaldheaded
@Carlile I don't think calling document.getElementById is going to have any serious performance impact: #12515470 .Kusin
K
-1

This is because fillRect is pretty slow compared to other methods. You could probably speed things up by using ImageData objects instead.

The way to do this would be to render everything to the canvas, get the corresponding ImageData, modify its contents and put it back onto the canvas:

var ctx = canvas.getContext("2d");
// render stuff here
var imageData = ctx.getImageData(0,0,canvasWidth,canvasHeight);
for (let x=0;x<canvasWidth;x++){
    for (let y=0;y<canvasHeight;y++){
        let i = (x+y*canvasWidth)*4;
        let alpha = calculateAlpha(x,y); // your method here (should result in a value between 0 and 1)
        imageData.data[i] = (1-alpha)*imageData.data[i]+alpha*25;
        imageData.data[i+1] = (1-alpha)*imageData.data[i+1]+alpha*25;
        imageData.data[i+2] = (1-alpha)*imageData.data[i+2]+alpha*30;
        imageData.data[i+3] = 1-(1-alpha)*(1-imageData.data[i+3]);
    }
}
ctx.putImageData(imageData,0,0);

This should do the lighting on a per-pixel basis, and much faster than using clearRect all the time. However, it might still slow things down, as you're doing a lot of calculations each frame. In that case, you could speed thing up by doing the lighting in a second canvas that is positioned over your main canvas using css:

<div id="container">
    <canvas id="canvas"></canvas>
    <canvas id="lightingCanvas"></canvas>
</div>

Css:

#container {
     position: relative;
}
#canvas, #lightingCanvas {
    position: absolute;
    top: 0;
    left: 0;
}
#container, #canvas, #lightingCanvas {
     width: 480px;
     height: 360px;
}

Javascript:

var canvas = document.getElementById("lightingCanvas")
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgb(25,25,30)";
ctx.fillRect(0,0,canvas.width,canvas.height);
var imageData = ctx.getImageData(0,0,canvasWidth,canvasHeight);
for (let x=0;x<canvasWidth;x++){
    for (let y=0;y<canvasHeight;y++){
        let i = (x+y*canvasWidth)*4;
        let alpha = calculateAlpha(x,y); // your method here (should result in a value between 0 and 1)
        imageData.data[i+3] = 255*alpha;
    }
}
ctx.putImageData(imageData,0,0);

This way the browser takes care of the blending for you and you just need to plug in the correct alpha values - so rendering should be even faster now.

This will also allow you to bring the large pixels back in - just use a lower resolution on the second canvas and use some css effect like image-rendering: -webkit-crisp-edges to make the canvas pixelated when scaled up.

Kusin answered 2/3, 2019 at 10:19 Comment(5)
fillRect is actually quite fast. I created a fiddle here with some running timers and it looks like it's about four times faster than your second function: jsfiddle.net/9hco6zyr On a 300x300 image, and on my computer, it took about a third of a millisecond to run his code, so I think it was more about other things running in his rendering loop than actual performance problems with fillrectCarlile
Oh well, I didn't pay attention to what pixel size he was using. If I change the pixel size to ten or below (just edited the values in your fiddle), the ImageData approach is definitely faster though. Also, keep in mind that this can also be sped up by using a smaller canvas as described in the second half of my answer.Kusin
Good point. It's interesting to see the point at which the performance crosses over. You may want to edit your code to fix "let i = x+ycanvasWidth;" which should be "let i = (x+ycanvasWidth)*4;" and put the close bracket after the "x++"Carlile
So I honestly haven't used "let" before, but it seems to work like a "var" but do why do you use it? Is it faster? does it only work well in loops?Selfsupporting
@OtherMe In general, let defines a variable at block level scope, so in this case i is only accessible inside the for loop. I have no idea why I used it there though, var would probably work just as good.Kusin

© 2022 - 2024 — McMap. All rights reserved.