@johnmorrisdotca/chikyu-earth 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/NOTICE.md +22 -0
  3. package/README.md +186 -27
  4. package/dist/countries.d.ts +11 -0
  5. package/dist/countries.js +11 -0
  6. package/dist/element.d.ts +18 -0
  7. package/dist/element.js +15 -2
  8. package/dist/globe.d.ts +15 -1
  9. package/dist/globe.js +15 -1
  10. package/dist/index.d.ts +12 -1
  11. package/dist/index.js +12 -1
  12. package/dist/markers/cluster.d.ts +27 -0
  13. package/dist/markers/cluster.js +16 -0
  14. package/dist/markers/labels.d.ts +40 -0
  15. package/dist/markers/labels.js +18 -0
  16. package/dist/markers/layer.d.ts +80 -0
  17. package/dist/markers/layer.js +21 -0
  18. package/dist/markers/project.d.ts +63 -2
  19. package/dist/markers/project.js +52 -2
  20. package/dist/markers/types.d.ts +48 -3
  21. package/dist/realistic/earth.d.ts +30 -5
  22. package/dist/realistic/earth.js +41 -134
  23. package/dist/realistic/element.d.ts +47 -7
  24. package/dist/realistic/element.js +48 -44
  25. package/dist/realistic/gate.d.ts +45 -1
  26. package/dist/realistic/gate.js +33 -1
  27. package/dist/realistic/shaders.d.ts +9 -4
  28. package/dist/realistic/shaders.js +54 -9
  29. package/dist/realistic/shared.d.ts +23 -0
  30. package/dist/realistic/shared.js +51 -0
  31. package/dist/realistic/sun.d.ts +40 -3
  32. package/dist/realistic/sun.js +40 -3
  33. package/dist/realistic/textures.d.ts +43 -0
  34. package/dist/realistic/textures.js +141 -0
  35. package/dist/realistic/tiers.d.ts +80 -6
  36. package/dist/realistic/tiers.js +69 -5
  37. package/dist/rotate.d.ts +59 -4
  38. package/dist/rotate.js +59 -4
  39. package/dist/satellites/index.d.ts +3 -0
  40. package/dist/satellites/index.js +3 -0
  41. package/dist/satellites/limits.d.ts +22 -0
  42. package/dist/satellites/limits.js +23 -0
  43. package/dist/satellites/track.d.ts +50 -0
  44. package/dist/satellites/track.js +227 -0
  45. package/dist/satellites/types.d.ts +161 -0
  46. package/dist/satellites/types.js +3 -0
  47. package/dist/space/element.d.ts +179 -0
  48. package/dist/space/element.js +541 -0
  49. package/dist/space/ephemeris.d.ts +151 -0
  50. package/dist/space/ephemeris.js +162 -0
  51. package/dist/space/index.d.ts +3 -0
  52. package/dist/space/index.js +5 -0
  53. package/dist/space/satellite-layer.d.ts +53 -0
  54. package/dist/space/satellite-layer.js +230 -0
  55. package/dist/space/scale.d.ts +124 -0
  56. package/dist/space/scale.js +141 -0
  57. package/dist/space/scene.d.ts +146 -0
  58. package/dist/space/scene.js +320 -0
  59. package/dist/styles.d.ts +65 -2
  60. package/dist/styles.js +43 -2
  61. package/dist/textures/moon-1024.webp +0 -0
  62. package/dist/textures/moon-2048.webp +0 -0
  63. package/dist/types.d.ts +55 -5
  64. package/dist/view.d.ts +63 -4
  65. package/dist/view.js +52 -4
  66. package/package.json +71 -8
@@ -1,14 +1,21 @@
1
1
  import { HOME_VIEW, lookAt } from "../view.js";
2
2
  import { ATMOSPHERE_FRAG, ATMOSPHERE_VERT, CLOUD_FRAG, SURFACE_FRAG, SURFACE_VERT } from "./shaders.js";
3
3
  import { subsolarPoint, unitVector } from "./sun.js";
4
- import { chooseTier, TIERS } from "./tiers.js";
4
+ import { createTextureSet } from "./textures.js";
5
5
  const RAD = Math.PI / 180;
