← Natura·Docs/Modules/Source

refraction.js

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

// refraction.js -- Natura ATMOSPHERIC REFRACTION / MIRAGE (physical / optical): light bending through a
// near-surface air-temperature gradient. Hot ground makes a thin low-density (low-n) layer, so rays curve UP and
// you see the sky pooled on the road ahead -- the INFERIOR mirage (the desert "water"). A cold surface under warm
// air (an INVERSION) makes a dense high-n layer, so rays curve DOWN and distant things LOOM, rise, and stack into
// a Fata Morgana -- the SUPERIOR mirage (a ship that becomes a castle in the air). The sign of the gradient sets
// the mirage; its strength sets how far a thing is displaced. Sibling of caustics.js (surface refraction); this is
// the atmospheric half. Couples to weather (the temperature). Deterministic, node-testable. UMD. From Castle in the Air.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.Refraction = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';
  // config: { groundTemp, airTemp, weather, distance, curveK, maxDtDays, name }
  function makeRefraction(config) {
    config = config || {};
    const distance = config.distance != null ? config.distance : 1.0;   // sight-line length (arbitrary units)
    const curveK = config.curveK != null ? config.curveK : 0.0022;      // ray curvature per degree of near-surface anomaly per unit distance
    const weatherName = config.weather || null;

    return {
      name: config.name || 'refraction',
      domain: 'physical',
      scale: 'landscape',
      params: config,

      init() { return { ground: config.groundTemp != null ? config.groundTemp : 20, air: config.airTemp != null ? config.airTemp : 20, bend: 0 }; },

      step(st, ctx, dt) {
        // temperatures from a coupled weather field (near-surface vs aloft), else the config
        if (weatherName && ctx && ctx.fields && ctx.fields[weatherName] && ctx.fields[weatherName].temp) {
          const T = ctx.fields[weatherName].temp; let s = 0; for (let i = 0; i < T.length; i++) s += T[i];
          st.air = s / T.length; st.ground = ctx.fields[weatherName].groundTemp != null ? ctx.fields[weatherName].groundTemp : st.ground;
        } else { st.ground = config.groundTemp != null ? config.groundTemp : st.ground; st.air = config.airTemp != null ? config.airTemp : st.air; }
        // anomaly: air warmer than the ground = INVERSION (rays curve DOWN, +bend -> looming); ground hotter = inferior (-bend)
        const anomaly = st.air - st.ground;
        st.bend = curveK * anomaly * distance;
        return st;
      },

      // OUT: bend (total ray-bend angle over the sight-line; + = looming/superior, - = inferior), mirageType,
      // apparent(trueElevation) -> the shifted apparent elevation, inverted (is there an inferior inverted image).
      // Other systems (a scene renderer) read `apparent` to displace a distant object, or drive it from weather.
      fields(st) {
        const b = st.bend, ab = Math.abs(b);
        const type = ab < 0.002 ? 'none' : (b > 0 ? (b > 0.02 ? 'fata-morgana' : 'looming') : 'inferior');
        return { bend: +b.toFixed(5), anomaly: +(st.air - st.ground).toFixed(2), mirageType: type,
          inverted: b < -0.002, apparent: (trueElev) => trueElev + b, loom: b };
      },
      events(st) { const f = this.fields(st); return f.mirageType !== 'none' ? [{ type: 'mirage', kind: f.mirageType, bend: f.bend }] : []; }
    };
  }
  return { makeRefraction };
});