mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 12:23:59 +00:00
tools(rpo-replay): v5 — critic fixes: relative-view inset, corrected copy, laps readout, hook into shadow, portrait layout
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0187rSCa4AKJPGw6uiuPSGyX
This commit is contained in:
5 files changed
+368
-118
No files matched your search
+62
-39
@@ -4,10 +4,13 @@
|
||||
// except label placement, which carries state between consecutive frames so
|
||||
// labels hold their side (hysteresis) and ease instead of jumping.
|
||||
|
||||
import { createInset } from './inset.js';
|
||||
|
||||
export function createHud(ctx) {
|
||||
const { D, W, H, U, PORTRAIT, sec, rate, sOfTau, CA_T, VANTOR_T, BURNS, CUTS, SHOTS, shotAt, DUR, project } = ctx;
|
||||
const { D, W, H, U, PORTRAIT, sec, rate, sOfTau, CA_T, VANTOR_T, BURNS, CUTS, SHOTS, shotAt, DUR, project, relAt } = ctx;
|
||||
const $ = (q) => document.querySelector(q);
|
||||
const CA_M = Math.round(D.ca.m / 5) * 5;
|
||||
// Element sets can't resolve tens of metres: quote the pass to the nearest 50 m.
|
||||
const CA_M = Math.round(D.ca.m / 50) * 50;
|
||||
const END = SHOTS.find((x) => x.id === 'end').s0;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -26,33 +29,33 @@ export function createHud(ctx) {
|
||||
];
|
||||
const cap = (id, title, body, extra = {}) => ({ s0: span(id)[0], s1: span(id)[1], title, body, ...extra });
|
||||
const CAPTIONS = [
|
||||
cap('hook', `A Russian military satellite came within <span class="c-cos nw">~${CA_M} m</span> of a U.S.-registered one`,
|
||||
'This is how it got there, rebuilt from public tracking data.', { hook: true, s0: -1 }),
|
||||
cap('hook', 'A Russian military satellite came within about half a kilometre of a U.S. radar satellite',
|
||||
'Rebuilt from public tracking data.', { hook: true, s0: -1 }),
|
||||
cap('climb', 'Climb higher to fall behind',
|
||||
'Same orbit, a quarter-orbit apart. Two burns lift <b class="c-cos">Cosmos 2612</b> <b>47 km</b>, and a higher orbit is a slower one.'),
|
||||
cap('lap', 'Fall behind, and lap it',
|
||||
'Drifting back <b>~6,700 km a day</b>, it laps almost twice, passing over <b class="c-x36">X36</b> on Oct 5 and Oct 12, as its orbit plane swings into line.'),
|
||||
'Two burns lift <b class="c-cos">Cosmos 2612</b> <b>47 km</b>. A higher orbit is a longer lap, so it falls behind.'),
|
||||
cap('lap', 'Lapped, twice',
|
||||
'Losing <b>~6,700 km a day</b>, it is lapped by <b class="c-x36">X36</b> on Oct 5 and Oct 12, while Earth\'s bulge swings the two orbits into line.'),
|
||||
cap('drop', 'Drop into the same orbit',
|
||||
'Oct 12: from <b>47 km</b> directly overhead it dives into X36\'s orbit, and lands about <b>480 km behind</b>.'),
|
||||
'Oct 12: from <b>54 km</b> directly overhead it dives into X36\'s orbit, landing <b>~500 km behind</b>.'),
|
||||
cap('close', 'Speed up, then brake',
|
||||
'A 2 km dip makes it faster, closing <b>~350 km a day</b>. Then it brakes 3 km behind.'),
|
||||
'A 2 km dip makes it faster, closing <b>~350 km a day</b>. Then it brakes <b>~10 km</b> behind.'),
|
||||
cap('dance', 'The dance',
|
||||
'Every orbit is a day and a night. Each one, Cosmos loops <b>~1.5 km</b> around X36, 3 km behind it.'),
|
||||
'Every orbit is a day and a night. Each one, Cosmos traces a <b>~1.5 km</b> loop as it edges in from 8 to 3 km behind.'),
|
||||
cap('dance-ff', 'Flatten the loops',
|
||||
'A tiny burn puts Cosmos almost exactly in X36\'s path. It creeps in <b>~3 km a day</b>.'),
|
||||
cap('approach', 'Closing in, slowly',
|
||||
'Creeping in at <b>a few centimetres per second</b>, while both fly at <b>27,000 km/h</b>.'),
|
||||
'Creeping in at <b>a few centimetres a second</b>, while both fly at <b>27,000 km/h</b>.'),
|
||||
cap('ca', `Closest pass: <span class="c-cos nw">~${CA_M} m</span>`,
|
||||
'True size. LeoLabs measured <b>~500 m</b>; other estimates run <b>150 to 650 m</b>.'),
|
||||
'In Earth\'s shadow, ~01:17 UTC. LeoLabs radar: <b>~500 m</b>. Public estimates: <b>150–650 m</b>.'),
|
||||
cap('sunrise', 'Then the sun comes up',
|
||||
'Twenty minutes later both leave Earth\'s shadow. Within hours, Cosmos backs away.'),
|
||||
'Twenty minutes later both leave Earth\'s shadow. Within the hour, Cosmos backs away.'),
|
||||
cap('leave', 'Back off, and wait',
|
||||
'It drifts back <b>~350 km a day</b>. By Oct 25 it sat <b>~480 km behind</b>, in the same orbit.'),
|
||||
'It drifts back <b>~350 km a day</b>, then matches orbits again. By Oct 25 it sat <b>~480 km behind</b>.'),
|
||||
];
|
||||
const BURN_NOTES = {
|
||||
'Phasing raise': 'raises its orbit 46 km',
|
||||
'Phasing raise': 'two burns raise its orbit 46 km',
|
||||
'Plane trim': 'tilts its orbit into line',
|
||||
'Descent to X36 orbit': 'drops 47 km into X36\'s orbit',
|
||||
'Descent to X36 orbit': 'drops 48 km into X36\'s orbit',
|
||||
'Closing burn': 'dips 2.3 km to speed up',
|
||||
'Braking burn': 'rises 2.5 km to stop',
|
||||
'Back-off': 'rises 140 m and starts to drift back',
|
||||
@@ -68,7 +71,7 @@ export function createHud(ctx) {
|
||||
{ t: sec('2026-10-05T12:28:00Z'), kind: 'note', title: 'Passes overhead', sub: '55 km above its target' },
|
||||
{ t: sec('2026-10-12T00:30:00Z'), kind: 'note', title: 'Directly overhead', sub: '54 km above its target' },
|
||||
{ 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 · in Earth's shadow` },
|
||||
{ t: CA_T, kind: 'ca', title: 'Closest approach', sub: '~01:17 UTC, Oct 18 · in Earth\'s shadow' },
|
||||
].map((e) => ({ ...e, s: sOfTau(e.t) })).filter((e) => e.s !== null).sort((a, b) => a.s - b.s);
|
||||
const caS = EVENTS.find((e) => e.kind === 'ca')?.s ?? 1e9;
|
||||
|
||||
@@ -135,6 +138,13 @@ export function createHud(ctx) {
|
||||
return `${(r / 86400).toFixed(1)} days`.replace('.0 days', ' days');
|
||||
}
|
||||
|
||||
// "Seen from X36" inset (relative frame).
|
||||
const insetEl = $('#inset');
|
||||
insetEl.width = Math.round(insetEl.clientWidth * 2);
|
||||
insetEl.height = Math.round(insetEl.clientHeight * 2);
|
||||
const inset = createInset({ D, U, relAt, canvas: insetEl });
|
||||
if (PORTRAIT) $('#readout').prepend($('#clock .light'));
|
||||
|
||||
// Label placement memory (hysteresis).
|
||||
const memo = { tagX: null, tagC: null, lastS: -1 };
|
||||
|
||||
@@ -143,7 +153,7 @@ export function createHud(ctx) {
|
||||
// -------------------------------------------------------------------------
|
||||
function update(s, t, b, st) {
|
||||
const sh = shotAt(s);
|
||||
const endA = fade(s, END, 99, 0.5);
|
||||
const endA = fade(s, END - 0.15, 99, 0.25);
|
||||
const live = 1 - endA;
|
||||
if (Math.abs(s - memo.lastS) > 0.2 || CUTS.some((c) => memo.lastS < c && s >= c)) {
|
||||
memo.tagX = null;
|
||||
@@ -191,27 +201,32 @@ export function createHud(ctx) {
|
||||
$('#clock .rate').innerHTML = `1 sec = ${perSecond(rate(s))}`;
|
||||
$('#clock .ff').style.display = sh.mode === 'ff' ? 'inline-block' : 'none';
|
||||
const lit = st.lx > 0.5;
|
||||
const light = $('#clock .light');
|
||||
const light = $('.light');
|
||||
light.className = `light ${lit ? 'day' : 'night'}`;
|
||||
light.innerHTML = lit ? '<i></i>In sunlight' : '<i></i>In Earth\'s shadow';
|
||||
const orbitN = sh.id === 'dance' ? Math.floor((t - sh.tauF(sh.s0)) / 5730) + 1 : 0;
|
||||
light.innerHTML = (lit ? '<i></i>Day' : '<i></i>Night · Earth\'s shadow') + (orbitN ? ` · orbit ${orbitN}` : '');
|
||||
light.style.display = sh.mode === 'true' && sh.id !== 'climb' && sh.id !== 'end' ? 'inline-flex' : 'none';
|
||||
$('#clock').style.opacity = live;
|
||||
|
||||
// Distance readout
|
||||
const ro = $('#readout');
|
||||
ro.style.opacity = fade(s, climbS + 0.6, END, 0.4);
|
||||
ro.querySelector('.val').innerHTML = fmtSep(st.sep);
|
||||
const lapMode = sh.id === 'climb' || sh.id === 'lap';
|
||||
ro.querySelector('.label').textContent = lapMode ? 'GAP ALONG THE ORBIT' : 'DISTANCE APART';
|
||||
ro.querySelector('.val').innerHTML = lapMode
|
||||
? `${(-st.rel[1] / (2 * Math.PI * D.a_ref_km * 1000)).toFixed(2)}<small>laps</small>`
|
||||
: fmtSep(st.sep);
|
||||
const [r, tt0] = st.rel;
|
||||
const circ = 2 * Math.PI * D.a_ref_km * 1000;
|
||||
const tt = ((tt0 % circ) + circ * 1.5) % circ - circ / 2;
|
||||
let ctxLine;
|
||||
if (st.sep < 1500) ctxLine = `≈ ${Math.max(1, Math.round(st.sep / 100))} football fields`;
|
||||
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 · ` : ''}a quarter-orbit or more apart`;
|
||||
else ctxLine = `${Math.abs(r) > 20e3 ? `${fmtNum(Math.abs(r) / 1000)} km higher · ` : ''}${fmtNum(st.sep / 1000)} km apart in a straight line`;
|
||||
if (s > caS && s < span('leave')[0] + 0.5) ctxLine += ` · closest ${CA_M} m`;
|
||||
ro.querySelector('.ctx').textContent = ctxLine;
|
||||
ro.querySelector('.ctx').textContent = lapMode ? `${Math.abs(r) > 20e3 ? `${fmtNum(Math.abs(r) / 1000)} km higher · ` : ''}behind X36` : ctxLine;
|
||||
const relLine = ro.querySelector('.rel');
|
||||
relLine.textContent = st.sep < 2e4 && sh.mode === 'true' ? (st.relSpeed < 0.2 ? `relative speed ${Math.round(st.relSpeed * 100)} cm/s` : `relative speed ${st.relSpeed.toFixed(1)} m/s`) : '';
|
||||
relLine.textContent = st.sep < 2e4 && sh.mode === 'true' ? (st.relSpeed < 0.2 ? `relative speed ~${Math.max(1, Math.round(st.relSpeed * 100))} cm/s (modelled)` : `relative speed ~${st.relSpeed.toFixed(1)} m/s (modelled)`) : '';
|
||||
|
||||
// Scale chip
|
||||
const magEl = $('#mag');
|
||||
@@ -219,7 +234,13 @@ export function createHud(ctx) {
|
||||
const trueSize = !enlarged && st.camDistX < 200;
|
||||
magEl.style.opacity = (enlarged || trueSize) ? fade(s, climbS, END, 0.3) : 0;
|
||||
magEl.classList.toggle('enlarged', enlarged);
|
||||
magEl.textContent = enlarged ? `Satellites enlarged ${Math.round(st.mag)}×` : 'True size';
|
||||
magEl.textContent = enlarged ? `Satellites enlarged ${Math.round(st.mag)}×` : sh.id === 'ca' ? 'True size · faint bar: ±1.5 km tracking uncertainty' : 'True size';
|
||||
const relChip = $('#relchip');
|
||||
relChip.style.opacity = ['drop', 'close', 'dance', 'dance-ff', 'approach', 'leave'].includes(sh.id) ? fade(s, sh.s0, sh.s1, 0.3) : 0;
|
||||
|
||||
// Inset
|
||||
const insA = sh.inset && s < END - 0.1 ? fade(s, Math.max(sh.s0, climbS + 0.8), END, 0.3) : 0;
|
||||
inset.draw(s, t, (st.sepG || st.sep) / 1000, { alpha: insA * live, trailHours: sh.inset || 1, cut: CUTS.some((c) => Math.abs(s - c) < 0.05) });
|
||||
|
||||
// Timeline
|
||||
const px = timeline.x(Math.min(Math.max(t, TL.t0), TL.t1));
|
||||
@@ -244,16 +265,19 @@ export function createHud(ctx) {
|
||||
const pC = project(st.cosW);
|
||||
const tX = tag('tagX', 'ICEYE-X36', 'c-x36');
|
||||
const tC = tag('tagC', 'COSMOS 2612', 'c-cos');
|
||||
const tagA = fade(s, span('climb')[0] + 0.6, END, 0.4);
|
||||
const tagA = fade(s, 0.3, END, 0.3);
|
||||
const onScreen = (p) => p && p.x > -40 && p.y > -40 && p.x < W + 40 && p.y < H + 40;
|
||||
const speedX = `${(st.vX / 1000).toFixed(2)} km/s`;
|
||||
const speedC = `${(st.vC / 1000).toFixed(2)} km/s`;
|
||||
tX.querySelector('.meta').textContent = pX?.hidden ? 'far side of Earth' : `U.S.-registered radar sat · ${speedX}`;
|
||||
tC.querySelector('.meta').textContent = pC?.hidden ? 'far side of Earth' : `Russian military · ${speedC}`;
|
||||
const speedShot = ['climb', 'lap', 'drop'].includes(sh.id);
|
||||
tX.querySelector('.meta').textContent = speedShot ? `U.S. radar satellite · ${speedX}` : sh.id === 'hook' ? 'U.S. radar satellite' : '';
|
||||
tC.querySelector('.meta').textContent = speedShot ? `Russian military · ${speedC}` : sh.id === 'hook' ? 'Russian military' : '';
|
||||
tX.querySelector('.meta').style.display = tX.querySelector('.meta').textContent ? '' : 'none';
|
||||
tC.querySelector('.meta').style.display = tC.querySelector('.meta').textContent ? '' : 'none';
|
||||
let svg = '';
|
||||
const M = 24 * U;
|
||||
const obstacles = [];
|
||||
for (const q of ['#logo', '#clock', '#caption', '#readout', '#tlwrap', '#chapters', '#mag']) {
|
||||
for (const q of ['#logo', '#clock', '#caption', '#readout', '#tlwrap', '#chapters', '#mag', '#inset', '#relchip']) {
|
||||
const el = $(q);
|
||||
if (Number(el.style.opacity || 1) > 0.05) obstacles.push(el.getBoundingClientRect());
|
||||
}
|
||||
@@ -290,9 +314,8 @@ export function createHud(ctx) {
|
||||
// Tags: keep the previous quadrant unless it has been blocked for a while;
|
||||
// ease the label toward its target so it never jumps.
|
||||
const place = (key, el, p, prefer, color) => {
|
||||
if (!onScreen(p)) {
|
||||
if (!onScreen(p) || p.hidden) {
|
||||
el.style.opacity = 0;
|
||||
memo[key] = null;
|
||||
return;
|
||||
}
|
||||
el.style.opacity = tagA;
|
||||
@@ -310,9 +333,9 @@ export function createHud(ctx) {
|
||||
let q = m ? [m.sx, m.sy] : prefer;
|
||||
let cand = rectFor(q);
|
||||
if (m) {
|
||||
m.blocked = cand.cost > 2000 * U * U ? m.blocked + 1 : 0;
|
||||
m.blocked = cand.cost > 4000 * U * U ? m.blocked + 1 : 0;
|
||||
}
|
||||
if (!m || m.blocked > 8) {
|
||||
if (!m || m.blocked > 20) {
|
||||
const quads = [q, prefer, [prefer[0], -prefer[1]], [-prefer[0], prefer[1]], [-prefer[0], -prefer[1]]];
|
||||
for (const qq of quads) {
|
||||
const c = rectFor(qq);
|
||||
@@ -353,7 +376,7 @@ export function createHud(ctx) {
|
||||
|
||||
// Dip at cuts
|
||||
let dip = 0;
|
||||
for (const c of CUTS) dip = Math.max(dip, 1 - Math.abs(s - c) / 0.12);
|
||||
|
||||
$('#dip').style.opacity = Math.max(0, dip);
|
||||
|
||||
// End card
|
||||
@@ -364,13 +387,13 @@ export function createHud(ctx) {
|
||||
ec.dataset.built = '1';
|
||||
ec.innerHTML = `<div class="panel">
|
||||
<div class="big">~${CA_M} m</div>
|
||||
<div class="range">rebuilt from public element sets · LeoLabs ~500 m · 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="range">closest pass, Oct 18, in Earth's shadow · LeoLabs ~500 m · estimates 150–650 m</div>
|
||||
<div class="stats">
|
||||
<div><b>22</b><span>days in pursuit</span></div>
|
||||
<div><b>${BURNS.length}</b><span>engine burns</span></div>
|
||||
<div><b>${Math.max(1, Math.round((D.ca.v || 0.05) * 100))} cm/s</b><span>relative speed at closest pass</span></div>
|
||||
<div><b>18</b><span>days from first burn to closest pass</span></div>
|
||||
<div><b>${BURNS.length}</b><span>manoeuvres</span></div>
|
||||
<div><b>~${Math.round(D.total_dv)} m/s</b><span>of thrust spent</span></div>
|
||||
</div>
|
||||
<div class="why">U.S. officials called it <b>"consistent with on-orbit weapons operations."</b><br>By Oct 25 it was still parked ~480 km behind, in the same orbit.</div>
|
||||
</div>`;
|
||||
}
|
||||
$('#endlogo').style.opacity = endA;
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
// "Seen from ICEYE-X36": a 2D inset in X36's rotating frame. X36 stays fixed
|
||||
// at the top of its orbit ring; Cosmos 2612's path is drawn around it.
|
||||
// Zoomed out it is the whole orbit (heights exaggerated, so "higher = slower,
|
||||
// falls behind, laps" is visible); zoomed in it becomes a true-scale
|
||||
// radial / along-track plot of the loops and the approach.
|
||||
|
||||
export function createInset({ D, U, relAt, canvas }) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const A = D.a_km; // km
|
||||
const AREF = D.a_ref_km;
|
||||
const RE = 6371;
|
||||
const state = { hw: null, lastS: -1 };
|
||||
const smooth = (x) => {
|
||||
const t = Math.min(1, Math.max(0, x));
|
||||
return t * t * (3 - 2 * t);
|
||||
};
|
||||
|
||||
function niceScale(km) {
|
||||
const p = 10 ** Math.floor(Math.log10(km));
|
||||
for (const m of [1, 2, 5, 10]) if (m * p >= km) return m * p;
|
||||
return 10 * p;
|
||||
}
|
||||
const fmt = (km) => (km >= 1 ? `${km.toLocaleString('en-US')} km` : `${Math.round(km * 1000)} m`);
|
||||
|
||||
function draw(s, t, sepGuideKm, opts) {
|
||||
const W = canvas.width;
|
||||
const H = canvas.height;
|
||||
const dpr = W / canvas.clientWidth || 1;
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
if (opts.alpha <= 0.01) return;
|
||||
ctx.globalAlpha = opts.alpha;
|
||||
|
||||
// Zoom: half-width (km) follows the guiding separation; eased frame to frame.
|
||||
const target = Math.min(9200, Math.max(1.4, 1.6 * sepGuideKm));
|
||||
if (state.hw === null || Math.abs(s - state.lastS) > 0.2 || opts.cut) state.hw = target;
|
||||
else state.hw = Math.exp(Math.log(state.hw) + (Math.log(target) - Math.log(state.hw)) * 0.12);
|
||||
state.lastS = s;
|
||||
const hw = state.hw;
|
||||
const wOut = smooth((hw - 150) / 2500); // 0 = close-up, 1 = whole-orbit view
|
||||
const exag = 1 + 19 * wOut; // radial exaggeration when zoomed out
|
||||
const wCenter = smooth((hw - 2500) / 5500);
|
||||
|
||||
// Panel
|
||||
const pad = 18 * U * dpr;
|
||||
const r = 14 * U * dpr;
|
||||
ctx.fillStyle = 'rgba(5,9,16,0.78)';
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.14)';
|
||||
ctx.lineWidth = 1.5 * dpr;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, 0, W, H, r);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
const head = 44 * U * dpr;
|
||||
ctx.fillStyle = 'rgba(244,247,250,0.92)';
|
||||
ctx.font = `800 ${22 * U * dpr}px Inter, sans-serif`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('SEEN FROM X36', pad, head / 2 + 4 * dpr);
|
||||
ctx.fillStyle = 'rgba(244,247,250,0.6)';
|
||||
ctx.font = `600 ${20 * U * dpr}px Inter, sans-serif`;
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText('flight →', W - pad, head / 2 + 4 * dpr);
|
||||
ctx.textAlign = 'left';
|
||||
|
||||
// Plot area
|
||||
const px0 = pad;
|
||||
const py0 = head;
|
||||
const pw = W - 2 * pad;
|
||||
const ph = H - head - pad;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(px0, py0, pw, ph);
|
||||
ctx.clip();
|
||||
const scale = pw / 2 / hw; // px per km
|
||||
// Centre: X36 when close; drifts toward Earth's centre when zoomed out.
|
||||
const cx = 0;
|
||||
const cy = A * (1 - wCenter) + 0 * wCenter;
|
||||
const ox = px0 + pw / 2;
|
||||
const oy = py0 + ph * (0.5 + 0.12 * (1 - wCenter));
|
||||
const P = (x, y) => [ox + (x - cx) * scale, oy - (y - cy) * scale];
|
||||
const ring = (rad) => {
|
||||
// Polyline of the visible part of a circle about Earth's centre.
|
||||
const pts = [];
|
||||
const span = Math.min(Math.PI, (hw * 1.6) / rad + 0.05);
|
||||
const n = 180;
|
||||
for (let k = 0; k <= n; k++) {
|
||||
const a = -span + (2 * span * k) / n;
|
||||
pts.push(P(rad * Math.sin(a), rad * Math.cos(a)));
|
||||
}
|
||||
return pts;
|
||||
};
|
||||
const path = (pts) => {
|
||||
ctx.beginPath();
|
||||
pts.forEach(([x, y], i) => (i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)));
|
||||
};
|
||||
|
||||
// Earth (exaggeration shrinks it so the altitude gap reads)
|
||||
const earthR = Math.max(A * 0.5, A - (A - RE) * exag); // schematic when heights are exaggerated
|
||||
if (earthR > 0) {
|
||||
const [ex, ey] = P(0, 0);
|
||||
const g = ctx.createRadialGradient(ex, ey - earthR * scale * 0.3, earthR * scale * 0.2, ex, ey, earthR * scale);
|
||||
g.addColorStop(0, '#1d3a5c');
|
||||
g.addColorStop(1, '#0b1a2c');
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath();
|
||||
ctx.arc(ex, ey, Math.max(0, earthR * scale), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(120,190,255,0.5)';
|
||||
ctx.lineWidth = 2 * dpr;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// X36's orbit
|
||||
ctx.setLineDash([8 * dpr, 7 * dpr]);
|
||||
ctx.strokeStyle = 'rgba(92,225,230,0.55)';
|
||||
ctx.lineWidth = 2 * dpr;
|
||||
path(ring(A));
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Cosmos path (relative), with a fading tail
|
||||
const toXY = (rel) => {
|
||||
const phi = rel[1] / 1000 / AREF;
|
||||
const rad = A + (rel[0] / 1000) * exag;
|
||||
return [rad * Math.sin(phi), rad * Math.cos(phi)];
|
||||
};
|
||||
const spanH = opts.trailHours;
|
||||
const n = 260;
|
||||
let prev = null;
|
||||
for (let k = 0; k <= n; k++) {
|
||||
const tt = t - spanH * 3600 + (k / n) * spanH * 3600;
|
||||
if (tt < 0) continue;
|
||||
const [x, y] = P(...toXY(relAt(tt)));
|
||||
if (prev) {
|
||||
ctx.strokeStyle = `rgba(255,90,60,${0.08 + 0.85 * (k / n) ** 1.5})`;
|
||||
ctx.lineWidth = (1.5 + 2.5 * (k / n)) * dpr;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(prev[0], prev[1]);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
prev = [x, y];
|
||||
}
|
||||
const rel = relAt(t);
|
||||
const [cxp, cyp] = P(...toXY(rel));
|
||||
const [xxp, xyp] = P(0, A);
|
||||
// Gap line
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
|
||||
ctx.setLineDash([5 * dpr, 6 * dpr]);
|
||||
ctx.lineWidth = 1.5 * dpr;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xxp, xyp);
|
||||
ctx.lineTo(cxp, cyp);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
const dot = (x, y, col, rad) => {
|
||||
ctx.fillStyle = col;
|
||||
ctx.shadowColor = col;
|
||||
ctx.shadowBlur = 14 * dpr;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, rad * dpr, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
};
|
||||
dot(xxp, xyp, '#5ce1e6', 7 * U);
|
||||
dot(cxp, cyp, '#ff5a3c', 7 * U);
|
||||
ctx.font = `800 ${20 * U * dpr}px Inter, sans-serif`;
|
||||
ctx.fillStyle = '#5ce1e6';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('X36', xxp, xyp - 20 * U * dpr);
|
||||
ctx.fillStyle = '#ff5a3c';
|
||||
const below = cyp > xyp + 10 * dpr || Math.abs(cxp - xxp) < 40 * U * dpr;
|
||||
ctx.fillText('COSMOS', Math.min(Math.max(cxp, px0 + 50 * U * dpr), px0 + pw - 50 * U * dpr), cyp + (below ? 24 : -20) * U * dpr);
|
||||
ctx.textAlign = 'left';
|
||||
ctx.restore();
|
||||
|
||||
// Scale bar + exaggeration note
|
||||
const sb = niceScale(hw * 0.4);
|
||||
const sbpx = sb * scale;
|
||||
const by = H - pad * 0.55;
|
||||
ctx.strokeStyle = 'rgba(244,247,250,0.8)';
|
||||
ctx.lineWidth = 2.5 * dpr;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pad, by);
|
||||
ctx.lineTo(pad + sbpx, by);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = 'rgba(244,247,250,0.85)';
|
||||
ctx.font = `700 ${19 * U * dpr}px 'JetBrains Mono', monospace`;
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.fillText(fmt(sb), pad, by - 6 * dpr);
|
||||
if (exag > 1.5) {
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillStyle = 'rgba(255,181,71,0.95)';
|
||||
ctx.font = `700 ${19 * U * dpr}px Inter, sans-serif`;
|
||||
ctx.fillText(`heights ×${Math.round(exag)}`, W - pad, by - 6 * dpr);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
return { draw };
|
||||
}
|
||||
+24
-13
@@ -46,7 +46,7 @@
|
||||
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)); white-space: nowrap; font: 700 calc(24 * var(--u))/1 Inter, sans-serif;
|
||||
.ch span { display: block; margin-top: calc(10 * var(--u)); white-space: nowrap; font: 800 calc(26 * var(--u))/1 Inter, sans-serif;
|
||||
letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); }
|
||||
.ch.on span { color: var(--ink); }
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
#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(196 * var(--u)); padding: calc(7 * var(--u)) calc(14 * var(--u)); border-radius: calc(6 * var(--u));
|
||||
#mag { position: absolute; left: calc(56 * var(--u) + 536 * var(--u)); bottom: calc(246 * 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); }
|
||||
|
||||
@@ -105,11 +105,16 @@
|
||||
#clock .light.day { color: #ffe08a; } #clock .light.day i { background: #ffd34d; box-shadow: 0 0 calc(14 * var(--u)) #ffd34d; }
|
||||
#clock .light.night { color: #b9c8ff; } #clock .light.night i { background: #20263a; border: calc(3 * var(--u)) solid #b9c8ff; }
|
||||
#readout .rel { margin-top: calc(6 * var(--u)); font: 600 calc(28 * var(--u))/1.2 Inter, sans-serif; color: var(--ink-2); }
|
||||
/* Inset + chips */
|
||||
#inset { position: absolute; left: var(--pad); bottom: calc(196 * var(--u)); width: calc(520 * var(--u)); height: calc(330 * var(--u)); }
|
||||
#relchip { position: absolute; left: calc(56 * var(--u) + 536 * var(--u)); bottom: calc(196 * var(--u)); padding: calc(7 * var(--u)) calc(14 * var(--u));
|
||||
border-radius: calc(6 * var(--u)); font: 800 calc(26 * var(--u))/1 Inter, sans-serif; color: #fff; background: rgba(255,90,60,0.85); opacity: 0; }
|
||||
.event { background: rgba(4,8,14,0.62); padding: calc(10 * var(--u)) calc(16 * var(--u)) calc(10 * var(--u)) calc(14 * var(--u)); border-radius: calc(8 * var(--u)); width: fit-content; }
|
||||
/* Timeline */
|
||||
#tlwrap { position: absolute; left: var(--pad); right: var(--pad); bottom: calc(52 * var(--u)); }
|
||||
#tlwrap .lab { font: 700 calc(24 * var(--u))/1 Inter, sans-serif; letter-spacing: 0.1em; color: var(--ink-3); margin-bottom: calc(8 * var(--u)); }
|
||||
#tlwrap .lab span { color: var(--burn); } #tlwrap .lab b { color: var(--ink-2); font-weight: 700; }
|
||||
#timeline { height: calc(76 * var(--u)); }
|
||||
#timeline { height: calc(70 * var(--u)); }
|
||||
#timeline svg { width: 100%; height: 100%; overflow: visible; font-family: 'JetBrains Mono', monospace; font-weight: 600; }
|
||||
#interstitial { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0;
|
||||
background: radial-gradient(ellipse at center, rgba(0,0,0,0.55), rgba(0,0,0,0.15) 70%); }
|
||||
@@ -159,15 +164,19 @@
|
||||
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(700 * var(--u)); width: auto; right: var(--pad); }
|
||||
body.portrait #readout { right: auto; left: var(--pad); bottom: calc(580 * 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(590 * var(--u)); }
|
||||
body.portrait #tlwrap { bottom: calc(440 * var(--u)); }
|
||||
body.portrait #timeline { height: calc(80 * var(--u)); }
|
||||
body.portrait #readout { right: auto; left: var(--pad); bottom: calc(420 * var(--u)); text-align: left; }
|
||||
body.portrait #readout .val { font-size: calc(104 * var(--u)); }
|
||||
body.portrait #mag { left: auto; right: var(--pad); bottom: calc(912 * var(--u)); }
|
||||
body.portrait #tlwrap { display: none; }
|
||||
body.portrait #inset { left: auto; right: var(--pad); bottom: calc(420 * var(--u)); width: calc(560 * var(--u)); height: calc(430 * var(--u)); }
|
||||
body.portrait #relchip { left: auto; right: var(--pad); bottom: calc(866 * var(--u)); }
|
||||
body.portrait #readout { max-width: calc(400 * var(--u)); }
|
||||
body.portrait #readout .light { margin: 0 0 calc(18 * var(--u)); display: inline-flex; }
|
||||
body.portrait .ch span { font-size: calc(24 * var(--u)); }
|
||||
body.portrait #interstitial .d { font-size: calc(120 * 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 #credits { white-space: normal; bottom: calc(330 * var(--u)); font-size: calc(17 * var(--u)); }
|
||||
body.portrait #endcard .panel { width: calc(960 * var(--u)); }
|
||||
body.portrait #endcard .big { font-size: calc(210 * var(--u)); }
|
||||
body.portrait #endcard .lead { font-size: calc(46 * var(--u)); }
|
||||
@@ -188,14 +197,16 @@
|
||||
<div id="chapters" class="shadow-text"></div>
|
||||
<div id="caption" class="shadow-text"><h1></h1><p></p></div>
|
||||
<div id="events" class="shadow-text"></div>
|
||||
<div id="clock" class="shadow-text"><div class="date"></div><div class="time"></div><div class="pills"><div class="ff">Fast-forward</div><div class="rate"></div></div><div class="light"></div></div>
|
||||
<div id="clock" class="shadow-text"><div class="date"></div><div class="time"></div><div class="pills"><div class="ff">Fast-forward · Earth slowed</div><div class="rate"></div></div><div class="light"></div></div>
|
||||
<div id="readout" class="shadow-text"><div class="label">DISTANCE APART</div><div class="val"></div><div class="ctx"></div><div class="rel"></div></div>
|
||||
<div id="mag" class="shadow-text"></div>
|
||||
<div id="tlwrap"><div class="lab">22 DAYS · <span>● ENGINE BURN</span> · <b>○ CLOSEST PASS</b></div><div id="timeline"></div></div>
|
||||
<div id="interstitial"><div class="k">22 days earlier</div><div class="d">SEP 30, 2026</div></div>
|
||||
<div id="tlwrap"><div id="timeline"></div></div>
|
||||
<canvas id="inset"></canvas>
|
||||
<div id="relchip">Red path: Cosmos as seen from X36</div>
|
||||
<div id="interstitial"><div class="k">18 days earlier</div><div class="d">SEP 30, 2026</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, 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="credits">Reconstruction from public Space-Track element sets (41 Cosmos 2612, 54 ICEYE-X36), not tracking data · Analyses: LeoLabs, KeepTrack, M. Langbroek, COMSPOC, Integrity ISR · 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>
|
||||
|
||||
@@ -195,6 +195,7 @@ function camAt(s) {
|
||||
sun: lerp(a.sun || 0, b.sun || 0),
|
||||
vup: lerp(a.vup || 0, b.vup || 0),
|
||||
comp: lerp(a.comp ?? 1, b.comp ?? 1),
|
||||
compY: lerp(a.compY ?? 0, b.compY ?? 0),
|
||||
pairD: lerp(a.pairD || 0, b.pairD || 0),
|
||||
pairTh: lerp(a.pairTh || 0, b.pairTh || 0),
|
||||
aimRel: [0, 1, 2].map((k) => lerp(ar[k], br[k])),
|
||||
@@ -356,7 +357,7 @@ function project(world) {
|
||||
return p;
|
||||
}
|
||||
const hud = createHud({
|
||||
D, W, H, U, PORTRAIT, sec, rate, sOfTau, sepAt, CA_T, VANTOR_T, BURNS, CUTS, SHOTS, shotAt, DUR, project,
|
||||
D, W, H, U, PORTRAIT, sec, rate, sOfTau, sepAt, CA_T, VANTOR_T, BURNS, CUTS, SHOTS, shotAt, DUR, project, relAt,
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -468,7 +469,7 @@ function setState(s) {
|
||||
// (sideways, in the orbit's horizontal), aimed at the bisector of the two.
|
||||
const u = v3.normalize(v3.subtract(cosW, x36W, new v3()), new v3());
|
||||
const perp = v3.normalize(v3.subtract(NW, v3.multiplyByScalar(u, v3.dot(NW, u), new v3()), new v3()), new v3());
|
||||
const th = Cesium.Math.toRadians(cam.pairTh);
|
||||
const th = Cesium.Math.toRadians(cam.pairTh * (PORTRAIT ? 1.3 : 1));
|
||||
v3.add(x36W, v3.multiplyByScalar(u, -cam.pairD * Math.cos(th), new v3()), camPos);
|
||||
v3.add(camPos, v3.multiplyByScalar(perp, cam.pairD * Math.sin(th), new v3()), camPos);
|
||||
const a1 = v3.normalize(v3.subtract(x36W, camPos, new v3()), new v3());
|
||||
@@ -481,7 +482,11 @@ function setState(s) {
|
||||
// Up: local zenith; in portrait chase shots, the direction of flight (so the
|
||||
// pair travels up the tall frame and Earth's limb runs down one side).
|
||||
const vup = PORTRAIT ? cam.vup : 0;
|
||||
const upHint = v3.normalize(v3.lerp(RW, TW, vup, new v3()), new v3());
|
||||
let upHint = v3.normalize(v3.lerp(RW, TW, vup, new v3()), new v3());
|
||||
if (PORTRAIT && cam.pairD > 0) {
|
||||
// Portrait true-size shot: run the pair up the tall frame.
|
||||
upHint = v3.normalize(v3.subtract(cosW, x36W, new v3()), new v3());
|
||||
}
|
||||
let right = v3.normalize(v3.cross(dir, upHint, new v3()), new v3());
|
||||
let up = v3.cross(right, dir, new v3());
|
||||
{
|
||||
@@ -491,13 +496,14 @@ function setState(s) {
|
||||
const tanH = PORTRAIT ? Math.tan(fovR / 2) * aspect : Math.tan(fovR / 2);
|
||||
const tanV = PORTRAIT ? Math.tan(fovR / 2) : Math.tan(fovR / 2) / aspect;
|
||||
const ndcX = (PORTRAIT ? 0 : 0.18) * cam.comp;
|
||||
const ndcY = (PORTRAIT ? -0.04 : 0.06) * cam.comp;
|
||||
const ndcY = (PORTRAIT ? -0.04 : 0.06) * cam.comp + cam.compY;
|
||||
v3.normalize(v3.add(v3.add(dir, v3.multiplyByScalar(right, -ndcX * tanH, new v3()), new v3()),
|
||||
v3.multiplyByScalar(up, -ndcY * tanV, new v3()), new v3()), dir);
|
||||
right = v3.normalize(v3.cross(dir, upHint, new v3()), new v3());
|
||||
up = v3.cross(right, dir, new v3());
|
||||
}
|
||||
scene.camera.setView({ destination: camPos, orientation: { direction: dir, up } });
|
||||
scene.skyBox.show = !sh.noSky;
|
||||
scene.camera.frustum.fov = Cesium.Math.toRadians(PORTRAIT ? cam.fov * 1.55 : cam.fov);
|
||||
{
|
||||
const icrf = Cesium.Transforms.computeIcrfToFixedMatrix(jd) || M;
|
||||
@@ -535,8 +541,8 @@ function setState(s) {
|
||||
bCos.position = cosW;
|
||||
bX36.scale = (pxX > 24 ? 0.16 : 0.5) * U;
|
||||
bCos.scale = (pxC > 24 ? 0.16 : 0.5) * U;
|
||||
bX36.color = Cesium.Color.WHITE.withAlpha(pxX > 60 ? 0.35 : 1);
|
||||
bCos.color = Cesium.Color.WHITE.withAlpha(pxC > 60 ? 0.35 : 1);
|
||||
bX36.color = Cesium.Color.WHITE.withAlpha(pxX > 60 ? 0.6 : 1);
|
||||
bCos.color = Cesium.Color.WHITE.withAlpha(pxC > 60 ? 0.6 : 1);
|
||||
|
||||
// Burn flare
|
||||
let flare = 0;
|
||||
@@ -602,6 +608,12 @@ function setState(s) {
|
||||
}
|
||||
polyline(pts, cols, 1.4);
|
||||
}
|
||||
// Element-set uncertainty at closest approach: ±1.5 km along-track.
|
||||
if (sh.id === 'ca') {
|
||||
const a0 = v3.add(cosW, v3.multiplyByScalar(TW, -1500, new v3()), new v3());
|
||||
const a1 = v3.add(cosW, v3.multiplyByScalar(TW, 1500, new v3()), new v3());
|
||||
polyline([a0, cosW, a1], [COL_COS.withAlpha(0), COL_COS.withAlpha(0.45), COL_COS.withAlpha(0)], 10);
|
||||
}
|
||||
// RIC reference rings (orbital plane) during proximity
|
||||
const gridA = Math.min(1, Math.max(0, (8e3 - Math.max(sep, camDistX)) / 3e3)) * (cam.mag > 0.4 ? 1 : 0.35);
|
||||
if (gridA > 0.01) {
|
||||
@@ -628,7 +640,7 @@ function setState(s) {
|
||||
const r0 = relAt(t - 30);
|
||||
const relSpeed = Math.hypot(r1[0] - r0[0], r1[1] - r0[1], r1[2] - r0[2]) / 60;
|
||||
|
||||
return { sh, t, b, jd, x36W, cosW, RW, TW, NW, rel, sep, mag, gridA, lx, camDistX, vX: vXv, vC, relSpeed };
|
||||
return { sh, t, b, jd, x36W, cosW, RW, TW, NW, rel, sep, sepG, mag, gridA, lx, camDistX, vX: vXv, vC, relSpeed };
|
||||
}
|
||||
|
||||
async function frame(s) {
|
||||
|
||||
+61
-59
@@ -1,4 +1,4 @@
|
||||
// v4 storyboard: a list of shots separated by hard cuts. Each shot owns its
|
||||
// Storyboard (v5): a list of shots separated by hard cuts. Each shot owns its
|
||||
// clock. `true` shots run Earth, sun, both satellites and the camera on one
|
||||
// physical clock (a real time-lapse: day and night cycle, the ground rushes
|
||||
// by). `ff` (fast-forward) shots are too fast for that to read, so the Earth
|
||||
@@ -6,118 +6,120 @@
|
||||
// relative motion runs at the stated rate; they carry a FAST-FORWARD badge.
|
||||
//
|
||||
// Camera keys use X36's local frame (R up, T velocity, N orbit normal):
|
||||
// dist (m) or f × separation; az 0 = behind X36 looking forward; el up;
|
||||
// look 0 = X36 … 1 = Cosmos; earth pulls the aim toward Earth's centre;
|
||||
// frame 'inertial' freezes the basis at the shot start (wide shots);
|
||||
// vup (portrait) rolls the camera so the direction of flight points up;
|
||||
// rtrail = Cosmos relative-path trail (hours); atrail = orbit trails (min);
|
||||
// mag/mx = fixed model enlargement; pairD/pairTh = pair-locked true-size cam.
|
||||
// dist (m) or f × guiding separation; az 0 = behind X36 looking forward;
|
||||
// el up; look 0 = X36 … 1 = Cosmos's guiding centre; earth pulls the aim
|
||||
// toward Earth's centre; frame 'inertial' freezes the basis at the shot
|
||||
// start (wide shots); vup (portrait) rolls the camera so the direction of
|
||||
// flight points up; rtrail = Cosmos relative-path trail (hours);
|
||||
// atrail = orbit trails (min); mag/mx = fixed model enlargement;
|
||||
// pairD/pairTh = pair-locked true-size camera; inset = hours of path shown
|
||||
// in the "seen from X36" inset.
|
||||
|
||||
export function buildStory({ sec, D }) {
|
||||
const CA = sec(D.ca.t);
|
||||
const M = 60;
|
||||
const iso = (s) => sec(s);
|
||||
const WIDE = { frame: 'inertial', look: 0, earth: 0.86, f: 0, fov: 40, mag: 0, sun: 0.5 };
|
||||
const WIDE = { frame: 'inertial', look: 0, earth: 0.86, f: 0, fov: 40, mag: 0, sun: 0.35 };
|
||||
|
||||
const shots = [
|
||||
{
|
||||
id: 'hook', mode: 'true', s0: 0, s1: 3.6, blur: 2,
|
||||
tau: [[0, CA - 7 * M], [3.6, CA - 2 * M]],
|
||||
id: 'hook', mode: 'true', s0: 0, s1: 3.6, blur: 1, inset: 1,
|
||||
tau: [[0, CA - 13 * M], [3.6, CA - 6 * M]],
|
||||
cam: [
|
||||
{ s: 0, dist: 1500, f: 2.6, az: 118, el: 18, look: 0.5, fov: 34, mag: 1, mx: 25, rtrail: 1.2, atrail: 4, vup: 1 },
|
||||
{ s: 3.6, dist: 1300, f: 2.4, az: 104, el: 14, look: 0.5, fov: 34, mag: 1, mx: 25, rtrail: 1.2, atrail: 4, vup: 1 },
|
||||
{ s: 0, dist: 1300, f: 2.4, az: 120, el: 20, look: 0.5, fov: 34, mag: 1, mx: 25, rtrail: 1.0, atrail: 2, vup: 1, compY: -0.16 },
|
||||
{ s: 3.6, dist: 1100, f: 2.2, az: 108, el: 16, look: 0.5, fov: 34, mag: 1, mx: 25, rtrail: 1.0, atrail: 2, vup: 1, compY: -0.16 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'climb', mode: 'true', s0: 3.6, s1: 10.6, blur: 3,
|
||||
tau: [[3.6, iso('2026-09-30T07:20:00Z')], [10.6, iso('2026-09-30T12:40:00Z')]],
|
||||
id: 'climb', mode: 'true', s0: 3.6, s1: 9.6, blur: 1, inset: 6,
|
||||
tau: [[3.6, iso('2026-09-30T07:30:00Z')], [9.6, iso('2026-09-30T12:30:00Z')]],
|
||||
cam: [
|
||||
{ s: 3.6, ...WIDE, dist: 3.1e7, az: 86, el: 38, atrail: 70, rtrail: 0 },
|
||||
{ s: 10.6, ...WIDE, dist: 2.6e7, az: 104, el: 34, atrail: 70, rtrail: 0 },
|
||||
{ s: 3.6, ...WIDE, dist: 3.3e7, az: 92, el: 28, atrail: 80, rtrail: 0 },
|
||||
{ s: 9.6, ...WIDE, dist: 2.9e7, az: 100, el: 24, atrail: 80, rtrail: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'lap', mode: 'ff', s0: 10.6, s1: 14.6, blur: 1, bgRate: 260,
|
||||
tau: [[10.6, iso('2026-09-30T13:00:00Z')], [14.6, iso('2026-10-11T23:40:00Z')]],
|
||||
id: 'lap', mode: 'ff', s0: 9.6, s1: 13.6, blur: 1, bgRate: 260, inset: 30,
|
||||
tau: [[9.6, iso('2026-09-30T13:00:00Z')], [13.6, iso('2026-10-11T23:40:00Z')]],
|
||||
cam: [
|
||||
{ s: 10.6, ...WIDE, dist: 2.7e7, az: 100, el: 36, atrail: 0, rtrail: 30 },
|
||||
{ s: 14.6, ...WIDE, dist: 2.3e7, az: 124, el: 30, atrail: 0, rtrail: 30 },
|
||||
{ s: 9.6, ...WIDE, dist: 2.9e7, az: 100, el: 30, atrail: 0, rtrail: 30 },
|
||||
{ s: 13.6, ...WIDE, dist: 2.5e7, az: 116, el: 28, atrail: 0, rtrail: 30 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'drop', mode: 'true', s0: 14.6, s1: 19.6, blur: 4,
|
||||
tau: [[14.6, iso('2026-10-12T00:00:00Z')], [19.6, iso('2026-10-12T03:30:00Z')]],
|
||||
id: 'drop', mode: 'true', s0: 13.6, s1: 18.6, blur: 3, inset: 4,
|
||||
tau: [[13.6, iso('2026-10-12T00:00:00Z')], [18.6, iso('2026-10-12T03:30:00Z')]],
|
||||
cam: [
|
||||
{ s: 14.6, dist: 2.4e5, f: 2.6, az: 62, el: 28, look: 0.5, fov: 36, mag: 0, rtrail: 3, atrail: 20, vup: 1 },
|
||||
{ s: 17.0, dist: 3.0e5, f: 2.6, az: 48, el: 30, look: 0.5, fov: 36, mag: 0, rtrail: 4, atrail: 20, vup: 1 },
|
||||
{ s: 19.6, dist: 4.0e5, f: 2.6, az: 34, el: 32, look: 0.5, fov: 36, mag: 0, rtrail: 4, atrail: 20, vup: 1 },
|
||||
{ s: 13.6, dist: 2.4e5, f: 2.6, az: 62, el: 28, look: 0.5, fov: 36, mag: 0, rtrail: 3, atrail: 20, vup: 1 },
|
||||
{ s: 16.0, dist: 3.0e5, f: 2.6, az: 48, el: 30, look: 0.5, fov: 36, mag: 0, rtrail: 4, atrail: 20, vup: 1 },
|
||||
{ s: 18.6, dist: 4.0e5, f: 2.6, az: 34, el: 32, look: 0.5, fov: 36, mag: 0, rtrail: 4, atrail: 20, vup: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'close', mode: 'ff', s0: 19.6, s1: 23.2, blur: 1, bgRate: 140,
|
||||
tau: [[19.6, iso('2026-10-12T03:40:00Z')], [21.4, iso('2026-10-15T06:00:00Z')], [23.2, iso('2026-10-16T07:00:00Z')]],
|
||||
id: 'close', mode: 'ff', s0: 18.6, s1: 22.0, blur: 1, bgRate: 140, inset: 30,
|
||||
tau: [[18.6, iso('2026-10-12T03:40:00Z')], [20.3, iso('2026-10-15T06:00:00Z')], [22.0, iso('2026-10-16T07:00:00Z')]],
|
||||
cam: [
|
||||
{ s: 19.6, dist: 1.0e6, f: 2.8, az: 30, el: 34, look: 0.5, fov: 34, mag: 0, rtrail: 30, atrail: 0, vup: 1 },
|
||||
{ s: 23.2, dist: 2.0e4, f: 2.8, az: 46, el: 34, look: 0.5, fov: 34, mag: 0, rtrail: 30, atrail: 0, vup: 1 },
|
||||
{ s: 18.6, dist: 1.0e6, f: 2.8, az: 30, el: 34, look: 0.5, fov: 34, mag: 0, rtrail: 30, atrail: 0, vup: 1 },
|
||||
{ s: 22.0, dist: 2.0e4, f: 2.8, az: 46, el: 34, look: 0.5, fov: 34, mag: 0, rtrail: 30, atrail: 0, vup: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dance', mode: 'true', s0: 23.2, s1: 29.2, blur: 5,
|
||||
tau: [[23.2, iso('2026-10-16T07:10:00Z')], [29.2, iso('2026-10-16T12:10:00Z')]],
|
||||
id: 'dance', mode: 'true', s0: 22.0, s1: 28.0, blur: 5, inset: 3.2,
|
||||
tau: [[22.0, iso('2026-10-16T07:10:00Z')], [28.0, iso('2026-10-16T12:10:00Z')]],
|
||||
cam: [
|
||||
{ s: 23.2, dist: 5.5e3, f: 1.7, az: 62, el: 30, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 3, atrail: 1.2, vup: 1 },
|
||||
{ s: 29.2, dist: 5.0e3, f: 1.7, az: 84, el: 24, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 3, atrail: 1.2, vup: 1 },
|
||||
{ s: 22.0, dist: 5.5e3, f: 1.7, az: 62, el: 30, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 3, atrail: 1.2, vup: 1 },
|
||||
{ s: 28.0, dist: 5.0e3, f: 1.7, az: 80, el: 26, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 3, atrail: 1.2, vup: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dance-ff', mode: 'ff', s0: 29.2, s1: 31.2, blur: 1, bgRate: 140,
|
||||
tau: [[29.2, iso('2026-10-16T12:20:00Z')], [31.2, iso('2026-10-17T22:20:00Z')]],
|
||||
id: 'dance-ff', mode: 'ff', s0: 28.0, s1: 30.0, blur: 1, bgRate: 140, inset: 10,
|
||||
tau: [[28.0, iso('2026-10-16T12:20:00Z')], [30.0, iso('2026-10-17T22:20:00Z')]],
|
||||
cam: [
|
||||
{ s: 29.2, dist: 4.5e3, f: 1.7, az: 70, el: 40, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 12, atrail: 0, vup: 1 },
|
||||
{ s: 31.2, dist: 3.4e3, f: 1.7, az: 92, el: 34, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 12, atrail: 0, vup: 1 },
|
||||
{ s: 28.0, dist: 4.5e3, f: 1.7, az: 72, el: 38, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 12, atrail: 0, vup: 1 },
|
||||
{ s: 30.0, dist: 3.4e3, f: 1.7, az: 90, el: 32, look: 0.5, fov: 38, mag: 1, mx: 40, rtrail: 12, atrail: 0, vup: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'approach', mode: 'true', s0: 31.2, s1: 35.4, blur: 4,
|
||||
tau: [[31.2, iso('2026-10-17T22:30:00Z')], [33.4, CA - 40 * M], [35.4, CA - 4 * M]],
|
||||
id: 'approach', mode: 'true', s0: 30.0, s1: 33.6, blur: 3, inset: 3,
|
||||
tau: [[30.0, iso('2026-10-17T22:30:00Z')], [32.0, CA - 40 * M], [33.6, CA - 5 * M]],
|
||||
cam: [
|
||||
{ s: 31.2, dist: 3.0e3, f: 1.7, az: 96, el: 26, look: 0.5, fov: 38, mag: 1, mx: 30, rtrail: 3, atrail: 1.5, vup: 1 },
|
||||
{ s: 35.4, dist: 1.2e3, f: 2.2, az: 112, el: 18, look: 0.5, fov: 36, mag: 1, mx: 25, rtrail: 1.6, atrail: 1.5, vup: 1 },
|
||||
{ s: 30.0, dist: 3.0e3, f: 1.7, az: 96, el: 26, look: 0.5, fov: 38, mag: 1, mx: 30, rtrail: 3, atrail: 1.5, vup: 1 },
|
||||
{ s: 33.6, dist: 1.4e3, f: 1.7, az: 112, el: 18, look: 0.5, fov: 36, mag: 1, mx: 25, rtrail: 1.6, atrail: 1.5, vup: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ca', mode: 'true', s0: 35.4, s1: 37.4, blur: 2,
|
||||
tau: [[35.4, CA - 4 * M], [37.4, CA + 1.5 * M]],
|
||||
id: 'ca', mode: 'true', s0: 33.6, s1: 36.6, blur: 1, inset: 1, noSky: true,
|
||||
tau: [[33.6, CA - 4 * M], [36.6, CA + 2 * M]],
|
||||
cam: [
|
||||
{ s: 35.4, dist: 26, az: 160, el: 0, look: 0, fov: 34, mag: 0, rtrail: 0.4, atrail: 0.6, comp: 0, pairD: 26, pairTh: 22 },
|
||||
{ s: 37.4, dist: 22, az: 163, el: 0, look: 0, fov: 34, mag: 0, rtrail: 0.4, atrail: 0.6, comp: 0, pairD: 22, pairTh: 18 },
|
||||
{ s: 33.6, dist: 26, az: 160, el: 0, look: 0, fov: 34, mag: 0, rtrail: 0.4, atrail: 0.6, comp: 0, pairD: 26, pairTh: 24 },
|
||||
{ s: 36.6, dist: 20, az: 163, el: 0, look: 0, fov: 34, mag: 0, rtrail: 0.4, atrail: 0.6, comp: 0, pairD: 20, pairTh: 14 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sunrise', mode: 'true', s0: 37.4, s1: 40.0, blur: 2,
|
||||
tau: [[37.4, CA + 2 * M], [40.0, CA + 27 * M]],
|
||||
id: 'sunrise', mode: 'true', s0: 36.6, s1: 39.0, blur: 1, inset: 1.2,
|
||||
tau: [[36.6, CA + 6 * M], [39.0, CA + 27 * M]],
|
||||
cam: [
|
||||
{ s: 37.4, dist: 1.3e3, f: 2.2, az: 96, el: 16, look: 0.5, fov: 36, mag: 1, mx: 25, rtrail: 1.6, atrail: 1.5, vup: 1 },
|
||||
{ s: 40.0, dist: 2.2e3, f: 2.4, az: 104, el: 22, look: 0.5, fov: 36, mag: 1, mx: 25, rtrail: 2, atrail: 1.5, vup: 1 },
|
||||
{ s: 36.6, dist: 1.3e3, f: 2.2, az: 96, el: 16, look: 0.5, fov: 36, mag: 1, mx: 25, rtrail: 1.6, atrail: 1.5, vup: 1 },
|
||||
{ s: 39.0, dist: 2.2e3, f: 2.4, az: 104, el: 22, look: 0.5, fov: 36, mag: 1, mx: 25, rtrail: 2, atrail: 1.5, vup: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'leave', mode: 'ff', s0: 40.0, s1: 44.0, blur: 1, bgRate: 140,
|
||||
tau: [[40.0, CA + 30 * M], [42.0, iso('2026-10-19T08:00:00Z')], [44.0, iso('2026-10-21T12:00:00Z')]],
|
||||
id: 'leave', mode: 'ff', s0: 39.0, s1: 42.4, blur: 1, bgRate: 140, inset: 40,
|
||||
tau: [[39.0, CA + 30 * M], [40.8, iso('2026-10-19T08:00:00Z')], [42.4, iso('2026-10-21T12:00:00Z')]],
|
||||
cam: [
|
||||
{ s: 40.0, dist: 1.2e4, f: 2.8, az: 110, el: 30, look: 0.4, fov: 36, mag: 1, mx: 40, rtrail: 8, atrail: 0, vup: 1 },
|
||||
{ s: 42.0, dist: 4.0e5, f: 2.8, az: 70, el: 30, look: 0.5, fov: 34, mag: 0, rtrail: 30, atrail: 0, vup: 1 },
|
||||
{ s: 44.0, dist: 1.6e6, f: 2.8, az: 44, el: 30, look: 0.5, fov: 34, mag: 0, rtrail: 60, atrail: 0, vup: 1 },
|
||||
{ s: 39.0, dist: 1.2e4, f: 2.8, az: 110, el: 30, look: 0.4, fov: 36, mag: 1, mx: 40, rtrail: 8, atrail: 0, vup: 1 },
|
||||
{ s: 40.8, dist: 4.0e5, f: 2.8, az: 70, el: 30, look: 0.5, fov: 34, mag: 0, rtrail: 30, atrail: 0, vup: 1 },
|
||||
{ s: 42.4, dist: 1.6e6, f: 2.8, az: 44, el: 30, look: 0.5, fov: 34, mag: 0, rtrail: 60, atrail: 0, vup: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'end', mode: 'true', s0: 44.0, s1: 48.5, blur: 1,
|
||||
tau: [[44.0, iso('2026-10-21T12:00:00Z')], [48.5, iso('2026-10-21T14:00:00Z')]],
|
||||
id: 'end', mode: 'true', s0: 42.4, s1: 45.4, blur: 1, inset: 0,
|
||||
tau: [[42.4, iso('2026-10-21T12:00:00Z')], [45.4, iso('2026-10-21T13:30:00Z')]],
|
||||
cam: [
|
||||
{ s: 44.0, ...WIDE, dist: 4.6e7, az: 82, el: 34, atrail: 60, rtrail: 0 },
|
||||
{ s: 48.5, ...WIDE, dist: 4.2e7, az: 96, el: 32, atrail: 60, rtrail: 0 },
|
||||
{ s: 42.4, ...WIDE, dist: 3.4e7, az: 92, el: 28, atrail: 80, rtrail: 0 },
|
||||
{ s: 45.4, ...WIDE, dist: 3.1e7, az: 100, el: 26, atrail: 80, rtrail: 0 },
|
||||
],
|
||||
},
|
||||
];
|
||||
return { shots, duration: 48.5 };
|
||||
return { shots, duration: 45.4 };
|
||||
}
|
||||
Reference in new issue
Block a user