From 8921d87b53e4adbd02a2e7e45938c63bfe1b3db0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 05:59:01 +0000 Subject: [PATCH] =?UTF-8?q?tools(rpo-replay):=20v3=20WIP=20=E2=80=94=20rev?= =?UTF-8?q?iew=20fixes=20(closest-pass=20sequence,=20fixed=20scale,=20safe?= =?UTF-8?q?=20zones,=20copy)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0187rSCa4AKJPGw6uiuPSGyX --- tools/rpo-replay/hud.js | 108 +++++++++++++++++++++++------------- tools/rpo-replay/scene.html | 89 +++++++++++++++-------------- tools/rpo-replay/scene.js | 29 +++++----- 3 files changed, 131 insertions(+), 95 deletions(-) diff --git a/tools/rpo-replay/hud.js b/tools/rpo-replay/hud.js index 4f1698e67..5d51a7080 100644 --- a/tools/rpo-replay/hud.js +++ b/tools/rpo-replay/hud.js @@ -14,14 +14,15 @@ export function createHud(ctx) { const CHAPTERS = [ { key: 'climb', label: 'Climb', s0: 4.4, s1: 12.5 }, { key: 'drop', label: 'Drop in', s0: 12.5, s1: 22.0 }, - { key: 'stalk', label: 'Stalk', s0: 22.0, s1: 35.6 }, + { key: 'close', label: 'Close in', s0: 22.0, s1: 29.7 }, + { key: 'closest', label: 'Closest', s0: 29.7, s1: 35.6 }, { key: 'leave', label: 'Leave', s0: 35.6, s1: 41.2 }, ]; const CAPTIONS = [ { - s0: 0.0, s1: 4.4, hook: true, - title: `A Russian satellite stalked a U.S. radar satellite — to ~${CA_M} m`, - body: 'How Cosmos 2612 hunted ICEYE-X36 for 22 days, rebuilt from public tracking data.', + s0: -1, s1: 4.4, hook: true, + title: `A Russian military satellite shadowed a U.S.-registered one — to ~${CA_M} m`, + body: 'How Cosmos 2612 matched ICEYE-X36\'s orbit over 22 days, rebuilt from public tracking data.', }, { s0: 4.4, s1: 12.5, @@ -29,19 +30,29 @@ export function createHud(ctx) { body: 'A higher orbit is slower. Lifted 47 km, it drifts back and laps its target.', }, { - s0: 12.5, s1: 22.0, + s0: 12.5, s1: 17.2, title: 'Drop into the same orbit', - body: 'Oct 12: it dives 47 km, lands 480 km behind — then closes ~350 km a day.', + body: 'Oct 12: it dives 47 km and lands about 480 km behind.', + }, + { + s0: 17.2, s1: 22.0, + title: 'Speed up, then brake', + body: 'Oct 15: a 2 km dip makes it faster — closing ~350 km a day — then it brakes 3 km behind.', }, { s0: 22.0, s1: 29.7, title: 'Park behind it. Creep closer.', - body: 'From 3 km back it loops 1.6 km every orbit, inching in for two days.', + body: 'Each orbit its path loops 1.6 km, inching in for two days.', }, { - s0: 29.7, s1: 35.6, - title: `Closest pass: ~${CA_M} meters`, - body: 'In Earth\'s shadow, at walking pace — while both fly at 27,000 km/h.', + s0: 29.7, s1: 32.3, + title: 'Into Earth\'s shadow', + body: 'Closing at strolling pace (0.9 m/s) — while both fly at 27,000 km/h.', + }, + { + s0: 32.3, s1: 35.6, + title: `Closest pass: ~${CA_M} m`, + body: 'Shown at true size. Published estimates: 150–650 m.', }, { s0: 35.6, s1: 41.2, @@ -66,8 +77,8 @@ export function createHud(ctx) { })), { t: sec('2026-10-05T11:55:00Z'), kind: 'note', title: 'Passes overhead', sub: '~50 km above its target' }, { t: sec('2026-10-12T00:10:00Z'), kind: 'note', title: 'Directly overhead', sub: '47 km above its target' }, - { t: VANTOR_T, kind: 'vantor', title: 'Vantor photographs the pair', sub: `from another satellite · ~${D.vantor.km.toFixed(0)} km apart` }, - { t: CA_T, kind: 'ca', title: 'Closest approach', sub: `${D.ca.t.slice(11, 16)} UTC Oct 18 · est. 150–650 m` }, + { t: VANTOR_T, kind: 'vantor', title: 'Vantor photographs the pair', sub: `from its own satellite · ~${D.vantor.km.toFixed(0)} km apart · Oct 17` }, + { t: CA_T, kind: 'ca', title: 'Closest approach', sub: `${D.ca.t.slice(11, 16)} UTC, Oct 18` }, ].map((e) => ({ ...e, s: sOfTau(e.t) })).sort((a, b) => a.s - b.s); // ------------------------------------------------------------------------- @@ -103,7 +114,7 @@ export function createHud(ctx) { for (let i = Math.floor(ta / D.dt); i <= Math.ceil(tb / D.dt); i++) mn = Math.min(mn, D.sep_km[Math.min(Math.max(i, 0), NS - 1)]); lo += `${c ? 'L' : 'M'}${(padL + c * 2).toFixed(1)},${y(Math.max(mn, CH.lo)).toFixed(1)}`; } - const fs = 24 * U; + const fs = (PORTRAIT ? 24 : 26) * U; const grid = [[10000, '10,000 km'], [100, '100 km'], [1, '1 km']] .map(([v, lab]) => ` ${lab}`).join(''); @@ -127,6 +138,7 @@ export function createHud(ctx) { el = document.createElement('div'); el.id = id; el.className = 'tag'; + el.dataset.meta = meta; el.innerHTML = `
${name}
${meta}
`; tagsEl.appendChild(el); } @@ -186,8 +198,6 @@ export function createHud(ctx) { $('#clock .date').textContent = `${mon} ${d.getUTCDate()}`; $('#clock .time').textContent = `${d.toISOString().slice(11, 16)} UTC`; $('#clock .rate').innerHTML = `1 sec = ${perSecond(rate(s))}`; - const inShadow = st.lx < 0.5 && st.sep < 5e3; - $('#clock .shadow').style.opacity = inShadow ? 1 : 0; $('#clock').style.opacity = live; // Distance readout @@ -196,18 +206,19 @@ export function createHud(ctx) { ro.querySelector('.val').innerHTML = fmtSep(st.sep); const [r, tt] = st.rel; let ctxLine; - if (st.sep < 1500) ctxLine = `≈ ${Math.max(1, Math.round(st.sep / 100))} football fields`; + const caS = EVENTS.find((e) => e.kind === 'ca').s; + if (st.sep < 1500) ctxLine = `≈ ${Math.max(1, Math.round(st.sep / 100))} football fields${s > caS && s < 36.5 ? ` · closest ${CA_M} m` : ''}`; else if (st.sep < 2e6) ctxLine = `${Math.abs(tt) >= 1e4 ? fmtNum(Math.abs(tt) / 1000) + ' km' : (Math.abs(tt) / 1000).toFixed(1) + ' km'} ${tt < 0 ? 'behind' : 'ahead'}${Math.abs(r) > 20e3 ? ` · ${fmtNum(Math.abs(r) / 1000)} km ${r > 0 ? 'higher' : 'lower'}` : ''}`; else ctxLine = `${Math.abs(r) > 20e3 ? `${fmtNum(Math.abs(r) / 1000)} km higher · ` : ''}far side of the orbit`; ro.querySelector('.ctx').textContent = ctxLine; - // Scale note + // Scale chip: amber when enlarged, white at true size. const magEl = $('#mag'); - const showMag = st.mag > 1.5 || (st.camDistX < 200 && st.sep < 2e3); - magEl.style.opacity = showMag ? live : 0; - magEl.textContent = st.mag > 1.5 - ? `Satellites drawn ${Math.round(st.mag / 5) * 5}× actual size` - : 'Actual scale · Cosmos 2612 shape is illustrative'; + const enlarged = st.mag > 1.5; + const trueSize = !enlarged && st.camDistX < 200; + magEl.style.opacity = (enlarged || trueSize) ? fade(s, 4.4, 41.2, 0.3) : 0; + magEl.classList.toggle('enlarged', enlarged); + magEl.textContent = enlarged ? `Satellites enlarged ${Math.round(st.mag)}×` : 'True size'; // Chart const px = chart.x(Math.min(Math.max(t, CH.t0), CH.t1)); @@ -223,6 +234,7 @@ export function createHud(ctx) { // Event toasts (max two, newest on top) let html = ''; + if (st.lx < 0.5 && st.sep < 5e3 && s < 41.2) html += '
In Earth\'s shadow
orbital night · sunrise in minutes
'; const active = EVENTS.map((e) => ({ e, a: fade(s, e.s, e.s + (e.kind === 'ca' ? 3.6 : e.kind === 'vantor' ? 2.8 : 2.0), 0.2) })) .filter((x) => x.a > 0).slice(-2).reverse(); for (const { e, a } of active) { @@ -233,7 +245,7 @@ export function createHud(ctx) { // Satellite tags + in-scene dimension line const pX = project(st.x36W); const pC = project(st.cosW); - const tX = tag('tagX', 'ICEYE-X36', 'U.S. radar satellite', 'c-x36'); + const tX = tag('tagX', 'ICEYE-X36', 'U.S.-registered radar satellite', 'c-x36'); const tC = tag('tagC', 'COSMOS 2612', 'Russian military', 'c-cos'); const tagA = fade(s, 4.0, 41.2, 0.4); const onScreen = (p) => p && p.x > -40 && p.y > -40 && p.x < W + 40 && p.y < H + 40; @@ -254,8 +266,9 @@ export function createHud(ctx) { el.style.opacity = 0; return; } - const a = tagA * (p.hidden ? 0.4 : 1); + const a = tagA; el.style.opacity = a; + el.querySelector('.meta').textContent = p.hidden ? 'far side of Earth' : el.dataset.meta; const w = el.offsetWidth; const h = el.offsetHeight; const quads = [prefer, [prefer[0], -prefer[1]], [-prefer[0], prefer[1]], [-prefer[0], -prefer[1]]]; @@ -287,6 +300,34 @@ export function createHud(ctx) { svg += ``; svg += ``; }; + // Dimension line + pill (offset perpendicular so it clears the line's ends). + const dim = $('#dim'); + const gap = onScreen(pX) && onScreen(pC) ? Math.hypot(pC.x - pX.x, pC.y - pX.y) : 0; + const dimA = st.sep < 60e3 && gap > 160 * U ? tagA : 0; + if (dimA > 0) { + svg += ``; + const nx = -(pC.y - pX.y) / gap; + const ny = (pC.x - pX.x) / gap; + const side = ny > 0 ? -1 : 1; // put the pill above the line + dim.innerHTML = fmtSep(st.sep).replace(/<\/?small>/g, ' ').trim(); + const mx = (pX.x + pC.x) / 2 + side * nx * 40 * U; + const my = (pX.y + pC.y) / 2 + side * ny * 40 * U; + dim.style.left = `${mx}px`; + dim.style.top = `${my}px`; + const w = dim.offsetWidth; + const h = dim.offsetHeight; + obstacles.push({ left: mx - w / 2, right: mx + w / 2, top: my - h / 2, bottom: my + h / 2 }); + } + dim.style.opacity = dimA; + // Pulse rings on both satellites at the closest moment. + const caS2 = EVENTS.find((e) => e.kind === 'ca').s; + const k = (s - caS2) / 1.2; + if (k > 0 && k < 1) { + for (const p of [pX, pC]) { + if (!onScreen(p)) continue; + svg += ``; + } + } let pX0 = [1, -1]; let pC0 = [-1, 1]; if (onScreen(pX) && onScreen(pC)) { @@ -298,17 +339,6 @@ export function createHud(ctx) { place(tX, pX, pX0, '#5ce1e6'); place(tC, pC, pC0, '#ff5a3c'); - // Dimension line with a distance pill, when the pair is close and both visible. - const dim = $('#dim'); - const gap = onScreen(pX) && onScreen(pC) ? Math.hypot(pC.x - pX.x, pC.y - pX.y) : 0; - const dimA = st.sep < 60e3 && gap > 160 * U ? tagA : 0; - if (dimA > 0) { - svg += ``; - dim.style.left = `${(pX.x + pC.x) / 2}px`; - dim.style.top = `${(pX.y + pC.y) / 2}px`; - dim.innerHTML = fmtSep(st.sep).replace(/<\/?small>/g, ' ').trim(); - } - dim.style.opacity = dimA; leaders.innerHTML = svg; // Burn flash ring on Cosmos @@ -327,13 +357,13 @@ export function createHud(ctx) { ec.dataset.built = '1'; ec.innerHTML = `
~${CA_M} m
-
Russia's Cosmos 2612 got within about ${CA_M} meters of the U.S. radar satellite ICEYE-X36.
+
estimated · published range 150–650 m
+
Closest pass between Cosmos 2612 and ICEYE-X36 · Oct 18, in Earth's shadow
-
22days of stalking
+
22days in pursuit
8engine burns
-
0.9 m/sclosest-pass speed
+
0.9 m/sspeed at closest pass
-
Tracking firm LeoLabs says maneuvers like these could support spying, inspection, interference — or preparing future attacks.
`; } $('#endlogo').style.opacity = endA; diff --git a/tools/rpo-replay/scene.html b/tools/rpo-replay/scene.html index 7c6e1f0ef..7c6f35032 100644 --- a/tools/rpo-replay/scene.html +++ b/tools/rpo-replay/scene.html @@ -43,43 +43,45 @@ /* Chapters */ #chapters { position: absolute; left: calc(520 * var(--u)); right: calc(440 * var(--u)); top: calc(58 * var(--u)); - display: grid; grid-template-columns: repeat(4, 1fr); gap: calc(12 * var(--u)); } + display: grid; grid-template-columns: repeat(5, 1fr); gap: calc(12 * var(--u)); } .ch i { display: block; height: calc(6 * var(--u)); background: rgba(255,255,255,0.22); border-radius: calc(3 * var(--u)); overflow: hidden; } .ch b { display: block; height: 100%; width: 0; background: #fff; } - .ch span { display: block; margin-top: calc(10 * var(--u)); font: 700 calc(24 * var(--u))/1 Inter, sans-serif; + .ch span { display: block; margin-top: calc(10 * var(--u)); white-space: nowrap; font: 700 calc(24 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); } .ch.on span { color: var(--ink); } /* Caption */ #caption { position: absolute; left: var(--pad); top: calc(150 * var(--u)); width: calc(1000 * var(--u)); } - #caption h1 { margin: 0 0 calc(14 * var(--u)); font: 800 calc(66 * var(--u))/1.04 Inter, sans-serif; letter-spacing: -0.025em; } - #caption p { margin: 0; font: 500 calc(36 * var(--u))/1.28 Inter, sans-serif; color: var(--ink-2); max-width: calc(900 * var(--u)); } + #caption h1 { margin: 0 0 calc(14 * var(--u)); font: 800 calc(70 * var(--u))/1.04 Inter, sans-serif; letter-spacing: -0.025em; } + #caption p { margin: 0; font: 500 calc(40 * var(--u))/1.26 Inter, sans-serif; color: var(--ink-2); max-width: calc(900 * var(--u)); } #caption p b { color: var(--ink); font-weight: 700; } #caption.hook { top: calc(170 * var(--u)); width: calc(1240 * var(--u)); } #caption.hook h1 { font-size: calc(84 * var(--u)); } - #caption.hook p { font-size: calc(38 * var(--u)); max-width: calc(1100 * var(--u)); } + #caption.hook p { font-size: calc(42 * var(--u)); max-width: calc(1100 * var(--u)); } /* Events */ - #events { position: absolute; left: var(--pad); top: calc(420 * var(--u)); width: calc(900 * var(--u)); } + #events { position: absolute; left: var(--pad); top: calc(440 * var(--u)); width: calc(900 * var(--u)); } .event { display: flex; gap: calc(16 * var(--u)); align-items: flex-start; margin-bottom: calc(18 * var(--u)); } .event i { flex: none; width: calc(18 * var(--u)); height: calc(18 * var(--u)); border-radius: 50%; margin-top: calc(8 * var(--u)); background: var(--burn); box-shadow: 0 0 calc(16 * var(--u)) var(--burn); } - .event .t { font: 800 calc(32 * var(--u))/1.1 Inter, sans-serif; letter-spacing: 0.02em; text-transform: uppercase; color: var(--burn); } - .event .sub { font: 500 calc(28 * var(--u))/1.25 Inter, sans-serif; color: var(--ink-2); margin-top: calc(4 * var(--u)); } + .event .t { font: 800 calc(34 * var(--u))/1.1 Inter, sans-serif; letter-spacing: 0.02em; text-transform: uppercase; color: var(--burn); } + .event .sub { font: 500 calc(30 * var(--u))/1.25 Inter, sans-serif; color: var(--ink-2); margin-top: calc(4 * var(--u)); } .event.note i { background: #fff; box-shadow: 0 0 calc(12 * var(--u)) #fff; } .event.note .t { color: #fff; } .event.vantor i { background: #c9b8ff; box-shadow: 0 0 calc(12 * var(--u)) #c9b8ff; } .event.vantor .t { color: #c9b8ff; } .event.ca i { background: #fff; } .event.ca .t { color: #fff; } + .event.shadow i { background: #9fb4ff; box-shadow: 0 0 calc(12 * var(--u)) #9fb4ff; } + .event.shadow .t { color: #b9c8ff; } /* Clock */ #clock { position: absolute; right: var(--pad); top: calc(40 * var(--u)); text-align: right; } #clock .date { font: 700 calc(48 * var(--u))/1 'JetBrains Mono', monospace; letter-spacing: -0.02em; } #clock .time { font: 500 calc(28 * var(--u))/1 'JetBrains Mono', monospace; color: var(--ink-2); margin-top: calc(10 * var(--u)); } #clock .rate { display: inline-block; margin-top: calc(16 * var(--u)); padding: calc(10 * var(--u)) calc(16 * var(--u)); - font: 800 calc(30 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.02em; color: #000; background: #fff; border-radius: calc(8 * var(--u)); } - #clock .shadow { margin-top: calc(14 * var(--u)); font: 700 calc(26 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.04em; color: #b9c8ff; } + font: 800 calc(34 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.02em; color: #000; background: #fff; border-radius: calc(8 * var(--u)); } + #clock .shadow { display: none; margin-top: calc(14 * var(--u)); font: 700 calc(26 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.04em; color: #b9c8ff; } /* Readout */ #readout { position: absolute; right: var(--pad); bottom: calc(250 * var(--u)); text-align: right; } @@ -88,65 +90,68 @@ #readout .val small { font-size: 0.42em; color: var(--ink-2); margin-left: 0.12em; letter-spacing: 0; } #readout .ctx { margin-top: calc(10 * var(--u)); font: 600 calc(32 * var(--u))/1.2 Inter, sans-serif; color: var(--ink); } - #mag { position: absolute; left: var(--pad); bottom: calc(262 * var(--u)); font: 600 calc(24 * var(--u))/1.3 Inter, sans-serif; color: var(--ink-3); } + #mag { position: absolute; left: var(--pad); bottom: calc(276 * var(--u)); padding: calc(7 * var(--u)) calc(14 * var(--u)); border-radius: calc(6 * var(--u)); + font: 800 calc(28 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.02em; color: #000; background: #fff; } + #mag.enlarged { background: var(--burn); } /* Chart */ #chartwrap { position: absolute; left: var(--pad); right: var(--pad); bottom: calc(58 * var(--u)); } - #chartwrap .lab { font: 700 calc(22 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.12em; color: var(--ink-3); margin-bottom: calc(10 * var(--u)); } - #chart { height: calc(160 * var(--u)); } + #chartwrap .lab { font: 700 calc(24 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.12em; color: var(--ink-3); margin-bottom: calc(10 * var(--u)); } + #chart { height: calc(170 * var(--u)); } #chart svg { width: 100%; height: 100%; overflow: visible; font-family: 'JetBrains Mono', monospace; font-weight: 600; } /* Tags + dimension */ #leaders { position: absolute; inset: 0; width: 100%; height: 100%; } .tag { position: absolute; white-space: nowrap; } - .tag .name { font: 800 calc(34 * var(--u))/1.05 Inter, sans-serif; letter-spacing: 0.01em; } - .tag .meta { font: 600 calc(24 * var(--u))/1.2 Inter, sans-serif; color: var(--ink-2); margin-top: calc(4 * var(--u)); } - #dim { position: absolute; transform: translate(-50%, -50%); padding: calc(6 * var(--u)) calc(14 * var(--u)); - font: 700 calc(30 * var(--u))/1 'JetBrains Mono', monospace; color: #000; background: #fff; border-radius: calc(6 * var(--u)); } + .tag .name { font: 800 calc(40 * var(--u))/1.05 Inter, sans-serif; letter-spacing: 0.01em; } + .tag .meta { font: 600 calc(28 * var(--u))/1.2 Inter, sans-serif; color: var(--ink-2); margin-top: calc(4 * var(--u)); } + #dim { position: absolute; transform: translate(-50%, -50%); padding: calc(6 * var(--u)) calc(14 * var(--u)); white-space: nowrap; + font: 700 calc(34 * var(--u))/1 'JetBrains Mono', monospace; color: #000; background: #fff; border-radius: calc(6 * var(--u)); } /* Attribution: present, deliberately quiet */ #credits { position: absolute; left: var(--pad); right: var(--pad); bottom: calc(16 * var(--u)); - font: 400 calc(15 * var(--u))/1.3 Inter, sans-serif; color: rgba(244,247,250,0.36); white-space: nowrap; overflow: hidden; } + font: 400 calc(17 * var(--u))/1.3 Inter, sans-serif; color: rgba(244,247,250,0.45); white-space: nowrap; overflow: hidden; } /* End card */ #endcard { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } #endcard .panel { width: calc(1300 * var(--u)); text-align: center; } - #endcard .big { font: 800 calc(190 * var(--u))/0.9 'JetBrains Mono', monospace; letter-spacing: -0.05em; color: var(--cosmos); } + #endcard .big { font: 800 calc(200 * var(--u))/0.9 Inter, sans-serif; letter-spacing: -0.04em; color: var(--cosmos); } + #endcard .range { margin-top: calc(18 * var(--u)); font: 600 calc(32 * var(--u))/1.2 Inter, sans-serif; color: var(--ink-2); } #endcard .lead { margin: calc(26 * var(--u)) auto 0; max-width: calc(1100 * var(--u)); font: 700 calc(44 * var(--u))/1.2 Inter, sans-serif; letter-spacing: -0.01em; } #endcard .stats { display: flex; justify-content: center; gap: calc(80 * var(--u)); margin: calc(44 * var(--u)) 0; } #endcard .stats b { display: block; font: 700 calc(64 * var(--u))/1 'JetBrains Mono', monospace; letter-spacing: -0.03em; } #endcard .stats span { display: block; margin-top: calc(10 * var(--u)); font: 600 calc(28 * var(--u))/1.2 Inter, sans-serif; color: var(--ink-2); } #endcard .why { max-width: calc(1100 * var(--u)); margin: 0 auto; font: 500 calc(32 * var(--u))/1.35 Inter, sans-serif; color: var(--ink-2); } #endlogo { left: 50%; top: calc(70 * var(--u)); transform: translateX(-50%); } - #endlogo img { height: calc(84 * var(--u)); } - #endlogo span { font-size: calc(36 * var(--u)); } + #endlogo img { height: calc(96 * var(--u)); } + #endlogo span { font-size: calc(40 * var(--u)); } - /* ---------------- Portrait ---------------- */ + /* ---------------- Portrait (Reels/Shorts/TikTok safe zones: top ~170, bottom ~400) ---------------- */ body.portrait { --pad: calc(52 * var(--u)); } - body.portrait #logo { top: calc(64 * var(--u)); } - body.portrait #clock { top: calc(56 * var(--u)); } + body.portrait #logo { top: calc(168 * var(--u)); } + body.portrait #clock { top: calc(160 * var(--u)); } body.portrait #clock .date { font-size: calc(44 * var(--u)); } - body.portrait #chapters { left: var(--pad); right: var(--pad); top: calc(236 * var(--u)); } - body.portrait #caption, body.portrait #caption.hook { top: calc(316 * var(--u)); width: auto; right: var(--pad); } - body.portrait #caption h1 { font-size: calc(70 * var(--u)); } - body.portrait #caption.hook h1 { font-size: calc(80 * var(--u)); } + body.portrait #chapters { left: var(--pad); right: var(--pad); top: calc(318 * var(--u)); } + body.portrait .ch span { font-size: calc(22 * var(--u)); letter-spacing: 0.02em; } + body.portrait #caption, body.portrait #caption.hook { top: calc(392 * var(--u)); width: auto; right: var(--pad); } + body.portrait #caption h1 { font-size: calc(68 * var(--u)); } + body.portrait #caption.hook h1 { font-size: calc(76 * var(--u)); } body.portrait #caption p, body.portrait #caption.hook p { font-size: calc(38 * var(--u)); max-width: none; } - body.portrait #events { top: calc(640 * var(--u)); width: auto; right: var(--pad); } - body.portrait #readout { right: auto; left: var(--pad); bottom: calc(590 * var(--u)); text-align: left; } - body.portrait #readout .val { font-size: calc(140 * var(--u)); } - body.portrait #mag { left: auto; right: var(--pad); bottom: calc(600 * var(--u)); text-align: right; max-width: calc(380 * var(--u)); } - body.portrait #chartwrap { bottom: calc(340 * var(--u)); } - body.portrait #chart { height: calc(190 * var(--u)); } - body.portrait #topshade { height: calc(900 * var(--u)); } - body.portrait #botshade { height: calc(1100 * var(--u)); background: linear-gradient(transparent, rgba(0,0,0,0.78) 45%, rgba(0,0,0,0.85)); } - body.portrait #credits { white-space: normal; bottom: calc(250 * var(--u)); font-size: calc(17 * var(--u)); } + body.portrait #events { top: calc(700 * var(--u)); width: auto; right: var(--pad); } + body.portrait #readout { right: auto; left: var(--pad); bottom: calc(690 * var(--u)); text-align: left; } + body.portrait #readout .val { font-size: calc(132 * var(--u)); } + body.portrait #mag { left: auto; right: var(--pad); bottom: calc(700 * var(--u)); } + body.portrait #chartwrap { bottom: calc(450 * var(--u)); } + body.portrait #chart { height: calc(170 * var(--u)); } + body.portrait #topshade { height: calc(1000 * var(--u)); } + body.portrait #botshade { height: calc(1200 * var(--u)); background: linear-gradient(transparent, rgba(0,0,0,0.78) 45%, rgba(0,0,0,0.85)); } + body.portrait #credits { white-space: normal; bottom: calc(400 * var(--u)); font-size: calc(17 * var(--u)); } body.portrait #endcard .panel { width: calc(960 * var(--u)); } - body.portrait #endcard .big { font-size: calc(200 * var(--u)); } - body.portrait #endcard .lead { font-size: calc(48 * var(--u)); } + body.portrait #endcard .big { font-size: calc(210 * var(--u)); } + body.portrait #endcard .lead { font-size: calc(46 * var(--u)); } body.portrait #endcard .stats { gap: calc(40 * var(--u)); } body.portrait #endcard .stats b { font-size: calc(58 * var(--u)); } - body.portrait #endcard .why { font-size: calc(36 * var(--u)); } - body.portrait #endlogo { top: calc(170 * var(--u)); } + body.portrait #endlogo { top: calc(260 * var(--u)); } @@ -167,7 +172,7 @@
DISTANCE OVER 22 DAYS · LOG SCALE
-
Reconstruction from public data (CelesTrak elements; burn timeline: KeepTrack, LeoLabs, M. Langbroek) · Imagery: Esri, Maxar, Earthstar Geographics; NASA Black Marble · Stars: Yale BSC, ESO/S. Brunier (CC BY 4.0) · Not tracking data
+
Reconstruction, not tracking data · Orbits: CelesTrak, KeepTrack, LeoLabs, M. Langbroek · Imagery: Esri, Maxar, Earthstar Geographics, NASA · Sky: ESO/S. Brunier (CC BY 4.0), Yale BSC
diff --git a/tools/rpo-replay/scene.js b/tools/rpo-replay/scene.js index f25f4a6c5..99df608dc 100644 --- a/tools/rpo-replay/scene.js +++ b/tools/rpo-replay/scene.js @@ -188,15 +188,15 @@ const CAM = [ { s: 15.4, dist: 1.6e6, f: 2.8, az: 32, el: 36, look: 0.5, earth: 0, fov: 34, mag: 0, trail: 8 }, { s: 18.0, dist: 1.4e6, f: 2.8, az: 26, el: 34, look: 0.5, earth: 0, fov: 34, mag: 0, trail: 12 }, { s: 20.4, dist: 4.0e4, f: 2.8, az: 40, el: 32, look: 0.5, earth: 0, fov: 34, mag: 0, trail: 10 }, - { s: 22.4, dist: 6.5e3, f: 2.4, az: 58, el: 34, look: 0.45, earth: 0, fov: 36, mag: 1, trail: 9 }, - { s: 26.0, dist: 4.6e3, f: 2.4, az: 76, el: 30, look: 0.5, earth: 0, fov: 36, mag: 1, trail: 9 }, - { s: 28.8, dist: 2.2e3, f: 2.4, az: 100, el: 22, look: 0.4, earth: 0, fov: 36, mag: 1, trail: 4 }, - { s: 29.9, dist: 12, f: 0, az: 96, el: 16, look: 0.0, earth: 0, fov: 40, mag: 0, trail: 0.3 }, - { s: 30.8, dist: 13, f: 0, az: 122, el: 14, look: 0.0, earth: 0, fov: 40, mag: 0, trail: 0.3 }, - { s: 31.6, dist: 18, f: 0, az: 168, el: 9, look: 0.0, earth: 0, fov: 40, mag: 0, trail: 0.3, aimRel: [0, -60, 0] }, - { s: 32.6, dist: 1.0e3, f: 0, az: 112, el: 26, look: 0.5, earth: 0, fov: 36, mag: 1, trail: 1.2 }, - { s: 35.0, dist: 1.3e3, f: 0, az: 96, el: 18, look: 0.5, earth: 0, fov: 36, mag: 1, trail: 1.6 }, - { s: 36.6, dist: 1.2e4, f: 2.8, az: 110, el: 30, look: 0.4, earth: 0, fov: 36, mag: 1, trail: 4 }, + { s: 22.4, dist: 6.5e3, f: 2.4, az: 58, el: 34, look: 0.45, earth: 0, fov: 36, mag: 1, mx: 40, trail: 9 }, + { s: 26.0, dist: 4.6e3, f: 2.4, az: 76, el: 30, look: 0.5, earth: 0, fov: 36, mag: 1, mx: 40, trail: 9 }, + { s: 29.7, dist: 2.0e3, f: 2.2, az: 104, el: 24, look: 0.5, earth: 0, fov: 36, mag: 1, mx: 25, trail: 4 }, + { s: 31.9, dist: 1.0e3, f: 2.2, az: 116, el: 22, look: 0.5, earth: 0, fov: 36, mag: 1, mx: 25, trail: 3 }, + { s: 31.9, cut: true, dist: 25, f: 0, az: 165, el: 8, look: 0.0, earth: 0, fov: 34, mag: 0, trail: 0.5, aimRel: [0, -60, 0] }, + { s: 33.9, dist: 22, f: 0, az: 170, el: 7, look: 0.0, earth: 0, fov: 34, mag: 0, trail: 0.5, aimRel: [0, -70, 0] }, + { s: 33.9, cut: true, dist: 1.3e3, f: 2.2, az: 96, el: 18, look: 0.5, earth: 0, fov: 36, mag: 1, mx: 25, trail: 1.6 }, + { s: 35.6, dist: 1.6e3, f: 2.4, az: 100, el: 22, look: 0.5, earth: 0, fov: 36, mag: 1, mx: 25, trail: 2 }, + { s: 37.0, dist: 1.2e4, f: 2.8, az: 110, el: 30, look: 0.4, earth: 0, fov: 36, mag: 1, mx: 40, trail: 4 }, { s: 39.0, dist: 9.0e5, f: 2.8, az: 60, el: 30, look: 0.5, earth: 0, fov: 34, mag: 0, trail: 30 }, { s: 41.2, dist: 2.0e6, f: 2.8, az: 44, el: 30, look: 0.5, earth: 0, fov: 34, mag: 0, trail: 60 }, { s: 41.2, cut: true, ...WIDE, dist: 4.8e7, az: 80, el: 34, trail: 60 }, @@ -227,6 +227,7 @@ function camAt(s) { earth: lerp(a.earth, b.earth), fov: lerp(a.fov, b.fov), mag: lerp(a.mag, b.mag), + mx: Math.exp(lerp(Math.log(a.mx || 1), Math.log(b.mx || 1))), trail: Math.exp(lerp(Math.log(a.trail), Math.log(b.trail))), aimRel: [0, 1, 2].map((k) => lerp(ar[k], br[k])), sun: lerp(a.sun || 0, b.sun || 0), @@ -489,11 +490,11 @@ async function frame(s) { const pxPerRad = (PORTRAIT ? W : W) / (2 * Math.tan(scene.camera.frustum.fov / 2)); const targetPx = 110 * U; // magnified apparent size const magFor = (d, size) => Math.max(1, (targetPx * d) / (pxPerRad * size)); - const magX = 1 + (magFor(camDistX, 3.25) - 1) * cam.mag; - const magC = 1 + (magFor(camDistC, 3.6) - 1) * cam.mag; - const MAG_MAX = 150; // beyond this, a glyph is more honest than a giant model - const rawMag = Math.max(1, Math.min(magX, magC)); - const mag = rawMag > MAG_MAX ? 1 : rawMag; + // One fixed enlargement per beat (keyed), so the on-screen factor holds still. + const rawMag = cam.mag > 0.5 ? cam.mx : 1; + const MAG_MAX = 150; + const mag = rawMag; + void magFor; const nearScale = Math.min(1, Math.max(0, cam.mag * 2)); mX36.show = (cam.mag > 0.01 && rawMag <= MAG_MAX) || camDistX < 3000; mCos.show = (cam.mag > 0.01 && rawMag <= MAG_MAX) || camDistC < 6000;