<div id="canvas_container_div">
  <canvas id="canvas" width="1000" height="1000"></canvas> <!--(large size for higher resolution)-->
</div>

<!-- AD -->
<!-- <div id="ad_div">
  <a href="https://codepen.io/matthewmain/full/MxwBmo" target="_blank">
    <img id="ad_svg" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/409445/kts_ad.svg">
  </a>
</div>
<style>
  #ad_div {
    position: fixed;
    width: 15%;
    bottom: 1%;
    right: 1%;
  }
  #ad_svg {
    width: 100%;
  }
</style> -->

body {
  background: #111111;
}

#canvas_container_div {
  position: absolute;
  top: 30%;
  left: 50%;
  transform: translate(-50%,-50%);
}

#canvas {
  width: 100%;
  height: 100%;
/*   border: solid 3px #000000; */
  background: #111111;
}

/////////////////////////////////////////////////////// 
////////     Plant Life Game Growth Prototype      //// ///////////////////////////////////////////////////////





///////////////////// VERLET.JS ////////////////////////////




////---INITIATION---////


///canvas  (*canvas width & height must be equal to retain aspect ratio)
var canvasContainerDiv = document.getElementById("canvas_container_div");
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var canvRatio = 1;//0.8;  // canvas ratio, as canvas size to lowest of window width or height

///trackers
var points = [], pointCount = 0;
var spans = [], spanCount = 0;
var skins = [], skinCount = 0;
var worldTime = 0;  // time as frame count

///settings
var gravity = 0.01;  // (rate of y-velocity increase per frame per point mass of 1)
var rigidity = 10;  // global span rigidity (as iterations of position accuracy refinement)
var friction = 0.999;  // (proportion of previous velocity after frame refresh)
var bounceLoss = 0.9;  // (proportion of previous velocity after bouncing)
var skidLoss = 0.8;  // (proportion of previous velocity if touching the ground)
var viewPoints = false;  // (point visibility)
var viewSpans = false;  // (span visibility)
var viewScaffolding = false; // (scaffolding visibility)
var viewSkins = true; // (skin visibility)
var breeze = 0.4;  // breeziness level (applied as brief left & right gusts)




////---OBJECTS---////


///point constructor
function Point(current_x, current_y, materiality="material") {  // materiality can be "material" or "immaterial"
  this.cx = current_x;
  this.cy = current_y; 
  this.px = this.cx;  // previous x value
  this.py = this.cy;  // previous y value
  this.mass = 1;  // (as ratio of gravity)
  this.materiality = materiality;
  this.fixed = false;
  this.id = pointCount;
  pointCount += 1;
}

///span constructor
function Span(point_1, point_2, visibility="visible") {  // visibility can be "visible" or "hidden"
  this.p1 = point_1;
  this.p2 = point_2;
  this.l = distance(this.p1,this.p2); // length
  this.strength = 1;  // (as ratio of rigidity)
  this.visibility = visibility;
  this.id = spanCount;
  spanCount += 1;
}

///skins constructor
function Skin(points_array,color) {
  this.points = points_array;  // an array of points for skin outline path
  this.color = color;
  this.id = skinCount;
  skinCount += 1;
}




////---FUNCTIONS---////


///scales canvas to window
function scaleToWindow() {
  if (window.innerWidth > window.innerHeight) {
    canvasContainerDiv.style.height = window.innerHeight*canvRatio+"px";
    canvasContainerDiv.style.width = canvasContainerDiv.style.height;
  } else {
    canvasContainerDiv.style.width = window.innerWidth*canvRatio+"px";
    canvasContainerDiv.style.height = canvasContainerDiv.style.width;
  }
}

///converts percentage to canvas x value
function xValFromPct(percent) {
  return percent * canvas.width / 100;
}

///converts percentage to canvas y value
function yValFromPct(percent) {
  return percent * canvas.height / 100;
}

///converts canvas x value to percentage of canvas width
function pctFromXVal(xValue) {
  return xValue * 100 / canvas.width;
}

///converts canvas y value to percentage of canvas height
function pctFromYVal(yValue) {
  return yValue * 100 / canvas.height;
}

///gets a point by id number
function getPt(id) {
  for (var i=0; i<points.length; i++) { 
    if (points[i].id == id) { return points[i]; }
  }
}

///gets distance between two points (pythogorian theorum)
function distance(point_1, point_2) {
  var x_difference = point_2.cx - point_1.cx;
  var	y_difference = point_2.cy - point_1.cy;
  return Math.sqrt( x_difference*x_difference + y_difference*y_difference);
}

