@johnmorrisdotca/chikyu-earth 0.5.0 → 0.8.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/NOTICE.md +22 -0
  3. package/README.md +186 -27
  4. package/dist/countries.d.ts +11 -0
  5. package/dist/countries.js +11 -0
  6. package/dist/element.d.ts +18 -0
  7. package/dist/element.js +15 -2
  8. package/dist/globe.d.ts +15 -1
  9. package/dist/globe.js +15 -1
  10. package/dist/index.d.ts +12 -1
  11. package/dist/index.js +12 -1
  12. package/dist/markers/cluster.d.ts +27 -0
  13. package/dist/markers/cluster.js +16 -0
  14. package/dist/markers/labels.d.ts +40 -0
  15. package/dist/markers/labels.js +18 -0
  16. package/dist/markers/layer.d.ts +80 -0
  17. package/dist/markers/layer.js +21 -0
  18. package/dist/markers/project.d.ts +63 -2
  19. package/dist/markers/project.js +52 -2
  20. package/dist/markers/types.d.ts +48 -3
  21. package/dist/realistic/earth.d.ts +30 -5
  22. package/dist/realistic/earth.js +41 -134
  23. package/dist/realistic/element.d.ts +47 -7
  24. package/dist/realistic/element.js +48 -44
  25. package/dist/realistic/gate.d.ts +45 -1
  26. package/dist/realistic/gate.js +33 -1
  27. package/dist/realistic/shaders.d.ts +9 -4
  28. package/dist/realistic/shaders.js +54 -9
  29. package/dist/realistic/shared.d.ts +23 -0
  30. package/dist/realistic/shared.js +51 -0
  31. package/dist/realistic/sun.d.ts +40 -3
  32. package/dist/realistic/sun.js +40 -3
  33. package/dist/realistic/textures.d.ts +43 -0
  34. package/dist/realistic/textures.js +141 -0
  35. package/dist/realistic/tiers.d.ts +80 -6
  36. package/dist/realistic/tiers.js +69 -5
  37. package/dist/rotate.d.ts +59 -4
  38. package/dist/rotate.js +59 -4
  39. package/dist/satellites/index.d.ts +3 -0
  40. package/dist/satellites/index.js +3 -0
  41. package/dist/satellites/limits.d.ts +22 -0
  42. package/dist/satellites/limits.js +23 -0
  43. package/dist/satellites/track.d.ts +50 -0
  44. package/dist/satellites/track.js +227 -0
  45. package/dist/satellites/types.d.ts +161 -0
  46. package/dist/satellites/types.js +3 -0
  47. package/dist/space/element.d.ts +179 -0
  48. package/dist/space/element.js +541 -0
  49. package/dist/space/ephemeris.d.ts +151 -0
  50. package/dist/space/ephemeris.js +162 -0
  51. package/dist/space/index.d.ts +3 -0
  52. package/dist/space/index.js +5 -0
  53. package/dist/space/satellite-layer.d.ts +53 -0
  54. package/dist/space/satellite-layer.js +230 -0
  55. package/dist/space/scale.d.ts +124 -0
  56. package/dist/space/scale.js +141 -0
  57. package/dist/space/scene.d.ts +146 -0
  58. package/dist/space/scene.js +320 -0
  59. package/dist/styles.d.ts +65 -2
  60. package/dist/styles.js +43 -2
  61. package/dist/textures/moon-1024.webp +0 -0
  62. package/dist/textures/moon-2048.webp +0 -0
  63. package/dist/types.d.ts +55 -5
  64. package/dist/view.d.ts +63 -4
  65. package/dist/view.js +52 -4
  66. package/package.json +71 -8
