Canvas generate problem with maximum call stack

I am making a script that auto generates planets see codepen for example. But the problem I have is that i want to make it less pixelated and I am having some problems doing that if i make the tiles 70 * 70 and tile size to 10 * 10 pixels it works fine. But i want to have it set to something like tiles 360 * 360 and size to 1 or 2 pixels. But when I try to do that I get maximum call stack error. So I tried to use the requestAnimationFrame but then it take ages to load is there a way to speed up the process?

var tileNum = 0;
    var tiles;
    var colorsLand;
    var colorsWater;
    var size = 360;
    var tileSize = 2;
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");

    window.onload = function () {
        generatePlanet();
    }

    function generatePlanet() {
        tileNum = 0;
        tiles = [{ x: 0, y: 0, land: false }];

        //Retrive colors        

        colorsLand = interpolateColors("rgb(" + getColor(true) + ")", "rgb(" + getColor(true) + ")", 6000);
        colorsWater = interpolateColors("rgb(" + getColor(false) + ")", "rgb(" + getColor(false) + ")", 6000);


        //Creates a array of my tiles and sets either water or land to them and calculates the % of being water/land
        for (var i = 0; i < (size * size); i++) {
            var currentTile = tiles[tiles.length - 1];
            if (currentTile.x <= (size - 1)) {
                var isLand = false;
                if (currentTile.land == true || tiles.length > size && tiles[tiles.length - size].land == true) {
                    isLand = (Math.floor(Math.random() * 100) + 1) > 35;
                }
                else if (currentTile.land == true || tiles.length > size &&
                    (tiles[tiles.length - 1].land == true ||
                        tiles[tiles.length - size].land == true)) {
                    isLand = (Math.floor(Math.random() * 100) + 1) > size;
                }
                else {
                    isLand = (Math.floor(Math.random() * 100) + 1) > 99;
                }
                tiles.push({ x: currentTile.x + 1, y: currentTile.y, land: isLand });
            }
            else {
                tiles.push({ x: 0, y: currentTile.y + 1, land: isLand });
            }
        }
        drawPlanet()
    }


    //retrive a random color if it's a land tile i want it dark water i want light
    function getColor(land) {
        while (true) {
            var r = Math.floor(Math.random() * 256) + 1
            var g = Math.floor(Math.random() * 256) + 1
            var b = Math.floor(Math.random() * 256) + 1
            var hsp = Math.sqrt(
                0.299 * (r * r) +
                0.587 * (g * g) +
                0.114 * (b * b)
            );
            //light color
            if (hsp > 127.5 && land == false) {
                return r + "," + g + "," + b;
            }
            //dark color
            else if (hsp < 127.5 && land == true) {
                return r + "," + g + "," + b;
            }
        }
    }

    //these 2 functions interpolateColor(s) takes 2 colors and gives me 'steps' colors between
    function interpolateColors(color1, color2, steps) {
        var stepFactor = 1 / (steps - 1),
            interpolatedColorArray = [];
        color1 = color1.match(/\d+/g).map(Number);
        color2 = color2.match(/\d+/g).map(Number);

        for (var i = 0; i < steps; i++) {
            interpolatedColorArray.push(interpolateColor(color1, color2, stepFactor * i));
        }
        return interpolatedColorArray;
    }

    function interpolateColor(color1, color2, factor) {
        if (arguments.length < 3) {
            factor = 0.5;
        }
        var result = color1.slice();
        for (var i = 0; i < 3; i++) {
            result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]));
        }
        return result;
    };

    //retrives a random color for land
    function rndLandColor() {
        return 'rgb(' + colorsLand[Math.floor(Math.random() * 5999) + 1] + ')';
    }
    //retrives a random color for water
    function rndWaterColor() {
        return 'rgb(' + colorsWater[Math.floor(Math.random() * 5999) + 1] + ')';
    }

    function drawPlanet() {
        var RAF;
        var i = 0, j = 0;
        function animate() {
            ctx.beginPath();

            //fill in holes in the land that is bigger then 1
            var score = 0;
            if (tiles[tileNum - (size + 1)] !== undefined && tiles[tileNum + (size + 1)] !== undefined) {
                if (tiles[tileNum].land == false) {
                    score++;
                }
                if (tiles[tileNum - 1].land == true) {
                    score++;
                }
                if (tiles[tileNum + 1].land == true) {
                    score++;
                }
                if (tiles[tileNum + (size + 1)].land == true) {
                    score++;
                }
                if (tiles[tileNum - (size + 1)].land == true) {
                    score++;
                }
            }

            if (score >= 3) {
                ctx.fillStyle = rndLandColor();
            }

            //cover single land tiles with water (if water tile is up,down,left and right of this tile)
            else if (
                tiles[tileNum - (size + 1)] !== undefined &&
                tiles[tileNum + (size + 1)] !== undefined &&
                tiles[tileNum - 1].land == false &&
                tiles[tileNum + 1].land == false &&
                tiles[tileNum - (size + 1)].land == false &&
                tiles[tileNum + (size + 1)].land == false) {
                ctx.fillStyle = rndWaterColor();
            }

            //cover single water tiles with land (if land tile is up,down,left and right of this tile)
            else if (
                tiles[tileNum - (size + 1)] !== undefined &&
                tiles[tileNum + (size + 1)] !== undefined &&
                tiles[tileNum - 1].land == true &&
                tiles[tileNum + 1].land == true &&
                tiles[tileNum - (size + 1)].land == true &&
                tiles[tileNum + (size + 1)].land == true) {
                ctx.fillStyle = rndLandColor();
            }
            //cover tile with land
            else if (tiles[tileNum] !== undefined && tiles[tileNum].land == true) {
                ctx.fillStyle = rndLandColor();
            }

            //cover tile with water
            else if (tiles[tileNum] !== undefined && tiles[tileNum].land == false) {
                ctx.fillStyle = rndWaterColor();
            }
            tileNum++;
            ctx.fill();
            ctx.closePath();
            ctx.fillRect(tileSize * j, tileSize * i, tileSize, tileSize);

            j++;
            if (j >= (size + 1)) {
                i += 1;
                j = 0;
                if (i >= (size + 1)) {
                    cancelAnimationFrame(RAF);
                }
            }
            RAF = requestAnimationFrame(function () {
                animate();
            });
        }
        animate();
}
#canvas {
        border: 10px solid #000000;
        border-radius: 50%;
        background-color: aquamarine;
    }

    .container {
        width: 720px;
        height: 720px;
        position: relative;
    }

    .gradient {
        position: absolute;
        height: 730px;
        width: 730px;
        top: 0;
        left: 0;
        border-radius: 50%;
        opacity: 0.8;
    }
