1

Me and a friend are playing around with fractals and wanted to make an interactive website where you can change values that generate the fractal, and you can see how its affected live. On small resolution tests, the website it quite responsive but still slow.

drawFractal = () => {
  for (let x = 0; x < this.canvas.width; x++) {
    for (let y = 0; y < this.canvas.height; y++) {
      const belongsToSet = this.checkIfBelongsToMandelbrotSet(x / this.state.magnificationFactor - this.state.panX, y / this.state.magnificationFactor - this.state.panY);
      if (belongsToSet === 0) {
        this.ctx.clearRect(x,y, 1,1);
      } else {
        this.ctx.fillStyle = `hsl(80, 100%, ${belongsToSet}%)`;
        // Draw a colorful pixel
        this.ctx.fillRect(x,y, 1,1);
        }
      }
    }
}

checkIfBelongsToMandelbrotSet = (x,y) => {
  let realComponentOfResult = x;
  let imaginaryComponentOfResult = y;
  // Set max number of iterations
  for (let i = 0; i < this.state.maxIterations; i++) {
    const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
    const tempImaginaryComponent = this.state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
    realComponentOfResult = tempRealComponent;
    imaginaryComponentOfResult = tempImaginaryComponent;
    // Return a number as a percentage
    if (realComponentOfResult * imaginaryComponentOfResult > 5) {
      return (i / this.state.maxIterations * 100);
    }
  }
  // Return zero if in set
  return 0;
}

This is the algorithm that handles the generation of the fractal. However we iterate over every pixel of the canvas which is quite inefficient. As a result the whole website is really slow. I wanted to ask whether it's a good idea to use html canvas or are there more efficient alternatives? Or can I optimise the drawFractal() function to be able to be more efficient? I have no idea how to continue from this point as i am inexperienced and would appreciate any feedback!

Middle
  • 19
  • 2
  • Hello @Middle, impressive project but your question seems more like an advertisement and recommendation page for your project than a question. Please check out [how to ask a good question](https://stackoverflow.com/help/how-to-ask) page and re-edit your question. If you don't have an actual question, please delete this post. – Mr PizzaGuy Apr 24 '20 at 00:12
  • 1
    @MrPizzaGuy thanks, i've changed it now. it's more just a personal project as this will never be a website. thanks for that – Middle Apr 24 '20 at 00:20

1 Answers1

1

Avoid painting operations as much as you can.

When you do fillRect(x, y, 1, 1) the browser has to go from the CPU to the GPU once per pixel, and that's very inefficient.

In your case, since you are drawing every pixels on their own, you can simply set all these pixels on an ImageBitmap and put the full image once per frame.

To improve a bit the color setting, I generated an Array of a hundred values before hand, you can make it more granular if you wish.

There might be improvements to do in your Mandelbrot, I didn't checked it, but this would be more suited to CodeReview than StackOverflow.

Here is a simple demo using a 800x600px canvas:

const state = {
  magnificationFactor: 5000,
  imaginaryConstant: 1,
  maxIterations: 20,
  panX: 1,
  panY: 1
};

const canvas = document.getElementById('canvas');
const width = canvas.width = 800;
const height = canvas.height = 600;
const ctx = canvas.getContext('2d');
// the ImageData on which we will draw
const img = new ImageData( width, height );
// create an Uint32 view so that we can set one pixel in one op
const img_data = new Uint32Array( img.data.buffer );

const drawFractal = () => {
  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      const belongsToSet = checkIfBelongsToMandelbrotSet(x / state.magnificationFactor - state.panX, y / state.magnificationFactor - state.panY);
      // setthe value in our ImageData's data
      img_data[ y * width + x] = getColor( belongsToSet );
      }
    }
  // only now we paint
  ctx.putImageData( img, 0, 0 );
};

checkIfBelongsToMandelbrotSet = (x,y) => {
  let realComponentOfResult = x;
  let imaginaryComponentOfResult = y;
  // Set max number of iterations
  for (let i = 0; i < state.maxIterations; i++) {
    const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
    const tempImaginaryComponent = state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
    realComponentOfResult = tempRealComponent;
    imaginaryComponentOfResult = tempImaginaryComponent;
    // Return a number as a percentage
    if (realComponentOfResult * imaginaryComponentOfResult > 5) {
      return (i / state.maxIterations * 100);
    }
  }
  // Return zero if in set
  return 0;
}
// we generate all the colors at init instead of generating every frame
const colors = Array.from( { length: 100 }, (_,i) => {
  if( !i ) { return 0; }
  return hslToRgb( 80/360, 100/100, i/100 );
} );

function getColor( ratio ) {
  if( ratio === 0 ) { return 0; }
  return colors[ Math.round( ratio ) ];
}

function anim() {
  state.magnificationFactor -= 10;
  drawFractal();
  requestAnimationFrame( anim );
}
requestAnimationFrame( anim );


// original by mjijackson.com
// borrowed from https://stackoverflow.com/a/9493060/3702797
function hslToRgb(h, s, l){
    var r, g, b;

    if(s == 0){
        r = g = b = l; // achromatic
    }else{
        var hue2rgb = function hue2rgb(p, q, t){
            if(t < 0) t += 1;
            if(t > 1) t -= 1;
            if(t < 1/6) return p + (q - p) * 6 * t;
            if(t < 1/2) return q;
            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        }

        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }
    // we want 0xAABBGGRR format
    function toHex( val ) {
      return  Math.round( val * 255 ).toString(16);
    }
    return Number( '0xFF' + toHex(b) + toHex(g) + toHex(r) );
}
<canvas id="canvas"></canvas>
Kaiido
  • 123,334
  • 13
  • 219
  • 285