@@ -0,0 +1,141 @@
1
+ // The two scales the space view can show. The true one is the Earth's radius for a unit and the real distances in it; the compressed one keeps every
2
+ // direction but squeezes the distances so that the Moon, the Sun and Neptune fit one view. Pure.
3
+ import { AU_IN_EARTH_RADII } from "./ephemeris.js";
4
+ const MOON_AU = 0.00257;
5
+ /**
6
+ * The Moon's distance in a compressed view, in Earth radii (the true one is about 60).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { MOON_COMPRESSED } from "@johnmorrisdotca/chikyu-earth/space";
11
+ *
12
+ * console.log(MOON_COMPRESSED);
13
+ * ```
14
+ */
15
+ export const MOON_COMPRESSED = 10;
16
+ /**
17
+ * The Sun's distance from the Earth in a compressed view, in Earth radii (the true one is about 23,500). One astronomical unit is this long there.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { SUN_COMPRESSED } from "@johnmorrisdotca/chikyu-earth/space";
22
+ *
23
+ * console.log(SUN_COMPRESSED);
24
+ * ```
25
+ */
26
+ export const SUN_COMPRESSED = 44;
27
+ const length = (v) => Math.hypot(v[0], v[1], v[2]);
28
+ const scaled = (v, k) => [v[0] * k, v[1] * k, v[2] * k];
29
+ /**
30
+ * A place in astronomical units from the Earth's centre, at true distance, in Earth radii.
31
+ *
32
+ * @param v - The place, in astronomical units.
33
+ * @returns The same direction, in Earth radii.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { truePosition } from "@johnmorrisdotca/chikyu-earth/space";
38
+ *
39
+ * console.log(truePosition([0.00257, 0, 0]).map(Math.round));
40
+ * ```
41
+ */
42
+ export const truePosition = (v) => scaled(v, AU_IN_EARTH_RADII);
43
+ /**
44
+ * The same direction at a compressed distance from the Earth: the Moon is 10 Earth radii away, and a body k times further than the Moon is k^0.25 times as far here.
45
+ *
46
+ * @param v - The place, in astronomical units from the Earth's centre.
47
+ * @returns The place in Earth radii, compressed; the centre stays at the centre.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { compress } from "@johnmorrisdotca/chikyu-earth/space";
52
+ *
53
+ * console.log(compress([0.00257, 0, 0]), compress([0, 0, 0]));
54
+ * ```
55
+ */
56
+ export function compress(v) {
57
+ const l = length(v);
58
+ return l === 0 ? [0, 0, 0] : scaled(v, (MOON_COMPRESSED * (l / MOON_AU) ** 0.25) / l);
59
+ }
60
+ /**
61
+ * The Moon, from the Earth's centre.
62
+ *
63
+ * @param mode - The scale.
64
+ * @param geo - The Moon's place in astronomical units from the Earth's centre.
65
+ * @returns Where to draw it, in Earth radii.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { placeMoon } from "@johnmorrisdotca/chikyu-earth/space";
70
+ *
71
+ * console.log(placeMoon("true", [0.00257, 0, 0]).map(Math.round));
72
+ * ```
73
+ */
74
+ export const placeMoon = (mode, geo) => (mode === "true" ? truePosition(geo) : compress(geo));
75
+ /**
76
+ * The Sun, from the Earth's centre: in a compressed view always 44 Earth radii away, in its real direction.
77
+ *
78
+ * @param mode - The scale.
79
+ * @param geo - The Sun's place in astronomical units from the Earth's centre.
80
+ * @returns Where to draw it, in Earth radii.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { placeSun } from "@johnmorrisdotca/chikyu-earth/space";
85
+ *
86
+ * console.log(placeSun("compressed", [1, 0, 0]));
87
+ * ```
88
+ */
89
+ export function placeSun(mode, geo) {
90
+ if (mode === "true")
91
+ return truePosition(geo);
92
+ const l = length(geo);
93
+ return l === 0 ? [0, 0, 0] : scaled(geo, SUN_COMPRESSED / l);
94
+ }
95
+ /**
96
+ * A place `helio` astronomical units from the Sun (a planet, or a point on its orbit), round the Sun at `sun`. True: as it is. Compressed: the same direction from
97
+ * the Sun, at 44 Earth radii times its distance in astronomical units to the power 0.45, so the Earth's own orbit is the 44 and Neptune's is about 200 (30 times as far, really).
98
+ *
99
+ * @param mode - The scale.
100
+ * @param sun - Where the Sun is drawn, in Earth radii.
101
+ * @param helio - The place in astronomical units from the Sun.
102
+ * @returns Where to draw the place, in Earth radii from the Earth's centre.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { placeAround } from "@johnmorrisdotca/chikyu-earth/space";
107
+ *
108
+ * console.log(placeAround("compressed", [44, 0, 0], [1, 0, 0]));
109
+ * ```
110
+ */
111
+ export function placeAround(mode, sun, helio) {
112
+ if (mode === "true")
113
+ return [sun[0] + helio[0] * AU_IN_EARTH_RADII, sun[1] + helio[1] * AU_IN_EARTH_RADII, sun[2] + helio[2] * AU_IN_EARTH_RADII];
114
+ const l = length(helio);
115
+ if (l === 0)
116
+ return sun;
117
+ const d = (SUN_COMPRESSED * l ** 0.45) / l;
118
+ return [sun[0] + helio[0] * d, sun[1] + helio[1] * d, sun[2] + helio[2] * d];
119
+ }
120
+ /**
121
+ * Radii in Earth radii: the real ones, and ones large enough to see in a compressed view.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * import { RADIUS } from "@johnmorrisdotca/chikyu-earth/space";
126
+ *
127
+ * console.log(RADIUS["Moon"]);
128
+ * ```
129
+ */
130
+ export const RADIUS = {
131
+ Sun: { true: 109.1, compressed: 5 },
132
+ Earth: { true: 1, compressed: 1 },
133
+ Moon: { true: 0.2727, compressed: 0.9 },
134
+ Mercury: { true: 0.383, compressed: 0.5 },
135
+ Venus: { true: 0.949, compressed: 0.8 },
136
+ Mars: { true: 0.532, compressed: 0.6 },
137
+ Jupiter: { true: 10.97, compressed: 1.8 },
138
+ Saturn: { true: 9.14, compressed: 1.6 },
139
+ Uranus: { true: 3.98, compressed: 1.0 },
140
+ Neptune: { true: 3.86, compressed: 1.0 },
141
+ };
@@ -0,0 +1,146 @@
1
+ import type * as THREE from "three";
2
+ import type { AssetCache, Item3D } from "@johnmorrisdotca/kazoku-ui/renderer";
3
+ import { type ThreeParts } from "../realistic/textures.ts";
4
+ import type { Layer } from "../realistic/tiers.ts";
5
+ import { type Frame, type PlanetName, type Vec3 } from "./ephemeris.ts";
6
+ import { type ScaleMode } from "./scale.ts";
7
+ import type { SatelliteRef } from "../satellites/types.ts";
8
+ import { type SatelliteState } from "./satellite-layer.ts";
9
+ type Three = ThreeParts;
10
+ /** The camera's distance, in Earth radii, at which the Earth fills the view at zoom 1. */
11
+ export declare const FIT_DISTANCE = 3.4;
12
+ /**
13
+ * The ten bodies the space view places, names and lets a reader choose: the Earth, the Sun, the Moon and the seven other planets.
14
+ *
15
+ * @example
16
+ * ```ts no-run
17
+ * import { BODY_NAMES } from "@johnmorrisdotca/chikyu-earth/space/element";
18
+ *
19
+ * console.log(BODY_NAMES.join(", "));
20
+ * ```
21
+ */
22
+ export declare const BODY_NAMES: readonly ["Earth", "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"];
23
+ /**
24
+ * The name of one of the bodies in `BODY_NAMES`.
25
+ *
26
+ * @example
27
+ * ```ts no-run
28
+ * import { BODY_NAMES, type BodyName } from "@johnmorrisdotca/chikyu-earth/space/element";
29
+ *
30
+ * const mars: BodyName = "Mars";
31
+ * console.log(BODY_NAMES.includes(mars));
32
+ * ```
33
+ */
34
+ export type BodyName = (typeof BODY_NAMES)[number];
35
+ export interface SpaceModel {
36
+ /** Moment shown, milliseconds since the epoch. */
37
+ time: number;
38
+ mode: ScaleMode;
39
+ /** The shared controls' yaw, tilt and zoom. */
40
+ view: {
41
+ yaw: number;
42
+ tilt: number;
43
+ zoom: number;
44
+ };
45
+ /** The home place: the camera starts above it. */
46
+ centre: {
47
+ lon: number;
48
+ lat: number;
49
+ };
50
+ /** Earth's turn against the stars (degrees) at the moment the camera was last moved: the camera is fixed to the stars, not to the ground. */
51
+ cameraSpin: number | null;
52
+ orbits: boolean;
53
+ labels: boolean;
54
+ /** Milliseconds of game time one second of wall time covers (the ephemeris is looked up once per such span). */
55
+ stepMs: number;
56
+ /** The body the camera looks at, and (in `zoom`) fills the view with at 1x. */
57
+ focus: BodyName;
58
+ /** Filled at each draw. */
59
+ drawn: {
60
+ frame: Frame;
61
+ distance: number;
62
+ radiusPx: number;
63
+ } | null;
64
+ /** How many times astronomy-engine has been asked for a frame. */
65
+ lookups: number;
66
+ /** Game seconds a second (the clock's speed). */
67
+ speed: number;
68
+ /** The satellites shown, the one chosen, and what the last draw did with them. */
69
+ satellites: SatelliteState;
70
+ /** The satellite at a point of the view (CSS pixels), within a radius, if a draw has been made and one is there. */
71
+ pick: ((x: number, y: number, radius: number) => SatelliteRef | null) | null;
72
+ ratio(): number;
73
+ }
74
+ export declare const newSpaceModel: () => SpaceModel;
75
+ /** Where a body is on the screen: its centre and its apparent radius, in CSS pixels in the box. */
76
+ export interface BodyPlace {
77
+ x: number;
78
+ y: number;
79
+ r: number;
80
+ }
81
+ /**
82
+ * One body as drawn by a painter: an object of radius 1 at the origin, which the scene places, scales and (for the Sun) turns to face the camera.
83
+ *
84
+ * @example
85
+ * ```ts no-run
86
+ * import type { BodyDrawing } from "@johnmorrisdotca/chikyu-earth/space";
87
+ *
88
+ * const drawing = { object: undefined as never, dispose: () => undefined } satisfies BodyDrawing;
89
+ * console.log(typeof drawing.dispose);
90
+ * ```
91
+ */
92
+ export interface BodyDrawing {
93
+ object: THREE.Object3D;
94
+ /** Told at each draw the unit direction from the body to the Sun, in scene axes (x to the vernal equinox, y north). */
95
+ light?(toSun: Vec3): void;
96
+ /** True to have the scene turn the object to face the camera (a disc or a glow). */
97
+ billboard?: boolean;
98
+ dispose(): void;
99
+ }
100
+ /**
101
+ * The seam for drawing the Sun and the planets. The space view places them, sizes them, lights them and labels them; a painter only makes what is drawn at
102
+ * each place. The default (`plainPainter`) is a sphere in the body's colour, and a glow for the Sun. Another package can supply better looking ones.
103
+ *
104
+ * @example
105
+ * ```ts no-run
106
+ * import { plainPainter, type BodyPainter } from "@johnmorrisdotca/chikyu-earth/space";
107
+ *
108
+ * const painter: BodyPainter = { make: (three, name) => plainPainter.make(three, name) };
109
+ * console.log(typeof painter.make);
110
+ * ```
111
+ */
112
+ export interface BodyPainter {
113
+ make(three: ThreeParts, name: PlanetName | "Sun"): BodyDrawing;
114
+ }
115
+ export interface SpaceDeps {
116
+ assets: AssetCache;
117
+ textureBase: () => string;
118
+ cap: () => number;
119
+ invalidate: () => void;
120
+ onError?: (error: unknown) => void;
121
+ onTier?: (layer: Layer, width: number) => void;
122
+ /** Where each body is on the screen, in CSS pixels in the box, or null when it is behind the camera. Called at each draw. */
123
+ onLabels?: (places: Record<BodyName, BodyPlace | null>) => void;
124
+ /** Where the chosen satellite is on the screen, or null. Called at each draw while a satellite is chosen (and once when it is let go). */
125
+ onSelected?: (place: {
126
+ x: number;
127
+ y: number;
128
+ } | null) => void;
129
+ /** The satellites could not be drawn (the three.js parts handed over lack `Points`). */
130
+ onSatellitesError?: (error: unknown) => void;
131
+ /** How the Sun and the planets are drawn; left out, plain spheres in the right colours (see `BodyPainter`). */
132
+ painter?: BodyPainter;
133
+ }
134
+ /**
135
+ * The default look: a sphere in the planet's colour, lit by the Sun, and for the Sun a glowing disc.
136
+ *
137
+ * @example
138
+ * ```ts no-run
139
+ * import { plainPainter } from "@johnmorrisdotca/chikyu-earth/space";
140
+ *
141
+ * console.log(typeof plainPainter.make);
142
+ * ```
143
+ */
144
+ export declare const plainPainter: BodyPainter;
145
+ export declare function createSpaceItem(model: SpaceModel, deps: SpaceDeps): Item3D<Three>;
146
+ export {};
@@ -0,0 +1,320 @@
1
+ import { ATMOSPHERE_OUTER, sphere } from "../realistic/earth.js";
2
+ import { ATMOSPHERE_FRAG, ATMOSPHERE_VERT, BODY_FRAG, CLOUD_FRAG, GLOW_FRAG, GLOW_VERT, SURFACE_FRAG, SURFACE_VERT } from "../realistic/shaders.js";
3
+ import { createTextureSet } from "../realistic/textures.js";
4
+ import { createTrack, orbitPath, PLANETS } from "./ephemeris.js";
5
+ import { placeAround, placeMoon, placeSun, RADIUS } from "./scale.js";
6
+ import { createSatelliteLayer, newSatelliteState } from "./satellite-layer.js";
7
+ const RAD = Math.PI / 180;
8
+ /** The camera's distance, in Earth radii, at which the Earth fills the view at zoom 1. */
9
+ export const FIT_DISTANCE = 3.4;
10
+ const FOV = 35;
11
+ const LAYERS = ["day", "night", "clouds", "water", "moon"];
12
+ const PLAIN_COLOR = (name) => (name === "Sun" ? "#ffb347" : COLORS[name]);
13
+ /**
14
+ * The ten bodies the space view places, names and lets a reader choose: the Earth, the Sun, the Moon and the seven other planets.
15
+ *
16
+ * @example
17
+ * ```ts no-run
18
+ * import { BODY_NAMES } from "@johnmorrisdotca/chikyu-earth/space/element";
19
+ *
20
+ * console.log(BODY_NAMES.join(", "));
21
+ * ```
22
+ */
23
+ export const BODY_NAMES = ["Earth", "Sun", "Moon", ...PLANETS];
24
+ const COLORS = { Mercury: "#a79f96", Venus: "#e3c98a", Mars: "#c1593a", Jupiter: "#d2b48c", Saturn: "#e4d4a0", Uranus: "#9fd6dc", Neptune: "#4a6bd6" };
25
+ const ORBIT_COLORS = { Mercury: "#6b655f", Venus: "#8c7c55", Mars: "#7a4034", Jupiter: "#85735a", Saturn: "#8d8567", Uranus: "#5f8a8e", Neptune: "#34499a" };
26
+ export const newSpaceModel = () => ({
27
+ time: Date.now(),
28
+ mode: "compressed",
29
+ view: { yaw: 0, tilt: 0, zoom: 1 },
30
+ centre: { lon: 0, lat: 0 },
31
+ cameraSpin: null,
32
+ orbits: true,
33
+ labels: true,
34
+ stepMs: 1000,
35
+ focus: "Earth",
36
+ drawn: null,
37
+ lookups: 0,
38
+ speed: 1,
39
+ satellites: newSatelliteState(),
40
+ pick: null,
41
+ ratio: () => Math.min(globalThis.devicePixelRatio || 1, 2),
42
+ });
43
+ /** An inertial direction from the Earth's centre, in scene axes, for right ascension and declination in degrees. */
44
+ const direction = (ra, dec) => [Math.cos(dec * RAD) * Math.cos(ra * RAD), Math.sin(dec * RAD), -Math.cos(dec * RAD) * Math.sin(ra * RAD)];
45
+ const unit = (v) => {
46
+ const l = Math.hypot(v[0], v[1], v[2]) || 1;
47
+ return [v[0] / l, v[1] / l, v[2] / l];
48
+ };
49
+ /** A square facing +z, 2 x 2, with uvs 0 to 1. */
50
+ function quad(three) {
51
+ const g = new three.BufferGeometry();
52
+ g.setAttribute("position", new three.BufferAttribute(new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]), 3));
53
+ g.setAttribute("uv", new three.BufferAttribute(new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), 2));
54
+ g.setIndex([0, 1, 2, 0, 2, 3]);
55
+ return g;
56
+ }
57
+ /**
58
+ * The default look: a sphere in the planet's colour, lit by the Sun, and for the Sun a glowing disc.
59
+ *
60
+ * @example
61
+ * ```ts no-run
62
+ * import { plainPainter } from "@johnmorrisdotca/chikyu-earth/space";
63
+ *
64
+ * console.log(typeof plainPainter.make);
65
+ * ```
66
+ */
67
+ export const plainPainter = {
68
+ make(three, name) {
69
+ if (name === "Sun") {
70
+ const geometry = quad(three);
71
+ const material = new three.ShaderMaterial({ vertexShader: GLOW_VERT, fragmentShader: GLOW_FRAG, uniforms: { tint: { value: new three.Color(PLAIN_COLOR(name)) } }, transparent: true, depthWrite: false });
72
+ const mesh = new three.Mesh(geometry, material);
73
+ mesh.renderOrder = 4;
74
+ mesh.frustumCulled = false;
75
+ mesh.scale.setScalar(3.2);
76
+ const group = new three.Group();
77
+ group.add(mesh);
78
+ return {
79
+ object: group,
80
+ billboard: true,
81
+ dispose() {
82
+ geometry.dispose();
83
+ material.dispose();
84
+ },
85
+ };
86
+ }
87
+ const geometry = sphere(three, 48, 24);
88
+ const sun = { value: new three.Vector3(1, 0, 0) };
89
+ const material = new three.ShaderMaterial({ vertexShader: SURFACE_VERT, fragmentShader: BODY_FRAG, uniforms: { map: { value: null }, tint: { value: new three.Color(PLAIN_COLOR(name)) }, textured: { value: 0 }, sunDir: sun, ambient: { value: 0.2 } } });
90
+ return {
91
+ object: new three.Mesh(geometry, material),
92
+ light(toSun) {
93
+ sun.value.set(toSun[0], toSun[1], toSun[2]);
94
+ },
95
+ dispose() {
96
+ geometry.dispose();
97
+ material.dispose();
98
+ },
99
+ };
100
+ },
101
+ };
102
+ export function createSpaceItem(model, deps) {
103
+ return {
104
+ key: "chikyu-earth:space",
105
+ async build({ three, width, height }) {
106
+ let size = { width, height };
107
+ const textures = await createTextureSet(three, LAYERS, {
108
+ assets: deps.assets,
109
+ textureBase: deps.textureBase,
110
+ cap: deps.cap,
111
+ invalidate: deps.invalidate,
112
+ ...(deps.onError ? { onError: deps.onError } : {}),
113
+ ...(deps.onTier ? { onTier: deps.onTier } : {}),
114
+ });
115
+ const uniform = textures.uniforms;
116
+ const track = createTrack(model.stepMs);
117
+ let trackStep = model.stepMs;
118
+ const sunEarth = { value: new three.Vector3(1, 0, 0) };
119
+ const sunView = { value: new three.Vector3(1, 0, 0) };
120
+ const grid = sphere(three, 96, 48);
121
+ const solid = sphere(three, 48, 24);
122
+ const earthMaterial = new three.ShaderMaterial({
123
+ vertexShader: SURFACE_VERT,
124
+ fragmentShader: SURFACE_FRAG,
125
+ uniforms: { dayMap: uniform.day, nightMap: uniform.night, waterMap: uniform.water, sunDir: sunEarth, lights: { value: 1 }, ortho: { value: 0 } },
126
+ });
127
+ const cloudShift = { value: 0 };
128
+ const cloudMaterial = new three.ShaderMaterial({
129
+ vertexShader: SURFACE_VERT,
130
+ fragmentShader: CLOUD_FRAG,
131
+ uniforms: { cloudMap: uniform.clouds, sunDir: sunEarth, shift: cloudShift, opacity: { value: 0.9 } },
132
+ transparent: true,
133
+ depthWrite: false,
134
+ });
135
+ const atmosphereMaterial = new three.ShaderMaterial({
136
+ vertexShader: ATMOSPHERE_VERT,
137
+ fragmentShader: ATMOSPHERE_FRAG,
138
+ uniforms: { sunView, outer: { value: ATMOSPHERE_OUTER }, ortho: { value: 0 } },
139
+ transparent: true,
140
+ depthWrite: false,
141
+ side: three.BackSide,
142
+ });
143
+ const earth = new three.Mesh(grid, earthMaterial);
144
+ const clouds = new three.Mesh(grid, cloudMaterial);
145
+ clouds.scale.setScalar(1.006);
146
+ clouds.renderOrder = 3;
147
+ const glow = new three.Mesh(grid, atmosphereMaterial);
148
+ glow.scale.setScalar(ATMOSPHERE_OUTER);
149
+ glow.renderOrder = 2;
150
+ glow.frustumCulled = false;
151
+ const moonSun = { value: new three.Vector3(1, 0, 0) };
152
+ const moon = new three.Mesh(solid, new three.ShaderMaterial({ vertexShader: SURFACE_VERT, fragmentShader: BODY_FRAG, uniforms: { map: uniform.moon, tint: { value: new three.Color("#bbbbbb") }, textured: { value: 1 }, sunDir: moonSun, ambient: { value: 0.03 } } }));
153
+ const painter = deps.painter ?? plainPainter;
154
+ const scene = new three.Scene();
155
+ scene.add(earth, clouds, glow, moon);
156
+ const sunDrawing = painter.make(three, "Sun");
157
+ scene.add(sunDrawing.object);
158
+ const planets = new Map();
159
+ for (const name of PLANETS) {
160
+ const drawing = painter.make(three, name);
161
+ const positions = new Float32Array(96 * 3);
162
+ const lineGeometry = new three.BufferGeometry();
163
+ lineGeometry.setAttribute("position", new three.BufferAttribute(positions, 3));
164
+ const orbit = new three.LineLoop(lineGeometry, new three.LineBasicMaterial({ color: ORBIT_COLORS[name], transparent: true, opacity: 0.7 }));
165
+ orbit.frustumCulled = false;
166
+ scene.add(drawing.object, orbit);
167
+ planets.set(name, { drawing, orbit, positions, samples: [], epoch: Number.NaN });
168
+ }
169
+ let layer;
170
+ let selectedShown = false;
171
+ const camera = new three.PerspectiveCamera(FOV, width / Math.max(1, height), 0.05, 1000);
172
+ const target = new three.Vector3();
173
+ const projected = new three.Vector3();
174
+ const labelFor = (v) => {
175
+ projected.set(v[0], v[1], v[2]).project(camera);
176
+ if (projected.z > 1 || projected.z < -1)
177
+ return null;
178
+ return { x: ((projected.x + 1) / 2) * size.width, y: ((1 - projected.y) / 2) * size.height };
179
+ };
180
+ return {
181
+ scene,
182
+ camera,
183
+ update() {
184
+ const frame = track.at(model.time);
185
+ model.lookups = track.lookups;
186
+ if (model.stepMs !== trackStep) {
187
+ trackStep = model.stepMs;
188
+ track.setStep(model.stepMs);
189
+ }
190
+ const mode = model.mode;
191
+ const sunPos = placeSun(mode, frame.sun);
192
+ const moonPos = placeMoon(mode, frame.moon);
193
+ const helioOf = (planet) => [planet[0] - frame.sun[0], planet[1] - frame.sun[1], planet[2] - frame.sun[2]];
194
+ // the Earth, turned by the sidereal time; light from the Sun
195
+ earth.rotation.set(0, (frame.spin + 90) * RAD, 0);
196
+ clouds.rotation.copy(earth.rotation);
197
+ const toSun = unit(frame.sun);
198
+ sunEarth.value.set(toSun[0], toSun[1], toSun[2]);
199
+ cloudShift.value = -(((model.time / 3_600_000) * 1.5) % 360) / 360;
200
+ earthMaterial.uniforms["lights"].value = 1;
201
+ // the Moon, with its near side to the Earth
202
+ moon.position.set(moonPos[0], moonPos[1], moonPos[2]);
203
+ moon.scale.setScalar(RADIUS["Moon"][mode]);
204
+ moon.up.set(0, 1, 0);
205
+ moon.lookAt(0, 0, 0);
206
+ const moonToSun = unit([frame.sun[0] - frame.moon[0], frame.sun[1] - frame.moon[1], frame.sun[2] - frame.moon[2]]);
207
+ moonSun.value.set(moonToSun[0], moonToSun[1], moonToSun[2]);
208
+ // where everything is, and how big
209
+ const pos = { Earth: [0, 0, 0], Moon: moonPos, Sun: sunPos };
210
+ for (const name of PLANETS)
211
+ pos[name] = placeAround(mode, sunPos, helioOf(frame.planets[name]));
212
+ const radiusOf = (name) => (name === "Earth" ? 1 : RADIUS[name][mode]);
213
+ // the Sun and the planets, drawn by the painter, and the planets' orbits round the Sun
214
+ sunDrawing.object.position.set(sunPos[0], sunPos[1], sunPos[2]);
215
+ sunDrawing.object.scale.setScalar(radiusOf("Sun"));
216
+ if (sunDrawing.billboard)
217
+ sunDrawing.object.quaternion.copy(camera.quaternion);
218
+ for (const name of PLANETS) {
219
+ const p = planets.get(name);
220
+ p.drawing.object.position.set(pos[name][0], pos[name][1], pos[name][2]);
221
+ p.drawing.object.scale.setScalar(radiusOf(name));
222
+ p.drawing.light?.(unit([frame.sun[0] - frame.planets[name][0], frame.sun[1] - frame.planets[name][1], frame.sun[2] - frame.planets[name][2]]));
223
+ p.orbit.visible = model.orbits;
224
+ if (model.orbits) {
225
+ if (!Number.isFinite(p.epoch) || Math.abs(model.time - p.epoch) > 365 * 86_400_000) {
226
+ p.samples = orbitPath(name, model.time, 96).slice(0, 96);
227
+ p.epoch = model.time;
228
+ }
229
+ for (let i = 0; i < 96; i += 1) {
230
+ const o = placeAround(mode, sunPos, p.samples[i]);
231
+ p.positions[i * 3] = o[0];
232
+ p.positions[i * 3 + 1] = o[1];
233
+ p.positions[i * 3 + 2] = o[2];
234
+ }
235
+ p.orbit.geometry.getAttribute("position").needsUpdate = true;
236
+ }
237
+ }
238
+ if (!layer && model.satellites.groups.size > 0) {
239
+ try {
240
+ layer = createSatelliteLayer(three, scene, model.satellites, model.ratio);
241
+ }
242
+ catch (error) {
243
+ model.satellites.groups.clear();
244
+ deps.onSatellitesError?.(error);
245
+ }
246
+ }
247
+ layer?.update(model.time, model.speed);
248
+ model.pick = layer ? (x, y, radius) => layer.pick(x, y, camera, size, radius) : null;
249
+ // the camera: looks at the focus body from the direction the sky was in when it was last moved, turned by yaw and tilt, as far as the zoom says
250
+ const focus = model.focus;
251
+ const focusRadius = radiusOf(focus);
252
+ const spin = model.cameraSpin ?? frame.spin;
253
+ const lon = model.centre.lon - model.view.yaw;
254
+ const lat = Math.max(-90, Math.min(90, model.centre.lat + model.view.tilt));
255
+ const aim = direction(spin + lon, lat);
256
+ const distance = Math.max(1.05 * focusRadius, (FIT_DISTANCE * focusRadius) / Math.max(1e-9, model.view.zoom));
257
+ target.set(pos[focus][0], pos[focus][1], pos[focus][2]);
258
+ camera.position.set(target.x + aim[0] * distance, target.y + aim[1] * distance, target.z + aim[2] * distance);
259
+ camera.up.set(0, 1, 0);
260
+ camera.lookAt(target);
261
+ camera.near = Math.max(0.0005, distance * 0.02 - focusRadius * 0.5, focusRadius * 0.01);
262
+ camera.far = Math.max(2000, distance * 4 + (mode === "true" ? 40 * 23_481 : 400));
263
+ camera.aspect = size.width / Math.max(1, size.height);
264
+ camera.updateProjectionMatrix();
265
+ camera.updateMatrixWorld();
266
+ sunView.value.copy(sunEarth.value).transformDirection(camera.matrixWorldInverse);
267
+ // how many pixels across each body is: for the pictures the Earth and the Moon need, and for what is chosen
268
+ const half = Math.tan((FOV / 2) * RAD);
269
+ const radiusPxOf = (name) => {
270
+ const d = Math.hypot(pos[name][0] - camera.position.x, pos[name][1] - camera.position.y, pos[name][2] - camera.position.z);
271
+ const r = radiusOf(name);
272
+ return d <= r * 1.001 ? 1e5 : ((size.height / 2) * (r / Math.sqrt(d * d - r * r))) / half;
273
+ };
274
+ const px = {};
275
+ for (const name of BODY_NAMES)
276
+ px[name] = radiusPxOf(name);
277
+ model.drawn = { frame, distance, radiusPx: px.Earth };
278
+ const ratio = model.ratio();
279
+ const earthPx = Math.min(px.Earth, 1e5) * ratio;
280
+ textures.sync({ day: earthPx, night: earthPx, clouds: earthPx, water: earthPx, moon: Math.min(px.Moon, 1e5) * ratio });
281
+ if (deps.onLabels && model.labels) {
282
+ const places = {};
283
+ for (const name of BODY_NAMES) {
284
+ const at = labelFor(pos[name]);
285
+ places[name] = at ? { x: at.x, y: at.y, r: px[name] } : null;
286
+ }
287
+ deps.onLabels(places);
288
+ }
289
+ if (layer && deps.onSelected) {
290
+ const at = model.satellites.selected ? layer.place(camera, size) : null;
291
+ if (at || selectedShown)
292
+ deps.onSelected(at);
293
+ selectedShown = at !== null;
294
+ }
295
+ return false;
296
+ },
297
+ resize(w, h) {
298
+ size = { width: w, height: h };
299
+ },
300
+ dispose() {
301
+ layer?.dispose();
302
+ model.pick = null;
303
+ textures.dispose();
304
+ sunDrawing.dispose();
305
+ for (const p of planets.values()) {
306
+ p.drawing.dispose();
307
+ p.orbit.geometry.dispose();
308
+ p.orbit.material.dispose();
309
+ }
310
+ grid.dispose();
311
+ solid.dispose();
312
+ earthMaterial.dispose();
313
+ cloudMaterial.dispose();
314
+ atmosphereMaterial.dispose();
315
+ moon.material.dispose();
316
+ },
317
+ };
318
+ },
319
+ };
320
+ }