tools(rpo-replay): v3 WIP — review fixes (closest-pass sequence, fixed scale, safe zones, copy)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0187rSCa4AKJPGw6uiuPSGyX
This commit is contained in:
Claude committed 2026-10-25 05:59:01 +00:00
1 parent 9d3d541b66
commit 8921d87b53
3 files changed
+131 -95

No files matched your search

+69 -39
View File
@@ -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 <span class="c-cos">~${CA_M} m</span>`,
body: 'How <b class="c-cos">Cosmos 2612</b> hunted <b class="c-x36">ICEYE-X36</b> 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 <span class="c-cos">~${CA_M} m</span>`,
body: 'How <b class="c-cos">Cosmos 2612</b> matched <b class="c-x36">ICEYE-X36</b>\'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 <b>47 km</b>, 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 <b>480 km behind</b> — then closes ~350 km a day.',
body: 'Oct 12: it dives 47 km and lands about <b>480 km behind</b>.',
},
{
s0: 17.2, s1: 22.0,
title: 'Speed up, then brake',
body: 'Oct 15: a 2 km dip makes it faster — closing <b>~350 km a day</b> — 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 <b>1.6 km</b> every orbit, inching in for two days.',
body: 'Each orbit its path loops <b>1.6 km</b>, inching in for two days.',
},
{
s0: 29.7, s1: 35.6,
title: `Closest pass: <span class="c-cos">~${CA_M} meters</span>`,
body: 'In Earth\'s shadow, at walking pace — while both fly at <b>27,000 km/h</b>.',
s0: 29.7, s1: 32.3,
title: 'Into Earth\'s shadow',
body: 'Closing at strolling pace (<b>0.9 m/s</b>) — while both fly at 27,000 km/h.',
},
{
s0: 32.3, s1: 35.6,
title: `Closest pass: <span class="c-cos">~${CA_M} m</span>`,
body: 'Shown at true size. Published estimates: <b>150–650 m</b>.',
},
{
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]) => `<line x1="${padL}" x2="${cw}" y1="${y(v)}" y2="${y(v)}" stroke="rgba(238,242,246,0.14)" stroke-width="${1.5 * U}"/>
<text x="${padL - 14 * U}" y="${y(v) + fs * 0.35}" text-anchor="end" font-size="${fs}" fill="rgba(238,242,246,0.6)">${lab}</text>`).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 = `<div class="name ${cls}">${name}</div><div class="meta">${meta}</div>`;
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 += '<div class="event shadow"><i></i><div><div class="t">In Earth\'s shadow</div><div class="sub">orbital night · sunrise in minutes</div></div></div>';
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 += `<line x1="${p.x + best.sx * 12 * U}" y1="${p.y + best.sy * 12 * U}" x2="${best.lx}" y2="${best.ly}" stroke="${color}" stroke-opacity="${a * 0.85}" stroke-width="${2.5 * U}"/>`;
svg += `<circle cx="${p.x}" cy="${p.y}" r="${15 * U}" fill="none" stroke="${color}" stroke-opacity="${a * 0.9}" stroke-width="${2.5 * U}"/>`;
};
// 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 += `<line x1="${pX.x}" y1="${pX.y}" x2="${pC.x}" y2="${pC.y}" stroke="#fff" stroke-opacity="${0.75 * dimA}" stroke-width="${2.5 * U}" stroke-dasharray="${10 * U} ${8 * U}"/>`;
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 += `<circle cx="${p.x}" cy="${p.y}" r="${(18 + 90 * k) * U}" fill="none" stroke="#fff" stroke-opacity="${(1 - k) * 0.9}" stroke-width="${4 * U}"/>`;
}
}
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 += `<line x1="${pX.x}" y1="${pX.y}" x2="${pC.x}" y2="${pC.y}" stroke="#fff" stroke-opacity="${0.75 * dimA}" stroke-width="${2.5 * U}" stroke-dasharray="${10 * U} ${8 * U}"/>`;
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 = `<div class="panel">
<div class="big">~${CA_M} m</div>
<div class="lead">Russia's Cosmos 2612 got within about ${CA_M} meters of the U.S. radar satellite ICEYE-X36.</div>
<div class="range">estimated · published range 150–650 m</div>
<div class="lead">Closest pass between Cosmos 2612 and ICEYE-X36 · Oct 18, in Earth's shadow</div>
<div class="stats">
<div><b>22</b><span>days of stalking</span></div>
<div><b>22</b><span>days in pursuit</span></div>
<div><b>8</b><span>engine burns</span></div>
<div><b>0.9 m/s</b><span>closest-pass speed</span></div>
<div><b>0.9 m/s</b><span>speed at closest pass</span></div>
</div>
<div class="why">Tracking firm LeoLabs says maneuvers like these could support spying, inspection, interference — or preparing future attacks.</div>
</div>`;
}
$('#endlogo').style.opacity = endA;
+47 -42
View File
@@ -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)); }
</style>
</head>
<body>
@@ -167,7 +172,7 @@
<div id="chartwrap"><div class="lab">DISTANCE OVER 22 DAYS · LOG SCALE</div><div id="chart"></div></div>
<div id="endcard"></div>
<div id="endlogo"><img src="logo.svg" alt=""><span>GOD'S EYE VIEW</span></div>
<div id="credits">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</div>
<div id="credits">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</div>
<div id="dip" style="position:absolute;inset:0;background:#000;opacity:0"></div>
</div>
<script src="/cesium/Cesium.js"></script>
+15 -14
View File
@@ -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;