<div id="gui"></div>
html, body {
  margin: 0;
  padding: 0;
}

canvas {
  display: block;
  z-index: -1000;
}

#gui {
  left: 0;
  position: fixed;
  top: 0;
  z-index: 9999999;
}
let terrainIncrement = 0.005;
let r = 35;
let k = 30;
let grid = [];
let perlinGrid = [];
let terrainGrid = [];
let inc = 0.15; // bigger values = more variation in clusters
let w = r / Math.sqrt(2);
let active = [];
let cols, rows;
let trees = [];
let options = {
  treeCount: 0,
  speed: 60,
  mapSize: r, 
  gridWidth: 2,
  terrainNoise: terrainIncrement,
  treeNoise: inc,
  pine: true,
  oak: true,
  apple: true,
  water: true,
  grass: true,
  createNewForest: function(){
    resizeCanvas(windowWidth, windowHeight);
    setupTerrain();
    setupPoissonDiskSampling();
  }
}

function setup() {
  createCanvas(windowWidth, windowHeight);
  strokeWeight(4);

  setupTerrain();
  
  setupPoissonDiskSampling();
}

function draw() {
  frameRate(options['speed']);
  runPoissonDiskSampling();

  for (let i = 0; i < trees.length; i++) {
    if (trees[i]) {
      trees[i].render();
      trees[i].update();
    }
  }
}

function setupTerrain() {
  terrainGrid = [];
  loadPixels();
  let yoff = Math.random() * 100
  for (let y = 0; y < height + r; y++) { // add some offscreen overlapping trees near the bottom
      let xoff = 0
      for (let x = 0; x < width + r; x++) {
        let noiseValue = noise(xoff, yoff)
        let terrainColor = getTerrainColor(noiseValue)
        terrainGrid[x + y * width] = noiseValue
        set(x, y, terrainColor)
        xoff += options['terrainNoise']
      }
      yoff += options['terrainNoise']
  }
  updatePixels();
}

function setupPoissonDiskSampling() {
  r = options['mapSize'];
  k = 30;
  grid = [];
  perlinGrid = [];
  inc = options['treeNoise']; // bigger values = more variation in clusters
  w = r / Math.sqrt(options['gridWidth']);
  active = [];
  cols, rows;
  trees = [];
  options['treeCount'] = 0;
  // setup grid
  cols = floor(width / w) + 1; // add an extra column to create a more natural looking overlap effect near the right side of the window
  rows = floor(height / w) + 1; // add an extra row to create a more natural looking overlap effect near the bottom of the window
  for (let i = 0; i < cols * rows; i++) {
    grid[i] = undefined;
  }
  
  // setup perlin noise grid for tree clustering
  let rowOff = Math.random() * 100
  for (let row = 0; row < rows; row++) {
      let colOff = 0
      for (let col = 0; col < cols; col++) {
        let noiseValue = noise(colOff, rowOff)
        perlinGrid[col + row * cols] = noiseValue
        colOff += options['treeNoise']
      }
      rowOff += options['treeNoise']
  }
  
  // initialize starting point
  let x = width * 0.5;
  let y = height * 0.5;
  let i = floor(x / w);
  let j = floor(y / w);
  let pos = createVector(x, y);
  grid[i + j * cols] = pos;
  active.push(pos);
  let sampleInWater = options['water'] && terrainGrid[Math.round(pos.x) + Math.round(pos.y) * width] >= 0.56
  if (!sampleInWater) {
    addTree(pos.x, pos.y, i, j, r)
  }
}