///gets a span's mid point (returns object: { x: <value>, y: <value> } )
function smp(span) {
  var mx = ( span.p1.cx + span.p2.cx ) / 2;  // mid x value
  var my = ( span.p1.cy + span.p2.cy ) / 2;  // mid y value
  return { x: mx, y: my};
}

///removes a span by id
function removeSpan(id) {
  for( var i = 0; i < spans.length-1; i++){ 
    if ( spans[i].id === id) { spans.splice(i, 1); }
  }
}

///creates a point object instance
function addPt(xPercent,yPercent,materiality="material") {
  points.push( new Point( xValFromPct(xPercent), yValFromPct(yPercent), materiality ) );
  return points[points.length-1];
}

///creates a span object instance
function addSp(p1,p2,visibility="visible") {
  spans.push( new Span( getPt(p1), getPt(p2), visibility ) );
  return spans[spans.length-1];
}

///creates a skin object instance
function addSk(id_path_array, color) {
  var points_array = [];
  for ( var i=0; i<id_path_array.length; i++) {
    points_array.push(points[id_path_array[i]]);
  }
  skins.push( new Skin(points_array,color) );
  return skins[skins.length-1];
}

///updates point positions based on verlet velocity (i.e., current coord minus previous coord)
function updatePoints() {
  for(var i=0; i<points.length; i++) {
    var p = points[i];  // point object
    if (!p.fixed) {
      var	xv = (p.cx - p.px) * friction;	// x velocity
      var	yv = (p.cy - p.py) * friction;	// y velocity
      if (p.py >= canvas.height-1 && p.py <= canvas.height) { xv *= skidLoss; }
      p.px = p.cx;  // updates previous x as current x
      p.py = p.cy;  // updates previous y as current y
      p.cx += xv;  // updates current x with new velocity
      p.cy += yv;  // updates current y with new velocity
      p.cy += gravity * p.mass;  // add gravity to y
      if (worldTime % Tl.rib( 100, 200 ) === 0) { p.cx += Tl.rfb( -breeze, breeze ); }  // apply breeze to x
    }
  } 
}

///applies constrains
function applyConstraints( currentIteration ) {
  for (var i=0; i<points.length; i++) {
    var p = points[i];
    //wall constraints (inverts velocity if point moves beyond a canvas edge)
    if (p.materiality === "material") {
      if (p.cx > canvas.width) {
        p.cx = canvas.width;
        p.px = p.cx + (p.cx - p.px) * bounceLoss;
      }
      if (p.cx < 0) {
        p.cx = 0;
        p.px = p.cx + (p.cx - p.px) * bounceLoss;
      }
      if (p.cy > canvas.height) {
        p.cy = canvas.height;
        p.py = p.cy + (p.cy - p.py) * bounceLoss;
      }
      if (p.cy < 0) {
        p.cy = 0;
        p.py = p.cy + (p.cy - p.py) * bounceLoss;
      }
    }
  }
}

///updates span positions and adjusts associated points
function updateSpans( currentIteration ) {
  for (var i=0; i<spans.length; i++) {
    var thisSpanIterations = Math.round( rigidity * spans[i].strength );
    if ( currentIteration+1 <= thisSpanIterations ) {
      var s = spans[i];
      var dx = s.p2.cx - s.p1.cx;  // distance between x values
      var	dy = s.p2.cy - s.p1.cy;  // distance between y values
      var d = Math.sqrt( dx*dx + dy*dy);  // distance between the points
      var	r = s.l / d;	// ratio (span length over distance between points)
      var	mx = s.p1.cx + dx / 2;  // midpoint between x values 
      var my = s.p1.cy + dy / 2;  // midpoint between y values
      var ox = dx / 2 * r;  // offset of each x value (compared to span length)
      var oy = dy / 2 * r;  // offset of each y value (compared to span length)
      if (!s.p1.fixed) {
        s.p1.cx = mx - ox;  // updates span's first point x value
        s.p1.cy = my - oy;  // updates span's first point y value
      }
      if (!s.p2.fixed) {
        s.p2.cx = mx + ox;  // updates span's second point x value
        s.p2.cy = my + oy;  // updates span's second point y value
      }
    }
  }
}

