Files
decePubClient/wwwroot/js/sun.js
T
thepraandClaude Opus 5.5 18331531b8 The Sun's year is a pure function too, checked against the almanac
sun.js gains analemma(date, days): where the Sun is overhead at the same UTC time on each coming day, the figure-8 the
globe will draw. tools/globe/check.mjs checks the pure functions against almanac values with Node's own test runner
(dev only, nothing installed): the 2026 June solstice's declination, the March equinox, perihelion's distance, Turin's
longest day, the season in October, and the analemma spanning the tropics and the equation of time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-06 23:51:35 +02:00

93 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Where the Sun is, for the globe's day and night (wwwroot/js/globe.js): the Astronomical Almanac's low-precision solar
// coordinates, good to about 0.01° between 1950 and 2050. Everything here is a pure function of a date.
const radians = Math.PI / 180
const J2000 = Date.UTC(2000, 0, 1, 12)//2000-01-01 12:00 UTC
// The subsolar point (where the Sun is overhead), the Earth–Sun distance and the angles that place the Sun in the year.
export function sunAt(date) {
const days = (date.getTime() - J2000) / 86400000
const meanLongitude = 280.460 + 0.9856474 * days
const meanAnomaly = (357.528 + 0.9856003 * days) * radians
const eclipticLongitude = degrees(meanLongitude + 1.915 * Math.sin(meanAnomaly) + 0.020 * Math.sin(2 * meanAnomaly))
const obliquity = 23.439 - 0.0000004 * days
const declination = Math.asin(Math.sin(obliquity * radians) * Math.sin(eclipticLongitude * radians)) / radians
const rightAscension = Math.atan2(Math.cos(obliquity * radians) * Math.sin(eclipticLongitude * radians), Math.cos(eclipticLongitude * radians)) / radians
const siderealTime = 280.46061837 + 360.98564736629 * days
const distance = 1.00014 - 0.01671 * Math.cos(meanAnomaly) - 0.00014 * Math.cos(2 * meanAnomaly)
return {
date,
lat: declination,
lng: longitude(rightAscension - siderealTime),
distance,
irradiance: 1 / (distance * distance),
eclipticLongitude,
obliquity
}
}
// What the Sun does at a place: its elevation, the phase of the day, the local solar time, the day's length, the season
// in that hemisphere and the next equinox or solstice.
export function sunAtPlace(sun, lat, lng) {
const hourAngle = longitude(lng - sun.lng)
const elevation = Math.asin(
Math.sin(lat * radians) * Math.sin(sun.lat * radians) + Math.cos(lat * radians) * Math.cos(sun.lat * radians) * Math.cos(hourAngle * radians)) / radians
const solarHours = (((12 + hourAngle / 15) % 24) + 24) % 24
// sunrise and sunset as almanacs give them: the Sun's upper edge on the horizon, refraction included (−0.833°)
const sunset = (Math.sin(-0.833 * radians) - Math.sin(lat * radians) * Math.sin(sun.lat * radians)) /
(Math.cos(lat * radians) * Math.cos(sun.lat * radians))
const dayLength = sunset <= -1 ? 24 : sunset >= 1 ? 0 : 2 * Math.acos(sunset) / radians / 15
const north = lat >= 0
const seasons = seasonsOf(sun)
return {
elevation,
phase: elevation > 6 ? "day" : elevation > 0 ? "golden" : elevation > -6 ? "civil" : elevation > -12 ? "nautical" : elevation > -18 ? "astronomical" : "night",
morning: hourAngle < 0,
solarTime: `${String(Math.floor(solarHours)).padStart(2, "0")}:${String(Math.floor(solarHours % 1 * 60)).padStart(2, "0")}`,
dayLength,
hemisphere: north ? "north" : "south",
season: north ? seasons.north : seasons.south,
nextEvent: seasons.nextEvent,
nextEventDays: seasons.nextEventDays
}
}
// The astronomical season in each hemisphere (from the equinoxes and solstices, by the Sun's ecliptic longitude) and the
// next equinox or solstice, in days at the Sun's mean pace.
export function seasonsOf(sun) {
const quarter = Math.floor(sun.eclipticLongitude / 90) % 4
const nextBoundary = (quarter + 1) * 90
return {
north: ["spring", "summer", "autumn", "winter"][quarter],
south: ["autumn", "winter", "spring", "summer"][quarter],
nextEvent: { 90: "june-solstice", 180: "september-equinox", 270: "december-solstice", 360: "march-equinox" }[nextBoundary],
nextEventDays: Math.round((nextBoundary - sun.eclipticLongitude) / 0.9856)
}
}
// Where the Sun is overhead at this same UTC time on each of the coming days (the first is the date itself): traced on
// the globe, the analemma, its figure-8 the year's declination and the equation of time.
export function analemma(date, days = 366) {
const points = []
for (let day = 0; day < days; day++)
points.push(sunAt(new Date(date.getTime() + day * 86400000)))
return points
}
// The Sun's direction in the frame of three.js's SphereGeometry, the frame the globe's shader draws in: texture u is
// 0 at longitude −180°, and a point is (−cos 2πu · cos lat, sin lat, sin 2πu · cos lat).
export function directionOf(sun) {
const u = (sun.lng + 180) / 360 * 2 * Math.PI
const lat = sun.lat * radians
return [-Math.cos(u) * Math.cos(lat), Math.sin(lat), Math.sin(u) * Math.cos(lat)]
}
function degrees(value) {
return ((value % 360) + 360) % 360
}
function longitude(value) {
const wrapped = degrees(value + 180) - 180
return wrapped === -180 ? 180 : wrapped
}