<div class="container">
    <img class="gradient" src="https://www.mediafire.com/convkey/1f5a/cgu50lw1ehcp4fq6g.jpg" />
    <canvas id="canvas" width="710" height="710"></canvas>
</div>

Answers:

Answer

Do not use canvas drawing methods to perform pixel art.

Filling a path is a relatively slow operation, to draw pixels through fillRect(), is almost never the correct way.
Instead one should prefer manipulating an ImageData object directly, and paint it on the canvas only once.

If you need to set up a scale, then use an unscaled ImageBitmap, put it on your context and then upscale it using drawImage.

Here is an updated version of your script, where I did apply some not-so minor improvements like not generating colors for out-of-screen pixels, along with this ImageData manipulation technique.
It now runs fast enough to be launched synchronously. But if you need to improve it even more, note that your getColor seems rather inneficient, but I didn't touch it.

var tileNum = 0;
var tiles;
var colorsLand;
var colorsWater;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var tileSize = 2;
canvas.width = canvas.height = 710;
// 'size' should be your grid size, not the actual pixel size painted on screen
var size = Math.ceil(canvas.width / tileSize);


function generatePlanet() {
  tileNum = 0;
  tiles = [{
    x: 0,
    y: 0,
    land: false
  }];

  //Retrive colors        

  colorsLand = interpolateColors(getColor(true), getColor(true), 6000);
  colorsWater = interpolateColors(getColor(false), getColor(false), 6000);

  //Creates a array of my tiles and sets either water or land to them and calculates the % of being water/land
  for (var i = 0; i < (size * size); i++) {
    var currentTile = tiles[tiles.length - 1];
    if (currentTile.x <= (size - 1)) {
      var isLand = false;
      if (currentTile.land == true || tiles.length > size && tiles[tiles.length - size].land == true) {
        isLand = (Math.floor(Math.random() * 100) + 1) > 35;
      } else if (currentTile.land == true || tiles.length > size &&
        (tiles[tiles.length - 1].land == true ||
          tiles[tiles.length - size].land == true)) {
        isLand = (Math.floor(Math.random() * 100) + 1) > size;
      } else {
        isLand = (Math.floor(Math.random() * 100) + 1) > 99;
      }
      tiles.push({
        x: currentTile.x + 1,
        y: currentTile.y,
        land: isLand
      });
    } else {
      tiles.push({
        x: 0,
        y: currentTile.y + 1,
        land: isLand
      });
    }
  }
  drawPlanet()
}


//retrive a random color if it's a land tile i want it dark water i want light
function getColor(land) {
  while (true) {
    var r = Math.floor(Math.random() * 256) + 1
    var g = Math.floor(Math.random() * 256) + 1
    var b = Math.floor(Math.random() * 256) + 1
    var hsp = Math.sqrt(
      0.299 * (r * r) +
      0.587 * (g * g) +
      0.114 * (b * b)
    );
    //light color
    if (hsp > 127.5 && land == false) {
      return [r,g,b];
    }
    //dark color
    else if (hsp < 127.5 && land == true) {
      return [r,g,b];
    }
  }
}

