#!/usr/bin/env python3 """Serves gallery/ (from scripts/make-gallery.sh) as a web page on every interface, for looking at the example renders from another machine on the network. python3 scripts/serve-gallery.py [--port 8000] [--live-port 8010] Stills: each example scene gets a card with its L0 and L1 renders side by side, both with camera C1, captioned from the comment at the top of its file in examples/scenes/. Click an image to see it full size; arrow keys step through them and Escape closes. Movie: gallery/movie.mp4 (from make-gallery.sh or resolved-movie --out) to play or download, and the live stream from `resolved-movie --serve 0.0.0.0:8010` on this machine, which the page connects to only while the tab is open. Delta: the comparison scripts/gallery-delta.py writes to gallery/delta/, the example scenes before and after a change. Scenes that changed come first, each with its before, after and difference images; in the full-size view, Space flips between before and after. The page is built on each request, so rerunning make-gallery.sh and reloading shows the new images. Only the PNG files and movie.mp4 in gallery/ are served. Needs only the Python standard library. """ import argparse import html import json import socket import time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote ROOT = Path(__file__).resolve().parent.parent GALLERY = ROOT / "gallery" DELTA = GALLERY / "delta" SCENES = ROOT / "examples" / "scenes" TIERS = ["L0", "L1"] # The camera tier make-gallery.sh renders with. CAMERA = "C1" STYLE = """ :root { --bg: #0b0d12; --card: #141821; --line: #232938; --text: #e6e8ee; --muted: #9aa3b5; --accent: #e8b04a; } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; } header { padding: 2.5rem 1.5rem 1rem; max-width: 1400px; margin: 0 auto; } h1 { margin: 0; font-size: 1.9rem; font-weight: 650; letter-spacing: -0.01em; } h1 span { color: var(--accent); } header p { margin: 0.4rem 0 0; color: var(--muted); } main { max-width: 1400px; margin: 0 auto; padding: 1rem 1.5rem 3rem; display: grid; gap: 1.5rem; } section { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 1.25rem 1.25rem 1.1rem; } h2 { margin: 0 0 0.35rem; font-size: 1.15rem; font-weight: 600; } .caption { margin: 0 0 1rem; color: var(--muted); max-width: 75ch; } details { margin: -0.6rem 0 1rem; color: var(--muted); font-size: 0.9rem; } details summary { cursor: pointer; color: var(--accent); } details p { margin: 0.5rem 0 0; max-width: 75ch; } .pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; } figure { margin: 0; } figure img { width: 100%; aspect-ratio: 16 / 9; object-fit: contain; display: block; background: #000; border-radius: 8px; border: 1px solid var(--line); cursor: zoom-in; transition: transform 0.15s ease, border-color 0.15s ease; } figure img:hover { border-color: var(--accent); transform: translateY(-2px); } figcaption { margin-top: 0.4rem; font-size: 0.85rem; color: var(--muted); } figcaption b { color: var(--text); font-weight: 600; } .missing { aspect-ratio: 16 / 9; display: grid; place-items: center; border-radius: 8px; border: 1px dashed var(--line); color: var(--muted); font-size: 0.9rem; } #view { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.93); display: none; align-items: center; justify-content: center; flex-direction: column; gap: 0.75rem; cursor: zoom-out; padding: 1rem; } #view.open { display: flex; } #view img { max-width: 100%; max-height: calc(100vh - 4rem); } #view div { color: var(--muted); font-size: 0.9rem; } [hidden] { display: none !important; } nav { max-width: 1400px; margin: 0 auto; padding: 0 1.5rem; display: flex; gap: 0.5rem; } nav a { color: var(--muted); text-decoration: none; padding: 0.45rem 1rem; border-radius: 999px; border: 1px solid var(--line); } nav a.on { color: var(--bg); background: var(--accent); border-color: var(--accent); } .movie video, .movie .live { width: 100%; aspect-ratio: 16 / 9; display: block; background: #000; border-radius: 8px; border: 1px solid var(--line); } .movie .live { object-fit: contain; } .movie a.download { color: var(--accent); } .trio { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; } .stats { margin: 0 0 0.9rem; color: var(--muted); font-size: 0.9rem; } .stats b { color: var(--text); font-weight: 600; } .flag { color: var(--accent); } .same { color: var(--muted); } .same li { margin: 0.15rem 0; } .movie code { color: var(--text); } """ SCRIPT = """ const live = document.getElementById('live'); const TABS = ['stills', 'movie', 'delta']; function tab() { const name = TABS.includes(location.hash.slice(1)) ? location.hash.slice(1) : 'stills'; // The panels' ids differ from the links' anchors, so a link switches the tab // instead of scrolling the page to it. TABS.forEach((t) => { document.getElementById(t + '-tab').hidden = t !== name; }); document.querySelectorAll('nav a').forEach( (a) => a.classList.toggle('on', a.hash === '#' + name)); // The live stream renders only while someone watches: connect only on this tab. if (live) live.src = name === 'movie' ? live.dataset.src : ''; } window.addEventListener('hashchange', tab); tab(); const view = document.getElementById('view'); const big = view.querySelector('img'); const label = view.querySelector('div'); let images = []; let current = -1; let flipped = false; function show(i) { current = (i + images.length) % images.length; flipped = false; big.src = images[current].src; label.textContent = images[current].alt; view.classList.add('open'); } // Delta images carry their pair: Space shows the other one of before and after. function flip() { const img = images[current]; if (!img.dataset.other) return; flipped = !flipped; big.src = flipped ? img.dataset.other : img.src; label.textContent = flipped ? img.dataset.otherAlt : img.alt; } document.querySelectorAll('figure img').forEach((img) => img.addEventListener('click', () => { // Step through the images of the tab the clicked one is on. images = [...img.closest('main').querySelectorAll('figure img')]; show(images.indexOf(img)); })); view.addEventListener('click', () => view.classList.remove('open')); document.addEventListener('keydown', (e) => { if (!view.classList.contains('open')) return; if (e.key === 'Escape') view.classList.remove('open'); if (e.key === 'ArrowRight') show(current + 1); if (e.key === 'ArrowLeft') show(current - 1); if (e.key === ' ') { e.preventDefault(); flip(); } }); """ def caption(scene): """The paragraphs of the comment at the top of `scene`, leaving out the example commands.""" paragraphs, lines = [], [] for line in scene.read_text().splitlines(): if not line.startswith("#"): break text = line[1:] if text.strip().startswith("resolved render"): continue if text.strip(): lines.append(text.strip()) elif lines: paragraphs.append(" ".join(lines)) lines = [] if lines: paragraphs.append(" ".join(lines)) return paragraphs def movie_section(host, live_port): movie = GALLERY / "movie.mp4" if movie.is_file(): stamp = int(movie.stat().st_mtime) size = movie.stat().st_size / 1e6 recorded = ( f'' f'
Download the MP4 ({size:.1f} MB)
' ) else: recorded = ( 'No movie yet: run scripts/make-gallery.sh, or '
"cargo run --release -p resolved-showcase -- --out gallery/movie.mp4"
".
An inspector holds station 120 m behind and above a Landsat 8 model 705 km up; the target is propagated by astrokit with J2 and everything is rendered at lighting L1 and camera C1. The camera drifts on a small natural relative orbit around its station and sets its exposure frame by frame. Time runs fast over the daylit Earth, slows as the Moon rises over the limb ahead, and slows almost to real time as the spacecraft passes into Earth's shadow.
{recorded}Rendered in real time on this machine's GPU and streamed as it is
made. Start it with
cargo run --release -p resolved-showcase -- --serve 0.0.0.0:{live_port};
it renders only while this tab is open.
Run '
"python3 scripts/gallery-delta.py, then reload.
The example scenes at {", ".join(data["tiers"])}, {noise}, ' f'compared {html.escape(data["made"])}. In a difference image the after image is ' "dimmed to gray and every changed pixel is colored, on a log scale of how much: " "orange brighter, blue darker. Click an image for full size; Space then flips " "between before and after.
{stats}
'
f"{html.escape(p)}
" for p in paragraphs[1:]) more = f"{first}
{more}' f'" "Run scripts/make-gallery.sh, then reload.
{count} renders of {len(names)} example scenes, last rendered {when}. Click an image to see it full size.