6
- /** The glow reaches this many radii from the centre; the globe at 1x is drawn to fit inside it. */
6
+ /**
7
+ * The glow reaches this many radii from the centre; the globe at 1x is drawn to fit inside it.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { ATMOSPHERE_OUTER } from "@johnmorrisdotca/chikyu-earth/realistic";
12
+ *
13
+ * console.log(ATMOSPHERE_OUTER);
14
+ * ```
15
+ */
7
16
  export const ATMOSPHERE_OUTER = 1.08;
8
17
  const CLOUD_RADIUS = 1.006;
9
18
  const LAYERS = ["day", "night", "clouds", "water"];
10
- /** Milliseconds a lower texture size must be enough before the larger one is let go. */
11
- const DOWNGRADE_AFTER_MS = 4000;
12
19
  export const newModel = () => ({
13
20
  centre: { lon: 0, lat: 0 },
14
21
  view: { ...HOME_VIEW },
@@ -18,11 +25,25 @@ export const newModel = () => ({
18
25
  drawn: null,
19
26
  ratio: () => Math.min(globalThis.devicePixelRatio || 1, 2),
20
27
  });
21
- /** The disc's radius in CSS pixels in a box of `width` x `height` at `zoom`: it fits the glow in the box at 1x. */
28
+ /**
29
+ * The disc's radius in CSS pixels in a box of `width` x `height` at `zoom`: it fits the glow in the box at 1x.
30
+ *
31
+ * @param width - The box's width in CSS pixels.
32
+ * @param height - The box's height in CSS pixels.
33
+ * @param zoom - The view's zoom, 1 at Fit.
34
+ * @returns The radius in CSS pixels, at least 1.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { discRadius } from "@johnmorrisdotca/chikyu-earth/realistic";
39
+ *
40
+ * console.log(discRadius(400, 400, 1).toFixed(1));
41
+ * ```
42
+ */
22
43
  export const discRadius = (width, height, zoom) => Math.max(1, (Math.min(width, height) / 2 / ATMOSPHERE_OUTER - 1) * zoom);
23
44
  export { lookAt };
24
45
  /** An equirectangular sphere: +z is longitude 0 on the equator, +x east, +y north; v is 0 at the north pole (the top row of the picture). */
25
- function sphere(three, columns, rows) {
46
+ export function sphere(three, columns, rows) {
26
47
  const positions = new Float32Array((columns + 1) * (rows + 1) * 3);
27
48
  const uvs = new Float32Array((columns + 1) * (rows + 1) * 2);
28
49
  let p = 0;
@@ -57,89 +78,34 @@ export function createEarthItem(model, deps) {
57
78
  key: "chikyu-earth:realistic",
58
79
  async build({ three, width, height }) {
59
80
  let size = { width, height };
60
- let disposed = false;
61
- const held = [];
62
- const makeTexture = (layer, texels) => {
63
- const key = `chikyu-earth:${layer}:${texels}`;
64
- const entry = deps.assets.acquire(key, () => {
65
- const texture = new three.Texture();
66
- texture.wrapS = three.RepeatWrapping;
67
- texture.wrapT = three.ClampToEdgeWrapping;
68
- texture.minFilter = three.LinearMipmapLinearFilter;
69
- texture.magFilter = three.LinearFilter;
70
- texture.anisotropy = 8;
71
- const controller = new AbortController();
72
- const made = { width: texels, texture, closed: false, ready: Promise.resolve(), release: () => undefined };
73
- made.ready = fetch(`${deps.textureBase()}${layer}-${texels}.webp`, { signal: controller.signal })
74
- .then((response) => {
75
- if (!response.ok)
76
- throw new Error(`${layer}-${texels}.webp: ${response.status}`);
77
- return response.blob();
78
- })
79
- .then((blob) => createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }))
80
- .then((bitmap) => {
81
- if (made.closed)
82
- bitmap.close();
83
- else {
84
- texture.image = bitmap;
85
- texture.needsUpdate = true;
86
- }
87
- });
88
- made.ready.catch(() => undefined);
89
- made.release = () => {
90
- made.closed = true;
91
- controller.abort();
92
- texture.dispose();
93
- texture.image?.close?.();
94
- };
95
- return made;
96
- }, (value) => value.release());
97
- held.push(key);
98
- return entry;
99
- };
100
- const letGo = (layer, texels) => {
101
- const key = `chikyu-earth:${layer}:${texels}`;
102
- const at = held.indexOf(key);
103
- if (at >= 0)
104
- held.splice(at, 1);
105
- deps.assets.release(key);
106
- };
107
- const slots = new Map();
108
- for (const layer of LAYERS) {
109
- const baseWidth = TIERS[layer][0];
110
- const base = makeTexture(layer, baseWidth);
111
- slots.set(layer, { layer, uniform: { value: null }, base, baseWidth, entry: base, width: baseWidth, loading: undefined, timer: undefined });
112
- }
113
- try {
114
- await Promise.all([...slots.values()].map((slot) => slot.base.ready));
115
- }
116
- catch (error) {
117
- for (const slot of slots.values())
118
- letGo(slot.layer, slot.baseWidth);
119
- deps.onError?.(error);
120
- throw error;
121
- }
122
- for (const slot of slots.values())
123
- slot.uniform.value = slot.base.texture;
81
+ const textures = await createTextureSet(three, LAYERS, {
82
+ assets: deps.assets,
83
+ textureBase: deps.textureBase,
84
+ cap: deps.cap,
85
+ invalidate: deps.invalidate,
86
+ ...(deps.onError ? { onError: deps.onError } : {}),
87
+ ...(deps.onTier ? { onTier: deps.onTier } : {}),
88
+ });
89
+ const uniform = textures.uniforms;
124
90
  const sunDir = { value: new three.Vector3(1, 0, 0) };
125
91
  const grid = sphere(three, 128, 64);
126
92
  const surface = new three.ShaderMaterial({
127
93
  vertexShader: SURFACE_VERT,
128
94
  fragmentShader: SURFACE_FRAG,
129
- uniforms: { dayMap: slots.get("day").uniform, nightMap: slots.get("night").uniform, waterMap: slots.get("water").uniform, sunDir, lights: { value: 1 } },
95
+ uniforms: { dayMap: uniform.day, nightMap: uniform.night, waterMap: uniform.water, sunDir, lights: { value: 1 }, ortho: { value: 1 } },
130
96
  });
131
97
  const cloudShift = { value: 0 };
132
98
  const cloudMaterial = new three.ShaderMaterial({
133
99
  vertexShader: SURFACE_VERT,
134
100
  fragmentShader: CLOUD_FRAG,
135
- uniforms: { cloudMap: slots.get("clouds").uniform, sunDir, shift: cloudShift, opacity: { value: 0.9 } },
101
+ uniforms: { cloudMap: uniform.clouds, sunDir, shift: cloudShift, opacity: { value: 0.9 } },
136
102
  transparent: true,
137
103
  depthWrite: false,
138
104
  });
139
105
  const atmosphere = new three.ShaderMaterial({
140
106
  vertexShader: ATMOSPHERE_VERT,
141
107
  fragmentShader: ATMOSPHERE_FRAG,
142
- uniforms: { sunDir, outer: { value: ATMOSPHERE_OUTER } },
108
+ uniforms: { sunView: sunDir, outer: { value: ATMOSPHERE_OUTER }, ortho: { value: 1 } },
143
109
  transparent: true,
144
110
  depthWrite: false,
145
111
  side: three.BackSide,
@@ -158,60 +124,6 @@ export function createEarthItem(model, deps) {
158
124
  scene.add(turned, glow);
159
125
  const camera = new three.OrthographicCamera(-1, 1, 1, -1, 0.1, 20);
160
126
  camera.position.set(0, 0, 5);
161
- const setUniformTexture = (slot, entry, texels) => {
162
- const previous = slot.entry;
163
- slot.entry = entry;
164
- slot.width = texels;
165
- slot.uniform.value = entry.texture;
166
- if (previous !== slot.base)
167
- letGo(slot.layer, previous.width);
168
- deps.onTier?.(slot.layer, texels);
169
- };
170
- const moveTo = (slot, texels) => {
171
- if (disposed || texels === slot.width || slot.loading === texels)
172
- return;
173
- if (texels === slot.baseWidth) {
174
- setUniformTexture(slot, slot.base, texels);
175
- deps.invalidate();
176
- return;
177
- }
178
- slot.loading = texels;
179
- const entry = makeTexture(slot.layer, texels);
180
- entry.ready.then(() => {
181
- if (disposed || slot.loading !== texels) {
182
- letGo(slot.layer, texels);
183
- return;
184
- }
185
- slot.loading = undefined;
186
- setUniformTexture(slot, entry, texels);
187
- deps.invalidate();
188
- }, (error) => {
189
- letGo(slot.layer, texels);
190
- if (slot.loading === texels)
191
- slot.loading = undefined;
192
- deps.onError?.(error);
193
- });
194
- };
195
- const syncTiers = (radiusCssPx) => {
196
- const radius = radiusCssPx * model.ratio();
197
- for (const slot of slots.values()) {
198
- const wanted = chooseTier(slot.layer, radius, deps.cap());
199
- if (slot.timer !== undefined && wanted >= slot.width) {
200
- clearTimeout(slot.timer);
201
- slot.timer = undefined;
202
- }
203
- if (wanted > slot.width)
204
- moveTo(slot, wanted);
205
- else if (wanted < slot.width && slot.timer === undefined) {
206
- slot.timer = setTimeout(() => {
207
- slot.timer = undefined;
208
- const now = chooseTier(slot.layer, discRadius(size.width, size.height, model.view.zoom) * model.ratio(), deps.cap());
209
- if (now < slot.width)
210
- moveTo(slot, now);
211
- }, DOWNGRADE_AFTER_MS);
212
- }
213
- }
214
- };
215
127
  return {
216
128
  scene,
217
129
  camera,
@@ -231,7 +143,7 @@ export function createEarthItem(model, deps) {
231
143
  camera.bottom = (-size.height / 2 + view.panY) / radius;
232
144
  camera.updateProjectionMatrix();
233
145
  model.drawn = { view: { ...view }, time: model.time, radius, centre: { ...model.centre } };
234
- syncTiers(radius);
146
+ textures.sync(radius * model.ratio());
235
147
  deps.onPose?.({ lon: look.lon, lat: look.lat, radius, cx: size.width / 2 + view.panX, cy: size.height / 2 + view.panY, width: size.width, height: size.height });
236
148
  return false;
237
149
  },
@@ -239,12 +151,7 @@ export function createEarthItem(model, deps) {
239
151
  size = { width: next, height: nextHeight };
240
152
  },
241
153
  dispose() {
242
- disposed = true;
243
- for (const slot of slots.values())
244
- if (slot.timer !== undefined)
245
- clearTimeout(slot.timer);
246
- for (const key of held.splice(0))
247
- deps.assets.release(key);
154
+ textures.dispose();
248
155
  grid.dispose();
249
156
  surface.dispose();
250
157
  cloudMaterial.dispose();
@@ -1,13 +1,49 @@
1
1
  import type { GeoPoint, GlobeView } from "../types.ts";
2
- import { type ThreeParts } from "./earth.ts";
2
+ import { setThreeLoader } from "./shared.ts";
3
+ export { setThreeLoader };
3
4
  import type { OverlayHost } from "../markers/layer.ts";
4
5
  import { type Layer } from "./tiers.ts";
5
- type Three = ThreeParts;
6
+ type EarthLayer = Extract<Layer, "day" | "night" | "clouds" | "water">;
7
+ /**
8
+ * What the realistic Earth is doing: `loading` its pictures, `live` and drawing, `unavailable` (no WebGL), `lost` (the GPU took the context; it comes back by itself) or `idle` (another view has the page's one WebGL context; press or focus it to take it).
9
+ *
10
+ * @example
11
+ * ```ts no-run
12
+ * import type { ChikyuEarthRealistic, EarthState } from "@johnmorrisdotca/chikyu-earth/realistic/element";
13
+ *
14
+ * const describe = (earth: ChikyuEarthRealistic): EarthState => earth.stats.state;
15
+ * console.log(typeof describe);
16
+ * ```
17
+ */
6
18
  export type EarthState = "loading" | "live" | "unavailable" | "lost" | "idle";
7
- /** A `time` attribute: "now" (or none) follows the clock, else an ISO date such as 2026-06-21T12:00:00Z. */
19
+ /**
20
+ * A `time` attribute: "now" (or none) follows the clock, else an ISO date such as 2026-06-21T12:00:00Z.
21
+ *
22
+ * @param value - The attribute's text, or `null` when it is not there.
23
+ * @returns The moment in milliseconds since the Unix epoch, or `"now"` when the value is empty, `"now"` or not a date.
24
+ *
25
+ * @example
26
+ * ```ts no-run
27
+ * import { readTime } from "@johnmorrisdotca/chikyu-earth/realistic/element";
28
+ *
29
+ * console.log(readTime("2026-06-21T12:00:00Z"), readTime(null), readTime("tomorrow"));
30
+ * ```
31
+ */
8
32
  export declare const readTime: (value: string | null) => number | "now";
9
- /** Where three.js comes from, if a page wants to hand it over itself (before the first element connects). */
10
- export declare function setThreeLoader(load: () => Promise<Three>): void;
33
+ /**
34
+ * 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.
35
+ *
36
+ * @example
37
+ * ```ts no-run
38
+ * import type { ChikyuEarthRealistic } from "@johnmorrisdotca/chikyu-earth/realistic/element";
39
+ *
40
+ * const earth = document.createElement("chikyu-earth-realistic") as ChikyuEarthRealistic;
41
+ * earth.setAttribute("center", "139.7,35.7");
42
+ * earth.setAttribute("time", "2026-06-21T03:00Z");
43
+ * earth.setAttribute("texture-base", "/textures/");
44
+ * document.body.append(earth);
45
+ * ```
46
+ */
11
47
  export declare class ChikyuEarthRealistic extends HTMLElement {
12
48
  #private;
13
49
  static observedAttributes: string[];
@@ -29,7 +65,7 @@ export declare class ChikyuEarthRealistic extends HTMLElement {
29
65
  /** What the element is doing, for a page or a test: its state, the texture widths held, the draws made. */
30
66
  get stats(): {
31
67
  state: EarthState;
32
- tiers: Record<Layer, number>;
68
+ tiers: Record<EarthLayer, number>;
33
69
  draws: number;
34
70
  contexts: number;
35
71
  canvas: {
@@ -50,4 +86,8 @@ export declare class ChikyuEarthRealistic extends HTMLElement {
50
86
  /** The view as it is (for a page that wants to read it back). */
51
87
  get view(): GlobeView;
52
88
  }
53
- export {};
89
+ declare global {
90
+ interface HTMLElementTagNameMap {
91
+ "chikyu-earth-realistic": ChikyuEarthRealistic;
92
+ }
93
+ }
@@ -1,9 +1,7 @@
1
- // <chikyu-earth-realistic>: the Earth from space as a web component, drawn by kazoku-ui's shared 3D renderer (one WebGL context for the
2
- // page) with three.js, which is the page's own. Optional entry point: the flat `<chikyu-earth>` needs neither. It draws only when something
3
- // changes, at most 30 times a second, and nothing at all while it is out of sight or the tab is hidden.
4
- import { createSharedRenderer } from "@johnmorrisdotca/kazoku-ui/renderer";
5
1
  import { createEarthItem, lookAt, newModel } from "./earth.js";
6
2
  import { createGate, frameCap } from "./gate.js";
3
+ import { setThreeLoader, takeRenderer } from "./shared.js";
4
+ export { setThreeLoader };
7
5
  import { MAX_TEXTURE, TIERS } from "./tiers.js";
8
6
  const RAD = Math.PI / 180;
9
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. */
@@ -17,33 +15,39 @@ const readCentre = (value) => {
17
15
  return null;
18
16
  return { lon: lon, lat: lat };
19
17
  };
20
- /** A `time` attribute: "now" (or none) follows the clock, else an ISO date such as 2026-06-21T12:00:00Z. */
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
+ */
21
31
  export const readTime = (value) => {
22
32
  if (value === null || value === "" || value === "now")
23
33
  return "now";
24
34
  const ms = Date.parse(value);
25
35
  return Number.isFinite(ms) ? ms : "now";
26
36
  };
27
- // --- one renderer for the page ----------------------------------------------------------------------------------------------------------------------
28
- let shared;
29
- let loader = () => import("three");
30
- const contextListeners = new Set();
31
- let elements = 0;
32
- let releaseTimer;
33
- /** Where three.js comes from, if a page wants to hand it over itself (before the first element connects). */
34
- export function setThreeLoader(load) {
35
- loader = load;
36
- }
37
- function renderer() {
38
- shared ??= createSharedRenderer({
39
- load: () => loader(),
40
- onContext: (state) => {
41
- for (const listener of contextListeners)
42
- listener(state);
43
- },
44
- });
45
- return shared;
46
- }
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
+ */
47
51
  export class ChikyuEarthRealistic extends HTMLElement {
48
52
  static observedAttributes = ["center", "time", "clouds", "lights", "live-seconds"];
49
53
  #model = newModel();
@@ -53,6 +57,7 @@ export class ChikyuEarthRealistic extends HTMLElement {
53
57
  #tick;
54
58
  #state = "loading";
55
59
  #tiers = { day: TIERS.day[0], night: TIERS.night[0], clouds: TIERS.clouds[0], water: TIERS.water[0] };
60
+ #taken;
56
61
  #stage;
57
62
  #overlay;
58
63
  #pose = null;
@@ -65,8 +70,6 @@ export class ChikyuEarthRealistic extends HTMLElement {
65
70
  #lost = () => this.#set("lost");
66
71
  #restored = () => this.#set("live");
67
72
  connectedCallback() {
68
- clearTimeout(releaseTimer);
69
- elements += 1;
70
73
  this.#model.centre = readCentre(this.getAttribute("center")) ?? this.#model.centre;
71
74
  this.#mode = readTime(this.getAttribute("time"));
72
75
  this.#model.time = this.#mode === "now" ? Date.now() : this.#mode;
@@ -89,9 +92,10 @@ export class ChikyuEarthRealistic extends HTMLElement {
89
92
  if (!battery.charging && battery.level <= 0.2)
90
93
  this.#gate?.setMinMs(1000 / Math.min(cap, 15));
91
94
  }, () => undefined);
92
- contextListeners.add(this.#onContext);
95
+ const taken = takeRenderer(this.#onContext);
96
+ this.#taken = taken;
93
97
  const item = createEarthItem(this.#model, {
94
- assets: renderer().assets,
98
+ assets: taken.renderer.assets,
95
99
  textureBase: () => this.#textureBase(),
96
100
  cap: () => this.#textureCap(),
97
101
  drift: 1.5,
@@ -103,11 +107,12 @@ export class ChikyuEarthRealistic extends HTMLElement {
103
107
  },
104
108
  onError: (error) => this.#fail(error instanceof Error ? error.message : String(error)),
105
109
  onTier: (layer, width) => {
106
- this.#tiers[layer] = width;
110
+ if (layer in this.#tiers)
111
+ this.#tiers[layer] = width;
107
112
  this.dispatchEvent(new CustomEvent("chikyu-tier", { detail: { layer, width } }));
108
113
  },
109
114
  });
110
- this.#view = renderer().view(this.#stage, item, { still: null });
115
+ this.#view = taken.renderer.view(this.#stage, item, { still: null });
111
116
  this.#set("loading");
112
117
  void this.#view.focus().then((live) => {
113
118
  if (live) {
@@ -115,7 +120,7 @@ export class ChikyuEarthRealistic extends HTMLElement {
115
120
  this.dispatchEvent(new CustomEvent("chikyu-ready"));
116
121
  }
117
122
  else if (this.#state === "loading")
118
- this.#set(this.#view?.live === false && renderer().focused !== undefined ? "idle" : "unavailable");
123
+ this.#set(this.#view?.live === false && taken.renderer.focused !== undefined ? "idle" : "unavailable");
119
124
  });
120
125
  this.addEventListener("pointerdown", this.#take);
121
126
  this.addEventListener("focusin", this.#take);
@@ -130,16 +135,8 @@ export class ChikyuEarthRealistic extends HTMLElement {
130
135
  this.#gate?.cancel();
131
136
  this.#view?.dispose();
132
137
  this.#view = undefined;
133
- contextListeners.delete(this.#onContext);
134
- elements -= 1;
135
- if (elements <= 0) {
136
- releaseTimer = setTimeout(() => {
137
- if (elements <= 0) {
138
- shared?.dispose();
139
- shared = undefined;
140
- }
141
- }, 1000);
142
- }
138
+ this.#taken?.release();
139
+ this.#taken = undefined;
143
140
  }
144
141
  attributeChangedCallback(name, _old, value) {
145
142
  if (name === "center")
@@ -203,8 +200,15 @@ export class ChikyuEarthRealistic extends HTMLElement {
203
200
  }
204
201
  /** What the element is doing, for a page or a test: its state, the texture widths held, the draws made. */
205
202
  get stats() {
206
- const stats = renderer().stats;
207
- return { state: this.#state, tiers: { ...this.#tiers }, draws: stats.draws, contexts: stats.contexts, canvas: stats.canvas, assets: { entries: stats.assets.entries, refs: stats.assets.refs } };
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
+ };
208
212
  }
209
213
  /**
210
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,
@@ -4,6 +4,18 @@ export interface Timers {
4
4
  now(): number;
5
5
  }
6
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
+ */
7
19
  export interface Gate {
8
20
  /** Ask for `fire` to run: now if the last run was `minMs` ago or more, else once, when that time has passed. */
9
21
  request(): void;
@@ -12,8 +24,40 @@ export interface Gate {
12
24
  /** Change the least time between runs (a lower frame rate on a battery). */
13
25
  setMinMs(minMs: number): void;
14
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
+ */
15
46
  export declare function createGate(minMs: number, fire: () => void, timers?: Timers): Gate;
16
- /** The frame rate cap for this device: `maxFps` (30 by default), lower on a small device. */
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
+ */
17
61
  export declare function frameCap(maxFps?: number, device?: {
18
62
  deviceMemory?: number;
19
63
  hardwareConcurrency?: number;
@@ -4,6 +4,25 @@ export const browserTimers = {
4
4
  clear: (handle) => clearTimeout(handle),
5
5
  now: () => performance.now(),
6
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
+ */
7
26
  export function createGate(minMs, fire, timers = browserTimers) {
8
27
  let least = minMs;
9
28
  let last = -Infinity;
@@ -33,7 +52,20 @@ export function createGate(minMs, fire, timers = browserTimers) {
33
52
  },
34
53
  };
35
54
  }
36
- /** The frame rate cap for this device: `maxFps` (30 by default), lower on a small device. */
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
+ */
37
69
  export function frameCap(maxFps = 30, device = {}) {
38
70
  const small = (device.deviceMemory !== undefined && device.deviceMemory <= 2) || (device.hardwareConcurrency !== undefined && device.hardwareConcurrency <= 2);
39
71
  return Math.max(1, small ? Math.min(maxFps, 20) : maxFps);
@@ -1,5 +1,10 @@
1
- export declare const SURFACE_VERT = "\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvoid main() {\n vUv = uv;\n vNormal = normalize(mat3(modelMatrix) * position);\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;\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvoid main() {\n vec3 n = normalize(vNormal);\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 + vec3(0.0, 0.0, 1.0));\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(n.z, 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}";
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
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 vView;\nvoid main() {\n vec4 view = modelViewMatrix * vec4(position, 1.0);\n vView = view.xyz;\n gl_Position = projectionMatrix * view;\n}";
5
- export declare const ATMOSPHERE_FRAG = "\nuniform vec3 sunDir;\nuniform float outer;\nvarying vec3 vView;\nvoid main() {\n float rho = length(vView.xy);\n float t = clamp((rho - 1.0) / (outer - 1.0), 0.0, 1.0);\n vec2 dir = rho > 0.0 ? vView.xy / rho : vec2(1.0, 0.0);\n float side = smoothstep(-0.40, 0.55, dot(vec3(dir, 0.0), sunDir));\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}";
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}";