← Natura·Docs/Modules/Source

canopy.js

60 lines · in natura-src-1.0.0.zip as natura/canopy.js · a module · its entry in the module reference · its test: canopy-test.js

// canopy.js -- a small Natura STRUCTURE system: a rainforest canopy as a LIGHT FIELD over height AND region.
//
// One concern, render-free: it exposes the forest's light structure as fields() the way weather exposes moisture --
//   sunAt(x,y)  : the LOCAL sun elevation [-1,1] at a place -- the world's day phase shifted by the place's REGION
//                 (longitude, east dawns earlier) and dimmed by its canopy SHADE (depth). Organisms read this to wear
//                 the day/night verb per their layer + region: the floor keeps the shortest, latest day of all.
//   lightAt(y)  : light intensity [0,1] reaching height y (ambient daylight * Beer-Lambert transmission down the leaves).
//   shadeAt(y)  : the canopy light-debt at height y (gentle-capped, fed to the verb's {shade}).
//   gaps, layers: the sky-gaps (where shafts fall) and the layer bands -- for the renderer.
// The sun comes from the world CLOCK (day phase), so region-phase is a real time-shift; a celestial coupling can refine
// amplitude later. Static structure (gaps/transmission are seeded once); nothing here touches a canvas. UMD.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.Canopy = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';
  const TAU = Math.PI * 2, clamp = (v, a, b) => v < a ? a : v > b ? b : v;
  const smooth = (a, b, x) => { const t = clamp((x - a) / (b - a), 0, 1); return t * t * (3 - 2 * t); };
  const LAYERS = { emergent: [0.00, 0.14], canopy: [0.14, 0.40], understory: [0.40, 0.72], floor: [0.72, 1.00] };

  function makeCanopy(config) {
    config = config || {};
    const K = config.extinction != null ? config.extinction : 2.02;         // Beer-Lambert extinction down the leaf mass
    const regionPhase = config.regionPhase != null ? config.regionPhase : 0.08;
    const shadeScale = config.shadeScale != null ? config.shadeScale : 0.62; // gentler than raw transmission so noon reads as day
    const shadeCap = config.shadeCap != null ? config.shadeCap : 0.6;
    const nGapCfg = config.nGap;

    return {
      name: config.name || 'canopy', domain: 'physical', scale: 'local', params: config,

      init(rng, params, clock) {
        const nGap = nGapCfg != null ? nGapCfg : (4 + Math.floor(rng() * 3));
        const gaps = []; for (let i = 0; i < nGap; i++) gaps.push(0.05 + 0.9 * rng());
        return { clock: clock, gaps: gaps };
      },
      step(state, ctx) { const wx = ctx && ctx.fields ? ctx.fields.weather : null; if (wx && wx.cloud != null) state.cloud = wx.cloud; return state; },   // read the sky's cloud cover; the structure is otherwise static and the sun is read live in fields()

      fields(state) {
        const canopyMass = y => 1.0 * smooth(0.10, 0.46, y) + 0.28 * smooth(0.46, 1.0, y);   // leaf mass ABOVE height y
        const transmission = y => Math.exp(-K * canopyMass(clamp(y, 0, 1)));                 // ~1 at top, ~0.08 at floor
        const t06 = transmission(0.06);
        const shadeAt = y => clamp((1 - transmission(clamp(y, 0, 1)) / t06) * shadeScale, 0, shadeCap);
        const dayPhase = (state.clock && state.clock.dayPhase) ? state.clock.dayPhase() : 0.5;
        const sunDP = p => -Math.cos(p * TAU);                                               // 0 midnight->-1, 0.5 noon->+1
        const cloud = state.cloud || 0;                                                      // cloud cover blocks the sun...
        const daylight = Math.max(0, sunDP(dayPhase)) * (1 - cloud * 0.7);                   // ...so less light reaches the canopy (the shafts weaken, the life dims)
        return {
          gaps: state.gaps.slice(), layers: LAYERS,
          transmission: transmission, shadeAt: shadeAt,
          lightAt: y => daylight * transmission(clamp(y, 0, 1)),
          sunAt: (x, y) => sunDP(dayPhase + regionPhase * ((x || 0) - 0.5)) - shadeAt(y),    // sun POSITION (day/night timing) is unchanged by cloud
          daylight: daylight, dayPhase: dayPhase, cloud: cloud
        };
      }
    };
  }

  return { makeCanopy, LAYERS };
});