@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
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
export const SURFACE_VERT = /* glsl */ `
|
|
4
4
|
varying vec2 vUv;
|
|
5
5
|
varying vec3 vNormal;
|
|
6
|
+
varying vec3 vWorld;
|
|
6
7
|
void main() {
|
|
7
8
|
vUv = uv;
|
|
8
9
|
vNormal = normalize(mat3(modelMatrix) * position);
|
|
10
|
+
vWorld = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
9
11
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
10
12
|
}`;
|
|
11
13
|
export const SURFACE_FRAG = /* glsl */ `
|
|
@@ -14,10 +16,13 @@ uniform sampler2D nightMap;
|
|
|
14
16
|
uniform sampler2D waterMap;
|
|
15
17
|
uniform vec3 sunDir;
|
|
16
18
|
uniform float lights;
|
|
19
|
+
uniform float ortho;
|
|
17
20
|
varying vec2 vUv;
|
|
18
21
|
varying vec3 vNormal;
|
|
22
|
+
varying vec3 vWorld;
|
|
19
23
|
void main() {
|
|
20
24
|
vec3 n = normalize(vNormal);
|
|
25
|
+
vec3 toEye = ortho > 0.5 ? vec3(0.0, 0.0, 1.0) : normalize(cameraPosition - vWorld);
|
|
21
26
|
float ndl = dot(n, sunDir);
|
|
22
27
|
float day = smoothstep(-0.10, 0.22, ndl);
|
|
23
28
|
vec3 albedo = texture2D(dayMap, vUv).rgb;
|
|
@@ -27,7 +32,7 @@ void main() {
|
|
|
27
32
|
float dusk = smoothstep(-0.14, 0.0, ndl) * (1.0 - smoothstep(0.0, 0.30, ndl));
|
|
28
33
|
lit += vec3(0.55, 0.28, 0.14) * dusk * 0.30 * (0.35 + 0.65 * albedo.g);
|
|
29
34
|
|
|
30
|
-
vec3 h = normalize(sunDir +
|
|
35
|
+
vec3 h = normalize(sunDir + toEye);
|
|
31
36
|
float glint = pow(max(dot(n, h), 0.0), 220.0) * water * day;
|
|
32
37
|
lit += vec3(1.0, 0.93, 0.80) * glint * 0.7;
|
|
33
38
|
|
|
@@ -35,7 +40,7 @@ void main() {
|
|
|
35
40
|
night = night * night * vec3(1.6, 1.5, 1.3) * lights + night * 0.25;
|
|
36
41
|
vec3 col = mix(night, lit, day);
|
|
37
42
|
|
|
38
|
-
float rim = pow(1.0 - clamp(n
|
|
43
|
+
float rim = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), 3.0);
|
|
39
44
|
float sunSide = 0.12 + 0.88 * smoothstep(-0.25, 0.5, ndl);
|
|
40
45
|
col += vec3(0.20, 0.45, 1.0) * rim * sunSide * 0.55;
|
|
41
46
|
gl_FragColor = vec4(col, 1.0);
|
|
@@ -58,22 +63,62 @@ void main() {
|
|
|
58
63
|
gl_FragColor = vec4(col, a);
|
|
59
64
|
}`;
|
|
60
65
|
export const ATMOSPHERE_VERT = /* glsl */ `
|
|
61
|
-
varying vec3
|
|
66
|
+
varying vec3 vPosView;
|
|
67
|
+
varying vec3 vNormalView;
|
|
62
68
|
void main() {
|
|
63
69
|
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
64
|
-
|
|
70
|
+
vPosView = view.xyz;
|
|
71
|
+
vNormalView = normalize(normalMatrix * position);
|
|
65
72
|
gl_Position = projectionMatrix * view;
|
|
66
73
|
}`;
|
|
67
74
|
export const ATMOSPHERE_FRAG = /* glsl */ `
|
|
68
|
-
uniform vec3
|
|
75
|
+
uniform vec3 sunView;
|
|
69
76
|
uniform float outer;
|
|
70
|
-
|
|
77
|
+
uniform float ortho;
|
|
78
|
+
varying vec3 vPosView;
|
|
79
|
+
varying vec3 vNormalView;
|
|
71
80
|
void main() {
|
|
72
|
-
|
|
81
|
+
vec3 n = normalize(vNormalView);
|
|
82
|
+
vec3 v = ortho > 0.5 ? vec3(0.0, 0.0, 1.0) : normalize(-vPosView);
|
|
83
|
+
float nz = clamp(dot(n, v), -1.0, 1.0);
|
|
84
|
+
float rho = outer * sqrt(max(0.0, 1.0 - nz * nz));
|
|
73
85
|
float t = clamp((rho - 1.0) / (outer - 1.0), 0.0, 1.0);
|
|
74
|
-
vec2 dir =
|
|
75
|
-
float side = smoothstep(-0.40, 0.55, dot(vec3(dir, 0.0),
|
|
86
|
+
vec2 dir = normalize(n.xy + vec2(1e-5, 0.0));
|
|
87
|
+
float side = smoothstep(-0.40, 0.55, dot(vec3(dir, 0.0), sunView));
|
|
76
88
|
float glow = pow(1.0 - t, 3.0) * (0.10 + 0.90 * side);
|
|
77
89
|
vec3 col = mix(vec3(0.10, 0.22, 0.60), vec3(0.42, 0.68, 1.0), side);
|
|
78
90
|
gl_FragColor = vec4(col, glow * 0.9);
|
|
79
91
|
}`;
|
|
92
|
+
/** The Moon and other plain bodies: a picture (or a flat colour) lit by the Sun, with a trace of light on the dark side. */
|
|
93
|
+
export const BODY_FRAG = /* glsl */ `
|
|
94
|
+
uniform sampler2D map;
|
|
95
|
+
uniform vec3 tint;
|
|
96
|
+
uniform float textured;
|
|
97
|
+
uniform float ambient;
|
|
98
|
+
uniform vec3 sunDir;
|
|
99
|
+
varying vec2 vUv;
|
|
100
|
+
varying vec3 vNormal;
|
|
101
|
+
void main() {
|
|
102
|
+
vec3 n = normalize(vNormal);
|
|
103
|
+
float ndl = dot(n, sunDir);
|
|
104
|
+
vec3 albedo = textured > 0.5 ? texture2D(map, vUv).rgb : tint;
|
|
105
|
+
float lit = smoothstep(-0.04, 0.12, ndl) * (0.10 + 0.95 * pow(max(ndl, 0.0), 0.6));
|
|
106
|
+
gl_FragColor = vec4(albedo * (ambient + lit), 1.0);
|
|
107
|
+
}`;
|
|
108
|
+
/** A flat quad facing the camera: the Sun's disc and its glow. */
|
|
109
|
+
export const GLOW_VERT = /* glsl */ `
|
|
110
|
+
varying vec2 vUv;
|
|
111
|
+
void main() {
|
|
112
|
+
vUv = uv;
|
|
113
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
114
|
+
}`;
|
|
115
|
+
export const GLOW_FRAG = /* glsl */ `
|
|
116
|
+
uniform vec3 tint;
|
|
117
|
+
varying vec2 vUv;
|
|
118
|
+
void main() {
|
|
119
|
+
float r = length(vUv - 0.5) * 2.0;
|
|
120
|
+
float core = 1.0 - smoothstep(0.10, 0.14, r);
|
|
121
|
+
float halo = pow(max(0.0, 1.0 - r), 2.5);
|
|
122
|
+
vec3 col = mix(tint, vec3(1.0), core);
|
|
123
|
+
gl_FragColor = vec4(col, clamp(core + halo * 0.85, 0.0, 1.0));
|
|
124
|
+
}`;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SharedRenderer } from "@johnmorrisdotca/kazoku-ui/renderer";
|
|
2
|
+
import type { ThreeParts } from "./textures.ts";
|
|
3
|
+
type Three = ThreeParts;
|
|
4
|
+
export type ContextState = "lost" | "restored" | "unavailable";
|
|
5
|
+
/**
|
|
6
|
+
* Where three.js comes from, if a page wants to hand it over itself (before the first element connects). A page that hands over a module re-exporting only the parts the Earth and the space view use ships a few hundred kilobytes of three.js instead of all of it.
|
|
7
|
+
*
|
|
8
|
+
* @param load - Returns the parts of three.js the Earth and the space view use, as a promise.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts no-run
|
|
12
|
+
* import { setThreeLoader } from "@johnmorrisdotca/chikyu-earth/realistic/element";
|
|
13
|
+
*
|
|
14
|
+
* setThreeLoader(() => import("three"));
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function setThreeLoader(load: () => Promise<Three>): void;
|
|
18
|
+
/** The page's renderer, made on first use. Call `release()` (from the result) when the element leaves. */
|
|
19
|
+
export declare function takeRenderer(onContext: (state: ContextState) => void): {
|
|
20
|
+
renderer: SharedRenderer<Three>;
|
|
21
|
+
release(): void;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// One shared renderer for the page, whichever element draws with it: the Earth from space and the space view both take it from here, so the page keeps one
|
|
2
|
+
// WebGL context between them. The last element to leave lets it go (after a second, in case it is only moving).
|
|
3
|
+
import { createSharedRenderer } from "@johnmorrisdotca/kazoku-ui/renderer";
|
|
4
|
+
let shared;
|
|
5
|
+
let loader = () => import("three");
|
|
6
|
+
const contextListeners = new Set();
|
|
7
|
+
let elements = 0;
|
|
8
|
+
let releaseTimer;
|
|
9
|
+
/**
|
|
10
|
+
* Where three.js comes from, if a page wants to hand it over itself (before the first element connects). A page that hands over a module re-exporting only the parts the Earth and the space view use ships a few hundred kilobytes of three.js instead of all of it.
|
|
11
|
+
*
|
|
12
|
+
* @param load - Returns the parts of three.js the Earth and the space view use, as a promise.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts no-run
|
|
16
|
+
* import { setThreeLoader } from "@johnmorrisdotca/chikyu-earth/realistic/element";
|
|
17
|
+
*
|
|
18
|
+
* setThreeLoader(() => import("three"));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function setThreeLoader(load) {
|
|
22
|
+
loader = load;
|
|
23
|
+
}
|
|
24
|
+
/** The page's renderer, made on first use. Call `release()` (from the result) when the element leaves. */
|
|
25
|
+
export function takeRenderer(onContext) {
|
|
26
|
+
clearTimeout(releaseTimer);
|
|
27
|
+
elements += 1;
|
|
28
|
+
contextListeners.add(onContext);
|
|
29
|
+
shared ??= createSharedRenderer({
|
|
30
|
+
load: () => loader(),
|
|
31
|
+
onContext: (state) => {
|
|
32
|
+
for (const listener of contextListeners)
|
|
33
|
+
listener(state);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
renderer: shared,
|
|
38
|
+
release() {
|
|
39
|
+
contextListeners.delete(onContext);
|
|
40
|
+
elements -= 1;
|
|
41
|
+
if (elements <= 0) {
|
|
42
|
+
releaseTimer = setTimeout(() => {
|
|
43
|
+
if (elements <= 0) {
|
|
44
|
+
shared?.dispose();
|
|
45
|
+
shared = undefined;
|
|
46
|
+
}
|
|
47
|
+
}, 1000);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/dist/realistic/sun.d.ts
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import type { GeoPoint } from "../types.ts";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Julian day number of a moment in milliseconds since the Unix epoch.
|
|
4
|
+
*
|
|
5
|
+
* @param ms - Milliseconds since 1970-01-01T00:00Z.
|
|
6
|
+
* @returns The Julian day number, with its fraction.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { julianDay } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
11
|
+
*
|
|
12
|
+
* console.log(julianDay(Date.UTC(2000, 0, 1, 12)));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
3
15
|
export declare const julianDay: (ms: number) => number;
|
|
4
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The point where the Sun is at the zenith at `ms`: latitude is the Sun's declination, longitude runs west at 15 degrees an hour.
|
|
18
|
+
*
|
|
19
|
+
* @param ms - A moment, in milliseconds since the Unix epoch.
|
|
20
|
+
* @returns The longitude and latitude on the Earth that has the Sun straight overhead.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { subsolarPoint } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
25
|
+
*
|
|
26
|
+
* const sun = subsolarPoint(Date.UTC(2026, 5, 21, 12));
|
|
27
|
+
* console.log(sun.lat.toFixed(1), sun.lon.toFixed(1));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
5
30
|
export declare function subsolarPoint(ms: number): GeoPoint;
|
|
6
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* A point on the globe as a unit vector: +z is longitude 0 on the equator, +x east, +y north.
|
|
33
|
+
*
|
|
34
|
+
* @param point - A longitude and latitude in degrees.
|
|
35
|
+
* @returns The `[x, y, z]` of the point on a sphere of radius 1.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { unitVector } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
40
|
+
*
|
|
41
|
+
* console.log(unitVector({ lon: 90, lat: 0 }).map((n) => Math.round(n)));
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
7
44
|
export declare function unitVector(point: GeoPoint): [number, number, number];
|
package/dist/realistic/sun.js
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
const RAD = Math.PI / 180;
|
|
2
2
|
const wrap360 = (degrees) => ((degrees % 360) + 360) % 360;
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Julian day number of a moment in milliseconds since the Unix epoch.
|
|
5
|
+
*
|
|
6
|
+
* @param ms - Milliseconds since 1970-01-01T00:00Z.
|
|
7
|
+
* @returns The Julian day number, with its fraction.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { julianDay } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
12
|
+
*
|
|
13
|
+
* console.log(julianDay(Date.UTC(2000, 0, 1, 12)));
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
4
16
|
export const julianDay = (ms) => ms / 86_400_000 + 2_440_587.5;
|
|
5
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* The point where the Sun is at the zenith at `ms`: latitude is the Sun's declination, longitude runs west at 15 degrees an hour.
|
|
19
|
+
*
|
|
20
|
+
* @param ms - A moment, in milliseconds since the Unix epoch.
|
|
21
|
+
* @returns The longitude and latitude on the Earth that has the Sun straight overhead.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { subsolarPoint } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
26
|
+
*
|
|
27
|
+
* const sun = subsolarPoint(Date.UTC(2026, 5, 21, 12));
|
|
28
|
+
* console.log(sun.lat.toFixed(1), sun.lon.toFixed(1));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
6
31
|
export function subsolarPoint(ms) {
|
|
7
32
|
const jd = julianDay(ms);
|
|
8
33
|
const t = (jd - 2_451_545) / 36_525;
|
|
@@ -20,7 +45,19 @@ export function subsolarPoint(ms) {
|
|
|
20
45
|
const lon = ((wrap360(rightAscension - siderealGreenwich) + 180) % 360) - 180;
|
|
21
46
|
return { lon, lat: declination / RAD };
|
|
22
47
|
}
|
|
23
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* A point on the globe as a unit vector: +z is longitude 0 on the equator, +x east, +y north.
|
|
50
|
+
*
|
|
51
|
+
* @param point - A longitude and latitude in degrees.
|
|
52
|
+
* @returns The `[x, y, z]` of the point on a sphere of radius 1.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { unitVector } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
57
|
+
*
|
|
58
|
+
* console.log(unitVector({ lon: 90, lat: 0 }).map((n) => Math.round(n)));
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
24
61
|
export function unitVector(point) {
|
|
25
62
|
const lon = point.lon * RAD;
|
|
26
63
|
const lat = point.lat * RAD;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
import type { AssetCache } from "@johnmorrisdotca/kazoku-ui/renderer";
|
|
3
|
+
import { type Layer } from "./tiers.ts";
|
|
4
|
+
/**
|
|
5
|
+
* The parts of three.js the Earth and the space view use. A page can hand over a module that re-exports just these (see `setThreeLoader`) instead of all of three.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts no-run
|
|
9
|
+
* import type { ThreeParts } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
10
|
+
*
|
|
11
|
+
* const load = (): Promise<ThreeParts> => import("three");
|
|
12
|
+
* console.log(typeof load);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export type ThreeParts = {
|
|
16
|
+
[Name in "Texture" | "BufferGeometry" | "BufferAttribute" | "ShaderMaterial" | "Group" | "Mesh" | "Scene" | "OrthographicCamera" | "PerspectiveCamera" | "Vector3" | "WebGLRenderer" | "RepeatWrapping" | "ClampToEdgeWrapping" | "LinearMipmapLinearFilter" | "LinearFilter" | "BackSide" | "DoubleSide" | "LineLoop" | "LineBasicMaterial" | "Color"]: (typeof THREE)[Name];
|
|
17
|
+
} & {
|
|
18
|
+
/** Only the satellites use it, so a loader that hands over a subset of three.js without it still draws everything else. */
|
|
19
|
+
Points?: typeof THREE.Points;
|
|
20
|
+
};
|
|
21
|
+
/** Milliseconds a lower texture size must be enough before the larger one is let go. */
|
|
22
|
+
export declare const DOWNGRADE_AFTER_MS = 4000;
|
|
23
|
+
export interface TextureDeps {
|
|
24
|
+
assets: AssetCache;
|
|
25
|
+
/** Ends with a slash. */
|
|
26
|
+
textureBase: () => string;
|
|
27
|
+
/** The widest texture, 4096 at most. */
|
|
28
|
+
cap: () => number;
|
|
29
|
+
invalidate: () => void;
|
|
30
|
+
onError?: (error: unknown) => void;
|
|
31
|
+
onTier?: (layer: Layer, width: number) => void;
|
|
32
|
+
}
|
|
33
|
+
export interface TextureSet {
|
|
34
|
+
/** The `{ value }` a shader uniform takes, one a layer; the value is the texture now in use. */
|
|
35
|
+
readonly uniforms: Record<string, {
|
|
36
|
+
value: THREE.Texture | null;
|
|
37
|
+
}>;
|
|
38
|
+
/** Hold the width each layer needs for a disc of this many device pixels across the radius (`radiusPx[layer]`, or one number for all). */
|
|
39
|
+
sync(radiusPx: number | Partial<Record<Layer, number>>): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
/** Load the base picture of each layer, then hand back the set. Rejects (after freeing what it took) if one will not load. */
|
|
43
|
+
export declare function createTextureSet(three: ThreeParts, layers: readonly Layer[], deps: TextureDeps): Promise<TextureSet>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { chooseTier, TIERS } from "./tiers.js";
|
|
2
|
+
/** Milliseconds a lower texture size must be enough before the larger one is let go. */
|
|
3
|
+
export const DOWNGRADE_AFTER_MS = 4000;
|
|
4
|
+
/** Load the base picture of each layer, then hand back the set. Rejects (after freeing what it took) if one will not load. */
|
|
5
|
+
export async function createTextureSet(three, layers, deps) {
|
|
6
|
+
let disposed = false;
|
|
7
|
+
const held = [];
|
|
8
|
+
const keyOf = (layer, texels) => `chikyu-earth:${layer}:${texels}`;
|
|
9
|
+
const take = (layer, texels) => {
|
|
10
|
+
const key = keyOf(layer, texels);
|
|
11
|
+
const entry = deps.assets.acquire(key, () => {
|
|
12
|
+
const texture = new three.Texture();
|
|
13
|
+
texture.wrapS = three.RepeatWrapping;
|
|
14
|
+
texture.wrapT = three.ClampToEdgeWrapping;
|
|
15
|
+
texture.minFilter = three.LinearMipmapLinearFilter;
|
|
16
|
+
texture.magFilter = three.LinearFilter;
|
|
17
|
+
texture.anisotropy = 8;
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const made = { width: texels, texture, closed: false, ready: Promise.resolve(), release: () => undefined };
|
|
20
|
+
made.ready = fetch(`${deps.textureBase()}${layer}-${texels}.webp`, { signal: controller.signal })
|
|
21
|
+
.then((response) => {
|
|
22
|
+
if (!response.ok)
|
|
23
|
+
throw new Error(`${layer}-${texels}.webp: ${response.status}`);
|
|
24
|
+
return response.blob();
|
|
25
|
+
})
|
|
26
|
+
.then((blob) => createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }))
|
|
27
|
+
.then((bitmap) => {
|
|
28
|
+
if (made.closed)
|
|
29
|
+
bitmap.close();
|
|
30
|
+
else {
|
|
31
|
+
texture.image = bitmap;
|
|
32
|
+
texture.needsUpdate = true;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
made.ready.catch(() => undefined);
|
|
36
|
+
made.release = () => {
|
|
37
|
+
made.closed = true;
|
|
38
|
+
controller.abort();
|
|
39
|
+
texture.dispose();
|
|
40
|
+
texture.image?.close?.();
|
|
41
|
+
};
|
|
42
|
+
return made;
|
|
43
|
+
}, (value) => value.release());
|
|
44
|
+
held.push(key);
|
|
45
|
+
return entry;
|
|
46
|
+
};
|
|
47
|
+
const letGo = (layer, texels) => {
|
|
48
|
+
const key = keyOf(layer, texels);
|
|
49
|
+
const at = held.indexOf(key);
|
|
50
|
+
if (at >= 0)
|
|
51
|
+
held.splice(at, 1);
|
|
52
|
+
deps.assets.release(key);
|
|
53
|
+
};
|
|
54
|
+
const slots = new Map();
|
|
55
|
+
for (const layer of layers) {
|
|
56
|
+
const baseWidth = TIERS[layer][0];
|
|
57
|
+
const base = take(layer, baseWidth);
|
|
58
|
+
slots.set(layer, { layer, uniform: { value: null }, base, baseWidth, entry: base, width: baseWidth, loading: undefined, timer: undefined });
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await Promise.all([...slots.values()].map((slot) => slot.base.ready));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
for (const slot of slots.values())
|
|
65
|
+
letGo(slot.layer, slot.baseWidth);
|
|
66
|
+
deps.onError?.(error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
for (const slot of slots.values())
|
|
70
|
+
slot.uniform.value = slot.base.texture;
|
|
71
|
+
const swap = (slot, entry, texels) => {
|
|
72
|
+
const previous = slot.entry;
|
|
73
|
+
slot.entry = entry;
|
|
74
|
+
slot.width = texels;
|
|
75
|
+
slot.uniform.value = entry.texture;
|
|
76
|
+
if (previous !== slot.base)
|
|
77
|
+
letGo(slot.layer, previous.width);
|
|
78
|
+
deps.onTier?.(slot.layer, texels);
|
|
79
|
+
};
|
|
80
|
+
const moveTo = (slot, texels) => {
|
|
81
|
+
if (disposed || texels === slot.width || slot.loading === texels)
|
|
82
|
+
return;
|
|
83
|
+
if (texels === slot.baseWidth) {
|
|
84
|
+
swap(slot, slot.base, texels);
|
|
85
|
+
deps.invalidate();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
slot.loading = texels;
|
|
89
|
+
const entry = take(slot.layer, texels);
|
|
90
|
+
entry.ready.then(() => {
|
|
91
|
+
if (disposed || slot.loading !== texels) {
|
|
92
|
+
letGo(slot.layer, texels);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
slot.loading = undefined;
|
|
96
|
+
swap(slot, entry, texels);
|
|
97
|
+
deps.invalidate();
|
|
98
|
+
}, (error) => {
|
|
99
|
+
letGo(slot.layer, texels);
|
|
100
|
+
if (slot.loading === texels)
|
|
101
|
+
slot.loading = undefined;
|
|
102
|
+
deps.onError?.(error);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
let lastRadius = 0;
|
|
106
|
+
const wantedFor = (slot, radius) => {
|
|
107
|
+
const px = typeof radius === "number" ? radius : (radius[slot.layer] ?? 0);
|
|
108
|
+
return chooseTier(slot.layer, px, deps.cap());
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
uniforms: Object.fromEntries([...slots].map(([layer, slot]) => [layer, slot.uniform])),
|
|
112
|
+
sync(radiusPx) {
|
|
113
|
+
lastRadius = radiusPx;
|
|
114
|
+
for (const slot of slots.values()) {
|
|
115
|
+
const wanted = wantedFor(slot, radiusPx);
|
|
116
|
+
if (slot.timer !== undefined && wanted >= slot.width) {
|
|
117
|
+
clearTimeout(slot.timer);
|
|
118
|
+
slot.timer = undefined;
|
|
119
|
+
}
|
|
120
|
+
if (wanted > slot.width)
|
|
121
|
+
moveTo(slot, wanted);
|
|
122
|
+
else if (wanted < slot.width && slot.timer === undefined) {
|
|
123
|
+
slot.timer = setTimeout(() => {
|
|
124
|
+
slot.timer = undefined;
|
|
125
|
+
const now = wantedFor(slot, lastRadius);
|
|
126
|
+
if (now < slot.width)
|
|
127
|
+
moveTo(slot, now);
|
|
128
|
+
}, DOWNGRADE_AFTER_MS);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
dispose() {
|
|
133
|
+
disposed = true;
|
|
134
|
+
for (const slot of slots.values())
|
|
135
|
+
if (slot.timer !== undefined)
|
|
136
|
+
clearTimeout(slot.timer);
|
|
137
|
+
for (const key of held.splice(0))
|
|
138
|
+
deps.assets.release(key);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,20 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* One of the pictures the realistic Earth and the space view draw with: `day`, `night`, `clouds`, `water` and `moon`.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { TIERS, type Layer } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
7
|
+
*
|
|
8
|
+
* const layer: Layer = "clouds";
|
|
9
|
+
* console.log(TIERS[layer]);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export type Layer = "day" | "night" | "clouds" | "water" | "moon";
|
|
13
|
+
/**
|
|
14
|
+
* The widths each layer is shipped in (every picture is 2:1, so the height is half).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { TIERS } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
19
|
+
*
|
|
20
|
+
* console.log(TIERS.day);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
3
23
|
export declare const TIERS: Record<Layer, readonly number[]>;
|
|
4
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The most pixels across a texture the package ever asks the GPU for.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { MAX_TEXTURE } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
30
|
+
*
|
|
31
|
+
* console.log(MAX_TEXTURE);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
5
34
|
export declare const MAX_TEXTURE = 4096;
|
|
6
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Texels across the whole circumference wanted per device pixel of the globe's radius: 3 is a little under 2 pi, so a texel is a little larger than a pixel.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { TEXELS_PER_RADIUS_PIXEL } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
41
|
+
*
|
|
42
|
+
* console.log(TEXELS_PER_RADIUS_PIXEL);
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
7
45
|
export declare const TEXELS_PER_RADIUS_PIXEL = 3;
|
|
8
46
|
/**
|
|
9
47
|
* The width to hold for a layer when the globe's disc has radius `radiusPx` device pixels: the smallest shipped width that gives
|
|
10
48
|
* `TEXELS_PER_RADIUS_PIXEL` texels per pixel, else the largest, never more than `cap`, never less than the smallest.
|
|
49
|
+
*
|
|
50
|
+
* @param layer - Which picture.
|
|
51
|
+
* @param radiusPx - The disc's radius in device pixels.
|
|
52
|
+
* @param cap - The most pixels across to ask for; {@link MAX_TEXTURE} by default.
|
|
53
|
+
* @returns One of the widths in {@link TIERS} for that layer.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { chooseTier } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
58
|
+
*
|
|
59
|
+
* console.log(chooseTier("day", 200), chooseTier("day", 900), chooseTier("day", 900, 2048));
|
|
60
|
+
* ```
|
|
11
61
|
*/
|
|
12
62
|
export declare function chooseTier(layer: Layer, radiusPx: number, cap?: number): number;
|
|
13
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* The cap for this device: low-memory phones stop at 2048, and the GPU's own limit and the option are never passed.
|
|
65
|
+
*
|
|
66
|
+
* @param options - `max` (the page's own limit), `gpuMax` (the GPU's `MAX_TEXTURE_SIZE`) and `deviceMemory` (gigabytes, from `navigator.deviceMemory`); each may be left out.
|
|
67
|
+
* @returns The most pixels across any picture may have on this device.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { textureCap } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
72
|
+
*
|
|
73
|
+
* console.log(textureCap(), textureCap({ deviceMemory: 2 }), textureCap({ gpuMax: 1024 }));
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
14
76
|
export declare function textureCap(options?: {
|
|
15
77
|
max?: number;
|
|
16
78
|
gpuMax?: number;
|
|
17
79
|
deviceMemory?: number;
|
|
18
80
|
}): number;
|
|
19
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Bytes of GPU memory a texture of this width costs, with its mipmaps, at 4 bytes a pixel (the most it can be).
|
|
83
|
+
*
|
|
84
|
+
* @param width - Pixels across; the picture is 2:1.
|
|
85
|
+
* @returns The bytes the GPU holds for it.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { textureBytes } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
90
|
+
*
|
|
91
|
+
* console.log(textureBytes(4096));
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
20
94
|
export declare const textureBytes: (width: number) => number;
|
package/dist/realistic/tiers.js
CHANGED
|
@@ -1,30 +1,94 @@
|
|
|
1
1
|
// Which size of each picture to hold on the GPU. Small first, sharper on zoom, and never past a cap. Pure.
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The widths each layer is shipped in (every picture is 2:1, so the height is half).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { TIERS } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
8
|
+
*
|
|
9
|
+
* console.log(TIERS.day);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
3
12
|
export const TIERS = {
|
|
4
13
|
day: [1024, 2048, 4096],
|
|
5
14
|
night: [1024, 2048],
|
|
6
15
|
clouds: [1024, 2048],
|
|
7
16
|
water: [1024, 2048],
|
|
17
|
+
moon: [1024, 2048],
|
|
8
18
|
};
|
|
9
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The most pixels across a texture the package ever asks the GPU for.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { MAX_TEXTURE } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
25
|
+
*
|
|
26
|
+
* console.log(MAX_TEXTURE);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
10
29
|
export const MAX_TEXTURE = 4096;
|
|
11
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Texels across the whole circumference wanted per device pixel of the globe's radius: 3 is a little under 2 pi, so a texel is a little larger than a pixel.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { TEXELS_PER_RADIUS_PIXEL } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
36
|
+
*
|
|
37
|
+
* console.log(TEXELS_PER_RADIUS_PIXEL);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
12
40
|
export const TEXELS_PER_RADIUS_PIXEL = 3;
|
|
13
41
|
/**
|
|
14
42
|
* The width to hold for a layer when the globe's disc has radius `radiusPx` device pixels: the smallest shipped width that gives
|
|
15
43
|
* `TEXELS_PER_RADIUS_PIXEL` texels per pixel, else the largest, never more than `cap`, never less than the smallest.
|
|
44
|
+
*
|
|
45
|
+
* @param layer - Which picture.
|
|
46
|
+
* @param radiusPx - The disc's radius in device pixels.
|
|
47
|
+
* @param cap - The most pixels across to ask for; {@link MAX_TEXTURE} by default.
|
|
48
|
+
* @returns One of the widths in {@link TIERS} for that layer.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { chooseTier } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
53
|
+
*
|
|
54
|
+
* console.log(chooseTier("day", 200), chooseTier("day", 900), chooseTier("day", 900, 2048));
|
|
55
|
+
* ```
|
|
16
56
|
*/
|
|
17
57
|
export function chooseTier(layer, radiusPx, cap = MAX_TEXTURE) {
|
|
18
58
|
const widths = TIERS[layer].filter((width) => width <= Math.max(cap, TIERS[layer][0]));
|
|
19
59
|
const wanted = radiusPx * TEXELS_PER_RADIUS_PIXEL;
|
|
20
60
|
return widths.find((width) => width >= wanted) ?? widths[widths.length - 1];
|
|
21
61
|
}
|
|
22
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The cap for this device: low-memory phones stop at 2048, and the GPU's own limit and the option are never passed.
|
|
64
|
+
*
|
|
65
|
+
* @param options - `max` (the page's own limit), `gpuMax` (the GPU's `MAX_TEXTURE_SIZE`) and `deviceMemory` (gigabytes, from `navigator.deviceMemory`); each may be left out.
|
|
66
|
+
* @returns The most pixels across any picture may have on this device.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { textureCap } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
71
|
+
*
|
|
72
|
+
* console.log(textureCap(), textureCap({ deviceMemory: 2 }), textureCap({ gpuMax: 1024 }));
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
23
75
|
export function textureCap(options = {}) {
|
|
24
76
|
let cap = Math.min(MAX_TEXTURE, options.max ?? MAX_TEXTURE, options.gpuMax ?? MAX_TEXTURE);
|
|
25
77
|
if (options.deviceMemory !== undefined && options.deviceMemory <= 2)
|
|
26
78
|
cap = Math.min(cap, 2048);
|
|
27
79
|
return cap;
|
|
28
80
|
}
|
|
29
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Bytes of GPU memory a texture of this width costs, with its mipmaps, at 4 bytes a pixel (the most it can be).
|
|
83
|
+
*
|
|
84
|
+
* @param width - Pixels across; the picture is 2:1.
|
|
85
|
+
* @returns The bytes the GPU holds for it.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { textureBytes } from "@johnmorrisdotca/chikyu-earth/realistic";
|
|
90
|
+
*
|
|
91
|
+
* console.log(textureBytes(4096));
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
30
94
|
export const textureBytes = (width) => Math.round(width * (width / 2) * 4 * (4 / 3));
|