OXYGENHUB
# da8f7826a645 oxygenhub / docs / backgrounds.md
13.1 KiB · Markdown · last changed Live arrivals on the sky 22 days ago renderedsource blame history raw

Backdrops: living backgrounds for OxygenHub

Status: every scene in §2 is built (2026-10-13): Your sky (oxygenhub#157), the hub's sky (#165), and the mark, ripples, the swarm and adrift (#167).

1. What a backdrop is

A backdrop is a quiet, moving picture behind a page's content: a field of small marks that drift slowly and respond a little to the cursor. It gives the hub a sense of motion and depth without asking for attention.

Three rules shape everything else:

  1. True, not invented. The footer says "nothing here is inferred: what a page says, the object store says". A backdrop keeps that promise. Every mark stands for something real the viewer may see: one of their repositories, a change, a push, a runner. Nothing is made-up decoration that only looks like data, such as fake chunks or fake peers.
  2. Behind, never in the way. Content is opaque cards on top. A backdrop shows only in the margins and the gaps between cards. It never takes a click, never moves the layout, and is never needed to read or use the page.
  3. Cheap and polite. It costs well under a millisecond a frame. It stops when the tab is hidden, and it is still for people who ask for reduced motion. Anyone can turn it off.

2. Which pages

PageSceneShows
DashboardYour sky (§5)Your repositories as clusters; their activity in the last 30 days as the marks around them
Signed-out home, ExploreThe hub's skyThe same scene from the public repositories listed, up to 40, the most recently active; a filter narrows it. Never a private repository, whoever looks
Sign in, register, device sign-inThe markThe Oxygen mark's two rings with marks orbiting them; brand only, no data
A repository's Diffusion tabRipplesThe upstream and its consumers as the Diffusion mark draws them: a pulse goes out on a bump, green or red as its run ends
Admin › Runners › ImagesThe swarmRunners as nodes; chunks moving between them while an image is fetched, from the swarm tracker's live state
Not found, server errorAdriftOne mark drifting away from the rest

Never: code, file, blame, diff, conflict and proposal pages, the editor, issues and proposals while being written, settings and admin tables. Those pages are for reading closely, and motion behind them costs attention. Repository overviews have none for the same reason; their README is the picture.

Each scene is its own issue, done one at a time, the dashboard first.

3. Colours

A backdrop takes its colours from the page's own tokens, so it is right in both looks (GitHub, the default, and classic) and in both themes (light, the default, and dark), and it follows a theme switch at once. Three new tokens join each look's light and dark sets:

TokenUseGitHub lightGitHub darkClassic (dark)
--bd-marka resting mark--mute at 16%--mute at 14%--acc at 12%
--bd-lita mark near the cursor--acc (#0969da) at 70%brand cyan #00F0FF at 75%#00F0FF at 85%
--bd-linka line between lit marks--acc at 22%#00F0FF at 28%#00F0FF at 35%

Two more colour rules:

  • The brand gradient (#00F0FF → #0072FF) is kept for moments: a new push arriving, the mark scene. A whole field in it would shout.
  • Status colours are used only where the data has a status: the Diffusion pill's green, amber and red, and a run's outcome. They never appear as decoration.

In light mode a backdrop must be barely there: if you notice it before the content, it is too strong. The contrast test (tests/contrast.rs) gains a check that --bd-mark stays under 1.3:1 against --bg.

4. Motion and the cursor

  • Drift. Marks move slowly, at most 12 px a second, along gentle streams around their cluster. Two streams can cross: their marks pass each other without touching, and the change in order means nothing. That is how Oxygen's changes behave, which is where this comes from.
  • The cursor lights, it does not pull. Marks within about 140 px of the cursor light up in --bd-lit, ramping in over 150 ms and fading out over 600 ms. Nothing is attracted or pushed, so the cursor never rearranges the picture.
  • Lines mean something. Among the lit marks, a faint line joins two marks only when they belong together in the data: events of the same repository that happened one after another and sit within 70 px of each other, so a busy repository reads as a constellation, not a scribble. Lines fade as the cursor moves on or rests for 2 s.
  • Labels, sparingly. When the cursor rests near a cluster, its name fades in just above the cluster's outer ring in --mute, small. This is the only text a backdrop draws, and it is never a link; the dashboard's own list is the link.
  • Parallax. Three depth layers shift by at most 6 px, 3 px and 1 px against the cursor's offset from the page's centre. That gives a little depth with no feeling of motion sickness.
  • Streams. Where a scene shows something travelling (Diffusion's changes, the swarm's chunks), dots move along a gentle arc at 40 px a second, faster than drift, fading in and out at the ends.
  • Arrivals. When the page learns of new activity, its mark appears with one soft ring in the brand gradient that widens and fades over 1.2 s. This is the one moment a backdrop may draw the eye, and only briefly.

5. The dashboard: Your sky

Data. The dashboard handler already loads your repositories and the activity feed from the database. The backdrop needs a little more of the same:

  • the repositories in Your repositories, by name;
  • up to 1,500 events from the last 30 days across them (pushes, proposal and issue events, releases, Flow runs), each with its repository, kind and time, and for a push the number of changes it brought. A push draws one dot for each change, up to 40, and the sky draws up to 3,000 marks.

The handler embeds these as JSON in the page (<script type="application/json" id="backdrop">), so the backdrop costs no second request and never reads a repository's store. That matters: some stores are slow, such as GCS-backed ones. Private repositories appear only for viewers who may see them, exactly as the list beside them does.

Layout.

  • Each repository is a cluster. The clusters fill the margins beside the content column, which the page marks with data-backdrop-avoid: in the order of their names' hashes, split between the two sides by their widths, each side a staggered grid from top to bottom. The same repositories in the same window always land in the same places. Where a margin is under 180 px, they sit on a ring around the page instead.
  • Marks and orbits are drawn at 1.4 times their first size, with orbit lanes close together, so each repository is a tight knot (Andy, 2026-10-13).
  • The most active repositories sit in the front layer and the quiet ones further back.
  • A cluster's events are its marks: today's are brightest and a month old are faint.
  • Kinds differ by shape, not colour: a change pushed is a dot, a proposal event a small diamond, an issue event a ring, a release a four-point star, a Flow run a small square, and the repository itself a thin ring at the cluster's core.
  • A busy repository gets more orbit lanes (6 to 14), so its marks spread out instead of piling up.

Live. Built (#172). The page's data names its sky ("live": "mine" on the dashboard, "hub" on Explore and the signed-out home), and every 30 s while the page is shown it asks GET /sky/arrivals?scene=…&since=MS what happened since (at most a day back). The answer is the same query over the same repositories as the sky it feeds: the viewer's member repositories for mine (nothing signed out), the public ones for hub, never a private repository in the hub's sky; it is private, no-store. Each event joins its cluster as the newest marks with an arrival ring (§4), a thin ring in the lit colour widening from 6 to 46 px (times the zoom) and fading over 1.2 s; a repository the sky does not draw is ignored, and a still sky adds the marks without the ring. Nothing reloads.

5.1 The other scenes

  • The mark (sign in, register, device sign-in): 560 dots on the Oxygen mark's two rings, centred on the page, turning opposite ways at 9 px a second at the rim, so the stream runs through the middle like the mark's infinity; each ring has its core. Each ring's dots are linked in the order round it. Nothing is named.
  • Ripples (a repository's Diffusion tab): the repository in the left margin with its upstreams beside it, its consumers in the right margin, each a ring of 18 dots about a thin-ring core in the tone of its Diffusion pill (the upstream's half for an upstream, the consumer's own half for a consumer). Ten dots a stream flow from each upstream to the repository and from it to each consumer, in the stream's tone, and a ring of 40 dots widens from the repository every 5 s over 3.2 s (never when still). Names: every repository.
  • The swarm (Admin › Runners › Images, now in the standard width so it has margins): the hub in the left margin, the runners in the right, each ringed by a dot for every image it holds; while a runner fetches an image, 8 small squares a stream (chunks) flow to it from every holder, the hub's streams bowed one way and peers' the other, up to 40 streams. Names: the hub and each runner.
  • Adrift (not found, and server errors; never a refusal or a bad request): a cluster of 70 dots in the left margin and one larger mark leaving it up and to the right at 7 px a second, fading after 70 s and setting out again.

Every scene is a Scene (crates/backdrop/src/scene.rs); all but the sky share field.rs: dots placed by their scene each frame, lit by the cursor, joined within a group, named on rest, coloured from the backdrop tokens or, where the data has a status, the page's --ok, --warn and --bad. The page names its scene in #backdrop-data ({"scene": "mark"}); none means Your sky.

6. Engine

The engine is Rust compiled to WebAssembly (Andy's choice, 2026-10-13): crates/backdrop, a workspace of its own outside the hub's. sky.rs is the scene with no browser in it, tested natively; gl.rs draws it; lib.rs is the browser glue. make backdrop builds it for wasm32-unknown-unknown and runs wasm-bindgen --target web into assets/backdrop/ (about 288 KB of wasm, 84 KB gzipped, with every scene, and the glue), which is committed and embedded in the hub, as make ts commits the API types, so neither the hub build nor its offline CI needs the wasm toolchain. make backdrop-check runs its fmt, clippy (wasm32) and tests.

  • Drawing. WebGL2 with instanced quads for marks (one draw for all, each shape a signed distance in the fragment shader) and one line buffer for links. Without WebGL2 there is no backdrop, and nothing breaks.
  • The canvas. One fixed full-viewport <canvas aria-hidden="true"> behind main, with pointer-events: none. Its device pixel ratio is capped at 1.5.
  • Frame rate. It runs at 60 fps while the cursor moves, eases to 30 fps when the cursor is still, and pauses on visibilitychange. After 2 minutes without input it draws one last frame and stops until the next movement or arrival (a scene's drift alone never keeps it running; lighting, links and rings do, while they change).
  • Reduced motion. With prefers-reduced-motion: reduce it draws one still frame with no drift, parallax or arrival rings; lit marks still respond, instantly.
  • Budget. Under 1 ms of script a frame on a 2020 laptop, checked with the browser's performance panel before each scene ships.
  • Scenes are data. A scene is a preset of layout, shapes, counts and which data it reads; the engine is shared.

Why Rust and WASM. A small script would have done for a few hundred marks; Rust keeps the whole of Oxygen in one language, lets the scene be tested like the rest, and is ready for scenes with heavy layout (a repository's full change graph) where sharing oxy-core's own types would pay. The cost is kept out of the hub's build by committing the output.

7. The viewer's choice

Settings › Appearance gains Background: Moving (default), Still, Off, kept in a backdrop cookie read before first paint, like the theme cookie, so a page never flashes a backdrop it should not have. Still draws the reduced-motion frame. Save-Data: on and a pointer that is coarse only (a phone) default to Still.

8. Order of work

  1. The engine and the dashboard's Your sky, with the tokens, the setting and the contrast check (the next issue).
  2. Signed-out home and Explore (the same scene, public data).
  3. Sign in and register (the mark).
  4. Diffusion ripples.
  5. The swarm on Admin › Runners › Images.
  6. Adrift on error pages.

9. Open questions

  • Should a cluster's lit marks show the event's title on rest, as a tooltip would? It is useful, but it is more text in the background. For now, no: the activity list beside them has the titles.
  • Organisation pages could have their own sky (the organisation's repositories). That is cheap once Your sky exists.