@johnmorrisdotca/chikyu-earth 0.4.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/NOTICE.md +39 -1
  3. package/README.md +199 -24
  4. package/dist/countries.d.ts +11 -0
  5. package/dist/countries.js +11 -0
  6. package/dist/element.d.ts +24 -1
  7. package/dist/element.js +70 -5
  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 +38 -0
  13. package/dist/markers/cluster.js +42 -0
  14. package/dist/markers/index.d.ts +5 -0
  15. package/dist/markers/index.js +6 -0
  16. package/dist/markers/labels.d.ts +64 -0
  17. package/dist/markers/labels.js +70 -0
  18. package/dist/markers/layer.d.ts +129 -0
  19. package/dist/markers/layer.js +455 -0
  20. package/dist/markers/project.d.ts +82 -0
  21. package/dist/markers/project.js +93 -0
  22. package/dist/markers/types.d.ts +83 -0
  23. package/dist/markers/types.js +1 -0
  24. package/dist/realistic/earth.d.ts +81 -0
  25. package/dist/realistic/earth.js +163 -0
  26. package/dist/realistic/element.d.ts +93 -0
  27. package/dist/realistic/element.js +284 -0
  28. package/dist/realistic/gate.d.ts +64 -0
  29. package/dist/realistic/gate.js +72 -0
  30. package/dist/realistic/index.d.ts +5 -0
  31. package/dist/realistic/index.js +6 -0
  32. package/dist/realistic/shaders.d.ts +10 -0
  33. package/dist/realistic/shaders.js +124 -0
  34. package/dist/realistic/shared.d.ts +23 -0
  35. package/dist/realistic/shared.js +51 -0
  36. package/dist/realistic/sun.d.ts +44 -0
  37. package/dist/realistic/sun.js +65 -0
  38. package/dist/realistic/textures.d.ts +40 -0
  39. package/dist/realistic/textures.js +141 -0
  40. package/dist/realistic/tiers.d.ts +94 -0
  41. package/dist/realistic/tiers.js +94 -0
  42. package/dist/rotate.d.ts +59 -4
  43. package/dist/rotate.js +59 -4
  44. package/dist/space/element.d.ts +134 -0
  45. package/dist/space/element.js +394 -0
  46. package/dist/space/ephemeris.d.ts +151 -0
  47. package/dist/space/ephemeris.js +162 -0
  48. package/dist/space/index.d.ts +3 -0
  49. package/dist/space/index.js +5 -0
  50. package/dist/space/scale.d.ts +124 -0
  51. package/dist/space/scale.js +141 -0
  52. package/dist/space/scene.d.ts +131 -0
  53. package/dist/space/scene.js +295 -0
  54. package/dist/styles.d.ts +65 -2
  55. package/dist/styles.js +43 -2
  56. package/dist/textures/clouds-1024.webp +0 -0
  57. package/dist/textures/clouds-2048.webp +0 -0
  58. package/dist/textures/day-1024.webp +0 -0
  59. package/dist/textures/day-2048.webp +0 -0
  60. package/dist/textures/day-4096.webp +0 -0
  61. package/dist/textures/moon-1024.webp +0 -0
  62. package/dist/textures/moon-2048.webp +0 -0
  63. package/dist/textures/night-1024.webp +0 -0
  64. package/dist/textures/night-2048.webp +0 -0
  65. package/dist/textures/water-1024.webp +0 -0
  66. package/dist/textures/water-2048.webp +0 -0
  67. package/dist/types.d.ts +55 -5
  68. package/dist/view.d.ts +64 -3
  69. package/dist/view.js +56 -3
  70. package/package.json +93 -8
