2
\$\begingroup\$

I want to reduce the big time it needed for my mandelbrot set generator to work. I'm working this for already a week. I tried to reduce the canvas size from 400 by 400 to 200 by 200 and the speed increased a bit. Tried using multithread.js but still slow...

Here's the JavaScript code:

//Create the canvas, then collect the height and width
var a, b, c, canvas, rect;

//zoomon click
var zoomOnClick = true;

//if running
var running = false;

//to prevent error during loading, make sure that
//the canvas is loaded first before calling any methods
canvas = document.getElementById("paper");
c = canvas.getContext("2d");
a = canvas.width;
b = canvas.height;

//when canvas is clicked, call drawOnClick function
canvas.onclick = function(e){
  setTimeout(function(){
    drawOnClick(e);
  }, 10)
}

//changes the mandelbrot set based on mouse clicks
function drawOnClick(e){
rect = canvas.getBoundingClientRect()
  if(zoomOnClick){
    var mx = panX + (e.clientX - rect.left) / zooms;
    var my = panY + (e.clientY - rect.top) / zooms;
    zooms *= zf;
    panX = mx - ((e.clientX - rect.left) / zooms);
    panY = my - ((e.clientY - rect.top) / zooms);
  }else{
    var mx = panX + (e.clientX - rect.left) / zooms;
    var my = panY + (e.clientY - rect.top) / zooms;
    zooms /= zf;
    panX = mx - ((e.clientX - rect.left) / zooms);
    panY = my - ((e.clientY - rect.top) / zooms);
  }

  pan = (panX + 2 / zooms) - (panX - 1 / zooms);

  document.getElementById("xa").value = panX;
  document.getElementById("ya").value = panY;
  document.getElementById("za").value = zooms;

  pallete.setNumberRange(0,maxI);
  if(0 < zooms && zooms < 50){
    pallete.setNumberRange(0, 50);
    maxI = 50;
  }else if(50 < zooms && zooms < 100){
    pallete.setNumberRange(0,100);
    maxI = 100;
  }else if(100 < zooms && zooms < 1000){
    pallete.setNumberRange(0,255);
    maxI = 255;
  }else if(1000 < zooms && zooms < 10000){
    pallete.setNumberRange(0,500);
    maxI = 500;
  }else if(10000 < zooms && zooms < 100000){
    pallete.setNumberRange(0, 750);
    maxI = 750;
  }else if(100000 < zooms && zooms < 1000000){
    pallete.setNumberRange(0, 1000);
    maxI = 1000;
  }else if(1000000 < zooms && zooms < 10000000){
    pallete.setNumberRange(0, 2500);
    maxI = 2500;
  }

  show();
  requestAnimationFrame(abortRun);
  requestAnimationFrame(startRun);
}
//when + was clicked above the canvas
function plus(){
  zoomOnClick = true;
}
//same here
function minus(){
  zoomOnClick = false;
}

//aborts startRun
function abortRun(){
  if(running){running = false}
}

