画布会产生最大调用堆栈数的问题

时间:2019-05-26 13:59:40

标签: javascript canvas requestanimationframe

我正在制作一个自动生成行星的脚本,请参见codepen for example。 但是我遇到的问题是我想使其像素化程度降低,如果我将图块的尺寸设置为70 * 70,并将图块的尺寸设置为10 * 10像素,则会出现问题。但我想将其设置为类似图块360 * 360并将大小设置为1或2像素。但是当我尝试这样做时,我得到最大的调用堆栈错误。因此,我尝试使用requestAnimationFrame,但是加载它需要花很长时间,有没有办法加快该过程?

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>

1 个答案:

答案 0 :(得分:3)

请勿使用画布绘制方法执行像素画。

填充路径是一个相对较慢的操作,通过 fillRect()绘制像素几乎是不正确的方法。
相反,应该选择直接操作ImageData对象,并将其仅在画布上绘制一次。

如果需要设置比例,请使用未缩放的ImageBitmap,将其放在上下文中,然后使用drawImage对其进行放大。

这是脚本的更新版本,我在其中做了一些不太细的改进,例如不为屏幕外像素生成颜色,以及此ImageData操作技术。
现在,它运行得足够快,可以同步启动。但是,如果您需要进一步改善它,请注意,您的getColor似乎效率很低,但是我没有碰它。

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>

现在,如果我处于您的位置,我想我甚至会开始完全寻找其他地方。对于您想做的事情,似乎有些噪声生成器在输出更逼真的情况下会更高效。
SVG过滤器中有一个这样的noise generator,因此可以被Canvas2D API访问,但是我不得不承认,控制它并不是那么容易。
但是,如果您想看一看,这里是一个崎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>