amaran-light-cli
Version:
Command line tool for controlling Aputure Amaran lights via WebSocket to a local Amaran desktop app.
283 lines • 12.5 kB
JavaScript
/**
* Realistic daylight calculation functions based on sun position and atmospheric models.
*/
/**
* Realistic daylight calculation based on sun altitude.
* @param altitude Sun altitude in radians
* @param maxAltitude Maximum sun altitude for the day in radians
*/
export function calculateRealisticSunAltitude(altitude, maxAltitude) {
// Convert altitude to degrees for easier calculations
const altitudeDeg = (altitude * 180) / Math.PI;
const maxAltitudeDeg = (maxAltitude * 180) / Math.PI;
// CCT: Based on real-world color temperature at different sun angles
let cctFactor;
if (altitudeDeg < -6) {
cctFactor = 0; // Before civil twilight - minimum CCT
}
else if (altitudeDeg < 0) {
// Civil twilight: 4000-5000K range
cctFactor = 0.3 + ((altitudeDeg + 6) / 6) * 0.2; // 0.3 to 0.5
}
else if (altitudeDeg < 30) {
// Morning/afternoon: 5000-6500K
cctFactor = 0.5 + (altitudeDeg / 30) * 0.3; // 0.5 to 0.8
}
else if (altitudeDeg < 60) {
// High sun: 5500-7000K
cctFactor = 0.8 + ((altitudeDeg - 30) / 30) * 0.2; // 0.8 to 1.0
}
else {
// Very high sun: 6500-7500K
cctFactor = 1.0;
}
// Calculate raw intensity factor based on absolute altitude
const calculateIntensity = (altDeg) => {
if (altDeg < -6)
return 0;
if (altDeg < 0)
return ((altDeg + 6) / 6) ** 2 * 0.05;
if (altDeg < 10)
return 0.05 + (altDeg / 10) * 0.15;
if (altDeg < 30)
return 0.2 + ((altDeg - 10) / 20) * 0.4;
return 0.6 + ((altDeg - 30) / 60) * 0.4;
};
const rawIntensity = calculateIntensity(altitudeDeg);
const maxDailyIntensity = calculateIntensity(maxAltitudeDeg);
// Normalize: Scale the current intensity so that at maxAltitude it reaches 1.0
let intensityFactor = maxDailyIntensity > 0.01 ? rawIntensity / maxDailyIntensity : 0;
intensityFactor = Math.min(1.0, intensityFactor);
return [Math.max(0, Math.min(1, cctFactor)), Math.max(0, Math.min(1, intensityFactor)), rawIntensity];
}
/**
* CIE daylight model with atmospheric path modeling.
* @param altitude Sun altitude in radians
* @param maxAltitude Maximum sun altitude for the day in radians
*/
export function calculateRealisticCIEDaylight(altitude, maxAltitude) {
const altitudeDeg = (altitude * 180) / Math.PI;
const maxAltitudeDeg = (maxAltitude * 180) / Math.PI;
let cctFactor;
if (altitudeDeg < -6) {
cctFactor = 0;
}
else if (altitudeDeg < 0) {
cctFactor = 0.4 + ((altitudeDeg + 6) / 6) ** 1.5 * 0.2;
}
else if (altitudeDeg < 15) {
cctFactor = 0.6 - Math.sin((altitudeDeg * Math.PI) / 30) * 0.1;
}
else if (altitudeDeg < 45) {
cctFactor = 0.5 + ((altitudeDeg - 15) / 30) * 0.4;
}
else {
cctFactor = 0.9 + Math.min(0.1, ((altitudeDeg - 45) / 45) * 0.1);
}
const calculateRawIntensity = (altDeg) => {
if (altDeg < -6)
return 0;
if (altDeg < 0)
return ((altDeg + 6) / 6) ** 3 * 0.03;
if (altDeg < 15) {
const altRad = Math.max(0.01, (altDeg * Math.PI) / 180);
const airMass = 1 / Math.sin(altRad);
return Math.min(0.25, 1 / airMass ** 0.7);
}
if (altDeg < 40)
return 0.25 + ((altDeg - 15) / 25) * 0.55;
if (altDeg < 70)
return 0.8 + ((altDeg - 40) / 30) * 0.15;
return 0.95 + ((altDeg - 70) / 20) * 0.05;
};
const rawIntensity = calculateRawIntensity(altitudeDeg);
const maxDailyIntensity = calculateRawIntensity(maxAltitudeDeg);
const intensityFactor = maxDailyIntensity > 0.001 ? rawIntensity / maxDailyIntensity : 0;
return [Math.max(0, Math.min(1, cctFactor)), Math.max(0, Math.min(1, intensityFactor)), rawIntensity];
}
/**
* Perez daylight model with turbidity and atmospheric effects.
* @param altitude Sun altitude in radians
* @param maxAltitude Maximum sun altitude for the day in radians
*/
export function calculateRealisticPerezDaylight(altitude, maxAltitude) {
const altitudeDeg = (altitude * 180) / Math.PI;
const maxAltitudeDeg = (maxAltitude * 180) / Math.PI;
let cctFactor;
if (altitudeDeg < -6) {
cctFactor = 0;
}
else if (altitudeDeg < 0) {
cctFactor = 0.35 + ((altitudeDeg + 6) / 6) ** 2 * 0.25;
}
else if (altitudeDeg < 25) {
const goldenHourEffect = Math.exp(-((altitudeDeg - 12.5) ** 2) / 100);
cctFactor = 0.6 - goldenHourEffect * 0.15 + (altitudeDeg / 25) * 0.3;
}
else if (altitudeDeg < 50) {
cctFactor = 0.75 + ((altitudeDeg - 25) / 25) * 0.2;
}
else {
cctFactor = Math.min(1.0, 0.95 + ((altitudeDeg - 50) / 40) * 0.05);
}
const calculateRawIntensity = (altDeg) => {
if (altDeg < -6)
return 0;
if (altDeg < 5)
return (Math.max(0, altDeg + 6) / 11) ** 4 * 0.15;
if (altDeg < 20) {
const zenithAngle = Math.max(0.01, ((90 - altDeg) * Math.PI) / 180);
const relativeLuminance = Math.exp(-0.2 / Math.max(0.01, Math.cos(zenithAngle)));
return Math.min(0.4, relativeLuminance * 0.35 + 0.05);
}
if (altDeg < 45)
return 0.25 + ((altDeg - 20) / 25) * 0.65;
if (altDeg < 80)
return 0.9 + ((altDeg - 45) / 35) * 0.1;
return 1.0;
};
const rawIntensity = calculateRawIntensity(altitudeDeg);
const maxDailyIntensity = calculateRawIntensity(maxAltitudeDeg);
const intensityFactor = maxDailyIntensity > 0.001 ? rawIntensity / maxDailyIntensity : 0;
return [Math.max(0, Math.min(1, cctFactor)), Math.max(0, Math.min(1, intensityFactor)), rawIntensity];
}
/**
* Kasten-Young air mass formula.
* Accurate even at low altitudes/horizon.
* @param altitudeDeg Altitude in degrees
*/
function calculateAirMass(altitudeDeg) {
// The Kasten-Young formula is valid for altitude > -0.5 deg.
// Below that, the atmosphere is essentially opaque for direct sunlight.
const gamma = Math.max(-0.5, altitudeDeg);
return 1 / (Math.sin((gamma * Math.PI) / 180) + 0.50572 * (gamma + 6.07995) ** -1.6364);
}
/**
* Physics-based Atmospheric model.
* Uses Beer-Lambert law for intensity and exponential decay for CCT.
* @param altitude Sun altitude in radians
* @param maxAltitude Maximum sun altitude for the day in radians
*/
export function calculateRealisticPhysicsDaylight(altitude, maxAltitude, weather) {
const altitudeDeg = (altitude * 180) / Math.PI;
const maxAltitudeDeg = (maxAltitude * 180) / Math.PI;
const { cloudCover = 0 } = weather || {};
const cloudMix = Math.min(1, Math.max(0, cloudCover));
// CCT: Exponential approach to zenith CCT
// Factors: 0 at horizon/twilight, 1 at zenith
let cctFactor;
if (altitudeDeg < -6) {
cctFactor = 0;
}
else {
// k=0.05 gives a nice natural curve for clear sky
const clearFactor = 1 - Math.exp(-0.05 * (altitudeDeg + 6));
// Overcast sky is visually flatter/uniform color (approx zenith color)
const overcastFactor = 1.0;
// Blend based on cloud cover
cctFactor = clearFactor * (1 - cloudMix) + overcastFactor * cloudMix;
}
// Intensity: Beer-Lambert Law + Ambient
const calculateIntensity = (altDeg) => {
if (altDeg < -6)
return 0;
// Clear sky parameters
const m = calculateAirMass(altDeg);
// Rain/Snow/Clouds reduce atmospheric transmittance
// But we rely on applyWeatherModifiers for bulk reduction.
// Here we model the SHAPE change.
// Heavier clouds = more diffusion, less direct dependency on air mass path length?
// Actually, simply shifting weight from Direct to Ambient models this best.
const tau = 0.75;
const directClear = tau ** m;
// Ambient Component (Diffuse twilight glow + scattered light)
// Reaches ~5% of possible direct intensity at horizon for clear sky
const ambientClear = altDeg < 0 ? ((altDeg + 6) / 6) ** 2 * 0.05 : 0.05 + (altDeg / 90) * 0.05;
// Overcast model (CIE Standard Overcast Sky)
// L = Lz * (1 + 2sin(a))/3
// Normalized to zenith=1 roughly for shape comparison
const altRad = Math.max(0, (altDeg * Math.PI) / 180);
const overcastShape = (1 + 2 * Math.sin(altRad)) / 3;
// Mix shapes
const shape = directClear * (1 - cloudMix) + overcastShape * cloudMix; // Direct dominance fades
const ambient = ambientClear * (1 - cloudMix); // Clear sky ambient fades into the general overcast glow
// For overcast, the "ambient" is essentially the whole signal, handled by overcastShape.
// But we need to maintain the twilight roll-off for the overcast shape too
const twilightFactor = altDeg < -6 ? 0 : altDeg < 0 ? ((altDeg + 6) / 6) ** 2 : 1;
return (shape + ambient) * twilightFactor;
};
const rawIntensity = calculateIntensity(altitudeDeg);
// We normalize against the MAX altitude for the day, but using the SAME weather.
// This preserves the curve SHAPE but normalized to 0-1 for the daily range.
// The bulk attenuation is handled by applyWeatherModifiers.
const maxDailyIntensity = calculateIntensity(maxAltitudeDeg);
const intensityFactor = maxDailyIntensity > 0.001 ? rawIntensity / maxDailyIntensity : 0;
return [Math.max(0, Math.min(1, cctFactor)), Math.max(0, Math.min(1, intensityFactor)), rawIntensity];
}
/**
* Blackbody Sun model.
* Simulates the sun as a blackbody shifting through the atmosphere.
* @param altitude Sun altitude in radians
* @param maxAltitude Maximum sun altitude for the day in radians
*/
export function calculateRealisticBlackbodyDaylight(altitude, maxAltitude) {
const altitudeDeg = (altitude * 180) / Math.PI;
const maxAltitudeDeg = (maxAltitude * 180) / Math.PI;
let cctFactor;
if (altitudeDeg < -6) {
cctFactor = 0;
}
else {
// Shifts faster at the beginning, then stabilizes.
// Simulates the Rayleigh scattering effect where blue light is lost at low angles.
cctFactor = 1 - Math.exp(-0.08 * (altitudeDeg + 6));
}
const calculateIntensity = (altDeg) => {
if (altDeg < -6)
return 0;
const m = calculateAirMass(altDeg);
const tau = 0.7;
const direct = tau ** m;
// Blackbody ambient is slightly warmer/weaker initially
const ambient = altDeg < 0 ? ((altDeg + 6) / 6) ** 2 * 0.04 : 0.04 + (altDeg / 90) * 0.04;
return direct + ambient;
};
const rawIntensity = calculateIntensity(altitudeDeg);
const maxDailyIntensity = calculateIntensity(maxAltitudeDeg);
const intensityFactor = maxDailyIntensity > 0.001 ? rawIntensity / maxDailyIntensity : 0;
return [Math.max(0, Math.min(1, cctFactor)), Math.max(0, Math.min(1, intensityFactor)), rawIntensity];
}
/**
* Hazy/Turbid model.
* Simulates a sky with high particulate matter (smog/mist).
* @param altitude Sun altitude in radians
* @param maxAltitude Maximum sun altitude for the day in radians
*/
export function calculateRealisticHazyDaylight(altitude, maxAltitude) {
const altitudeDeg = (altitude * 180) / Math.PI;
const maxAltitudeDeg = (maxAltitude * 180) / Math.PI;
let cctFactor;
if (altitudeDeg < -6) {
cctFactor = 0;
}
else {
// Hazy skies have more scattering even at high angles,
// so CCT doesn't reach "pure blue" as easily (approximated by slower exponent)
cctFactor = 1 - Math.exp(-0.03 * (altitudeDeg + 6));
}
const calculateIntensity = (altDeg) => {
if (altDeg < -6)
return 0;
const m = calculateAirMass(altDeg);
const tau = 0.5; // Significant turbidity
const direct = tau ** m;
// Hazy ambient is stronger due to more scattering (Mie scattering)
const ambient = altDeg < 0 ? ((altDeg + 6) / 6) ** 1.5 * 0.08 : 0.08 + (altDeg / 90) * 0.04;
return direct + ambient;
};
const rawIntensity = calculateIntensity(altitudeDeg);
const maxDailyIntensity = calculateIntensity(maxAltitudeDeg);
const intensityFactor = maxDailyIntensity > 0.001 ? rawIntensity / maxDailyIntensity : 0;
return [Math.max(0, Math.min(1, cctFactor)), Math.max(0, Math.min(1, intensityFactor)), rawIntensity];
}
//# sourceMappingURL=realistic.js.map