The globe under the Sun of now or of a post, with borders and country names
Build / Build (push) Successful in 46s
Deploy / decepub.thepra.dev (push) Successful in 1m52s

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
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-04 20:23:42 +02:00
1 parent 69d0a641fe
commit 0f68e74e38
13 files changed
+1066 -50

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
// 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
}