The globe draws the poles as they are, and nothing while idle
The globe is a UV sphere: towards the poles its triangles narrow to slivers, and texture coordinates interpolated across them pinched the map into a star around each pole. Each pixel now computes its own coordinates from its exact point on the sphere, with seam-safe mipmap gradients; the map wraps horizontally and gets 8x anisotropic filtering. That showed a flaw in the day texture: NASA's mosaic has a lighter strip at its east edge near the poles, a line from each pole once drawn exactly. The day texture is now built from NASA's original Blue Marble (December 2004) by tools/globe/prepare-day-texture.py, which blends that strip into the map where both sides of the seam agree; same 4096x2048 size, provenance and hashes in SOURCES.md. globe.gl redrew 60 times a second even when nothing moved; it now draws only during camera flights, hover pulses, auto-rotation and the reader's drags, so an idle globe costs no GPU time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
620cbef219
commit
69d0a641fe
8 files changed
+205
-14
No files matched your search
@@ -200,7 +200,11 @@ supplies:
|
|||||||
- **The globe** (`wwwroot/js/globe.js`, an ES module imported by `GlobePanel`) runs on globe.gl, vendored in
|
- **The globe** (`wwwroot/js/globe.js`, an ES module imported by `GlobePanel`) runs on globe.gl, vendored in
|
||||||
`wwwroot/vendor/globe/` with its textures (`SOURCES.md` has versions, licences and hashes; the service worker never
|
`wwwroot/vendor/globe/` with its textures (`SOURCES.md` has versions, licences and hashes; the service worker never
|
||||||
precaches it). It looks at the spherical mean of the located posts in view, pulses and draws an arc to a hovered
|
precaches it). It looks at the spherical mean of the located posts in view, pulses and draws an arc to a hovered
|
||||||
post, follows the theme (day/night texture), honours `prefers-reduced-motion` and pauses while hidden. CDN-fronted
|
post, follows the theme (day/night texture), honours `prefers-reduced-motion`, and draws only while something moves
|
||||||
|
(idle, it costs no GPU time; `wake()` before anything that animates). Each pixel computes its own map coordinates
|
||||||
|
from its point on the sphere (`sharpen`), so the poles don't pinch; the map wraps horizontally and gets 8×
|
||||||
|
anisotropic filtering (`keepSharp`). The day texture is NASA's, its seam repaired by
|
||||||
|
`tools/globe/prepare-day-texture.py` (dev-only, needs Pillow). CDN-fronted
|
||||||
posts are labels per CDN on a ring at 68°N, never a place; hovering one draws its arc there and a ring where the server
|
posts are labels per CDN on a ring at 68°N, never a place; hovering one draws its arc there and a ring where the server
|
||||||
was before the CDN. The posts'
|
was before the CDN. The posts'
|
||||||
places reach it in one call per feed change; cards are matched by `data-post-id`.
|
places reach it in one call per feed change; cards are matched by `data-post-id`.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<i class="ion-md-planet"></i>
|
<i class="ion-md-planet"></i>
|
||||||
@if (stats is { Error: null } live)
|
@if (stats is { Error: null } live)
|
||||||
{
|
{
|
||||||
<span>@Fixed(live.Latitude), @Fixed(live.Longitude) · @Fixed(live.Altitude) · @live.Fps fps</span>
|
<span>@Fixed(live.Latitude), @Fixed(live.Longitude) · @Fixed(live.Altitude) · @(live.Idle ? Localizer["idle"] : $"{live.Fps} fps")</span>
|
||||||
}
|
}
|
||||||
else if (stats?.Error is { Length: > 0 })
|
else if (stats?.Error is { Length: > 0 })
|
||||||
{
|
{
|
||||||
@@ -129,9 +129,9 @@
|
|||||||
yield return NerdRow.Of(Localizer["points"], $"{stats.Points} ({stats.Posts} {Localizer["posts"]}, {stats.Servers} {Localizer["servers"]})");
|
yield return NerdRow.Of(Localizer["points"], $"{stats.Points} ({stats.Posts} {Localizer["posts"]}, {stats.Servers} {Localizer["servers"]})");
|
||||||
yield return NerdRow.Of(Localizer["CDN groups"], stats.CdnGroups > 0 ? stats.CdnGroups : null);
|
yield return NerdRow.Of(Localizer["CDN groups"], stats.CdnGroups > 0 ? stats.CdnGroups : null);
|
||||||
yield return NerdRow.Of(Localizer["hovered"], stats.Hovered is { } hovered ? $"{hovered} ({stats.Arcs} {Localizer["arcs"]}, {stats.Rings} {Localizer["rings"]})" : null);
|
yield return NerdRow.Of(Localizer["hovered"], stats.Hovered is { } hovered ? $"{hovered} ({stats.Arcs} {Localizer["arcs"]}, {stats.Rings} {Localizer["rings"]})" : null);
|
||||||
yield return NerdRow.Of("fps", stats.Paused ? Localizer["paused"] : stats.Fps.ToString());
|
yield return NerdRow.Of("fps", stats.Paused ? Localizer["paused"] : stats.Idle ? Localizer["idle: nothing moves, nothing is drawn"] : stats.Fps.ToString());
|
||||||
yield return NerdRow.Of(Localizer["auto-rotate"], stats.AutoRotate);
|
yield return NerdRow.Of(Localizer["auto-rotate"], stats.AutoRotate);
|
||||||
yield return NerdRow.Of(Localizer["texture"], stats.Texture is { } texture ? $"{texture} 4096×2048" : null);
|
yield return NerdRow.Of(Localizer["texture"], stats.Texture is { } texture ? $"{texture} 4096×2048, {stats.Anisotropy}× anisotropic" : null);
|
||||||
yield return NerdRow.Of("WebGL", stats.Renderer);
|
yield return NerdRow.Of("WebGL", stats.Renderer);
|
||||||
yield return NerdRow.Of(Localizer["error"], stats.Error);
|
yield return NerdRow.Of(Localizer["error"], stats.Error);
|
||||||
}
|
}
|
||||||
@@ -179,8 +179,10 @@
|
|||||||
public int Fps { get; set; }
|
public int Fps { get; set; }
|
||||||
public string Renderer { get; set; }
|
public string Renderer { get; set; }
|
||||||
public string Texture { get; set; }
|
public string Texture { get; set; }
|
||||||
|
public int Anisotropy { get; set; }
|
||||||
public bool AutoRotate { get; set; }
|
public bool AutoRotate { get; set; }
|
||||||
public bool Paused { get; set; }
|
public bool Paused { get; set; }
|
||||||
|
public bool Idle { get; set; }
|
||||||
public string Hovered { get; set; }
|
public string Hovered { get; set; }
|
||||||
public int Arcs { get; set; }
|
public int Arcs { get; set; }
|
||||||
public int Rings { get; set; }
|
public int Rings { get; set; }
|
||||||
|
|||||||
@@ -680,4 +680,10 @@
|
|||||||
<data name="CDN groups" xml:space="preserve">
|
<data name="CDN groups" xml:space="preserve">
|
||||||
<value>gruppi CDN</value>
|
<value>gruppi CDN</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="idle" xml:space="preserve">
|
||||||
|
<value>fermo</value>
|
||||||
|
</data>
|
||||||
|
<data name="idle: nothing moves, nothing is drawn" xml:space="preserve">
|
||||||
|
<value>fermo: nulla si muove, nulla viene disegnato</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -680,4 +680,10 @@
|
|||||||
<data name="CDN groups" xml:space="preserve">
|
<data name="CDN groups" xml:space="preserve">
|
||||||
<value>CDN groups</value>
|
<value>CDN groups</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="idle" xml:space="preserve">
|
||||||
|
<value>idle</value>
|
||||||
|
</data>
|
||||||
|
<data name="idle: nothing moves, nothing is drawn" xml:space="preserve">
|
||||||
|
<value>idle: nothing moves, nothing is drawn</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Builds wwwroot/vendor/globe/earth-blue-marble.jpg from NASA's Blue Marble Next Generation (topography and bathymetry,
|
||||||
|
# December 2004, public domain), downloaded once:
|
||||||
|
# https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73909/world.topo.bathy.200412.3x5400x2700.jpg
|
||||||
|
# usage: tools/globe/prepare-day-texture.py <that jpg> <output jpg> (needs Pillow; not part of the build)
|
||||||
|
#
|
||||||
|
# NASA's mosaic does not wrap cleanly: towards the poles a strip at its east edge is lighter than the map on both sides
|
||||||
|
# of the 180° meridian, which the globe draws as a line from each pole. On every row where the map agrees on both sides
|
||||||
|
# of the seam but the strip around it does not, the strip is replaced by a blend between the two sides. A real feature
|
||||||
|
# on the meridian (Fiji, the Ross Ice Shelf) makes the two sides differ, so its rows are left alone. Within half a degree
|
||||||
|
# of each pole a row is a circle a few kilometres across, so it gets one colour, its median, which removes the strip
|
||||||
|
# there too. Then the map is scaled to 4096×2048 (the globe's texture size) with Lanczos.
|
||||||
|
import sys
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
BAND = 16 # pixels on each side of the seam that may be repaired
|
||||||
|
ANCHOR = 4 # pixels averaged just outside the band, on each side
|
||||||
|
AGREE = 24 # the two sides agree when their mean channels differ by less than this
|
||||||
|
ODD = 30 # a band pixel is an artefact when it differs from the blend by more than this
|
||||||
|
POLAR = 0.5 # degrees from a pole within which a row is one colour
|
||||||
|
|
||||||
|
|
||||||
|
def mean(pixels):
|
||||||
|
return tuple(sum(p[i] for p in pixels) / len(pixels) for i in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def distance(a, b):
|
||||||
|
return sum(abs(x - y) for x, y in zip(a, b)) / 3
|
||||||
|
|
||||||
|
|
||||||
|
source, target = sys.argv[1], sys.argv[2]
|
||||||
|
image = Image.open(source).convert("RGB")
|
||||||
|
width, height = image.size
|
||||||
|
pixels = image.load()
|
||||||
|
repaired = 0
|
||||||
|
for y in range(height):
|
||||||
|
west = mean([pixels[width - BAND - 1 - i, y] for i in range(ANCHOR)])
|
||||||
|
east = mean([pixels[BAND + i, y] for i in range(ANCHOR)])
|
||||||
|
if distance(west, east) >= AGREE:
|
||||||
|
continue
|
||||||
|
columns = [width - BAND + i for i in range(BAND)] + list(range(BAND))
|
||||||
|
blend = [tuple(w + (e - w) * (i + 1) / (2 * BAND + 1) for w, e in zip(west, east)) for i in range(2 * BAND)]
|
||||||
|
if max(distance(pixels[x, y], b) for x, b in zip(columns, blend)) <= ODD:
|
||||||
|
continue
|
||||||
|
for x, b in zip(columns, blend):
|
||||||
|
pixels[x, y] = tuple(round(c) for c in b)
|
||||||
|
repaired += 1
|
||||||
|
|
||||||
|
polar = 0
|
||||||
|
for y in range(height):
|
||||||
|
latitude = 90 - (y + 0.5) * 180 / height
|
||||||
|
if abs(latitude) < 90 - POLAR:
|
||||||
|
continue
|
||||||
|
row = [pixels[x, y] for x in range(width)]
|
||||||
|
median = tuple(sorted(p[i] for p in row)[len(row) // 2] for i in range(3))
|
||||||
|
for x in range(width):
|
||||||
|
pixels[x, y] = median
|
||||||
|
polar += 1
|
||||||
|
|
||||||
|
image.resize((4096, 2048), Image.LANCZOS).save(target, "JPEG", quality=90, optimize=True, progressive=True)
|
||||||
|
print(f"{repaired} of {height} rows repaired at the seam, {polar} polar rows made one colour; wrote {target}")
|
||||||
+113
-7
@@ -4,8 +4,9 @@
|
|||||||
// Hovering a post pulses a ring where it is and draws an arc to it from PrivaPub's server; leaving it goes back to the
|
// Hovering a post pulses a ring where it is and draws an arc to it from PrivaPub's server; leaving it goes back to the
|
||||||
// view. Posts of servers behind a CDN have no place: they are grouped per CDN in labels on a ring above the Arctic,
|
// view. Posts of servers behind a CDN have no place: they are grouped per CDN in labels on a ring above the Arctic,
|
||||||
// which says "CDN" rather than any place; hovering one draws its arc to that group and a ring where the server was
|
// which says "CDN" rather than any place; hovering one draws its arc to that group and a ring where the server was
|
||||||
// before the CDN, when PrivaPub saw it there. Day texture with the light theme, night with the dark one; motion follows prefers-reduced-motion, and nothing
|
// before the CDN, when PrivaPub saw it there. Day texture with the light theme, night with the dark one; motion follows prefers-reduced-motion. It
|
||||||
// renders while the page or the globe is hidden. No .NET round-trip on scroll or hover: the posts' places come in one
|
// renders only while something moves (a camera flight, a hovered post's pulse, auto-rotation, the reader dragging or
|
||||||
|
// zooming) and never while the page or the globe is hidden: an idle globe costs no GPU time. No .NET round-trip on scroll or hover: the posts' places come in one
|
||||||
// call per feed change (setPosts), the cards are matched by data-post-id.
|
// call per feed change (setPosts), the cards are matched by data-post-id.
|
||||||
|
|
||||||
const vendor = new URL("../vendor/globe/", import.meta.url).href
|
const vendor = new URL("../vendor/globe/", import.meta.url).href
|
||||||
@@ -47,6 +48,10 @@ export function create(element, scrollContainerId) {
|
|||||||
renderer: null,
|
renderer: null,
|
||||||
texture: null,
|
texture: null,
|
||||||
paused: false,
|
paused: false,
|
||||||
|
idle: false,
|
||||||
|
running: true,
|
||||||
|
interacting: false,
|
||||||
|
awakeUntil: 0,
|
||||||
observers: [],
|
observers: [],
|
||||||
listeners: [],
|
listeners: [],
|
||||||
timers: [],
|
timers: [],
|
||||||
@@ -71,6 +76,7 @@ export function setPosts(posts, self) {
|
|||||||
state.self = self && Number.isFinite(self.lat) && Number.isFinite(self.lng) ? self : null
|
state.self = self && Number.isFinite(self.lat) && Number.isFinite(self.lng) ? self : null
|
||||||
drawPoints()
|
drawPoints()
|
||||||
scheduleView()
|
scheduleView()
|
||||||
|
wake()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live numbers for the nerd stats overlay; null until the globe exists.
|
// Live numbers for the nerd stats overlay; null until the globe exists.
|
||||||
@@ -95,8 +101,10 @@ export function stats() {
|
|||||||
fps: state.fps,
|
fps: state.fps,
|
||||||
renderer: state.renderer,
|
renderer: state.renderer,
|
||||||
texture: state.texture,
|
texture: state.texture,
|
||||||
|
anisotropy: state.anisotropy,
|
||||||
autoRotate: !!state.globe.controls().autoRotate,
|
autoRotate: !!state.globe.controls().autoRotate,
|
||||||
paused: state.paused,
|
paused: state.paused,
|
||||||
|
idle: state.idle,
|
||||||
hovered: state.hovered && state.points.has(state.hovered) ? state.points.get(state.hovered).label ?? state.hovered : null,
|
hovered: state.hovered && state.points.has(state.hovered) ? state.points.get(state.hovered).label ?? state.hovered : null,
|
||||||
arcs: state.globe.arcsData().length,
|
arcs: state.globe.arcsData().length,
|
||||||
rings: state.globe.ringsData().length
|
rings: state.globe.ringsData().length
|
||||||
@@ -129,6 +137,7 @@ function resize() {
|
|||||||
}
|
}
|
||||||
state.globe.width(width).height(height)
|
state.globe.width(width).height(height)
|
||||||
pause(document.visibilityState !== "visible")
|
pause(document.visibilityState !== "visible")
|
||||||
|
wake()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function build(width, height) {
|
async function build(width, height) {
|
||||||
@@ -167,9 +176,19 @@ async function build(width, height) {
|
|||||||
.htmlLng(g => g.lng)
|
.htmlLng(g => g.lng)
|
||||||
.htmlAltitude(0.12)
|
.htmlAltitude(0.12)
|
||||||
.htmlElement(cdnLabel)
|
.htmlElement(cdnLabel)
|
||||||
|
sharpen(globe)
|
||||||
const controls = globe.controls()
|
const controls = globe.controls()
|
||||||
controls.autoRotateSpeed = 0.35
|
controls.autoRotateSpeed = 0.35
|
||||||
controls.enableDamping = true
|
controls.enableDamping = true
|
||||||
|
controls.addEventListener("start", () => {
|
||||||
|
state.interacting = true
|
||||||
|
wake()
|
||||||
|
})
|
||||||
|
controls.addEventListener("end", () => {
|
||||||
|
state.interacting = false
|
||||||
|
wake(1500)
|
||||||
|
})
|
||||||
|
listen(state.element, "pointermove", () => wake(1000))
|
||||||
globe.pointOfView({ lat: 30, lng: 10, altitude: 2.6 * narrowness() }, 0)
|
globe.pointOfView({ lat: 30, lng: 10, altitude: 2.6 * narrowness() }, 0)
|
||||||
applyTheme()
|
applyTheme()
|
||||||
readRenderer()
|
readRenderer()
|
||||||
@@ -187,6 +206,57 @@ async function build(width, height) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The globe is a UV sphere: towards the poles its triangles narrow to slivers meeting at one point, and texture
|
||||||
|
// coordinates interpolated across them pinch the map into a star around the axis. Each pixel computes its own instead,
|
||||||
|
// from its exact point on the sphere (the inverse of three.js SphereGeometry's mapping), so the map is drawn the same
|
||||||
|
// everywhere. The mipmap level comes from gradients taken on whichever of two longitude ranges has no jump at that pixel,
|
||||||
|
// and the map wraps around horizontally (keepSharp), so the 180° meridian shows no seam; anisotropic filtering keeps
|
||||||
|
// high latitudes sharp at grazing angles, at 8× at most: sharper than that is barely visible and costs on phones.
|
||||||
|
function sharpen(globe) {
|
||||||
|
const material = globe.globeMaterial()
|
||||||
|
material.onBeforeCompile = shader => {
|
||||||
|
shader.vertexShader = shader.vertexShader
|
||||||
|
.replace("#include <common>", "#include <common>\nvarying vec3 vSpherePoint;")
|
||||||
|
.replace("#include <begin_vertex>", "#include <begin_vertex>\nvSpherePoint = position;")
|
||||||
|
shader.fragmentShader = shader.fragmentShader
|
||||||
|
.replace("#include <common>", "#include <common>\nvarying vec3 vSpherePoint;")
|
||||||
|
.replace("#include <map_fragment>", `
|
||||||
|
#ifdef USE_MAP
|
||||||
|
vec3 spherePoint = normalize(vSpherePoint);
|
||||||
|
float sphereU = atan(spherePoint.z, -spherePoint.x) / (2.0 * PI);
|
||||||
|
float sphereV = 1.0 - acos(clamp(spherePoint.y, -1.0, 1.0)) / PI;
|
||||||
|
float uWhole = fract(sphereU);
|
||||||
|
float uShifted = fract(sphereU + 0.5) - 0.5;
|
||||||
|
vec2 gradientX = vec2(dFdx(uWhole), dFdx(sphereV));
|
||||||
|
vec2 gradientY = vec2(dFdy(uWhole), dFdy(sphereV));
|
||||||
|
float shiftedX = dFdx(uShifted);
|
||||||
|
float shiftedY = dFdy(uShifted);
|
||||||
|
if (abs(shiftedX) + abs(shiftedY) < abs(gradientX.x) + abs(gradientY.x)) {
|
||||||
|
gradientX.x = shiftedX;
|
||||||
|
gradientY.x = shiftedY;
|
||||||
|
}
|
||||||
|
vec4 sampledDiffuseColor = textureGrad(map, vec2(uWhole, sphereV), gradientX, gradientY);
|
||||||
|
diffuseColor *= sampledDiffuseColor;
|
||||||
|
#endif`)
|
||||||
|
}
|
||||||
|
material.customProgramCacheKey = () => "privapub-per-pixel-sphere"
|
||||||
|
material.needsUpdate = true
|
||||||
|
state.anisotropy = Math.min(8, globe.renderer().capabilities.getMaxAnisotropy())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every texture the globe loads (a theme change loads the other one) wraps around horizontally, so filtering at the
|
||||||
|
// 180° meridian blends the map's two edges instead of clamping each, and gets anisotropic filtering.
|
||||||
|
const RepeatWrapping = 1000//three.js's RepeatWrapping: the bundle does not expose THREE
|
||||||
|
function keepSharp() {
|
||||||
|
const map = state?.globe?.globeMaterial().map
|
||||||
|
if (map && (map.anisotropy !== state.anisotropy || map.wrapS !== RepeatWrapping)) {
|
||||||
|
map.anisotropy = state.anisotropy
|
||||||
|
map.wrapS = RepeatWrapping
|
||||||
|
map.needsUpdate = true
|
||||||
|
wake(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Which cards are in view: an IntersectionObserver on every [data-post-id] of the scroll container, re-attached as
|
// Which cards are in view: an IntersectionObserver on every [data-post-id] of the scroll container, re-attached as
|
||||||
// the list changes; hover by delegation on the container.
|
// the list changes; hover by delegation on the container.
|
||||||
function watchCards() {
|
function watchCards() {
|
||||||
@@ -257,10 +327,40 @@ function pause(paused) {
|
|||||||
if (!state?.globe || state.paused === paused)
|
if (!state?.globe || state.paused === paused)
|
||||||
return
|
return
|
||||||
state.paused = paused
|
state.paused = paused
|
||||||
if (paused)
|
render()
|
||||||
state.globe.pauseAnimation()
|
}
|
||||||
else
|
|
||||||
|
// globe.gl draws every frame; this keeps its loop running only while the globe is shown and something moves.
|
||||||
|
function render() {
|
||||||
|
const run = !state.paused && !state.idle
|
||||||
|
if (run === state.running)
|
||||||
|
return
|
||||||
|
state.running = run
|
||||||
|
if (run)
|
||||||
state.globe.resumeAnimation()
|
state.globe.resumeAnimation()
|
||||||
|
else
|
||||||
|
state.globe.pauseAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something is about to move: draw for at least ms more.
|
||||||
|
function wake(ms = 1500) {
|
||||||
|
if (!state?.globe)
|
||||||
|
return
|
||||||
|
state.awakeUntil = Math.max(state.awakeUntil, performance.now() + ms)
|
||||||
|
if (state.idle) {
|
||||||
|
state.idle = false
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checked every second: nothing hovered, dragged, rotating or flying, so the last frame can stay on screen.
|
||||||
|
function settle() {
|
||||||
|
if (!state?.globe || state.idle)
|
||||||
|
return
|
||||||
|
if (state.hovered || state.interacting || state.globe.controls().autoRotate || performance.now() < state.awakeUntil)
|
||||||
|
return
|
||||||
|
state.idle = true
|
||||||
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTheme() {
|
function applyTheme() {
|
||||||
@@ -275,6 +375,7 @@ function applyTheme() {
|
|||||||
state.colours = null
|
state.colours = null
|
||||||
state.globe.atmosphereColor(colour("server", 1))
|
state.globe.atmosphereColor(colour("server", 1))
|
||||||
drawPoints()
|
drawPoints()
|
||||||
|
wake(2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The theme's colours, resolved to rgb (the tokens are hsl() of CSS variables, which three.js cannot read).
|
// The theme's colours, resolved to rgb (the tokens are hsl() of CSS variables, which three.js cannot read).
|
||||||
@@ -392,6 +493,7 @@ function hover(key) {
|
|||||||
if (!state?.globe || state.hovered === key)
|
if (!state?.globe || state.hovered === key)
|
||||||
return
|
return
|
||||||
state.hovered = key
|
state.hovered = key
|
||||||
|
wake(1300)
|
||||||
const post = key && state.points.get(key)
|
const post = key && state.points.get(key)
|
||||||
drawRings()
|
drawRings()
|
||||||
if (!post) {
|
if (!post) {
|
||||||
@@ -434,6 +536,7 @@ function updateView() {
|
|||||||
state.view = null
|
state.view = null
|
||||||
state.spreadKm = 0
|
state.spreadKm = 0
|
||||||
controls.autoRotate = !reducedMotion.matches
|
controls.autoRotate = !reducedMotion.matches
|
||||||
|
wake()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controls.autoRotate = false
|
controls.autoRotate = false
|
||||||
@@ -458,6 +561,7 @@ function updateView() {
|
|||||||
function fly(view, ms) {
|
function fly(view, ms) {
|
||||||
if (!view || !state?.globe)
|
if (!view || !state?.globe)
|
||||||
return
|
return
|
||||||
|
wake((reducedMotion.matches ? 0 : ms) + 400)
|
||||||
state.globe.pointOfView(view, reducedMotion.matches ? 0 : ms)
|
state.globe.pointOfView(view, reducedMotion.matches ? 0 : ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,14 +595,16 @@ function countFrames() {
|
|||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (state !== current)
|
if (state !== current)
|
||||||
return
|
return
|
||||||
if (!state.paused)
|
if (state.running)
|
||||||
state.frames++
|
state.frames++
|
||||||
|
keepSharp()
|
||||||
requestAnimationFrame(tick)
|
requestAnimationFrame(tick)
|
||||||
}
|
}
|
||||||
requestAnimationFrame(tick)
|
requestAnimationFrame(tick)
|
||||||
state.timers.push(setInterval(() => {
|
state.timers.push(setInterval(() => {
|
||||||
state.fps = state.paused ? 0 : state.frames
|
state.fps = state.running ? state.frames : 0
|
||||||
state.frames = 0
|
state.frames = 0
|
||||||
|
settle()
|
||||||
}, 1000))
|
}, 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+9
-3
@@ -1,12 +1,18 @@
|
|||||||
# The globe's vendored files
|
# The globe's vendored files
|
||||||
|
|
||||||
Copied once from the npm registry (registry.npmjs.org) on 2026-10-04; the build and the app never fetch them from
|
Copied once on 2026-10-04 from the npm registry (registry.npmjs.org) and, for the day texture, from NASA
|
||||||
anywhere else. To update, download the new tarballs from the registry, copy the same files, and replace the hashes.
|
(eoimages.gsfc.nasa.gov); the build and the app never fetch them from anywhere else. To update, download the new files,
|
||||||
|
copy them the same way (the day texture through `tools/globe/prepare-day-texture.py`), and replace the hashes.
|
||||||
|
|
||||||
|
NASA's mosaic does not wrap cleanly at the 180° meridian near the poles (a lighter strip at its east edge), which the
|
||||||
|
globe, drawing each pixel at its exact place, shows as a line from each pole; the script blends that strip into the
|
||||||
|
map on both sides, only on rows where the two sides agree, and gives the rows within half a degree of each pole one
|
||||||
|
colour.
|
||||||
|
|
||||||
| File | From | Licence | sha256 |
|
| File | From | Licence | sha256 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `globe.gl.min.js` | `globe.gl` 2.46.2, `dist/globe.gl.min.js` (UMD, defines `window.Globe`) | MIT, `LICENSE-globe.gl` | `2c3e445c04d121215910a89688b96091c8a72071c122a4f830081a39b636c94c` |
|
| `globe.gl.min.js` | `globe.gl` 2.46.2, `dist/globe.gl.min.js` (UMD, defines `window.Globe`) | MIT, `LICENSE-globe.gl` | `2c3e445c04d121215910a89688b96091c8a72071c122a4f830081a39b636c94c` |
|
||||||
| `earth-blue-marble.jpg` | `three-globe` 2.45.3, `example/img/earth-blue-marble.jpg`, 4096×2048 | NASA Visible Earth "Blue Marble", public domain | `228deba2e4b600146bdcb6cfa359b8ead6aacc2b1c13550a29cd82824cfa1c01` |
|
| `earth-blue-marble.jpg` | NASA Visible Earth, Blue Marble Next Generation with topography and bathymetry, December 2004 (`world.topo.bathy.200412.3x5400x2700.jpg`, sha256 `a9f0088972dee0254610af851c4d6838ca3f2cf79176987e0a5713e2c15ec042`), seam repaired and scaled to 4096×2048 by `tools/globe/prepare-day-texture.py` | NASA, public domain | `1334612d91d5104afb0e48cce916e100575c2969baf99e21454c23535f4ce713` |
|
||||||
| `earth-night.jpg` | `three-globe` 2.45.3, `example/img/earth-night.jpg`, 4096×2048 | NASA Earth Observatory "Black Marble" (city lights), public domain | `355ab23dd1323315b393d7b91dd2d7ee223a1cbaaba2b48dc72ba90d371ced24` |
|
| `earth-night.jpg` | `three-globe` 2.45.3, `example/img/earth-night.jpg`, 4096×2048 | NASA Earth Observatory "Black Marble" (city lights), public domain | `355ab23dd1323315b393d7b91dd2d7ee223a1cbaaba2b48dc72ba90d371ced24` |
|
||||||
|
|
||||||
`globe.gl.min.js` bundles its dependencies: three.js r185 (MIT, © 2010–2026 three.js authors), three-globe (MIT, © 2019
|
`globe.gl.min.js` bundles its dependencies: three.js r185 (MIT, © 2010–2026 three.js authors), three-globe (MIT, © 2019
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Reference in new issue
Block a user