///refines points for position accuracy & shape rigidity by updating spans and applying constraints iteratively
function refinePositions() {
  var requiredIterations = rigidity;
  for (var i=0; i<spans.length; i++) {
    var thisSpanIterations = Math.round( rigidity * spans[i].strength );
    if ( thisSpanIterations > requiredIterations ) {
      requiredIterations = thisSpanIterations;
    }
  }
  for (var j=0; j<requiredIterations; j++) {
    updateSpans(j);
    applyConstraints(i);
  }
}

///displays points
function renderPoints() {
  for (var i=0; i<points.length; i++) {
    var p = points[i];
    ctx.beginPath();
    ctx.fillStyle = "blue";
    ctx.arc( p.cx, p.cy, 3, 0 , Math.PI*2 );
    ctx.fill(); 
  }
}

///displays spans
function renderSpans() {
  for (var i=0; i<spans.length; i++) {
    var s = spans[i];
    if (s.visibility == "visible") {
      ctx.beginPath();
      ctx.lineWidth = 1;
      ctx.strokeStyle = "blue";
      ctx.moveTo(s.p1.cx, s.p1.cy);
      ctx.lineTo(s.p2.cx, s.p2.cy);
      ctx.stroke(); 
    }
  }
}

///displays scaffolding & binding spans (i.e., "hidden" spans)
function renderScaffolding() {
  ctx.beginPath();
  for (var i=0; i<spans.length; i++) {
    var s = spans[i];
    if (s.visibility === "hidden") {
      ctx.strokeStyle="pink";
      ctx.moveTo(s.p1.cx, s.p1.cy);
      ctx.lineTo(s.p2.cx, s.p2.cy);
    }
  }
  ctx.stroke();
}

///displays skins 
function renderSkins() {
  for(var i=0; i<skins.length; i++) {
    var s = skins[i];
    ctx.beginPath();
    ctx.strokeStyle = s.color;
    ctx.lineWidth = 0;
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    ctx.fillStyle = s.color;
    ctx.moveTo(s.points[0].cx, s.points[0].cy);
    for(var j=1; j<s.points.length; j++) { ctx.lineTo(s.points[j].cx, s.points[j].cy); }
    ctx.lineTo(s.points[0].cx, s.points[0].cy);
    ctx.stroke();
    ctx.fill();  
  }
}

///clears canvas frame
function clearCanvas() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
}

///renders all visible components
function renderImages() {
  //if ( viewSkins ) { renderSkins(); }  // disabled here so plants can be rendered sequentially in plants.js
  if ( viewSpans ) { renderSpans(); }
  if ( viewPoints ) { renderPoints(); }
  if ( viewScaffolding ) { renderScaffolding(); }
}




////---EVENTS---////


///scaling
window.addEventListener('resize', scaleToWindow);




////---RUN---////


function runVerlet() {
	scaleToWindow();
  updatePoints();
  refinePositions();
  clearCanvas();
  renderImages();
  worldTime++;
}









////////////////////// TOOLS.JS ////////////////////////////



const Tl = {

	//random integer between two numbers (min/max inclusive)
	rib: function( min, max ) {
 		return Math.floor( Math.random() * ( Math.floor(max) - Math.ceil(min) + 1 ) ) + Math.ceil(min);
	},

	//random float between two numbers
	rfb: function( min, max ) {
 		return Math.random() * ( max - min ) + min;
	},

	//converts radians to degrees
	radToDeg: function( radian ) {
	  return radian * 180 / Math.PI;
	},

	//converts degrees to radians
	degToRad: function( degree ) {
	  return degree / 180 * Math.PI;
	},

	//pauses program
	pause: function( milliseconds ) {
  	var then = Date.now(); 
  	var now;
  	do { now = Date.now() } while ( now - then < milliseconds );
	}


};









/////////////////////////////////////////////////////////////
///////////////////// PLANT LIFE ////////////////////////////
/////////////////////////////////////////////////////////////




////---INITIATION---////


///trackers
var plants = [], plantCount = 0;
var sunRays = [], sunRayCount = 0;
var shadows = [], shadowCount = 0;

///settings
var worldSpeed = 1;//3;  // (as frames per iteration; higher is slower) (does not affect physics iterations)
var restrictGrowthByEnergy = false;  // restricts plant growth by energy level (if false plants grow freely)
var viewShadows = false;  // (shadow visibility)
var phr = 2;  // photosynthesis rate ( rate plants store energy from sunlight )
var geer = 0.5;  // growth energy expenditure rate (rate energy is expended for growth)
var leer = 0.03;  // living energy expenditure rate (rate energy is expended for living, per segment)




////---OBJECTS---////


