@johnmorrisdotca/wakusei-planets 0.0.0-stage → 0.3.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 (114) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +48 -0
  4. package/README.md +214 -2
  5. package/dist/bodies/all.d.ts +16 -0
  6. package/dist/bodies/all.js +16 -0
  7. package/dist/bodies/earth.d.ts +1 -0
  8. package/dist/bodies/earth.js +23 -0
  9. package/dist/bodies/jupiter.d.ts +1 -0
  10. package/dist/bodies/jupiter.js +22 -0
  11. package/dist/bodies/mars.d.ts +1 -0
  12. package/dist/bodies/mars.js +25 -0
  13. package/dist/bodies/mercury.d.ts +1 -0
  14. package/dist/bodies/mercury.js +9 -0
  15. package/dist/bodies/moons-earth.d.ts +1 -0
  16. package/dist/bodies/moons-earth.js +37 -0
  17. package/dist/bodies/moons-jupiter.d.ts +1 -0
  18. package/dist/bodies/moons-jupiter.js +29 -0
  19. package/dist/bodies/moons-mars.d.ts +1 -0
  20. package/dist/bodies/moons-mars.js +4 -0
  21. package/dist/bodies/moons-neptune.d.ts +1 -0
  22. package/dist/bodies/moons-neptune.js +11 -0
  23. package/dist/bodies/moons-pluto.d.ts +1 -0
  24. package/dist/bodies/moons-pluto.js +5 -0
  25. package/dist/bodies/moons-saturn.d.ts +1 -0
  26. package/dist/bodies/moons-saturn.js +34 -0
  27. package/dist/bodies/moons-uranus.d.ts +1 -0
  28. package/dist/bodies/moons-uranus.js +7 -0
  29. package/dist/bodies/neptune.d.ts +1 -0
  30. package/dist/bodies/neptune.js +19 -0
  31. package/dist/bodies/pluto.d.ts +1 -0
  32. package/dist/bodies/pluto.js +22 -0
  33. package/dist/bodies/saturn.d.ts +1 -0
  34. package/dist/bodies/saturn.js +14 -0
  35. package/dist/bodies/sun.d.ts +1 -0
  36. package/dist/bodies/sun.js +20 -0
  37. package/dist/bodies/uranus.d.ts +1 -0
  38. package/dist/bodies/uranus.js +15 -0
  39. package/dist/bodies/venus.d.ts +1 -0
  40. package/dist/bodies/venus.js +10 -0
  41. package/dist/data/orbits.d.ts +20 -0
  42. package/dist/data/orbits.js +27 -0
  43. package/dist/element.d.ts +88 -0
  44. package/dist/element.js +603 -0
  45. package/dist/ephemeris.d.ts +189 -0
  46. package/dist/ephemeris.js +340 -0
  47. package/dist/facts.d.ts +213 -0
  48. package/dist/facts.js +198 -0
  49. package/dist/gate.d.ts +37 -0
  50. package/dist/gate.js +54 -0
  51. package/dist/index.d.ts +5 -0
  52. package/dist/index.js +5 -0
  53. package/dist/line.d.ts +82 -0
  54. package/dist/line.js +280 -0
  55. package/dist/live.d.ts +42 -0
  56. package/dist/live.js +62 -0
  57. package/dist/pose.d.ts +62 -0
  58. package/dist/pose.js +69 -0
  59. package/dist/scene/body.d.ts +68 -0
  60. package/dist/scene/body.js +337 -0
  61. package/dist/scene/glsl.d.ts +13 -0
  62. package/dist/scene/glsl.js +186 -0
  63. package/dist/scene/look.d.ts +106 -0
  64. package/dist/scene/look.js +35 -0
  65. package/dist/scene/recipes.d.ts +49 -0
  66. package/dist/scene/recipes.js +76 -0
  67. package/dist/scene/rings.d.ts +34 -0
  68. package/dist/scene/rings.js +64 -0
  69. package/dist/scene/shared.d.ts +24 -0
  70. package/dist/scene/shared.js +55 -0
  71. package/dist/scene/store.d.ts +19 -0
  72. package/dist/scene/store.js +8 -0
  73. package/dist/textures/callisto-1024.webp +0 -0
  74. package/dist/textures/callisto-512.webp +0 -0
  75. package/dist/textures/enceladus-1024.webp +0 -0
  76. package/dist/textures/enceladus-512.webp +0 -0
  77. package/dist/textures/europa-1024.webp +0 -0
  78. package/dist/textures/europa-512.webp +0 -0
  79. package/dist/textures/ganymede-1024.webp +0 -0
  80. package/dist/textures/ganymede-512.webp +0 -0
  81. package/dist/textures/io-1024.webp +0 -0
  82. package/dist/textures/io-512.webp +0 -0
  83. package/dist/textures/jupiter-1024.webp +0 -0
  84. package/dist/textures/jupiter-512.webp +0 -0
  85. package/dist/textures/mars-1024.webp +0 -0
  86. package/dist/textures/mars-512.webp +0 -0
  87. package/dist/textures/mercury-1024.webp +0 -0
  88. package/dist/textures/mercury-512.webp +0 -0
  89. package/dist/textures/mimas-1024.webp +0 -0
  90. package/dist/textures/mimas-512.webp +0 -0
  91. package/dist/textures/moon-1024.webp +0 -0
  92. package/dist/textures/moon-512.webp +0 -0
  93. package/dist/textures/neptune-1024.webp +0 -0
  94. package/dist/textures/neptune-512.webp +0 -0
  95. package/dist/textures/pluto-1024.webp +0 -0
  96. package/dist/textures/pluto-512.webp +0 -0
  97. package/dist/textures/rhea-1024.webp +0 -0
  98. package/dist/textures/rhea-512.webp +0 -0
  99. package/dist/textures/saturn-1024.webp +0 -0
  100. package/dist/textures/saturn-512.webp +0 -0
  101. package/dist/textures/saturn-ring-2048.webp +0 -0
  102. package/dist/textures/sun-1024.webp +0 -0
  103. package/dist/textures/sun-512.webp +0 -0
  104. package/dist/textures/titan-1024.webp +0 -0
  105. package/dist/textures/titan-512.webp +0 -0
  106. package/dist/textures/uranus-1024.webp +0 -0
  107. package/dist/textures/uranus-512.webp +0 -0
  108. package/dist/textures/venus-1024.webp +0 -0
  109. package/dist/textures/venus-512.webp +0 -0
  110. package/dist/vec.d.ts +19 -0
  111. package/dist/vec.js +32 -0
  112. package/dist/version.d.ts +1 -0
  113. package/dist/version.js +1 -0
  114. package/package.json +236 -4
