diff --git a/fleet/web/README.md b/fleet/web/README.md index abaf314e9..05fc18a87 100644 --- a/fleet/web/README.md +++ b/fleet/web/README.md @@ -4,8 +4,9 @@ The page for `docs/fleet/ARCHITECTURE.md`: one image tiled across every GPU, fcv left and EC2 on the right, each side as wide as the GPUs it has live, live from each tile's own renderer. The render is the page: the image fills the screen (contain-fit, centred), with a row of controls over its top and a strip over its bottom (the one button, "Restart all N GPUs", the last race in one line, the next scheduled restart, and what draws the -image: resolution, GPUs, frame rate). The controls fade when the pointer rests. While a race runs, its two bars, -fcvm against EC2, sit over the bottom of the image. Each tile asks its renderer for at most `MAX_TILE_PX` (0.5 M) +image: resolution, GPUs, frame rate). The controls fade when the pointer rests. A race is drawn on the picture: every +dark tile counts, each half has one large clock (fcvm freezes when its last tile is back, EC2 keeps counting), each tile +shows its own time as it relights, and a card gives the result for 8 s before the strip's line takes it over. Each tile asks its renderer for at most `MAX_TILE_PX` (0.5 M) pixels, so one viewer on a large screen cannot push a GPU under one sample per pixel. Everything else (the camera's next stop, words, lights, how the streams arrive, stream stats, the counters and recent actions) is in the drawer, where the sender of a word also picks how it is drawn: Lockup (type above the clock), Skywriting (smoke in the sky) or Landmark (big letters diff --git a/fleet/web/app/_wall/Overlays.tsx b/fleet/web/app/_wall/Overlays.tsx index 13db13d52..7d2395c07 100644 --- a/fleet/web/app/_wall/Overlays.tsx +++ b/fleet/web/app/_wall/Overlays.tsx @@ -1,12 +1,13 @@ "use client"; // Everything drawn over the frames: the line between the halves, each half's -// tag, dark tiles' glyphs and phases, +// tag, dark tiles' clocks, glyphs and phases, the race's clocks and result, +// the wake's progress, // the stats switch's per-tile lines, the seams and the hovered tile's outline // and label, banners and notices. import type { CSSProperties } from "react"; -import type { Glyph, TileView, WallView } from "../../core/index.ts"; +import type { Glyph, HalfClock, SleepView, TileView, WallView } from "../../core/index.ts"; function accent(color: string): CSSProperties { return { ["--accent" as string]: color }; @@ -66,7 +67,7 @@ export function GlyphMark({ g, size }: { g: Glyph; size: number }) { function DarkTile({ t }: { t: TileView }) { const b = t.box!; const big = b.h >= 64 && b.w >= 110; - const size = Math.max(14, Math.min(34, b.h / 3.2)); + const size = Math.max(14, Math.min(34, b.h / (t.clock ? 5 : 3.2))); return (
+ {t.clock && big ? ( + + {t.clock} + + ) : null} {big ? ( @@ -85,7 +91,64 @@ function DarkTile({ t }: { t: TileView }) { ); } -export function Overlays({ view }: { view: WallView; phone: boolean }) { +/** One half's big clock: on the image (absolute), or in the phone's strip under it. */ +export function HalfClockView({ c, style }: { c: HalfClock; style?: CSSProperties }) { + return ( +
+ + {c.value} + + + {c.sub} + + {c.hatch ? ( + + {c.hatch} + + ) : null} + {c.steps.length > 0 ? ( + + {c.steps.map((st, i) => ( + + {i > 0 ? : null} + {st.label} + + ))} + + ) : null} +
+ ); +} + +/** Asleep or waking, in one lower third: on the image, or under it on an upright phone. */ +export function WakeCard({ sleep, style }: { sleep: SleepView; style?: CSSProperties }) { + return ( +
+ + {sleep.title} + {sleep.clock ? ( + + {sleep.clock} + + ) : null} + + {sleep.state === "asleep" ? {sleep.line} : null} + {sleep.steps.length > 0 ? ( +
    + {sleep.steps.map((st) => ( +
  1. +
  2. + ))} +
+ ) : null} +
+ ); +} + +export function Overlays({ view, phone }: { view: WallView; phone: boolean }) { const L = view.layout; const showMosaic = L !== null && view.focus === null && view.own === null; const img = L?.image; @@ -107,6 +170,28 @@ export function Overlays({ view }: { view: WallView; phone: boolean }) {
) : null} {view.tiles.map((t) => (t.dark && t.box ? : null))} + {view.tiles.map((t) => + t.chip && t.box ? ( +
+ {t.box.w >= 56 && t.box.h >= 28 ? ( + + {t.chip.text} + + ) : null} +
+ ) : null, + )} + {phone ? null : view.race?.clocks.map((c) => )} + {view.race?.end && !phone ? ( +
+ {view.race.end} +
+ ) : null} {statsOn ? view.tiles.map((t) => t.box ? ( @@ -171,28 +256,7 @@ export function Overlays({ view }: { view: WallView; phone: boolean }) { ) : null} ) : null} - {view.sleep && img ? ( -
- {view.sleep.title} - {view.sleep.clock ? ( - - {view.sleep.clock} - - ) : null} - {view.sleep.line} - {view.sleep.steps.length > 0 ? ( -
    - {view.sleep.steps.map((st) => ( -
  1. -
  2. - ))} -
- ) : null} -
- ) : null} + {view.sleep && img && !phone ? : null} {view.banners.length > 0 ? (
{view.banners.map((b) => ( diff --git a/fleet/web/app/_wall/Reel.tsx b/fleet/web/app/_wall/Reel.tsx index cfcf7aa49..ca0da4540 100644 --- a/fleet/web/app/_wall/Reel.tsx +++ b/fleet/web/app/_wall/Reel.tsx @@ -13,7 +13,7 @@ import type { ReelView } from "../../core/index.ts"; /** The crossfade from the recording to the live image (wall.css uses the same). */ const FADE_MS = 700; -export function Reel({ reel, box, onDone }: { reel: ReelView; box: CSSProperties | undefined; onDone: () => void }) { +export function Reel({ reel, box, onDone, badgeTop }: { reel: ReelView; box: CSSProperties | undefined; onDone: () => void; badgeTop: number }) { const video = useRef(null); const [playing, setPlaying] = useState(false); const [failed, setFailed] = useState(false); @@ -72,8 +72,8 @@ export function Reel({ reel, box, onDone }: { reel: ReelView; box: CSSProperties ) : null} {playing && !failed && (reel.badge || reel.caption) ? ( - - {reel.badge} + + {reel.badge} {reel.caption ? {reel.caption} : null} ) : null} diff --git a/fleet/web/app/_wall/Score.tsx b/fleet/web/app/_wall/Score.tsx deleted file mode 100644 index 0e269103d..000000000 --- a/fleet/web/app/_wall/Score.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -// The race's two bars, fcvm against EC2, on one time axis, shown over the -// bottom of the image while a race runs. Every word and number comes from the -// view model. - -import type { BarView, ScoreView } from "../../core/index.ts"; - -function pct(f: number): string { - return `${(Math.max(0, Math.min(1, f)) * 100).toFixed(2)}%`; -} - -function Bar({ b, compact }: { b: BarView; compact: boolean }) { - const hatch = b.hatch > 0 ? : null; - const solid = ; - return ( -
- {b.name} -
- ); -} - -export function Scoreboard({ score, compact }: { score: ScoreView; compact: boolean }) { - return ( -
- {compact ? null : {score.title}} - - - {compact ? ( - - {[score.fcvm, score.ec2].map((b) => ( - - {b.name}: {b.line} - {b.measured ? `, ${b.measured}` : ""} - {b.hatchLabel ? `. ${b.hatchLabel}` : ""} - - ))} - - ) : null} -
- ); -} diff --git a/fleet/web/app/_wall/Wall.tsx b/fleet/web/app/_wall/Wall.tsx index 0c4323ea3..d44cce9a5 100644 --- a/fleet/web/app/_wall/Wall.tsx +++ b/fleet/web/app/_wall/Wall.tsx @@ -22,6 +22,7 @@ import { lightPosition, sceneT, serverNow, + HUD_BAND, LIGHT_COLORS, wallView, type Box, @@ -31,10 +32,9 @@ import { import { canDecodeH264, WallClient } from "./wall-client.ts"; import { FocusPanel } from "./FocusPanel.tsx"; import { OwnGpu } from "./OwnGpu.tsx"; -import { Overlays } from "./Overlays.tsx"; +import { HalfClockView, Overlays, WakeCard } from "./Overlays.tsx"; import { PlayDrawer, VerifyBox } from "./PlayDrawer.tsx"; import { Reel } from "./Reel.tsx"; -import { Scoreboard } from "./Score.tsx"; import type { DrawSettings } from "./worker-msgs.ts"; const localNow = () => performance.timeOrigin + performance.now(); @@ -593,6 +593,7 @@ export function Wall({ coordUrl, mediaUrl }: { coordUrl: string; mediaUrl: strin reel={view.reel} onDone={reelDone} box={{ left: view.plan.image.x - areas.mosaic.x, top: view.plan.image.y - areas.mosaic.y, width: view.plan.image.w, height: view.plan.image.h }} + badgeTop={phone ? 10 : Math.max(10, HUD_BAND - (view.plan.image.y - areas.mosaic.y))} /> ) : null}
@@ -671,22 +672,22 @@ export function Wall({ coordUrl, mediaUrl }: { coordUrl: string; mediaUrl: strin ) : null; - // While a race runs, and for a few seconds after, its two bars sit over the bottom of the image; at rest, one line says how the last one went. - const race = score && view.racePanel ? ( -
- {next} - -
- ) : null; - + // While a race runs the strip says who started it (the clocks are on the image); after + // its result card, one line says how the last race went. + const racing = view.race !== null; const info = (
- {!view.racePanel && view.lastRace ? ( + {view.race?.label ? ( + + {view.race.label} + + ) : null} + {!racing && view.lastRace ? ( {view.lastRace} ) : null} - {!view.racePanel ? next : null} + {!racing ? next : null} {view.spec ? ( {view.spec} @@ -707,7 +708,19 @@ export function Wall({ coordUrl, mediaUrl }: { coordUrl: string; mediaUrl: strin {stage} - {race} + {view.race && view.race.clocks.length > 0 ? ( +
+ {view.race.clocks.map((c) => ( + + ))} +
+ ) : null} + {view.sleep ? : null} + {view.race?.end ? ( +

+ {view.race.end} +

+ ) : null} {info}

{view.rotateHint} @@ -769,7 +782,6 @@ export function Wall({ coordUrl, mediaUrl }: { coordUrl: string; mediaUrl: strin