//these 2 functions interpolateColor(s) takes 2 colors and gives me 'steps' colors between
function interpolateColors(color1, color2, steps) {
  var stepFactor = 1 / (steps - 1),
    interpolatedColorArray = [];

  for (var i = 0; i < steps; i++) {
    interpolatedColorArray.push(toUint32AARRGGBB(interpolateColor(color1, color2, stepFactor * i)));
  }
  return interpolatedColorArray;
}
function toUint32AARRGGBB(arr) {
  return Number('0xFF' + arr.map(toHexString2).join(''))
}
function toHexString2(val) {
  return val.toString(16)
    .padStart(2, '0'); // padStart may need a polyfill
}
function interpolateColor(color1, color2, factor) {
  if (arguments.length < 3) {
    factor = 0.5;
  }
  var result = color1.slice();
  for (var i = 0; i < 3; i++) {
    result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]));
  }
  return result;
};

//retrives a random color for land
function rndLandColor() {
  return colorsLand[Math.floor(Math.random() * 5999) + 1];
}
//retrives a random color for water
function rndWaterColor() {
  return colorsWater[Math.floor(Math.random() * 5999) + 1];
}

// now drawing synchronously:
function drawPlanet() {

  var gridsize = size;
  var rad = gridsize / 2;
  
  // generate an ImageData, the size of our pixel grid
  var imgData = new ImageData(gridsize, gridsize);
  // work directly on Uint32 values (0xAARRGGBB on LittleEndian)
  var data = new Uint32Array(imgData.data.buffer);

  var score, y, x;
  for (y = 0; y < gridsize; y++) {
    for (x = 0; x < gridsize; x++) {
      score = 0;
      
      // if we are outside of the inner area
      if (Math.hypot(rad - x, rad - y) > rad + 2) {
        tileNum++;
        continue;
      }
      //fill in holes in the land that is bigger then 1
     if (tiles[tileNum - (gridsize + 1)] !== undefined && tiles[tileNum + (size + 1)] !== undefined) {
          if (tiles[tileNum].land == false) {
              score++;
          }
          if (tiles[tileNum - 1].land == true) {
              score++;
          }
          if (tiles[tileNum + 1].land == true) {
              score++;
          }
          if (tiles[tileNum + (gridsize + 1)].land == true) {
              score++;
          }
          if (tiles[tileNum - (gridsize + 1)].land == true) {
              score++;
          }
      }

      if (score >= 3) {
          color = rndLandColor();
      }

      //cover single land tiles with water (if water tile is up,down,left and right of this tile)
      else if (
          tiles[tileNum - (gridsize + 1)] !== undefined &&
          tiles[tileNum + (gridsize + 1)] !== undefined &&
          tiles[tileNum - 1].land == false &&
          tiles[tileNum + 1].land == false &&
          tiles[tileNum - (gridsize + 1)].land == false &&
          tiles[tileNum + (gridsize + 1)].land == false) {
          color = rndWaterColor();
      }

      //cover single water tiles with land (if land tile is up,down,left and right of this tile)
      else if (
          tiles[tileNum - (gridsize + 1)] !== undefined &&
          tiles[tileNum + (gridsize + 1)] !== undefined &&
          tiles[tileNum - 1].land == true &&
          tiles[tileNum + 1].land == true &&
          tiles[tileNum - (gridsize + 1)].land == true &&
          tiles[tileNum + (gridsize + 1)].land == true) {
          color = rndLandColor();
      }
      //cover tile with land
      else if (tiles[tileNum] !== undefined && tiles[tileNum].land == true) {
          color = rndLandColor();
      }

      //cover tile with water
      else if (tiles[tileNum] !== undefined && tiles[tileNum].land == false) {
          color = rndWaterColor();
      }
      tileNum++;
      data[(y * gridsize) + x] = color;
    }
  }
  // all done populating the ImageData
  // put it on the context at scale(1,1)
  ctx.putImageData(imgData, 0, 0);
  // remove antialiasing
  ctx.imageSmoothingEnabled = false;
  // up-scale
  ctx.scale(tileSize, tileSize);
  // draw the canvas over itself
  ctx.drawImage(ctx.canvas, 0, 0);
  ctx.setTransform(1, 0, 0, 1, 0, 0);
}

generatePlanet();
#canvas {
  border: 10px solid #000000;
  border-radius: 50%;
  background-color: aquamarine;
}

.container {
  width: 720px;
  height: 720px;
  position: relative;
}

