0

I am learning JavaScript, coming from eight years of Python experience, and as an exercise I made a Mandelbrot renderer that generates the fractal and uses putImageData to update the canvas from left to right one single-pixel column at a time.

I found that with this approach the visible image in the browser only updates when the full-screen calculation is complete (rather than seeing it appear gradually from left to right as I wanted). I understand that it is the expected behaviour, so I decided to add the "scan-line" animation by using requestAnimationFrame. (something like seen here: Christian Stigen Larsen renderer)

My expectation is that lighter calculations should render faster (as the next frame is available sooner) and higher-iteration calculations should render slower. What I found with my implementation is that the scan-line is consistently slow.

I have isolated this issue from anything to do with my Mandelbrot calculations, as this behaviour also happens for the minimal case below. I am running this on Chrome 83 and see the canvas populate very slowly at a constant rate (approx 30 pixels per second) from left to right.

Is my implementation of rAF incorrect or are my expectations wrong? The renderer I linked to uses setTimeout() to animate, but I read that it is a widely discouraged practice these days.

How should I implement a left-to-right scan update of my canvas at the highest available frame-rate (I am not worried about limiting it at the moment)?

EDIT: For clarity, the code below draws a single thin rectangle at every frame request by rAF, and takes exactly the same amount of time to paint the full canvas as a 100-iteration Mandelbrot render.

This suggest to me that the slowness of it is not due to amount of calculations taking place between frames.

const canvas = document.querySelector('.myCanvas');
const width =  window.innerWidth;
const height = window.innerHeight;
const ctx = canvas.getContext('2d');

function anim(timestamp, i) {
  if (i < width) {
    ctx.fillRect(i, 0, 1, height);
    window.requestAnimationFrame(function(timestamp) {
      anim(timestamp, i + 1);
    })
  }
}

ctx.fillStyle = 'rgb(0,0,0)';
window.requestAnimationFrame(function(timestamp) {
  anim(timestamp, 0);
});
<!DOCTYPE html>
<html lang="en">

<head>
  <canvas class="myCanvas">
        <p>Add suitable fallback here.</p>
    </canvas>
  <script src="slow%20rAF.js"></script>
  <style>
    body {
      margin: 0;
      overflow: hidden;
    }
  </style>
  <meta charset="UTF-8">
  <title>Mandelbrot</title>
</head>

<body>




</body>


</html>
KIO
  • 61
  • 6

1 Answers1

2

There is no 'faster' requestAnimationFrame.

Generally the answer to this is to separate calcuations from rendering. Ideally the calculations happen off of the main thread and rendering is handled on the main thread. Even without threading you should be able to find a way to do the calculations outside of the RAF, and just have the RAF render the pixels.

Graham P Heath
  • 7,009
  • 3
  • 31
  • 45
  • Thanks for this, I suspected that in my investigation. In the question I outline that I have isolated this behaviour from any mandelbrot calculation. However, rAF is "slow" by my expectations even in the minimal example I provided where no calculation takes place: it just draws pixel-wide rectangles at every frame. In fact, I find the speed of drawing a single rectangle at every frame is exactly the same as performing a 100-iteration mandelbrot render on a slice of pixels of the same size. For me this feels wrong. – KIO Jun 03 '20 at 17:17
  • 1
    @KIO, you are incrementing by one pixel at every frame. A requestAnimationFrame loop will run at screen-refresh rate in Chrome and at 60Hz in FF. If your monitor is a 60Hz monitor (which is quite common), then to entirely fill the canvas in your snippet will take **at least 4980ms**. ( 300px * (1px * (1000/60)) Graham is right in the approaches to workaround the issue too, I'd just like to point that even if ran in a parallel thread, you should still run it in batches so it can react to the rAF callback when needed. See https://stackoverflow.com/q/54478195/ – Kaiido Jun 04 '20 at 00:59
  • @Kaiido, thanks. I see my problem - it is because I do a single pixel increment per frame. I did not do the maths properly. The snippet I included in the question seems to be 300px wide, my monitor is 144Hz, so it takes about 2 seconds to scan the canvas with that. I should have spotted that. Your suggestion about separating the threads is valid and I will look into it, for the moment it was bottlenecking at the refresh rate because of the way I set up the increment and that's the thing I will fix. Thanks! – KIO Jun 04 '20 at 10:10
  • @Kaiido, the mandelbrot renderer runs in batches anyway. As in, every pixel value is completely independent of adjacent pixels - my current approach is to calculate them a strip at a time and update the canvas that way. There are always some pixels ready for rAF to draw. – KIO Jun 04 '20 at 10:13