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 };
});