//starts calling mandelbrot
function startRun(){
  //(function(){
      setTimeout(function(){running = true}, 10);
      setTimeout(function(){mandelbrot(zooms, panX, panY, 8)}, 10);
      setTimeout(function(){mandelbrot(zooms, panX, panY, 5)}, 20);
      setTimeout(function(){mandelbrot(zooms, panX, panY, 1)}, 30);
  //})()
}
//in the instance, create all thngs
try{
//pan is the length of scroll
//zooms is the current number of zoom
//panX is the upper left Corner
//panY is the bottom left Corner
//zf is the increase factor in the zoom
//maxI is the total number of iteration
//per complex number
//create pallete to color mandelbrot by
//using rainbowvis.js
var pan, zooms, panX, panY, zf = 1.5, maxI = 50, ticks;

var pallete = new Rainbow();
pallete.setSpectrum("#000764","#206bcb","#edffff","#ffaa00","#000200");
pallete.setNumberRange(0,maxI);

//function that draws the mandelbrot set
// based on current zoom, panX, panY and scale
function mandelbrot(zm, panX, panY, scale){
//cncel run in some case
if(!running){
    return;
}
if(scale === 1){
  running = false;
}
scale = scale || 1;
//reset ticks
ticks = 0;
//px - Canvas x
//py - canvas y
//x - real x
//y - imaginary y

var px, py, x, y; 

//loop from y's, then loop all x's

for(px = 0; px < a; px+=scale){
  for(py = 0; py < b; py+=scale){
    //zoom factors
    x0 = panX + px/zm;
    y0 = panY + py/zm;

    var x = 0;
    var y = 0;

    var i = 0;
    var xtemp;

    while (x*x + y*y <= 4  &&  i < maxI) {
      ticks++
      xtemp = x*x - y*y + x0
      y = 2*x*y + y0
      x = xtemp
      i = i + 1
    }

    //coloring
    var shade = pallete.colourAt(i);
    c.fillStyle = "#"+shade;
    c.fillRect(px,py,scale, scale);
    }
}
console.log("Total ticks: " + ticks + ", based on scale " + scale);
}

//reset
function work(){
document.getElementById("xa").value = -2.5;
document.getElementById("ya").value = -2;
document.getElementById("za").value = a/4;

pan = 0.01;
zooms = a / 4;
panX = -2.5;
panY = -2.0;
zf = 1.5;
maxI = 50;
pallete.setSpectrum("#000764","#206bcb","#edffff","#ffaa00","#000200");
pallete.setNumberRange(0,maxI);

show();
abortRun();
startRun();
}

//left to right scroll adjustment
function xScroll(n){
var temp = n ? parseFloat(document.getElementById("xa").value) + pan : parseFloat(document.getElementById("xa").value) - pan;
document.getElementById("xa").value = temp;
panX = temp;
show();
abortRun();
startRun();
}

//top to bottom scroll adjustment
function yScroll(n){
var temp = n ? parseFloat(document.getElementById("ya").value) + pan : parseFloat(document.getElementById("ya").value) - pan;
document.getElementById("ya").value = temp;
panY = temp;
show();
abortRun();
startRun();
}

//zoom in function
function zoomIn(){
zooms = zooms + zf;
pan = (panX + 2 / zooms) - (panX - 1 / zooms);
document.getElementById("za").value = zooms;

if(0 < zooms && zooms < 100){
pallete.setNumberRange(0,100);
maxI = 100;
}else if(100 < zooms && zooms < 1000){
pallete.setNumberRange(0,255);
maxI = 255;
}else if(1000 < zooms && zooms < 10000){
pallete.setNumberRange(0,500);
maxI = 500;
}

show();
abortRun();
startRun();
}

//zoom out function
function zoomOut(){
zooms = zooms - zf;
pan = (panX + 2 / zooms) - (panX - 1 / zooms)
document.getElementById("za").value = zooms;

if(zooms < 100){
pallete.setNumberRange(0,50);
maxI = 50;
}else if(zooms > 100 && zooms < 1000){
pallete.setNumberRange(0,100);
maxI = 100;
}else if(zooms > 1000 && zooms < 10000){
pallete.setNumberRange(0,255);
maxI = 255;
}
show();
abortRun();
startRun();
}

//adjust zoomfactor
function zoomFactor(){
var temp = document.getElementById("zf").value;
zf = parseInt(temp);
show();
}

//adjust maxI
function changeMaxI(){
  var temp = document.getElementById("mi").value;
  maxI = parseInt(temp);
  pallete.setNumberRange(0,maxI);
  show();
  abortRun();
  startRun();
}

//adjust pallete
function changePallete(){
  var temp = (document.getElementById("plt").value).split(" ");
  if(temp.length < 3){
    alert(" Please enter more colors ");
    return
  }
  pallete.setSpectrumByArray(temp);
  show();
  abortRun();
  startRun();
}

//show details
function show(){
var temp = "Scroll: " + pan + "<br /> Current zoom: " + zooms + "<br /> topLeftX: " +  panX + "<br /> topRightY: " + panY + "<br /> zoom factor: " +  zf + "<br /> max iterations of loop: " + maxI;
document.getElementById("dtls").innerHTML = temp;
}

/*favorable zoom
-0.373346235978374
-0.6582261932152258
7000

-0.3618206208864465
-0.6453957620586814
155300315925100
*/
//about function
function about(){
  alert("A mandelbrot set generator in javascript created by pvzzombs")
  console.log("A mandelbrot set generator in javascript created by pvzzombs");
}
}catch(e){

throw "Error: " + e ;

}

Here's a preview link.

EDIT:

The javascript libraries I used are rainbow-vis.js,

and multithread.js

Edit 5/6/19

The link above is now working :-). I updated the code and I need more reviews from here. My mandelbrot set viewer is slow on operating system like android, it takes few seconds before it completes. But in operating system like windows, my mandelbrot set viewer just takes fraction of seconds.

Thanks.

\$\endgroup\$
  • \$\begingroup\$ Do I see correctly that you do not use var r anywhere? \$\endgroup\$ – ChatterOne Mar 28 at 14:36
  • 1
    \$\begingroup\$ The linked page does not do anything. And what libraries are you using? there are many external references in the code snippet you provided, guessing what they are or how they works is not going to make for a good review. \$\endgroup\$ – Blindman67 Mar 28 at 16:06
4
\$\begingroup\$

What is Slow?

