mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(mobile): add the untethered vision mockup and document the mobile app
mobile/vision/index.html is a single self-contained file: a hand-written WebGL2 planet with real sun position, Black Marble lights, atmosphere, clouds and a mobile post chain; the full feed, card-to-world morph, Ride, lenses and time ribbon over synthetic traffic; capture with real WebCrypto seals; and reconstruction threads with a verifiable ledger and an AI refine composer. All data is illustrative and marked as such. build.mjs regenerates it offline from src/ and assets/. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
16 files changed
+198
-128
No files matched your search
@@ -59,6 +59,24 @@ provider registration rejects compositions that cannot fit rather than truncatin
|
||||
identities. A malformed or duplicate entry invalidates the complete custom-switch
|
||||
field. Existing whole-link size limits still apply.
|
||||
|
||||
## Mobile app prototype — October 18, 2026
|
||||
|
||||
`/mobile/` is a phone and foldable shell over the same engine, specified in
|
||||
[`mobile/DESIGN.md`](../mobile/DESIGN.md). It has five tabs: a situation feed,
|
||||
the globe, capture, reconstruction threads and the user's own captures.
|
||||
`mobile/globe/` is a phone-tuned Cesium engine. It blends Esri day imagery
|
||||
with NASA Black Marble night lights across the terminator, uses Google 3D
|
||||
tiles below 25 km when keyed, and builds its layers on the existing `/api`
|
||||
routes and source parsers. Capture signs each bundle on the device with a
|
||||
non-extractable ECDSA P-256 WebCrypto key held in IndexedDB. A bundle carries
|
||||
a SHA-256 media hash, a per-device hash chain, and the IMU and GPS trace.
|
||||
These signatures are browser equivalents of hardware attestation; they are
|
||||
not hardware attestation. Hypotheses and authors in the reconstruction
|
||||
threads are labelled example content. `npm run build` now emits
|
||||
`dist/mobile/index.html` beside the app, while `npm run build:panel` still
|
||||
builds the app page alone. `mobile/vision/index.html` is a self-contained
|
||||
concept mockup with illustrative data and no production code.
|
||||
|
||||
## God's Eye View in conversations — October 2, 2026
|
||||
|
||||
Tool answers that can be shown in God's Eye View include a view: camera, layers,
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ This folder holds two things:
|
||||
| Path | What it is | Tethered to production? |
|
||||
| --- | --- | --- |
|
||||
| [`mobile/index.html`](index.html) → `/mobile/` | **Prototype.** A working phone/foldable app that sets the design language on top of the real engine: CesiumJS, Esri cloudless imagery, NASA Black Marble night lights, Google Photorealistic 3D Tiles (when keyed), and the app's own live `/api` feeds (ADS-B, military, AIS, TLEs, quakes, FIRMS fires, cyclones, launches, CCTV) plus the bundled Bhote Koshi event pack. Capture is real: camera, IMU, GPS and a WebCrypto signature. | Yes. Runs on the existing dev server (`npm run dev`, open `http://localhost:4173/mobile/`). |
|
||||
| [`mobile/vision/index.html`](vision/index.html) | **Vision.** A single self-contained file with no server, no keys and no production code. It shows where the product goes: the full feed, 4D reconstructions with collaborative hypotheses, the provenance ledger, holograms left in places, a hand-written WebGL globe with a cinematic post chain. | No. Open the file in any browser. |
|
||||
| [`mobile/vision/index.html`](vision/index.html) | **Vision.** A single self-contained file with no server, no keys and no production code. It shows where the product goes: the full feed, 4D reconstructions with collaborative hypotheses, the provenance ledger, holograms left in places, a hand-written WebGL globe with a cinematic post chain. | No. Open the file straight from disk in any browser (the dev server's CSP blocks its inline script). Rebuild it with `node mobile/vision/build.mjs`. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 722 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 238 KiB |
+89
-63
@@ -294,7 +294,7 @@ input, textarea { font: inherit; color: inherit; }
|
||||
.hud svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.hud .tape { position: absolute; top: 50%; transform: translateY(-50%); font-size: 15px; font-weight: 600; padding: 6px 9px; border: 1px solid rgba(190,255,200,.55); border-radius: 6px; background: rgba(0,20,8,.25); }
|
||||
.hud .tape small { display: block; font-size: 9.5px; opacity: .7; font-weight: 500; }
|
||||
.chat { position: absolute; left: 12px; right: 12px; bottom: calc(var(--sab) + 72px); height: 46%; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; pointer-events: none !important; mask-image: linear-gradient(to top, #000 70%, transparent); -webkit-mask-image: linear-gradient(to top, #000 70%, transparent); }
|
||||
.chat { position: absolute; left: 12px; right: 12px; bottom: calc(var(--sab) + 72px); height: 34%; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; pointer-events: none !important; mask-image: linear-gradient(to top, #000 70%, transparent); -webkit-mask-image: linear-gradient(to top, #000 70%, transparent); }
|
||||
.bub { align-self: flex-start; max-width: 84%; display: flex; gap: 8px; align-items: flex-end; animation: bubIn .5s var(--spring); }
|
||||
.bub .bx { padding: 8px 12px; border-radius: 18px 18px 18px 6px; font-size: 13.5px; line-height: 1.35; }
|
||||
.bub .bx b { display: block; font-size: 11.5px; color: var(--text2); font-weight: 600; margin-bottom: 1px; }
|
||||
@@ -377,7 +377,7 @@ input, textarea { font: inherit; color: inherit; }
|
||||
.ws .wsh > * { pointer-events: auto; }
|
||||
.ws .wsh .tt { flex: 1; min-width: 0; height: 44px; border-radius: 22px; display: flex; align-items: center; gap: 10px; padding: 0 8px 0 14px; }
|
||||
.ws .wsh .tt b { font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }
|
||||
.ws .wsh .tt span { font-size: 11.5px; color: var(--text2); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ws .wsh .tt span.sub { font-size: 11.5px; color: var(--text2); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ws .body { position: absolute; left: 0; right: 0; bottom: 0; top: 40%; border-radius: 28px 28px 0 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.ws .seg { display: flex; margin: 12px 14px 8px; padding: 3px; border-radius: 12px; background: rgba(255,255,255,.07); flex: none; }
|
||||
.ws .seg button { flex: 1; height: 34px; border-radius: 9px; font-size: 13px; font-weight: 600; color: var(--text2); transition: all .25s; }
|
||||
@@ -524,7 +524,10 @@ input, textarea { font: inherit; color: inherit; }
|
||||
@media (min-width: 600px) {
|
||||
:root { --paneW: clamp(360px, 40vw, 440px); }
|
||||
.pane { width: var(--paneW); background: var(--bg); border-right: .5px solid var(--sep); z-index: 22; }
|
||||
.tabwrap { --tabW: var(--paneW); }
|
||||
.tabwrap { --tabW: var(--paneW); padding-left: 8px; padding-right: 8px; gap: 8px; }
|
||||
.tab, .tablens { width: 54px; }
|
||||
.searchbtn { width: 54px; height: 54px; }
|
||||
.tabbar { height: 58px; } .tablens { height: 50px; } .tab { height: 50px; }
|
||||
.acc-host { right: auto; width: calc(var(--paneW) - 24px); }
|
||||
.app[data-tab="globe"] .acc-host { display: flex; }
|
||||
.wide-only { display: block !important; }
|
||||
@@ -548,10 +551,12 @@ input, textarea { font: inherit; color: inherit; }
|
||||
#v-capture { z-index: 45; }
|
||||
.ride { left: var(--paneW); }
|
||||
.concept-tag, .app[data-tab] .concept-tag { top: calc(var(--sat) + 14px); bottom: auto; left: auto; right: 70px; }
|
||||
.app[data-mode="situation"] .concept-tag { right: 124px; }
|
||||
.composer { left: 10px; right: 10px; }
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
:root { --paneW: 440px; }
|
||||
.tab, .tablens { width: 66px; } .searchbtn { width: 62px; height: 62px; } .tabbar { height: 62px; } .tablens { height: 54px; } .tab { height: 54px; }
|
||||
.masonry { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
/* hinge-aware: vertical fold — panes split exactly at the hinge */
|
||||
@@ -732,7 +737,7 @@ input, textarea { font: inherit; color: inherit; }
|
||||
<div class="tag">The window to the world.</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>window.__GE_TEX={"clouds":"data:image/webp;base64,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 truncated
|
||||
<script>window.__GE_TEX={"clouds":"data:image/webp;base64,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 truncated
|
||||
<script>
|
||||
(() => {
|
||||
'use strict';
|
||||
@@ -983,6 +988,9 @@ Object.assign(GE, { D2R, R2D, TAU, EARTH_KM, clamp, lerp, smooth, ease, v3, m4,
|
||||
callsigns and reconstruction claims are illustrative (Concept · illustrative data). */
|
||||
const NOW = Date.now();
|
||||
GE.NOW = NOW;
|
||||
const lastUTC = (h, m) => { const d = new Date(NOW); let t = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), h, m); if (t > NOW - 3600000) t -= 86400000; return t; };
|
||||
GE.T_CONCERT = lastUTC(21, 44); GE.T_CLIMB = lastUTC(10, 52);
|
||||
GE.dayWord = (t, part) => (new Date(t).getUTCDate() === new Date(NOW).getUTCDate() ? 'this ' + part : (part === 'night' ? 'last night' : 'yesterday ' + part)).replace('this night', 'tonight');
|
||||
const R = rng(20261018);
|
||||
|
||||
/* ---------- airports [iata, city, lat, lon, weight] ---------- */
|
||||
@@ -1406,9 +1414,9 @@ vec3 aurora(vec3 n, float nightF){
|
||||
float ml = asin(abs(s)) * 57.2958;
|
||||
float band = 66.5 - uAurora*5.5;
|
||||
float lonm = atan(dot(n, normalize(cross(mp, vec3(0,0,1)))), dot(n, normalize(cross(mp, cross(mp, vec3(0,0,1))))));
|
||||
float curt = 0.45 + 0.55*fbm(vec2(lonm*9. + uT*0.0006, uT*0.00025));
|
||||
float g = exp(-pow((ml-band)/(2.2+uAurora*0.6), 2.)) * curt;
|
||||
float r = exp(-pow((ml-band-3.5)/3.5, 2.)) * 0.25;
|
||||
float curt = 0.25 + 0.75*fbm(vec2(lonm*14. + uT*0.0006, uT*0.00025 + ml*0.15));
|
||||
float g = exp(-pow((ml-band)/(0.7+uAurora*0.25), 2.)) * curt * curt * 0.6;
|
||||
float r = exp(-pow((ml-band-2.5)/2.0, 2.)) * 0.08 * curt;
|
||||
return (vec3(0.12, 1.0, 0.5)*g + vec3(0.75, 0.15, 0.45)*r*0.45) * nightF * uAurora * 0.75;
|
||||
}
|
||||
vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3 emis){
|
||||
@@ -1428,14 +1436,18 @@ vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3
|
||||
float lot = vnoise(p*0.11)*0.12;
|
||||
vec3 urb = mix(roof*(0.9+lot), vec3(0.075,0.078,0.085), road);
|
||||
// rural: patchwork fields & forest
|
||||
vec2 fid = floor(p/(B*2.6)); float fh = hash12(fid+7.);
|
||||
vec3 field = base * mix(vec3(0.9,1.05,0.8), vec3(1.25,1.1,0.75), fh) * (0.85+0.3*vnoise(p*0.02));
|
||||
float forest = smoothstep(0.45,0.62, fbm(p*0.0016+3.));
|
||||
field = mix(field, base*vec3(0.55,0.75,0.5), forest);
|
||||
vec2 fid = floor(p/(B*2.6)); float fh = mix(hash12(fid+7.), 0.5, clamp(fw/(B*2.6)*1.6, 0., 1.));
|
||||
vec3 field = base * mix(vec3(0.94,1.02,0.9), vec3(1.12,1.06,0.86), fh) * (0.92+0.16*vnoise(p*0.02));
|
||||
float forest = smoothstep(0.5,0.66, fbm(p*0.0011+3.));
|
||||
field = mix(field, base*vec3(0.78,0.88,0.74), forest*0.6);
|
||||
vec3 col = mix(field, urb, urban);
|
||||
// street lights at night along roads
|
||||
float lamps = road * urban * (0.5+0.5*hash12(floor(p/9.)));
|
||||
emis = vec3(1.0,0.58,0.24)*lamps*night*1.1*(1.-aa*0.55);
|
||||
float emin = min(e.x, e.y);
|
||||
float curb = smoothstep(rw*0.55, rw*0.8, emin) * (1. - smoothstep(rw*0.8, rw*0.98, emin));
|
||||
float along2 = (e.x < e.y) ? g.y : g.x;
|
||||
float dotsL = smoothstep(0.75, 0.95, fract(along2*B/28.));
|
||||
float lamps = mix(curb*(0.25 + dotsL*1.6), 0.07, aa) * urban;
|
||||
emis = vec3(1.0,0.56,0.22)*lamps*night*1.0;
|
||||
if (uTraffic > 0.5) {
|
||||
vec2 rp = p/B; float along = abs(f.x-0.5) < abs(f.y-0.5) ? rp.y : rp.x;
|
||||
float cars = smoothstep(0.75, 1., sin(along*38. - uT*2.5 + h*20.)) * road * urban * (1.-aa);
|
||||
@@ -1530,7 +1542,7 @@ void main(){
|
||||
}
|
||||
// night lights + moonlit black-marble land
|
||||
vec3 lightCol = mix(vec3(1.0,0.66,0.32), vec3(1.0,0.86,0.64), smoothstep(0.4,1.4,lights));
|
||||
vec3 emis = lightCol * lights * 3.2 * nightF * (1. - uCity.x*0.94);
|
||||
vec3 emis = lightCol * lights * 3.2 * nightF * (1. - uCity.x*0.995);
|
||||
emis += pow(nightTex, vec3(2.2)) * vec3(0.5,0.6,1.0) * 0.12 * nightF;
|
||||
emis += emisD;
|
||||
emis *= (1.-cl*0.82);
|
||||
@@ -1858,9 +1870,12 @@ void main(){
|
||||
vec2 id = floor(f); vec2 fr = fract(f);
|
||||
float win = step(0.18, fr.x)*step(fr.x,0.82)*step(0.22,fr.y)*step(fr.y,0.78);
|
||||
float r = hash12(id + h*91.);
|
||||
float lit = step(1. - (0.12 + 0.5*uNight), r) * win * step(vUV.y, vAttr.z - 1.);
|
||||
float frac = 0.12 + 0.5*uNight;
|
||||
float lit = step(1. - frac, r) * win * step(vUV.y, vAttr.z - 1.);
|
||||
vec3 wc = mix(vec3(1.0,0.72,0.42), vec3(0.75,0.85,1.0), step(0.6, hash12(id*1.3+h)));
|
||||
emis += wc * lit * (0.9 + 0.6*r) * uNight * 2.2;
|
||||
float fwv = length(fwidth(vUV));
|
||||
float pat = mix(lit * (0.9 + 0.6*r), frac * 0.36 * 0.75 * (0.15 + 1.6*fract(h*13.7)*fract(h*13.7)), smoothstep(0.9, 2.6, fwv));
|
||||
emis += wc * pat * uNight * 2.2;
|
||||
base = mix(base, base*0.45 + uSkyCol*0.08, win*0.8);
|
||||
spec = win*(1.-uNight)*0.9 + step(0.68, h)*0.5;
|
||||
} else base *= 0.82;
|
||||
@@ -1908,6 +1923,10 @@ void main(){
|
||||
base = vec3(0.05);
|
||||
emis = mix(vec3(0.9,0.3,1.0), vec3(0.3,0.8,1.0), 0.5+0.5*sin(uT*1.7+vPos.x*0.05)) * 3.0;
|
||||
heat = 0.9;
|
||||
} else if (type == 7) { // floodlit launch structure
|
||||
base = vec3(0.75, 0.76, 0.78) * (0.6 + 0.4*vAttr.y);
|
||||
emis = vec3(1.0, 0.93, 0.82) * uNight * (0.35 + 0.25*smoothstep(0., 60., vUV.y));
|
||||
heat = 0.6;
|
||||
} else { // houses
|
||||
base = mix(vec3(0.55,0.45,0.38), vec3(0.75,0.72,0.66), vAttr.y);
|
||||
if (n.z > 0.5) base = mix(vec3(0.45,0.2,0.15), vec3(0.3,0.35,0.42), step(0.5, vAttr.y));
|
||||
@@ -2265,7 +2284,7 @@ W.buildStatic = function () {
|
||||
{ const a = []; for (const q of GE.QUAKES) pushSpr(a, geo.vec(q.lat, q.lon, 0.5), [1.0, 0.5, 0.3, 1], 10 + (q.mag - 2.5) * 9, 2, -10, q.t, 1e9, 0, RS(), 0.8); addSet('quakes', makeSpriteSet('quakes', a)); }
|
||||
// fires (+ smoke in alpha pass)
|
||||
{ const a = [], sm = []; for (const f of GE.FIRES) { pushSpr(a, geo.vec(f.lat, f.lon, 0.2), [1, 0.5, 0.15, 1], 4 + Math.sqrt(f.frp) * 0.9, 3, -10, f.t, 1e9, 0.4, RS(), 1.0); if (RS() < 0.35) pushSpr(sm, geo.vec(f.lat + 0.15, f.lon + 0.35, 2), [0.3, 0.28, 0.27, 0.32], 26 + RS() * 30, 11, -10, f.t + 3600, 1e9, 0.8, RS(), 0.5); }
|
||||
addSet('fires', makeSpriteSet('fires', a)); addSet('fires', makeSpriteSet('smoke', sm, 'alpha')); }
|
||||
addSet('fires', makeSpriteSet('fires', a)); addSet('smoke', makeSpriteSet('smoke', sm, 'alpha')); }
|
||||
// perimeters
|
||||
{ const a = []; for (const [la, lo, , , s] of GE.FIRE_CLUSTERS) pushPolyline(a, circlePts(la, lo, s * 111 * 0.9, 40, 0.3), [1, 0.42, 0.24, 0.6], 1); addSet('perims', makeLineSet(a)); }
|
||||
// floods
|
||||
@@ -2455,7 +2474,7 @@ W.updateDynamic = function () {
|
||||
if (W.on('transit')) for (let i = 0; i < 400; i++) { const c = BIG_CITIES[i % 16]; const ang = hash01(i) * TAU + t / (600 + hash01(i + 3) * 900); const rr = 2 + hash01(i + 9) * 10; const [la, lo] = geo.dest(c.lat, c.lon, ang, rr); pushSpr(a, geo.vec(la, lo, 0.01), [0.62, 0.9, 0.37, 0.95], 5, 0); }
|
||||
if (W.on('wind')) for (const w of WIND) { const age = ((t / w.life + w.ph) % 1 + 1) % 1; let la = w.lat, lo = w.lon; const steps = 4, dtH = (age * w.life) / 3600 / steps; for (let k = 0; k < steps; k++) { const [u, v] = windVel(la, lo, t); la += v * dtH; lo += u * dtH; } const [u, v] = windVel(la, lo, t); const fade = Math.sin(age * Math.PI); pushSpr(a, geo.vec(la, lo, 3), [0.75, 0.85, 1, 0.55 * fade], 14, 15, Math.atan2(u * Math.cos(la * D2R), v), -1e9, 1e9, 0, 0, 0.1); }
|
||||
for (const hh of W.userHolos) pushSpr(a, geo.vec(hh.lat, hh.lon, 0.06), [1, 0.85, 0.45, 1], 24, 6, -10, -1e9, 1e9, 0, 0.2, 0.4);
|
||||
if (W.activeExtrasId === 'climb') { const f = clamp((W.simMs - (Date.now() - 10.5 * 3600000)) / (1.9 * 3600000), 0, 1); const [la, lo] = geo.offset(40.7553, -73.9838, 16, 23.5); pushSpr(a, geo.vec(la, lo, (12 + f * 372) / 1000), [1, 0.3, 0.3, 1], 16, 9, -10, -1e9, 1e9, 0, 0.7, 0.9); }
|
||||
if (W.activeExtrasId === 'climb') { const f = clamp((W.simMs - GE.T_CLIMB) / (2.23 * 3600000), 0, 1); const [la, lo] = geo.offset(40.7553, -73.9838, 16, 23.5); pushSpr(a, geo.vec(la, lo, (12 + f * 372) / 1000), [1, 0.3, 0.3, 1], 16, 9, -10, -1e9, 1e9, 0, 0.7, 0.9); }
|
||||
// situation pins
|
||||
for (const p of W.situationPins) pushSpr(a, geo.vec(p.lat, p.lon, 0.2), p.col, p.size || 16, 9, -10, -1e9, 1e9, 0, p.seed || 0, 0.6);
|
||||
// selection highlight
|
||||
@@ -2539,7 +2558,8 @@ W.project = function (view, p) { // world unit coords → css px {x,y,z,vis}
|
||||
/* ---------- render ---------- */
|
||||
W.quality = { scale: 1, steps: 12, bloom: true, grain: true };
|
||||
const LOD_MIN = { radar: 0.12 };
|
||||
const LOD = { cctv: 0.07, street: 0.03, alpr: 0.035, bikes: 0.04, streams: 0.4, signed: 0.6, venues: 0.9, power: 0.7, dc: 1.6, dams: 1.6, milbase: 1.6, airports: 1.3, ports: 1.4, imagery: 1.6, fishing: 2.2, radar: 1.6 };
|
||||
W.on = (id) => id === 'smoke' ? !!W.layerById.fires?.on : !!W.layerById[id]?.on;
|
||||
const LOD = { smoke: 0.7, cctv: 0.07, street: 0.03, alpr: 0.035, bikes: 0.04, streams: 0.4, signed: 0.6, venues: 0.9, power: 0.7, dc: 1.6, dams: 1.6, milbase: 1.6, airports: 1.3, ports: 1.4, imagery: 1.6, fishing: 2.2, radar: 1.6 };
|
||||
W.render = function (view, chain, outFb, opts = {}) {
|
||||
const gl = GL.gl, PR = GL.progs;
|
||||
W.matrices(view);
|
||||
@@ -2624,7 +2644,7 @@ W.render = function (view, chain, outFb, opts = {}) {
|
||||
if (opts.extraSprites) for (const s of opts.extraSprites) { gl.bindVertexArray(s.vao); gl.drawArrays(gl.POINTS, 0, s.count); }
|
||||
// flights
|
||||
if (kindMask[0] || kindMask[1] || kindMask[3]) {
|
||||
const FP = PR.flight; common(FP); gl.uniform4fv(FP.u.uKindMask, kindMask); gl.uniform1f(FP.u.uTrail, 0); gl.uniform1f(FP.u.uTrailLen, 0); gl.uniform1f(FP.u.uSelId, W.selFlight ?? -1);
|
||||
const FP = PR.flight; common(FP, lerp(1, 0.42, smooth(0.35, 1.8, range))); gl.uniform4fv(FP.u.uKindMask, kindMask); gl.uniform1f(FP.u.uTrail, 0); gl.uniform1f(FP.u.uTrailLen, 0); gl.uniform1f(FP.u.uSelId, W.selFlight ?? -1);
|
||||
gl.uniform4f(FP.u.uKindSize, 13, 15, 14, 10);
|
||||
gl.bindVertexArray(W.flightVao); gl.drawArraysInstanced(gl.POINTS, 0, 1, W.nFlights);
|
||||
}
|
||||
@@ -2788,7 +2808,7 @@ function genCity(sc, M, R0) {
|
||||
const hh = hash01(lpx * 0.37 + lpy * 0.71);
|
||||
const core = Math.exp(-ddt / (sc.R * 0.28));
|
||||
let h = sc.kind === 'suburb' ? 5 + hh * 4 : 9 + Math.pow(dens, 1.6) * (sc.maxH * (0.15 + 0.85 * core)) * Math.exp(rr.gauss() * 0.45);
|
||||
h = clamp(h, 6, sc.maxH * 1.4);
|
||||
h = clamp(h, 6, sc.maxH * (sc.tower ? 1.0 : 1.4));
|
||||
const inset = 2 + hh * 3;
|
||||
const type = sc.kind === 'suburb' ? 6 : 0;
|
||||
M.box(x, y, lw - inset * 2, ld - inset * 2, h, sc.rot, [type, hh, h, 0]);
|
||||
@@ -2796,7 +2816,7 @@ function genCity(sc, M, R0) {
|
||||
}
|
||||
}
|
||||
if (twr) { // the fictional tower (skyscraper climb reconstruction)
|
||||
const T = sc.tower; M.box(twr[0], twr[1], T.w, T.w, T.h * 0.62, sc.rot, [0, 0.8, T.h, 0]); M.box(twr[0], twr[1], T.w * 0.72, T.w * 0.72, T.h * 0.3, sc.rot, [0, 0.8, T.h, 0], T.h * 0.62); M.box(twr[0], twr[1], 3, 3, T.h * 0.08, sc.rot, [5, 0.5, T.h, 0], T.h * 0.92);
|
||||
const T = sc.tower; M.box(twr[0], twr[1], T.w, T.w, T.h * 0.62, sc.rot, [0, 0.8, T.h, 0]); M.box(twr[0], twr[1], T.w * 0.72, T.w * 0.72, T.h * 0.3, sc.rot, [0, 0.8, T.h, 0], T.h * 0.62); M.box(twr[0], twr[1], 3, 3, T.h * 0.08, sc.rot, [5, 0.5, T.h, 0], T.h * 0.92); M.box(twr[0], twr[1], T.w * 0.76, T.w * 0.76, 5, sc.rot, [5, 0.5, T.h, 0], T.h * 0.9);
|
||||
}
|
||||
}
|
||||
function genStadium(sc, M) {
|
||||
@@ -2833,8 +2853,8 @@ function genAirfield(sc, M) {
|
||||
}
|
||||
function genPad(sc, M) {
|
||||
M.box(0, 0, 220, 160, 0.5, 0, [4, 0, 0.5, 0]);
|
||||
M.box(-14, 0, 12, 12, 88, 0, [0, 0.55, 88, 0]);
|
||||
M.box(4, 0, 4.2, 4.2, 70, 0, [0, 0.98, 70, 0]);
|
||||
M.box(-14, 0, 12, 12, 88, 0, [7, 0.35, 88, 0]);
|
||||
M.box(4, 0, 4.2, 4.2, 70, 0, [7, 1.0, 70, 0]);
|
||||
M.box(4, 0, 4.4, 4.4, 2, 0, [5, 0.5, 2, 0], 70);
|
||||
for (const [x, y, w, d, h] of [[300, 180, 60, 40, 22], [-260, 220, 90, 30, 12], [400, -260, 30, 30, 14]]) M.box(x, y, w, d, h, 0, [0, 0.4, h, 0]);
|
||||
}
|
||||
@@ -3266,7 +3286,7 @@ add({
|
||||
add({
|
||||
id: 'novosel', short: 'Fort Novosel circuits', fam: 'sky', kind: 'heli', section: 'always', live: true, aspect: 1.5, ageS: 15, watchers: 3100, ride: true,
|
||||
title: 'Fort Novosel helicopter school — jump into the chopper', place: 'Cairns Army Airfield, Alabama', lat: 31.276, lon: -85.713,
|
||||
camera: { lat: 31.2705, lon: -85.7075, range: 0.0011, heading: -0.65, tilt: 1.15 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2),
|
||||
camera: { lat: 31.2745, lon: -85.7105, range: 0.00062, heading: -0.65, tilt: 0.9 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2), labels3d: [[31.276, -85.713, 'Cairns AAF', 0.05], [31.36, -85.63, 'Stagefield A', 0.05]],
|
||||
metric: () => [`${GE.HELI_CIRCUITS.filter((h) => h.field).length}<small>in circuits</small>`, 'Cairns + 3 stagefields · 500 ft AGL'],
|
||||
metrics: () => [{ v: GE.HELI_CIRCUITS.filter((h) => h.field).length, u: '', l: 'Helicopters in circuits', s: 'ADS-B · 15 s' }, { v: 500, u: 'ft', l: 'Pattern altitude', s: 'Observed' }, { v: 5.5, u: 'min', l: 'Circuit time', s: 'Derived' }],
|
||||
brief: { lede: 'Student pilots are flying rectangular traffic patterns at Cairns and three stagefields: upwind, crosswind, downwind, base, final — over and over. Tap Ride to sit in the left seat of one of them.', series: [{ label: 'Helicopters aloft', unit: '', data: [2, 4, 9, 14, 17, 19, 19, 18, 19, 17, 19, 19], src: 'ADS-B / MLAT · hourly · 15 s', fmt: (v) => v }, { label: 'Groundspeed, HAWK 21', unit: 'kt', data: Array.from({ length: 40 }, (_, i) => 70 + 12 * Math.sin(i / 3.2) + (i % 10 > 7 ? -30 : 0)), src: 'ADS-B · 1 s · 15 s', fmt: (v) => Math.round(v) }], sources: [['ADS-B / MLAT', '15 s', 'live']] },
|
||||
@@ -3274,7 +3294,7 @@ add({
|
||||
add({
|
||||
id: 'launch', short: 'Launch · SLC-40', fam: 'orbit', kind: 'launch', section: 'now', live: true, aspect: 1.5, ageS: 300, watchers: 33800,
|
||||
title: 'Rideshare launch from SLC-40 — countdown', place: 'Cape Canaveral, Florida', lat: 28.5618, lon: -80.5772,
|
||||
camera: { lat: 28.552, lon: -80.590, range: 0.0011, heading: 0.75, tilt: 1.2 }, layers: ['launches', 'flights', 'ais', 'tracks'], source: 'Range schedule (illustrative)',
|
||||
camera: { lat: 28.5585, lon: -80.5825, range: 0.00045, heading: 0.85, tilt: 1.28 }, labels3d: [[28.5618, -80.5772, 'SLC-40 · vehicle on pad', 0.11]], thumbCam: { lat: 28.25, lon: -80.95, range: 0.028, heading: 0.75, tilt: 1.0 }, layers: ['launches', 'flights', 'ais', 'tracks'], source: 'Range schedule (illustrative)',
|
||||
metric: () => [`<span class="num">T−${fmt.dur((LAUNCH_T0 - Date.now()) / 1000)}</span>`, 'Rideshare to SSO · trajectory NE'],
|
||||
metrics: () => [{ v: fmt.dur((LAUNCH_T0 - Date.now()) / 1000), u: '', l: 'Countdown', s: 'Range · 5 min' }, { v: 92, u: '%', l: 'Weather go', s: 'Forecast · 1 h' }, { v: 14, u: '', l: 'Vessels in hazard area', s: 'AIS · 40 s' }],
|
||||
brief: { lede: 'Vehicle is vertical on the pad. The hazard area offshore is still being cleared — 14 vessels remain inside. Trajectory heads northeast along the coast.', series: [{ label: 'Vessels in hazard area', unit: '', data: [41, 39, 36, 33, 31, 28, 24, 21, 19, 17, 15, 14], src: 'AIS · 15 min · 40 s', fmt: (v) => v }], sources: [['Range schedule (illustrative)', '5 min', 'live'], ['AIS', '40 s', 'live']] },
|
||||
@@ -3289,16 +3309,16 @@ add({
|
||||
});
|
||||
add({
|
||||
id: 'concert', short: 'Stadium encore · 4D', fam: 'eyes', kind: 'recon', section: 'recon', live: false, aspect: 1.25, ageS: 20 * 3600, watchers: 88200, thread: 'concert', d4: true,
|
||||
title: 'Stadium encore in 4D — from 2,311 crowd captures', place: 'Wembley, London · last night', lat: 51.556, lon: -0.2796,
|
||||
camera: { lat: 51.5552, lon: -0.2810, range: 0.00034, heading: 0.55, tilt: 1.12 }, time: { at: NOW - 20 * 3600000 + 0.0, start: NOW - 22 * 3600000, end: NOW - 19 * 3600000 },
|
||||
title: 'Stadium encore in 4D — from 2,311 crowd captures', place: 'Wembley, London · ' + GE.dayWord(GE.T_CONCERT, 'night'), lat: 51.556, lon: -0.2796,
|
||||
camera: { lat: 51.5553, lon: -0.2806, range: 0.00026, heading: 0.55, tilt: 1.0 }, time: { at: GE.T_CONCERT, start: GE.T_CONCERT - 3600000, end: GE.T_CONCERT + 1800000 },
|
||||
layers: ['signed', 'holo', 'venues', 'cctv'], source: 'Thread · 2,311 signed captures', nightThumb: true,
|
||||
metric: () => ['2,311<small>captures</small>', '4D · 41 min · 86 seat blocks'], metrics: () => [{ v: '2,311', u: '', l: 'Signed captures', s: 'Ledger · 20 h' }, { v: 86, u: '', l: 'Seat blocks covered', s: 'Derived' }, { v: '±9', u: 'ms', l: 'Time alignment', s: 'Audio sync' }],
|
||||
brief: { lede: 'A crowd-sourced reconstruction of last night’s encore: 2,311 signed phone captures aligned by audio fingerprint and IMU traces into a single 4D scene you can scrub. Performer and attendee identities are not part of the record.', series: [{ label: 'Captures per minute', unit: '', data: [3, 5, 9, 21, 64, 140, 188, 162, 120, 96, 72, 40], src: 'Signed bundles · 1 min', fmt: (v) => v }], sources: [['Signed crowd captures (illustrative)', '20 h', 'stale']] },
|
||||
});
|
||||
add({
|
||||
id: 'climb', short: 'Tower climb · 4D', fam: 'eyes', kind: 'recon', section: 'recon', live: false, aspect: 1.5, ageS: 9 * 3600, watchers: 41500, thread: 'climb', d4: true,
|
||||
title: 'Free-climber on a Midtown tower — reconstructed', place: 'Midtown Manhattan · this morning', lat: 40.7553, lon: -73.9838,
|
||||
camera: { lat: 40.7528, lon: -73.9870, range: 0.00027, heading: 0.62, tilt: 1.2 }, time: { at: NOW - 9 * 3600000, start: NOW - 11 * 3600000, end: NOW - 7 * 3600000 },
|
||||
title: 'Free-climber on a Midtown tower — reconstructed', place: 'Midtown Manhattan · ' + GE.dayWord(GE.T_CLIMB, 'morning'), lat: 40.7553, lon: -73.9838,
|
||||
camera: { lat: 40.7532, lon: -73.9865, range: 0.00055, heading: 0.62, tilt: 1.12 }, time: { at: GE.T_CLIMB + 1.2 * 3600000, start: GE.T_CLIMB - 1800000, end: GE.T_CLIMB + 3 * 3600000 },
|
||||
layers: ['helis', 'signed', 'holo', 'cctv'], source: 'Thread · 3 news helicopters (ADS-B)',
|
||||
metric: () => ['41<small>clips</small> · 3<small>helis</small>', 'Fictional tower · ascent 2 h 14 m'], metrics: () => [{ v: 41, u: '', l: 'Witness clips', s: 'Ledger · 9 h' }, { v: 3, u: '', l: 'News helicopters', s: 'ADS-B · 9 h' }, { v: 412, u: 'm', l: 'Tower height', s: 'Model (fictional tower)' }],
|
||||
brief: { lede: 'Three news helicopters orbited the tower for two hours; their ADS-B tracks give precise camera positions for every broadcast frame. Combined with 41 signed street-level clips, the climber’s height over time is reconstructed to ±3 m. The tower and people are fictional.', series: [{ label: 'Climber height (reconstructed)', unit: 'm', data: Array.from({ length: 30 }, (_, i) => Math.min(380, i * 14 + Math.sin(i) * 6)), band: 3, src: 'Hypothesis H2 · ±3 m', fmt: (v) => Math.round(v) }], sources: [['ADS-B: 3 helicopters', '9 h', 'stale'], ['Signed street clips (illustrative)', '9 h', 'stale']] },
|
||||
@@ -3370,7 +3390,7 @@ SIT.sections = [
|
||||
// dynamic cameras
|
||||
SIT.cameraFor = function (s) {
|
||||
if (s.id === 'starlink') { const g = GE.world.sun.gmst; const p = GE.satAt(GE.SATS.find((x) => x.kind === 5), GE.world.t, g); const [la, lo] = geo.ll(p); return { lat: la - 6, lon: lo - 8, range: 0.32, heading: 0.6, tilt: 0.85 }; }
|
||||
if (s.id === 'squawk') { const f = GE.FLIGHTS[GE.world.squawks[0]]; const p = GE.flightAt(f, GE.world.t) || { lat: 50, lon: -30 }; return { lat: p.lat - 1.2, lon: p.lon - 1, range: 0.06, heading: 0.3, tilt: 0.85 }; }
|
||||
if (s.id === 'squawk') { const f = GE.FLIGHTS[GE.world.squawks[0]]; const p = GE.flightAt(f, GE.world.t) || { lat: 50, lon: -30 }; return { lat: p.lat - 4, lon: p.lon - 3, range: 0.3, heading: 0.3, tilt: 0.75 }; }
|
||||
if (s.id === 'iss') { const p = GE.satAt(GE.ISS, GE.world.t, GE.world.sun.gmst); const [la, lo] = geo.ll(p); return { lat: la - 9, lon: lo - 4, range: 0.48, heading: 0.35, tilt: 0.78 }; }
|
||||
return s.camera;
|
||||
};
|
||||
@@ -3720,7 +3740,7 @@ SH.open = function (id, o = {}) {
|
||||
bus.emit('layers', W.layers);
|
||||
if (s.time) { W.setWindow({ start: s.time.start ?? s.time.at - 6 * 3600000, end: s.time.end ?? s.time.at + 6 * 3600000 }); W.setTime(s.time.at); }
|
||||
W.auroraK = s.auroraK ?? 0.85;
|
||||
GE.setExtras && GE.setExtras(s.id);
|
||||
GE.setExtras && GE.setExtras(s.id); GE.setLabels3d(s.thread ? GE.threads.data[s.thread].labels : s.labels3d);
|
||||
// camera
|
||||
const cam = GE.SIT.cameraFor(s);
|
||||
const hs = SH.heights();
|
||||
@@ -3740,7 +3760,7 @@ SH.open = function (id, o = {}) {
|
||||
SH.close = function (silent, keepTime) {
|
||||
if (!SH.id) return; const id = SH.id; const W = GE.world;
|
||||
const el = sheetEl(); el.classList.remove('drag', 'wopen'); el.style.transform = APP.isWide() ? '' : 'translateY(100%)';
|
||||
SH.id = null; W.focus = null; GE.setExtras && GE.setExtras(null);
|
||||
SH.id = null; W.focus = null; GE.setExtras && GE.setExtras(null); GE.setLabels3d(null);
|
||||
if (SH._savedLayers) { W.layers.forEach((l, i) => (l.on = SH._savedLayers[i])); SH._savedLayers = null; bus.emit('layers', W.layers); }
|
||||
if (!keepTime) { W.setWindow(null); W.setTime(null); } W.auroraK = 0.85;
|
||||
APP.setMode('');
|
||||
@@ -4309,7 +4329,7 @@ CAP.frame = function (view) {
|
||||
CAP._sats.sort((a, b) => a.km - b.km); CAP._sats = CAP._sats.slice(0, 3);
|
||||
}
|
||||
for (const o of CAP._sats) { const p = GE.satAt(o.s, CAP.t, sun.gmst); want.push({ k: 's' + o.s.name, v: p, c: 'var(--orbit)', i: 'satellite', t: o.s.name, s: `${Math.round(o.s.alt)} km up · ${Math.round(o.el)}° elev`, d: o.km }); }
|
||||
const seen = new Set(); let n = 0;
|
||||
const seen = new Set(); let n = 0; const used = [];
|
||||
for (const it of want.sort((a, b) => a.d - b.d)) {
|
||||
const s = W.project(view, it.v); let el = host.querySelector(`[data-k="${it.k}"]`);
|
||||
const onScreen = s && s.x > 8 && s.x < view.w - 8 && s.y > 60 && s.y < view.h - 230;
|
||||
@@ -4318,7 +4338,8 @@ CAP.frame = function (view) {
|
||||
if (!el) { el = h('div', { 'data-k': it.k }); el.innerHTML = `<div class="reticle" style="--c:${it.c}"></div><div class="callout glass" style="--c:${it.c}"><span class="ci">${ic(it.i, 'sm')}</span><span><b>${esc(it.t)}</b><span data-s></span></span></div>`; host.append(el); }
|
||||
el.querySelector('[data-s]').textContent = it.s;
|
||||
el.querySelector('.reticle').style.transform = `translate(${s.x}px, ${s.y}px)`;
|
||||
const flip = s.x > view.w - 210; el.querySelector('.callout').style.transform = flip ? `translate(${s.x - 24}px, ${s.y}px) translate(-100%, -50%)` : `translate(${s.x + 24}px, ${s.y}px) translate(0, -50%)`;
|
||||
let cy = s.y; for (const u of used) if (Math.abs(u - cy) < 44) cy = u + 44; used.push(cy);
|
||||
const flip = s.x > view.w - 210; el.querySelector('.callout').style.transform = flip ? `translate(${s.x - 24}px, ${cy}px) translate(-100%, -50%)` : `translate(${s.x + 24}px, ${cy}px) translate(0, -50%)`;
|
||||
}
|
||||
host.querySelectorAll('[data-k]').forEach((e) => { if (!seen.has(e.dataset.k)) e.remove(); });
|
||||
// witness sensors
|
||||
@@ -4398,7 +4419,8 @@ const THREADS = {
|
||||
koshi: {
|
||||
id: 'koshi', sit: 'koshi', title: 'Bhote Koshi outburst flood', place: 'Rasuwagadhi → Syaphrubesi, Nepal', date: '26 Aug 2026',
|
||||
window: { start: KT - 1.5 * HR_, end: KT + 6 * HR_ }, people: ['hydro', 'kora', 'tamang', 'geo', 'sky', 'ais'],
|
||||
cam: { lat: 28.205, lon: 85.352, range: 0.0042, heading: 0.32, tilt: 1.02 },
|
||||
cam: { lat: 28.2, lon: 85.35, range: 0.0046, heading: 0.32, tilt: 0.98 },
|
||||
labels: [[28.1600, 85.3395, 'Syaphrubesi'], [28.2560, 85.3690, 'Timure'], [28.2802, 85.3797, 'Rasuwagadhi bridge']],
|
||||
obs: [
|
||||
{ id: 'O1', i: 'frame', f: 'infra', t: 'Optical satellite tile — before', src: 'Public 10 m optical archive', at: Date.UTC(2026, 7, 24, 4, 52), meta: ['10 m', 'cloud 6%'] },
|
||||
{ id: 'O2', i: 'frame', f: 'infra', t: 'Optical satellite tile — after: new scour along 9.4 km of channel', src: 'Public 10 m optical archive', at: Date.UTC(2026, 7, 27, 4, 58), meta: ['10 m', 'cloud 12%'] },
|
||||
@@ -4418,24 +4440,25 @@ const THREADS = {
|
||||
{ id: 'H4', by: 'ai', ai: true, text: 'Peak stage at Syaphrubesi lagged the visible crest by ~20 min — consistent with debris damming at the bridge.', conf: 0.41, ci: [0.22, 0.6], cites: ['O3', 'O6'], agree: 3, dispute: 5, status: 'disputed', prompt: 'Why does the gauge peak late?' },
|
||||
],
|
||||
beats: [
|
||||
{ at: KT, t: '07:45', text: 'Outburst upstream (inferred). The valley is quiet; the river is at its normal 3 m stage.', cites: ['H1', 'O4', 'O9'], cam: { lat: 28.262, lon: 85.372, range: 0.0032, heading: 3.4, tilt: 1.05 } },
|
||||
{ at: KT + 20 * MIN, t: '08:05', text: 'The crest reaches Rasuwagadhi and the border bridge.', cites: ['H2', 'O7'], cam: { lat: 28.272, lon: 85.37, range: 0.0018, heading: 0.6, tilt: 1.12 } },
|
||||
{ at: KT + 27 * MIN, t: '08:12', text: 'A witness on the north bank at Timure films water reaching the road.', cites: ['H3', 'O5'], cam: { lat: 28.247, lon: 85.364, range: 0.0016, heading: 0.35, tilt: 1.15 } },
|
||||
{ at: KT + 61 * MIN, t: '08:46', text: 'Debris piles against the Syaphrubesi bridge deck.', cites: ['O6', 'H4'], cam: { lat: 28.152, lon: 85.336, range: 0.0016, heading: 0.2, tilt: 1.12 } },
|
||||
{ at: KT + 105 * MIN, t: '09:30', text: 'Gauge peaks at 7.8 m — about 20 minutes after the visible crest.', cites: ['O3', 'H4'], cam: { lat: 28.15, lon: 85.33, range: 0.0026, heading: 0.9, tilt: 1.05 } },
|
||||
{ at: KT + 140 * MIN, t: '10:05', text: 'First rescue sortie climbs the valley.', cites: ['O8'], cam: { lat: 28.19, lon: 85.345, range: 0.0045, heading: 0.3, tilt: 1.0 } },
|
||||
{ at: KT, t: '07:45', text: 'Outburst upstream (inferred). The valley is quiet; the river is at its normal 3 m stage.', cites: ['H1', 'O4', 'O9'], cam: { lat: 28.268, lon: 85.37, range: 0.0032, heading: 3.55, tilt: 0.98 } },
|
||||
{ at: KT + 20 * MIN, t: '08:05', text: 'The crest reaches Rasuwagadhi and the border bridge.', cites: ['H2', 'O7'], cam: { lat: 28.276, lon: 85.376, range: 0.0026, heading: 2.6, tilt: 0.95 } },
|
||||
{ at: KT + 27 * MIN, t: '08:12', text: 'A witness on the north bank at Timure films water reaching the road.', cites: ['H3', 'O5'], cam: { lat: 28.252, lon: 85.368, range: 0.0027, heading: 3.4, tilt: 0.96 } },
|
||||
{ at: KT + 61 * MIN, t: '08:46', text: 'Debris piles against the Syaphrubesi bridge deck.', cites: ['O6', 'H4'], cam: { lat: 28.16, lon: 85.338, range: 0.0027, heading: 0.25, tilt: 0.96 } },
|
||||
{ at: KT + 105 * MIN, t: '09:30', text: 'Gauge peaks at 7.8 m — about 20 minutes after the visible crest.', cites: ['O3', 'H4'], cam: { lat: 28.155, lon: 85.332, range: 0.0032, heading: 0.9, tilt: 0.98 } },
|
||||
{ at: KT + 140 * MIN, t: '10:05', text: 'First rescue sortie climbs the valley.', cites: ['O8'], cam: { lat: 28.2, lon: 85.35, range: 0.0045, heading: 0.3, tilt: 0.98 } },
|
||||
],
|
||||
suggestions: ['Align the witness clip to the corridor using the bridge as a landmark', 'Estimate crest speed between Timure and Syaphrubesi', 'What would falsify H1?'],
|
||||
fallback: 'Using the Syaphrubesi bridge as a fixed landmark, clip [O6] was taken from the east abutment looking upstream; matching its horizon to the IMU pitch places the crest front 85 ± 20 m upstream of the bridge at 08:46 UTC. That timing agrees with the gauge [O3] if the crest travelled 4.6 ± 0.6 m/s from Timure [O5].\nConfidence: 0.63',
|
||||
},
|
||||
concert: {
|
||||
id: 'concert', sit: 'concert', title: 'Stadium encore in 4D', place: 'Wembley, London', date: 'last night',
|
||||
window: { start: Date.now() - 21 * HR_, end: Date.now() - 19.5 * HR_ }, people: ['crowd', 'sky', 'kora', 'geo'],
|
||||
id: 'concert', sit: 'concert', title: 'Stadium encore in 4D', place: 'Wembley, London', date: GE.dayWord(GE.T_CONCERT, 'night'),
|
||||
window: { start: GE.T_CONCERT - 1 * HR_, end: GE.T_CONCERT + 0.5 * HR_ }, people: ['crowd', 'sky', 'kora', 'geo'],
|
||||
cam: { lat: 51.5546, lon: -0.2822, range: 0.00042, heading: 0.6, tilt: 1.1 },
|
||||
labels: [[51.556, -0.2803, 'Stage · encore', 0.05]],
|
||||
obs: [
|
||||
{ id: 'O1', i: 'seal', f: 'eyes', t: '2,311 signed phone captures from 86 seat blocks', src: 'Signed bundles (illustrative)', at: Date.now() - 20 * HR_, meta: ['IMU ✓ on 2,204', 'GPS indoor ±12 m'] },
|
||||
{ id: 'O2', i: 'wave', f: 'eyes', t: 'Audio fingerprints align 1,980 clips to ±9 ms', src: 'Derived', at: Date.now() - 19.8 * HR_, meta: ['common track'] },
|
||||
{ id: 'O3', i: 'video', f: 'eyes', t: 'Venue public camera, concourse north', src: 'Public feed (illustrative)', at: Date.now() - 20 * HR_, meta: ['720p'] },
|
||||
{ id: 'O1', i: 'seal', f: 'eyes', t: '2,311 signed phone captures from 86 seat blocks', src: 'Signed bundles (illustrative)', at: GE.T_CONCERT, meta: ['IMU ✓ on 2,204', 'GPS indoor ±12 m'] },
|
||||
{ id: 'O2', i: 'wave', f: 'eyes', t: 'Audio fingerprints align 1,980 clips to ±9 ms', src: 'Derived', at: GE.T_CONCERT + 0.2 * HR_, meta: ['common track'] },
|
||||
{ id: 'O3', i: 'video', f: 'eyes', t: 'Venue public camera, concourse north', src: 'Public feed (illustrative)', at: GE.T_CONCERT, meta: ['720p'] },
|
||||
{ id: 'O4', i: 'ticket', f: 'ground', t: 'Seat map & stage geometry', src: 'Published venue plan', at: Date.now() - 30 * 24 * HR_, meta: ['static'] },
|
||||
],
|
||||
hyps: [
|
||||
@@ -4443,21 +4466,22 @@ const THREADS = {
|
||||
{ id: 'H2', by: 'geo', text: 'The pyro cue at 22:44 is visible from 86 of 92 seat blocks; 6 blocks are occluded by the roof cantilever.', conf: 0.71, ci: [0.6, 0.8], cites: ['O1', 'O4'], agree: 44, dispute: 6, status: 'supported' },
|
||||
],
|
||||
beats: [
|
||||
{ at: Date.now() - 20 * HR_ - 10 * MIN, t: '22:41', text: 'Lights down. 2,311 phones come up together.', cites: ['H1'], cam: { lat: 51.5552, lon: -0.2815, range: 0.00035, heading: 0.9, tilt: 1.15 } },
|
||||
{ at: Date.now() - 20 * HR_, t: '22:44', text: 'Pyro cue — seen from 86 blocks at once.', cites: ['H2'], cam: { lat: 51.5568, lon: -0.2778, range: 0.0003, heading: 3.6, tilt: 1.12 } },
|
||||
{ at: Date.now() - 20 * HR_ + 12 * MIN, t: '22:56', text: 'Final chorus; the crowd reconstruction is densest here.', cites: ['O1', 'O2'], cam: { lat: 51.556, lon: -0.2796, range: 0.0006, heading: 2.2, tilt: 0.9 } },
|
||||
{ at: GE.T_CONCERT - 3 * MIN, t: '22:41', text: 'Lights down. 2,311 phones come up together.', cites: ['H1'], cam: { lat: 51.5552, lon: -0.2815, range: 0.00035, heading: 0.9, tilt: 1.15 } },
|
||||
{ at: GE.T_CONCERT, t: '22:44', text: 'Pyro cue — seen from 86 blocks at once.', cites: ['H2'], cam: { lat: 51.5568, lon: -0.2778, range: 0.0003, heading: 3.6, tilt: 1.12 } },
|
||||
{ at: GE.T_CONCERT + 12 * MIN, t: '22:56', text: 'Final chorus; the crowd reconstruction is densest here.', cites: ['O1', 'O2'], cam: { lat: 51.556, lon: -0.2796, range: 0.0006, heading: 2.2, tilt: 0.9 } },
|
||||
],
|
||||
suggestions: ['Which seat blocks saw the pyro cue?', 'Align the concourse camera to the crowd timeline'],
|
||||
fallback: 'The concourse camera [O3] shows the crowd surge 2.1 s after the audio onset in [O2]; offsetting its clock by −2.1 s aligns it with 1,980 crowd clips [O1].\nConfidence: 0.58',
|
||||
},
|
||||
climb: {
|
||||
id: 'climb', sit: 'climb', title: 'Free-climber on a Midtown tower', place: 'Midtown Manhattan (fictional tower)', date: 'this morning',
|
||||
window: { start: Date.now() - 11 * HR_, end: Date.now() - 7 * HR_ }, people: ['news', 'sky', 'kora', 'geo', 'ais'],
|
||||
cam: { lat: 40.7533, lon: -73.9866, range: 0.00034, heading: 0.62, tilt: 1.2 },
|
||||
id: 'climb', sit: 'climb', title: 'Free-climber on a Midtown tower', place: 'Midtown Manhattan (fictional tower)', date: GE.dayWord(GE.T_CLIMB, 'morning'),
|
||||
window: { start: GE.T_CLIMB - 0.5 * HR_, end: GE.T_CLIMB + 3 * HR_ }, people: ['news', 'sky', 'kora', 'geo', 'ais'],
|
||||
cam: { lat: 40.7532, lon: -73.9865, range: 0.00055, heading: 0.62, tilt: 1.12 },
|
||||
labels: [[40.7553, -73.9838, 'Meridian Tower (fictional)', 0.43]],
|
||||
obs: [
|
||||
{ id: 'O1', i: 'helicopter', f: 'sky', t: 'ADS-B tracks: 3 news helicopters orbiting the tower', src: 'ADS-B (illustrative callsigns)', at: Date.now() - 9 * HR_, meta: ['1 Hz', '2 h 14 m'] },
|
||||
{ id: 'O2', i: 'video', f: 'eyes', t: 'Broadcast frames, time-coded', src: 'Public broadcast (illustrative)', at: Date.now() - 9 * HR_, meta: ['29.97 fps'] },
|
||||
{ id: 'O3', i: 'seal', f: 'eyes', t: '41 signed street-level clips', src: 'Signed bundles (illustrative)', at: Date.now() - 9 * HR_, meta: ['IMU ✓', 'GPS ±5 m'] },
|
||||
{ id: 'O1', i: 'helicopter', f: 'sky', t: 'ADS-B tracks: 3 news helicopters orbiting the tower', src: 'ADS-B (illustrative callsigns)', at: GE.T_CLIMB + 1.2 * HR_, meta: ['1 Hz', '2 h 14 m'] },
|
||||
{ id: 'O2', i: 'video', f: 'eyes', t: 'Broadcast frames, time-coded', src: 'Public broadcast (illustrative)', at: GE.T_CLIMB + 1.2 * HR_, meta: ['29.97 fps'] },
|
||||
{ id: 'O3', i: 'seal', f: 'eyes', t: '41 signed street-level clips', src: 'Signed bundles (illustrative)', at: GE.T_CLIMB + 1.2 * HR_, meta: ['IMU ✓', 'GPS ±5 m'] },
|
||||
{ id: 'O4', i: 'cube', f: 'infra', t: 'Tower model, 412 m (fictional building)', src: 'Model', at: Date.now() - 400 * 24 * HR_, meta: ['static'] },
|
||||
],
|
||||
hyps: [
|
||||
@@ -4465,9 +4489,9 @@ const THREADS = {
|
||||
{ id: 'H2', by: 'geo', text: 'Climber height over time is recoverable to ±3 m by triangulating helicopter camera positions (from ADS-B) with frame bearings.', conf: 0.69, ci: [0.55, 0.8], cites: ['O1', 'O2', 'O4'], agree: 33, dispute: 7, status: 'supported' },
|
||||
],
|
||||
beats: [
|
||||
{ at: Date.now() - 10.5 * HR_, t: '06:52', text: 'First witness clip from the street: the climber is at the 3rd setback.', cites: ['H1', 'O3'], cam: { lat: 40.7538, lon: -73.9852, range: 0.00022, heading: 0.5, tilt: 1.3 } },
|
||||
{ at: Date.now() - 9.5 * HR_, t: '07:55', text: 'Three news helicopters in orbit; their ADS-B tracks pin each broadcast camera.', cites: ['O1', 'H2'], cam: { lat: 40.751, lon: -73.99, range: 0.0012, heading: 0.7, tilt: 1.0 } },
|
||||
{ at: Date.now() - 8.6 * HR_, t: '09:06', text: 'The climber reaches the crown at 384 m.', cites: ['H2'], cam: { lat: 40.7549, lon: -73.9848, range: 0.0003, heading: 0.4, tilt: 1.42 } },
|
||||
{ at: GE.T_CLIMB, t: '06:52', text: 'First witness clip from the street: the climber is at the 3rd setback.', cites: ['H1', 'O3'], cam: { lat: 40.7538, lon: -73.9852, range: 0.00022, heading: 0.5, tilt: 1.3 } },
|
||||
{ at: GE.T_CLIMB + 63 * MIN, t: '07:55', text: 'Three news helicopters in orbit; their ADS-B tracks pin each broadcast camera.', cites: ['O1', 'H2'], cam: { lat: 40.751, lon: -73.99, range: 0.0012, heading: 0.7, tilt: 1.0 } },
|
||||
{ at: GE.T_CLIMB + 134 * MIN, t: '09:06', text: 'The climber reaches the crown at 384 m.', cites: ['H2'], cam: { lat: 40.7549, lon: -73.9848, range: 0.0003, heading: 0.4, tilt: 1.42 } },
|
||||
],
|
||||
suggestions: ['Triangulate the climber at 08:30 from two helicopters', 'Which face was the route on?'],
|
||||
fallback: 'At 08:30 the bearings from N9 NEWS and SKY 4 [O1] to the climber in broadcast frames [O2] intersect 232 ± 4 m up the northeast face of the tower model [O4].\nConfidence: 0.66',
|
||||
@@ -4479,6 +4503,7 @@ THR.data = THREADS;
|
||||
const EXTRAS = {};
|
||||
GE.extrasFor = function (id) {
|
||||
if (EXTRAS[id] !== undefined) return EXTRAS[id];
|
||||
if (id === 'launch') { const sp = [], ln = []; const pad = [28.5618, -80.5772]; pushSpr(sp, geo.vec(pad[0], pad[1], 0.04), [1, 0.86, 0.6, 0.9], 70, 0, -10, -1e9, 1e9, 0, 0, 0.9); for (const [dx, dy] of [[90, 70], [-80, 60], [70, -80], [-90, -70]]) { const [la, lo] = geo.offset(pad[0], pad[1], dx, dy); pushSpr(sp, geo.vec(la, lo, 0.02), [1, 0.9, 0.7, 0.8], 26, 0, -10, -1e9, 1e9, 0, 0, 0.6); } return (EXTRAS[id] = { sprites: [makeSpriteSet('xl', sp)], lines: null }); }
|
||||
const T = THREADS[id] || (GE.SIT.byId[id]?.thread && THREADS[GE.SIT.byId[id].thread]);
|
||||
if (!T) return (EXTRAS[id] = null);
|
||||
const sp = [], ln = [];
|
||||
@@ -4509,6 +4534,7 @@ GE.extrasFor = function (id) {
|
||||
const set = { sprites: sp.length ? [makeSpriteSet('x' + id, sp)] : null, lines: ln.length ? [makeLineSet(ln)] : null };
|
||||
return (EXTRAS[id] = set);
|
||||
};
|
||||
GE.setLabels3d = function (L) { GE.extraLabels.clear(); for (const [la, lo, t, alt] of L || []) { const z = GE.scenes.byId.koshi.height && Math.abs(la - 28.2) < 0.2 && Math.abs(lo - 85.35) < 0.2 ? GE.scenes.groundAt(la, lo) / 1000 + 0.12 : alt || 0.05; GE.extraLabels.set('x:' + t, { lat: la, lon: lo, alt: z, html: `<i style="--c:var(--gold)"></i>${esc(t)}`, w: t.length * 6.6 + 32, cls: 'glass' }); } };
|
||||
GE.setExtras = function (id) { GE.world.activeExtras = id ? GE.extrasFor(id) : null; GE.world.activeExtrasId = id; };
|
||||
|
||||
/* ---------- ledger (real SHA-256 hash chain) ---------- */
|
||||
@@ -4560,7 +4586,7 @@ THR.openWs = function (id) {
|
||||
$('#tlist').style.display = 'none';
|
||||
const ws = $('#ws'); ws.classList.add('on');
|
||||
W.setWindow(T.window); W.setTime(T.beats[Math.min(2, T.beats.length - 1)].at);
|
||||
GE.setExtras(T.id);
|
||||
GE.setExtras(T.id); GE.setLabels3d(T.labels);
|
||||
for (const lid of ['signed', 'holo', 'helis']) W.layerById[lid].on = true;
|
||||
const wide = APP.isWide();
|
||||
Cam.autoSpin = 0; Cam.flyTo({ ...T.cam, shiftY: wide ? 0 : 0.6, shiftX: 0 });
|
||||
@@ -4570,12 +4596,12 @@ THR.openWs = function (id) {
|
||||
THR.closeWs = function (silent) {
|
||||
if (!THR.ws) return; THR.stop(); THR.ws = null; const W = GE.world;
|
||||
$('#ws').classList.remove('on'); $('#ws').innerHTML = ''; const tl = $('#tlist'); if (tl) tl.style.display = '';
|
||||
W.setWindow(null); W.setTime(null); GE.setExtras(null); APP.setMode('');
|
||||
W.setWindow(null); W.setTime(null); GE.setExtras(null); GE.setLabels3d(null); APP.setMode('');
|
||||
if (!silent) Cam.flyTo(APP.heroCam());
|
||||
};
|
||||
THR.renderWs = function () {
|
||||
const { T, seg } = THR.ws; const ws = $('#ws');
|
||||
ws.innerHTML = `<div class="wsh"><button class="circ glass" id="wsBack" aria-label="Back">${ic('chevl')}</button><div class="tt glass"><span style="min-width:0;flex:1"><b>${esc(T.title)}</b><span>${esc(T.place)} · ${esc(T.date)}</span></span><span class="presence"><span class="stack">${T.people.slice(0, 4).map((k, i) => `<span class="avatar sm ${i < 3 ? 'on' : ''}" style="--h:${P[k].hue}">${P[k].h[0].toUpperCase()}</span>`).join('')}</span></span></div></div>
|
||||
ws.innerHTML = `<div class="wsh"><button class="circ glass" id="wsBack" aria-label="Back">${ic('chevl')}</button><div class="tt glass"><span style="min-width:0;flex:1"><b>${esc(T.title)}</b><span class="sub">${esc(T.place)} · ${esc(T.date)}</span></span><span class="presence"><span class="stack">${T.people.slice(0, 4).map((k, i) => `<span class="avatar sm ${i < 3 ? 'on' : ''}" style="--h:${P[k].hue}">${P[k].h[0].toUpperCase()}</span>`).join('')}</span></span></div></div>
|
||||
<div class="body glass strong"><div class="seg" id="wsSeg">${[['obs', 'Observations'], ['hyp', 'Hypotheses'], ['nar', 'Narrative'], ['led', 'Ledger']].map(([k, l]) => `<button data-s="${k}" class="${seg === k ? 'on' : ''}">${l}</button>`).join('')}</div><div class="wsc" id="wsc"></div>
|
||||
${seg === 'hyp' ? `<div class="composer glass strong" id="composer"><div class="sug">${T.suggestions.map((s) => `<button>${esc(s)}</button>`).join('')}</div><div class="row"><textarea id="aiIn" rows="1" placeholder="Ask AI to refine this reconstruction…"></textarea><button class="send" id="aiSend" aria-label="Send">${ic('arrowup')}</button></div></div>` : ''}</div>`;
|
||||
$('#wsBack').addEventListener('click', () => THR.closeWs());
|
||||
@@ -4790,7 +4816,7 @@ function frame(now) {
|
||||
APP.updateLabels(view); GE.ride.frame(view); GE.capture.frame(view);
|
||||
}
|
||||
if (APP.tab === 'globe' || APP.isWide() || APP.mode) GE.gui.ribbonFrame();
|
||||
GE.thumbs.step(APP.intro ? 14 : 8);
|
||||
if (APP.intro || !(Cam.anim || Cam.interacting || GE.thumbs._busyUntil > now)) GE.thumbs.step(APP.intro ? 14 : 8);
|
||||
if (now - lastSec > 1000) { lastSec = now; W.updateCounts(); GE.now.tick(); GE.sheet.tick(); GE.you.tick(); }
|
||||
governor(dt);
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
@@ -13,8 +13,8 @@ const src = process.argv[2];
|
||||
if (!src) { console.error('usage: node prep-textures.mjs <download-dir>'); process.exit(1); }
|
||||
const out = (f) => path.join(here, 'assets', f);
|
||||
const jobs = [
|
||||
['earth-blue-marble.jpg', 'day.webp', (s) => s.resize(4096, 2048).webp({ quality: 74, smartSubsample: true, effort: 6 })],
|
||||
['BlackMarble_2016_3km.jpg', 'night.webp', (s) => s.resize(4096, 2048, { kernel: 'lanczos3' }).webp({ quality: 74, smartSubsample: true, effort: 6 })],
|
||||
['earth-blue-marble.jpg', 'day.webp', (s) => s.resize(4096, 2048).webp({ quality: 84, smartSubsample: true, effort: 6 })],
|
||||
['BlackMarble_2016_3km.jpg', 'night.webp', (s) => s.resize(4096, 2048, { kernel: 'lanczos3' }).webp({ quality: 80, smartSubsample: true, effort: 6 })],
|
||||
['clouds.png', 'clouds.webp', (s) => s.resize(2048, 1024).extractChannel('alpha').webp({ quality: 66, effort: 6 })],
|
||||
['earth-water.png', 'water.webp', (s) => s.resize(2048, 1024).greyscale().webp({ quality: 60, effort: 6 })],
|
||||
['earth-topology.png', 'topo.webp', (s) => s.resize(2048, 1024).greyscale().webp({ quality: 74, effort: 6 })],
|
||||
|
||||
@@ -289,7 +289,7 @@ input, textarea { font: inherit; color: inherit; }
|
||||
.hud svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.hud .tape { position: absolute; top: 50%; transform: translateY(-50%); font-size: 15px; font-weight: 600; padding: 6px 9px; border: 1px solid rgba(190,255,200,.55); border-radius: 6px; background: rgba(0,20,8,.25); }
|
||||
.hud .tape small { display: block; font-size: 9.5px; opacity: .7; font-weight: 500; }
|
||||
.chat { position: absolute; left: 12px; right: 12px; bottom: calc(var(--sab) + 72px); height: 46%; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; pointer-events: none !important; mask-image: linear-gradient(to top, #000 70%, transparent); -webkit-mask-image: linear-gradient(to top, #000 70%, transparent); }
|
||||
.chat { position: absolute; left: 12px; right: 12px; bottom: calc(var(--sab) + 72px); height: 34%; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; pointer-events: none !important; mask-image: linear-gradient(to top, #000 70%, transparent); -webkit-mask-image: linear-gradient(to top, #000 70%, transparent); }
|
||||
.bub { align-self: flex-start; max-width: 84%; display: flex; gap: 8px; align-items: flex-end; animation: bubIn .5s var(--spring); }
|
||||
.bub .bx { padding: 8px 12px; border-radius: 18px 18px 18px 6px; font-size: 13.5px; line-height: 1.35; }
|
||||
.bub .bx b { display: block; font-size: 11.5px; color: var(--text2); font-weight: 600; margin-bottom: 1px; }
|
||||
@@ -372,7 +372,7 @@ input, textarea { font: inherit; color: inherit; }
|
||||
.ws .wsh > * { pointer-events: auto; }
|
||||
.ws .wsh .tt { flex: 1; min-width: 0; height: 44px; border-radius: 22px; display: flex; align-items: center; gap: 10px; padding: 0 8px 0 14px; }
|
||||
.ws .wsh .tt b { font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }
|
||||
.ws .wsh .tt span { font-size: 11.5px; color: var(--text2); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ws .wsh .tt span.sub { font-size: 11.5px; color: var(--text2); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ws .body { position: absolute; left: 0; right: 0; bottom: 0; top: 40%; border-radius: 28px 28px 0 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.ws .seg { display: flex; margin: 12px 14px 8px; padding: 3px; border-radius: 12px; background: rgba(255,255,255,.07); flex: none; }
|
||||
.ws .seg button { flex: 1; height: 34px; border-radius: 9px; font-size: 13px; font-weight: 600; color: var(--text2); transition: all .25s; }
|
||||
@@ -519,7 +519,10 @@ input, textarea { font: inherit; color: inherit; }
|
||||
@media (min-width: 600px) {
|
||||
:root { --paneW: clamp(360px, 40vw, 440px); }
|
||||
.pane { width: var(--paneW); background: var(--bg); border-right: .5px solid var(--sep); z-index: 22; }
|
||||
.tabwrap { --tabW: var(--paneW); }
|
||||
.tabwrap { --tabW: var(--paneW); padding-left: 8px; padding-right: 8px; gap: 8px; }
|
||||
.tab, .tablens { width: 54px; }
|
||||
.searchbtn { width: 54px; height: 54px; }
|
||||
.tabbar { height: 58px; } .tablens { height: 50px; } .tab { height: 50px; }
|
||||
.acc-host { right: auto; width: calc(var(--paneW) - 24px); }
|
||||
.app[data-tab="globe"] .acc-host { display: flex; }
|
||||
.wide-only { display: block !important; }
|
||||
@@ -543,10 +546,12 @@ input, textarea { font: inherit; color: inherit; }
|
||||
#v-capture { z-index: 45; }
|
||||
.ride { left: var(--paneW); }
|
||||
.concept-tag, .app[data-tab] .concept-tag { top: calc(var(--sat) + 14px); bottom: auto; left: auto; right: 70px; }
|
||||
.app[data-mode="situation"] .concept-tag { right: 124px; }
|
||||
.composer { left: 10px; right: 10px; }
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
:root { --paneW: 440px; }
|
||||
.tab, .tablens { width: 66px; } .searchbtn { width: 62px; height: 62px; } .tabbar { height: 62px; } .tablens { height: 54px; } .tab { height: 54px; }
|
||||
.masonry { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
/* hinge-aware: vertical fold — panes split exactly at the hinge */
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
callsigns and reconstruction claims are illustrative (Concept · illustrative data). */
|
||||
const NOW = Date.now();
|
||||
GE.NOW = NOW;
|
||||
const lastUTC = (h, m) => { const d = new Date(NOW); let t = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), h, m); if (t > NOW - 3600000) t -= 86400000; return t; };
|
||||
GE.T_CONCERT = lastUTC(21, 44); GE.T_CLIMB = lastUTC(10, 52);
|
||||
GE.dayWord = (t, part) => (new Date(t).getUTCDate() === new Date(NOW).getUTCDate() ? 'this ' + part : (part === 'night' ? 'last night' : 'yesterday ' + part)).replace('this night', 'tonight');
|
||||
const R = rng(20261018);
|
||||
|
||||
/* ---------- airports [iata, city, lat, lon, weight] ---------- */
|
||||
|
||||
@@ -142,9 +142,9 @@ vec3 aurora(vec3 n, float nightF){
|
||||
float ml = asin(abs(s)) * 57.2958;
|
||||
float band = 66.5 - uAurora*5.5;
|
||||
float lonm = atan(dot(n, normalize(cross(mp, vec3(0,0,1)))), dot(n, normalize(cross(mp, cross(mp, vec3(0,0,1))))));
|
||||
float curt = 0.45 + 0.55*fbm(vec2(lonm*9. + uT*0.0006, uT*0.00025));
|
||||
float g = exp(-pow((ml-band)/(2.2+uAurora*0.6), 2.)) * curt;
|
||||
float r = exp(-pow((ml-band-3.5)/3.5, 2.)) * 0.25;
|
||||
float curt = 0.25 + 0.75*fbm(vec2(lonm*14. + uT*0.0006, uT*0.00025 + ml*0.15));
|
||||
float g = exp(-pow((ml-band)/(0.7+uAurora*0.25), 2.)) * curt * curt * 0.6;
|
||||
float r = exp(-pow((ml-band-2.5)/2.0, 2.)) * 0.08 * curt;
|
||||
return (vec3(0.12, 1.0, 0.5)*g + vec3(0.75, 0.15, 0.45)*r*0.45) * nightF * uAurora * 0.75;
|
||||
}
|
||||
vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3 emis){
|
||||
@@ -164,14 +164,18 @@ vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3
|
||||
float lot = vnoise(p*0.11)*0.12;
|
||||
vec3 urb = mix(roof*(0.9+lot), vec3(0.075,0.078,0.085), road);
|
||||
// rural: patchwork fields & forest
|
||||
vec2 fid = floor(p/(B*2.6)); float fh = hash12(fid+7.);
|
||||
vec3 field = base * mix(vec3(0.9,1.05,0.8), vec3(1.25,1.1,0.75), fh) * (0.85+0.3*vnoise(p*0.02));
|
||||
float forest = smoothstep(0.45,0.62, fbm(p*0.0016+3.));
|
||||
field = mix(field, base*vec3(0.55,0.75,0.5), forest);
|
||||
vec2 fid = floor(p/(B*2.6)); float fh = mix(hash12(fid+7.), 0.5, clamp(fw/(B*2.6)*1.6, 0., 1.));
|
||||
vec3 field = base * mix(vec3(0.94,1.02,0.9), vec3(1.12,1.06,0.86), fh) * (0.92+0.16*vnoise(p*0.02));
|
||||
float forest = smoothstep(0.5,0.66, fbm(p*0.0011+3.));
|
||||
field = mix(field, base*vec3(0.78,0.88,0.74), forest*0.6);
|
||||
vec3 col = mix(field, urb, urban);
|
||||
// street lights at night along roads
|
||||
float lamps = road * urban * (0.5+0.5*hash12(floor(p/9.)));
|
||||
emis = vec3(1.0,0.58,0.24)*lamps*night*1.1*(1.-aa*0.55);
|
||||
float emin = min(e.x, e.y);
|
||||
float curb = smoothstep(rw*0.55, rw*0.8, emin) * (1. - smoothstep(rw*0.8, rw*0.98, emin));
|
||||
float along2 = (e.x < e.y) ? g.y : g.x;
|
||||
float dotsL = smoothstep(0.75, 0.95, fract(along2*B/28.));
|
||||
float lamps = mix(curb*(0.25 + dotsL*1.6), 0.07, aa) * urban;
|
||||
emis = vec3(1.0,0.56,0.22)*lamps*night*1.0;
|
||||
if (uTraffic > 0.5) {
|
||||
vec2 rp = p/B; float along = abs(f.x-0.5) < abs(f.y-0.5) ? rp.y : rp.x;
|
||||
float cars = smoothstep(0.75, 1., sin(along*38. - uT*2.5 + h*20.)) * road * urban * (1.-aa);
|
||||
@@ -266,7 +270,7 @@ void main(){
|
||||
}
|
||||
// night lights + moonlit black-marble land
|
||||
vec3 lightCol = mix(vec3(1.0,0.66,0.32), vec3(1.0,0.86,0.64), smoothstep(0.4,1.4,lights));
|
||||
vec3 emis = lightCol * lights * 3.2 * nightF * (1. - uCity.x*0.94);
|
||||
vec3 emis = lightCol * lights * 3.2 * nightF * (1. - uCity.x*0.995);
|
||||
emis += pow(nightTex, vec3(2.2)) * vec3(0.5,0.6,1.0) * 0.12 * nightF;
|
||||
emis += emisD;
|
||||
emis *= (1.-cl*0.82);
|
||||
@@ -594,9 +598,12 @@ void main(){
|
||||
vec2 id = floor(f); vec2 fr = fract(f);
|
||||
float win = step(0.18, fr.x)*step(fr.x,0.82)*step(0.22,fr.y)*step(fr.y,0.78);
|
||||
float r = hash12(id + h*91.);
|
||||
float lit = step(1. - (0.12 + 0.5*uNight), r) * win * step(vUV.y, vAttr.z - 1.);
|
||||
float frac = 0.12 + 0.5*uNight;
|
||||
float lit = step(1. - frac, r) * win * step(vUV.y, vAttr.z - 1.);
|
||||
vec3 wc = mix(vec3(1.0,0.72,0.42), vec3(0.75,0.85,1.0), step(0.6, hash12(id*1.3+h)));
|
||||
emis += wc * lit * (0.9 + 0.6*r) * uNight * 2.2;
|
||||
float fwv = length(fwidth(vUV));
|
||||
float pat = mix(lit * (0.9 + 0.6*r), frac * 0.36 * 0.75 * (0.15 + 1.6*fract(h*13.7)*fract(h*13.7)), smoothstep(0.9, 2.6, fwv));
|
||||
emis += wc * pat * uNight * 2.2;
|
||||
base = mix(base, base*0.45 + uSkyCol*0.08, win*0.8);
|
||||
spec = win*(1.-uNight)*0.9 + step(0.68, h)*0.5;
|
||||
} else base *= 0.82;
|
||||
@@ -644,6 +651,10 @@ void main(){
|
||||
base = vec3(0.05);
|
||||
emis = mix(vec3(0.9,0.3,1.0), vec3(0.3,0.8,1.0), 0.5+0.5*sin(uT*1.7+vPos.x*0.05)) * 3.0;
|
||||
heat = 0.9;
|
||||
} else if (type == 7) { // floodlit launch structure
|
||||
base = vec3(0.75, 0.76, 0.78) * (0.6 + 0.4*vAttr.y);
|
||||
emis = vec3(1.0, 0.93, 0.82) * uNight * (0.35 + 0.25*smoothstep(0., 60., vUV.y));
|
||||
heat = 0.6;
|
||||
} else { // houses
|
||||
base = mix(vec3(0.55,0.45,0.38), vec3(0.75,0.72,0.66), vAttr.y);
|
||||
if (n.z > 0.5) base = mix(vec3(0.45,0.2,0.15), vec3(0.3,0.35,0.42), step(0.5, vAttr.y));
|
||||
|
||||
@@ -129,7 +129,7 @@ W.buildStatic = function () {
|
||||
{ const a = []; for (const q of GE.QUAKES) pushSpr(a, geo.vec(q.lat, q.lon, 0.5), [1.0, 0.5, 0.3, 1], 10 + (q.mag - 2.5) * 9, 2, -10, q.t, 1e9, 0, RS(), 0.8); addSet('quakes', makeSpriteSet('quakes', a)); }
|
||||
// fires (+ smoke in alpha pass)
|
||||
{ const a = [], sm = []; for (const f of GE.FIRES) { pushSpr(a, geo.vec(f.lat, f.lon, 0.2), [1, 0.5, 0.15, 1], 4 + Math.sqrt(f.frp) * 0.9, 3, -10, f.t, 1e9, 0.4, RS(), 1.0); if (RS() < 0.35) pushSpr(sm, geo.vec(f.lat + 0.15, f.lon + 0.35, 2), [0.3, 0.28, 0.27, 0.32], 26 + RS() * 30, 11, -10, f.t + 3600, 1e9, 0.8, RS(), 0.5); }
|
||||
addSet('fires', makeSpriteSet('fires', a)); addSet('fires', makeSpriteSet('smoke', sm, 'alpha')); }
|
||||
addSet('fires', makeSpriteSet('fires', a)); addSet('smoke', makeSpriteSet('smoke', sm, 'alpha')); }
|
||||
// perimeters
|
||||
{ const a = []; for (const [la, lo, , , s] of GE.FIRE_CLUSTERS) pushPolyline(a, circlePts(la, lo, s * 111 * 0.9, 40, 0.3), [1, 0.42, 0.24, 0.6], 1); addSet('perims', makeLineSet(a)); }
|
||||
// floods
|
||||
@@ -319,7 +319,7 @@ W.updateDynamic = function () {
|
||||
if (W.on('transit')) for (let i = 0; i < 400; i++) { const c = BIG_CITIES[i % 16]; const ang = hash01(i) * TAU + t / (600 + hash01(i + 3) * 900); const rr = 2 + hash01(i + 9) * 10; const [la, lo] = geo.dest(c.lat, c.lon, ang, rr); pushSpr(a, geo.vec(la, lo, 0.01), [0.62, 0.9, 0.37, 0.95], 5, 0); }
|
||||
if (W.on('wind')) for (const w of WIND) { const age = ((t / w.life + w.ph) % 1 + 1) % 1; let la = w.lat, lo = w.lon; const steps = 4, dtH = (age * w.life) / 3600 / steps; for (let k = 0; k < steps; k++) { const [u, v] = windVel(la, lo, t); la += v * dtH; lo += u * dtH; } const [u, v] = windVel(la, lo, t); const fade = Math.sin(age * Math.PI); pushSpr(a, geo.vec(la, lo, 3), [0.75, 0.85, 1, 0.55 * fade], 14, 15, Math.atan2(u * Math.cos(la * D2R), v), -1e9, 1e9, 0, 0, 0.1); }
|
||||
for (const hh of W.userHolos) pushSpr(a, geo.vec(hh.lat, hh.lon, 0.06), [1, 0.85, 0.45, 1], 24, 6, -10, -1e9, 1e9, 0, 0.2, 0.4);
|
||||
if (W.activeExtrasId === 'climb') { const f = clamp((W.simMs - (Date.now() - 10.5 * 3600000)) / (1.9 * 3600000), 0, 1); const [la, lo] = geo.offset(40.7553, -73.9838, 16, 23.5); pushSpr(a, geo.vec(la, lo, (12 + f * 372) / 1000), [1, 0.3, 0.3, 1], 16, 9, -10, -1e9, 1e9, 0, 0.7, 0.9); }
|
||||
if (W.activeExtrasId === 'climb') { const f = clamp((W.simMs - GE.T_CLIMB) / (2.23 * 3600000), 0, 1); const [la, lo] = geo.offset(40.7553, -73.9838, 16, 23.5); pushSpr(a, geo.vec(la, lo, (12 + f * 372) / 1000), [1, 0.3, 0.3, 1], 16, 9, -10, -1e9, 1e9, 0, 0.7, 0.9); }
|
||||
// situation pins
|
||||
for (const p of W.situationPins) pushSpr(a, geo.vec(p.lat, p.lon, 0.2), p.col, p.size || 16, 9, -10, -1e9, 1e9, 0, p.seed || 0, 0.6);
|
||||
// selection highlight
|
||||
@@ -403,7 +403,8 @@ W.project = function (view, p) { // world unit coords → css px {x,y,z,vis}
|
||||
/* ---------- render ---------- */
|
||||
W.quality = { scale: 1, steps: 12, bloom: true, grain: true };
|
||||
const LOD_MIN = { radar: 0.12 };
|
||||
const LOD = { cctv: 0.07, street: 0.03, alpr: 0.035, bikes: 0.04, streams: 0.4, signed: 0.6, venues: 0.9, power: 0.7, dc: 1.6, dams: 1.6, milbase: 1.6, airports: 1.3, ports: 1.4, imagery: 1.6, fishing: 2.2, radar: 1.6 };
|
||||
W.on = (id) => id === 'smoke' ? !!W.layerById.fires?.on : !!W.layerById[id]?.on;
|
||||
const LOD = { smoke: 0.7, cctv: 0.07, street: 0.03, alpr: 0.035, bikes: 0.04, streams: 0.4, signed: 0.6, venues: 0.9, power: 0.7, dc: 1.6, dams: 1.6, milbase: 1.6, airports: 1.3, ports: 1.4, imagery: 1.6, fishing: 2.2, radar: 1.6 };
|
||||
W.render = function (view, chain, outFb, opts = {}) {
|
||||
const gl = GL.gl, PR = GL.progs;
|
||||
W.matrices(view);
|
||||
@@ -488,7 +489,7 @@ W.render = function (view, chain, outFb, opts = {}) {
|
||||
if (opts.extraSprites) for (const s of opts.extraSprites) { gl.bindVertexArray(s.vao); gl.drawArrays(gl.POINTS, 0, s.count); }
|
||||
// flights
|
||||
if (kindMask[0] || kindMask[1] || kindMask[3]) {
|
||||
const FP = PR.flight; common(FP); gl.uniform4fv(FP.u.uKindMask, kindMask); gl.uniform1f(FP.u.uTrail, 0); gl.uniform1f(FP.u.uTrailLen, 0); gl.uniform1f(FP.u.uSelId, W.selFlight ?? -1);
|
||||
const FP = PR.flight; common(FP, lerp(1, 0.42, smooth(0.35, 1.8, range))); gl.uniform4fv(FP.u.uKindMask, kindMask); gl.uniform1f(FP.u.uTrail, 0); gl.uniform1f(FP.u.uTrailLen, 0); gl.uniform1f(FP.u.uSelId, W.selFlight ?? -1);
|
||||
gl.uniform4f(FP.u.uKindSize, 13, 15, 14, 10);
|
||||
gl.bindVertexArray(W.flightVao); gl.drawArraysInstanced(gl.POINTS, 0, 1, W.nFlights);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ function genCity(sc, M, R0) {
|
||||
const hh = hash01(lpx * 0.37 + lpy * 0.71);
|
||||
const core = Math.exp(-ddt / (sc.R * 0.28));
|
||||
let h = sc.kind === 'suburb' ? 5 + hh * 4 : 9 + Math.pow(dens, 1.6) * (sc.maxH * (0.15 + 0.85 * core)) * Math.exp(rr.gauss() * 0.45);
|
||||
h = clamp(h, 6, sc.maxH * 1.4);
|
||||
h = clamp(h, 6, sc.maxH * (sc.tower ? 1.0 : 1.4));
|
||||
const inset = 2 + hh * 3;
|
||||
const type = sc.kind === 'suburb' ? 6 : 0;
|
||||
M.box(x, y, lw - inset * 2, ld - inset * 2, h, sc.rot, [type, hh, h, 0]);
|
||||
@@ -79,7 +79,7 @@ function genCity(sc, M, R0) {
|
||||
}
|
||||
}
|
||||
if (twr) { // the fictional tower (skyscraper climb reconstruction)
|
||||
const T = sc.tower; M.box(twr[0], twr[1], T.w, T.w, T.h * 0.62, sc.rot, [0, 0.8, T.h, 0]); M.box(twr[0], twr[1], T.w * 0.72, T.w * 0.72, T.h * 0.3, sc.rot, [0, 0.8, T.h, 0], T.h * 0.62); M.box(twr[0], twr[1], 3, 3, T.h * 0.08, sc.rot, [5, 0.5, T.h, 0], T.h * 0.92);
|
||||
const T = sc.tower; M.box(twr[0], twr[1], T.w, T.w, T.h * 0.62, sc.rot, [0, 0.8, T.h, 0]); M.box(twr[0], twr[1], T.w * 0.72, T.w * 0.72, T.h * 0.3, sc.rot, [0, 0.8, T.h, 0], T.h * 0.62); M.box(twr[0], twr[1], 3, 3, T.h * 0.08, sc.rot, [5, 0.5, T.h, 0], T.h * 0.92); M.box(twr[0], twr[1], T.w * 0.76, T.w * 0.76, 5, sc.rot, [5, 0.5, T.h, 0], T.h * 0.9);
|
||||
}
|
||||
}
|
||||
function genStadium(sc, M) {
|
||||
@@ -116,8 +116,8 @@ function genAirfield(sc, M) {
|
||||
}
|
||||
function genPad(sc, M) {
|
||||
M.box(0, 0, 220, 160, 0.5, 0, [4, 0, 0.5, 0]);
|
||||
M.box(-14, 0, 12, 12, 88, 0, [0, 0.55, 88, 0]);
|
||||
M.box(4, 0, 4.2, 4.2, 70, 0, [0, 0.98, 70, 0]);
|
||||
M.box(-14, 0, 12, 12, 88, 0, [7, 0.35, 88, 0]);
|
||||
M.box(4, 0, 4.2, 4.2, 70, 0, [7, 1.0, 70, 0]);
|
||||
M.box(4, 0, 4.4, 4.4, 2, 0, [5, 0.5, 2, 0], 70);
|
||||
for (const [x, y, w, d, h] of [[300, 180, 60, 40, 22], [-260, 220, 90, 30, 12], [400, -260, 30, 30, 14]]) M.box(x, y, w, d, h, 0, [0, 0.4, h, 0]);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ add({
|
||||
add({
|
||||
id: 'novosel', short: 'Fort Novosel circuits', fam: 'sky', kind: 'heli', section: 'always', live: true, aspect: 1.5, ageS: 15, watchers: 3100, ride: true,
|
||||
title: 'Fort Novosel helicopter school — jump into the chopper', place: 'Cairns Army Airfield, Alabama', lat: 31.276, lon: -85.713,
|
||||
camera: { lat: 31.2705, lon: -85.7075, range: 0.0011, heading: -0.65, tilt: 1.15 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2),
|
||||
camera: { lat: 31.2745, lon: -85.7105, range: 0.00062, heading: -0.65, tilt: 0.9 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2), labels3d: [[31.276, -85.713, 'Cairns AAF', 0.05], [31.36, -85.63, 'Stagefield A', 0.05]],
|
||||
metric: () => [`${GE.HELI_CIRCUITS.filter((h) => h.field).length}<small>in circuits</small>`, 'Cairns + 3 stagefields · 500 ft AGL'],
|
||||
metrics: () => [{ v: GE.HELI_CIRCUITS.filter((h) => h.field).length, u: '', l: 'Helicopters in circuits', s: 'ADS-B · 15 s' }, { v: 500, u: 'ft', l: 'Pattern altitude', s: 'Observed' }, { v: 5.5, u: 'min', l: 'Circuit time', s: 'Derived' }],
|
||||
brief: { lede: 'Student pilots are flying rectangular traffic patterns at Cairns and three stagefields: upwind, crosswind, downwind, base, final — over and over. Tap Ride to sit in the left seat of one of them.', series: [{ label: 'Helicopters aloft', unit: '', data: [2, 4, 9, 14, 17, 19, 19, 18, 19, 17, 19, 19], src: 'ADS-B / MLAT · hourly · 15 s', fmt: (v) => v }, { label: 'Groundspeed, HAWK 21', unit: 'kt', data: Array.from({ length: 40 }, (_, i) => 70 + 12 * Math.sin(i / 3.2) + (i % 10 > 7 ? -30 : 0)), src: 'ADS-B · 1 s · 15 s', fmt: (v) => Math.round(v) }], sources: [['ADS-B / MLAT', '15 s', 'live']] },
|
||||
@@ -124,7 +124,7 @@ add({
|
||||
add({
|
||||
id: 'launch', short: 'Launch · SLC-40', fam: 'orbit', kind: 'launch', section: 'now', live: true, aspect: 1.5, ageS: 300, watchers: 33800,
|
||||
title: 'Rideshare launch from SLC-40 — countdown', place: 'Cape Canaveral, Florida', lat: 28.5618, lon: -80.5772,
|
||||
camera: { lat: 28.552, lon: -80.590, range: 0.0011, heading: 0.75, tilt: 1.2 }, layers: ['launches', 'flights', 'ais', 'tracks'], source: 'Range schedule (illustrative)',
|
||||
camera: { lat: 28.5585, lon: -80.5825, range: 0.00045, heading: 0.85, tilt: 1.28 }, labels3d: [[28.5618, -80.5772, 'SLC-40 · vehicle on pad', 0.11]], thumbCam: { lat: 28.25, lon: -80.95, range: 0.028, heading: 0.75, tilt: 1.0 }, layers: ['launches', 'flights', 'ais', 'tracks'], source: 'Range schedule (illustrative)',
|
||||
metric: () => [`<span class="num">T−${fmt.dur((LAUNCH_T0 - Date.now()) / 1000)}</span>`, 'Rideshare to SSO · trajectory NE'],
|
||||
metrics: () => [{ v: fmt.dur((LAUNCH_T0 - Date.now()) / 1000), u: '', l: 'Countdown', s: 'Range · 5 min' }, { v: 92, u: '%', l: 'Weather go', s: 'Forecast · 1 h' }, { v: 14, u: '', l: 'Vessels in hazard area', s: 'AIS · 40 s' }],
|
||||
brief: { lede: 'Vehicle is vertical on the pad. The hazard area offshore is still being cleared — 14 vessels remain inside. Trajectory heads northeast along the coast.', series: [{ label: 'Vessels in hazard area', unit: '', data: [41, 39, 36, 33, 31, 28, 24, 21, 19, 17, 15, 14], src: 'AIS · 15 min · 40 s', fmt: (v) => v }], sources: [['Range schedule (illustrative)', '5 min', 'live'], ['AIS', '40 s', 'live']] },
|
||||
@@ -139,16 +139,16 @@ add({
|
||||
});
|
||||
add({
|
||||
id: 'concert', short: 'Stadium encore · 4D', fam: 'eyes', kind: 'recon', section: 'recon', live: false, aspect: 1.25, ageS: 20 * 3600, watchers: 88200, thread: 'concert', d4: true,
|
||||
title: 'Stadium encore in 4D — from 2,311 crowd captures', place: 'Wembley, London · last night', lat: 51.556, lon: -0.2796,
|
||||
camera: { lat: 51.5552, lon: -0.2810, range: 0.00034, heading: 0.55, tilt: 1.12 }, time: { at: NOW - 20 * 3600000 + 0.0, start: NOW - 22 * 3600000, end: NOW - 19 * 3600000 },
|
||||
title: 'Stadium encore in 4D — from 2,311 crowd captures', place: 'Wembley, London · ' + GE.dayWord(GE.T_CONCERT, 'night'), lat: 51.556, lon: -0.2796,
|
||||
camera: { lat: 51.5553, lon: -0.2806, range: 0.00026, heading: 0.55, tilt: 1.0 }, time: { at: GE.T_CONCERT, start: GE.T_CONCERT - 3600000, end: GE.T_CONCERT + 1800000 },
|
||||
layers: ['signed', 'holo', 'venues', 'cctv'], source: 'Thread · 2,311 signed captures', nightThumb: true,
|
||||
metric: () => ['2,311<small>captures</small>', '4D · 41 min · 86 seat blocks'], metrics: () => [{ v: '2,311', u: '', l: 'Signed captures', s: 'Ledger · 20 h' }, { v: 86, u: '', l: 'Seat blocks covered', s: 'Derived' }, { v: '±9', u: 'ms', l: 'Time alignment', s: 'Audio sync' }],
|
||||
brief: { lede: 'A crowd-sourced reconstruction of last night’s encore: 2,311 signed phone captures aligned by audio fingerprint and IMU traces into a single 4D scene you can scrub. Performer and attendee identities are not part of the record.', series: [{ label: 'Captures per minute', unit: '', data: [3, 5, 9, 21, 64, 140, 188, 162, 120, 96, 72, 40], src: 'Signed bundles · 1 min', fmt: (v) => v }], sources: [['Signed crowd captures (illustrative)', '20 h', 'stale']] },
|
||||
});
|
||||
add({
|
||||
id: 'climb', short: 'Tower climb · 4D', fam: 'eyes', kind: 'recon', section: 'recon', live: false, aspect: 1.5, ageS: 9 * 3600, watchers: 41500, thread: 'climb', d4: true,
|
||||
title: 'Free-climber on a Midtown tower — reconstructed', place: 'Midtown Manhattan · this morning', lat: 40.7553, lon: -73.9838,
|
||||
camera: { lat: 40.7528, lon: -73.9870, range: 0.00027, heading: 0.62, tilt: 1.2 }, time: { at: NOW - 9 * 3600000, start: NOW - 11 * 3600000, end: NOW - 7 * 3600000 },
|
||||
title: 'Free-climber on a Midtown tower — reconstructed', place: 'Midtown Manhattan · ' + GE.dayWord(GE.T_CLIMB, 'morning'), lat: 40.7553, lon: -73.9838,
|
||||
camera: { lat: 40.7532, lon: -73.9865, range: 0.00055, heading: 0.62, tilt: 1.12 }, time: { at: GE.T_CLIMB + 1.2 * 3600000, start: GE.T_CLIMB - 1800000, end: GE.T_CLIMB + 3 * 3600000 },
|
||||
layers: ['helis', 'signed', 'holo', 'cctv'], source: 'Thread · 3 news helicopters (ADS-B)',
|
||||
metric: () => ['41<small>clips</small> · 3<small>helis</small>', 'Fictional tower · ascent 2 h 14 m'], metrics: () => [{ v: 41, u: '', l: 'Witness clips', s: 'Ledger · 9 h' }, { v: 3, u: '', l: 'News helicopters', s: 'ADS-B · 9 h' }, { v: 412, u: 'm', l: 'Tower height', s: 'Model (fictional tower)' }],
|
||||
brief: { lede: 'Three news helicopters orbited the tower for two hours; their ADS-B tracks give precise camera positions for every broadcast frame. Combined with 41 signed street-level clips, the climber’s height over time is reconstructed to ±3 m. The tower and people are fictional.', series: [{ label: 'Climber height (reconstructed)', unit: 'm', data: Array.from({ length: 30 }, (_, i) => Math.min(380, i * 14 + Math.sin(i) * 6)), band: 3, src: 'Hypothesis H2 · ±3 m', fmt: (v) => Math.round(v) }], sources: [['ADS-B: 3 helicopters', '9 h', 'stale'], ['Signed street clips (illustrative)', '9 h', 'stale']] },
|
||||
@@ -220,7 +220,7 @@ SIT.sections = [
|
||||
// dynamic cameras
|
||||
SIT.cameraFor = function (s) {
|
||||
if (s.id === 'starlink') { const g = GE.world.sun.gmst; const p = GE.satAt(GE.SATS.find((x) => x.kind === 5), GE.world.t, g); const [la, lo] = geo.ll(p); return { lat: la - 6, lon: lo - 8, range: 0.32, heading: 0.6, tilt: 0.85 }; }
|
||||
if (s.id === 'squawk') { const f = GE.FLIGHTS[GE.world.squawks[0]]; const p = GE.flightAt(f, GE.world.t) || { lat: 50, lon: -30 }; return { lat: p.lat - 1.2, lon: p.lon - 1, range: 0.06, heading: 0.3, tilt: 0.85 }; }
|
||||
if (s.id === 'squawk') { const f = GE.FLIGHTS[GE.world.squawks[0]]; const p = GE.flightAt(f, GE.world.t) || { lat: 50, lon: -30 }; return { lat: p.lat - 4, lon: p.lon - 3, range: 0.3, heading: 0.3, tilt: 0.75 }; }
|
||||
if (s.id === 'iss') { const p = GE.satAt(GE.ISS, GE.world.t, GE.world.sun.gmst); const [la, lo] = geo.ll(p); return { lat: la - 9, lon: lo - 4, range: 0.48, heading: 0.35, tilt: 0.78 }; }
|
||||
return s.camera;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ SH.open = function (id, o = {}) {
|
||||
bus.emit('layers', W.layers);
|
||||
if (s.time) { W.setWindow({ start: s.time.start ?? s.time.at - 6 * 3600000, end: s.time.end ?? s.time.at + 6 * 3600000 }); W.setTime(s.time.at); }
|
||||
W.auroraK = s.auroraK ?? 0.85;
|
||||
GE.setExtras && GE.setExtras(s.id);
|
||||
GE.setExtras && GE.setExtras(s.id); GE.setLabels3d(s.thread ? GE.threads.data[s.thread].labels : s.labels3d);
|
||||
// camera
|
||||
const cam = GE.SIT.cameraFor(s);
|
||||
const hs = SH.heights();
|
||||
@@ -48,7 +48,7 @@ SH.open = function (id, o = {}) {
|
||||
SH.close = function (silent, keepTime) {
|
||||
if (!SH.id) return; const id = SH.id; const W = GE.world;
|
||||
const el = sheetEl(); el.classList.remove('drag', 'wopen'); el.style.transform = APP.isWide() ? '' : 'translateY(100%)';
|
||||
SH.id = null; W.focus = null; GE.setExtras && GE.setExtras(null);
|
||||
SH.id = null; W.focus = null; GE.setExtras && GE.setExtras(null); GE.setLabels3d(null);
|
||||
if (SH._savedLayers) { W.layers.forEach((l, i) => (l.on = SH._savedLayers[i])); SH._savedLayers = null; bus.emit('layers', W.layers); }
|
||||
if (!keepTime) { W.setWindow(null); W.setTime(null); } W.auroraK = 0.85;
|
||||
APP.setMode('');
|
||||
|
||||
@@ -101,7 +101,7 @@ CAP.frame = function (view) {
|
||||
CAP._sats.sort((a, b) => a.km - b.km); CAP._sats = CAP._sats.slice(0, 3);
|
||||
}
|
||||
for (const o of CAP._sats) { const p = GE.satAt(o.s, CAP.t, sun.gmst); want.push({ k: 's' + o.s.name, v: p, c: 'var(--orbit)', i: 'satellite', t: o.s.name, s: `${Math.round(o.s.alt)} km up · ${Math.round(o.el)}° elev`, d: o.km }); }
|
||||
const seen = new Set(); let n = 0;
|
||||
const seen = new Set(); let n = 0; const used = [];
|
||||
for (const it of want.sort((a, b) => a.d - b.d)) {
|
||||
const s = W.project(view, it.v); let el = host.querySelector(`[data-k="${it.k}"]`);
|
||||
const onScreen = s && s.x > 8 && s.x < view.w - 8 && s.y > 60 && s.y < view.h - 230;
|
||||
@@ -110,7 +110,8 @@ CAP.frame = function (view) {
|
||||
if (!el) { el = h('div', { 'data-k': it.k }); el.innerHTML = `<div class="reticle" style="--c:${it.c}"></div><div class="callout glass" style="--c:${it.c}"><span class="ci">${ic(it.i, 'sm')}</span><span><b>${esc(it.t)}</b><span data-s></span></span></div>`; host.append(el); }
|
||||
el.querySelector('[data-s]').textContent = it.s;
|
||||
el.querySelector('.reticle').style.transform = `translate(${s.x}px, ${s.y}px)`;
|
||||
const flip = s.x > view.w - 210; el.querySelector('.callout').style.transform = flip ? `translate(${s.x - 24}px, ${s.y}px) translate(-100%, -50%)` : `translate(${s.x + 24}px, ${s.y}px) translate(0, -50%)`;
|
||||
let cy = s.y; for (const u of used) if (Math.abs(u - cy) < 44) cy = u + 44; used.push(cy);
|
||||
const flip = s.x > view.w - 210; el.querySelector('.callout').style.transform = flip ? `translate(${s.x - 24}px, ${cy}px) translate(-100%, -50%)` : `translate(${s.x + 24}px, ${cy}px) translate(0, -50%)`;
|
||||
}
|
||||
host.querySelectorAll('[data-k]').forEach((e) => { if (!seen.has(e.dataset.k)) e.remove(); });
|
||||
// witness sensors
|
||||
|
||||
@@ -10,7 +10,8 @@ const THREADS = {
|
||||
koshi: {
|
||||
id: 'koshi', sit: 'koshi', title: 'Bhote Koshi outburst flood', place: 'Rasuwagadhi → Syaphrubesi, Nepal', date: '26 Aug 2026',
|
||||
window: { start: KT - 1.5 * HR_, end: KT + 6 * HR_ }, people: ['hydro', 'kora', 'tamang', 'geo', 'sky', 'ais'],
|
||||
cam: { lat: 28.205, lon: 85.352, range: 0.0042, heading: 0.32, tilt: 1.02 },
|
||||
cam: { lat: 28.2, lon: 85.35, range: 0.0046, heading: 0.32, tilt: 0.98 },
|
||||
labels: [[28.1600, 85.3395, 'Syaphrubesi'], [28.2560, 85.3690, 'Timure'], [28.2802, 85.3797, 'Rasuwagadhi bridge']],
|
||||
obs: [
|
||||
{ id: 'O1', i: 'frame', f: 'infra', t: 'Optical satellite tile — before', src: 'Public 10 m optical archive', at: Date.UTC(2026, 7, 24, 4, 52), meta: ['10 m', 'cloud 6%'] },
|
||||
{ id: 'O2', i: 'frame', f: 'infra', t: 'Optical satellite tile — after: new scour along 9.4 km of channel', src: 'Public 10 m optical archive', at: Date.UTC(2026, 7, 27, 4, 58), meta: ['10 m', 'cloud 12%'] },
|
||||
@@ -30,24 +31,25 @@ const THREADS = {
|
||||
{ id: 'H4', by: 'ai', ai: true, text: 'Peak stage at Syaphrubesi lagged the visible crest by ~20 min — consistent with debris damming at the bridge.', conf: 0.41, ci: [0.22, 0.6], cites: ['O3', 'O6'], agree: 3, dispute: 5, status: 'disputed', prompt: 'Why does the gauge peak late?' },
|
||||
],
|
||||
beats: [
|
||||
{ at: KT, t: '07:45', text: 'Outburst upstream (inferred). The valley is quiet; the river is at its normal 3 m stage.', cites: ['H1', 'O4', 'O9'], cam: { lat: 28.262, lon: 85.372, range: 0.0032, heading: 3.4, tilt: 1.05 } },
|
||||
{ at: KT + 20 * MIN, t: '08:05', text: 'The crest reaches Rasuwagadhi and the border bridge.', cites: ['H2', 'O7'], cam: { lat: 28.272, lon: 85.37, range: 0.0018, heading: 0.6, tilt: 1.12 } },
|
||||
{ at: KT + 27 * MIN, t: '08:12', text: 'A witness on the north bank at Timure films water reaching the road.', cites: ['H3', 'O5'], cam: { lat: 28.247, lon: 85.364, range: 0.0016, heading: 0.35, tilt: 1.15 } },
|
||||
{ at: KT + 61 * MIN, t: '08:46', text: 'Debris piles against the Syaphrubesi bridge deck.', cites: ['O6', 'H4'], cam: { lat: 28.152, lon: 85.336, range: 0.0016, heading: 0.2, tilt: 1.12 } },
|
||||
{ at: KT + 105 * MIN, t: '09:30', text: 'Gauge peaks at 7.8 m — about 20 minutes after the visible crest.', cites: ['O3', 'H4'], cam: { lat: 28.15, lon: 85.33, range: 0.0026, heading: 0.9, tilt: 1.05 } },
|
||||
{ at: KT + 140 * MIN, t: '10:05', text: 'First rescue sortie climbs the valley.', cites: ['O8'], cam: { lat: 28.19, lon: 85.345, range: 0.0045, heading: 0.3, tilt: 1.0 } },
|
||||
{ at: KT, t: '07:45', text: 'Outburst upstream (inferred). The valley is quiet; the river is at its normal 3 m stage.', cites: ['H1', 'O4', 'O9'], cam: { lat: 28.268, lon: 85.37, range: 0.0032, heading: 3.55, tilt: 0.98 } },
|
||||
{ at: KT + 20 * MIN, t: '08:05', text: 'The crest reaches Rasuwagadhi and the border bridge.', cites: ['H2', 'O7'], cam: { lat: 28.276, lon: 85.376, range: 0.0026, heading: 2.6, tilt: 0.95 } },
|
||||
{ at: KT + 27 * MIN, t: '08:12', text: 'A witness on the north bank at Timure films water reaching the road.', cites: ['H3', 'O5'], cam: { lat: 28.252, lon: 85.368, range: 0.0027, heading: 3.4, tilt: 0.96 } },
|
||||
{ at: KT + 61 * MIN, t: '08:46', text: 'Debris piles against the Syaphrubesi bridge deck.', cites: ['O6', 'H4'], cam: { lat: 28.16, lon: 85.338, range: 0.0027, heading: 0.25, tilt: 0.96 } },
|
||||
{ at: KT + 105 * MIN, t: '09:30', text: 'Gauge peaks at 7.8 m — about 20 minutes after the visible crest.', cites: ['O3', 'H4'], cam: { lat: 28.155, lon: 85.332, range: 0.0032, heading: 0.9, tilt: 0.98 } },
|
||||
{ at: KT + 140 * MIN, t: '10:05', text: 'First rescue sortie climbs the valley.', cites: ['O8'], cam: { lat: 28.2, lon: 85.35, range: 0.0045, heading: 0.3, tilt: 0.98 } },
|
||||
],
|
||||
suggestions: ['Align the witness clip to the corridor using the bridge as a landmark', 'Estimate crest speed between Timure and Syaphrubesi', 'What would falsify H1?'],
|
||||
fallback: 'Using the Syaphrubesi bridge as a fixed landmark, clip [O6] was taken from the east abutment looking upstream; matching its horizon to the IMU pitch places the crest front 85 ± 20 m upstream of the bridge at 08:46 UTC. That timing agrees with the gauge [O3] if the crest travelled 4.6 ± 0.6 m/s from Timure [O5].\nConfidence: 0.63',
|
||||
},
|
||||
concert: {
|
||||
id: 'concert', sit: 'concert', title: 'Stadium encore in 4D', place: 'Wembley, London', date: 'last night',
|
||||
window: { start: Date.now() - 21 * HR_, end: Date.now() - 19.5 * HR_ }, people: ['crowd', 'sky', 'kora', 'geo'],
|
||||
id: 'concert', sit: 'concert', title: 'Stadium encore in 4D', place: 'Wembley, London', date: GE.dayWord(GE.T_CONCERT, 'night'),
|
||||
window: { start: GE.T_CONCERT - 1 * HR_, end: GE.T_CONCERT + 0.5 * HR_ }, people: ['crowd', 'sky', 'kora', 'geo'],
|
||||
cam: { lat: 51.5546, lon: -0.2822, range: 0.00042, heading: 0.6, tilt: 1.1 },
|
||||
labels: [[51.556, -0.2803, 'Stage · encore', 0.05]],
|
||||
obs: [
|
||||
{ id: 'O1', i: 'seal', f: 'eyes', t: '2,311 signed phone captures from 86 seat blocks', src: 'Signed bundles (illustrative)', at: Date.now() - 20 * HR_, meta: ['IMU ✓ on 2,204', 'GPS indoor ±12 m'] },
|
||||
{ id: 'O2', i: 'wave', f: 'eyes', t: 'Audio fingerprints align 1,980 clips to ±9 ms', src: 'Derived', at: Date.now() - 19.8 * HR_, meta: ['common track'] },
|
||||
{ id: 'O3', i: 'video', f: 'eyes', t: 'Venue public camera, concourse north', src: 'Public feed (illustrative)', at: Date.now() - 20 * HR_, meta: ['720p'] },
|
||||
{ id: 'O1', i: 'seal', f: 'eyes', t: '2,311 signed phone captures from 86 seat blocks', src: 'Signed bundles (illustrative)', at: GE.T_CONCERT, meta: ['IMU ✓ on 2,204', 'GPS indoor ±12 m'] },
|
||||
{ id: 'O2', i: 'wave', f: 'eyes', t: 'Audio fingerprints align 1,980 clips to ±9 ms', src: 'Derived', at: GE.T_CONCERT + 0.2 * HR_, meta: ['common track'] },
|
||||
{ id: 'O3', i: 'video', f: 'eyes', t: 'Venue public camera, concourse north', src: 'Public feed (illustrative)', at: GE.T_CONCERT, meta: ['720p'] },
|
||||
{ id: 'O4', i: 'ticket', f: 'ground', t: 'Seat map & stage geometry', src: 'Published venue plan', at: Date.now() - 30 * 24 * HR_, meta: ['static'] },
|
||||
],
|
||||
hyps: [
|
||||
@@ -55,21 +57,22 @@ const THREADS = {
|
||||
{ id: 'H2', by: 'geo', text: 'The pyro cue at 22:44 is visible from 86 of 92 seat blocks; 6 blocks are occluded by the roof cantilever.', conf: 0.71, ci: [0.6, 0.8], cites: ['O1', 'O4'], agree: 44, dispute: 6, status: 'supported' },
|
||||
],
|
||||
beats: [
|
||||
{ at: Date.now() - 20 * HR_ - 10 * MIN, t: '22:41', text: 'Lights down. 2,311 phones come up together.', cites: ['H1'], cam: { lat: 51.5552, lon: -0.2815, range: 0.00035, heading: 0.9, tilt: 1.15 } },
|
||||
{ at: Date.now() - 20 * HR_, t: '22:44', text: 'Pyro cue — seen from 86 blocks at once.', cites: ['H2'], cam: { lat: 51.5568, lon: -0.2778, range: 0.0003, heading: 3.6, tilt: 1.12 } },
|
||||
{ at: Date.now() - 20 * HR_ + 12 * MIN, t: '22:56', text: 'Final chorus; the crowd reconstruction is densest here.', cites: ['O1', 'O2'], cam: { lat: 51.556, lon: -0.2796, range: 0.0006, heading: 2.2, tilt: 0.9 } },
|
||||
{ at: GE.T_CONCERT - 3 * MIN, t: '22:41', text: 'Lights down. 2,311 phones come up together.', cites: ['H1'], cam: { lat: 51.5552, lon: -0.2815, range: 0.00035, heading: 0.9, tilt: 1.15 } },
|
||||
{ at: GE.T_CONCERT, t: '22:44', text: 'Pyro cue — seen from 86 blocks at once.', cites: ['H2'], cam: { lat: 51.5568, lon: -0.2778, range: 0.0003, heading: 3.6, tilt: 1.12 } },
|
||||
{ at: GE.T_CONCERT + 12 * MIN, t: '22:56', text: 'Final chorus; the crowd reconstruction is densest here.', cites: ['O1', 'O2'], cam: { lat: 51.556, lon: -0.2796, range: 0.0006, heading: 2.2, tilt: 0.9 } },
|
||||
],
|
||||
suggestions: ['Which seat blocks saw the pyro cue?', 'Align the concourse camera to the crowd timeline'],
|
||||
fallback: 'The concourse camera [O3] shows the crowd surge 2.1 s after the audio onset in [O2]; offsetting its clock by −2.1 s aligns it with 1,980 crowd clips [O1].\nConfidence: 0.58',
|
||||
},
|
||||
climb: {
|
||||
id: 'climb', sit: 'climb', title: 'Free-climber on a Midtown tower', place: 'Midtown Manhattan (fictional tower)', date: 'this morning',
|
||||
window: { start: Date.now() - 11 * HR_, end: Date.now() - 7 * HR_ }, people: ['news', 'sky', 'kora', 'geo', 'ais'],
|
||||
cam: { lat: 40.7533, lon: -73.9866, range: 0.00034, heading: 0.62, tilt: 1.2 },
|
||||
id: 'climb', sit: 'climb', title: 'Free-climber on a Midtown tower', place: 'Midtown Manhattan (fictional tower)', date: GE.dayWord(GE.T_CLIMB, 'morning'),
|
||||
window: { start: GE.T_CLIMB - 0.5 * HR_, end: GE.T_CLIMB + 3 * HR_ }, people: ['news', 'sky', 'kora', 'geo', 'ais'],
|
||||
cam: { lat: 40.7532, lon: -73.9865, range: 0.00055, heading: 0.62, tilt: 1.12 },
|
||||
labels: [[40.7553, -73.9838, 'Meridian Tower (fictional)', 0.43]],
|
||||
obs: [
|
||||
{ id: 'O1', i: 'helicopter', f: 'sky', t: 'ADS-B tracks: 3 news helicopters orbiting the tower', src: 'ADS-B (illustrative callsigns)', at: Date.now() - 9 * HR_, meta: ['1 Hz', '2 h 14 m'] },
|
||||
{ id: 'O2', i: 'video', f: 'eyes', t: 'Broadcast frames, time-coded', src: 'Public broadcast (illustrative)', at: Date.now() - 9 * HR_, meta: ['29.97 fps'] },
|
||||
{ id: 'O3', i: 'seal', f: 'eyes', t: '41 signed street-level clips', src: 'Signed bundles (illustrative)', at: Date.now() - 9 * HR_, meta: ['IMU ✓', 'GPS ±5 m'] },
|
||||
{ id: 'O1', i: 'helicopter', f: 'sky', t: 'ADS-B tracks: 3 news helicopters orbiting the tower', src: 'ADS-B (illustrative callsigns)', at: GE.T_CLIMB + 1.2 * HR_, meta: ['1 Hz', '2 h 14 m'] },
|
||||
{ id: 'O2', i: 'video', f: 'eyes', t: 'Broadcast frames, time-coded', src: 'Public broadcast (illustrative)', at: GE.T_CLIMB + 1.2 * HR_, meta: ['29.97 fps'] },
|
||||
{ id: 'O3', i: 'seal', f: 'eyes', t: '41 signed street-level clips', src: 'Signed bundles (illustrative)', at: GE.T_CLIMB + 1.2 * HR_, meta: ['IMU ✓', 'GPS ±5 m'] },
|
||||
{ id: 'O4', i: 'cube', f: 'infra', t: 'Tower model, 412 m (fictional building)', src: 'Model', at: Date.now() - 400 * 24 * HR_, meta: ['static'] },
|
||||
],
|
||||
hyps: [
|
||||
@@ -77,9 +80,9 @@ const THREADS = {
|
||||
{ id: 'H2', by: 'geo', text: 'Climber height over time is recoverable to ±3 m by triangulating helicopter camera positions (from ADS-B) with frame bearings.', conf: 0.69, ci: [0.55, 0.8], cites: ['O1', 'O2', 'O4'], agree: 33, dispute: 7, status: 'supported' },
|
||||
],
|
||||
beats: [
|
||||
{ at: Date.now() - 10.5 * HR_, t: '06:52', text: 'First witness clip from the street: the climber is at the 3rd setback.', cites: ['H1', 'O3'], cam: { lat: 40.7538, lon: -73.9852, range: 0.00022, heading: 0.5, tilt: 1.3 } },
|
||||
{ at: Date.now() - 9.5 * HR_, t: '07:55', text: 'Three news helicopters in orbit; their ADS-B tracks pin each broadcast camera.', cites: ['O1', 'H2'], cam: { lat: 40.751, lon: -73.99, range: 0.0012, heading: 0.7, tilt: 1.0 } },
|
||||
{ at: Date.now() - 8.6 * HR_, t: '09:06', text: 'The climber reaches the crown at 384 m.', cites: ['H2'], cam: { lat: 40.7549, lon: -73.9848, range: 0.0003, heading: 0.4, tilt: 1.42 } },
|
||||
{ at: GE.T_CLIMB, t: '06:52', text: 'First witness clip from the street: the climber is at the 3rd setback.', cites: ['H1', 'O3'], cam: { lat: 40.7538, lon: -73.9852, range: 0.00022, heading: 0.5, tilt: 1.3 } },
|
||||
{ at: GE.T_CLIMB + 63 * MIN, t: '07:55', text: 'Three news helicopters in orbit; their ADS-B tracks pin each broadcast camera.', cites: ['O1', 'H2'], cam: { lat: 40.751, lon: -73.99, range: 0.0012, heading: 0.7, tilt: 1.0 } },
|
||||
{ at: GE.T_CLIMB + 134 * MIN, t: '09:06', text: 'The climber reaches the crown at 384 m.', cites: ['H2'], cam: { lat: 40.7549, lon: -73.9848, range: 0.0003, heading: 0.4, tilt: 1.42 } },
|
||||
],
|
||||
suggestions: ['Triangulate the climber at 08:30 from two helicopters', 'Which face was the route on?'],
|
||||
fallback: 'At 08:30 the bearings from N9 NEWS and SKY 4 [O1] to the climber in broadcast frames [O2] intersect 232 ± 4 m up the northeast face of the tower model [O4].\nConfidence: 0.66',
|
||||
@@ -91,6 +94,7 @@ THR.data = THREADS;
|
||||
const EXTRAS = {};
|
||||
GE.extrasFor = function (id) {
|
||||
if (EXTRAS[id] !== undefined) return EXTRAS[id];
|
||||
if (id === 'launch') { const sp = [], ln = []; const pad = [28.5618, -80.5772]; pushSpr(sp, geo.vec(pad[0], pad[1], 0.04), [1, 0.86, 0.6, 0.9], 70, 0, -10, -1e9, 1e9, 0, 0, 0.9); for (const [dx, dy] of [[90, 70], [-80, 60], [70, -80], [-90, -70]]) { const [la, lo] = geo.offset(pad[0], pad[1], dx, dy); pushSpr(sp, geo.vec(la, lo, 0.02), [1, 0.9, 0.7, 0.8], 26, 0, -10, -1e9, 1e9, 0, 0, 0.6); } return (EXTRAS[id] = { sprites: [makeSpriteSet('xl', sp)], lines: null }); }
|
||||
const T = THREADS[id] || (GE.SIT.byId[id]?.thread && THREADS[GE.SIT.byId[id].thread]);
|
||||
if (!T) return (EXTRAS[id] = null);
|
||||
const sp = [], ln = [];
|
||||
@@ -121,6 +125,7 @@ GE.extrasFor = function (id) {
|
||||
const set = { sprites: sp.length ? [makeSpriteSet('x' + id, sp)] : null, lines: ln.length ? [makeLineSet(ln)] : null };
|
||||
return (EXTRAS[id] = set);
|
||||
};
|
||||
GE.setLabels3d = function (L) { GE.extraLabels.clear(); for (const [la, lo, t, alt] of L || []) { const z = GE.scenes.byId.koshi.height && Math.abs(la - 28.2) < 0.2 && Math.abs(lo - 85.35) < 0.2 ? GE.scenes.groundAt(la, lo) / 1000 + 0.12 : alt || 0.05; GE.extraLabels.set('x:' + t, { lat: la, lon: lo, alt: z, html: `<i style="--c:var(--gold)"></i>${esc(t)}`, w: t.length * 6.6 + 32, cls: 'glass' }); } };
|
||||
GE.setExtras = function (id) { GE.world.activeExtras = id ? GE.extrasFor(id) : null; GE.world.activeExtrasId = id; };
|
||||
|
||||
/* ---------- ledger (real SHA-256 hash chain) ---------- */
|
||||
@@ -172,7 +177,7 @@ THR.openWs = function (id) {
|
||||
$('#tlist').style.display = 'none';
|
||||
const ws = $('#ws'); ws.classList.add('on');
|
||||
W.setWindow(T.window); W.setTime(T.beats[Math.min(2, T.beats.length - 1)].at);
|
||||
GE.setExtras(T.id);
|
||||
GE.setExtras(T.id); GE.setLabels3d(T.labels);
|
||||
for (const lid of ['signed', 'holo', 'helis']) W.layerById[lid].on = true;
|
||||
const wide = APP.isWide();
|
||||
Cam.autoSpin = 0; Cam.flyTo({ ...T.cam, shiftY: wide ? 0 : 0.6, shiftX: 0 });
|
||||
@@ -182,12 +187,12 @@ THR.openWs = function (id) {
|
||||
THR.closeWs = function (silent) {
|
||||
if (!THR.ws) return; THR.stop(); THR.ws = null; const W = GE.world;
|
||||
$('#ws').classList.remove('on'); $('#ws').innerHTML = ''; const tl = $('#tlist'); if (tl) tl.style.display = '';
|
||||
W.setWindow(null); W.setTime(null); GE.setExtras(null); APP.setMode('');
|
||||
W.setWindow(null); W.setTime(null); GE.setExtras(null); GE.setLabels3d(null); APP.setMode('');
|
||||
if (!silent) Cam.flyTo(APP.heroCam());
|
||||
};
|
||||
THR.renderWs = function () {
|
||||
const { T, seg } = THR.ws; const ws = $('#ws');
|
||||
ws.innerHTML = `<div class="wsh"><button class="circ glass" id="wsBack" aria-label="Back">${ic('chevl')}</button><div class="tt glass"><span style="min-width:0;flex:1"><b>${esc(T.title)}</b><span>${esc(T.place)} · ${esc(T.date)}</span></span><span class="presence"><span class="stack">${T.people.slice(0, 4).map((k, i) => `<span class="avatar sm ${i < 3 ? 'on' : ''}" style="--h:${P[k].hue}">${P[k].h[0].toUpperCase()}</span>`).join('')}</span></span></div></div>
|
||||
ws.innerHTML = `<div class="wsh"><button class="circ glass" id="wsBack" aria-label="Back">${ic('chevl')}</button><div class="tt glass"><span style="min-width:0;flex:1"><b>${esc(T.title)}</b><span class="sub">${esc(T.place)} · ${esc(T.date)}</span></span><span class="presence"><span class="stack">${T.people.slice(0, 4).map((k, i) => `<span class="avatar sm ${i < 3 ? 'on' : ''}" style="--h:${P[k].hue}">${P[k].h[0].toUpperCase()}</span>`).join('')}</span></span></div></div>
|
||||
<div class="body glass strong"><div class="seg" id="wsSeg">${[['obs', 'Observations'], ['hyp', 'Hypotheses'], ['nar', 'Narrative'], ['led', 'Ledger']].map(([k, l]) => `<button data-s="${k}" class="${seg === k ? 'on' : ''}">${l}</button>`).join('')}</div><div class="wsc" id="wsc"></div>
|
||||
${seg === 'hyp' ? `<div class="composer glass strong" id="composer"><div class="sug">${T.suggestions.map((s) => `<button>${esc(s)}</button>`).join('')}</div><div class="row"><textarea id="aiIn" rows="1" placeholder="Ask AI to refine this reconstruction…"></textarea><button class="send" id="aiSend" aria-label="Send">${ic('arrowup')}</button></div></div>` : ''}</div>`;
|
||||
$('#wsBack').addEventListener('click', () => THR.closeWs());
|
||||
|
||||
@@ -43,7 +43,7 @@ function frame(now) {
|
||||
APP.updateLabels(view); GE.ride.frame(view); GE.capture.frame(view);
|
||||
}
|
||||
if (APP.tab === 'globe' || APP.isWide() || APP.mode) GE.gui.ribbonFrame();
|
||||
GE.thumbs.step(APP.intro ? 14 : 8);
|
||||
if (APP.intro || !(Cam.anim || Cam.interacting || GE.thumbs._busyUntil > now)) GE.thumbs.step(APP.intro ? 14 : 8);
|
||||
if (now - lastSec > 1000) { lastSec = now; W.updateCounts(); GE.now.tick(); GE.sheet.tick(); GE.you.tick(); }
|
||||
governor(dt);
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
Reference in new issue
Block a user