@johnmorrisdotca/chikyu-earth 0.5.0 → 0.7.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 +42 -0
- package/NOTICE.md +11 -0
- package/README.md +172 -27
- package/dist/countries.d.ts +11 -0
- package/dist/countries.js +11 -0
- package/dist/element.d.ts +18 -0
- package/dist/element.js +15 -2
- package/dist/globe.d.ts +15 -1
- package/dist/globe.js +15 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +12 -1
- package/dist/markers/cluster.d.ts +27 -0
- package/dist/markers/cluster.js +16 -0
- package/dist/markers/labels.d.ts +40 -0
- package/dist/markers/labels.js +18 -0
- package/dist/markers/layer.d.ts +80 -0
- package/dist/markers/layer.js +21 -0
- package/dist/markers/project.d.ts +63 -2
- package/dist/markers/project.js +52 -2
- package/dist/markers/types.d.ts +48 -3
- package/dist/realistic/earth.d.ts +30 -5
- package/dist/realistic/earth.js +41 -134
- package/dist/realistic/element.d.ts +47 -7
- package/dist/realistic/element.js +48 -44
- package/dist/realistic/gate.d.ts +45 -1
- package/dist/realistic/gate.js +33 -1
- package/dist/realistic/shaders.d.ts +9 -4
- package/dist/realistic/shaders.js +54 -9
- package/dist/realistic/shared.d.ts +23 -0
- package/dist/realistic/shared.js +51 -0
- package/dist/realistic/sun.d.ts +40 -3
- package/dist/realistic/sun.js +40 -3
- package/dist/realistic/textures.d.ts +40 -0
- package/dist/realistic/textures.js +141 -0
- package/dist/realistic/tiers.d.ts +80 -6
- package/dist/realistic/tiers.js +69 -5
- package/dist/rotate.d.ts +59 -4
- package/dist/rotate.js +59 -4
- package/dist/space/element.d.ts +134 -0
- package/dist/space/element.js +394 -0
- package/dist/space/ephemeris.d.ts +151 -0
- package/dist/space/ephemeris.js +162 -0
- package/dist/space/index.d.ts +3 -0
- package/dist/space/index.js +5 -0
- package/dist/space/scale.d.ts +124 -0
- package/dist/space/scale.js +141 -0
- package/dist/space/scene.d.ts +131 -0
- package/dist/space/scene.js +295 -0
- package/dist/styles.d.ts +65 -2
- package/dist/styles.js +43 -2
- package/dist/textures/moon-1024.webp +0 -0
- package/dist/textures/moon-2048.webp +0 -0
- package/dist/types.d.ts +55 -5
- package/dist/view.d.ts +63 -4
- package/dist/view.js +52 -4
- package/package.json +55 -8
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
type Three = ThreeParts;
|
|
8
|
+
/** The camera's distance, in Earth radii, at which the Earth fills the view at zoom 1. */
|
|
9
|
+
export declare const FIT_DISTANCE = 3.4;
|
|
10
|
+
/**
|
|
11
|
+
* The ten bodies the space view places, names and lets a reader choose: the Earth, the Sun, the Moon and the seven other planets.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts no-run
|
|
15
|
+
* import { BODY_NAMES } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
16
|
+
*
|
|
17
|
+
* console.log(BODY_NAMES.join(", "));
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const BODY_NAMES: readonly ["Earth", "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"];
|
|
21
|
+
/**
|
|
22
|
+
* The name of one of the bodies in `BODY_NAMES`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts no-run
|
|
26
|
+
* import { BODY_NAMES, type BodyName } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
27
|
+
*
|
|
28
|
+
* const mars: BodyName = "Mars";
|
|
29
|
+
* console.log(BODY_NAMES.includes(mars));
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export type BodyName = (typeof BODY_NAMES)[number];
|
|
33
|
+
export interface SpaceModel {
|
|
34
|
+
/** Moment shown, milliseconds since the epoch. */
|
|
35
|
+
time: number;
|
|
36
|
+
mode: ScaleMode;
|
|
37
|
+
/** The shared controls' yaw, tilt and zoom. */
|
|
38
|
+
view: {
|
|
39
|
+
yaw: number;
|
|
40
|
+
tilt: number;
|
|
41
|
+
zoom: number;
|
|
42
|
+
};
|
|
43
|
+
/** The home place: the camera starts above it. */
|
|
44
|
+
centre: {
|
|
45
|
+
lon: number;
|
|
46
|
+
lat: number;
|
|
47
|
+
};
|
|
48
|
+
/** 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. */
|
|
49
|
+
cameraSpin: number | null;
|
|
50
|
+
orbits: boolean;
|
|
51
|
+
labels: boolean;
|
|
52
|
+
/** Milliseconds of game time one second of wall time covers (the ephemeris is looked up once per such span). */
|
|
53
|
+
stepMs: number;
|
|
54
|
+
/** The body the camera looks at, and (in `zoom`) fills the view with at 1x. */
|
|
55
|
+
focus: BodyName;
|
|
56
|
+
/** Filled at each draw. */
|
|
57
|
+
drawn: {
|
|
58
|
+
frame: Frame;
|
|
59
|
+
distance: number;
|
|
60
|
+
radiusPx: number;
|
|
61
|
+
} | null;
|
|
62
|
+
/** How many times astronomy-engine has been asked for a frame. */
|
|
63
|
+
lookups: number;
|
|
64
|
+
ratio(): number;
|
|
65
|
+
}
|
|
66
|
+
export declare const newSpaceModel: () => SpaceModel;
|
|
67
|
+
/** Where a body is on the screen: its centre and its apparent radius, in CSS pixels in the box. */
|
|
68
|
+
export interface BodyPlace {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
r: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 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.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts no-run
|
|
78
|
+
* import type { BodyDrawing } from "@johnmorrisdotca/chikyu-earth/space";
|
|
79
|
+
*
|
|
80
|
+
* const drawing = { object: undefined as never, dispose: () => undefined } satisfies BodyDrawing;
|
|
81
|
+
* console.log(typeof drawing.dispose);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export interface BodyDrawing {
|
|
85
|
+
object: THREE.Object3D;
|
|
86
|
+
/** Told at each draw the unit direction from the body to the Sun, in scene axes (x to the vernal equinox, y north). */
|
|
87
|
+
light?(toSun: Vec3): void;
|
|
88
|
+
/** True to have the scene turn the object to face the camera (a disc or a glow). */
|
|
89
|
+
billboard?: boolean;
|
|
90
|
+
dispose(): void;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 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
|
|
94
|
+
* 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.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts no-run
|
|
98
|
+
* import { plainPainter, type BodyPainter } from "@johnmorrisdotca/chikyu-earth/space";
|
|
99
|
+
*
|
|
100
|
+
* const painter: BodyPainter = { make: (three, name) => plainPainter.make(three, name) };
|
|
101
|
+
* console.log(typeof painter.make);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export interface BodyPainter {
|
|
105
|
+
make(three: ThreeParts, name: PlanetName | "Sun"): BodyDrawing;
|
|
106
|
+
}
|
|
107
|
+
export interface SpaceDeps {
|
|
108
|
+
assets: AssetCache;
|
|
109
|
+
textureBase: () => string;
|
|
110
|
+
cap: () => number;
|
|
111
|
+
invalidate: () => void;
|
|
112
|
+
onError?: (error: unknown) => void;
|
|
113
|
+
onTier?: (layer: Layer, width: number) => void;
|
|
114
|
+
/** 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. */
|
|
115
|
+
onLabels?: (places: Record<BodyName, BodyPlace | null>) => void;
|
|
116
|
+
/** How the Sun and the planets are drawn; left out, plain spheres in the right colours (see `BodyPainter`). */
|
|
117
|
+
painter?: BodyPainter;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The default look: a sphere in the planet's colour, lit by the Sun, and for the Sun a glowing disc.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts no-run
|
|
124
|
+
* import { plainPainter } from "@johnmorrisdotca/chikyu-earth/space";
|
|
125
|
+
*
|
|
126
|
+
* console.log(typeof plainPainter.make);
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare const plainPainter: BodyPainter;
|
|
130
|
+
export declare function createSpaceItem(model: SpaceModel, deps: SpaceDeps): Item3D<Three>;
|
|
131
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
const RAD = Math.PI / 180;
|
|
7
|
+
/** The camera's distance, in Earth radii, at which the Earth fills the view at zoom 1. */
|
|
8
|
+
export const FIT_DISTANCE = 3.4;
|
|
9
|
+
const FOV = 35;
|
|
10
|
+
const LAYERS = ["day", "night", "clouds", "water", "moon"];
|
|
11
|
+
const PLAIN_COLOR = (name) => (name === "Sun" ? "#ffb347" : COLORS[name]);
|
|
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 const BODY_NAMES = ["Earth", "Sun", "Moon", ...PLANETS];
|
|
23
|
+
const COLORS = { Mercury: "#a79f96", Venus: "#e3c98a", Mars: "#c1593a", Jupiter: "#d2b48c", Saturn: "#e4d4a0", Uranus: "#9fd6dc", Neptune: "#4a6bd6" };
|
|
24
|
+
const ORBIT_COLORS = { Mercury: "#6b655f", Venus: "#8c7c55", Mars: "#7a4034", Jupiter: "#85735a", Saturn: "#8d8567", Uranus: "#5f8a8e", Neptune: "#34499a" };
|
|
25
|
+
export const newSpaceModel = () => ({
|
|
26
|
+
time: Date.now(),
|
|
27
|
+
mode: "compressed",
|
|
28
|
+
view: { yaw: 0, tilt: 0, zoom: 1 },
|
|
29
|
+
centre: { lon: 0, lat: 0 },
|
|
30
|
+
cameraSpin: null,
|
|
31
|
+
orbits: true,
|
|
32
|
+
labels: true,
|
|
33
|
+
stepMs: 1000,
|
|
34
|
+
focus: "Earth",
|
|
35
|
+
drawn: null,
|
|
36
|
+
lookups: 0,
|
|
37
|
+
ratio: () => Math.min(globalThis.devicePixelRatio || 1, 2),
|
|
38
|
+
});
|
|
39
|
+
/** An inertial direction from the Earth's centre, in scene axes, for right ascension and declination in degrees. */
|
|
40
|
+
const direction = (ra, dec) => [Math.cos(dec * RAD) * Math.cos(ra * RAD), Math.sin(dec * RAD), -Math.cos(dec * RAD) * Math.sin(ra * RAD)];
|
|
41
|
+
const unit = (v) => {
|
|
42
|
+
const l = Math.hypot(v[0], v[1], v[2]) || 1;
|
|
43
|
+
return [v[0] / l, v[1] / l, v[2] / l];
|
|
44
|
+
};
|
|
45
|
+
/** A square facing +z, 2 x 2, with uvs 0 to 1. */
|
|
46
|
+
function quad(three) {
|
|
47
|
+
const g = new three.BufferGeometry();
|
|
48
|
+
g.setAttribute("position", new three.BufferAttribute(new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]), 3));
|
|
49
|
+
g.setAttribute("uv", new three.BufferAttribute(new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), 2));
|
|
50
|
+
g.setIndex([0, 1, 2, 0, 2, 3]);
|
|
51
|
+
return g;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The default look: a sphere in the planet's colour, lit by the Sun, and for the Sun a glowing disc.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts no-run
|
|
58
|
+
* import { plainPainter } from "@johnmorrisdotca/chikyu-earth/space";
|
|
59
|
+
*
|
|
60
|
+
* console.log(typeof plainPainter.make);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export const plainPainter = {
|
|
64
|
+
make(three, name) {
|
|
65
|
+
if (name === "Sun") {
|
|
66
|
+
const geometry = quad(three);
|
|
67
|
+
const material = new three.ShaderMaterial({ vertexShader: GLOW_VERT, fragmentShader: GLOW_FRAG, uniforms: { tint: { value: new three.Color(PLAIN_COLOR(name)) } }, transparent: true, depthWrite: false });
|
|
68
|
+
const mesh = new three.Mesh(geometry, material);
|
|
69
|
+
mesh.renderOrder = 4;
|
|
70
|
+
mesh.frustumCulled = false;
|
|
71
|
+
mesh.scale.setScalar(3.2);
|
|
72
|
+
const group = new three.Group();
|
|
73
|
+
group.add(mesh);
|
|
74
|
+
return {
|
|
75
|
+
object: group,
|
|
76
|
+
billboard: true,
|
|
77
|
+
dispose() {
|
|
78
|
+
geometry.dispose();
|
|
79
|
+
material.dispose();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const geometry = sphere(three, 48, 24);
|
|
84
|
+
const sun = { value: new three.Vector3(1, 0, 0) };
|
|
85
|
+
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 } } });
|
|
86
|
+
return {
|
|
87
|
+
object: new three.Mesh(geometry, material),
|
|
88
|
+
light(toSun) {
|
|
89
|
+
sun.value.set(toSun[0], toSun[1], toSun[2]);
|
|
90
|
+
},
|
|
91
|
+
dispose() {
|
|
92
|
+
geometry.dispose();
|
|
93
|
+
material.dispose();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
export function createSpaceItem(model, deps) {
|
|
99
|
+
return {
|
|
100
|
+
key: "chikyu-earth:space",
|
|
101
|
+
async build({ three, width, height }) {
|
|
102
|
+
let size = { width, height };
|
|
103
|
+
const textures = await createTextureSet(three, LAYERS, {
|
|
104
|
+
assets: deps.assets,
|
|
105
|
+
textureBase: deps.textureBase,
|
|
106
|
+
cap: deps.cap,
|
|
107
|
+
invalidate: deps.invalidate,
|
|
108
|
+
...(deps.onError ? { onError: deps.onError } : {}),
|
|
109
|
+
...(deps.onTier ? { onTier: deps.onTier } : {}),
|
|
110
|
+
});
|
|
111
|
+
const uniform = textures.uniforms;
|
|
112
|
+
const track = createTrack(model.stepMs);
|
|
113
|
+
let trackStep = model.stepMs;
|
|
114
|
+
const sunEarth = { value: new three.Vector3(1, 0, 0) };
|
|
115
|
+
const sunView = { value: new three.Vector3(1, 0, 0) };
|
|
116
|
+
const grid = sphere(three, 96, 48);
|
|
117
|
+
const solid = sphere(three, 48, 24);
|
|
118
|
+
const earthMaterial = new three.ShaderMaterial({
|
|
119
|
+
vertexShader: SURFACE_VERT,
|
|
120
|
+
fragmentShader: SURFACE_FRAG,
|
|
121
|
+
uniforms: { dayMap: uniform.day, nightMap: uniform.night, waterMap: uniform.water, sunDir: sunEarth, lights: { value: 1 }, ortho: { value: 0 } },
|
|
122
|
+
});
|
|
123
|
+
const cloudShift = { value: 0 };
|
|
124
|
+
const cloudMaterial = new three.ShaderMaterial({
|
|
125
|
+
vertexShader: SURFACE_VERT,
|
|
126
|
+
fragmentShader: CLOUD_FRAG,
|
|
127
|
+
uniforms: { cloudMap: uniform.clouds, sunDir: sunEarth, shift: cloudShift, opacity: { value: 0.9 } },
|
|
128
|
+
transparent: true,
|
|
129
|
+
depthWrite: false,
|
|
130
|
+
});
|
|
131
|
+
const atmosphereMaterial = new three.ShaderMaterial({
|
|
132
|
+
vertexShader: ATMOSPHERE_VERT,
|
|
133
|
+
fragmentShader: ATMOSPHERE_FRAG,
|
|
134
|
+
uniforms: { sunView, outer: { value: ATMOSPHERE_OUTER }, ortho: { value: 0 } },
|
|
135
|
+
transparent: true,
|
|
136
|
+
depthWrite: false,
|
|
137
|
+
side: three.BackSide,
|
|
138
|
+
});
|
|
139
|
+
const earth = new three.Mesh(grid, earthMaterial);
|
|
140
|
+
const clouds = new three.Mesh(grid, cloudMaterial);
|
|
141
|
+
clouds.scale.setScalar(1.006);
|
|
142
|
+
clouds.renderOrder = 3;
|
|
143
|
+
const glow = new three.Mesh(grid, atmosphereMaterial);
|
|
144
|
+
glow.scale.setScalar(ATMOSPHERE_OUTER);
|
|
145
|
+
glow.renderOrder = 2;
|
|
146
|
+
glow.frustumCulled = false;
|
|
147
|
+
const moonSun = { value: new three.Vector3(1, 0, 0) };
|
|
148
|
+
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 } } }));
|
|
149
|
+
const painter = deps.painter ?? plainPainter;
|
|
150
|
+
const scene = new three.Scene();
|
|
151
|
+
scene.add(earth, clouds, glow, moon);
|
|
152
|
+
const sunDrawing = painter.make(three, "Sun");
|
|
153
|
+
scene.add(sunDrawing.object);
|
|
154
|
+
const planets = new Map();
|
|
155
|
+
for (const name of PLANETS) {
|
|
156
|
+
const drawing = painter.make(three, name);
|
|
157
|
+
const positions = new Float32Array(96 * 3);
|
|
158
|
+
const lineGeometry = new three.BufferGeometry();
|
|
159
|
+
lineGeometry.setAttribute("position", new three.BufferAttribute(positions, 3));
|
|
160
|
+
const orbit = new three.LineLoop(lineGeometry, new three.LineBasicMaterial({ color: ORBIT_COLORS[name], transparent: true, opacity: 0.7 }));
|
|
161
|
+
orbit.frustumCulled = false;
|
|
162
|
+
scene.add(drawing.object, orbit);
|
|
163
|
+
planets.set(name, { drawing, orbit, positions, samples: [], epoch: Number.NaN });
|
|
164
|
+
}
|
|
165
|
+
const camera = new three.PerspectiveCamera(FOV, width / Math.max(1, height), 0.05, 1000);
|
|
166
|
+
const target = new three.Vector3();
|
|
167
|
+
const projected = new three.Vector3();
|
|
168
|
+
const labelFor = (v) => {
|
|
169
|
+
projected.set(v[0], v[1], v[2]).project(camera);
|
|
170
|
+
if (projected.z > 1 || projected.z < -1)
|
|
171
|
+
return null;
|
|
172
|
+
return { x: ((projected.x + 1) / 2) * size.width, y: ((1 - projected.y) / 2) * size.height };
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
scene,
|
|
176
|
+
camera,
|
|
177
|
+
update() {
|
|
178
|
+
const frame = track.at(model.time);
|
|
179
|
+
model.lookups = track.lookups;
|
|
180
|
+
if (model.stepMs !== trackStep) {
|
|
181
|
+
trackStep = model.stepMs;
|
|
182
|
+
track.setStep(model.stepMs);
|
|
183
|
+
}
|
|
184
|
+
const mode = model.mode;
|
|
185
|
+
const sunPos = placeSun(mode, frame.sun);
|
|
186
|
+
const moonPos = placeMoon(mode, frame.moon);
|
|
187
|
+
const helioOf = (planet) => [planet[0] - frame.sun[0], planet[1] - frame.sun[1], planet[2] - frame.sun[2]];
|
|
188
|
+
// the Earth, turned by the sidereal time; light from the Sun
|
|
189
|
+
earth.rotation.set(0, (frame.spin + 90) * RAD, 0);
|
|
190
|
+
clouds.rotation.copy(earth.rotation);
|
|
191
|
+
const toSun = unit(frame.sun);
|
|
192
|
+
sunEarth.value.set(toSun[0], toSun[1], toSun[2]);
|
|
193
|
+
cloudShift.value = -(((model.time / 3_600_000) * 1.5) % 360) / 360;
|
|
194
|
+
earthMaterial.uniforms["lights"].value = 1;
|
|
195
|
+
// the Moon, with its near side to the Earth
|
|
196
|
+
moon.position.set(moonPos[0], moonPos[1], moonPos[2]);
|
|
197
|
+
moon.scale.setScalar(RADIUS["Moon"][mode]);
|
|
198
|
+
moon.up.set(0, 1, 0);
|
|
199
|
+
moon.lookAt(0, 0, 0);
|
|
200
|
+
const moonToSun = unit([frame.sun[0] - frame.moon[0], frame.sun[1] - frame.moon[1], frame.sun[2] - frame.moon[2]]);
|
|
201
|
+
moonSun.value.set(moonToSun[0], moonToSun[1], moonToSun[2]);
|
|
202
|
+
// where everything is, and how big
|
|
203
|
+
const pos = { Earth: [0, 0, 0], Moon: moonPos, Sun: sunPos };
|
|
204
|
+
for (const name of PLANETS)
|
|
205
|
+
pos[name] = placeAround(mode, sunPos, helioOf(frame.planets[name]));
|
|
206
|
+
const radiusOf = (name) => (name === "Earth" ? 1 : RADIUS[name][mode]);
|
|
207
|
+
// the Sun and the planets, drawn by the painter, and the planets' orbits round the Sun
|
|
208
|
+
sunDrawing.object.position.set(sunPos[0], sunPos[1], sunPos[2]);
|
|
209
|
+
sunDrawing.object.scale.setScalar(radiusOf("Sun"));
|
|
210
|
+
if (sunDrawing.billboard)
|
|
211
|
+
sunDrawing.object.quaternion.copy(camera.quaternion);
|
|
212
|
+
for (const name of PLANETS) {
|
|
213
|
+
const p = planets.get(name);
|
|
214
|
+
p.drawing.object.position.set(pos[name][0], pos[name][1], pos[name][2]);
|
|
215
|
+
p.drawing.object.scale.setScalar(radiusOf(name));
|
|
216
|
+
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]]));
|
|
217
|
+
p.orbit.visible = model.orbits;
|
|
218
|
+
if (model.orbits) {
|
|
219
|
+
if (!Number.isFinite(p.epoch) || Math.abs(model.time - p.epoch) > 365 * 86_400_000) {
|
|
220
|
+
p.samples = orbitPath(name, model.time, 96).slice(0, 96);
|
|
221
|
+
p.epoch = model.time;
|
|
222
|
+
}
|
|
223
|
+
for (let i = 0; i < 96; i += 1) {
|
|
224
|
+
const o = placeAround(mode, sunPos, p.samples[i]);
|
|
225
|
+
p.positions[i * 3] = o[0];
|
|
226
|
+
p.positions[i * 3 + 1] = o[1];
|
|
227
|
+
p.positions[i * 3 + 2] = o[2];
|
|
228
|
+
}
|
|
229
|
+
p.orbit.geometry.getAttribute("position").needsUpdate = true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// 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
|
|
233
|
+
const focus = model.focus;
|
|
234
|
+
const focusRadius = radiusOf(focus);
|
|
235
|
+
const spin = model.cameraSpin ?? frame.spin;
|
|
236
|
+
const lon = model.centre.lon - model.view.yaw;
|
|
237
|
+
const lat = Math.max(-90, Math.min(90, model.centre.lat + model.view.tilt));
|
|
238
|
+
const aim = direction(spin + lon, lat);
|
|
239
|
+
const distance = Math.max(1.05 * focusRadius, (FIT_DISTANCE * focusRadius) / Math.max(1e-9, model.view.zoom));
|
|
240
|
+
target.set(pos[focus][0], pos[focus][1], pos[focus][2]);
|
|
241
|
+
camera.position.set(target.x + aim[0] * distance, target.y + aim[1] * distance, target.z + aim[2] * distance);
|
|
242
|
+
camera.up.set(0, 1, 0);
|
|
243
|
+
camera.lookAt(target);
|
|
244
|
+
camera.near = Math.max(0.0005, distance * 0.02 - focusRadius * 0.5, focusRadius * 0.01);
|
|
245
|
+
camera.far = Math.max(2000, distance * 4 + (mode === "true" ? 40 * 23_481 : 400));
|
|
246
|
+
camera.aspect = size.width / Math.max(1, size.height);
|
|
247
|
+
camera.updateProjectionMatrix();
|
|
248
|
+
camera.updateMatrixWorld();
|
|
249
|
+
sunView.value.copy(sunEarth.value).transformDirection(camera.matrixWorldInverse);
|
|
250
|
+
// how many pixels across each body is: for the pictures the Earth and the Moon need, and for what is chosen
|
|
251
|
+
const half = Math.tan((FOV / 2) * RAD);
|
|
252
|
+
const radiusPxOf = (name) => {
|
|
253
|
+
const d = Math.hypot(pos[name][0] - camera.position.x, pos[name][1] - camera.position.y, pos[name][2] - camera.position.z);
|
|
254
|
+
const r = radiusOf(name);
|
|
255
|
+
return d <= r * 1.001 ? 1e5 : ((size.height / 2) * (r / Math.sqrt(d * d - r * r))) / half;
|
|
256
|
+
};
|
|
257
|
+
const px = {};
|
|
258
|
+
for (const name of BODY_NAMES)
|
|
259
|
+
px[name] = radiusPxOf(name);
|
|
260
|
+
model.drawn = { frame, distance, radiusPx: px.Earth };
|
|
261
|
+
const ratio = model.ratio();
|
|
262
|
+
const earthPx = Math.min(px.Earth, 1e5) * ratio;
|
|
263
|
+
textures.sync({ day: earthPx, night: earthPx, clouds: earthPx, water: earthPx, moon: Math.min(px.Moon, 1e5) * ratio });
|
|
264
|
+
if (deps.onLabels && model.labels) {
|
|
265
|
+
const places = {};
|
|
266
|
+
for (const name of BODY_NAMES) {
|
|
267
|
+
const at = labelFor(pos[name]);
|
|
268
|
+
places[name] = at ? { x: at.x, y: at.y, r: px[name] } : null;
|
|
269
|
+
}
|
|
270
|
+
deps.onLabels(places);
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
},
|
|
274
|
+
resize(w, h) {
|
|
275
|
+
size = { width: w, height: h };
|
|
276
|
+
},
|
|
277
|
+
dispose() {
|
|
278
|
+
textures.dispose();
|
|
279
|
+
sunDrawing.dispose();
|
|
280
|
+
for (const p of planets.values()) {
|
|
281
|
+
p.drawing.dispose();
|
|
282
|
+
p.orbit.geometry.dispose();
|
|
283
|
+
p.orbit.material.dispose();
|
|
284
|
+
}
|
|
285
|
+
grid.dispose();
|
|
286
|
+
solid.dispose();
|
|
287
|
+
earthMaterial.dispose();
|
|
288
|
+
cloudMaterial.dispose();
|
|
289
|
+
atmosphereMaterial.dispose();
|
|
290
|
+
moon.material.dispose();
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
package/dist/styles.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lines and fills of one look: colours and widths only, never the geometry.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { STYLES, type Style } from "@johnmorrisdotca/chikyu-earth";
|
|
7
|
+
*
|
|
8
|
+
* const thick: Style = STYLES.thick;
|
|
9
|
+
* console.log(thick.outline, thick.line);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
1
12
|
export interface Style {
|
|
2
13
|
/** The colour of the outlines and the rim. */
|
|
3
14
|
line: string;
|
|
@@ -12,6 +23,16 @@ export interface Style {
|
|
|
12
23
|
/** Graticule width, in SVG units. */
|
|
13
24
|
graticule: number;
|
|
14
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The looks the globe is drawn in, by name: `line` (thin outlines) and `thick`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { STYLES } from "@johnmorrisdotca/chikyu-earth";
|
|
32
|
+
*
|
|
33
|
+
* console.log(Object.keys(STYLES), STYLES.line.ocean);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
15
36
|
export declare const STYLES: {
|
|
16
37
|
readonly line: {
|
|
17
38
|
readonly line: "#8fa1b8";
|
|
@@ -30,9 +51,51 @@ export declare const STYLES: {
|
|
|
30
51
|
readonly graticule: 0.5;
|
|
31
52
|
};
|
|
32
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* The name of a look: `"line"` or `"thick"`.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { globe, type StyleName } from "@johnmorrisdotca/chikyu-earth";
|
|
60
|
+
*
|
|
61
|
+
* const look: StyleName = "thick";
|
|
62
|
+
* console.log(globe({ style: look, size: 100 }).includes("stroke-width"));
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
33
65
|
export type StyleName = keyof typeof STYLES;
|
|
34
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The style a drawing is in when none is named.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { DEFAULT_STYLE } from "@johnmorrisdotca/chikyu-earth";
|
|
72
|
+
*
|
|
73
|
+
* console.log(DEFAULT_STYLE);
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
35
76
|
export declare const DEFAULT_STYLE: StyleName;
|
|
77
|
+
/**
|
|
78
|
+
* Every style's name, in the order they are listed.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { STYLE_NAMES } from "@johnmorrisdotca/chikyu-earth";
|
|
83
|
+
*
|
|
84
|
+
* console.log(STYLE_NAMES.join(", "));
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
36
87
|
export declare const STYLE_NAMES: StyleName[];
|
|
37
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* A value from outside (an attribute, a query parameter) as a style's name: the default for anything else.
|
|
90
|
+
*
|
|
91
|
+
* @param value - Anything.
|
|
92
|
+
* @returns A valid style name; {@link DEFAULT_STYLE} when `value` names none.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { cleanStyle } from "@johnmorrisdotca/chikyu-earth";
|
|
97
|
+
*
|
|
98
|
+
* console.log(cleanStyle("thick"), cleanStyle("neon"), cleanStyle(null));
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
38
101
|
export declare const cleanStyle: (value: unknown) => StyleName;
|
package/dist/styles.js
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
// The two looks the globe is drawn in, the same line colours as hako-devices and uchusen-rockets: one table entry each,
|
|
2
2
|
// a look changes lines and fills, never the geometry. Pure.
|
|
3
|
+
/**
|
|
4
|
+
* The looks the globe is drawn in, by name: `line` (thin outlines) and `thick`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { STYLES } from "@johnmorrisdotca/chikyu-earth";
|
|
9
|
+
*
|
|
10
|
+
* console.log(Object.keys(STYLES), STYLES.line.ocean);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
3
13
|
export const STYLES = {
|
|
4
14
|
line: { line: "#8fa1b8", ocean: "#0b1424", land: "#13203a", outline: 0.5, horizon: 1, graticule: 0.3 },
|
|
5
15
|
thick: { line: "#b4c2d4", ocean: "#0b1424", land: "#1a2b47", outline: 1.2, horizon: 2, graticule: 0.5 },
|
|
6
16
|
};
|
|
7
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* The style a drawing is in when none is named.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { DEFAULT_STYLE } from "@johnmorrisdotca/chikyu-earth";
|
|
23
|
+
*
|
|
24
|
+
* console.log(DEFAULT_STYLE);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
8
27
|
export const DEFAULT_STYLE = "line";
|
|
28
|
+
/**
|
|
29
|
+
* Every style's name, in the order they are listed.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { STYLE_NAMES } from "@johnmorrisdotca/chikyu-earth";
|
|
34
|
+
*
|
|
35
|
+
* console.log(STYLE_NAMES.join(", "));
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
9
38
|
export const STYLE_NAMES = Object.keys(STYLES);
|
|
10
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* A value from outside (an attribute, a query parameter) as a style's name: the default for anything else.
|
|
41
|
+
*
|
|
42
|
+
* @param value - Anything.
|
|
43
|
+
* @returns A valid style name; {@link DEFAULT_STYLE} when `value` names none.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { cleanStyle } from "@johnmorrisdotca/chikyu-earth";
|
|
48
|
+
*
|
|
49
|
+
* console.log(cleanStyle("thick"), cleanStyle("neon"), cleanStyle(null));
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
11
52
|
export const cleanStyle = (value) => typeof value === "string" && Object.hasOwn(STYLES, value) ? value : DEFAULT_STYLE;
|
|
Binary file
|
|
Binary file
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { StyleName } from "./styles.ts";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* What one country's outline carries: its English and Japanese name and its ISO alpha-2 code, all from kuni.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { countryList, type CountryProps } from "@johnmorrisdotca/chikyu-earth";
|
|
8
|
+
*
|
|
9
|
+
* const first: CountryProps | undefined = countryList()[0];
|
|
10
|
+
* console.log(first?.name, first?.alpha2);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
3
13
|
export interface CountryProps {
|
|
4
14
|
/** English name, from kuni (Natural Earth's own name where kuni has no entry). */
|
|
5
15
|
name: string;
|
|
@@ -8,7 +18,17 @@ export interface CountryProps {
|
|
|
8
18
|
/** ISO 3166-1 alpha-2 code, from kuni, or null where there is no code. */
|
|
9
19
|
alpha2: string | null;
|
|
10
20
|
}
|
|
11
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* What a shared view controller tells the globe: its turn (`yaw`, degrees), tip (`tilt`, degrees), zoom (1 is Fit) and pan (`panX` and `panY`, CSS pixels).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { HOME_VIEW, type GlobeView } from "@johnmorrisdotca/chikyu-earth";
|
|
27
|
+
*
|
|
28
|
+
* const turned: GlobeView = { ...HOME_VIEW, yaw: 30 };
|
|
29
|
+
* console.log(turned);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
12
32
|
export interface GlobeView {
|
|
13
33
|
yaw: number;
|
|
14
34
|
tilt: number;
|
|
@@ -16,14 +36,44 @@ export interface GlobeView {
|
|
|
16
36
|
panX: number;
|
|
17
37
|
panY: number;
|
|
18
38
|
}
|
|
19
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* A point on the globe, in degrees: `lon` from -180 to 180 (east is positive), `lat` from -90 to 90 (north is positive).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import type { GeoPoint } from "@johnmorrisdotca/chikyu-earth";
|
|
45
|
+
*
|
|
46
|
+
* const tokyo: GeoPoint = { lon: 139.7, lat: 35.7 };
|
|
47
|
+
* console.log(tokyo);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
20
50
|
export interface GeoPoint {
|
|
21
51
|
lon: number;
|
|
22
52
|
lat: number;
|
|
23
53
|
}
|
|
24
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* How the globe is turned: [lambda, phi] in degrees, the pair d3-geo's `rotate` takes.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { globe, type Rotation } from "@johnmorrisdotca/chikyu-earth";
|
|
60
|
+
*
|
|
61
|
+
* const rotation: Rotation = [-10, -30];
|
|
62
|
+
* console.log(globe({ rotation, size: 100 }).length > 1000);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
25
65
|
export type Rotation = [lambda: number, phi: number];
|
|
26
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* What `globe()` may be asked.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { globe, type GlobeOptions } from "@johnmorrisdotca/chikyu-earth";
|
|
72
|
+
*
|
|
73
|
+
* const options: GlobeOptions = { size: 200, style: "thick", graticule: true, centre: { lon: 10, lat: 30 } };
|
|
74
|
+
* console.log(globe(options).startsWith("<svg"));
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
27
77
|
export interface GlobeOptions {
|
|
28
78
|
/** Width and height of the square SVG, in pixels. Default 320. */
|
|
29
79
|
size?: number;
|