@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.
- package/CHANGELOG.md +70 -0
- package/NOTICE.md +22 -0
- package/README.md +186 -27
- package/dist/countries.d.ts +11 -0
- package/dist/countries.js +11 -0
- package/dist/element.d.ts +18 -0
- package/dist/element.js +15 -2
- package/dist/globe.d.ts +15 -1
- package/dist/globe.js +15 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +12 -1
- package/dist/markers/cluster.d.ts +27 -0
- package/dist/markers/cluster.js +16 -0
- package/dist/markers/labels.d.ts +40 -0
- package/dist/markers/labels.js +18 -0
- package/dist/markers/layer.d.ts +80 -0
- package/dist/markers/layer.js +21 -0
- package/dist/markers/project.d.ts +63 -2
- package/dist/markers/project.js +52 -2
- package/dist/markers/types.d.ts +48 -3
- package/dist/realistic/earth.d.ts +30 -5
- package/dist/realistic/earth.js +41 -134
- package/dist/realistic/element.d.ts +47 -7
- package/dist/realistic/element.js +48 -44
- package/dist/realistic/gate.d.ts +45 -1
- package/dist/realistic/gate.js +33 -1
- package/dist/realistic/shaders.d.ts +9 -4
- package/dist/realistic/shaders.js +54 -9
- package/dist/realistic/shared.d.ts +23 -0
- package/dist/realistic/shared.js +51 -0
- package/dist/realistic/sun.d.ts +40 -3
- package/dist/realistic/sun.js +40 -3
- package/dist/realistic/textures.d.ts +43 -0
- package/dist/realistic/textures.js +141 -0
- package/dist/realistic/tiers.d.ts +80 -6
- package/dist/realistic/tiers.js +69 -5
- package/dist/rotate.d.ts +59 -4
- package/dist/rotate.js +59 -4
- package/dist/satellites/index.d.ts +3 -0
- package/dist/satellites/index.js +3 -0
- package/dist/satellites/limits.d.ts +22 -0
- package/dist/satellites/limits.js +23 -0
- package/dist/satellites/track.d.ts +50 -0
- package/dist/satellites/track.js +227 -0
- package/dist/satellites/types.d.ts +161 -0
- package/dist/satellites/types.js +3 -0
- package/dist/space/element.d.ts +179 -0
- package/dist/space/element.js +541 -0
- package/dist/space/ephemeris.d.ts +151 -0
- package/dist/space/ephemeris.js +162 -0
- package/dist/space/index.d.ts +3 -0
- package/dist/space/index.js +5 -0
- package/dist/space/satellite-layer.d.ts +53 -0
- package/dist/space/satellite-layer.js +230 -0
- package/dist/space/scale.d.ts +124 -0
- package/dist/space/scale.js +141 -0
- package/dist/space/scene.d.ts +146 -0
- package/dist/space/scene.js +320 -0
- package/dist/styles.d.ts +65 -2
- package/dist/styles.js +43 -2
- package/dist/textures/moon-1024.webp +0 -0
- package/dist/textures/moon-2048.webp +0 -0
- package/dist/types.d.ts +55 -5
- package/dist/view.d.ts +63 -4
- package/dist/view.js +52 -4
- package/package.json +71 -8
package/dist/realistic/earth.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { createTextureSet } from "./textures.js";
|
|
5
5
|
const RAD = Math.PI / 180;
|
|
6
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
10
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
95
|
+
const taken = takeRenderer(this.#onContext);
|
|
96
|
+
this.#taken = taken;
|
|
93
97
|
const item = createEarthItem(this.#model, {
|
|
94
|
-
assets: renderer
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
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
|
|
207
|
-
return {
|
|
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,
|
package/dist/realistic/gate.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/realistic/gate.js
CHANGED
|
@@ -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
|
-
/**
|
|
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 +
|
|
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
|
|
5
|
-
export declare const ATMOSPHERE_FRAG = "\nuniform vec3
|
|
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}";
|