Works out the clock hour for a named time of day (dawn, noon, golden hour, dusk and so on), the date for a season, and the exposure for the weather.
How the precision is kept
Sunrise, sunset and solar noon are found by sampling the sun's altitude every 0.05 hour (3 minutes) through the day, not by solving for them. Hours are rounded to 0.01 hour before they are returned and saved with the model's sun.
Calculations under this policy
3 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The clock hours of sunrise, sunset and solar noon and the sun's highest altitude on a day.
hᵢ = min(0.05·i, 23.99), i = 0 … 480; rise = first hᵢ with altitude > 0 after one ≤ 0; noon = hᵢ of the highest altitude
The times are sample times, so each is known to one step of 0.05 hour. Noon and the highest altitude are rounded to two decimals; rise and set are returned as sampled. Where the sun does not cross the horizon, rise or set is null.
scene-say.js
lines 44 to 54
ModelCraft 0.8.1
/** Sunrise, sunset and solar noon (local clock hours) and the highest the sun gets, on the day in `sun` ({month, day, lat, lon, tz}). */
export function dayTimes(sun) {
let rise = null, set = null, noon = 12, maxAlt = -90, prev = null;
for (let i = 0; i <= 480; i++) {
const h = Math.min(i * 0.05, 23.99), a = sunPosition({ ...sun, hour: h }).altitude;
if (a > maxAlt) { maxAlt = a; noon = h; }
if (prev !== null) { if (prev <= 0 && a > 0 && rise === null) rise = h; if (prev > 0 && a <= 0) set = h; }
prev = a;
}
return { rise, set, noon: r2(noon), maxAlt: r2(maxAlt) };
}
Clock hour for a named time
64-bit
The hour that stands for dawn, morning, noon, afternoon, golden hour, dusk or night on a given day.
dawn = rise + 0.3; morning = rise + 0.45·(noon − rise); afternoon = noon + 0.55·(set − noon); dusk = set − 0.1; night = set + 2; hour = round to 0.01
The offsets are fixed choices applied to the sampled times. Golden hour steps from noon in 0.05 hour steps by repeated addition and takes the first step at 7° altitude or lower. The result is rounded to 0.01 hour (36 seconds) and that rounded value is what is stored.
scene-say.js
lines 60 to 90
ModelCraft 0.8.1
/** A named time of day (or an hour, 0–24) on the day in `sun`: the hour on the clock, the sky it asks for, its exposure nudge. */
export function hourFor(time, sun) {
const notes = [];
if (typeof time === 'number') {
const hour = Math.min(23.99, Math.max(0, time));
return { hour: r2(hour), sky: null, exposure: 0, night: sunPosition({ ...sun, hour }).altitude <= 0, notes };
}
if (!TIMES.includes(time)) throw new RangeError('time is one of ' + TIMES.join(', ') + ', or an hour from 0 to 24.');
const d = dayTimes(sun);
let hour;
if (d.rise === null || d.set === null) {
hour = FIXED[time];
notes.push(d.maxAlt > 0 ? 'The sun does not set on this date at this latitude: the clock hour is a plain ' + hour + ':00.' : 'The sun does not rise on this date at this latitude: the picture is dark whatever the hour.');
} else if (time === 'dawn') hour = d.rise + 0.3;
else if (time === 'morning') hour = d.rise + 0.45 * (d.noon - d.rise);
else if (time === 'noon') hour = d.noon;
else if (time === 'afternoon') hour = d.noon + 0.55 * (d.set - d.noon);
else if (time === 'golden-hour') {
let h = d.set - 0.8;
for (let x = d.noon; x < d.set; x += 0.05) if (sunPosition({ ...sun, hour: x }).altitude <= 7) { h = x; break; }
hour = h;
} else if (time === 'dusk') hour = d.set - 0.1;
else {
hour = d.set + 2;
if (hour > 23.9) { hour = 23.9; notes.push('Night falls late on this date here: 23:54 is the darkest clock time on the day.'); }
}
hour = r2(Math.min(23.99, Math.max(0, hour)));
const night = sunPosition({ ...sun, hour }).altitude <= 0;
if (night) notes.push('The sun is down: the sky is the dark night sky and only lamps (Render › lamps) and lit windows light the picture.');
return { hour, sky: time === 'dusk' ? 'dusk' : null, exposure: TIME_EXPOSURE[time] || 0, night, notes };
}
The look for the choices made
64-bit
Combines time, date or season and weather into the sun's date and hour and the render's exposure.
month, day = round(given); exposure = round((weather + time of day) × 100) ÷ 100
A given month and day are rounded to whole numbers. The hour and the exposure are rounded to two decimals. The weather's exposure, fog visibility and wetness are fixed table values.
scene-say.js
lines 136 to 168
ModelCraft 0.8.1
/**
* The whole look for the choices made. `sun` = the model's sun {month, day, hour, lat, lon, tz}; `engine` = the render settings'
* engine id (for the notes only). time may be a name, an hour, or absent (the sun's hour stays). date {month, day} beats season.
*/
export function lookFor({ time, date, season, weather, sun, engine = '' } = {}) {
const base = { month: 6, day: 21, hour: 15, lat: 40, lon: -105, tz: -7, ...(sun || {}) };
const notes = [];
let seas = null;
let day = { month: base.month, day: base.day };
if (date) {
const m = Math.round(date.month), d = Math.round(date.day);
if (!(m >= 1 && m <= 12 && d >= 1 && d <= 31)) throw new RangeError('date is {month: 1–12, day: 1–31}.');
day = { month: m, day: d };
seas = season || seasonOf(m, base.lat);
} else if (season) { day = seasonDate(season, base.lat); seas = season; }
const onDay = { ...base, ...day };
let hour = base.hour, skyHint = null, expo = 0;
if (time !== undefined && time !== null) {
const h = hourFor(time, onDay);
hour = h.hour; skyHint = h.sky; expo = h.exposure; notes.push(...h.notes);
}
const wName = weather || 'clear';
const w = weatherLook(wName);
const sky = w.sky || skyHint || 'clear';
const nightNow = sunPosition({ ...onDay, hour }).altitude <= 0;
notes.push(...engineNotes(wName, engine));
return {
sun: { month: day.month, day: day.day, hour: r2(hour) },
patch: { sky, exposure: r2(w.exposure + expo), fog: w.fog ? { ...w.fog } : { on: false }, sun: null },
wet: w.wet, rain: w.rain, season: seas || seasonOf(day.month, base.lat), seasonGiven: !!seas,
weather: wName, night: nightNow, notes,
};
}
In detail
Method
481 samples of sunPosition (sun.js) from 0:00 to 23.99; sunrise is the first sample above the horizon after one at or below it.
Number types
64-bit numbers; hours and exposure rounded to two decimals.
Tolerances
Sample step 0.05 hour. Golden hour is the first sample after solar noon with altitude ≤ 7°. Returned hours are clamped to 0 to 23.99.
Limits
Sunrise and sunset are up to 3 minutes late (the first sample after the crossing); solar noon is the sample with the highest altitude.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.