← Natura·Docs/Modules/Source

scene-meadow.js

202 lines · in natura-src-1.0.0.zip as natura/scene-meadow.js · a module · its entry in the module reference

// scene-meadow.js -- MorningMeadow: the shared early-morning MEADOW BACKDROP.
//
// The environment behind the creatures, factored out so First Light and the sim-driven Living Meadow
// (and any future ground-level Natura view) draw the SAME morning world instead of each keeping its own
// copy. One concern -- the backdrop -- in one place: soft dawn sky, a low sun glow, a lit green ground,
// a layered hazy treeline (the sun not yet over it), ground mist, depth-graded grass and wildflowers,
// and a few birds crossing the sky. Deterministic (seeded), so it is stable frame to frame.
//
// MorningMeadow.draw(g, W, Hn, e) paints it; MorningMeadow.horizon(Hn) is the horizon line the caller
// places creatures against. UMD (window.MorningMeadow / module.exports). Self-contained.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.MorningMeadow = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';
  function mulberry(seed){ let a=seed>>>0; return function(){ a=(a+0x6D2B79F5)|0; let t=Math.imul(a^(a>>>15), 1|a); t=(t+Math.imul(t^(t>>>7), 61|t))^t; return ((t^(t>>>14))>>>0)/4294967296; }; }
  const HORIZON = 0.30;                                     // horizon as a fraction of canvas height
  const horizon = Hn => Hn*HORIZON;
  const sunAt = e => 0.68 + 0.30*Math.sin((e||0)*0.05 + 1.1);   // the slow drift of the low sun behind thin high cloud (shared by the backdrop + the final light tint)
  // ---- optional DAY/NIGHT CYCLE ----------------------------------------------------------------
  // When a caller passes a time-of-day `tod` in [0,1) (midnight=0, dawn=0.25, noon=0.5, dusk=0.75),
  // the whole world moves through the day: sky, ground, sun/moon, stars and the final light tint all
  // key off it. Callers that pass NOTHING (First Light) keep the fixed early-morning look untouched.
  const clamp = (v,a,b) => v<a?a : v>b?b : v;
  const lc = (a,b,t) => [ a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t ];   // lerp two [r,g,b]
  const rgb = c => 'rgb('+(c[0]|0)+','+(c[1]|0)+','+(c[2]|0)+')';
  function palAt(tod){
    const sunH = Math.sin((tod-0.25)*6.2832);                    // sun elevation proxy: -1 midnight .. 0 dawn/dusk .. +1 noon
    const daylight = clamp(sunH*1.6+0.42, 0, 1);                 // overall brightness, quick dawn/dusk ramp
    const night    = clamp(-sunH*2.0, 0, 1);                     // how deep into night
    const golden   = clamp(1-Math.abs(sunH)*3.4, 0, 1) * clamp(sunH*8+1.4, 0, 1);   // horizon fire, only near/above the horizon
    const pal = (d,gd,nt) => lc(lc(d,gd,golden), nt, night);     // day -> golden (dawn/dusk) -> night; the three peak at disjoint times
    return { sunH, daylight, night, golden,
      skyTop:   pal([156,184,209],[92,104,150],[9,16,40]),       // zenith: pale-blue day, mauve dusk, deep-blue night
      skyHz:    pal([237,231,215],[236,150,84],[26,42,72]),      // horizon: cream day, orange golden, dim blue night
      gndTop:   pal([149,160,108],[128,118,78],[26,36,42]),      // meadow at the treeline
      gndFront: pal([82,92,61],   [70,62,42], [16,22,26]),       // meadow in front
      sunCol:   lc([255,245,216],[255,176,104], golden) };       // the disc: white-gold high, ember low
  }

  // A FLYING BIRD, seen crossing the sky (from below/behind): a gull-curve of wings through a real little
  // body, with a head, a tail, and -- for a heron -- a long neck and trailing legs. Not a bare wing-mark:
  // s scales it, flap (in cycles) beats the wings, type sets the rhythm and shape, dir faces it into travel.
  function drawBird(g, cx, cy, s, flap, type, dir, alpha, colOverride){
    const beat = type==='soar'?0.16 : type==='heron'?0.82 : type==='flock'?0.6 : 0.66;   // how deep the wings swing
    const up = Math.sin(flap*6.2832)*beat, wy=-up*s*0.78;                                 // vertical wing swing this instant
    const wtip = (type==='soar'?1.55 : type==='heron'?1.5 : 1.28)*s;                      // wingspan half-width
    const col = colOverride || (type==='heron' ? 'rgba(70,78,90,'+alpha+')' : 'rgba(44,50,60,'+alpha+')');
    g.save(); g.translate(cx,cy); g.scale(dir,1);
    g.strokeStyle=col; g.fillStyle=col; g.lineWidth=Math.max(0.9, s*(type==='soar'?0.19:0.15)); g.lineCap='round'; g.lineJoin='round';
    g.beginPath(); g.moveTo(-wtip, wy);                                                   // gull-curve: wingtip -> body -> wingtip
    g.quadraticCurveTo(-s*0.30, wy*0.35 + s*0.03, -s*0.05, s*0.03);
    g.quadraticCurveTo( s*0.30, wy*0.35 + s*0.03,  wtip, wy); g.stroke();
    if(s>2.4){                                                                            // near enough to be a MODEL, not a speck
      g.beginPath(); g.ellipse(0, s*0.05, s*0.30, s*0.15, 0,0,7); g.fill();               // body
      g.beginPath(); g.arc(s*0.32, 0, s*0.10, 0,7); g.fill();                             // head, forward
      g.beginPath(); g.moveTo(-s*0.22,s*0.02); g.lineTo(-s*0.48,-s*0.06); g.lineTo(-s*0.48,s*0.12); g.closePath(); g.fill();  // tail fan
      if(type==='heron'){ g.lineWidth=Math.max(0.7,s*0.07);
        g.beginPath(); g.moveTo(-s*0.28,s*0.05); g.lineTo(-s*0.72,s*0.13); g.stroke();    // trailing legs
        g.beginPath(); g.moveTo(s*0.30,0); g.lineTo(s*0.66,-s*0.04); g.stroke(); }        // long neck + beak
    }
    g.restore();
  }

  function draw(g, W, Hn, e, tod){
    e = e || 0;
    const hz = Hn*HORIZON;
    let sun;
    if(tod==null){
      // SKY: soft early morning -- pale, cool up high warming to cream at the horizon
      const sky=g.createLinearGradient(0,0,0,hz+40);
      sky.addColorStop(0,'#9cb8d1'); sky.addColorStop(0.55,'#c4d1d7'); sky.addColorStop(1,'#ede7d7'); g.fillStyle=sky; g.fillRect(0,0,W,hz+40);
      // a low sun not yet over the trees: a soft pale-gold brightening near the horizon
      sun = sunAt(e);                                              // the low sun drifting behind thin high cloud: brightens + softens over ~2 min
      const gcy = hz+4 - sun*9;                                           // the glow lifts a touch as it strengthens
      const glow=g.createRadialGradient(W*0.26,gcy,0, W*0.26,gcy, W*(0.55+0.12*sun));
      glow.addColorStop(0,'rgba(255,245,216,'+(0.30+0.42*sun).toFixed(2)+')'); glow.addColorStop(0.5,'rgba(255,240,210,'+(0.05+0.14*sun).toFixed(2)+')'); glow.addColorStop(1,'rgba(255,240,210,0)'); g.fillStyle=glow; g.fillRect(0,0,W,hz+80);
      // GROUND: a lit morning meadow, fresh green near the horizon deepening to the front
      const gnd=g.createLinearGradient(0,hz,0,Hn);
      gnd.addColorStop(0,'#95a06c'); gnd.addColorStop(0.42,'#727c52'); gnd.addColorStop(1,'#525c3d'); g.fillStyle=gnd; g.fillRect(0,hz,W,Hn-hz);
      if(sun>0.4){ const wr=g.createRadialGradient(W*0.26,hz+4,0, W*0.26,hz+4, W*0.5);   // a warmth washing the near meadow when the sun is out
        wr.addColorStop(0,'rgba(255,232,176,'+(0.11*(sun-0.4)).toFixed(3)+')'); wr.addColorStop(1,'rgba(255,232,176,0)'); g.fillStyle=wr; g.fillRect(0,hz,W,(Hn-hz)*0.6); }
    } else {
      // ---- the world at hour `tod`: sky, stars, sun/moon and ground all move through the day ----
      const P=palAt(tod); sun=P.daylight;
      const sky=g.createLinearGradient(0,0,0,hz+40);
      sky.addColorStop(0, rgb(P.skyTop)); sky.addColorStop(0.6, rgb(lc(P.skyTop,P.skyHz,0.55))); sky.addColorStop(1, rgb(P.skyHz)); g.fillStyle=sky; g.fillRect(0,0,W,hz+40);
      if(P.night>0.12){                                            // STARS: seeded, faint, softly twinkling; fade in as night deepens
        const stt=mulberry(9001);
        for(let i=0;i<100;i++){ const sx=stt()*W, sy=stt()*hz*0.94, r=0.4+stt()*1.2;
          const a=P.night*(0.3+0.55*stt())*(0.55+0.45*Math.sin(e*(0.5+stt()*2.2)+i*1.7));
          if(a<=0.01) continue; g.globalAlpha=Math.min(0.9,a); g.fillStyle='#f2f4ff'; g.beginPath(); g.arc(sx,sy,r,0,7); g.fill(); }
        g.globalAlpha=1;
      }
      const up=P.sunH>=-0.03;                                       // sun up by day, moon up by night
      const frac = up ? (tod-0.25)/0.5 : (((tod-0.75+1)%1))/0.5;    // 0..1 arc across the sky (east->west) for whichever body is up
      const bodyX=W*(0.12+0.76*clamp(frac,0,1)), bodyY=hz+6 - Math.abs(P.sunH)*(hz*1.5), by=Math.min(bodyY, hz+2);
      const gc = up ? P.sunCol : [206,216,240];                    // glow colour: warm sun / cool moon
      const gS = up ? (0.14+0.46*P.daylight+0.5*P.golden) : (0.08+0.26*P.night);
      const glow=g.createRadialGradient(bodyX,by,0, bodyX,by, W*(0.26+0.32*(up?P.daylight:P.night)));
      glow.addColorStop(0,'rgba('+(gc[0]|0)+','+(gc[1]|0)+','+(gc[2]|0)+','+Math.min(0.85,gS).toFixed(2)+')');
      glow.addColorStop(0.5,'rgba('+(gc[0]|0)+','+(gc[1]|0)+','+(gc[2]|0)+','+(gS*0.26).toFixed(2)+')');
      glow.addColorStop(1,'rgba('+(gc[0]|0)+','+(gc[1]|0)+','+(gc[2]|0)+',0)'); g.fillStyle=glow; g.fillRect(0,0,W,hz+90);
      { const R = up ? (6.5+5*P.golden) : 6; g.fillStyle = up ? rgb(P.sunCol) : 'rgba(228,232,240,0.96)';
        g.beginPath(); g.arc(bodyX,by,R,0,7); g.fill();
        if(!up){ g.fillStyle=rgb(P.skyTop); g.beginPath(); g.arc(bodyX-R*0.45,by-R*0.28,R*0.92,0,7); g.fill(); } }   // a crescent bite carves the moon
      const gnd=g.createLinearGradient(0,hz,0,Hn);
      gnd.addColorStop(0, rgb(P.gndTop)); gnd.addColorStop(0.42, rgb(lc(P.gndTop,P.gndFront,0.55))); gnd.addColorStop(1, rgb(P.gndFront)); g.fillStyle=gnd; g.fillRect(0,hz,W,Hn-hz);
      if(P.golden>0.04){ const wr=g.createRadialGradient(bodyX,hz+4,0, bodyX,hz+4, W*0.55);   // dawn/dusk fire washing the near meadow, from under the low sun
        wr.addColorStop(0,'rgba(255,178,108,'+(0.17*P.golden).toFixed(3)+')'); wr.addColorStop(1,'rgba(255,178,108,0)'); g.fillStyle=wr; g.fillRect(0,hz,W,(Hn-hz)*0.6); }
    }
    // (the global cloud-dim / day-night wash is applied by MorningMeadow.lightTint over the WHOLE frame -- backdrop AND animals)
    // a distant layered WOODLAND -- a far hazy band behind a nearer band, deciduous canopies + conifers
    function treeBand(baseY, sizeMul, rgb, aBase, seed){ const tr=mulberry(seed); let tx=-18;
      while(tx<W+28){ const tw=(22+tr()*52)*sizeMul, th=(12+tr()*22)*sizeMul, conifer=tr()<0.26;
        g.fillStyle='rgba('+rgb+','+(aBase+tr()*0.16).toFixed(2)+')';
        if(conifer){ g.beginPath(); g.moveTo(tx-tw*0.30, baseY+3); g.lineTo(tx-tw*0.06, baseY-th*1.05);
          g.lineTo(tx, baseY-th*1.55); g.lineTo(tx+tw*0.06, baseY-th*1.05); g.lineTo(tx+tw*0.30, baseY+3); g.closePath(); g.fill(); }
        else { g.beginPath(); g.ellipse(tx, baseY-th*0.42, tw*0.5, th, 0, 0, 7);
          g.ellipse(tx-tw*0.24, baseY-th*0.08, tw*0.3, th*0.68, 0,0,7); g.fill(); }
        tx += tw*0.46; } }
    treeBand(hz-3, 0.72, '74,90,88', 0.24, 4200);   // far band: small, cool, hazy
    treeBand(hz+3, 1.06, '43,57,44', 0.42, 4242);   // near band: larger, dark green
    const mist=g.createLinearGradient(0,hz-16,0,hz+48);
    const mistA=(0.60-0.40*sun).toFixed(2);                             // the ground mist thins as the sun burns through, gathers when it softens
    mist.addColorStop(0,'rgba(236,233,218,0)'); mist.addColorStop(0.5,'rgba(236,233,218,'+mistA+')'); mist.addColorStop(1,'rgba(236,233,218,0)'); g.fillStyle=mist; g.fillRect(0,hz-16,W,64);
    // GRASS: tufts, sparser/shorter far, denser/taller front (seed-stable)
    const gr=mulberry(1337); g.lineCap='round';
    for(let i=0;i<240;i++){ const yy=hz+gr()*(Hn-hz), xx=gr()*W, dens=(yy-hz)/(Hn-hz);
      if(gr()>0.12+dens){ gr();gr(); continue; }
      const bl=3+dens*9; g.lineWidth=0.7+dens; g.strokeStyle='rgba('+(120+dens*34|0)+','+(150+dens*26|0)+',88,'+(0.30+dens*0.30).toFixed(2)+')';
      const wind=Math.sin(e*1.3 + xx*0.013 + yy*0.02)*0.30*dens;   // a gentle traveling gust; taller front grass leans more
      for(let b=-1;b<=1;b++){ const a=(gr()-0.5)*0.75 + wind; g.beginPath(); g.moveTo(xx+b*1.7,yy); g.lineTo(xx+b*1.7+Math.sin(a)*bl, yy-Math.cos(a)*bl); g.stroke(); }
    }
    // WILDFLOWERS + seed-heads through the near meadow
    { const fl=mulberry(2718);
      for(let i=0;i<80;i++){ const yy=hz+(0.32+fl()*0.68)*(Hn-hz), xx=fl()*W, dens=(yy-hz)/(Hn-hz);
        if(fl()>dens*0.95){ fl();fl(); continue; }
        const stH=5+dens*11, windF=Math.sin(e*1.3 + xx*0.013 + yy*0.02)*0.5*dens, hx=xx+(fl()-0.5)*3 + windF*stH, hy=yy-stH;
        g.strokeStyle='rgba(94,120,70,'+(0.28+dens*0.3).toFixed(2)+')'; g.lineWidth=0.8; g.beginPath(); g.moveTo(xx,yy); g.lineTo(hx,hy); g.stroke();
        if(fl()<0.5){ const which=(fl()*3)|0, a=(0.5+dens*0.32).toFixed(2);
          g.fillStyle = which===0?'rgba(240,235,216,'+a+')' : which===1?'rgba(236,214,116,'+a+')' : 'rgba(226,182,196,'+a+')';
          g.beginPath(); g.arc(hx,hy,1+dens*1.7,0,7); g.fill(); }
        else { g.strokeStyle='rgba(150,160,96,'+(0.32+dens*0.3).toFixed(2)+')'; g.lineWidth=1.1;
          g.beginPath(); g.moveTo(hx,hy+2); g.lineTo(hx+1.6,hy-3); g.moveTo(hx,hy+2); g.lineTo(hx-1.6,hy-3); g.stroke(); } }
    }
    // BIRD LIFE in the sky: layered KINDS with their own rhythms, not one repeated wing-mark.
    { const sb=mulberry(88), H=Hn;
      const cross=(off,rate,dir)=>(((off + e*rate*dir)%1)+1)%1*W;                          // a bird drifting across, wrapping at the edges
      // a soft shadow gliding on the meadow under a crossing bird (sun low-left -> cast down-right;
      // higher fliers throw a longer, fainter shadow nearer the horizon). Grounds the sky birds.
      // the shadow IS the bird: its own wing silhouette, foreshortened onto the grass and beating in sync,
      // thrown down-right by the low-left sun; longer + fainter + nearer the horizon the higher it flies.
      const birdShadow=(x, cy, s, flap, type, dir, base)=>{
        const hgt=Math.max(0, Math.min(1, (hz-cy)/hz));
        const sy=hz + (0.05+0.20*(1-hgt))*(Hn-hz);      // land in the OPEN band just below the treeline (backdrop draws under the animals)
        const sx=x + W*(0.045 + hgt*0.055);
        const a=(base*(2.3+1.0*(1-hgt))*(0.34+0.66*sun)).toFixed(3);   // shadows are crisp in full sun, soften toward overcast -- light + shadow coupled
        g.save(); g.translate(sx,sy); g.scale(1, 0.32);         // flatten the silhouette onto the ground plane
        drawBird(g, 0, 0, s, flap, type, dir, a, 'rgba(26,34,20,'+a+')');
        g.restore();
      };
      // a SOARING raptor: high, broad near-still wings, slow circling drift (faces the way it curves)
      { const ph=sb()*7, s=8+sb()*3.5, ax=e*0.05+ph, cx=W*(0.5+0.34*Math.sin(ax)), cy=H*0.085 + 22*Math.sin(e*0.06+ph*1.3);
        const rdir=Math.cos(ax)>=0?1:-1;
        birdShadow(cx, cy, s, e*0.5+ph, 'soar', rdir, 0.12);
        drawBird(g, cx, cy, s, e*0.5+ph, 'soar', rdir, '0.44'); }
      // two straight-crossing CROWS, opposite ways, steady beat
      for(let i=0;i<2;i++){ const dir=i?1:-1, y0=H*(0.07+sb()*0.09), rate=0.018+sb()*0.014, off=sb(), phz=sb()*10, s=5.5+sb()*3;
        const bx=cross(off,rate,dir), by=y0+Math.sin(e*0.5+phz)*4;
        birdShadow(bx, by, s, e*4+phz, 'flap', dir, 0.10);
        drawBird(g, bx, by, s, e*4+phz, 'flap', dir, (0.36+sb()*0.2).toFixed(2)); }
      // a HERON crossing low and slow near the treeline: deep beats, long neck, trailing legs
      { const dir=sb()<0.5?1:-1, s=9.5+sb()*2.5;
        const hx=cross(sb(),0.011,dir), hy=H*0.205+Math.sin(e*0.35)*3;
        birdShadow(hx, hy, s, e*1.7, 'heron', dir, 0.10);
        drawBird(g, hx, hy, s, e*1.7, 'heron', dir, '0.42'); }
      // a loose FLOCK of small quick birds, moving as one cohesive group
      { const dir=sb()<0.5?1:-1, y0=H*(0.05+sb()*0.06), cxc=cross(sb(),0.03,dir), cyc=y0+Math.sin(e*0.6)*4, s=2.7+sb()*0.9;
        for(let k=0;k<7;k++){ const ox=(sb()-0.5)*40, oy=(sb()-0.5)*17, phz=sb()*10, fbx=cxc+ox, fby=cyc+oy+Math.sin(e*1.1+phz)*2;
          birdShadow(fbx, fby, s, e*9+phz, 'flock', dir, 0.085);   // each little bird casts its own tiny beating shadow
          drawBird(g, fbx, fby, s, e*9+phz, 'flock', dir, (0.32+sb()*0.2).toFixed(2)); } }
    }
  }

  // a FINAL screen-space light tint the consumer applies AFTER drawing the animals, so a passing cloud dims
  // EVERYTHING it falls on (the meadow AND the creatures on it), not just the backdrop -- the light as one system.
  function lightTint(g, W, Hn, e, tod){
    g.save(); g.setTransform(1,0,0,1,0,0);
    if(tod==null){
      const sun = sunAt(e);
      if(sun<0.66){ g.fillStyle='rgba(104,120,142,'+((0.66-sun)*0.36).toFixed(3)+')'; g.fillRect(0,0,W,Hn); }        // cool dim under the cloud
      else if(sun>0.78){ g.fillStyle='rgba(255,238,198,'+((sun-0.78)*0.14).toFixed(3)+')'; g.fillRect(0,0,W,Hn); }   // a faint warmth at the brightest
    } else {
      const P=palAt(tod);
      if(P.night>0.02){ g.fillStyle='rgba(12,20,46,'+(P.night*0.6).toFixed(3)+')'; g.fillRect(0,0,W,Hn); }           // night falls over the ground AND the creatures on it -- one light
      if(P.golden>0.04){ g.fillStyle='rgba(255,150,70,'+(P.golden*0.14).toFixed(3)+')'; g.fillRect(0,0,W,Hn); }      // golden-hour warmth washes everything
    }
    g.restore();
  }
  return { draw, horizon, HORIZON, lightTint, sunAt, palAt };
});