GODSIGH/js/layers/celestial.js
type-two b3499381bf feat: celestial layer — zodiac ring + Sun/Moon/planets in true sky positions
New 'Sky: zodiac · planets' layer (Space, default off). All computed, no data:
- 12 zodiac signs labeled around the ecliptic ring
- Sun & Moon from Cesium's Simon1994 model, 5 naked-eye planets from Schlyter's
  low-precision series — correct directions, verified (Sun sits in the right
  sign for the date, planets hug the ecliptic)
- placed in the INERTIAL frame (CallbackProperty × ICRF→Fixed each frame) so the
  sky stays star-locked while the globe rotates; distances compressed to one
  display ring beyond GEO so they're visible when zoomed out

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:19:21 +10:00

162 lines
8.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.

// Celestial layer (Wave 3.2) — the sky around the world: the 12 zodiac signs as
// a labeled ecliptic ring, plus the Sun, Moon and naked-eye planets, all placed
// in their TRUE directions and locked to the stars (inertial frame), not spun
// with the globe. A "distant visual" you see when zoomed out.
//
// Everything is computed (no external data): Sun/Moon from Cesium's Simon1994
// model, planets from Schlyter's low-precision series, the zodiac from the
// ecliptic geometry. Directions are correct; distances are COMPRESSED to one
// display radius so they ring the Earth instead of vanishing at true scale.
export default function create(ctx) {
const { viewer, Cesium, lib, ui } = ctx;
const scene = viewer.scene;
const ds = new Cesium.CustomDataSource('celestial');
viewer.dataSources.add(ds);
const R = 8.0e7; // display radius (~80,000 km) — a ring beyond GEO
const OBLIQ = Cesium.Math.toRadians(23.4393); // ecliptic obliquity
const cosE = Math.cos(OBLIQ), sinE = Math.sin(OBLIQ);
// Shared per-frame inertial→fixed rotation (computed once per tick, reused by
// every object's position callback).
let frameIcrf = Cesium.Matrix3.IDENTITY.clone();
scene.postUpdate.addEventListener((s, time) => {
const m = Cesium.Transforms.computeIcrfToFixedMatrix(time);
if (Cesium.defined(m)) frameIcrf = m;
});
// Inertial unit direction → fixed-frame position at radius `rad`.
const scratch = new Cesium.Cartesian3();
function fixedPos(dirInertial, rad) {
Cesium.Matrix3.multiplyByVector(frameIcrf, dirInertial, scratch);
return Cesium.Cartesian3.multiplyByScalar(scratch, rad, new Cesium.Cartesian3());
}
// A CallbackProperty position from a function(time) -> inertial unit vector.
function skyPosition(dirFn, rad = R) {
return new Cesium.CallbackProperty((time) => fixedPos(dirFn(time), rad), false);
}
// Ecliptic (lon λ, lat β=0) → inertial equatorial unit vector.
function eclipticDir(lonRad, latRad = 0) {
const cl = Math.cos(lonRad), sl = Math.sin(lonRad), cb = Math.cos(latRad), sb = Math.sin(latRad);
const xe = cb * cl;
const ye = cb * sl * cosE - sb * sinE;
const ze = cb * sl * sinE + sb * cosE;
return new Cesium.Cartesian3(xe, ye, ze);
}
// Days since 2000 Jan 0.0 UTC (Schlyter's epoch).
// d = JD 2451543.5 (Schlyter epoch); unix-epoch JD is 2440587.5 → offset 10956.0.
const dayNumber = (time) => Cesium.JulianDate.toDate(time).getTime() / 86400000 - 10956.0;
// ---- planets (Schlyter low-precision) ----
const rev = (x) => x - Math.floor(x / 360) * 360;
const D2R = Math.PI / 180;
// Orbital elements as functions of day d (degrees / AU).
const ELEM = {
Mercury: (d) => ({ N: 48.3313 + 3.24587e-5 * d, i: 7.0047 + 5.00e-8 * d, w: 29.1241 + 1.01444e-5 * d, a: 0.387098, e: 0.205635 + 5.59e-10 * d, M: 168.6562 + 4.0923344368 * d }),
Venus: (d) => ({ N: 76.6799 + 2.46590e-5 * d, i: 3.3946 + 2.75e-8 * d, w: 54.8910 + 1.38374e-5 * d, a: 0.723330, e: 0.006773 - 1.302e-9 * d, M: 48.0052 + 1.6021302244 * d }),
Mars: (d) => ({ N: 49.5574 + 2.11081e-5 * d, i: 1.8497 - 1.78e-8 * d, w: 286.5016 + 2.92961e-5 * d, a: 1.523688, e: 0.093405 + 2.516e-9 * d, M: 18.6021 + 0.5240207766 * d }),
Jupiter: (d) => ({ N: 100.4542 + 2.76854e-5 * d, i: 1.3030 - 1.557e-7 * d, w: 273.8777 + 1.64505e-5 * d, a: 5.20256, e: 0.048498 + 4.469e-9 * d, M: 19.8950 + 0.0830853001 * d }),
Saturn: (d) => ({ N: 113.6634 + 2.38980e-5 * d, i: 2.4886 - 1.081e-7 * d, w: 339.3939 + 2.97661e-5 * d, a: 9.55475, e: 0.055546 - 9.499e-9 * d, M: 316.9670 + 0.0334442282 * d }),
};
function sunRect(d) { // Sun's geocentric ecliptic rectangular (AU)
const w = 282.9404 + 4.70935e-5 * d, e = 0.016709 - 1.151e-9 * d, M = rev(356.0470 + 0.9856002585 * d);
const E = M + (180 / Math.PI) * e * Math.sin(M * D2R) * (1 + e * Math.cos(M * D2R));
const xv = Math.cos(E * D2R) - e, yv = Math.sqrt(1 - e * e) * Math.sin(E * D2R);
const v = Math.atan2(yv, xv), r = Math.hypot(xv, yv), lon = v + w * D2R;
return { x: r * Math.cos(lon), y: r * Math.sin(lon) };
}
function planetDir(name, time) {
const d = dayNumber(time);
const el = ELEM[name](d);
const N = el.N * D2R, i = el.i * D2R, w = el.w * D2R, e = el.e, M = rev(el.M) * D2R;
let E = M + e * Math.sin(M) * (1 + e * Math.cos(M));
for (let k = 0; k < 5; k++) E = E - (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E));
const xv = el.a * (Math.cos(E) - e), yv = el.a * Math.sqrt(1 - e * e) * Math.sin(E);
const v = Math.atan2(yv, xv), r = Math.hypot(xv, yv), u = v + w;
// Heliocentric ecliptic rectangular.
const xh = r * (Math.cos(N) * Math.cos(u) - Math.sin(N) * Math.sin(u) * Math.cos(i));
const yh = r * (Math.sin(N) * Math.cos(u) + Math.cos(N) * Math.sin(u) * Math.cos(i));
const zh = r * Math.sin(u) * Math.sin(i);
// Geocentric = heliocentric + (Earth→Sun) = helio + sun-geocentric.
const s = sunRect(d);
const xg = xh + s.x, yg = yh + s.y, zg = zh;
const lon = Math.atan2(yg, xg), lat = Math.atan2(zg, Math.hypot(xg, yg));
return eclipticDir(lon, lat);
}
const sunDir = (time) => {
const p = Cesium.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(time);
return Cesium.Cartesian3.normalize(p, new Cesium.Cartesian3());
};
const moonDir = (time) => {
const p = Cesium.Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame(time);
return Cesium.Cartesian3.normalize(p, new Cesium.Cartesian3());
};
// ---- build entities ----
const bodyLabel = (extra) => ({
font: '13px "Segoe UI", system-ui, sans-serif',
fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.fromCssColorString('#05080b'),
outlineWidth: 3, style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, -12),
...extra,
});
const BODIES = [
{ name: '☉ Sun', dir: sunDir, color: '#ffcf40', size: 22, rad: R },
{ name: '☾ Moon', dir: moonDir, color: '#d8d8e0', size: 16, rad: R * 0.9 },
{ name: '☿ Mercury', dir: (t) => planetDir('Mercury', t), color: '#b0a080', size: 7 },
{ name: '♀ Venus', dir: (t) => planetDir('Venus', t), color: '#f5e8c0', size: 10 },
{ name: '♂ Mars', dir: (t) => planetDir('Mars', t), color: '#ff6b4d', size: 8 },
{ name: '♃ Jupiter', dir: (t) => planetDir('Jupiter', t), color: '#e8c090', size: 12 },
{ name: '♄ Saturn', dir: (t) => planetDir('Saturn', t), color: '#e8dcb0', size: 10 },
];
for (const b of BODIES) {
ds.entities.add({
name: b.name,
position: skyPosition(b.dir, b.rad || R),
point: { pixelSize: b.size, color: lib.cz(b.color), outlineColor: Cesium.Color.BLACK, outlineWidth: 1 },
label: bodyLabel({ text: b.name, fillColor: lib.cz(b.color) }),
});
}
// Zodiac: 12 sign labels at the centre of each 30° sector + the ecliptic ring.
const SIGNS = ['♈ Aries', '♉ Taurus', '♊ Gemini', '♋ Cancer', '♌ Leo', '♍ Virgo',
'♎ Libra', '♏ Scorpio', '♐ Sagittarius', '♑ Capricorn', '♒ Aquarius', '♓ Pisces'];
for (let k = 0; k < 12; k++) {
const lon = Cesium.Math.toRadians(k * 30 + 15);
ds.entities.add({
name: SIGNS[k],
position: fixedPosStatic(eclipticDir(lon), R * 1.06), // static ok: sky barely moves in ±6h
label: bodyLabel({ text: SIGNS[k], font: '12px "Segoe UI", system-ui, sans-serif', fillColor: lib.cz('#8fb7ff'), pixelOffset: new Cesium.Cartesian2(0, 0) }),
});
}
// Ecliptic ring + 30° boundary ticks (rebuilt each frame in the fixed frame).
ds.entities.add({
name: 'Ecliptic',
polyline: {
width: 1.5,
material: new Cesium.PolylineGlowMaterialProperty({ glowPower: 0.25, color: lib.cz('#8fb7ff', 0.5) }),
positions: new Cesium.CallbackProperty(() => {
const pts = [];
for (let a = 0; a <= 360; a += 4) pts.push(fixedPos(eclipticDir(Cesium.Math.toRadians(a)), R * 1.02));
return pts;
}, false),
},
});
// A single static-ish snapshot position (zodiac labels don't need per-frame update
// at this zoom; recompute once so they sit in the current fixed frame).
function fixedPosStatic(dirInertial, rad) {
return skyPosition(() => dirInertial, rad); // still callback so it tracks Earth rotation
}
ui.addLayer('celestial', 'Sky: zodiac · planets', false, (on) => { ds.show = on; }, 'Space');
ui.setStatus('celestial', 'Sun · Moon · 5 planets · zodiac', 'ok');
ds.show = false;
return { id: 'celestial' };
}