///plant constructor
function Plant( xLocation ) {
  this.id = plantCount;
  this.segments = []; this.segmentCount = 0;
  this.xLocation = xLocation;
  this.energy = 5000;  // seed energy (starting energy level at germination)
  this.isAlive = true;
  //settings
  this.forwardGrowthRate = gravity * Tl.rfb(35,50);//Tl.rfb(18,22);  // (rate of cross spans increase per frame)
  this.outwardGrowthRate = this.forwardGrowthRate * Tl.rfb(0.18,0.22);  // (rate forward span widens per frame)
  this.maxSegmentWidth = Tl.rfb(11,13);  // maximum segment width (in pixels)
  this.maxTotalSegments = Tl.rib(10,20);  // maximum total number of segments
  this.firstLeafSegment = Tl.rib(2,4);  // (segment on which first leaf set grows)
  this.leafFrequency = Tl.rib(2,3);  // (number of segments until next leaf set)
  this.maxLeaflength = this.maxSegmentWidth * Tl.rfb(4,7);  // maximum leaf length at maturity
  this.leafGrowthRate = this.forwardGrowthRate * Tl.rfb(1.4,1.6);  // leaf growth rate
  //base segment
  this.ptB1 = addPt( this.xLocation - 0.1, 100 );  // base point 1
  this.ptB2 = addPt( this.xLocation + 0.1, 100 );  // base point 2
  this.ptB1.fixed = this.ptB2.fixed = true;  // fixes base points to ground
  this.spB = addSp( this.ptB1.id, this.ptB2.id );  // adds base span
  createSegment( this, null, this.ptB1, this.ptB2 );  // creates the base segment (with "null" parent)
}


///segment constructor
function Segment( plant, parentSegment, basePoint1, basePoint2 ) {
  this.plantId = plant.id;
  this.id = plant.segmentCount;
  this.childSegment = null;
  this.hasChildSegment = false;
  this.parentSegment = parentSegment;
  this.isBaseSegment = false; if (this.parentSegment === null) { this.isBaseSegment = true; }
  this.hasLeaves = false;
  this.hasLeafScaffolding = false;
  //settings
  this.forwardGrowthRateVariation = Tl.rfb(0.95,1.05);//(0.95,1.05);  // forward growth rate variation
  this.mass = 1;  // mass of the segment stalk portion ( divided between the two extension points)
  this.strength = 1.5;  // as multiple of global rigidity (higher values effect performance)
  //base points
  this.ptB1 = basePoint1;  // base point 1
  this.ptB2 = basePoint2;  // base point 2
  //extension points
  var originX = ( this.ptB1.cx + this.ptB2.cx ) / 2;  // center of base points x values
  var originY = ( this.ptB1.cy + this.ptB2.cy ) / 2;  // center of base points y values
  this.ptE1 = addPt( pctFromXVal( originX ) - 0.1, pctFromYVal( originY ) - 0.1 );  // extension point 1
  this.ptE2 = addPt( pctFromXVal( originX ) + 0.1, pctFromYVal( originY ) - 0.1 );  // extension point 2
  this.ptE1.mass = this.mass / 2;
  this.ptE2.mass = this.mass / 2;
  //spans
  this.spL = addSp( this.ptB1.id, this.ptE1.id );  // left span
  this.spR = addSp( this.ptB2.id, this.ptE2.id );  // right span
  this.spF = addSp( this.ptE1.id, this.ptE2.id );  // forward span
  this.spCd = addSp( this.ptE1.id, this.ptB2.id );  // downward (l to r) cross span
  this.spCu = addSp( this.ptB1.id, this.ptE2.id );  // new upward (l to r) cross span
  this.spL.rigidity = this.strength;
  this.spR.rigidity = this.strength;
  this.spF.rigidity = this.strength;
  this.spCd.rigidity = this.strength;
  this.spCu.rigidity = this.strength;
  //base segment
  if (!this.isBaseSegment) {
    this.spCdP = addSp( this.ptE1.id, this.parentSegment.ptB2.id ); // downward (l to r) cross span to parent
    this.spCuP = addSp( this.parentSegment.ptB1.id, this.ptE2.id ); // upward (l to r) cross span to parent
    this.spCdP.rigidity = this.strength;
    this.spCdP.rigidity = this.strength;
  }
  //leaves
  this.ptLf1 = null;  // leaf point 1 (leaf tip)
  this.ptLf2 = null;  // leaf point 2 (leaf tip)  
  this.spLf1 = null;  // leaf 1 Span
  this.spLf2 = null;  // leaf 2 Span
  //skins
  this.skins = [];
  this.skins.push( addSk( [ this.ptE1.id, this.ptE2.id, this.ptB2.id, this.ptB1.id ], "darkgreen" ) );
}