function addTree(xPos, yPos, col, row, radius) {
  let p = options['pine']
  let a = options['apple']
  let o = options['oak']
  yPos -= (0.5 * radius)
  let newTree;
  if (p && a && o) {
    if (perlinGrid[col + row * cols] < 0.4) {
      newTree = new PineTree(xPos, yPos, radius);
    } else if (perlinGrid[col + row * cols] < 0.55) {
      newTree = new OakTree(xPos, yPos, radius);
    } else {
      newTree = new AppleTree(xPos, yPos, radius);
    }
  } else if (p && a && !o) {
    if (perlinGrid[col + row * cols] < 0.5) {
      newTree = new PineTree(xPos, yPos, radius);
    } else {
      newTree = new AppleTree(xPos, yPos, radius);
    }
  } else if (p && !a && o) {
    if (perlinGrid[col + row * cols] < 0.5) {
      newTree = new PineTree(xPos, yPos, radius);
    } else {
      newTree = new OakTree(xPos, yPos, radius);
    }
  } else if (p && !a && !o) {
    newTree = new PineTree(xPos, yPos, radius);
  } else if (!p && a && o) {
    if (perlinGrid[col + row * cols] < 0.5) {
      newTree = new AppleTree(xPos, yPos, radius);
    } else {
      newTree = new OakTree(xPos, yPos, radius);
    }
  } else if (!p && a && !o) {
    newTree = new AppleTree(xPos, yPos, radius);
  } else if (!p && !a && o) {
    newTree = new OakTree(xPos, yPos, radius);
  }
  if (p || a || o) {
    trees.push(newTree)
  }
}

function runPoissonDiskSampling() {
  if (active.length > 0) {
    let randIndex = floor(random(active.length));
    let pos = active[randIndex];
    let found = false;
    for (let n = 0; n < k; n++) {
      let sample = p5.Vector.random2D();
      let m = random(r, 2 * r);
      sample.setMag(m);
      sample.add(pos);
  
      let col = floor(sample.x / w);
      let row = floor(sample.y / w);
       
      if (col > -1 && row > -1 && col < cols && row < rows && !grid[col + row * cols]) {
        let ok = true;
        for (let i = -1; i <= 1; i++) {
          for (let j = -1; j <= 1; j++) {
            let index = (col + i) + (row + j) * cols;
            let neighbor = grid[index];
            if (neighbor) {
              let d = p5.Vector.dist(sample, neighbor);
              if (d < r) {
                ok = false;
              }
            }
          }
        }
        if (ok) {
          found = true;
          grid[col + row * cols] = sample;
          active.push(sample);
          options['treeCount'] += 1
          let sampleInWater = options['water'] && terrainGrid[Math.round(sample.x) + Math.round(sample.y) * width] >= 0.56
          if (!sampleInWater) {
            addTree(sample.x, sample.y, col, row, r)
          }
          trees.sort((a, b) => a.position.y - b.position.y)
          break;
        }
      }
    }
  
    if (!found) {
      active.splice(randIndex, 1);
    }
  }
}

function getTerrainColor(noiseValue) {
  let green = color(140, 151, 140)
  let brown = color(237, 201, 175)
  let blue = color(140, 140, 251)
  if (options['water'] && options['grass']) {
    if (noiseValue < 0.54) {
      return green
    } else if (noiseValue < 0.6) {
        return brown
    } else {
        return blue
    }
  } else if (options['water'] && !options['grass']) {
    if (noiseValue < 0.6) {
        return brown
    } else {
        return blue
    }
  } else if (!options['water'] && options['grass']) {
    if (noiseValue < 0.6) {
        return green
    } else {
        return brown
    }
  } else {
    return color(237, 201, 175)
  }
}

class Tree {
  constructor(x, y, size) {
    this.position = createVector(x, y);
    this.size = size;
    this.opacityFadeIn = 0;
  }

  update() {
    if (this.opacityFadeIn < 255) {
      this.opacityFadeIn += 5;
    }
  }
}

class PineTree extends Tree {
  constructor(x, y, size) {
    super(x, y, size * 0.75)
  }

  render() {
    let { x, y } = this.position;
    let size = map(this.opacityFadeIn, 0, 255, 0, this.size);

    let trunkWidth = size * 0.25;
    let trunkHeight = size;
    let leavesWidth = size;
    let leavesOverhang = (size - trunkWidth) * 0.5;

    let leavesX1 = x - leavesOverhang; // left point of triangle
    let leavesX2 = x + trunkWidth + leavesOverhang; // right point of triangle
    let leavesX3 = x + (trunkWidth * 0.5); // middle and top of triangle

    push();
    noStroke();
    fill(101, 67, 33); // dark brown
    rect(x, y, trunkWidth, trunkHeight);
    fill(0, 100, 0); // dark green
    triangle(leavesX1, y + (size * 0.75), leavesX2, y + (size * 0.75), leavesX3, y - (size * 0.5));
    triangle(leavesX1, y + (size * 0.5), leavesX2, y + (size * 0.5), leavesX3, y - (size * 0.5));
    triangle(leavesX1, y + (size * 0.25), leavesX2, y + (size * 0.25), leavesX3, y - (size * 0.5));
    pop();
  }
}