It is hard to know what people mean by slow. Your expectation may be comparable to a good C multi threaded Mandelbrot viewer zooming at 60fps, and slow means your code is more like 30-20fps.

So all I can do is give you some pointers in regard to performance.

Use ctx.getImageData to create an array to hold the pixels as 32Bit ints. Use a Uint32Array to hold the pallet colors so you can move them to the image pixels in one step.

In performance code don't create and then dump arrays. var arr = [] has a lot of overhead. so does a.push(1) even dumping an array has overhead arr = undefined

Don't do what is not needed inside performance code. At the very core of the render you had a completely redundant calculation that amounted to about 1/3 of the calculations done in the inner loop. that one line would have been 20% of the CPU cost (if you had all else optimal)

A slight improvement

Below is a quick example of the above points. I don't know if its the type of improvement you are looking for, (there are faster algorithms but they are complex) For a single thread JS per pixel Mandelbrot its about as fast as it can be done using the standard method.

const blindmansPallet = {
    color: (r, g, b, a = 255) => ({r, g, b, a}),
    createLookup(range, size) {
        var idx = 0;
        blindmansPallet.range = range;
        blindmansPallet.int32 = new Uint32Array(size);
        const int8 = new Uint8ClampedArray(blindmansPallet.int32.buffer);
        const rangeStep = size / (range.length -1);
        for(let i = 0; i < size; i ++) {
            const low = i / rangeStep | 0;
            const high = low + 1;
            const unitDistBetweenRange = (i - (low * rangeStep)) / rangeStep;
            const u = unitDistBetweenRange;
            const lRGBA = range[low];
            const hRGBA = range[high];
            int8[idx++] = ((hRGBA.r ** 2 - lRGBA.r ** 2) * u + lRGBA.r ** 2) ** 0.5;
            int8[idx++] = ((hRGBA.g ** 2 - lRGBA.g ** 2) * u + lRGBA.g ** 2) ** 0.5;
            int8[idx++] = ((hRGBA.b ** 2 - lRGBA.b ** 2) * u + lRGBA.b ** 2) ** 0.5;
            int8[idx++] = ((hRGBA.a ** 2 - lRGBA.a ** 2) * u + lRGBA.a ** 2) ** 0.5;
        }
        return blindmansPallet.int32;
    }
};

;(() => {
    var panX = -3.5, panY = -1.5, zoom = 50, maxI = 100;
    const pallet = blindmansPallet.createLookup([
            blindmansPallet.color(0x00, 0x07, 0x64), 
            blindmansPallet.color(0x20, 0x6b, 0xcb), 
            blindmansPallet.color(0xed, 0xff, 0xff), 
            blindmansPallet.color(0xff, 0xaa, 0x00), 
            blindmansPallet.color(0x00, 0x02, 0x00)
        ], maxI
    );

    const ctx = canvas.getContext("2d");
    const W = canvas.width, H = canvas.height;
    const imgData = ctx.getImageData(0,0,W,H);
    const pixels = new Uint32Array(imgData.data.buffer);
    canvas.addEventListener("click",(e) => {
       var mx = panX + e.clientX / zoom;
       var my = panY + e.clientY / zoom;
       zoom *= 1.20;
       panX = mx - (e.clientX / zoom);
       panY = my - (e.clientY / zoom);
       draw(zoom, panX, panY);
    });

    draw(zoom, panX, panY);
    function draw(zoom, panX, panY, w = W, h = H, maxI = 100) {
        var px,py,x,y,xOld,yOld,xNew,yNew,i;
        for (px = 0; px < w; px++) {
            for (py = 0; py < h; py++) {
                xOld = x = panX + px / zoom;
                yOld = y = panY + py / zoom;
                for (i = 0; i < maxI; i++) {
                    xNew = (xOld * xOld) - (yOld * yOld) - x;
                    yNew = (2 * xOld * yOld) - y;
                    if (xNew * xNew + yNew * yNew > 4) { break }
                    xOld = xNew;
                    yOld = yNew;
                }
                pixels[px + py * W] = pallet[i];
            }
        }
        ctx.putImageData(imgData,0,0);
    }
})();
body {
    font-family: verdana;
    user-select: none;    
    -moz-user-select: none;    
}
canvas {
   cursor: crosshair;
   border: 1px solid black;
}
<canvas id="canvas" width="400" hieght="250" style="cursor:crosshair"></canvas>
<div>Click to image to zoom. 400px by 250px single thread</div>

More performance

For better you can use web-workers and share the typed arrays.

You can use previous rendering to estimate where to add detail as you zoom in (a type of quad tree renderer).

You can have instant refresh. That is, rather than wait for a frame to finish, as soon as a pan/zoom request is made, you drop the current processing and start on the new frame (this gives the illusion of better performance)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.