///sun ray constructor
function SunRay() {
  this.id = sunRayCount;
  this.x = xValFromPct( this.id );
  this.intensity = 1;
  this.leafContacts = [];  // (as array of objects: { y: <leaf contact y value>, plant: <plant> })
}

//shadow constructor
function Shadow( leafSpan ) {
  this.p1 = leafSpan.p1;
  this.p2 = leafSpan.p2;
  this.p3 = { cx: this.p2.cx, cy: yValFromPct( 100 ) };
  this.p4 = { cx: this.p1.cx, cy: yValFromPct( 100 ) };
}




////---FUNCTIONS---////


//creates a new plant
function createPlant() {
  plantCount++;
  plants.push( new Plant(Tl.rib(10,90)) );
}

///creates a new segment
function createSegment( plant, parentSegment, basePoint1, basePoint2 ) {
  plant.segmentCount++;
  plant.segments.unshift( new Segment( plant, parentSegment, basePoint1, basePoint2 ) );
  if (parentSegment !== null) {
    parentSegment.childSegment = plant.segments[plant.segments.length-1];
    parentSegment.hasChildSegment = true;
  }
}

///creates a new sun ray (one for each x value as an integer percentage of the canvas's width)
function createSunRays() {
  for ( var i=0; i<101; i++ ) {
    sunRays.push( new SunRay() );
    sunRayCount++;
  }
}

///gets each leaf span's y values at integer x values as points where sun rays contact leaf
function markRayLeafIntersections() {
  for ( var i=0; i<plants.length; i++ ) {
    var p = plants[i];
    for ( var j=0; j<p.segments.length; j++ ) {
      var s = p.segments[j];
      if ( s.hasLeaves ) {
        var p1, p2;
        //leaf 1
        //assigns p1 as leftmost leaf span point and p2 as rightmost leaf span point
        if ( s.ptLf1.cx < s.ptB1.cx ) { p1 = s.ptLf1; p2 = s.ptB1; } else { p1 = s.ptB1; p2 = s.ptLf1; }  
        //loops through leaf span's integer x values
        var xPctMin = Math.ceil( pctFromXVal( p1.cx ) );
        var xPctMax = Math.floor( pctFromXVal( p2.cx ) );
        for ( var lcx=xPctMin; lcx<=xPctMax; lcx++ ) {  // leaf contact x value
          var lcy = p1.cy + (xValFromPct(lcx)-p1.cx) * (p2.cy-p1.cy) / (p2.cx-p1.cx);  // leaf contact y value
          //pushes corresponding y value and plant instance to associated sun ray instance
          sunRays[lcx].leafContacts.push( { y: lcy, plant: p } );
        }
        //leaf 2
        if ( s.ptLf2.cx < s.ptB2.cx ) { p1 = s.ptLf2; p2 = s.ptB2; } else { p1 = s.ptB2; p2 = s.ptLf2; }
        xPctMin = Math.ceil( pctFromXVal( p1.cx ) );
        xPctMax = Math.floor( pctFromXVal( p2.cx ) );  
        for ( lcx=xPctMin; lcx<=xPctMax; lcx++ ) {  // leaf contact x value
          lcy = p1.cy + (xValFromPct(lcx)-p1.cx) * ( p2.cy - p1.cy) / ( p2.cx - p1.cx ); // leaf contact y value
          sunRays[lcx].leafContacts.push( { y: lcy, plant: p } );
        }
      }
    } 
  }
}

///transfers energy from sun rays to leaves
function photosynthesize() {
  for ( var i=0; i<sunRays.length; i++ ) {
    var sr = sunRays[i];  // sun ray  
    //sorts leaf contact points from highest to lowest elevation (increasing y value)
    sr.leafContacts.sort( function( a, b ) { return a.y - b.y } );
    //when a sun ray hits a leaf, transfers half of the ray's intensity to the plant as energy
    for ( var j=0; j<sr.leafContacts.length; j++) {
      var lc = sr.leafContacts[j];  // leaf contact ({ y: <leaf contact y value>, plant: <plant> })
      sr.intensity /= 2;  
      lc.plant.energy += sr.intensity * phr;
    }
    sr.leafContacts = []; sr.intensity = 1;  // resets sun ray's leaf contact points & intensity for next iteration
  }
}

