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
93 lines
4.4 KiB
JavaScript
93 lines
4.4 KiB
JavaScript
// 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
|
||
}
|