Move Elsewhen into Omarchy as omarchy.elsewhen (#13429)
* Move Elsewhen into Omarchy as omarchy.elsewhen The world clock ships in the shell tree instead of its own package. A migration renames existing bar entries with their settings and removes the retired package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Remove Elsewhen's shelved Earth row, overlap band and sky tint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tidy Elsewhen's models, data helper and docs, and drop currency Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restructure Elsewhen's panel and globe on the shared shell components Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show Elsewhen's bar globe upright Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use the shared city search for Elsewhen's globe jump bar Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep legacy Elsewhen entries single and retry offline geocodes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fetch Elsewhen's weather with curl instead of a Python helper Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c7cef1e78
commit
e7218e36f2
37 files changed
+6780
-155
No files matched your search
@@ -0,0 +1,300 @@
|
||||
.pragma library
|
||||
|
||||
// Orthographic globe math: projection, the day/night terminator, the moon,
|
||||
// and label placement.
|
||||
|
||||
var DEG = Math.PI / 180
|
||||
|
||||
// Earth's obliquity, also the hero icon's lean.
|
||||
var AXIAL_TILT = 23.44
|
||||
|
||||
// Every `keepEvery`th vertex of a flat [lon, lat, ...] ring, for drawing while
|
||||
// the globe is scaled down. Rings at or under `minPoints` are returned whole.
|
||||
function decimateRing(ring, keepEvery, minPoints) {
|
||||
var step = keepEvery === undefined ? 2 : keepEvery
|
||||
var floor = minPoints === undefined ? 8 : minPoints
|
||||
var n = ring.length / 2
|
||||
if (step < 2 || n <= floor) return ring
|
||||
var out = []
|
||||
for (var i = 0; i < n; i += step) out.push(ring[i * 2], ring[i * 2 + 1])
|
||||
// End on the original last vertex, not a chord back to the start.
|
||||
var lastI = (n - 1) * 2
|
||||
if (out[out.length - 2] !== ring[lastI] || out[out.length - 1] !== ring[lastI + 1])
|
||||
out.push(ring[lastI], ring[lastI + 1])
|
||||
return out
|
||||
}
|
||||
|
||||
// A drawn pixel size that follows the shell's UI scale, floored so hairlines
|
||||
// do not drop out of the raster.
|
||||
function scalePx(px, scale, minPx) {
|
||||
var s = (typeof scale === "number" && isFinite(scale) && scale > 0) ? scale : 1
|
||||
var n = px * s
|
||||
var floor = (minPx === undefined) ? 1 : minPx
|
||||
return n < floor ? floor : n
|
||||
}
|
||||
|
||||
// Orthographic projection onto a disc of radius r seen from over (viewLat, spin).
|
||||
function project(lat, lon, spin, viewLat, r) {
|
||||
var phi = lat * DEG
|
||||
var lam = (lon - spin) * DEG
|
||||
var p0 = viewLat * DEG
|
||||
var cosc = Math.sin(p0) * Math.sin(phi) + Math.cos(p0) * Math.cos(phi) * Math.cos(lam)
|
||||
return {
|
||||
x: r * Math.cos(phi) * Math.sin(lam),
|
||||
y: -r * (Math.cos(p0) * Math.sin(phi) - Math.sin(p0) * Math.cos(phi) * Math.cos(lam)),
|
||||
visible: cosc > 0,
|
||||
cosc: cosc
|
||||
}
|
||||
}
|
||||
|
||||
// The point with the sun directly overhead, good to a fraction of a degree.
|
||||
function subsolarPoint(ms) {
|
||||
var d = new Date(ms)
|
||||
var jd = ms / 86400000 + 2440587.5
|
||||
var n = jd - 2451545.0
|
||||
var L = (280.460 + 0.9856474 * n) % 360 // mean longitude
|
||||
var g = ((357.528 + 0.9856003 * n) % 360) * DEG // mean anomaly
|
||||
var lambda = (L + 1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g)) * DEG // ecliptic longitude
|
||||
var eps = (AXIAL_TILT - 0.0000004 * n) * DEG // obliquity, slowly drifting
|
||||
|
||||
var decl = Math.asin(Math.sin(eps) * Math.sin(lambda)) / DEG
|
||||
|
||||
// Equation of time, in minutes, then the subsolar meridian.
|
||||
var alpha = Math.atan2(Math.cos(eps) * Math.sin(lambda), Math.cos(lambda)) / DEG
|
||||
var eot = (L - alpha + 540) % 360 - 180 // degrees, wrapped to +-180
|
||||
var utcHours = d.getUTCHours() + d.getUTCMinutes() / 60 + d.getUTCSeconds() / 3600
|
||||
var lon = -15 * (utcHours - 12) - eot
|
||||
lon = ((lon + 540) % 360) - 180
|
||||
|
||||
return { lat: decl, lon: lon }
|
||||
}
|
||||
|
||||
// Degrees above the horizon, negative below it.
|
||||
function solarElevation(lat, lon, sub) {
|
||||
var cosz = Math.sin(lat * DEG) * Math.sin(sub.lat * DEG)
|
||||
+ Math.cos(lat * DEG) * Math.cos(sub.lat * DEG) * Math.cos((lon - sub.lon) * DEG)
|
||||
return Math.asin(Math.max(-1, Math.min(1, cosz))) / DEG
|
||||
}
|
||||
|
||||
// -0.833 allows for refraction and the sun's disc, as sunrise tables do.
|
||||
function isDaylight(lat, lon, sub) {
|
||||
return solarElevation(lat, lon, sub) > -0.833
|
||||
}
|
||||
|
||||
// The great circle 90 degrees from the subsolar point: the day/night line.
|
||||
function terminator(sub, steps) {
|
||||
var n = steps || 180
|
||||
var out = []
|
||||
var slat = sub.lat * DEG, slon = sub.lon * DEG
|
||||
// Build an orthonormal frame around the subsolar axis and sweep a circle.
|
||||
var s = [Math.cos(slat) * Math.cos(slon), Math.cos(slat) * Math.sin(slon), Math.sin(slat)]
|
||||
var up = Math.abs(s[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0]
|
||||
var a = norm(cross(up, s))
|
||||
var b = norm(cross(s, a))
|
||||
for (var i = 0; i <= n; i++) {
|
||||
var t = i / n * 2 * Math.PI
|
||||
var v = [a[0] * Math.cos(t) + b[0] * Math.sin(t),
|
||||
a[1] * Math.cos(t) + b[1] * Math.sin(t),
|
||||
a[2] * Math.cos(t) + b[2] * Math.sin(t)]
|
||||
out.push([Math.asin(v[2]) / DEG, Math.atan2(v[1], v[0]) / DEG])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function cross(u, v) {
|
||||
return [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]]
|
||||
}
|
||||
|
||||
function norm(v) {
|
||||
var m = Math.hypot(v[0], v[1], v[2]) || 1
|
||||
return [v[0] / m, v[1] / m, v[2] / m]
|
||||
}
|
||||
|
||||
// Keep points, in priority order, that clear every kept point by minDist.
|
||||
// Points marked `keep` always survive.
|
||||
function declutter(points, minDist) {
|
||||
var kept = []
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
var p = points[i]
|
||||
if (p.keep) { kept.push(p); continue }
|
||||
var clash = false
|
||||
for (var j = 0; j < kept.length; j++) {
|
||||
if (Math.hypot(p.x - kept[j].x, p.y - kept[j].y) < minDist) { clash = true; break }
|
||||
}
|
||||
if (!clash) kept.push(p)
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
||||
// ---- the moon
|
||||
|
||||
// The mean synodic month against a known new moon: good to a few hours.
|
||||
var SYNODIC_MONTH = 29.530588853 // days
|
||||
var KNOWN_NEW_MOON_JD = 2451550.1 // 2000-01-06 18:14 UTC
|
||||
|
||||
// Position through the lunation: 0 new, 0.25 first quarter, 0.5 full,
|
||||
// 0.75 last quarter.
|
||||
function moonPhase(ms) {
|
||||
var jd = Number(ms) / 86400000 + 2440587.5
|
||||
var p = ((jd - KNOWN_NEW_MOON_JD) / SYNODIC_MONTH) % 1
|
||||
return p < 0 ? p + 1 : p
|
||||
}
|
||||
|
||||
// The lit part of the moon on a disc of radius r at the origin: the limb, then
|
||||
// the terminator back, a semicircle squashed by the signed cos(2*pi*phase).
|
||||
function moonLitOutline(phase, r, steps) {
|
||||
var n = steps || 24
|
||||
var theta = 2 * Math.PI * Number(phase)
|
||||
var squash = Math.cos(theta)
|
||||
var side = Number(phase) > 0.5 ? -1 : 1 // waning lights the other limb
|
||||
var out = []
|
||||
var i, t
|
||||
for (i = 0; i <= n; i++) {
|
||||
t = Math.PI * i / n
|
||||
out.push({ x: side * r * Math.sin(t), y: -r * Math.cos(t) })
|
||||
}
|
||||
for (i = n; i >= 0; i--) {
|
||||
t = Math.PI * i / n
|
||||
out.push({ x: side * r * squash * Math.sin(t), y: -r * Math.cos(t) })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---- clipping to the disc
|
||||
|
||||
// Where a [lat, lon] segment crosses the horizon, by bisection.
|
||||
function limbCrossing(a, b, spin, viewLat, r) {
|
||||
// Segments spanning the antimeridian cannot be interpolated in lat/lon.
|
||||
if (Math.abs(b[1] - a[1]) > 180) return null
|
||||
var lo = 0, hi = 1
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var m = (lo + hi) / 2
|
||||
var p = project(a[0] + (b[0] - a[0]) * m, a[1] + (b[1] - a[1]) * m, spin, viewLat, r)
|
||||
if (p.visible) lo = m; else hi = m
|
||||
}
|
||||
return project(a[0] + (b[0] - a[0]) * lo, a[1] + (b[1] - a[1]) * lo, spin, viewLat, r)
|
||||
}
|
||||
|
||||
// The near-side runs of a polyline, each ending exactly on the horizon.
|
||||
function visibleSegments(pts, spin, viewLat, r) {
|
||||
var out = [], run = [], prev = null, prevVis = false
|
||||
function flush() { if (run.length > 1) out.push(run); run = [] }
|
||||
for (var i = 0; i < pts.length; i++) {
|
||||
var p = project(pts[i][0], pts[i][1], spin, viewLat, r)
|
||||
if (p.visible) {
|
||||
if (run.length === 0 && prev !== null && !prevVis) {
|
||||
var enter = limbCrossing(pts[i], prev, spin, viewLat, r)
|
||||
if (enter) run.push(enter)
|
||||
}
|
||||
run.push(p)
|
||||
} else {
|
||||
if (run.length > 0 && prev !== null) {
|
||||
var exit = limbCrossing(prev, pts[i], spin, viewLat, r)
|
||||
if (exit) run.push(exit)
|
||||
}
|
||||
flush()
|
||||
}
|
||||
prev = pts[i]; prevVis = p.visible
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
|
||||
// A flat [lon, lat, ...] ring clipped to the visible hemisphere as one polygon
|
||||
// that follows the limb, so its area changes smoothly as the globe turns.
|
||||
function clipRingToDisc(ring, spin, viewLat, r) {
|
||||
var pts = []
|
||||
for (var k = 0; k < ring.length; k += 2) pts.push([ring[k + 1], ring[k]])
|
||||
var out = []
|
||||
for (var i = 0; i < pts.length; i++) {
|
||||
var A = pts[i], B = pts[(i + 1) % pts.length]
|
||||
var pa = project(A[0], A[1], spin, viewLat, r)
|
||||
var pb = project(B[0], B[1], spin, viewLat, r)
|
||||
if (pa.visible && pb.visible) out.push({ p: pb, limb: false })
|
||||
else if (pa.visible) {
|
||||
var ex = limbCrossing(A, B, spin, viewLat, r)
|
||||
if (ex) out.push({ p: ex, limb: true })
|
||||
} else if (pb.visible) {
|
||||
var en = limbCrossing(B, A, spin, viewLat, r)
|
||||
if (en) out.push({ p: en, limb: true })
|
||||
out.push({ p: pb, limb: false })
|
||||
}
|
||||
}
|
||||
if (out.length < 3) return []
|
||||
|
||||
var res = []
|
||||
for (var j = 0; j < out.length; j++) {
|
||||
res.push(out[j].p)
|
||||
var nx = out[(j + 1) % out.length]
|
||||
if (!out[j].limb || !nx.limb) continue
|
||||
var a0 = Math.atan2(out[j].p.y, out[j].p.x)
|
||||
var a1 = Math.atan2(nx.p.y, nx.p.x)
|
||||
var d = a1 - a0
|
||||
while (d > Math.PI) d -= 2 * Math.PI
|
||||
while (d < -Math.PI) d += 2 * Math.PI
|
||||
var steps = Math.max(1, Math.round(Math.abs(d) / 0.15))
|
||||
for (var t = 1; t < steps; t++) {
|
||||
var a = a0 + d * t / steps
|
||||
res.push({ x: r * Math.cos(a), y: r * Math.sin(a) })
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Greedy label placement by rank, then nearest the disc center; a label that
|
||||
// would pass `maxX` flips to the left of its dot. `gap` is dot to name.
|
||||
function layoutLabels(candidates, charWidth, lineHeight, limit, maxX, gap) {
|
||||
var g = (typeof gap === "number" && isFinite(gap) && gap > 0) ? gap : 6
|
||||
var placed = []
|
||||
var sorted = candidates.slice().sort(function (p, q) {
|
||||
if (p.rank !== q.rank) return p.rank - q.rank
|
||||
return q.cosc - p.cosc
|
||||
})
|
||||
for (var i = 0; i < sorted.length; i++) {
|
||||
var c = sorted[i]
|
||||
var w = c.name.length * charWidth
|
||||
var x = c.x + g
|
||||
if (maxX !== undefined && x + w > maxX) x = c.x - g - w
|
||||
var box = { x: x, y: c.y - lineHeight / 2, w: w, h: lineHeight }
|
||||
var clash = false
|
||||
for (var j = 0; j < placed.length; j++) {
|
||||
var o = placed[j].box
|
||||
if (box.x < o.x + o.w && box.x + box.w > o.x && box.y < o.y + o.h && box.y + box.h > o.y) {
|
||||
clash = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (clash) continue
|
||||
placed.push({ index: c.index, box: box })
|
||||
if (limit && placed.length >= limit) break
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
// Signed degrees in (-180, 180] to turn from one longitude to another.
|
||||
function shortestTurn(from, to) {
|
||||
var d = ((to - from) % 360 + 360) % 360
|
||||
return d > 180 ? d - 360 : d
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
AXIAL_TILT: AXIAL_TILT,
|
||||
SYNODIC_MONTH: SYNODIC_MONTH,
|
||||
decimateRing: decimateRing,
|
||||
scalePx: scalePx,
|
||||
project: project,
|
||||
subsolarPoint: subsolarPoint,
|
||||
solarElevation: solarElevation,
|
||||
isDaylight: isDaylight,
|
||||
terminator: terminator,
|
||||
declutter: declutter,
|
||||
moonPhase: moonPhase,
|
||||
moonLitOutline: moonLitOutline,
|
||||
limbCrossing: limbCrossing,
|
||||
visibleSegments: visibleSegments,
|
||||
clipRingToDisc: clipRingToDisc,
|
||||
layoutLabels: layoutLabels,
|
||||
shortestTurn: shortestTurn
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user