///sheds sunlight
function shedSunlight() {
  markRayLeafIntersections();
  photosynthesize(); 
}

///marks shadow positions (based on leaf spans)
function markShadowPositions( segment ) {
  shadows.push( new Shadow( segment.spLf1 ) );
  shadows.push( new Shadow( segment.spLf2 ) );
}

///grows all plants
function growPlants() {
  for (var i=0; i<plants.length; i++) {
    var plant = plants[i];
    //caps plant energy based on segment count
    if ( plant.energy > plant.segmentCount * 1000 && plant.energy > 5000 ) {
      plant.energy = plant.segmentCount * 1000;
    }
    //checks for sufficient energy for growth (must be greater than zero to grow)
    if ( plant.energy > 0 || !restrictGrowthByEnergy ) {
      for (var j=0; j<plants[i].segments.length; j++) {
        var segment = plants[i].segments[j];
        //lengthens segment spans
        if ( segment.spF.l < plant.maxSegmentWidth && plant.segments.length < plant.maxTotalSegments) { 
          lengthenSegmentSpans( plant, segment );
          plant.energy -= segment.spCd.l * geer;  // reduces energy by a ratio of segment size
        }
        //generates new segment
        if ( readyForChildSegment( plant, segment ) ) { 
          createSegment( plant, segment, segment.ptE1, segment.ptE2 ); 
        }
        //handles leaves
        if ( !segment.hasLeaves ) { 
          generateLeavesWhenReady( plant, segment ); 
        } else if ( plant.segments.length < plant.maxTotalSegments ) {
          growLeaves( plant, segment );
          plant.energy -= ( segment.spLf1.l + segment.spLf2.l ) * geer;  // reduces energy by a ratio of leaf length
        }
      }
    }
    //cost of living
    plant.energy -= plant.segmentCount * leer;  // reduces energy by a ratio of segment count
  }
}

///lengthens segment spans for growth
function lengthenSegmentSpans( plant, segment ) {
  if (segment.isBaseSegment) {
    segment.ptB1.cx -= plant.outwardGrowthRate / 2;
    segment.ptB2.cx += plant.outwardGrowthRate / 2;
    plant.spB.l = distance( segment.ptB1, segment.ptB2 );
    segment.spCd.l = distance( segment.ptE1, segment.ptB2 ) + plant.forwardGrowthRate / 3;
    segment.spCu.l = segment.spCd.l;
  } else {
    segment.spCdP.l = distance( segment.ptE1, segment.parentSegment.ptB2 ) + plant.forwardGrowthRate;
    segment.spCuP.l = segment.spCdP.l * segment.forwardGrowthRateVariation;
    segment.spCd.l = distance( segment.ptE1, segment.ptB2 );
    segment.spCu.l = distance( segment.ptB1, segment.ptE2 );
  } 
  segment.spF.l += plant.outwardGrowthRate;
  segment.spL.l = distance( segment.ptB1, segment.ptE1 );
  segment.spR.l = distance( segment.ptB2, segment.ptE2 );
}

///checks whether a segment is ready to generate a child segment
function readyForChildSegment( plant, segment ) {
  return segment.spF.l > plant.maxSegmentWidth * 0.333 && 
         !segment.hasChildSegment && 
         plant.segmentCount < plant.maxTotalSegments;
}

///generates leaves when segment is ready
function generateLeavesWhenReady ( plant, segment ) {
  var p = plant;
  var s = segment;
  if (  s.id >= p.firstLeafSegment && 
        s.id % p.leafFrequency === 0 && 
        s.spF.l > p.maxSegmentWidth * 0.1 ||
        s.id === p.maxTotalSegments-1 ) {
    var fsmp = smp( s.spF );  // forward span mid point ( { x: <value>, y: <value> } )
    s.ptLf1 = addPt( pctFromXVal( fsmp.x ), pctFromYVal( fsmp.y - 1 ) );  // leaf 1 tip point (left)
    s.ptLf2 = addPt( pctFromXVal( fsmp.x ), pctFromYVal( fsmp.y - 1 ) );  // leaf 2 tip point (right)
    s.spLf1 = addSp( s.ptB1.id, s.ptLf1.id );  // leaf 1 span (left)
    s.spLf2 = addSp( s.ptB2.id, s.ptLf2.id );  // leaf 2 span (right)
    s.leafTipsTetherSpan = addSp( s.ptLf1.id, s.ptLf2.id );  // leaf tip tether span
    s.hasLeaves = true;
  }
}