@@ -0,0 +1,284 @@
1
+ import { createEarthItem, lookAt, newModel } from "./earth.js";
2
+ import { createGate, frameCap } from "./gate.js";
3
+ import { setThreeLoader, takeRenderer } from "./shared.js";
4
+ export { setThreeLoader };
5
+ import { MAX_TEXTURE, TIERS } from "./tiers.js";
6
+ const RAD = Math.PI / 180;
7
+ /** The Sun and the clouds are re-placed this often while the time is "now": 15 degrees an hour is 0.25 degree a minute. */
8
+ const LIVE_TICK_MS = 60_000;
9
+ const MIN_TICK_MS = 1000;
10
+ const readCentre = (value) => {
11
+ if (!value)
12
+ return null;
13
+ const [lon, lat] = value.split(",").map((part) => Number(part.trim()));
14
+ if (!Number.isFinite(lon) || !Number.isFinite(lat) || Math.abs(lon) > 180 || Math.abs(lat) > 90)
15
+ return null;
16
+ return { lon: lon, lat: lat };
17
+ };
18
+ /**
19
+ * A `time` attribute: "now" (or none) follows the clock, else an ISO date such as 2026-06-21T12:00:00Z.
20
+ *
21
+ * @param value - The attribute's text, or `null` when it is not there.
22
+ * @returns The moment in milliseconds since the Unix epoch, or `"now"` when the value is empty, `"now"` or not a date.
23
+ *
24
+ * @example
25
+ * ```ts no-run
26
+ * import { readTime } from "@johnmorrisdotca/chikyu-earth/realistic/element";
27
+ *
28
+ * console.log(readTime("2026-06-21T12:00:00Z"), readTime(null), readTime("tomorrow"));
29
+ * ```
30
+ */
31
+ export const readTime = (value) => {
32
+ if (value === null || value === "" || value === "now")
33
+ return "now";
34
+ const ms = Date.parse(value);
35
+ return Number.isFinite(ms) ? ms : "now";
36
+ };
37
+ /**
38
+ * The `<chikyu-earth-realistic>` custom element: the Earth from space, drawn with three.js through kazoku-ui's shared renderer (one WebGL context for the page). It takes `center` (`"lon,lat"`), `time` (`now` or an ISO date; the real Sun at that moment), `clouds="off"`, `lights="off"`, `live-seconds` and `texture-base` (where the pictures are served from), and a shared view controller's camera through `setView`. It draws only when something changes, at most 30 times a second, and nothing while it is out of sight or the tab is hidden.
39
+ *
40
+ * @example
41
+ * ```ts no-run
42
+ * import type { ChikyuEarthRealistic } from "@johnmorrisdotca/chikyu-earth/realistic/element";
43
+ *
44
+ * const earth = document.createElement("chikyu-earth-realistic") as ChikyuEarthRealistic;
45
+ * earth.setAttribute("center", "139.7,35.7");
46
+ * earth.setAttribute("time", "2026-06-21T03:00Z");
47
+ * earth.setAttribute("texture-base", "/textures/");
48
+ * document.body.append(earth);
49
+ * ```
50
+ */
51
+ export class ChikyuEarthRealistic extends HTMLElement {
52
+ static observedAttributes = ["center", "time", "clouds", "lights", "live-seconds"];
53
+ #model = newModel();
54
+ #mode = "now";
55
+ #view;
56
+ #gate;
57
+ #tick;
58
+ #state = "loading";
59
+ #tiers = { day: TIERS.day[0], night: TIERS.night[0], clouds: TIERS.clouds[0], water: TIERS.water[0] };
60
+ #taken;
61
+ #stage;
62
+ #overlay;
63
+ #pose = null;
64
+ #listeners = new Set();
65
+ #onContext = (state) => {
66
+ if (state === "unavailable")
67
+ this.#fail("WebGL is not available");
68
+ };
69
+ // The canvas's own events reach the stage in the capture phase (they do not bubble); the shared renderer handles them and this follows.
70
+ #lost = () => this.#set("lost");
71
+ #restored = () => this.#set("live");
72
+ connectedCallback() {
73
+ this.#model.centre = readCentre(this.getAttribute("center")) ?? this.#model.centre;
74
+ this.#mode = readTime(this.getAttribute("time"));
75
+ this.#model.time = this.#mode === "now" ? Date.now() : this.#mode;
76
+ this.#model.clouds = this.getAttribute("clouds") !== "off";
77
+ this.#model.lights = this.getAttribute("lights") !== "off";
78
+ if (!this.hasAttribute("role"))
79
+ this.setAttribute("role", "img");
80
+ if (!this.hasAttribute("aria-label"))
81
+ this.setAttribute("aria-label", "The Earth from space, with its sunlit and dark sides");
82
+ const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
83
+ root.innerHTML = `<style>:host{display:block;aspect-ratio:1/1;position:relative}#stage,#overlay{position:absolute;inset:0}#overlay{pointer-events:none}</style><div id="stage"></div><div id="overlay"></div>`;
84
+ this.#stage = root.getElementById("stage");
85
+ this.#overlay = root.getElementById("overlay");
86
+ this.#stage.addEventListener("webglcontextlost", this.#lost, true);
87
+ this.#stage.addEventListener("webglcontextrestored", this.#restored, true);
88
+ const nav = navigator;
89
+ const cap = frameCap(30, { deviceMemory: nav.deviceMemory, hardwareConcurrency: nav.hardwareConcurrency });
90
+ this.#gate = createGate(1000 / cap, () => this.#view?.invalidate());
91
+ nav.getBattery?.().then((battery) => {
92
+ if (!battery.charging && battery.level <= 0.2)
93
+ this.#gate?.setMinMs(1000 / Math.min(cap, 15));
94
+ }, () => undefined);
95
+ const taken = takeRenderer(this.#onContext);
96
+ this.#taken = taken;
97
+ const item = createEarthItem(this.#model, {
98
+ assets: taken.renderer.assets,
99
+ textureBase: () => this.#textureBase(),
100
+ cap: () => this.#textureCap(),
101
+ drift: 1.5,
102
+ invalidate: () => this.#gate?.request(),
103
+ onPose: (pose) => {
104
+ this.#pose = { ...pose, transform: "" };
105
+ for (const listener of this.#listeners)
106
+ listener(this.#pose);
107
+ },
108
+ onError: (error) => this.#fail(error instanceof Error ? error.message : String(error)),
109
+ onTier: (layer, width) => {
110
+ if (layer in this.#tiers)
111
+ this.#tiers[layer] = width;
112
+ this.dispatchEvent(new CustomEvent("chikyu-tier", { detail: { layer, width } }));
113
+ },
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(this.#view?.live === false && taken.renderer.focused !== undefined ? "idle" : "unavailable");
124
+ });
125
+ this.addEventListener("pointerdown", this.#take);
126
+ this.addEventListener("focusin", this.#take);
127
+ this.#startTick();
128
+ this.#locate();
129
+ }
130
+ disconnectedCallback() {
131
+ this.removeEventListener("pointerdown", this.#take);
132
+ this.removeEventListener("focusin", this.#take);
133
+ clearInterval(this.#tick);
134
+ this.#tick = undefined;
135
+ this.#gate?.cancel();
136
+ this.#view?.dispose();
137
+ this.#view = undefined;
138
+ this.#taken?.release();
139
+ this.#taken = undefined;
140
+ }
141
+ attributeChangedCallback(name, _old, value) {
142
+ if (name === "center")
143
+ this.centre = readCentre(value) ?? { lon: 0, lat: 0 };
144
+ else if (name === "time")
145
+ this.time = readTime(value);
146
+ else if (name === "clouds")
147
+ this.clouds = value !== "off";
148
+ else if (name === "lights")
149
+ this.lights = value !== "off";
150
+ else if (name === "live-seconds")
151
+ this.#startTick();
152
+ }
153
+ /** A box laid over the Earth for markers (see `@johnmorrisdotca/chikyu-earth/markers`): where it is, the pose of the last draw, and a way to hear the next. */
154
+ get overlay() {
155
+ if (!this.#overlay)
156
+ throw new Error("chikyu-earth-realistic: attach markers once the element is in the page");
157
+ return {
158
+ overlayRoot: this.#overlay,
159
+ pose: this.#pose,
160
+ subscribe: (listener) => {
161
+ this.#listeners.add(listener);
162
+ return () => this.#listeners.delete(listener);
163
+ },
164
+ };
165
+ }
166
+ /** Where the view starts: longitude and latitude; the shared controls' yaw and tilt turn it from there. */
167
+ get centre() {
168
+ return { ...this.#model.centre };
169
+ }
170
+ set centre(value) {
171
+ this.#model.centre = { ...value };
172
+ this.#gate?.request();
173
+ }
174
+ /** The moment shown: milliseconds since the epoch, or "now" to follow the clock. */
175
+ get time() {
176
+ return this.#mode;
177
+ }
178
+ set time(value) {
179
+ this.#mode = value instanceof Date ? value.getTime() : value;
180
+ if (this.#mode === "now")
181
+ this.#model.time = Date.now();
182
+ else
183
+ this.#model.time = this.#mode;
184
+ this.#startTick();
185
+ this.#gate?.request();
186
+ }
187
+ get clouds() {
188
+ return this.#model.clouds;
189
+ }
190
+ set clouds(value) {
191
+ this.#model.clouds = value;
192
+ this.#gate?.request();
193
+ }
194
+ get lights() {
195
+ return this.#model.lights;
196
+ }
197
+ set lights(value) {
198
+ this.#model.lights = value;
199
+ this.#gate?.request();
200
+ }
201
+ /** What the element is doing, for a page or a test: its state, the texture widths held, the draws made. */
202
+ get stats() {
203
+ const stats = this.#taken?.renderer.stats;
204
+ return {
205
+ state: this.#state,
206
+ tiers: { ...this.#tiers },
207
+ draws: stats?.draws ?? 0,
208
+ contexts: stats?.contexts ?? 0,
209
+ canvas: stats?.canvas ?? { width: 0, height: 0, pixels: 0 },
210
+ assets: { entries: stats?.assets.entries ?? 0, refs: stats?.assets.refs ?? 0 },
211
+ };
212
+ }
213
+ /**
214
+ * Apply a shared view controller's camera (yaw, tilt, zoom, pan). A change under half a pixel is not drawn, a settled change always is,
215
+ * and draws are at most 30 a second.
216
+ */
217
+ setView(view, final = true) {
218
+ this.#model.view = { ...view };
219
+ const drawn = this.#model.drawn;
220
+ if (!drawn) {
221
+ this.#gate?.request();
222
+ return;
223
+ }
224
+ const shifted = view.zoom !== drawn.view.zoom || view.panX !== drawn.view.panX || view.panY !== drawn.view.panY;
225
+ const a = lookAt(drawn.centre, drawn.view);
226
+ const b = lookAt(this.#model.centre, view);
227
+ const px = (Math.abs(b.lon - a.lon) * Math.cos(((a.lat + b.lat) / 2) * RAD) + Math.abs(b.lat - a.lat)) * RAD * drawn.radius;
228
+ if (shifted || px >= 0.5 || (final && px > 0.01))
229
+ this.#gate?.request();
230
+ }
231
+ /** The view as it is (for a page that wants to read it back). */
232
+ get view() {
233
+ return { ...this.#model.view };
234
+ }
235
+ /** While the time is "now", the Sun and the clouds are put right once a minute (`live-seconds` changes that): no frames between. */
236
+ #startTick() {
237
+ clearInterval(this.#tick);
238
+ this.#tick = undefined;
239
+ if (!this.isConnected || this.#mode !== "now")
240
+ return;
241
+ const seconds = Number(this.getAttribute("live-seconds"));
242
+ const ms = Number.isFinite(seconds) && seconds > 0 ? Math.max(MIN_TICK_MS, seconds * 1000) : LIVE_TICK_MS;
243
+ this.#tick = setInterval(this.#onTick, ms);
244
+ }
245
+ #onTick = () => {
246
+ this.#model.time = Date.now();
247
+ this.#gate?.request();
248
+ };
249
+ #take = () => {
250
+ if (this.#view && !this.#view.live)
251
+ void this.#view.focus();
252
+ };
253
+ #textureBase() {
254
+ const given = this.getAttribute("texture-base");
255
+ const base = given ?? new URL("../textures/", import.meta.url).href;
256
+ return base.endsWith("/") ? base : `${base}/`;
257
+ }
258
+ #textureCap() {
259
+ const given = Number(this.getAttribute("max-texture"));
260
+ const memory = navigator.deviceMemory;
261
+ const cap = Number.isFinite(given) && given > 0 ? Math.min(given, MAX_TEXTURE) : MAX_TEXTURE;
262
+ return memory !== undefined && memory <= 2 ? Math.min(cap, 2048) : cap;
263
+ }
264
+ #set(state) {
265
+ if (this.#state === state)
266
+ return;
267
+ this.#state = state;
268
+ this.dataset["state"] = state;
269
+ }
270
+ #fail(reason) {
271
+ this.#set("unavailable");
272
+ this.dispatchEvent(new CustomEvent("chikyu-unavailable", { detail: { reason } }));
273
+ }
274
+ #locate() {
275
+ if (readCentre(this.getAttribute("center")) || typeof navigator === "undefined" || !navigator.geolocation)
276
+ return;
277
+ navigator.geolocation.getCurrentPosition((position) => {
278
+ this.centre = { lon: position.coords.longitude, lat: position.coords.latitude };
279
+ }, () => undefined, { timeout: 8000, maximumAge: 600000 });
280
+ }
281
+ }
282
+ if (typeof customElements !== "undefined" && !customElements.get("chikyu-earth-realistic")) {
283
+ customElements.define("chikyu-earth-realistic", ChikyuEarthRealistic);
284
+ }
@@ -0,0 +1,64 @@
1
+ export interface Timers {
2
+ set(callback: () => void, ms: number): unknown;
3
+ clear(handle: unknown): void;
4
+ now(): number;
5
+ }
6
+ export declare const browserTimers: Timers;
7
+ /**
8
+ * A rate gate for the draw: what `createGate` returns.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createGate, type Gate } from "@johnmorrisdotca/chikyu-earth/realistic";
13
+ *
14
+ * const gate: Gate = createGate(33, () => console.log("draw"));
15
+ * gate.request();
16
+ * gate.cancel();
17
+ * ```
18
+ */
19
+ export interface Gate {
20
+ /** Ask for `fire` to run: now if the last run was `minMs` ago or more, else once, when that time has passed. */
21
+ request(): void;
22
+ /** Forget a run that is waiting. */
23
+ cancel(): void;
24
+ /** Change the least time between runs (a lower frame rate on a battery). */
25
+ setMinMs(minMs: number): void;
26
+ }
27
+ /**
28
+ * A rate gate for the draw: asks that come faster than `minMs` apart become one, and the last ask is never lost, so a burst of changes costs one draw.
29
+ *
30
+ * @param minMs - The least time between two runs, in milliseconds.
31
+ * @param fire - What to run.
32
+ * @param timers - A clock and timers; the browser's by default (a test passes its own).
33
+ * @returns The gate: `request()` to ask, `cancel()` to forget a waiting run, `setMinMs()` to change the pace.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { createGate } from "@johnmorrisdotca/chikyu-earth/realistic";
38
+ *
39
+ * const gate = createGate(30, () => console.log("drawn"));
40
+ * gate.request();
41
+ * gate.request();
42
+ * gate.request();
43
+ * gate.cancel();
44
+ * ```
45
+ */
46
+ export declare function createGate(minMs: number, fire: () => void, timers?: Timers): Gate;
47
+ /**
48
+ * The frame rate cap for this device: `maxFps` (30 by default), lower on a small device.
49
+ *
50
+ * @param maxFps - The most frames a second to allow.
51
+ * @param device - `deviceMemory` in gigabytes and `hardwareConcurrency` in cores, from `navigator`; either may be left out.
52
+ * @returns The frames a second to allow, at least 1; 20 at most on a small device.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { frameCap } from "@johnmorrisdotca/chikyu-earth/realistic";
57
+ *
58
+ * console.log(frameCap(), frameCap(30, { deviceMemory: 1 }));
59
+ * ```
60
+ */
61
+ export declare function frameCap(maxFps?: number, device?: {
62
+ deviceMemory?: number;
63
+ hardwareConcurrency?: number;
64
+ }): number;
@@ -0,0 +1,72 @@
1
+ // A rate gate for the draw: asks that come faster than `minMs` apart become one, and the last ask is never lost. Pure, with its own clock.
2
+ export const browserTimers = {
3
+ set: (callback, ms) => setTimeout(callback, ms),
4
+ clear: (handle) => clearTimeout(handle),
5
+ now: () => performance.now(),
6
+ };
7
+ /**
8
+ * A rate gate for the draw: asks that come faster than `minMs` apart become one, and the last ask is never lost, so a burst of changes costs one draw.
9
+ *
10
+ * @param minMs - The least time between two runs, in milliseconds.
11
+ * @param fire - What to run.
12
+ * @param timers - A clock and timers; the browser's by default (a test passes its own).
13
+ * @returns The gate: `request()` to ask, `cancel()` to forget a waiting run, `setMinMs()` to change the pace.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { createGate } from "@johnmorrisdotca/chikyu-earth/realistic";
18
+ *
19
+ * const gate = createGate(30, () => console.log("drawn"));
20
+ * gate.request();
21
+ * gate.request();
22
+ * gate.request();
23
+ * gate.cancel();
24
+ * ```
25
+ */
26
+ export function createGate(minMs, fire, timers = browserTimers) {
27
+ let least = minMs;
28
+ let last = -Infinity;
29
+ let waiting;
30
+ const run = () => {
31
+ waiting = undefined;
32
+ last = timers.now();
33
+ fire();
34
+ };
35
+ return {
36
+ request() {
37
+ if (waiting !== undefined)
38
+ return;
39
+ const wait = last + least - timers.now();
40
+ if (wait <= 0)
41
+ run();
42
+ else
43
+ waiting = timers.set(run, wait);
44
+ },
45
+ cancel() {
46
+ if (waiting !== undefined)
47
+ timers.clear(waiting);
48
+ waiting = undefined;
49
+ },
50
+ setMinMs(value) {
51
+ least = value;
52
+ },
53
+ };
54
+ }
55
+ /**
56
+ * The frame rate cap for this device: `maxFps` (30 by default), lower on a small device.
57
+ *
58
+ * @param maxFps - The most frames a second to allow.
59
+ * @param device - `deviceMemory` in gigabytes and `hardwareConcurrency` in cores, from `navigator`; either may be left out.
60
+ * @returns The frames a second to allow, at least 1; 20 at most on a small device.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { frameCap } from "@johnmorrisdotca/chikyu-earth/realistic";
65
+ *
66
+ * console.log(frameCap(), frameCap(30, { deviceMemory: 1 }));
67
+ * ```
68
+ */
69
+ export function frameCap(maxFps = 30, device = {}) {
70
+ const small = (device.deviceMemory !== undefined && device.deviceMemory <= 2) || (device.hardwareConcurrency !== undefined && device.hardwareConcurrency <= 2);
71
+ return Math.max(1, small ? Math.min(maxFps, 20) : maxFps);
72
+ }
@@ -0,0 +1,5 @@
1
+ export { subsolarPoint, unitVector, julianDay } from "./sun.ts";
2
+ export { chooseTier, textureBytes, textureCap, MAX_TEXTURE, TIERS, TEXELS_PER_RADIUS_PIXEL, type Layer } from "./tiers.ts";
3
+ export { createGate, frameCap, type Gate } from "./gate.ts";
4
+ export { lookAt, discRadius, ATMOSPHERE_OUTER } from "./earth.ts";
5
+ export type { ThreeParts } from "./earth.ts";
@@ -0,0 +1,6 @@
1
+ // The realistic Earth's pure parts, which import on a server and need neither three.js nor a browser. The element is
2
+ // `@johnmorrisdotca/chikyu-earth/realistic/element`.
3
+ export { subsolarPoint, unitVector, julianDay } from "./sun.js";
4
+ export { chooseTier, textureBytes, textureCap, MAX_TEXTURE, TIERS, TEXELS_PER_RADIUS_PIXEL } from "./tiers.js";
5
+ export { createGate, frameCap } from "./gate.js";
6
+ export { lookAt, discRadius, ATMOSPHERE_OUTER } from "./earth.js";
@@ -0,0 +1,10 @@
1
+ export declare const SURFACE_VERT = "\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying vec3 vWorld;\nvoid main() {\n vUv = uv;\n vNormal = normalize(mat3(modelMatrix) * position);\n vWorld = (modelMatrix * vec4(position, 1.0)).xyz;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}";
2
+ export declare const SURFACE_FRAG = "\nuniform sampler2D dayMap;\nuniform sampler2D nightMap;\nuniform sampler2D waterMap;\nuniform vec3 sunDir;\nuniform float lights;\nuniform float ortho;\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying vec3 vWorld;\nvoid main() {\n vec3 n = normalize(vNormal);\n vec3 toEye = ortho > 0.5 ? vec3(0.0, 0.0, 1.0) : normalize(cameraPosition - vWorld);\n float ndl = dot(n, sunDir);\n float day = smoothstep(-0.10, 0.22, ndl);\n vec3 albedo = texture2D(dayMap, vUv).rgb;\n float water = texture2D(waterMap, vUv).r;\n\n vec3 lit = albedo * (0.07 + 1.0 * pow(max(ndl, 0.0), 0.5));\n float dusk = smoothstep(-0.14, 0.0, ndl) * (1.0 - smoothstep(0.0, 0.30, ndl));\n lit += vec3(0.55, 0.28, 0.14) * dusk * 0.30 * (0.35 + 0.65 * albedo.g);\n\n vec3 h = normalize(sunDir + toEye);\n float glint = pow(max(dot(n, h), 0.0), 220.0) * water * day;\n lit += vec3(1.0, 0.93, 0.80) * glint * 0.7;\n\n vec3 night = texture2D(nightMap, vUv).rgb;\n night = night * night * vec3(1.6, 1.5, 1.3) * lights + night * 0.25;\n vec3 col = mix(night, lit, day);\n\n float rim = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), 3.0);\n float sunSide = 0.12 + 0.88 * smoothstep(-0.25, 0.5, ndl);\n col += vec3(0.20, 0.45, 1.0) * rim * sunSide * 0.55;\n gl_FragColor = vec4(col, 1.0);\n}";
3
+ export declare const CLOUD_FRAG = "\nuniform sampler2D cloudMap;\nuniform vec3 sunDir;\nuniform float shift;\nuniform float opacity;\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvoid main() {\n vec3 n = normalize(vNormal);\n float ndl = dot(n, sunDir);\n float c = texture2D(cloudMap, vec2(vUv.x + shift, vUv.y)).r;\n float a = smoothstep(0.28, 0.88, c) * opacity;\n float day = smoothstep(-0.10, 0.25, ndl);\n vec3 col = mix(vec3(0.015, 0.02, 0.04), vec3(1.0), day * (0.18 + 0.82 * pow(max(ndl, 0.0), 0.45)));\n col += vec3(0.55, 0.28, 0.14) * smoothstep(-0.14, 0.0, ndl) * (1.0 - smoothstep(0.0, 0.25, ndl)) * 0.35;\n gl_FragColor = vec4(col, a);\n}";
4
+ export declare const ATMOSPHERE_VERT = "\nvarying vec3 vPosView;\nvarying vec3 vNormalView;\nvoid main() {\n vec4 view = modelViewMatrix * vec4(position, 1.0);\n vPosView = view.xyz;\n vNormalView = normalize(normalMatrix * position);\n gl_Position = projectionMatrix * view;\n}";
5
+ export declare const ATMOSPHERE_FRAG = "\nuniform vec3 sunView;\nuniform float outer;\nuniform float ortho;\nvarying vec3 vPosView;\nvarying vec3 vNormalView;\nvoid main() {\n vec3 n = normalize(vNormalView);\n vec3 v = ortho > 0.5 ? vec3(0.0, 0.0, 1.0) : normalize(-vPosView);\n float nz = clamp(dot(n, v), -1.0, 1.0);\n float rho = outer * sqrt(max(0.0, 1.0 - nz * nz));\n float t = clamp((rho - 1.0) / (outer - 1.0), 0.0, 1.0);\n vec2 dir = normalize(n.xy + vec2(1e-5, 0.0));\n float side = smoothstep(-0.40, 0.55, dot(vec3(dir, 0.0), sunView));\n float glow = pow(1.0 - t, 3.0) * (0.10 + 0.90 * side);\n vec3 col = mix(vec3(0.10, 0.22, 0.60), vec3(0.42, 0.68, 1.0), side);\n gl_FragColor = vec4(col, glow * 0.9);\n}";
6
+ /** The Moon and other plain bodies: a picture (or a flat colour) lit by the Sun, with a trace of light on the dark side. */
7
+ export declare const BODY_FRAG = "\nuniform sampler2D map;\nuniform vec3 tint;\nuniform float textured;\nuniform float ambient;\nuniform vec3 sunDir;\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvoid main() {\n vec3 n = normalize(vNormal);\n float ndl = dot(n, sunDir);\n vec3 albedo = textured > 0.5 ? texture2D(map, vUv).rgb : tint;\n float lit = smoothstep(-0.04, 0.12, ndl) * (0.10 + 0.95 * pow(max(ndl, 0.0), 0.6));\n gl_FragColor = vec4(albedo * (ambient + lit), 1.0);\n}";
8
+ /** A flat quad facing the camera: the Sun's disc and its glow. */
9
+ export declare const GLOW_VERT = "\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}";
10
+ export declare const GLOW_FRAG = "\nuniform vec3 tint;\nvarying vec2 vUv;\nvoid main() {\n float r = length(vUv - 0.5) * 2.0;\n float core = 1.0 - smoothstep(0.10, 0.14, r);\n float halo = pow(max(0.0, 1.0 - r), 2.5);\n vec3 col = mix(tint, vec3(1.0), core);\n gl_FragColor = vec4(col, clamp(core + halo * 0.85, 0.0, 1.0));\n}";
@@ -0,0 +1,124 @@
1
+ // The three programs of the realistic Earth, as GLSL strings: the surface (day, night, sun glint, rim), the cloud layer and the
2
+ // atmosphere's glow. Colours are shaded in the textures' own (sRGB) space: a look, not a radiometric model.
3
+ export const SURFACE_VERT = /* glsl */ `
4
+ varying vec2 vUv;
5
+ varying vec3 vNormal;
6
+ varying vec3 vWorld;
7
+ void main() {
8
+ vUv = uv;
9
+ vNormal = normalize(mat3(modelMatrix) * position);
10
+ vWorld = (modelMatrix * vec4(position, 1.0)).xyz;
11
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
12
+ }`;
13
+ export const SURFACE_FRAG = /* glsl */ `
14
+ uniform sampler2D dayMap;
15
+ uniform sampler2D nightMap;
16
+ uniform sampler2D waterMap;
17
+ uniform vec3 sunDir;
18
+ uniform float lights;
19
+ uniform float ortho;
20
+ varying vec2 vUv;
21
+ varying vec3 vNormal;
22
+ varying vec3 vWorld;
23
+ void main() {
24
+ vec3 n = normalize(vNormal);
25
+ vec3 toEye = ortho > 0.5 ? vec3(0.0, 0.0, 1.0) : normalize(cameraPosition - vWorld);
26
+ float ndl = dot(n, sunDir);
27
+ float day = smoothstep(-0.10, 0.22, ndl);
28
+ vec3 albedo = texture2D(dayMap, vUv).rgb;
29
+ float water = texture2D(waterMap, vUv).r;
30
+
31
+ vec3 lit = albedo * (0.07 + 1.0 * pow(max(ndl, 0.0), 0.5));
32
+ float dusk = smoothstep(-0.14, 0.0, ndl) * (1.0 - smoothstep(0.0, 0.30, ndl));
33
+ lit += vec3(0.55, 0.28, 0.14) * dusk * 0.30 * (0.35 + 0.65 * albedo.g);
34
+
35
+ vec3 h = normalize(sunDir + toEye);
36
+ float glint = pow(max(dot(n, h), 0.0), 220.0) * water * day;
37
+ lit += vec3(1.0, 0.93, 0.80) * glint * 0.7;
38
+
39
+ vec3 night = texture2D(nightMap, vUv).rgb;
40
+ night = night * night * vec3(1.6, 1.5, 1.3) * lights + night * 0.25;
41
+ vec3 col = mix(night, lit, day);
42
+
43
+ float rim = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), 3.0);
44
+ float sunSide = 0.12 + 0.88 * smoothstep(-0.25, 0.5, ndl);
45
+ col += vec3(0.20, 0.45, 1.0) * rim * sunSide * 0.55;
46
+ gl_FragColor = vec4(col, 1.0);
47
+ }`;
48
+ export const CLOUD_FRAG = /* glsl */ `
49
+ uniform sampler2D cloudMap;
50
+ uniform vec3 sunDir;
51
+ uniform float shift;
52
+ uniform float opacity;
53
+ varying vec2 vUv;
54
+ varying vec3 vNormal;
55
+ void main() {
56
+ vec3 n = normalize(vNormal);
57
+ float ndl = dot(n, sunDir);
58
+ float c = texture2D(cloudMap, vec2(vUv.x + shift, vUv.y)).r;
59
+ float a = smoothstep(0.28, 0.88, c) * opacity;
60
+ float day = smoothstep(-0.10, 0.25, ndl);
61
+ vec3 col = mix(vec3(0.015, 0.02, 0.04), vec3(1.0), day * (0.18 + 0.82 * pow(max(ndl, 0.0), 0.45)));
62
+ col += vec3(0.55, 0.28, 0.14) * smoothstep(-0.14, 0.0, ndl) * (1.0 - smoothstep(0.0, 0.25, ndl)) * 0.35;
63
+ gl_FragColor = vec4(col, a);
64
+ }`;
65
+ export const ATMOSPHERE_VERT = /* glsl */ `
66
+ varying vec3 vPosView;
67
+ varying vec3 vNormalView;
68
+ void main() {
69
+ vec4 view = modelViewMatrix * vec4(position, 1.0);
70
+ vPosView = view.xyz;
71
+ vNormalView = normalize(normalMatrix * position);
72
+ gl_Position = projectionMatrix * view;
73
+ }`;
74
+ export const ATMOSPHERE_FRAG = /* glsl */ `
75
+ uniform vec3 sunView;
76
+ uniform float outer;
77
+ uniform float ortho;
78
+ varying vec3 vPosView;
79
+ varying vec3 vNormalView;
80
+ void main() {
81
+ vec3 n = normalize(vNormalView);
82
+ vec3 v = ortho > 0.5 ? vec3(0.0, 0.0, 1.0) : normalize(-vPosView);
83
+ float nz = clamp(dot(n, v), -1.0, 1.0);
84
+ float rho = outer * sqrt(max(0.0, 1.0 - nz * nz));
85
+ float t = clamp((rho - 1.0) / (outer - 1.0), 0.0, 1.0);
86
+ vec2 dir = normalize(n.xy + vec2(1e-5, 0.0));
87
+ float side = smoothstep(-0.40, 0.55, dot(vec3(dir, 0.0), sunView));
88
+ float glow = pow(1.0 - t, 3.0) * (0.10 + 0.90 * side);
89
+ vec3 col = mix(vec3(0.10, 0.22, 0.60), vec3(0.42, 0.68, 1.0), side);
90
+ gl_FragColor = vec4(col, glow * 0.9);
91
+ }`;
92
+ /** The Moon and other plain bodies: a picture (or a flat colour) lit by the Sun, with a trace of light on the dark side. */
93
+ export const BODY_FRAG = /* glsl */ `
94
+ uniform sampler2D map;
95
+ uniform vec3 tint;
96
+ uniform float textured;
97
+ uniform float ambient;
98
+ uniform vec3 sunDir;
99
+ varying vec2 vUv;
100
+ varying vec3 vNormal;
101
+ void main() {
102
+ vec3 n = normalize(vNormal);
103
+ float ndl = dot(n, sunDir);
104
+ vec3 albedo = textured > 0.5 ? texture2D(map, vUv).rgb : tint;
105
+ float lit = smoothstep(-0.04, 0.12, ndl) * (0.10 + 0.95 * pow(max(ndl, 0.0), 0.6));
106
+ gl_FragColor = vec4(albedo * (ambient + lit), 1.0);
107
+ }`;
108
+ /** A flat quad facing the camera: the Sun's disc and its glow. */
109
+ export const GLOW_VERT = /* glsl */ `
110
+ varying vec2 vUv;
111
+ void main() {
112
+ vUv = uv;
113
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
114
+ }`;
115
+ export const GLOW_FRAG = /* glsl */ `
116
+ uniform vec3 tint;
117
+ varying vec2 vUv;
118
+ void main() {
119
+ float r = length(vUv - 0.5) * 2.0;
120
+ float core = 1.0 - smoothstep(0.10, 0.14, r);
121
+ float halo = pow(max(0.0, 1.0 - r), 2.5);
122
+ vec3 col = mix(tint, vec3(1.0), core);
123
+ gl_FragColor = vec4(col, clamp(core + halo * 0.85, 0.0, 1.0));
124
+ }`;
@@ -0,0 +1,23 @@
1
+ import { type SharedRenderer } from "@johnmorrisdotca/kazoku-ui/renderer";
2
+ import type { ThreeParts } from "./textures.ts";
3
+ type Three = ThreeParts;
4
+ export type ContextState = "lost" | "restored" | "unavailable";
5
+ /**
6
+ * Where three.js comes from, if a page wants to hand it over itself (before the first element connects). A page that hands over a module re-exporting only the parts the Earth and the space view use ships a few hundred kilobytes of three.js instead of all of it.
7
+ *
8
+ * @param load - Returns the parts of three.js the Earth and the space view use, as a promise.
9
+ *
10
+ * @example
11
+ * ```ts no-run
12
+ * import { setThreeLoader } from "@johnmorrisdotca/chikyu-earth/realistic/element";
13
+ *
14
+ * setThreeLoader(() => import("three"));
15
+ * ```
16
+ */
17
+ export declare function setThreeLoader(load: () => Promise<Three>): void;
18
+ /** The page's renderer, made on first use. Call `release()` (from the result) when the element leaves. */
19
+ export declare function takeRenderer(onContext: (state: ContextState) => void): {
20
+ renderer: SharedRenderer<Three>;
21
+ release(): void;
22
+ };
23
+ export {};
@@ -0,0 +1,51 @@
1
+ // One shared renderer for the page, whichever element draws with it: the Earth from space and the space view both take it from here, so the page keeps one
2
+ // WebGL context between them. The last element to leave lets it go (after a second, in case it is only moving).
3
+ import { createSharedRenderer } from "@johnmorrisdotca/kazoku-ui/renderer";
4
+ let shared;
5
+ let loader = () => import("three");
6
+ const contextListeners = new Set();
7
+ let elements = 0;
8
+ let releaseTimer;
9
+ /**
10
+ * Where three.js comes from, if a page wants to hand it over itself (before the first element connects). A page that hands over a module re-exporting only the parts the Earth and the space view use ships a few hundred kilobytes of three.js instead of all of it.
11
+ *
12
+ * @param load - Returns the parts of three.js the Earth and the space view use, as a promise.
13
+ *
14
+ * @example
15
+ * ```ts no-run
16
+ * import { setThreeLoader } from "@johnmorrisdotca/chikyu-earth/realistic/element";
17
+ *
18
+ * setThreeLoader(() => import("three"));
19
+ * ```
20
+ */
21
+ export function setThreeLoader(load) {
22
+ loader = load;
23
+ }
24
+ /** The page's renderer, made on first use. Call `release()` (from the result) when the element leaves. */
25
+ export function takeRenderer(onContext) {
26
+ clearTimeout(releaseTimer);
27
+ elements += 1;
28
+ contextListeners.add(onContext);
29
+ shared ??= createSharedRenderer({
30
+ load: () => loader(),
31
+ onContext: (state) => {
32
+ for (const listener of contextListeners)
33
+ listener(state);
34
+ },
35
+ });
36
+ return {
37
+ renderer: shared,
38
+ release() {
39
+ contextListeners.delete(onContext);
40
+ elements -= 1;
41
+ if (elements <= 0) {
42
+ releaseTimer = setTimeout(() => {
43
+ if (elements <= 0) {
44
+ shared?.dispose();
45
+ shared = undefined;
46
+ }
47
+ }, 1000);
48
+ }
49
+ },
50
+ };
51
+ }