The Sun lights the globe where it really is (wwwroot/js/sun.js, the Astronomical Almanac's low-precision solar coordinates): the day map shaded by the Sun's height and the Earth–Sun distance, warm city lights on the night side, and a twilight blend about 13° wide between them. It shows now, refreshed every 30 s, or the moment of the hovered, focused or pinned post: a click on a card pins it, and the subsolar point travels there the shorter way round. The panel's top corner shows that moment, the phase of the day and solar time at the post's place, and the season in its hemisphere (or in both, for a post without a place). Its tooltip shows the subsolar point, declination, axial tilt, distance, irradiance, day length and the next equinox or solstice. Land borders are thin paths. Country names are in the reader's language and appear as the camera comes closer: largest countries first, away from the globe's edge, never overlapping. They come from world-atlas 2.0.2 (Natural Earth 1:50m), prepared by tools/globe/prepare-countries.py, and are vendored with their hashes in SOURCES.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
84 lines
4.0 KiB
JavaScript
84 lines
4.0 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)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|