.gradient {
  position: absolute;
  height: 730px;
  width: 730px;
  top: 0;
  left: 0;
  border-radius: 50%;
  opacity: 0.8;
}
<div class="container">
  <img class="gradient" src="https://www.mediafire.com/convkey/1f5a/cgu50lw1ehcp4fq6g.jpg" />
  <canvas id="canvas" width="710" height="710"></canvas>
</div>

Now, if I were in your position, I think I would even start looking somewhere else completely. For what you want to do, it seems that some noise generator could be more efficient with a more realistic output.
There is one such noise generator available in SVG filters, and hence accessible to Canvas2D API, however I have to admit that controlling it is not that easy.
But if you wish to take a look at it, here is a rough playground:

const controls = new Set();

function randColor() {
  return '#' + (Math.floor((Math.random()*0xFFFFFF)))
    .toString(16)
    .padStart(6, 0);
}
function makeInput(type, options) {
  return Object.assign(document.createElement('input'), {type}, options);
}
class Control {
  constructor() {
    this.color = makeInput('color', {value: randColor()});
    this.freq = makeInput('range', {min: 0.0001, max:1, step: 0.0001, value: Math.random() / 20});
    this.numOctaves = makeInput('range', {min: 1, max:10, step: 1, value: 7});
    this.opacity = makeInput('range', {min:0.01, max:1, step: 0.001, value:1});
    this.seed = Math.random() * 1000;
    const remover = document.createElement('span');
    remover.textContent = 'x';
    remover.classList.add('remover');
    const container = document.createElement('div');
    container.classList.add('control');
    
    container.append(
      "color: ", this.color,
      "baseFrequency: ", this.freq,
      "numOctaves: ", this.numOctaves,
      "opacity", this.opacity,
      remover
    );
    
    document.querySelector('.controls').append(container);
    
    remover.onclick = e => {
      container.remove();
      controls.delete(this);
      draw();
    };
    this.color.oninput = this.freq.oninput =  this.numOctaves.oninput = this.opacity.oninput = draw;
  }
}
for(let i=0; i<3; i++) {
  controls.add(new Control());
}

const main = c.getContext('2d');
const ctx = c.cloneNode().getContext('2d');

main.arc(c.width/2, c.height/2, Math.min(c.width, c.height)/2,0,Math.PI*2);


draw();

add_control.onclick = e => {
  controls.add(new Control());
  draw();
}

function draw() {

  main.globalCompositeOperation = 'source-over';
  main.clearRect(0,0,c.width,c.height);

  controls.forEach(control => {
    ctx.globalCompositeOperation = 'source-over';
    ctx.filter = "none";
    ctx.clearRect(0,0,c.width,c.height);
    
    // update <filter>
    turb.setAttribute('seed', control.seed);
    turb.setAttribute('baseFrequency', control.freq.value);
    turb.setAttribute('numOctaves', control.numOctaves.value);
    // draw black and transp
    
    ctx.filter = "url(#myFilter)"
    ctx.fillRect(0,0,c.width, c.width);
    // do the composition with solid color
    ctx.filter = "none"
    ctx.fillStyle = control.color.value;
    ctx.globalCompositeOperation = 'source-in'
    ctx.fillRect(0,0,c.width, c.width);
    main.globalAlpha = control.opacity.value;
    // draw on visible context
    main.drawImage(ctx.canvas, 0,0)
    main.globalAlpha = 1;
  });
  // cut-out as a circle
  main.globalCompositeOperation = 'destination-in';
  main.fill()
}
.control {
  display: inline-block;
  border: 1px solid;
  padding: 6px;
  position: relative
}
.control input {
  display: block;
}
.control span {
  position: absolute;
  top: 6px;
  right: 6px;
  cursor: pointer;
}

#canvas {
  border: 10px solid #000000;
  border-radius: 50%;
  background-color: aquamarine;
}

.container {
  width: 360px;
  height: 360px;
  position: relative;
}

.gradient {
  position: absolute;
  height: 360px;
  width: 360px;
  top: 0;
  left: 0;
  border-radius: 50%;
  opacity: 0.8;
}
<div class="controls">
  <button id="add_control">add new layer</button><br>
</div>
<div class="container">
<canvas id="c" width="360" height="360"></canvas>
<svg>
  <filter id="myFilter">
    <feTurbulence type="fractalNoise" baseFrequency="0.045"
        id="turb" result="turb"/>
     <feComponentTransfer in="turb" result="contrast">
       <feFuncR type="linear" slope="1.6" intercept="-0.15"/>
       <feFuncG type="linear" slope="1.6" intercept="-0.15"/>
       <feFuncB type="linear" slope="1.6" intercept="-0.15"/>
     </feComponentTransfer>
    <feColorMatrix in="contrast"
      type="luminanceToAlpha" result="alpha"/>

  </filter>
</svg>
<img class="gradient" src="https://www.mediafire.com/convkey/1f5a/cgu50lw1ehcp4fq6g.jpg" />
</div>

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.