class OakTree extends Tree {
  constructor(x, y, size) {
    super(x, y, size * 0.5)
  }

  render() {
    let { x, y } = this.position;
    let size = map(this.opacityFadeIn, 0, 255, 0, this.size);

    const trunkWidth = size * 0.25;
    const trunkHeight = size;
    const leavesWidth = size;
    const leavesOverhang = (size - trunkWidth) * 0.3;

    const leavesX1 = x - leavesOverhang; // left point of triangle
    const leavesX2 = x + trunkWidth + leavesOverhang; // right point of triangle
    const leavesX3 = x + (trunkWidth * 0.5); // middle and top of triangle

    push();
    noStroke();
    fill(166, 94, 46); // orange brown
    rect(x, y, trunkWidth, trunkHeight);
    fill(10, 140, 10) // dark green
    circle(x - leavesOverhang, y + (size * 0.1), trunkWidth * 3);
    circle(x + trunkWidth + leavesOverhang, y + (size * 0.1), trunkWidth * 3);
    // circle(x + (trunkWidth * 0.5), y - size, trunkWidth * 3);
    fill(60, 179, 113); // medium sea green
    circle(x + (trunkWidth * 0.5), y - (size * 0.5), trunkWidth * 5);
    pop();
  }
}

class AppleTree extends Tree {
  constructor(x, y, size) {
    super(x, y, size * 0.5)
  }

  render() {
    let { x, y } = this.position;
    let size = map(this.opacityFadeIn, 0, 255, 0, this.size);

    const trunkWidth = size * 0.5;
    const trunkHeight = size;
    const leavesWidth = size;
    const leavesOverhang = (size - trunkWidth) * 0.5;

    const leavesX1 = x - leavesOverhang; // left point of triangle
    const leavesX2 = x + trunkWidth + leavesOverhang; // right point of triangle
    const leavesX3 = x + (trunkWidth * 0.5); // middle and top of triangle

    push();
    noStroke();
    fill(100, 69, 34); // orange brown
    rect(x, y, trunkWidth, trunkHeight);
    fill(50, 205, 50);
    circle(x + (trunkWidth * 0.5), y - (size * 0.25), size * 1.5);
    circle(x - trunkWidth * 0.5, y + (size * 0.25), size * 0.5);
    circle(x + trunkWidth * 1.5, y + (size * 0.25), size * 0.5);
    fill(255, 98, 84); // red
    circle(x, y, size * 0.35);
    circle(x + trunkWidth * 0.5, y - trunkHeight * 0.5, size * 0.35);
    circle(x + trunkWidth, y, size * 0.35);
    pop();
  }
}

gui = new dat.GUI( { autoPlace: false } )
gui.add( options, 'treeCount' ).name( 'Tree Count' ).listen();
gui.add( options, 'speed' ).min( 0 ).max( 60 ).step( 1 ).name( 'Speed' );
gui.add( options, 'mapSize' ).min( 5 ).max( 100 ).step( 1 ).name( 'Zoom' );
gui.add( options, 'gridWidth' ).min( 0.5 ).max( 9 ).step( 0.1 ).name( 'Tree Density' );
gui.add( options, 'terrainNoise' ).min( 0 ).max( 0.015 ).step( 0.001 ).name( 'Terrain Noise' );
gui.add( options, 'treeNoise' ).min( 0 ).max( 0.5 ).step( 0.001 ).name( 'Tree Noise' );
gui.add( options, 'pine' ).name( 'Pine Trees?' )
gui.add( options, 'oak' ).name( 'Oak Trees?' )
gui.add( options, 'apple' ).name( 'Apple Trees?' )
gui.add( options, 'water' ).name( 'Water?' )
gui.add( options, 'grass' ).name( 'Grass?' )

gui.add( options, 'createNewForest' ).name( 'Create New Forest' );
customContainer = document.getElementById( 'gui' );
customContainer.appendChild(gui.domElement);
  
document.onselectstart = function(){
  return false;
};
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js