@@ -0,0 +1,603 @@
1
+ import { orbitPath, nearMoons } from "./ephemeris.js";
2
+ import { BODIES, ringReach } from "./facts.js";
3
+ import { createGate, frameCap } from "./gate.js";
4
+ import { lineSvg } from "./line.js";
5
+ import { createLive } from "./live.js";
6
+ import { VERSION } from "./version.js";
7
+ import { createBodyItem } from "./scene/body.js";
8
+ import { delegates, onRegister } from "./scene/store.js";
9
+ import { setThreeLoader, takeRenderer } from "./scene/shared.js";
10
+ import { turn as turnVec } from "./vec.js";
11
+ export { setThreeLoader };
12
+ /**
13
+ * The default home of the surface pictures: the files of this exact version on jsDelivr. Set `texture-base` on an element to serve them yourself.
14
+ *
15
+ * @example
16
+ * ```ts no-run
17
+ * import { TEXTURE_BASE } from "@johnmorrisdotca/wakusei-planets/element";
18
+ *
19
+ * console.log(TEXTURE_BASE);
20
+ * ```
21
+ */
22
+ export const TEXTURE_BASE = `https://cdn.jsdelivr.net/npm/@johnmorrisdotca/wakusei-planets@${VERSION}/dist/textures/`;
23
+ /**
24
+ * Where the sharpest surface pictures are: the files of this exact version in the repository, on jsDelivr's GitHub address. The npm package holds only the two smaller widths, so a page that
25
+ * never zooms in never fetches them. Set `texture-far-base` on an element to serve them yourself.
26
+ *
27
+ * @example
28
+ * ```ts no-run
29
+ * import { TEXTURE_FAR_BASE } from "@johnmorrisdotca/wakusei-planets/element";
30
+ *
31
+ * console.log(TEXTURE_FAR_BASE);
32
+ * ```
33
+ */
34
+ export const TEXTURE_FAR_BASE = `https://cdn.jsdelivr.net/gh/johnmorrisdotca/wakusei-planets@v${VERSION}/textures/`;
35
+ const num = (value, fallback) => {
36
+ if (value === null || value.trim() === "")
37
+ return fallback;
38
+ const n = Number(value);
39
+ return Number.isFinite(n) ? n : fallback;
40
+ };
41
+ const readLook = (value) => (value === "realistic" || value === "painted" ? value : "line");
42
+ const readTime = (value) => {
43
+ if (value === null || value === "")
44
+ return undefined;
45
+ if (value === "now")
46
+ return "now";
47
+ const ms = Date.parse(value);
48
+ return Number.isFinite(ms) ? ms : undefined;
49
+ };
50
+ /** The half-width of the picture, in body radii, for a body alone, with a scale to another body, or with its moons. */
51
+ function extentOf(id, options) {
52
+ const f = BODIES[id];
53
+ if (options.moons === true && id in BODIES && f.parent === undefined && id !== "sun") {
54
+ const near = nearMoons(id);
55
+ const far = Math.max(1.8, ringReach(f) * 1.15, ...near.map((m) => m.reach * 1.1));
56
+ return options.range ?? far;
57
+ }
58
+ if (options.scaleTo !== undefined)
59
+ return (BODIES[options.scaleTo].radiusKm * 1.06) / f.radiusKm;
60
+ return Math.max(id === "sun" ? 1.55 : 1.06, ringReach(f) * 1.04);
61
+ }
62
+ const TEMPLATE = `<style>
63
+ :host { display: block; width: var(--wakusei-size, 320px); max-width: 100%; aspect-ratio: 1 / 1; position: relative; contain: layout style; }
64
+ #stage { position: absolute; inset: 0; }
65
+ #stage svg { display: block; width: 100%; height: 100%; }
66
+ </style><div id="stage"></div>`;
67
+ /**
68
+ * The `<wakusei-body>` custom element: a body of the solar system, drawn in a look.
69
+ *
70
+ * Attributes: `body` (a name, such as `jupiter`), `look` (`line`, `realistic` or `painted`), `size` (pixels across), `time` (`now` or an ISO date: the body as the Earth sees it then; leave it out for a portrait),
71
+ * `tilt`, `pitch`, `phase` and `meridian` (a portrait's pose, degrees), `turn` (a portrait turns this many degrees a second), `rate` (simulated seconds for each real second), `moons` (draw the planet's moons on their orbits),
72
+ * `range` (planet radii the moons picture reaches), `scale-to` (draw to scale with another body), `live` (`always`, `hover` or `off`: when it takes the page's one WebGL context), `texture-base` (where the surface pictures are) and `lang` (`en` or `ja`).
73
+ *
74
+ * @example
75
+ * ```ts no-run
76
+ * import { WakuseiBody } from "@johnmorrisdotca/wakusei-planets/element";
77
+ * import "@johnmorrisdotca/wakusei-planets/saturn";
78
+ *
79
+ * const saturn = document.createElement("wakusei-body") as WakuseiBody;
80
+ * saturn.setAttribute("body", "saturn");
81
+ * saturn.setAttribute("look", "realistic");
82
+ * document.body.append(saturn);
83
+ * ```
84
+ */
85
+ export class WakuseiBody extends HTMLElement {
86
+ static observedAttributes = ["body", "look", "size", "time", "tilt", "pitch", "phase", "meridian", "turn", "rate", "moons", "range", "scale-to", "live", "texture-base", "texture-far-base", "lang", "labels"];
87
+ #stage;
88
+ #state = "line";
89
+ #renderer;
90
+ #release;
91
+ #view;
92
+ #viewKey = "";
93
+ #viewSig = "";
94
+ #delegateEl;
95
+ #delegateKind = "";
96
+ #stilled = false;
97
+ #wantLive = false;
98
+ #visible = typeof IntersectionObserver !== "function";
99
+ #io;
100
+ #tick;
101
+ #refreshQueued = false;
102
+ #gate;
103
+ #live = createLive();
104
+ #startedAt = 0;
105
+ #start;
106
+ #yaw = 0;
107
+ #tilt = 0;
108
+ #pitchDelta = 0;
109
+ #lastAnchors = -1;
110
+ #zoom = 1;
111
+ #panX = 0;
112
+ #panY = 0;
113
+ #orbits = new Map();
114
+ #orbitKey = "";
115
+ #textureWidth = 0;
116
+ #sceneState;
117
+ #unsub;
118
+ #reduce = false;
119
+ #hover = () => void this.goLive();
120
+ #onContext = (state) => {
121
+ if (state === "unavailable")
122
+ this.#set("unavailable");
123
+ else if (state === "lost")
124
+ this.#set("lost");
125
+ else if (this.#view?.live)
126
+ this.#set("live");
127
+ };
128
+ #onHidden = () => {
129
+ if (document.visibilityState === "hidden")
130
+ this.#stopTick();
131
+ else
132
+ this.#resume();
133
+ };
134
+ get body() {
135
+ const id = this.getAttribute("body");
136
+ return id !== null && id in BODIES ? id : "earth";
137
+ }
138
+ get look() {
139
+ return readLook(this.getAttribute("look"));
140
+ }
141
+ /** What the element is doing now. */
142
+ get state() {
143
+ return this.#state;
144
+ }
145
+ /** Numbers for a test or a status line: the state, the texture width in use and the renderer's own counts. */
146
+ get stats() {
147
+ return { state: this.#state, textureWidth: this.#textureWidth, anchors: this.#live.anchors, renderer: this.#renderer?.stats };
148
+ }
149
+ connectedCallback() {
150
+ const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
151
+ root.innerHTML = TEMPLATE;
152
+ this.#stage = root.getElementById("stage");
153
+ this.#startedAt = performance.now();
154
+ this.#reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
155
+ this.#clockFromAttributes();
156
+ if (!this.hasAttribute("role"))
157
+ this.setAttribute("role", "img");
158
+ this.#gate = createGate(1000 / frameCap(30, { deviceMemory: navigator.deviceMemory, hardwareConcurrency: navigator.hardwareConcurrency }), () => this.#frame());
159
+ if (typeof IntersectionObserver === "function") {
160
+ this.#io = new IntersectionObserver((entries) => {
161
+ const last = entries[entries.length - 1];
162
+ if (last === undefined)
163
+ return;
164
+ this.#visible = last.isIntersecting;
165
+ if (this.#visible) {
166
+ this.#still();
167
+ this.#resume();
168
+ if (this.look !== "line" && this.#view?.live !== true && this.#hasPriority())
169
+ void this.goLive();
170
+ }
171
+ else
172
+ this.#stopTick();
173
+ });
174
+ this.#io.observe(this);
175
+ }
176
+ document.addEventListener("visibilitychange", this.#onHidden);
177
+ this.#unsub = onRegister(() => this.#queue());
178
+ this.#applyHover();
179
+ this.#queue();
180
+ }
181
+ disconnectedCallback() {
182
+ this.#stopTick();
183
+ this.#gate?.cancel();
184
+ this.#io?.disconnect();
185
+ document.removeEventListener("visibilitychange", this.#onHidden);
186
+ this.#unsub?.();
187
+ this.removeEventListener("pointerenter", this.#hover);
188
+ this.removeEventListener("focusin", this.#hover);
189
+ this.removeEventListener("pointerdown", this.#hover);
190
+ this.#view?.dispose();
191
+ this.#view = undefined;
192
+ this.#viewKey = "";
193
+ this.#release?.();
194
+ this.#release = undefined;
195
+ this.#renderer = undefined;
196
+ }
197
+ attributeChangedCallback(name) {
198
+ if (!this.isConnected)
199
+ return;
200
+ if (name === "time" || name === "rate")
201
+ this.#clockFromAttributes();
202
+ if (name === "live")
203
+ this.#applyHover();
204
+ this.#lastAnchors = -1;
205
+ this.#queue();
206
+ }
207
+ /** Put the picture on the page's one WebGL context and draw it, moving if it is asked to. Resolves true when it is live. A `line` look has nothing to take and resolves false. */
208
+ async goLive() {
209
+ if (this.look === "line")
210
+ return false;
211
+ this.#wantLive = true;
212
+ if (delegates.has(this.body)) {
213
+ await this.#delegated(delegates.get(this.body));
214
+ return true;
215
+ }
216
+ await this.#ensureView();
217
+ const ok = (await this.#view?.focus()) === true;
218
+ if (ok) {
219
+ this.#set("live");
220
+ this.#sync();
221
+ this.#resume();
222
+ }
223
+ else if (this.#state !== "unavailable" && this.#state !== "lost")
224
+ this.#set("still");
225
+ return ok;
226
+ }
227
+ /** Give the WebGL context back: the picture stays as a still. */
228
+ rest() {
229
+ this.#wantLive = false;
230
+ if (delegates.has(this.body)) {
231
+ this.#queue(true);
232
+ return;
233
+ }
234
+ this.#stopTick();
235
+ if (this.#view !== undefined) {
236
+ this.#view.blur();
237
+ if (this.look !== "line" && this.#state !== "unavailable") {
238
+ this.#set("still");
239
+ this.#rekey();
240
+ }
241
+ }
242
+ }
243
+ /** The shared controllers' camera: `yaw` turns the body about its pole, `tilt` tips its top away from you as it grows (a drag up moves the surface up), `zoom` and the pan move the picture. */
244
+ setView(view) {
245
+ this.#yaw = view.yaw ?? 0;
246
+ this.#tilt = view.tilt ?? 0;
247
+ this.#zoom = view.zoom ?? 1;
248
+ this.#panX = view.panX ?? 0;
249
+ this.#panY = view.panY ?? 0;
250
+ this.#queue(true);
251
+ }
252
+ // --- the clock ------------------------------------------------------------------------------------------------------------------------------------
253
+ #clockFromAttributes() {
254
+ const t = readTime(this.getAttribute("time"));
255
+ this.#startedAt = performance.now();
256
+ this.#start = t === "now" ? Date.now() : t;
257
+ }
258
+ #request() {
259
+ const moons = this.hasAttribute("moons");
260
+ return {
261
+ id: this.body,
262
+ start: this.#start ?? (moons ? Date.now() : undefined),
263
+ startedAt: this.#startedAt,
264
+ rate: Math.max(-1e7, Math.min(1e7, num(this.getAttribute("rate"), 1))),
265
+ moons,
266
+ portrait: {
267
+ ...(this.hasAttribute("tilt") ? { tilt: num(this.getAttribute("tilt"), 0) } : {}),
268
+ ...(this.hasAttribute("pitch") ? { pitch: num(this.getAttribute("pitch"), 0) } : {}),
269
+ ...(this.hasAttribute("phase") ? { phase: num(this.getAttribute("phase"), 0) } : {}),
270
+ ...(this.hasAttribute("meridian") ? { spin: num(this.getAttribute("meridian"), 0) } : {}),
271
+ },
272
+ turn: num(this.getAttribute("turn"), 0),
273
+ };
274
+ }
275
+ /** The picture moves on its own: a turn, or time running faster than the clock. */
276
+ get #moving() {
277
+ const r = this.#request();
278
+ return !this.#reduce && (r.turn !== 0 || ((this.#start !== undefined || r.moons) && r.rate !== 1));
279
+ }
280
+ // --- the state of the scene --------------------------------------------------------------------------------------------------------------------
281
+ #sceneOf(frame) {
282
+ let { pole, meridian, light } = frame.pose;
283
+ const tiltBy = this.#pitchDelta - this.#tilt;
284
+ if (this.#yaw !== 0)
285
+ meridian = turnVec(meridian, pole, this.#yaw);
286
+ if (tiltBy !== 0) {
287
+ const x = [1, 0, 0];
288
+ pole = turnVec(pole, x, tiltBy);
289
+ meridian = turnVec(meridian, x, tiltBy);
290
+ light = turnVec(light, x, tiltBy);
291
+ }
292
+ const r = this.#request();
293
+ const scaleTo = this.getAttribute("scale-to");
294
+ const range = this.hasAttribute("range") ? num(this.getAttribute("range"), 0) : undefined;
295
+ const extent = extentOf(this.body, { ...(scaleTo !== null && scaleTo in BODIES ? { scaleTo } : {}), moons: r.moons, ...(range !== undefined && range > 0 ? { range } : {}) });
296
+ const moons = r.moons
297
+ ? frame.moons
298
+ .filter((m) => m.distance <= extent * 1.05)
299
+ .map((m) => {
300
+ const orbit = this.#orbits.get(m.id);
301
+ const rotated = tiltBy === 0 ? [m.x, m.y, m.z] : turnVec([m.x, m.y, m.z], [1, 0, 0], tiltBy);
302
+ return { id: m.id, x: rotated[0], y: rotated[1], z: rotated[2], ...(orbit !== undefined ? { orbit: tiltBy === 0 ? orbit : this.#tiltedOrbit(m.id, orbit, tiltBy) } : {}) };
303
+ })
304
+ : undefined;
305
+ return { pose: { pole, meridian, light }, extent, zoom: this.#zoom, panX: this.#panX, panY: this.#panY, ...(moons !== undefined ? { moons } : {}) };
306
+ }
307
+ #tilted = new Map();
308
+ #tiltedOrbit(id, orbit, tilt) {
309
+ const held = this.#tilted.get(id);
310
+ if (held !== undefined && held.source === orbit && held.tilt === tilt)
311
+ return held.out;
312
+ const out = new Float32Array(orbit.length);
313
+ for (let i = 0; i < orbit.length; i += 3) {
314
+ const v = turnVec([orbit[i], orbit[i + 1], orbit[i + 2]], [1, 0, 0], tilt);
315
+ out[i] = v[0];
316
+ out[i + 1] = v[1];
317
+ out[i + 2] = v[2];
318
+ }
319
+ this.#tilted.set(id, { source: orbit, tilt, out });
320
+ return out;
321
+ }
322
+ #ensureOrbits(frame) {
323
+ const r = this.#request();
324
+ if (!r.moons || frame.time === undefined)
325
+ return;
326
+ const bucket = `${r.id}:${Math.floor(frame.time / 3_600_000)}`;
327
+ if (bucket === this.#orbitKey)
328
+ return;
329
+ this.#orbitKey = bucket;
330
+ this.#orbits.clear();
331
+ for (const m of frame.moons)
332
+ this.#orbits.set(m.id, orbitPath(m.id, frame.time));
333
+ }
334
+ #frameNow() {
335
+ const frame = this.#live.at(this.#request(), performance.now());
336
+ if (frame.anchors !== this.#lastAnchors) {
337
+ this.#lastAnchors = frame.anchors;
338
+ const b = (Math.asin(Math.max(-1, Math.min(1, frame.pose.pole[2]))) * 180) / Math.PI;
339
+ this.#pitchDelta = this.hasAttribute("pitch") && frame.time !== undefined ? Math.round((num(this.getAttribute("pitch"), 0) - b) * 10) / 10 : 0;
340
+ }
341
+ this.#ensureOrbits(frame);
342
+ return frame;
343
+ }
344
+ // --- drawing ------------------------------------------------------------------------------------------------------------------------------------
345
+ #queue(now = false) {
346
+ if (this.#refreshQueued && !now)
347
+ return;
348
+ this.#refreshQueued = true;
349
+ queueMicrotask(() => {
350
+ this.#refreshQueued = false;
351
+ if (this.isConnected)
352
+ void this.#refresh();
353
+ });
354
+ }
355
+ async #refresh() {
356
+ if (!this.isConnected)
357
+ return;
358
+ const stage = this.#stage;
359
+ this.setAttribute("aria-label", this.#label());
360
+ if (this.hasAttribute("size"))
361
+ this.style.setProperty("--wakusei-size", `${num(this.getAttribute("size"), 320)}px`);
362
+ const delegate = delegates.get(this.body);
363
+ if (delegate !== undefined) {
364
+ await this.#delegated(delegate);
365
+ return;
366
+ }
367
+ if (this.#delegateEl !== undefined) {
368
+ this.#delegateEl.remove();
369
+ this.#delegateEl = undefined;
370
+ this.#delegateKind = "";
371
+ }
372
+ const wants3d = this.look !== "line" && this.#state !== "unavailable";
373
+ if (!wants3d) {
374
+ if (this.#view !== undefined) {
375
+ this.#view.dispose();
376
+ this.#view = undefined;
377
+ this.#viewKey = "";
378
+ this.#release?.();
379
+ this.#release = undefined;
380
+ this.#renderer = undefined;
381
+ }
382
+ this.#drawLine();
383
+ if (this.look === "line")
384
+ this.#set("line");
385
+ return;
386
+ }
387
+ const sig = `${this.body}|${this.look}`;
388
+ if (this.#view !== undefined && this.#viewSig !== sig) {
389
+ if (this.#view.live)
390
+ this.#wantLive = true;
391
+ this.#view.dispose();
392
+ this.#view = undefined;
393
+ this.#viewKey = "";
394
+ }
395
+ if (this.#view === undefined || this.#viewKey === "")
396
+ await this.#ensureView();
397
+ if (this.#view?.live !== true && this.#wantLive) {
398
+ void this.goLive();
399
+ return;
400
+ }
401
+ if (this.#view?.live === true) {
402
+ this.#sync();
403
+ this.#view.invalidate();
404
+ }
405
+ else {
406
+ this.#rekey();
407
+ if (this.#wantLive || this.#hasPriority())
408
+ void this.goLive();
409
+ }
410
+ void stage;
411
+ }
412
+ #hasPriority() {
413
+ const mode = this.getAttribute("live") ?? "always";
414
+ return mode === "always" && this.#visible;
415
+ }
416
+ #applyHover() {
417
+ this.removeEventListener("pointerenter", this.#hover);
418
+ this.removeEventListener("focusin", this.#hover);
419
+ this.removeEventListener("pointerdown", this.#hover);
420
+ if ((this.getAttribute("live") ?? "always") === "hover") {
421
+ this.addEventListener("pointerenter", this.#hover);
422
+ this.addEventListener("focusin", this.#hover);
423
+ this.addEventListener("pointerdown", this.#hover);
424
+ }
425
+ }
426
+ #label() {
427
+ const f = BODIES[this.body];
428
+ const ja = this.getAttribute("lang") === "ja";
429
+ const looks = { line: ja ? "線画" : "line drawing", realistic: ja ? "リアルな絵" : "realistic drawing", painted: ja ? "表面の地図" : "painted surface" };
430
+ return ja ? `${f.name.ja}の${looks[this.look]}` : `${f.name.en}, ${looks[this.look]}`;
431
+ }
432
+ #drawLine() {
433
+ const r = this.#request();
434
+ const frame = this.#frameNow();
435
+ const scaleTo = this.getAttribute("scale-to");
436
+ const stroke = getComputedStyle(this).getPropertyValue("--wakusei-line").trim();
437
+ const paper = getComputedStyle(this).getPropertyValue("--wakusei-paper").trim();
438
+ const scene = this.#sceneOf(frame);
439
+ const size = 480;
440
+ const html = lineSvg(this.body, {
441
+ size,
442
+ pose: scene.pose,
443
+ ...(r.moons ? { moons: true, ...(frame.time !== undefined ? { time: frame.time } : {}), range: scene.extent, moonsAt: (scene.moons ?? []).map((m) => ({ id: m.id, x: m.x, y: m.y, z: m.z, distance: Math.hypot(m.x, m.y, m.z) })), orbits: new Map((scene.moons ?? []).filter((m) => m.orbit !== undefined).map((m) => [m.id, m.orbit])) } : {}),
444
+ ...(scaleTo !== null && scaleTo in BODIES ? { fill: BODIES[this.body].radiusKm / (BODIES[scaleTo].radiusKm * 1.06) } : {}),
445
+ ...(this.getAttribute("lang") === "ja" ? { lang: "ja" } : {}),
446
+ ...(this.getAttribute("labels") === "off" ? { labels: false } : {}),
447
+ ...(stroke !== "" ? { stroke } : {}),
448
+ ...(paper !== "" ? { paper } : {}),
449
+ ...(this.#zoom !== 1 ? { zoom: this.#zoom, panX: this.#panX, panY: this.#panY } : {}),
450
+ });
451
+ this.#stage.innerHTML = html;
452
+ }
453
+ #key(state) {
454
+ const q = (v) => v.map((n) => n.toFixed(2)).join(",");
455
+ const moons = (state.moons ?? []).map((m) => `${m.id}:${m.x.toFixed(1)},${m.y.toFixed(1)}`).join(";");
456
+ return `${this.body}|${this.look}|${q(state.pose.pole)}|${q(state.pose.meridian)}|${q(state.pose.light)}|${state.extent.toFixed(2)}|${state.zoom.toFixed(2)}|${state.panX.toFixed(2)},${state.panY.toFixed(2)}|${moons}`;
457
+ }
458
+ /** The renderer, the view and its first still. */
459
+ async #ensureView() {
460
+ if (this.#renderer === undefined) {
461
+ const taken = takeRenderer(this.#onContext);
462
+ this.#renderer = taken.renderer;
463
+ this.#release = taken.release;
464
+ }
465
+ if (this.#view === undefined)
466
+ this.#makeView();
467
+ }
468
+ #makeView() {
469
+ const frame = this.#frameNow();
470
+ const state = this.#sceneOf(frame);
471
+ this.#sceneState = state;
472
+ this.#viewKey = this.#key(state);
473
+ this.#viewSig = `${this.body}|${this.look}`;
474
+ this.#stilled = false;
475
+ this.#stage.innerHTML = "";
476
+ const base = () => {
477
+ const own = this.getAttribute("texture-base");
478
+ return own === null || own === "" ? TEXTURE_BASE : own.endsWith("/") ? own : `${own}/`;
479
+ };
480
+ const item = createBodyItem({
481
+ id: this.body,
482
+ look: this.look === "painted" ? "painted" : "realistic",
483
+ key: this.#viewKey,
484
+ state: () => this.#sceneState ?? state,
485
+ textureBase: base,
486
+ farBase: () => {
487
+ const own = this.getAttribute("texture-far-base");
488
+ if (own !== null && own !== "")
489
+ return own.endsWith("/") ? own : `${own}/`;
490
+ return this.hasAttribute("texture-base") ? base() : TEXTURE_FAR_BASE;
491
+ },
492
+ wantedWidth: (discPx) => discPx * Math.min(2, window.devicePixelRatio || 1) * 1.6,
493
+ redraw: () => this.#view?.invalidate(),
494
+ report: (note) => {
495
+ this.#textureWidth = note.width;
496
+ if (note.failed === true)
497
+ this.setAttribute("data-fallback", "shader");
498
+ else
499
+ this.removeAttribute("data-fallback");
500
+ },
501
+ });
502
+ this.#view = this.#renderer.view(this.#stage, item);
503
+ this.#set("loading");
504
+ if (this.#visible)
505
+ this.#still();
506
+ }
507
+ /** A body another package draws (the Earth): its line globe, or its shaded Earth once it is asked to go live. */
508
+ async #delegated(delegate) {
509
+ if (this.#view !== undefined) {
510
+ this.#view.dispose();
511
+ this.#view = undefined;
512
+ this.#viewKey = "";
513
+ }
514
+ const shaded = this.look !== "line" && (this.#wantLive || (this.#hasPriority() && (this.getAttribute("live") ?? "always") === "always"));
515
+ const kind = shaded ? "live" : "line";
516
+ const time = this.#start === undefined ? "now" : new Date(this.#start).toISOString();
517
+ const base = this.getAttribute("texture-base");
518
+ const center = this.hasAttribute("meridian") ? `${num(this.getAttribute("meridian"), 10)},20` : "10,20";
519
+ const key = `${kind}|${center}|${time}|${base ?? ""}`;
520
+ if (this.#delegateEl !== undefined && this.#delegateKind === key)
521
+ return;
522
+ const size = Math.round(this.clientWidth || 320);
523
+ const el = kind === "live" ? await delegate.live({ center, time, textureBase: base }) : await delegate.line({ center, size });
524
+ this.#delegateEl?.remove();
525
+ this.#stage.innerHTML = "";
526
+ el.style.cssText = "display:block;width:100%;height:100%;aspect-ratio:1";
527
+ this.#stage.append(el);
528
+ this.#delegateEl = el;
529
+ this.#delegateKind = key;
530
+ this.#set(kind === "live" ? "live" : this.look === "line" ? "line" : "still");
531
+ }
532
+ /** Make the view's still, once, when it is in sight. */
533
+ #still() {
534
+ if (this.#stilled || this.#view === undefined)
535
+ return;
536
+ this.#stilled = true;
537
+ void this.#view.showStill().then(() => {
538
+ if (this.#state === "loading")
539
+ this.#set("still");
540
+ });
541
+ }
542
+ /** Make a new view when what is shown has changed enough that the still would be wrong. */
543
+ #rekey() {
544
+ if (this.#renderer === undefined || this.#view === undefined || this.#view.live)
545
+ return;
546
+ const state = this.#sceneOf(this.#frameNow());
547
+ const key = this.#key(state);
548
+ if (key === this.#viewKey && this.#view.item !== undefined)
549
+ return;
550
+ this.#view.dispose();
551
+ this.#view = undefined;
552
+ this.#makeView();
553
+ }
554
+ /** Hand the live scene the current state. */
555
+ #sync() {
556
+ const scene = this.#view?.scene;
557
+ if (scene === undefined || typeof scene.apply !== "function")
558
+ return;
559
+ const state = this.#sceneOf(this.#frameNow());
560
+ this.#sceneState = state;
561
+ scene.apply(state);
562
+ this.#textureWidth = scene.textureWidth;
563
+ }
564
+ // --- moving ---------------------------------------------------------------------------------------------------------------------------------------
565
+ #frame() {
566
+ if (this.#view?.live !== true || !this.#visible || document.visibilityState === "hidden")
567
+ return;
568
+ this.#sync();
569
+ this.#view.invalidate();
570
+ if (this.#moving)
571
+ this.#tick = setTimeout(() => this.#gate?.request(), 0);
572
+ }
573
+ #resume() {
574
+ this.#stopTick();
575
+ if (this.#view?.live === true && this.#visible && this.#moving && document.visibilityState !== "hidden")
576
+ this.#gate?.request();
577
+ else if (this.look === "line" && this.#visible && this.#moving)
578
+ this.#lineTick();
579
+ }
580
+ #lineTick() {
581
+ this.#stopTick();
582
+ if (!this.#visible || document.visibilityState === "hidden" || !this.#moving || this.look !== "line")
583
+ return;
584
+ this.#drawLine();
585
+ this.#tick = setTimeout(() => this.#lineTick(), 1000 / 30);
586
+ }
587
+ #stopTick() {
588
+ clearTimeout(this.#tick);
589
+ this.#tick = undefined;
590
+ this.#gate?.cancel();
591
+ }
592
+ #set(state) {
593
+ if (state === this.#state)
594
+ return;
595
+ this.#state = state;
596
+ this.setAttribute("data-state", state);
597
+ this.dispatchEvent(new CustomEvent("wakusei-state", { detail: { state } }));
598
+ if (state === "unavailable")
599
+ this.#drawLine();
600
+ }
601
+ }
602
+ if (typeof customElements !== "undefined" && !customElements.get("wakusei-body"))
603
+ customElements.define("wakusei-body", WakuseiBody);