///add leaf scaffolding
function addLeafScaffolding( plant, segment ) {
  var p = plant;
  var s = segment;
  //remove leaf tips tether
  removeSpan(s.leafTipsTetherSpan.id);
  //apply leaf-unfold boosters
  s.ptLf1.cx -= gravity * 100;
  s.ptLf2.cx += gravity * 100;
  //add scaffolding points
  //(leaf 1)
  var x = s.ptE1.cx + ( s.ptE1.cx - s.ptE2.cx ) * 0.5;
  var y = s.ptE1.cy + ( s.ptE1.cy - s.ptE2.cy ) * 0.5;
  s.ptLf1ScA = addPt( pctFromXVal( x ), pctFromXVal( y ), "immaterial" ); s.ptLf1ScA.mass = 0;
  x = ( s.ptLf1.cx + s.ptLf1ScA.cx ) / 2 ;
  y = ( s.ptLf1.cy + s.ptLf1ScA.cy ) / 2 ;
  s.ptLf1ScB = addPt( pctFromXVal( x ), pctFromXVal( y ), "immaterial" ); s.ptLf1ScB.mass = 0;
  //(leaf 2)
  x = s.ptE2.cx + ( s.ptE2.cx - s.ptE1.cx ) * 0.5;
  y = s.ptE2.cy + ( s.ptE2.cy - s.ptE1.cy ) * 0.5;
  s.ptLf2ScA = addPt( pctFromXVal( x ), pctFromXVal( y ), "immaterial" ); s.ptLf2ScA.mass = 0;
  x = ( s.ptLf2.cx + s.ptLf2ScA.cx ) / 2 ;
  y = ( s.ptLf2.cy + s.ptLf2ScA.cy ) / 2 ;
  s.ptLf2ScB = addPt( pctFromXVal( x ), pctFromXVal( y ), "immaterial" ); s.ptLf2ScB.mass = 0;
  //add scaffolding spans
  //(leaf 1)
  s.spLf1ScA = addSp( s.ptE1.id, s.ptLf1ScA.id, "hidden" );
  s.spLf1ScB = addSp( s.ptB1.id, s.ptLf1ScA.id, "hidden" ); 
  s.spLf1ScC = addSp( s.ptLf1ScA.id, s.ptLf1ScB.id, "hidden" ); 
  s.spLf1ScD = addSp( s.ptLf1ScB.id, s.ptLf1.id, "hidden" ); 
  //(leaf 2)
  s.spLf2ScA = addSp( s.ptE2.id, s.ptLf2ScA.id, "hidden" ); 
  s.spLf2ScB = addSp( s.ptB2.id, s.ptLf2ScA.id, "hidden" ); 
  s.spLf2ScC = addSp( s.ptLf2ScA.id, s.ptLf2ScB.id, "hidden" ); 
  s.spLf2ScD = addSp( s.ptLf2ScB.id, s.ptLf2.id, "hidden" );
  s.hasLeafScaffolding = true;
}

///grows leaves
function growLeaves( plant, segment ) {
  var p = plant;
  var s = segment;
  if ( s.spLf1.l < p.maxLeaflength ) {
    //extend leaves
    s.spLf1.l = s.spLf2.l += p.leafGrowthRate;
    if ( s.spF.l > p.maxSegmentWidth*0.6 && !s.hasLeafScaffolding ) {
      // add scaffolding when leaves unfold
      addLeafScaffolding( plant, segment );
    } else if ( s.hasLeafScaffolding ) {
      //extend scaffolding if present
      //(leaf 1)
      s.spLf1ScA.l += p.leafGrowthRate * 1.25;
      s.spLf1ScB.l += p.leafGrowthRate * 1.5;
      s.spLf1ScC.l += p.leafGrowthRate * 0.06;
      s.spLf1ScD.l += p.leafGrowthRate * 0.06;
      //(leaf 2)
      s.spLf2ScA.l += p.leafGrowthRate * 1.25;
      s.spLf2ScB.l += p.leafGrowthRate * 1.5;
      s.spLf2ScC.l += p.leafGrowthRate * 0.06;
      s.spLf2ScD.l += p.leafGrowthRate * 0.06;
    }
  }
}

