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>
162 lines
8.4 KiB
JavaScript
162 lines
8.4 KiB
JavaScript
// 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' };
|
||
}
|