@johnmorrisdotca/wakusei-planets 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/NOTICE.md +48 -0
- package/README.md +214 -2
- package/dist/bodies/all.d.ts +16 -0
- package/dist/bodies/all.js +16 -0
- package/dist/bodies/earth.d.ts +1 -0
- package/dist/bodies/earth.js +23 -0
- package/dist/bodies/jupiter.d.ts +1 -0
- package/dist/bodies/jupiter.js +22 -0
- package/dist/bodies/mars.d.ts +1 -0
- package/dist/bodies/mars.js +25 -0
- package/dist/bodies/mercury.d.ts +1 -0
- package/dist/bodies/mercury.js +9 -0
- package/dist/bodies/moons-earth.d.ts +1 -0
- package/dist/bodies/moons-earth.js +37 -0
- package/dist/bodies/moons-jupiter.d.ts +1 -0
- package/dist/bodies/moons-jupiter.js +29 -0
- package/dist/bodies/moons-mars.d.ts +1 -0
- package/dist/bodies/moons-mars.js +4 -0
- package/dist/bodies/moons-neptune.d.ts +1 -0
- package/dist/bodies/moons-neptune.js +11 -0
- package/dist/bodies/moons-pluto.d.ts +1 -0
- package/dist/bodies/moons-pluto.js +5 -0
- package/dist/bodies/moons-saturn.d.ts +1 -0
- package/dist/bodies/moons-saturn.js +34 -0
- package/dist/bodies/moons-uranus.d.ts +1 -0
- package/dist/bodies/moons-uranus.js +7 -0
- package/dist/bodies/neptune.d.ts +1 -0
- package/dist/bodies/neptune.js +19 -0
- package/dist/bodies/pluto.d.ts +1 -0
- package/dist/bodies/pluto.js +22 -0
- package/dist/bodies/saturn.d.ts +1 -0
- package/dist/bodies/saturn.js +14 -0
- package/dist/bodies/sun.d.ts +1 -0
- package/dist/bodies/sun.js +20 -0
- package/dist/bodies/uranus.d.ts +1 -0
- package/dist/bodies/uranus.js +15 -0
- package/dist/bodies/venus.d.ts +1 -0
- package/dist/bodies/venus.js +10 -0
- package/dist/data/orbits.d.ts +20 -0
- package/dist/data/orbits.js +27 -0
- package/dist/element.d.ts +88 -0
- package/dist/element.js +603 -0
- package/dist/ephemeris.d.ts +189 -0
- package/dist/ephemeris.js +340 -0
- package/dist/facts.d.ts +213 -0
- package/dist/facts.js +198 -0
- package/dist/gate.d.ts +37 -0
- package/dist/gate.js +54 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/line.d.ts +82 -0
- package/dist/line.js +280 -0
- package/dist/live.d.ts +42 -0
- package/dist/live.js +62 -0
- package/dist/pose.d.ts +62 -0
- package/dist/pose.js +69 -0
- package/dist/scene/body.d.ts +68 -0
- package/dist/scene/body.js +337 -0
- package/dist/scene/glsl.d.ts +13 -0
- package/dist/scene/glsl.js +186 -0
- package/dist/scene/look.d.ts +106 -0
- package/dist/scene/look.js +35 -0
- package/dist/scene/recipes.d.ts +49 -0
- package/dist/scene/recipes.js +76 -0
- package/dist/scene/rings.d.ts +34 -0
- package/dist/scene/rings.js +64 -0
- package/dist/scene/shared.d.ts +24 -0
- package/dist/scene/shared.js +55 -0
- package/dist/scene/store.d.ts +19 -0
- package/dist/scene/store.js +8 -0
- package/dist/textures/callisto-1024.webp +0 -0
- package/dist/textures/callisto-512.webp +0 -0
- package/dist/textures/enceladus-1024.webp +0 -0
- package/dist/textures/enceladus-512.webp +0 -0
- package/dist/textures/europa-1024.webp +0 -0
- package/dist/textures/europa-512.webp +0 -0
- package/dist/textures/ganymede-1024.webp +0 -0
- package/dist/textures/ganymede-512.webp +0 -0
- package/dist/textures/io-1024.webp +0 -0
- package/dist/textures/io-512.webp +0 -0
- package/dist/textures/jupiter-1024.webp +0 -0
- package/dist/textures/jupiter-512.webp +0 -0
- package/dist/textures/mars-1024.webp +0 -0
- package/dist/textures/mars-512.webp +0 -0
- package/dist/textures/mercury-1024.webp +0 -0
- package/dist/textures/mercury-512.webp +0 -0
- package/dist/textures/mimas-1024.webp +0 -0
- package/dist/textures/mimas-512.webp +0 -0
- package/dist/textures/moon-1024.webp +0 -0
- package/dist/textures/moon-512.webp +0 -0
- package/dist/textures/neptune-1024.webp +0 -0
- package/dist/textures/neptune-512.webp +0 -0
- package/dist/textures/pluto-1024.webp +0 -0
- package/dist/textures/pluto-512.webp +0 -0
- package/dist/textures/rhea-1024.webp +0 -0
- package/dist/textures/rhea-512.webp +0 -0
- package/dist/textures/saturn-1024.webp +0 -0
- package/dist/textures/saturn-512.webp +0 -0
- package/dist/textures/saturn-ring-2048.webp +0 -0
- package/dist/textures/sun-1024.webp +0 -0
- package/dist/textures/sun-512.webp +0 -0
- package/dist/textures/titan-1024.webp +0 -0
- package/dist/textures/titan-512.webp +0 -0
- package/dist/textures/uranus-1024.webp +0 -0
- package/dist/textures/uranus-512.webp +0 -0
- package/dist/textures/venus-1024.webp +0 -0
- package/dist/textures/venus-512.webp +0 -0
- package/dist/vec.d.ts +19 -0
- package/dist/vec.js +32 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +236 -4
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { type BodyId, type MoonId, type PlanetId } from "./facts.ts";
|
|
2
|
+
import { type Vec3 } from "./vec.ts";
|
|
3
|
+
/** Solve Kepler's equation for the eccentric anomaly, all in radians. */
|
|
4
|
+
export declare function kepler(meanAnomaly: number, e: number): number;
|
|
5
|
+
/**
|
|
6
|
+
* A planet's position in the Sun's frame (J2000 ecliptic, astronomical units) at a time, from JPL's approximate elements.
|
|
7
|
+
* @param planet - The planet's name.
|
|
8
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
9
|
+
* @returns The position as x, y, z in au.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { heliocentric } from "@johnmorrisdotca/wakusei-planets";
|
|
14
|
+
*
|
|
15
|
+
* const [x, y, z] = heliocentric("earth", Date.UTC(2026, 0, 3));
|
|
16
|
+
* console.log(Math.hypot(x, y, z).toFixed(3));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function heliocentric(planet: PlanetId, ms: number): Vec3;
|
|
20
|
+
/**
|
|
21
|
+
* The Moon's position from the Earth's centre (J2000 ecliptic, kilometres), from the largest terms of Meeus's lunar series.
|
|
22
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
23
|
+
* @returns The position as x, y, z in kilometres.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { moonGeocentric } from "@johnmorrisdotca/wakusei-planets";
|
|
28
|
+
*
|
|
29
|
+
* const [x, y, z] = moonGeocentric(Date.UTC(2026, 10, 2));
|
|
30
|
+
* console.log(Math.round(Math.hypot(x, y, z)) > 350000);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function moonGeocentric(ms: number): Vec3;
|
|
34
|
+
/** The north pole of a body as a J2000 ecliptic unit vector. */
|
|
35
|
+
export declare function poleOf(id: BodyId): Vec3;
|
|
36
|
+
/** The prime meridian's direction (J2000 ecliptic unit vector) when it has turned to `w` degrees. */
|
|
37
|
+
export declare function primeMeridian(id: BodyId, w: number): Vec3;
|
|
38
|
+
/**
|
|
39
|
+
* The way a body is turned toward the viewer, in the viewer's frame (x to the right, y up, z toward the viewer), as unit vectors: its north pole, the direction of longitude 0 and the Sun's direction.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { portrait, type Pose } from "@johnmorrisdotca/wakusei-planets";
|
|
44
|
+
*
|
|
45
|
+
* const pose: Pose = portrait("saturn");
|
|
46
|
+
* console.log(pose.pole.map((n) => n.toFixed(2)).join(", "));
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export interface Pose {
|
|
50
|
+
/** The north pole. */
|
|
51
|
+
pole: Vec3;
|
|
52
|
+
/** The direction of longitude 0 on the equator. */
|
|
53
|
+
meridian: Vec3;
|
|
54
|
+
/** The direction of the Sun, from the body. */
|
|
55
|
+
light: Vec3;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What the sky shows of a body at a time: its pose, the latitude the viewer sees it from (how open its rings are), the longitude at the middle of the disc, the phase and the size.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { apparent, type Apparent } from "@johnmorrisdotca/wakusei-planets";
|
|
63
|
+
*
|
|
64
|
+
* const moon: Apparent = apparent("moon", Date.UTC(2026, 10, 24, 14));
|
|
65
|
+
* console.log(moon.illumination.toFixed(2));
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export interface Apparent extends Pose {
|
|
69
|
+
/** How the body is turned. */
|
|
70
|
+
id: BodyId;
|
|
71
|
+
/** The instant, in milliseconds since 1970. */
|
|
72
|
+
time: number;
|
|
73
|
+
/** Degrees above (positive) or below the body's equator that the viewer sees it from: how open its rings are. */
|
|
74
|
+
subObserverLat: number;
|
|
75
|
+
/** The longitude at the middle of the disc, degrees east, 0 to 360. */
|
|
76
|
+
centralMeridian: number;
|
|
77
|
+
/** The Sun–body–viewer angle in degrees: 0 is full, 180 is new. */
|
|
78
|
+
phaseAngle: number;
|
|
79
|
+
/** The fraction of the disc that is lit, 0 to 1. */
|
|
80
|
+
illumination: number;
|
|
81
|
+
/** The distance from the viewer in kilometres. */
|
|
82
|
+
distanceKm: number;
|
|
83
|
+
/** The angular diameter in degrees. */
|
|
84
|
+
diameterDeg: number;
|
|
85
|
+
/** The viewer's frame in J2000 ecliptic vectors, to turn more vectors into the same view. */
|
|
86
|
+
frame: {
|
|
87
|
+
right: Vec3;
|
|
88
|
+
up: Vec3;
|
|
89
|
+
toward: Vec3;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The viewer's axes for looking along `u` (toward the body): north up, east to the left. */
|
|
93
|
+
export declare function viewFrame(toBody: Vec3): {
|
|
94
|
+
right: Vec3;
|
|
95
|
+
up: Vec3;
|
|
96
|
+
toward: Vec3;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* How a body looks from the Earth at a time, from its real pole, rotation and the Sun's direction. A moon that keeps one face to its planet shows that face to the planet.
|
|
100
|
+
*
|
|
101
|
+
* @param id - The body's name.
|
|
102
|
+
* @param time - The instant, in milliseconds since 1970 (`Date.now()`).
|
|
103
|
+
* @returns The pole, the prime meridian and the Sun's direction in the viewer's frame, with the phase and the size.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { apparent } from "@johnmorrisdotca/wakusei-planets";
|
|
108
|
+
*
|
|
109
|
+
* const saturn = apparent("saturn", Date.UTC(2026, 10, 2));
|
|
110
|
+
* console.log(saturn.subObserverLat.toFixed(1), saturn.illumination.toFixed(2));
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare function apparent(id: BodyId, time: number): Apparent;
|
|
114
|
+
/** A moon's position from its planet's centre (J2000 ecliptic, kilometres) at a time. */
|
|
115
|
+
export declare function moonVector(id: MoonId, ms: number): Vec3;
|
|
116
|
+
/**
|
|
117
|
+
* One moon, placed in a planet's system: in planet radii, x to the right, y up, z toward the viewer, with its distance from the planet and its phase angle.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { system, type MoonPlace } from "@johnmorrisdotca/wakusei-planets";
|
|
122
|
+
*
|
|
123
|
+
* const io: MoonPlace | undefined = system("jupiter", Date.UTC(2026, 10, 2)).moons[0];
|
|
124
|
+
* console.log(io?.id, io?.distance.toFixed(1));
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export interface MoonPlace {
|
|
128
|
+
id: MoonId;
|
|
129
|
+
x: number;
|
|
130
|
+
y: number;
|
|
131
|
+
z: number;
|
|
132
|
+
/** The distance from the planet's centre in planet radii. */
|
|
133
|
+
distance: number;
|
|
134
|
+
/** The Sun–moon–viewer angle in degrees (0 is full). */
|
|
135
|
+
phaseAngle: number;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A planet's own pose and its moons as the Earth sees them at a time, nearest first.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* import { system, type System } from "@johnmorrisdotca/wakusei-planets";
|
|
143
|
+
*
|
|
144
|
+
* const jupiter: System = system("jupiter", Date.UTC(2026, 10, 2));
|
|
145
|
+
* console.log(jupiter.moons.length);
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export interface System {
|
|
149
|
+
planet: Apparent;
|
|
150
|
+
moons: MoonPlace[];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A planet and its moons as the Earth sees them at a time: each moon's place in planet radii, in the viewer's frame (x right, y up, z toward the viewer).
|
|
154
|
+
*
|
|
155
|
+
* @param planet - The planet's name.
|
|
156
|
+
* @param time - The instant, in milliseconds since 1970.
|
|
157
|
+
* @returns The planet's pose and every moon of it, nearest first.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* import { system } from "@johnmorrisdotca/wakusei-planets";
|
|
162
|
+
*
|
|
163
|
+
* const { moons } = system("jupiter", Date.UTC(2026, 10, 2, 12));
|
|
164
|
+
* console.log(moons.map((moon) => `${moon.id} ${moon.x.toFixed(1)}`).join(", "));
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export declare function system(planet: PlanetId, time: number): System;
|
|
168
|
+
/**
|
|
169
|
+
* The path one moon follows round its planet over one trip, as the Earth sees it at `time`: x, y, z in planet radii, in the viewer's frame at that moment.
|
|
170
|
+
*
|
|
171
|
+
* @param id - The moon's name.
|
|
172
|
+
* @param time - The instant, in milliseconds since 1970.
|
|
173
|
+
* @param points - How many points to make. Default 96.
|
|
174
|
+
* @returns A flat array of `points` triples, x, y, z each; empty for a moon with no orbit.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { orbitPath } from "@johnmorrisdotca/wakusei-planets";
|
|
179
|
+
*
|
|
180
|
+
* const path = orbitPath("io", Date.UTC(2026, 10, 2), 8);
|
|
181
|
+
* console.log(path.length / 3);
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export declare function orbitPath(id: MoonId, time: number, points?: number): Float32Array;
|
|
185
|
+
/** The planets' moons that are near enough to draw in a system view, nearest first, with how far each goes in planet radii. */
|
|
186
|
+
export declare function nearMoons(planet: PlanetId, within?: number): {
|
|
187
|
+
id: MoonId;
|
|
188
|
+
reach: number;
|
|
189
|
+
}[];
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// Where the planets and their moons are at a date and time, and how a body looks from the Earth then: its pole, the face that turns toward us, where the Sun is.
|
|
2
|
+
// Planets: JPL's "Keplerian Elements for Approximate Positions of the Major Planets", Table 1 (1800 to 2050, https://ssd.jpl.nasa.gov/planets/approx_pos.html),
|
|
3
|
+
// good to about an arc minute. The Moon: the largest terms of Meeus, "Astronomical Algorithms", chapter 47. Other moons: JPL's mean elements (data/orbits.ts).
|
|
4
|
+
// Light travel time is allowed for, so a turning planet shows the face it really showed when the light left. Pure: no DOM, no three.js.
|
|
5
|
+
import { ORBITS } from "./data/orbits.js";
|
|
6
|
+
import { BODIES, MOONS_OF } from "./facts.js";
|
|
7
|
+
import { add, cross, DEG, dot, fromRaDec, length, OBLIQUITY, RAD, scale, sub, toEcliptic, unit, wrap360 } from "./vec.js";
|
|
8
|
+
/** Milliseconds since 1970 at J2000.0 (2000-01-01 12:00 TT, taken as UTC). */
|
|
9
|
+
const J2000 = Date.UTC(2000, 0, 1, 12);
|
|
10
|
+
const DAY = 86_400_000;
|
|
11
|
+
const AU_KM = 149_597_870.7;
|
|
12
|
+
/** Light takes this many seconds to cross one astronomical unit. */
|
|
13
|
+
const LIGHT_SECONDS_PER_AU = 499.004784;
|
|
14
|
+
// a (au), e, I, L, long.peri, long.node (degrees), then their rates per Julian century.
|
|
15
|
+
const ELEMENTS = {
|
|
16
|
+
mercury: [0.38709927, 0.20563593, 7.00497902, 252.2503235, 77.45779628, 48.33076593, 0.00000037, 0.00001906, -0.00594749, 149472.67411175, 0.16047689, -0.12534081],
|
|
17
|
+
venus: [0.72333566, 0.00677672, 3.39467605, 181.9790995, 131.60246718, 76.67984255, 0.0000039, -0.00004107, -0.0007889, 58517.81538729, 0.00268329, -0.27769418],
|
|
18
|
+
earth: [1.00000261, 0.01671123, -0.00001531, 100.46457166, 102.93768193, 0, 0.00000562, -0.00004392, -0.01294668, 35999.37244981, 0.32327364, 0],
|
|
19
|
+
mars: [1.52371034, 0.0933941, 1.84969142, -4.55343205, -23.94362959, 49.55953891, 0.00001847, 0.00007882, -0.00813131, 19140.30268499, 0.44441088, -0.29257343],
|
|
20
|
+
jupiter: [5.202887, 0.04838624, 1.30439695, 34.39644051, 14.72847983, 100.47390909, -0.00011607, -0.00013253, -0.00183714, 3034.74612775, 0.21252668, 0.20469106],
|
|
21
|
+
saturn: [9.53667594, 0.05386179, 2.48599187, 49.95424423, 92.59887831, 113.66242448, -0.0012506, -0.00050991, 0.00193609, 1222.49362201, -0.41897216, -0.28867794],
|
|
22
|
+
uranus: [19.18916464, 0.04725744, 0.77263783, 313.23810451, 170.9542763, 74.01692503, -0.00196176, -0.00004397, -0.00242939, 428.48202785, 0.40805281, 0.04240589],
|
|
23
|
+
neptune: [30.06992276, 0.00859048, 1.77004347, -55.12002969, 44.96476227, 131.78422574, 0.00026291, 0.00005105, 0.00035372, 218.45945325, -0.32241464, -0.00508664],
|
|
24
|
+
pluto: [39.48211675, 0.2488273, 17.14001206, 238.92903833, 224.06891629, 110.30393684, -0.00031596, 0.0000517, 0.00004818, 145.20780515, -0.04062942, -0.01183482],
|
|
25
|
+
};
|
|
26
|
+
const centuries = (ms) => (ms - J2000) / (DAY * 36525);
|
|
27
|
+
const days = (ms) => (ms - J2000) / DAY;
|
|
28
|
+
/** Solve Kepler's equation for the eccentric anomaly, all in radians. */
|
|
29
|
+
export function kepler(meanAnomaly, e) {
|
|
30
|
+
let m = meanAnomaly % (2 * Math.PI);
|
|
31
|
+
if (m > Math.PI)
|
|
32
|
+
m -= 2 * Math.PI;
|
|
33
|
+
if (m < -Math.PI)
|
|
34
|
+
m += 2 * Math.PI;
|
|
35
|
+
let E = e < 0.8 ? m : Math.PI;
|
|
36
|
+
for (let i = 0; i < 12; i += 1) {
|
|
37
|
+
const d = (E - e * Math.sin(E) - m) / (1 - e * Math.cos(E));
|
|
38
|
+
E -= d;
|
|
39
|
+
if (Math.abs(d) < 1e-12)
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return E;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A planet's position in the Sun's frame (J2000 ecliptic, astronomical units) at a time, from JPL's approximate elements.
|
|
46
|
+
* @param planet - The planet's name.
|
|
47
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
48
|
+
* @returns The position as x, y, z in au.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { heliocentric } from "@johnmorrisdotca/wakusei-planets";
|
|
53
|
+
*
|
|
54
|
+
* const [x, y, z] = heliocentric("earth", Date.UTC(2026, 0, 3));
|
|
55
|
+
* console.log(Math.hypot(x, y, z).toFixed(3));
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function heliocentric(planet, ms) {
|
|
59
|
+
const el = ELEMENTS[planet];
|
|
60
|
+
const T = centuries(ms);
|
|
61
|
+
const a = el[0] + el[6] * T;
|
|
62
|
+
const e = el[1] + el[7] * T;
|
|
63
|
+
const I = (el[2] + el[8] * T) * RAD;
|
|
64
|
+
const L = el[3] + el[9] * T;
|
|
65
|
+
const peri = el[4] + el[10] * T;
|
|
66
|
+
const node = (el[5] + el[11] * T) * RAD;
|
|
67
|
+
const w = peri * RAD - node;
|
|
68
|
+
const E = kepler((L - peri) * RAD, e);
|
|
69
|
+
const x = a * (Math.cos(E) - e);
|
|
70
|
+
const y = a * Math.sqrt(1 - e * e) * Math.sin(E);
|
|
71
|
+
const cw = Math.cos(w);
|
|
72
|
+
const sw = Math.sin(w);
|
|
73
|
+
const cn = Math.cos(node);
|
|
74
|
+
const sn = Math.sin(node);
|
|
75
|
+
const ci = Math.cos(I);
|
|
76
|
+
const si = Math.sin(I);
|
|
77
|
+
return [(cw * cn - sw * sn * ci) * x + (-sw * cn - cw * sn * ci) * y, (cw * sn + sw * cn * ci) * x + (-sw * sn + cw * cn * ci) * y, sw * si * x + cw * si * y];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The Moon's position from the Earth's centre (J2000 ecliptic, kilometres), from the largest terms of Meeus's lunar series.
|
|
81
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
82
|
+
* @returns The position as x, y, z in kilometres.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { moonGeocentric } from "@johnmorrisdotca/wakusei-planets";
|
|
87
|
+
*
|
|
88
|
+
* const [x, y, z] = moonGeocentric(Date.UTC(2026, 10, 2));
|
|
89
|
+
* console.log(Math.round(Math.hypot(x, y, z)) > 350000);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function moonGeocentric(ms) {
|
|
93
|
+
const T = centuries(ms);
|
|
94
|
+
const Lm = 218.3164477 + 481267.88123421 * T;
|
|
95
|
+
const D = (297.8501921 + 445267.1114034 * T) * RAD;
|
|
96
|
+
const M = (357.5291092 + 35999.0502909 * T) * RAD;
|
|
97
|
+
const Mp = (134.9633964 + 477198.8675055 * T) * RAD;
|
|
98
|
+
const F = (93.272095 + 483202.0175233 * T) * RAD;
|
|
99
|
+
const lon = Lm + 6.289 * Math.sin(Mp) + 1.274 * Math.sin(2 * D - Mp) + 0.658 * Math.sin(2 * D) + 0.214 * Math.sin(2 * Mp) - 0.186 * Math.sin(M) - 0.114 * Math.sin(2 * F) - 0.059 * Math.sin(2 * D - 2 * Mp) - 0.057 * Math.sin(2 * D - M - Mp) + 0.053 * Math.sin(2 * D + Mp) + 0.046 * Math.sin(2 * D - M) + 0.041 * Math.sin(Mp - M);
|
|
100
|
+
const lat = 5.128 * Math.sin(F) + 0.2806 * Math.sin(Mp + F) + 0.2777 * Math.sin(Mp - F) + 0.1732 * Math.sin(2 * D - F);
|
|
101
|
+
const dist = 385000.56 - 20905.355 * Math.cos(Mp) - 3699.111 * Math.cos(2 * D - Mp) - 2955.968 * Math.cos(2 * D) - 569.925 * Math.cos(2 * Mp) + 48.888 * Math.cos(M) - 3.149 * Math.cos(2 * F);
|
|
102
|
+
// The longitude is of the date; take off the precession since J2000 (about 1.4 degrees a century) to put it in the J2000 frame.
|
|
103
|
+
const lam = (lon - 1.3969713 * T) * RAD;
|
|
104
|
+
const bet = lat * RAD;
|
|
105
|
+
return [dist * Math.cos(bet) * Math.cos(lam), dist * Math.cos(bet) * Math.sin(lam), dist * Math.sin(bet)];
|
|
106
|
+
}
|
|
107
|
+
/** The north pole of a body as a J2000 ecliptic unit vector. */
|
|
108
|
+
export function poleOf(id) {
|
|
109
|
+
const { ra, dec } = BODIES[id].pole;
|
|
110
|
+
return toEcliptic(fromRaDec(ra, dec));
|
|
111
|
+
}
|
|
112
|
+
/** The prime meridian's direction (J2000 ecliptic unit vector) when it has turned to `w` degrees. */
|
|
113
|
+
export function primeMeridian(id, w) {
|
|
114
|
+
const { ra, dec } = BODIES[id].pole;
|
|
115
|
+
const pole = fromRaDec(ra, dec);
|
|
116
|
+
const node = fromRaDec(ra + 90, 0);
|
|
117
|
+
const third = cross(pole, node);
|
|
118
|
+
const c = Math.cos(w * RAD);
|
|
119
|
+
const s = Math.sin(w * RAD);
|
|
120
|
+
return toEcliptic([node[0] * c + third[0] * s, node[1] * c + third[1] * s, node[2] * c + third[2] * s]);
|
|
121
|
+
}
|
|
122
|
+
const north = [0, Math.sin(OBLIQUITY * RAD), Math.cos(OBLIQUITY * RAD)];
|
|
123
|
+
/** The viewer's axes for looking along `u` (toward the body): north up, east to the left. */
|
|
124
|
+
export function viewFrame(toBody) {
|
|
125
|
+
const u = unit(toBody);
|
|
126
|
+
const up = unit(sub(north, scale(u, dot(north, u))));
|
|
127
|
+
const right = unit(cross(u, north));
|
|
128
|
+
return { right, up, toward: scale(u, -1) };
|
|
129
|
+
}
|
|
130
|
+
const screen = (frame, v) => [dot(v, frame.right), dot(v, frame.up), dot(v, frame.toward)];
|
|
131
|
+
/** The Earth's position and the geocentric vector to `id`, in au and J2000 ecliptic, at the time the light left. */
|
|
132
|
+
function geometry(id, ms) {
|
|
133
|
+
const earth = heliocentric("earth", ms);
|
|
134
|
+
if (id === "sun") {
|
|
135
|
+
const g = scale(earth, -1);
|
|
136
|
+
return { earth, target: g, sunFrom: g, emit: ms - length(g) * LIGHT_SECONDS_PER_AU * 1000, distanceAu: length(g) };
|
|
137
|
+
}
|
|
138
|
+
if (id === "moon") {
|
|
139
|
+
const g = scale(moonGeocentric(ms), 1 / AU_KM);
|
|
140
|
+
return { earth, target: g, sunFrom: scale(earth, -1), emit: ms - length(g) * LIGHT_SECONDS_PER_AU * 1000, distanceAu: length(g) };
|
|
141
|
+
}
|
|
142
|
+
if (id === "earth") {
|
|
143
|
+
// The Earth is seen from the Moon: the viewer is a Moon's distance away, looking back.
|
|
144
|
+
const g = scale(moonGeocentric(ms), -1 / AU_KM);
|
|
145
|
+
return { earth, target: g, sunFrom: scale(earth, -1), emit: ms - length(g) * LIGHT_SECONDS_PER_AU * 1000, distanceAu: length(g) };
|
|
146
|
+
}
|
|
147
|
+
const facts = BODIES[id];
|
|
148
|
+
const planet = (facts.parent ?? id);
|
|
149
|
+
let emit = ms;
|
|
150
|
+
let h = heliocentric(planet, ms);
|
|
151
|
+
let g = sub(h, earth);
|
|
152
|
+
for (let i = 0; i < 2; i += 1) {
|
|
153
|
+
emit = ms - length(g) * LIGHT_SECONDS_PER_AU * 1000;
|
|
154
|
+
h = heliocentric(planet, emit);
|
|
155
|
+
g = sub(h, earth);
|
|
156
|
+
}
|
|
157
|
+
return { earth, target: g, sunFrom: scale(h, -1), emit, distanceAu: length(g) };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* How a body looks from the Earth at a time, from its real pole, rotation and the Sun's direction. A moon that keeps one face to its planet shows that face to the planet.
|
|
161
|
+
*
|
|
162
|
+
* @param id - The body's name.
|
|
163
|
+
* @param time - The instant, in milliseconds since 1970 (`Date.now()`).
|
|
164
|
+
* @returns The pole, the prime meridian and the Sun's direction in the viewer's frame, with the phase and the size.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* import { apparent } from "@johnmorrisdotca/wakusei-planets";
|
|
169
|
+
*
|
|
170
|
+
* const saturn = apparent("saturn", Date.UTC(2026, 10, 2));
|
|
171
|
+
* console.log(saturn.subObserverLat.toFixed(1), saturn.illumination.toFixed(2));
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export function apparent(id, time) {
|
|
175
|
+
const facts = BODIES[id];
|
|
176
|
+
const { target, sunFrom, emit, distanceAu } = geometry(id, time);
|
|
177
|
+
let toBody = target;
|
|
178
|
+
let moonOffset;
|
|
179
|
+
if (facts.parent !== undefined && id !== "moon") {
|
|
180
|
+
moonOffset = moonVector(id, emit);
|
|
181
|
+
toBody = add(target, scale(moonOffset, 1 / AU_KM));
|
|
182
|
+
}
|
|
183
|
+
const frame = viewFrame(toBody);
|
|
184
|
+
const light = screen(frame, unit(sunFrom));
|
|
185
|
+
const toward = unit(scale(toBody, -1));
|
|
186
|
+
let poleEcl;
|
|
187
|
+
let meridianEcl;
|
|
188
|
+
if (facts.synchronous === true && id !== "moon" && moonOffset !== undefined) {
|
|
189
|
+
// the face toward the planet: the direction from the moon to the planet, in the equator
|
|
190
|
+
poleEcl = poleOf(facts.parent);
|
|
191
|
+
const planetward = scale(unit(moonOffset), -1);
|
|
192
|
+
meridianEcl = unit(sub(planetward, scale(poleEcl, dot(planetward, poleEcl))));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const w = facts.prime.w0 + facts.prime.rate * days(emit);
|
|
196
|
+
poleEcl = poleOf(id);
|
|
197
|
+
meridianEcl = primeMeridian(id, wrap360(w));
|
|
198
|
+
}
|
|
199
|
+
const pole = screen(frame, poleEcl);
|
|
200
|
+
const meridian = screen(frame, meridianEcl);
|
|
201
|
+
const subObserverLat = Math.asin(Math.max(-1, Math.min(1, pole[2]))) * DEG;
|
|
202
|
+
const east = cross(poleEcl, meridianEcl);
|
|
203
|
+
const centralMeridian = wrap360(Math.atan2(dot(toward, east), dot(toward, meridianEcl)) * DEG);
|
|
204
|
+
const cosPhase = Math.max(-1, Math.min(1, light[2]));
|
|
205
|
+
const phaseAngle = Math.acos(cosPhase) * DEG;
|
|
206
|
+
const distanceKm = id === "sun" ? distanceAu * AU_KM : length(toBody) * AU_KM;
|
|
207
|
+
return {
|
|
208
|
+
id,
|
|
209
|
+
time,
|
|
210
|
+
pole,
|
|
211
|
+
meridian,
|
|
212
|
+
light,
|
|
213
|
+
subObserverLat,
|
|
214
|
+
centralMeridian,
|
|
215
|
+
phaseAngle,
|
|
216
|
+
illumination: id === "sun" ? 1 : (1 + cosPhase) / 2,
|
|
217
|
+
distanceKm,
|
|
218
|
+
diameterDeg: 2 * Math.asin(Math.min(1, facts.radiusKm / distanceKm)) * DEG,
|
|
219
|
+
frame,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** A moon's position from its planet's centre (J2000 ecliptic, kilometres) at a time. */
|
|
223
|
+
export function moonVector(id, ms) {
|
|
224
|
+
if (id === "moon")
|
|
225
|
+
return moonGeocentric(ms);
|
|
226
|
+
const orbit = ORBITS[id];
|
|
227
|
+
if (orbit === undefined)
|
|
228
|
+
throw new Error(`no orbit for ${id}`);
|
|
229
|
+
return orbitVector(orbit, ms);
|
|
230
|
+
}
|
|
231
|
+
function referenceBasis(orbit) {
|
|
232
|
+
if (orbit.frame === "ecliptic")
|
|
233
|
+
return { x: [1, 0, 0], y: [0, 1, 0], z: [0, 0, 1] };
|
|
234
|
+
const pole = orbit.frame === "laplace" && orbit.pole !== undefined ? orbit.pole : [BODIES[orbit.parent].pole.ra, BODIES[orbit.parent].pole.dec];
|
|
235
|
+
const z = fromRaDec(pole[0], pole[1]);
|
|
236
|
+
const x = fromRaDec(pole[0] + 90, 0);
|
|
237
|
+
return { x: toEcliptic(x), y: toEcliptic(cross(z, x)), z: toEcliptic(z) };
|
|
238
|
+
}
|
|
239
|
+
const basisCache = new Map();
|
|
240
|
+
function orbitVector(orbit, ms) {
|
|
241
|
+
const key = `${orbit.parent}:${orbit.frame}:${orbit.pole?.join(",") ?? ""}`;
|
|
242
|
+
let basis = basisCache.get(key);
|
|
243
|
+
if (basis === undefined) {
|
|
244
|
+
basis = referenceBasis(orbit);
|
|
245
|
+
basisCache.set(key, basis);
|
|
246
|
+
}
|
|
247
|
+
const dt = (ms - orbit.epoch) / DAY;
|
|
248
|
+
const w = orbit.w + orbit.dw * dt;
|
|
249
|
+
const node = orbit.node + orbit.dn * dt;
|
|
250
|
+
const lambda0 = orbit.w + orbit.node + orbit.m;
|
|
251
|
+
const M = lambda0 + orbit.sid * dt - w - node;
|
|
252
|
+
const E = kepler(M * RAD, orbit.e);
|
|
253
|
+
const x = orbit.a * (Math.cos(E) - orbit.e);
|
|
254
|
+
const y = orbit.a * Math.sqrt(1 - orbit.e * orbit.e) * Math.sin(E);
|
|
255
|
+
const cw = Math.cos(w * RAD);
|
|
256
|
+
const sw = Math.sin(w * RAD);
|
|
257
|
+
const cn = Math.cos(node * RAD);
|
|
258
|
+
const sn = Math.sin(node * RAD);
|
|
259
|
+
const ci = Math.cos(orbit.i * RAD);
|
|
260
|
+
const si = Math.sin(orbit.i * RAD);
|
|
261
|
+
const px = (cw * cn - sw * sn * ci) * x + (-sw * cn - cw * sn * ci) * y;
|
|
262
|
+
const py = (cw * sn + sw * cn * ci) * x + (-sw * sn + cw * cn * ci) * y;
|
|
263
|
+
const pz = sw * si * x + cw * si * y;
|
|
264
|
+
return [px * basis.x[0] + py * basis.y[0] + pz * basis.z[0], px * basis.x[1] + py * basis.y[1] + pz * basis.z[1], px * basis.x[2] + py * basis.y[2] + pz * basis.z[2]];
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* A planet and its moons as the Earth sees them at a time: each moon's place in planet radii, in the viewer's frame (x right, y up, z toward the viewer).
|
|
268
|
+
*
|
|
269
|
+
* @param planet - The planet's name.
|
|
270
|
+
* @param time - The instant, in milliseconds since 1970.
|
|
271
|
+
* @returns The planet's pose and every moon of it, nearest first.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* import { system } from "@johnmorrisdotca/wakusei-planets";
|
|
276
|
+
*
|
|
277
|
+
* const { moons } = system("jupiter", Date.UTC(2026, 10, 2, 12));
|
|
278
|
+
* console.log(moons.map((moon) => `${moon.id} ${moon.x.toFixed(1)}`).join(", "));
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export function system(planet, time) {
|
|
282
|
+
const pose = apparent(planet, time);
|
|
283
|
+
const emit = time - pose.distanceKm / AU_KM * LIGHT_SECONDS_PER_AU * 1000;
|
|
284
|
+
const radius = BODIES[planet].radiusKm;
|
|
285
|
+
const ids = planet in MOONS_OF ? MOONS_OF[planet] : [];
|
|
286
|
+
const moons = ids.map((id) => {
|
|
287
|
+
const v = moonVector(id, emit);
|
|
288
|
+
const s = screen(pose.frame, v);
|
|
289
|
+
const km = length(v);
|
|
290
|
+
const place = [s[0] / radius, s[1] / radius, s[2] / radius];
|
|
291
|
+
return { id, x: place[0], y: place[1], z: place[2], distance: km / radius, phaseAngle: pose.phaseAngle };
|
|
292
|
+
});
|
|
293
|
+
return { planet: pose, moons: moons.sort((a, b) => a.distance - b.distance) };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The path one moon follows round its planet over one trip, as the Earth sees it at `time`: x, y, z in planet radii, in the viewer's frame at that moment.
|
|
297
|
+
*
|
|
298
|
+
* @param id - The moon's name.
|
|
299
|
+
* @param time - The instant, in milliseconds since 1970.
|
|
300
|
+
* @param points - How many points to make. Default 96.
|
|
301
|
+
* @returns A flat array of `points` triples, x, y, z each; empty for a moon with no orbit.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* import { orbitPath } from "@johnmorrisdotca/wakusei-planets";
|
|
306
|
+
*
|
|
307
|
+
* const path = orbitPath("io", Date.UTC(2026, 10, 2), 8);
|
|
308
|
+
* console.log(path.length / 3);
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
export function orbitPath(id, time, points = 96) {
|
|
312
|
+
const facts = BODIES[id];
|
|
313
|
+
const planet = (facts.parent ?? "earth");
|
|
314
|
+
const orbit = id === "moon" ? undefined : ORBITS[id];
|
|
315
|
+
const rate = id === "moon" ? 13.1763585 : orbit?.sid;
|
|
316
|
+
if (rate === undefined)
|
|
317
|
+
return new Float32Array(0);
|
|
318
|
+
const pose = apparent(planet, time);
|
|
319
|
+
const radius = BODIES[planet].radiusKm;
|
|
320
|
+
const period = (360 / Math.abs(rate)) * DAY;
|
|
321
|
+
const emit = time - (pose.distanceKm / AU_KM) * LIGHT_SECONDS_PER_AU * 1000;
|
|
322
|
+
const out = new Float32Array(points * 3);
|
|
323
|
+
for (let k = 0; k < points; k += 1) {
|
|
324
|
+
const v = moonVector(id, emit + (k / points) * period);
|
|
325
|
+
const s = screen(pose.frame, v);
|
|
326
|
+
out[k * 3] = s[0] / radius;
|
|
327
|
+
out[k * 3 + 1] = s[1] / radius;
|
|
328
|
+
out[k * 3 + 2] = s[2] / radius;
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
/** The planets' moons that are near enough to draw in a system view, nearest first, with how far each goes in planet radii. */
|
|
333
|
+
export function nearMoons(planet, within = 80) {
|
|
334
|
+
const ids = planet in MOONS_OF ? MOONS_OF[planet] : [];
|
|
335
|
+
const radius = BODIES[planet].radiusKm;
|
|
336
|
+
return ids
|
|
337
|
+
.map((id) => ({ id, reach: id === "moon" ? 405500 / radius : ((ORBITS[id]?.a ?? 0) * (1 + (ORBITS[id]?.e ?? 0))) / radius }))
|
|
338
|
+
.filter((m) => m.reach <= within)
|
|
339
|
+
.sort((a, b) => a.reach - b.reach);
|
|
340
|
+
}
|