| 1 |
# Backdrops: living backgrounds for OxygenHub |
| 2 |
|
| 3 |
Status: the engine and the dashboard's Your sky are built (oxygenhub#157, |
| 4 |
2026-10-13); the other scenes are proposed (oxygenhub#156). |
| 5 |
|
| 6 |
## 1. What a backdrop is |
| 7 |
|
| 8 |
A backdrop is a quiet, moving picture behind a page's content: a field of |
| 9 |
small marks that drift slowly and respond a little to the cursor. It |
| 10 |
gives the hub a sense of motion and depth without asking for attention. |
| 11 |
|
| 12 |
Three rules shape everything else: |
| 13 |
|
| 14 |
1. **True, not invented.** The footer says "nothing here is inferred: |
| 15 |
what a page says, the object store says". A backdrop keeps that |
| 16 |
promise. Every mark stands for something real the viewer may see: one |
| 17 |
of their repositories, a change, a push, a runner. Nothing is made-up |
| 18 |
decoration that only looks like data, such as fake chunks or fake |
| 19 |
peers. |
| 20 |
2. **Behind, never in the way.** Content is opaque cards on top. A |
| 21 |
backdrop shows only in the margins and the gaps between cards. It never |
| 22 |
takes a click, never moves the layout, and is never needed to read or |
| 23 |
use the page. |
| 24 |
3. **Cheap and polite.** It costs well under a millisecond a frame. It |
| 25 |
stops when the tab is hidden, and it is still for people who ask for |
| 26 |
reduced motion. Anyone can turn it off. |
| 27 |
|
| 28 |
## 2. Which pages |
| 29 |
|
| 30 |
|
| 31 |
|
| 32 |
| Dashboard | **Your sky** (§5) | Your repositories as clusters; their activity in the last 30 days as the marks around them | |
| 33 |
| Signed-out home, Explore | **The hub's sky** | The same scene from public repositories only | |
| 34 |
| Sign in, register, device sign-in | **The mark** | The Oxygen mark's two rings with marks orbiting them; brand only, no data | |
| 35 |
| A repository's Diffusion tab | **Ripples** | The upstream and its consumers as the Diffusion mark draws them: a pulse goes out on a bump, green or red as its run ends | |
| 36 |
| Admin › Runners › Images | **The swarm** | Runners as nodes; chunks moving between them while an image is fetched, from the swarm tracker's live state | |
| 37 |
| Not found, server error | **Adrift** | One mark drifting away from the rest | |
| 38 |
|
| 39 |
**Never:** code, file, blame, diff, conflict and proposal pages, the |
| 40 |
editor, issues and proposals while being written, settings and admin |
| 41 |
tables. Those pages are for reading closely, and motion behind them |
| 42 |
costs attention. Repository overviews have none for the same reason; |
| 43 |
their README is the picture. |
| 44 |
|
| 45 |
Each scene is its own issue, done one at a time, the dashboard first. |
| 46 |
|
| 47 |
## 3. Colours |
| 48 |
|
| 49 |
A backdrop takes its colours from the page's own tokens, so it is right in |
| 50 |
both looks (GitHub, the default, and classic) and in both themes (light, |
| 51 |
the default, and dark), and it follows a theme switch at once. Three new |
| 52 |
tokens join each look's light and dark sets: |
| 53 |
|
| 54 |
|
| 55 |
|
| 56 |
| `--bd-mark` | a resting mark | `--mute` at 16% | `--mute` at 14% | `--acc` at 12% | |
| 57 |
| `--bd-lit` | a mark near the cursor | `--acc` (#0969da) at 70% | brand cyan #00F0FF at 75% | #00F0FF at 85% | |
| 58 |
| `--bd-link` | a line between lit marks | `--acc` at 22% | #00F0FF at 28% | #00F0FF at 35% | |
| 59 |
|
| 60 |
Two more colour rules: |
| 61 |
|
| 62 |
- **The brand gradient (#00F0FF → #0072FF)** is kept for moments: a new |
| 63 |
push arriving, the mark scene. A whole field in it would shout. |
| 64 |
- **Status colours are used only where the data has a status:** the |
| 65 |
Diffusion pill's green, amber and red, and a run's outcome. They never |
| 66 |
appear as decoration. |
| 67 |
|
| 68 |
In light mode a backdrop must be barely there: if you notice it before the |
| 69 |
content, it is too strong. The contrast test (`tests/contrast.rs`) gains a |
| 70 |
check that `--bd-mark` stays under 1.3:1 against `--bg`. |
| 71 |
|
| 72 |
## 4. Motion and the cursor |
| 73 |
|
| 74 |
- **Drift.** Marks move slowly, at most 12 px a second, along gentle |
| 75 |
streams around their cluster. Two streams can cross: their marks pass |
| 76 |
each other without touching, and the change in order means nothing. |
| 77 |
That is how Oxygen's changes behave, which is where this comes from. |
| 78 |
- **The cursor lights, it does not pull.** Marks within about 140 px of |
| 79 |
the cursor light up in `--bd-lit`, ramping in over 150 ms and fading |
| 80 |
out over 600 ms. Nothing is attracted or pushed, so the cursor never |
| 81 |
rearranges the picture. |
| 82 |
- **Lines mean something.** Among the lit marks, a faint line joins two |
| 83 |
marks only when they belong together in the data: events of the same |
| 84 |
repository that happened one after another and sit within 70 px of |
| 85 |
each other, so a busy repository reads as a constellation, not a |
| 86 |
scribble. Lines fade as the cursor moves on or rests for 2 s. |
| 87 |
- **Labels, sparingly.** When the cursor rests near a cluster, its name |
| 88 |
fades in just above the cluster's outer ring in `--mute`, small. This is the only text a backdrop |
| 89 |
draws, and it is never a link; the dashboard's own list is the link. |
| 90 |
- **Parallax.** Three depth layers shift by at most 6 px, 3 px and 1 px |
| 91 |
against the cursor's offset from the page's centre. That gives a little |
| 92 |
depth with no feeling of motion sickness. |
| 93 |
- **Arrivals.** When the page learns of new activity, its mark appears |
| 94 |
with one soft ring in the brand gradient that widens and fades over |
| 95 |
1.2 s. This is the one moment a backdrop may draw the eye, and only |
| 96 |
briefly. |
| 97 |
|
| 98 |
## 5. The dashboard: Your sky |
| 99 |
|
| 100 |
**Data.** The dashboard handler already loads your repositories and the |
| 101 |
activity feed from the database. The backdrop needs a little more of the |
| 102 |
same: |
| 103 |
|
| 104 |
- the repositories in Your repositories, by name; |
| 105 |
- up to 1,500 events from the last 30 days across them (pushes, |
| 106 |
proposal and issue events, releases, Flow runs), each with its |
| 107 |
repository, kind and time, and for a push the number of changes it |
| 108 |
brought. A push draws one dot for each change, up to 40, and the sky |
| 109 |
draws up to 3,000 marks. |
| 110 |
|
| 111 |
The handler embeds these as JSON in the page (`<script |
| 112 |
type="application/json" id="backdrop">`), so the backdrop costs no |
| 113 |
second request and never reads a repository's store. That matters: |
| 114 |
some stores are slow, such as GCS-backed ones. Private repositories |
| 115 |
appear only for viewers who may see them, exactly as the list beside |
| 116 |
them does. |
| 117 |
|
| 118 |
**Layout.** |
| 119 |
|
| 120 |
- Each repository is a cluster, placed by its name's BLAKE3 on a golden |
| 121 |
angle spiral, so your sky looks the same on every visit and on every |
| 122 |
device. |
| 123 |
- The most active repositories sit in the front layer and the quiet ones |
| 124 |
further back. |
| 125 |
- A cluster's events are its marks: today's are brightest and a month |
| 126 |
old are faint. |
| 127 |
- Kinds differ by shape, not colour: a change pushed is a dot, a |
| 128 |
proposal event a small diamond, an issue event a ring, a release a |
| 129 |
four-point star, a Flow run a small square, and the repository itself a |
| 130 |
thin ring at the cluster's core. |
| 131 |
- A busy repository gets more orbit lanes (6 to 14), so its marks spread |
| 132 |
out instead of piling up. |
| 133 |
|
| 134 |
**Live.** When the page learns of new activity (it already polls for the |
| 135 |
notification count), the event joins its cluster with an arrival ring |
| 136 |
(§4). Nothing reloads. |
| 137 |
|
| 138 |
## 6. Engine |
| 139 |
|
| 140 |
The engine is Rust compiled to WebAssembly (Andy's choice, 2026-10-13): |
| 141 |
`crates/backdrop`, a workspace of its own outside the hub's. `sky.rs` is |
| 142 |
the scene with no browser in it, tested natively; `gl.rs` draws it; |
| 143 |
`lib.rs` is the browser glue. `make backdrop` builds it for |
| 144 |
`wasm32-unknown-unknown` and runs `wasm-bindgen --target web` into |
| 145 |
`assets/backdrop/` (about 186 KB of wasm, 60 KB gzipped, and the glue), |
| 146 |
which is committed and embedded in the hub, as `make ts` commits the API |
| 147 |
types, so neither the hub build nor its offline CI needs the wasm |
| 148 |
toolchain. `make backdrop-check` runs its fmt, clippy (wasm32) and tests. |
| 149 |
|
| 150 |
- **Drawing.** WebGL2 with instanced quads for marks (one draw for all, |
| 151 |
each shape a signed distance in the fragment shader) and one line |
| 152 |
buffer for links. Without WebGL2 there is no backdrop, and nothing |
| 153 |
breaks. |
| 154 |
- **The canvas.** One fixed full-viewport `<canvas aria-hidden="true">` |
| 155 |
behind `main`, with `pointer-events: none`. Its device pixel ratio is |
| 156 |
capped at 1.5. |
| 157 |
- **Frame rate.** It runs at 60 fps while the cursor moves, eases to |
| 158 |
30 fps when the cursor is still, and pauses on `visibilitychange`. |
| 159 |
After 2 minutes without input it draws one last frame and stops until |
| 160 |
the next movement. |
| 161 |
- **Reduced motion.** With `prefers-reduced-motion: reduce` it draws one |
| 162 |
still frame with no drift, parallax or arrival rings; lit marks still |
| 163 |
respond, instantly. |
| 164 |
- **Budget.** Under 1 ms of script a frame on a 2020 laptop, checked |
| 165 |
with the browser's performance panel before each scene ships. |
| 166 |
- **Scenes are data.** A scene is a preset of layout, shapes, counts and |
| 167 |
which data it reads; the engine is shared. |
| 168 |
|
| 169 |
**Why Rust and WASM.** A small script would have done for a few hundred |
| 170 |
marks; Rust keeps the whole of Oxygen in one language, lets the scene be |
| 171 |
tested like the rest, and is ready for scenes with heavy layout (a |
| 172 |
repository's full change graph) where sharing oxy-core's own types would |
| 173 |
pay. The cost is kept out of the hub's build by committing the output. |
| 174 |
|
| 175 |
## 7. The viewer's choice |
| 176 |
|
| 177 |
Settings › Appearance gains **Background: Moving (default), Still, Off**, |
| 178 |
kept in a `backdrop` cookie read before first paint, like the theme |
| 179 |
cookie, so a page never flashes a backdrop it should not have. Still draws |
| 180 |
the reduced-motion frame. `Save-Data: on` and a pointer that is coarse |
| 181 |
only (a phone) default to Still. |
| 182 |
|
| 183 |
## 8. Order of work |
| 184 |
|
| 185 |
1. The engine and the dashboard's Your sky, with the tokens, the |
| 186 |
setting and the contrast check (the next issue). |
| 187 |
2. Signed-out home and Explore (the same scene, public data). |
| 188 |
3. Sign in and register (the mark). |
| 189 |
4. Diffusion ripples. |
| 190 |
5. The swarm on Admin › Runners › Images. |
| 191 |
6. Adrift on error pages. |
| 192 |
|
| 193 |
## 9. Open questions |
| 194 |
|
| 195 |
- Should a cluster's lit marks show the event's title on rest, as a |
| 196 |
tooltip would? It is useful, but it is more text in the background. |
| 197 |
For now, no: the activity list beside them has the titles. |
| 198 |
- Organisation pages could have their own sky (the organisation's |
| 199 |
repositories). That is cheap once Your sky exists. |