///renders leaf
function renderLeaf( plant, leafSpan ) {
  var p1x = leafSpan.p1.cx;
  var p1y = leafSpan.p1.cy;
  var p2x = leafSpan.p2.cx;
  var p2y = leafSpan.p2.cy;
  var mpx = ( p1x + p2x ) / 2;  // mid point x
  var mpy = ( p1y + p2y ) / 2;  // mid point y
  ctx.lineWidth = 2;
  ctx.lineJoin = "round";
  ctx.lineCap = "round";
  ctx.strokeStyle = "#003000";
  ctx.fillStyle = "green";
  var ah = 0.35;  // arc height
  //leaf top
  var ccpx = mpx + ( p2y - p1y ) * ah;  // curve control point x
  var ccpy = mpy + ( p1x - p2x ) * ah;  // curve control point y
  ctx.beginPath();
  ctx.moveTo(p1x,p1y);
  ctx.quadraticCurveTo(ccpx,ccpy,p2x,p2y);
  ctx.stroke();
  ctx.fill();
  //leaf bottom
  ccpx = mpx + ( p1y - p2y ) * ah;  // curve control point x
  ccpy = mpy + ( p2x - p1x ) * ah;  // curve control point y
  ctx.beginPath();
  ctx.moveTo(p1x,p1y);
  ctx.quadraticCurveTo(ccpx,ccpy,p2x,p2y);
  ctx.stroke();
  ctx.fill();
  //leaf center
  ctx.beginPath();
  ctx.lineWidth = 1;
  ctx.strokeStyle = "#007000";
  ctx.moveTo(p1x,p1y);
  ctx.lineTo(p2x,p2y);
  ctx.stroke();
}

///renders leaves
function renderLeaves( plant, segment ) {
  if ( segment.hasLeaves ) {
    renderLeaf( plant, segment.spLf1 );
    renderLeaf( plant, segment.spLf2 );
    if ( viewShadows ) { markShadowPositions( segment ); }
  }
}

///renders stalks
function renderStalks( plant, segment ) {
  for (var i=0; i<segment.skins.length; i++) {
    var s = segment.skins[i];
    //fills
    ctx.beginPath();
    ctx.fillStyle = s.color;
    ctx.lineWidth = 1;
    ctx.strokeStyle = "darkgreen";
    ctx.moveTo(s.points[0].cx, s.points[0].cy);
    for(var j=1; j<s.points.length; j++) { ctx.lineTo(s.points[j].cx, s.points[j].cy); }
    ctx.lineTo(s.points[0].cx, s.points[0].cy);
    ctx.stroke();
    ctx.fill(); 
    //outlines
    ctx.beginPath();
    ctx.lineWidth = 1;
    ctx.strokeStyle = "#2A2000";
    ctx.moveTo(s.points[3].cx, s.points[3].cy);
    ctx.lineTo(s.points[0].cx, s.points[0].cy);
    ctx.moveTo(s.points[2].cx, s.points[2].cy);
    ctx.lineTo(s.points[1].cx, s.points[1].cy);
    ctx.stroke();
    if ( !segment.hasChildSegment ) {
      ctx.beginPath();
      ctx.moveTo(s.points[3].cx, s.points[3].cy);
      ctx.lineTo(s.points[2].cx, s.points[2].cy);
      ctx.stroke();
    }
  }
}

///renders plants (sequentially)
function renderPlants() {
  for (var i=0; i<plants.length; i++) {
    for (var j=0; j<plants[i].segments.length; j++) {
      var plant = plants[i];
      var segment = plants[i].segments[j];
      renderStalks( plant, segment );
      renderLeaves( plant, segment );
    }
  }
}

///renders shadows (from highest to lowest elevation)
function renderShadows() {
  shadows.sort( function( a, b ) { return a.p2.cy - b.p2.cy } );
  for ( var i=0; i<shadows.length; i++ ) {
    var sh = shadows[i];
    ctx.beginPath();
    ctx.moveTo( sh.p1.cx, sh.p1.cy );
    ctx.lineTo( sh.p2.cx, sh.p2.cy ); 
    ctx.lineTo( sh.p3.cx, sh.p3.cy );
    ctx.lineTo( sh.p4.cx, sh.p4.cy );
    ctx.lineTo( sh.p1.cx, sh.p1.cy );
    ctx.fillStyle = "rgba( 0, 0, 0, 0.1 )";
    ctx.fill();  
  }
  //resets shadows
  shadows = []; shadowCount = 0;
}




////---DISPLAY---////

for ( var i=0; i<25; i++ ) {
  createPlant();
}

function display() {
  runVerlet();
  if ( worldTime % worldSpeed === 0 ) { growPlants(); }
  renderPlants();
  shedSunlight();
  renderShadows();
  window.requestAnimationFrame(display);
}

createSunRays();
display();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js