@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
package/dist/rotate.d.ts
CHANGED
|
@@ -1,14 +1,69 @@
|
|
|
1
1
|
import type { GeoPoint, Rotation } from "./types.ts";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* One sidereal day — the Earth's turn against the stars, 23 h 56 min 4.0905 s — in milliseconds.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
8
|
+
*
|
|
9
|
+
* console.log(SIDEREAL_DAY_MS / 3_600_000, "hours");
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
3
12
|
export declare const SIDEREAL_DAY_MS = 86164090.5;
|
|
4
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* How far the Earth turns in one millisecond, in degrees.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { DEGREES_PER_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
19
|
+
*
|
|
20
|
+
* console.log(DEGREES_PER_MS * 1000, "degrees a second");
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
5
23
|
export declare const DEGREES_PER_MS: number;
|
|
6
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* How far the Earth has turned after `ms` milliseconds, in degrees.
|
|
26
|
+
*
|
|
27
|
+
* @param ms - A time span in milliseconds.
|
|
28
|
+
* @returns The turn in degrees; a whole {@link SIDEREAL_DAY_MS} is 360.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { SIDEREAL_DAY_MS, spinAt } from "@johnmorrisdotca/chikyu-earth";
|
|
33
|
+
*
|
|
34
|
+
* console.log(spinAt(SIDEREAL_DAY_MS / 4));
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
7
37
|
export declare const spinAt: (ms: number) => number;
|
|
8
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* The rotation that puts `centre` in the middle of the disc: d3-geo's rotate is the negative of the point.
|
|
40
|
+
*
|
|
41
|
+
* @param centre - The place to centre on.
|
|
42
|
+
* @returns The `[lambda, phi]` pair for `globe()`'s `rotation` option.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { centreRotation } from "@johnmorrisdotca/chikyu-earth";
|
|
47
|
+
*
|
|
48
|
+
* console.log(centreRotation({ lon: 139.7, lat: 35.7 }));
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
9
51
|
export declare const centreRotation: (centre: GeoPoint) => Rotation;
|
|
10
52
|
/**
|
|
11
53
|
* The rotation at `now` for a globe that was centred on `centre` at `start`. The tilt stays; the spin advances at the
|
|
12
54
|
* Earth's own rate, so the disc turns as the real Earth does and the centred place drifts with it.
|
|
55
|
+
*
|
|
56
|
+
* @param centre - The place the globe was centred on at `start`.
|
|
57
|
+
* @param now - The time to draw for, in milliseconds.
|
|
58
|
+
* @param start - The time the globe was centred, in the same milliseconds as `now`.
|
|
59
|
+
* @returns The `[lambda, phi]` rotation at `now`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { rotationAt, SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
64
|
+
*
|
|
65
|
+
* const tokyo = { lon: 139.7, lat: 35.7 };
|
|
66
|
+
* console.log(rotationAt(tokyo, SIDEREAL_DAY_MS / 2, 0));
|
|
67
|
+
* ```
|
|
13
68
|
*/
|
|
14
69
|
export declare function rotationAt(centre: GeoPoint, now: number, start: number): Rotation;
|
package/dist/rotate.js
CHANGED
|
@@ -1,14 +1,69 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* One sidereal day — the Earth's turn against the stars, 23 h 56 min 4.0905 s — in milliseconds.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
7
|
+
*
|
|
8
|
+
* console.log(SIDEREAL_DAY_MS / 3_600_000, "hours");
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
2
11
|
export const SIDEREAL_DAY_MS = 86_164_090.5;
|
|
3
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* How far the Earth turns in one millisecond, in degrees.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { DEGREES_PER_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
18
|
+
*
|
|
19
|
+
* console.log(DEGREES_PER_MS * 1000, "degrees a second");
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
4
22
|
export const DEGREES_PER_MS = 360 / SIDEREAL_DAY_MS;
|
|
5
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* How far the Earth has turned after `ms` milliseconds, in degrees.
|
|
25
|
+
*
|
|
26
|
+
* @param ms - A time span in milliseconds.
|
|
27
|
+
* @returns The turn in degrees; a whole {@link SIDEREAL_DAY_MS} is 360.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { SIDEREAL_DAY_MS, spinAt } from "@johnmorrisdotca/chikyu-earth";
|
|
32
|
+
*
|
|
33
|
+
* console.log(spinAt(SIDEREAL_DAY_MS / 4));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
6
36
|
export const spinAt = (ms) => ms * DEGREES_PER_MS;
|
|
7
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The rotation that puts `centre` in the middle of the disc: d3-geo's rotate is the negative of the point.
|
|
39
|
+
*
|
|
40
|
+
* @param centre - The place to centre on.
|
|
41
|
+
* @returns The `[lambda, phi]` pair for `globe()`'s `rotation` option.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { centreRotation } from "@johnmorrisdotca/chikyu-earth";
|
|
46
|
+
*
|
|
47
|
+
* console.log(centreRotation({ lon: 139.7, lat: 35.7 }));
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
8
50
|
export const centreRotation = (centre) => [-centre.lon, -centre.lat];
|
|
9
51
|
/**
|
|
10
52
|
* The rotation at `now` for a globe that was centred on `centre` at `start`. The tilt stays; the spin advances at the
|
|
11
53
|
* Earth's own rate, so the disc turns as the real Earth does and the centred place drifts with it.
|
|
54
|
+
*
|
|
55
|
+
* @param centre - The place the globe was centred on at `start`.
|
|
56
|
+
* @param now - The time to draw for, in milliseconds.
|
|
57
|
+
* @param start - The time the globe was centred, in the same milliseconds as `now`.
|
|
58
|
+
* @returns The `[lambda, phi]` rotation at `now`.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { rotationAt, SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
63
|
+
*
|
|
64
|
+
* const tokyo = { lon: 139.7, lat: 35.7 };
|
|
65
|
+
* console.log(rotationAt(tokyo, SIDEREAL_DAY_MS / 2, 0));
|
|
66
|
+
* ```
|
|
12
67
|
*/
|
|
13
68
|
export function rotationAt(centre, now, start) {
|
|
14
69
|
const [lambda, phi] = centreRotation(centre);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { GeoPoint } from "../types.ts";
|
|
2
|
+
import { setThreeLoader } from "../realistic/shared.ts";
|
|
3
|
+
import { type ScaleMode } from "./scale.ts";
|
|
4
|
+
import { BODY_NAMES, type BodyName, type BodyPainter } from "./scene.ts";
|
|
5
|
+
export { BODY_NAMES, type BodyName };
|
|
6
|
+
/**
|
|
7
|
+
* Change how the Sun and the planets are drawn (before the first element connects): the space view keeps their places, sizes, light, names and orbits and asks
|
|
8
|
+
* the painter only for what to draw at each place (see `BodyPainter`). Left alone, they are plain spheres in the right colours.
|
|
9
|
+
*
|
|
10
|
+
* @param next - The painter to use, or `undefined` for the plain one.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts no-run
|
|
14
|
+
* import { plainPainter } from "@johnmorrisdotca/chikyu-earth/space";
|
|
15
|
+
* import { setBodyPainter } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
16
|
+
*
|
|
17
|
+
* setBodyPainter({ make: (three, name) => plainPainter.make(three, name) });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function setBodyPainter(next: BodyPainter | undefined): void;
|
|
21
|
+
/**
|
|
22
|
+
* What a page can tell a reader about a body now.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts no-run
|
|
26
|
+
* import type { BodyInfo } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
27
|
+
*
|
|
28
|
+
* const info: BodyInfo = { name: "Mars", radiusKm: 3389.5, distanceKm: 225_000_000 };
|
|
29
|
+
* console.log(`${info.name}: ${info.radiusKm} km`);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export interface BodyInfo {
|
|
33
|
+
name: BodyName;
|
|
34
|
+
/** Mean radius, kilometres. */
|
|
35
|
+
radiusKm: number;
|
|
36
|
+
/** Distance from the Earth's centre now, kilometres (0 for the Earth). */
|
|
37
|
+
distanceKm: number;
|
|
38
|
+
}
|
|
39
|
+
export { setThreeLoader };
|
|
40
|
+
/**
|
|
41
|
+
* What the space view is doing: `loading`, `live`, `unavailable` (no WebGL), `lost` (the GPU took the context; it comes back by itself) or `idle` (another view has the page's one context).
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts no-run
|
|
45
|
+
* import type { ChikyuSpace, SpaceState } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
46
|
+
*
|
|
47
|
+
* const stateOf = (space: ChikyuSpace): SpaceState => space.stats.state;
|
|
48
|
+
* console.log(typeof stateOf);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export type SpaceState = "loading" | "live" | "unavailable" | "lost" | "idle";
|
|
52
|
+
/**
|
|
53
|
+
* A named distance for `zoomFor`: the Earth, the Moon, the Sun, the inner planets or the whole solar system.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts no-run
|
|
57
|
+
* import type { ChikyuSpace, SpacePreset } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
58
|
+
*
|
|
59
|
+
* const zoomTo = (space: ChikyuSpace, preset: SpacePreset): number => space.zoomFor(preset);
|
|
60
|
+
* console.log(typeof zoomTo);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export type SpacePreset = "earth" | "moon" | "sun" | "inner" | "solar-system";
|
|
64
|
+
/**
|
|
65
|
+
* The `<chikyu-space>` custom element: the Earth, the Moon, the Sun and the planets where they are at a date and time, drawn with three.js through the shared renderer. It takes `center`, `time` (`now` or an ISO date), `speed` (1 is real time, 0 holds, 86400 is a day a second, negative runs backwards), `scale` (`compressed` or `true`), `labels` and `orbits` (`off` hides them); positions are computed in the browser by astronomy-engine, an optional peer dependency. It draws only when something changes, and nothing while it is out of sight.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts no-run
|
|
69
|
+
* import type { ChikyuSpace } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
70
|
+
*
|
|
71
|
+
* const space = document.createElement("chikyu-space") as ChikyuSpace;
|
|
72
|
+
* space.setAttribute("time", "2026-11-02T12:00Z");
|
|
73
|
+
* space.setAttribute("speed", "0");
|
|
74
|
+
* document.body.append(space);
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare class ChikyuSpace extends HTMLElement {
|
|
78
|
+
#private;
|
|
79
|
+
static observedAttributes: string[];
|
|
80
|
+
connectedCallback(): void;
|
|
81
|
+
disconnectedCallback(): void;
|
|
82
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
83
|
+
get centre(): GeoPoint;
|
|
84
|
+
set centre(value: GeoPoint);
|
|
85
|
+
/** The moment shown: milliseconds since the epoch, or "now" to follow the clock. Setting it points the camera at the same place on the ground again. */
|
|
86
|
+
get time(): number;
|
|
87
|
+
set time(value: number | Date | "now");
|
|
88
|
+
/** Seconds of game time per second: 1 is real time, 0 holds still, negative runs backwards. */
|
|
89
|
+
get speed(): number;
|
|
90
|
+
set speed(value: number);
|
|
91
|
+
get scale(): ScaleMode;
|
|
92
|
+
set scale(value: ScaleMode);
|
|
93
|
+
get labels(): boolean;
|
|
94
|
+
set labels(value: boolean);
|
|
95
|
+
get orbits(): boolean;
|
|
96
|
+
set orbits(value: boolean);
|
|
97
|
+
/** The shared controls' camera: yaw and tilt turn the view, zoom brings the camera in (1 shows the Earth, less shows the sky round it). Pan is not used. */
|
|
98
|
+
setView(view: {
|
|
99
|
+
yaw: number;
|
|
100
|
+
tilt: number;
|
|
101
|
+
zoom: number;
|
|
102
|
+
}): void;
|
|
103
|
+
/** The body the camera looks at: at zoom 1 it fills the view; the yaw and tilt turn the camera round it. */
|
|
104
|
+
get body(): BodyName;
|
|
105
|
+
set body(name: BodyName);
|
|
106
|
+
/** The radius and the distance from the Earth of a body now, for a page's own caption. */
|
|
107
|
+
bodyInfo(name: BodyName): BodyInfo;
|
|
108
|
+
/** The zoom that shows a part of the sky: the Earth alone, the Moon's orbit, the Sun, the planets out to Mars, or all of them, at this scale and moment. */
|
|
109
|
+
zoomFor(preset: SpacePreset): number;
|
|
110
|
+
/** What the element is doing, for a page or a test. */
|
|
111
|
+
get stats(): {
|
|
112
|
+
state: SpaceState;
|
|
113
|
+
draws: number;
|
|
114
|
+
contexts: number;
|
|
115
|
+
canvas: {
|
|
116
|
+
width: number;
|
|
117
|
+
height: number;
|
|
118
|
+
pixels: number;
|
|
119
|
+
};
|
|
120
|
+
tiers: Record<string, number>;
|
|
121
|
+
distance: number;
|
|
122
|
+
assets: {
|
|
123
|
+
entries: number;
|
|
124
|
+
refs: number;
|
|
125
|
+
};
|
|
126
|
+
time: number;
|
|
127
|
+
lookups: number;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
declare global {
|
|
131
|
+
interface HTMLElementTagNameMap {
|
|
132
|
+
"chikyu-space": ChikyuSpace;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { createGate, frameCap } from "../realistic/gate.js";
|
|
2
|
+
import { setThreeLoader, takeRenderer } from "../realistic/shared.js";
|
|
3
|
+
import { MAX_TEXTURE } from "../realistic/tiers.js";
|
|
4
|
+
import { AU_IN_EARTH_RADII, EARTH_RADIUS_KM, frameAt } from "./ephemeris.js";
|
|
5
|
+
import { placeAround, placeMoon, placeSun } from "./scale.js";
|
|
6
|
+
import { BODY_NAMES, createSpaceItem, FIT_DISTANCE, newSpaceModel } from "./scene.js";
|
|
7
|
+
export { BODY_NAMES };
|
|
8
|
+
let painter;
|
|
9
|
+
/**
|
|
10
|
+
* Change how the Sun and the planets are drawn (before the first element connects): the space view keeps their places, sizes, light, names and orbits and asks
|
|
11
|
+
* the painter only for what to draw at each place (see `BodyPainter`). Left alone, they are plain spheres in the right colours.
|
|
12
|
+
*
|
|
13
|
+
* @param next - The painter to use, or `undefined` for the plain one.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts no-run
|
|
17
|
+
* import { plainPainter } from "@johnmorrisdotca/chikyu-earth/space";
|
|
18
|
+
* import { setBodyPainter } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
19
|
+
*
|
|
20
|
+
* setBodyPainter({ make: (three, name) => plainPainter.make(three, name) });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function setBodyPainter(next) {
|
|
24
|
+
painter = next;
|
|
25
|
+
}
|
|
26
|
+
/** Mean radii in kilometres (IAU), for what a click tells. */
|
|
27
|
+
const RADIUS_KM = { Sun: 695_700, Mercury: 2439.7, Venus: 6051.8, Earth: 6371, Moon: 1737.4, Mars: 3389.5, Jupiter: 69_911, Saturn: 58_232, Uranus: 25_362, Neptune: 24_622 };
|
|
28
|
+
export { setThreeLoader };
|
|
29
|
+
const RAD = Math.PI / 180;
|
|
30
|
+
/** The Earth's own turn, degrees a second. */
|
|
31
|
+
const EARTH_RATE = 360 / 86_164.0905;
|
|
32
|
+
const REAL_TIME_TICK_MS = 60_000;
|
|
33
|
+
const readCentre = (value) => {
|
|
34
|
+
if (!value)
|
|
35
|
+
return null;
|
|
36
|
+
const [lon, lat] = value.split(",").map((part) => Number(part.trim()));
|
|
37
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat) || Math.abs(lon) > 180 || Math.abs(lat) > 90)
|
|
38
|
+
return null;
|
|
39
|
+
return { lon: lon, lat: lat };
|
|
40
|
+
};
|
|
41
|
+
const readTime = (value) => {
|
|
42
|
+
if (value === null || value === "" || value === "now")
|
|
43
|
+
return "now";
|
|
44
|
+
const ms = Date.parse(value);
|
|
45
|
+
return Number.isFinite(ms) ? ms : "now";
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The `<chikyu-space>` custom element: the Earth, the Moon, the Sun and the planets where they are at a date and time, drawn with three.js through the shared renderer. It takes `center`, `time` (`now` or an ISO date), `speed` (1 is real time, 0 holds, 86400 is a day a second, negative runs backwards), `scale` (`compressed` or `true`), `labels` and `orbits` (`off` hides them); positions are computed in the browser by astronomy-engine, an optional peer dependency. It draws only when something changes, and nothing while it is out of sight.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts no-run
|
|
52
|
+
* import type { ChikyuSpace } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
53
|
+
*
|
|
54
|
+
* const space = document.createElement("chikyu-space") as ChikyuSpace;
|
|
55
|
+
* space.setAttribute("time", "2026-11-02T12:00Z");
|
|
56
|
+
* space.setAttribute("speed", "0");
|
|
57
|
+
* document.body.append(space);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export class ChikyuSpace extends HTMLElement {
|
|
61
|
+
static observedAttributes = ["center", "time", "speed", "scale", "labels", "orbits"];
|
|
62
|
+
#model = newSpaceModel();
|
|
63
|
+
#mode = "now";
|
|
64
|
+
#speed = 1;
|
|
65
|
+
#view;
|
|
66
|
+
#gate;
|
|
67
|
+
#taken;
|
|
68
|
+
#stage;
|
|
69
|
+
#labelBox;
|
|
70
|
+
#state = "loading";
|
|
71
|
+
#timer;
|
|
72
|
+
#last = 0;
|
|
73
|
+
#pool = new Map();
|
|
74
|
+
#places;
|
|
75
|
+
#tiers = {};
|
|
76
|
+
connectedCallback() {
|
|
77
|
+
this.#model.centre = readCentre(this.getAttribute("center")) ?? this.#model.centre;
|
|
78
|
+
this.#mode = readTime(this.getAttribute("time"));
|
|
79
|
+
this.#model.time = this.#mode === "now" ? Date.now() : this.#mode;
|
|
80
|
+
this.#speed = this.#readSpeed();
|
|
81
|
+
this.#model.mode = this.getAttribute("scale") === "true" ? "true" : "compressed";
|
|
82
|
+
this.#model.labels = this.getAttribute("labels") !== "off";
|
|
83
|
+
this.#model.orbits = this.getAttribute("orbits") !== "off";
|
|
84
|
+
this.#model.stepMs = Math.max(1000, Math.abs(this.#speed) * 1000);
|
|
85
|
+
if (!this.hasAttribute("role"))
|
|
86
|
+
this.setAttribute("role", "img");
|
|
87
|
+
if (!this.hasAttribute("aria-label"))
|
|
88
|
+
this.setAttribute("aria-label", "The Earth, the Moon, the Sun and the planets in their places for the date shown");
|
|
89
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
90
|
+
root.innerHTML = `<style>:host{display:block;aspect-ratio:1/1;position:relative;overflow:hidden}#stage,#labels{position:absolute;inset:0}#labels{pointer-events:none;overflow:hidden}.sp-label{position:absolute;left:0;top:0;margin:0;padding:0;border:0;background:none;font:12px/16px system-ui,-apple-system,sans-serif;color:#fff;white-space:nowrap;text-shadow:0 0 3px #000,0 0 3px #000;will-change:transform;pointer-events:none}.sp-label:focus-visible{outline:2px solid #6cd0ff;outline-offset:2px;border-radius:4px}.sp-label::before{content:"";display:inline-block;width:5px;height:5px;margin:0 5px 1px -2px;border-radius:50%;background:#fff}.sp-label[hidden]{display:none}</style><div id="stage"></div><div id="labels" aria-hidden="true"></div>`;
|
|
91
|
+
this.#stage = root.getElementById("stage");
|
|
92
|
+
this.#labelBox = root.getElementById("labels");
|
|
93
|
+
this.#pool.clear();
|
|
94
|
+
const nav = navigator;
|
|
95
|
+
this.#gate = createGate(1000 / frameCap(30, { deviceMemory: nav.deviceMemory, hardwareConcurrency: nav.hardwareConcurrency }), () => this.#view?.invalidate());
|
|
96
|
+
this.#stage.addEventListener("webglcontextlost", this.#lost, true);
|
|
97
|
+
this.#stage.addEventListener("webglcontextrestored", this.#restored, true);
|
|
98
|
+
const taken = takeRenderer((state) => {
|
|
99
|
+
if (state === "unavailable")
|
|
100
|
+
this.#fail("WebGL is not available");
|
|
101
|
+
});
|
|
102
|
+
this.#taken = taken;
|
|
103
|
+
const item = createSpaceItem(this.#model, {
|
|
104
|
+
assets: taken.renderer.assets,
|
|
105
|
+
textureBase: () => this.#textureBase(),
|
|
106
|
+
cap: () => this.#textureCap(),
|
|
107
|
+
invalidate: () => this.#gate?.request(),
|
|
108
|
+
onError: (error) => this.#fail(error instanceof Error ? error.message : String(error)),
|
|
109
|
+
onTier: (layer, width) => {
|
|
110
|
+
this.#tiers[layer] = width;
|
|
111
|
+
},
|
|
112
|
+
onLabels: (places) => this.#placeLabels(places),
|
|
113
|
+
...(painter ? { painter } : {}),
|
|
114
|
+
});
|
|
115
|
+
this.#view = taken.renderer.view(this.#stage, item, { still: null });
|
|
116
|
+
this.#set("loading");
|
|
117
|
+
void this.#view.focus().then((live) => {
|
|
118
|
+
if (live) {
|
|
119
|
+
this.#set("live");
|
|
120
|
+
this.dispatchEvent(new CustomEvent("chikyu-ready"));
|
|
121
|
+
}
|
|
122
|
+
else if (this.#state === "loading")
|
|
123
|
+
this.#set("unavailable");
|
|
124
|
+
});
|
|
125
|
+
this.#last = performance.now();
|
|
126
|
+
this.#schedule();
|
|
127
|
+
this.addEventListener("pointerdown", this.#down);
|
|
128
|
+
this.#labelBox.addEventListener("click", this.#labelClick);
|
|
129
|
+
this.#labelBox.addEventListener("focusin", this.#labelFocus);
|
|
130
|
+
}
|
|
131
|
+
disconnectedCallback() {
|
|
132
|
+
this.removeEventListener("pointerdown", this.#down);
|
|
133
|
+
window.removeEventListener("pointerup", this.#up, true);
|
|
134
|
+
clearTimeout(this.#timer);
|
|
135
|
+
this.#timer = undefined;
|
|
136
|
+
this.#gate?.cancel();
|
|
137
|
+
this.#view?.dispose();
|
|
138
|
+
this.#view = undefined;
|
|
139
|
+
this.#taken?.release();
|
|
140
|
+
this.#taken = undefined;
|
|
141
|
+
}
|
|
142
|
+
attributeChangedCallback(name, _old, value) {
|
|
143
|
+
if (name === "center")
|
|
144
|
+
this.centre = readCentre(value) ?? { lon: 0, lat: 0 };
|
|
145
|
+
else if (name === "time")
|
|
146
|
+
this.time = readTime(value);
|
|
147
|
+
else if (name === "speed")
|
|
148
|
+
this.speed = this.#readSpeed();
|
|
149
|
+
else if (name === "scale")
|
|
150
|
+
this.scale = value === "true" ? "true" : "compressed";
|
|
151
|
+
else if (name === "labels")
|
|
152
|
+
this.labels = value !== "off";
|
|
153
|
+
else if (name === "orbits")
|
|
154
|
+
this.orbits = value !== "off";
|
|
155
|
+
}
|
|
156
|
+
get centre() {
|
|
157
|
+
return { ...this.#model.centre };
|
|
158
|
+
}
|
|
159
|
+
set centre(value) {
|
|
160
|
+
this.#model.centre = { ...value };
|
|
161
|
+
this.#gate?.request();
|
|
162
|
+
}
|
|
163
|
+
/** The moment shown: milliseconds since the epoch, or "now" to follow the clock. Setting it points the camera at the same place on the ground again. */
|
|
164
|
+
get time() {
|
|
165
|
+
return this.#model.time;
|
|
166
|
+
}
|
|
167
|
+
set time(value) {
|
|
168
|
+
this.#mode = value instanceof Date ? value.getTime() : value;
|
|
169
|
+
this.#model.time = this.#mode === "now" ? Date.now() : this.#mode;
|
|
170
|
+
this.#model.cameraSpin = null;
|
|
171
|
+
this.#last = performance.now();
|
|
172
|
+
this.#schedule();
|
|
173
|
+
this.#gate?.request();
|
|
174
|
+
}
|
|
175
|
+
/** Seconds of game time per second: 1 is real time, 0 holds still, negative runs backwards. */
|
|
176
|
+
get speed() {
|
|
177
|
+
return this.#speed;
|
|
178
|
+
}
|
|
179
|
+
set speed(value) {
|
|
180
|
+
this.#speed = Number.isFinite(value) ? value : 1;
|
|
181
|
+
this.#model.stepMs = Math.max(1000, Math.abs(this.#speed) * 1000);
|
|
182
|
+
this.#last = performance.now();
|
|
183
|
+
this.#schedule();
|
|
184
|
+
this.#gate?.request();
|
|
185
|
+
}
|
|
186
|
+
get scale() {
|
|
187
|
+
return this.#model.mode;
|
|
188
|
+
}
|
|
189
|
+
set scale(value) {
|
|
190
|
+
this.#model.mode = value;
|
|
191
|
+
this.#gate?.request();
|
|
192
|
+
}
|
|
193
|
+
get labels() {
|
|
194
|
+
return this.#model.labels;
|
|
195
|
+
}
|
|
196
|
+
set labels(value) {
|
|
197
|
+
this.#model.labels = value;
|
|
198
|
+
if (!value)
|
|
199
|
+
for (const label of this.#pool.values())
|
|
200
|
+
label.hidden = true;
|
|
201
|
+
this.#gate?.request();
|
|
202
|
+
}
|
|
203
|
+
get orbits() {
|
|
204
|
+
return this.#model.orbits;
|
|
205
|
+
}
|
|
206
|
+
set orbits(value) {
|
|
207
|
+
this.#model.orbits = value;
|
|
208
|
+
this.#gate?.request();
|
|
209
|
+
}
|
|
210
|
+
/** The shared controls' camera: yaw and tilt turn the view, zoom brings the camera in (1 shows the Earth, less shows the sky round it). Pan is not used. */
|
|
211
|
+
setView(view) {
|
|
212
|
+
const now = this.#model.view;
|
|
213
|
+
if (view.yaw === now.yaw && view.tilt === now.tilt && view.zoom === now.zoom)
|
|
214
|
+
return;
|
|
215
|
+
this.#model.view = { yaw: view.yaw, tilt: view.tilt, zoom: view.zoom };
|
|
216
|
+
this.#gate?.request();
|
|
217
|
+
}
|
|
218
|
+
/** The body the camera looks at: at zoom 1 it fills the view; the yaw and tilt turn the camera round it. */
|
|
219
|
+
get body() {
|
|
220
|
+
return this.#model.focus;
|
|
221
|
+
}
|
|
222
|
+
set body(name) {
|
|
223
|
+
if (!BODY_NAMES.includes(name))
|
|
224
|
+
return;
|
|
225
|
+
this.#model.focus = name;
|
|
226
|
+
this.#gate?.request();
|
|
227
|
+
}
|
|
228
|
+
/** The radius and the distance from the Earth of a body now, for a page's own caption. */
|
|
229
|
+
bodyInfo(name) {
|
|
230
|
+
const frame = this.#model.drawn?.frame ?? frameAt(this.#model.time);
|
|
231
|
+
const geo = name === "Earth" ? [0, 0, 0] : name === "Sun" ? frame.sun : name === "Moon" ? frame.moon : frame.planets[name];
|
|
232
|
+
return { name, radiusKm: RADIUS_KM[name], distanceKm: Math.hypot(geo[0], geo[1], geo[2]) * (AU_IN_EARTH_RADII * EARTH_RADIUS_KM) };
|
|
233
|
+
}
|
|
234
|
+
/** The zoom that shows a part of the sky: the Earth alone, the Moon's orbit, the Sun, the planets out to Mars, or all of them, at this scale and moment. */
|
|
235
|
+
zoomFor(preset) {
|
|
236
|
+
const frame = this.#model.drawn?.frame ?? frameAt(this.#model.time);
|
|
237
|
+
const mode = this.#model.mode;
|
|
238
|
+
const sun = placeSun(mode, frame.sun);
|
|
239
|
+
const around = (name) => Math.hypot(...placeAround(mode, sun, [frame.planets[name][0] - frame.sun[0], frame.planets[name][1] - frame.sun[1], frame.planets[name][2] - frame.sun[2]]));
|
|
240
|
+
const reach = {
|
|
241
|
+
earth: 0,
|
|
242
|
+
moon: Math.hypot(...placeMoon(mode, frame.moon)),
|
|
243
|
+
sun: Math.hypot(...sun),
|
|
244
|
+
inner: Math.max(around("Mercury"), around("Venus"), around("Mars"), Math.hypot(...sun)),
|
|
245
|
+
"solar-system": Math.max(...Object.keys(frame.planets).map(around), Math.hypot(...sun)),
|
|
246
|
+
}[preset];
|
|
247
|
+
return preset === "earth" ? 1 : FIT_DISTANCE / Math.max(FIT_DISTANCE, reach * 3.4);
|
|
248
|
+
}
|
|
249
|
+
/** What the element is doing, for a page or a test. */
|
|
250
|
+
get stats() {
|
|
251
|
+
const stats = this.#taken?.renderer.stats;
|
|
252
|
+
return {
|
|
253
|
+
state: this.#state,
|
|
254
|
+
draws: stats?.draws ?? 0,
|
|
255
|
+
contexts: stats?.contexts ?? 0,
|
|
256
|
+
canvas: stats?.canvas ?? { width: 0, height: 0, pixels: 0 },
|
|
257
|
+
tiers: { ...this.#tiers },
|
|
258
|
+
distance: this.#model.drawn?.distance ?? 0,
|
|
259
|
+
assets: { entries: stats?.assets.entries ?? 0, refs: stats?.assets.refs ?? 0 },
|
|
260
|
+
time: this.#model.time,
|
|
261
|
+
lookups: this.#model.lookups,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// --- the game clock ----------------------------------------------------------------------------------------------------------------------------
|
|
265
|
+
#readSpeed() {
|
|
266
|
+
const raw = this.getAttribute("speed");
|
|
267
|
+
const value = raw === null || raw === "" ? 1 : Number(raw);
|
|
268
|
+
return Number.isFinite(value) ? value : 1;
|
|
269
|
+
}
|
|
270
|
+
/** While time runs, draw as often as it shows: real time once a minute; faster time as often as the Earth's turn moves a pixel (1 to 30 times a second). */
|
|
271
|
+
#schedule() {
|
|
272
|
+
clearTimeout(this.#timer);
|
|
273
|
+
this.#timer = undefined;
|
|
274
|
+
if (!this.isConnected || this.#speed === 0)
|
|
275
|
+
return;
|
|
276
|
+
const realTime = this.#speed === 1 && this.#mode === "now";
|
|
277
|
+
const radius = this.#model.drawn?.radiusPx ?? 200;
|
|
278
|
+
const pixelsPerSecond = EARTH_RATE * RAD * Math.abs(this.#speed) * radius;
|
|
279
|
+
const perSecond = Math.min(30, Math.max(1, pixelsPerSecond));
|
|
280
|
+
const wait = realTime || this.#speed === 1 ? REAL_TIME_TICK_MS : 1000 / perSecond;
|
|
281
|
+
this.#timer = setTimeout(this.#tick, wait);
|
|
282
|
+
}
|
|
283
|
+
#tick = () => {
|
|
284
|
+
const now = performance.now();
|
|
285
|
+
if (this.#speed === 1 && this.#mode === "now")
|
|
286
|
+
this.#model.time = Date.now();
|
|
287
|
+
else
|
|
288
|
+
this.#model.time += (now - this.#last) * this.#speed;
|
|
289
|
+
this.#last = now;
|
|
290
|
+
this.#gate?.request();
|
|
291
|
+
this.#schedule();
|
|
292
|
+
this.dispatchEvent(new CustomEvent("chikyu-time", { detail: { time: this.#model.time } }));
|
|
293
|
+
};
|
|
294
|
+
// --- labels --------------------------------------------------------------------------------------------------------------------------------------
|
|
295
|
+
#placeLabels(places) {
|
|
296
|
+
this.#places = places;
|
|
297
|
+
const box = this.#labelBox;
|
|
298
|
+
if (!box)
|
|
299
|
+
return;
|
|
300
|
+
for (const name of BODY_NAMES) {
|
|
301
|
+
let label = this.#pool.get(name);
|
|
302
|
+
const at = places[name];
|
|
303
|
+
if (!label) {
|
|
304
|
+
if (!at)
|
|
305
|
+
continue;
|
|
306
|
+
const button = box.ownerDocument.createElement("button");
|
|
307
|
+
button.type = "button";
|
|
308
|
+
button.className = "sp-label";
|
|
309
|
+
button.dataset["body"] = name;
|
|
310
|
+
button.textContent = name;
|
|
311
|
+
button.setAttribute("aria-label", `${name}: choose to look at it`);
|
|
312
|
+
box.append(button);
|
|
313
|
+
this.#pool.set(name, button);
|
|
314
|
+
label = button;
|
|
315
|
+
}
|
|
316
|
+
if (!at) {
|
|
317
|
+
if (!label.hidden)
|
|
318
|
+
label.hidden = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (label.hidden)
|
|
322
|
+
label.hidden = false;
|
|
323
|
+
label.style.transform = `translate(${Math.round(at.x + Math.max(4, Math.min(at.r, 40)))}px,${Math.round(at.y - 8)}px)`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// A body is chosen by pressing on it or on its name (and by Tab and Enter on the name); a drag that turns the view is not a choice.
|
|
327
|
+
#downX = 0;
|
|
328
|
+
#downY = 0;
|
|
329
|
+
#down = (event) => {
|
|
330
|
+
if (event.button !== 0)
|
|
331
|
+
return;
|
|
332
|
+
this.#downX = event.clientX;
|
|
333
|
+
this.#downY = event.clientY;
|
|
334
|
+
window.addEventListener("pointerup", this.#up, { capture: true, once: true });
|
|
335
|
+
};
|
|
336
|
+
#up = (event) => {
|
|
337
|
+
if (Math.hypot(event.clientX - this.#downX, event.clientY - this.#downY) > 6 || !this.#places)
|
|
338
|
+
return;
|
|
339
|
+
const rect = this.getBoundingClientRect();
|
|
340
|
+
const x = event.clientX - rect.left;
|
|
341
|
+
const y = event.clientY - rect.top;
|
|
342
|
+
let best;
|
|
343
|
+
let bestD = Infinity;
|
|
344
|
+
for (const name of BODY_NAMES) {
|
|
345
|
+
const at = this.#places[name];
|
|
346
|
+
if (!at)
|
|
347
|
+
continue;
|
|
348
|
+
const d = Math.hypot(x - at.x, y - at.y);
|
|
349
|
+
if (d <= Math.max(at.r, 14) && d < bestD) {
|
|
350
|
+
best = name;
|
|
351
|
+
bestD = d;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (best)
|
|
355
|
+
this.dispatchEvent(new CustomEvent("chikyu-body-click", { detail: this.bodyInfo(best), bubbles: true, composed: true }));
|
|
356
|
+
};
|
|
357
|
+
#labelClick = (event) => {
|
|
358
|
+
const name = event.target?.dataset?.["body"];
|
|
359
|
+
if (name)
|
|
360
|
+
this.dispatchEvent(new CustomEvent("chikyu-body-click", { detail: this.bodyInfo(name), bubbles: true, composed: true }));
|
|
361
|
+
};
|
|
362
|
+
#labelFocus = (event) => {
|
|
363
|
+
const name = event.target?.dataset?.["body"];
|
|
364
|
+
if (name)
|
|
365
|
+
this.dispatchEvent(new CustomEvent("chikyu-body-focus", { detail: this.bodyInfo(name), bubbles: true, composed: true }));
|
|
366
|
+
};
|
|
367
|
+
// --- the rest --------------------------------------------------------------------------------------------------------------------------------------
|
|
368
|
+
#textureBase() {
|
|
369
|
+
const given = this.getAttribute("texture-base");
|
|
370
|
+
const base = given ?? new URL("../textures/", import.meta.url).href;
|
|
371
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
372
|
+
}
|
|
373
|
+
#textureCap() {
|
|
374
|
+
const given = Number(this.getAttribute("max-texture"));
|
|
375
|
+
const memory = navigator.deviceMemory;
|
|
376
|
+
const cap = Number.isFinite(given) && given > 0 ? Math.min(given, MAX_TEXTURE) : MAX_TEXTURE;
|
|
377
|
+
return memory !== undefined && memory <= 2 ? Math.min(cap, 2048) : cap;
|
|
378
|
+
}
|
|
379
|
+
#lost = () => this.#set("lost");
|
|
380
|
+
#restored = () => this.#set("live");
|
|
381
|
+
#set(state) {
|
|
382
|
+
if (this.#state === state)
|
|
383
|
+
return;
|
|
384
|
+
this.#state = state;
|
|
385
|
+
this.dataset["state"] = state;
|
|
386
|
+
}
|
|
387
|
+
#fail(reason) {
|
|
388
|
+
this.#set("unavailable");
|
|
389
|
+
this.dispatchEvent(new CustomEvent("chikyu-unavailable", { detail: { reason } }));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (typeof customElements !== "undefined" && !customElements.get("chikyu-space")) {
|
|
393
|
+
customElements.define("chikyu-space", ChikyuSpace);
|
|
394
|
+
}
|