OXYGENHUB
# 05f1a85a2842 oxygenhub / docs / backgrounds.md
13.6 KiB · Markdown · last changed UI polish: keys, backups, wiki, editor, bookmarks, summaries, backdrops 22 days ago renderedsource blame history raw
2f14c5f3 Andy McCall ↶ 1
# Backdrops: living backgrounds for OxygenHub
2
6246d1e5 Andy McCall ↶ 3
Status: every scene in §2 is built (2026-10-13): Your sky (oxygenhub#157),
4
the hub's sky (#165), and the mark, ripples, the swarm and adrift (#167).
2f14c5f3 Andy McCall ↶ 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
| Page | Scene | Shows |
31
|---|---|---|
32
| Dashboard | **Your sky** (§5) | Your repositories as clusters; their activity in the last 30 days as the marks around them |
0a3b244b Andy McCall ↶ 33
| Signed-out home, Explore | **The hub's sky** | The same scene from the public repositories listed, up to 40, the most recently active; a filter narrows it. Never a private repository, whoever looks |
2f14c5f3 Andy McCall ↶ 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 |
ffb67a81 Andy McCall 38
| A repository's overview, wiki, proposals and Conflicts tab | **Its sky** | Your sky's scene for that repository alone: its activity of the last 30 days, from the database (#196) |
39
| Settings › Keys, Tokens and Backups | **The mark** | As on sign-in: brand only, no data (#196) |
2f14c5f3 Andy McCall ↶ 40
ffb67a81 Andy McCall 41
**Never:** file, blame and diff pages, the editor, issues and proposals
42
while being written, and admin tables. Those pages are for reading
43
closely, and motion behind them costs attention.
2f14c5f3 Andy McCall ↶ 44
ffb67a81 Andy McCall 45
On 2026-10-14 Andy chose to give backdrops to the pages of that day's
46
polish batch as well (#196): a repository's overview, wiki, proposals
47
and Conflicts tab, and Settings › Keys, Tokens and Backups. They keep the
48
rules above: the content is opaque and marked `data-backdrop-avoid`, so
49
a scene shows only in the margins, and anyone can turn backdrops off.
50
2f14c5f3 Andy McCall ↶ 51
Each scene is its own issue, done one at a time, the dashboard first.
52
53
## 3. Colours
54
55
A backdrop takes its colours from the page's own tokens, so it is right in
56
both looks (GitHub, the default, and classic) and in both themes (light,
57
the default, and dark), and it follows a theme switch at once. Three new
58
tokens join each look's light and dark sets:
59
60
| Token | Use | GitHub light | GitHub dark | Classic (dark) |
61
|---|---|---|---|---|
df5c69cb Andy McCall ↶ 62
| `--bd-mark` | a resting mark | `--mute` at 16% | `--mute` at 14% | `--acc` at 12% |
2f14c5f3 Andy McCall ↶ 63
| `--bd-lit` | a mark near the cursor | `--acc` (#0969da) at 70% | brand cyan #00F0FF at 75% | #00F0FF at 85% |
64
| `--bd-link` | a line between lit marks | `--acc` at 22% | #00F0FF at 28% | #00F0FF at 35% |
65
66
Two more colour rules:
67
68
- **The brand gradient (#00F0FF → #0072FF)** is kept for moments: a new
69
  push arriving, the mark scene. A whole field in it would shout.
70
- **Status colours are used only where the data has a status:** the
71
  Diffusion pill's green, amber and red, and a run's outcome. They never
72
  appear as decoration.
73
74
In light mode a backdrop must be barely there: if you notice it before the
75
content, it is too strong. The contrast test (`tests/contrast.rs`) gains a
76
check that `--bd-mark` stays under 1.3:1 against `--bg`.
77
78
## 4. Motion and the cursor
79
80
- **Drift.** Marks move slowly, at most 12 px a second, along gentle
81
  streams around their cluster. Two streams can cross: their marks pass
82
  each other without touching, and the change in order means nothing.
83
  That is how Oxygen's changes behave, which is where this comes from.
84
- **The cursor lights, it does not pull.** Marks within about 140 px of
85
  the cursor light up in `--bd-lit`, ramping in over 150 ms and fading
86
  out over 600 ms. Nothing is attracted or pushed, so the cursor never
87
  rearranges the picture.
88
- **Lines mean something.** Among the lit marks, a faint line joins two
df5c69cb Andy McCall ↶ 89
  marks only when they belong together in the data: events of the same
90
  repository that happened one after another and sit within 70 px of
91
  each other, so a busy repository reads as a constellation, not a
92
  scribble. Lines fade as the cursor moves on or rests for 2 s.
2f14c5f3 Andy McCall ↶ 93
- **Labels, sparingly.** When the cursor rests near a cluster, its name
df5c69cb Andy McCall ↶ 94
  fades in just above the cluster's outer ring in `--mute`, small. This is the only text a backdrop
2f14c5f3 Andy McCall ↶ 95
  draws, and it is never a link; the dashboard's own list is the link.
96
- **Parallax.** Three depth layers shift by at most 6 px, 3 px and 1 px
97
  against the cursor's offset from the page's centre. That gives a little
98
  depth with no feeling of motion sickness.
6246d1e5 Andy McCall ↶ 99
- **Streams.** Where a scene shows something travelling (Diffusion's
100
  changes, the swarm's chunks), dots move along a gentle arc at 40 px a
101
  second, faster than drift, fading in and out at the ends.
2f14c5f3 Andy McCall ↶ 102
- **Arrivals.** When the page learns of new activity, its mark appears
103
  with one soft ring in the brand gradient that widens and fades over
104
  1.2 s. This is the one moment a backdrop may draw the eye, and only
105
  briefly.
106
107
## 5. The dashboard: Your sky
108
109
**Data.** The dashboard handler already loads your repositories and the
110
activity feed from the database. The backdrop needs a little more of the
111
same:
112
df5c69cb Andy McCall ↶ 113
- the repositories in Your repositories, by name;
114
- up to 1,500 events from the last 30 days across them (pushes,
115
  proposal and issue events, releases, Flow runs), each with its
116
  repository, kind and time, and for a push the number of changes it
117
  brought. A push draws one dot for each change, up to 40, and the sky
118
  draws up to 3,000 marks.
2f14c5f3 Andy McCall ↶ 119
120
The handler embeds these as JSON in the page (`<script
121
type="application/json" id="backdrop">`), so the backdrop costs no
122
second request and never reads a repository's store. That matters:
123
some stores are slow, such as GCS-backed ones. Private repositories
124
appear only for viewers who may see them, exactly as the list beside
125
them does.
126
127
**Layout.**
128
6907a989 Andy McCall ↶ 129
- Each repository is a cluster. The clusters fill the margins beside the
130
  content column, which the page marks with `data-backdrop-avoid`: in the
131
  order of their names' hashes, split between the two sides by their
132
  widths, each side a staggered grid from top to bottom. The same
133
  repositories in the same window always land in the same places. Where
134
  a margin is under 180 px, they sit on a ring around the page instead.
135
- Marks and orbits are drawn at 1.4 times their first size, with orbit
136
  lanes close together, so each repository is a tight knot (Andy,
137
  2026-10-13).
df5c69cb Andy McCall ↶ 138
- The most active repositories sit in the front layer and the quiet ones
139
  further back.
2f14c5f3 Andy McCall ↶ 140
- A cluster's events are its marks: today's are brightest and a month
141
  old are faint.
df5c69cb Andy McCall ↶ 142
- Kinds differ by shape, not colour: a change pushed is a dot, a
143
  proposal event a small diamond, an issue event a ring, a release a
144
  four-point star, a Flow run a small square, and the repository itself a
145
  thin ring at the cluster's core.
146
- A busy repository gets more orbit lanes (6 to 14), so its marks spread
147
  out instead of piling up.
2f14c5f3 Andy McCall ↶ 148
da8f7826 Andy McCall ↶ 149
**Live.** Built (#172). The page's data names its sky (`"live": "mine"`
150
on the dashboard, `"hub"` on Explore and the signed-out home), and every
151
30 s while the page is shown it asks `GET /sky/arrivals?scene=…&since=MS`
152
what happened since (at most a day back). The answer is the same query
153
over the same repositories as the sky it feeds: the viewer's member
154
repositories for `mine` (nothing signed out), the public ones for `hub`,
155
never a private repository in the hub's sky; it is `private, no-store`.
156
Each event joins its cluster as the newest marks with an arrival ring
157
(§4), a thin ring in the lit colour widening from 6 to 46 px (times the
158
zoom) and fading over 1.2 s; a repository the sky does not draw is
159
ignored, and a still sky adds the marks without the ring. Nothing
160
reloads.
2f14c5f3 Andy McCall ↶ 161
6246d1e5 Andy McCall ↶ 162
## 5.1 The other scenes
163
164
- **The mark** (sign in, register, device sign-in): 560 dots on the
165
  Oxygen mark's two rings, centred on the page, turning opposite ways at
166
  9 px a second at the rim, so the stream runs through the middle like
167
  the mark's infinity; each ring has its core. Each ring's dots are
168
  linked in the order round it. Nothing is named.
169
- **Ripples** (a repository's Diffusion tab): the repository in the left
170
  margin with its upstreams beside it, its consumers in the right margin,
171
  each a ring of 18 dots about a thin-ring core in the tone of its
172
  Diffusion pill (the upstream's half for an upstream, the consumer's own
173
  half for a consumer). Ten dots a stream flow from each upstream to the
174
  repository and from it to each consumer, in the stream's tone, and a
175
  ring of 40 dots widens from the repository every 5 s over 3.2 s (never
176
  when still). Names: every repository.
177
- **The swarm** (Admin › Runners › Images, now in the standard width so it
178
  has margins): the hub in the left margin, the runners in the right,
179
  each ringed by a dot for every image it holds; while a runner fetches
180
  an image, 8 small squares a stream (chunks) flow to it from every
181
  holder, the hub's streams bowed one way and peers' the other, up to 40
182
  streams. Names: the hub and each runner.
183
- **Adrift** (not found, and server errors; never a refusal or a bad
184
  request): a cluster of 70 dots in the left margin and one larger mark
185
  leaving it up and to the right at 7 px a second, fading after 70 s and
186
  setting out again.
187
188
Every scene is a `Scene` (`crates/backdrop/src/scene.rs`); all but the
189
sky share `field.rs`: dots placed by their scene each frame, lit by the
190
cursor, joined within a group, named on rest, coloured from the backdrop
191
tokens or, where the data has a status, the page's `--ok`, `--warn` and
192
`--bad`. The page names its scene in `#backdrop-data`
193
(`{"scene": "mark"}`); none means Your sky.
194
2f14c5f3 Andy McCall ↶ 195
## 6. Engine
196
df5c69cb Andy McCall ↶ 197
The engine is Rust compiled to WebAssembly (Andy's choice, 2026-10-13):
198
`crates/backdrop`, a workspace of its own outside the hub's. `sky.rs` is
199
the scene with no browser in it, tested natively; `gl.rs` draws it;
200
`lib.rs` is the browser glue. `make backdrop` builds it for
201
`wasm32-unknown-unknown` and runs `wasm-bindgen --target web` into
6246d1e5 Andy McCall ↶ 202
`assets/backdrop/` (about 288 KB of wasm, 84 KB gzipped, with every scene, and the glue),
df5c69cb Andy McCall ↶ 203
which is committed and embedded in the hub, as `make ts` commits the API
204
types, so neither the hub build nor its offline CI needs the wasm
205
toolchain. `make backdrop-check` runs its fmt, clippy (wasm32) and tests.
2f14c5f3 Andy McCall ↶ 206
df5c69cb Andy McCall ↶ 207
- **Drawing.** WebGL2 with instanced quads for marks (one draw for all,
208
  each shape a signed distance in the fragment shader) and one line
209
  buffer for links. Without WebGL2 there is no backdrop, and nothing
2f14c5f3 Andy McCall ↶ 210
  breaks.
211
- **The canvas.** One fixed full-viewport `<canvas aria-hidden="true">`
212
  behind `main`, with `pointer-events: none`. Its device pixel ratio is
213
  capped at 1.5.
214
- **Frame rate.** It runs at 60 fps while the cursor moves, eases to
215
  30 fps when the cursor is still, and pauses on `visibilitychange`.
216
  After 2 minutes without input it draws one last frame and stops until
da8f7826 Andy McCall ↶ 217
  the next movement or arrival (a scene's drift alone never keeps it
218
  running; lighting, links and rings do, while they change).
2f14c5f3 Andy McCall ↶ 219
- **Reduced motion.** With `prefers-reduced-motion: reduce` it draws one
220
  still frame with no drift, parallax or arrival rings; lit marks still
221
  respond, instantly.
222
- **Budget.** Under 1 ms of script a frame on a 2020 laptop, checked
223
  with the browser's performance panel before each scene ships.
224
- **Scenes are data.** A scene is a preset of layout, shapes, counts and
225
  which data it reads; the engine is shared.
226
df5c69cb Andy McCall ↶ 227
**Why Rust and WASM.** A small script would have done for a few hundred
228
marks; Rust keeps the whole of Oxygen in one language, lets the scene be
229
tested like the rest, and is ready for scenes with heavy layout (a
230
repository's full change graph) where sharing oxy-core's own types would
231
pay. The cost is kept out of the hub's build by committing the output.
2f14c5f3 Andy McCall ↶ 232
233
## 7. The viewer's choice
234
235
Settings › Appearance gains **Background: Moving (default), Still, Off**,
236
kept in a `backdrop` cookie read before first paint, like the theme
237
cookie, so a page never flashes a backdrop it should not have. Still draws
238
the reduced-motion frame. `Save-Data: on` and a pointer that is coarse
239
only (a phone) default to Still.
240
241
## 8. Order of work
242
243
1. The engine and the dashboard's Your sky, with the tokens, the
244
   setting and the contrast check (the next issue).
245
2. Signed-out home and Explore (the same scene, public data).
246
3. Sign in and register (the mark).
247
4. Diffusion ripples.
248
5. The swarm on Admin › Runners › Images.
249
6. Adrift on error pages.
250
251
## 9. Open questions
252
253
- Should a cluster's lit marks show the event's title on rest, as a
254
  tooltip would? It is useful, but it is more text in the background.
255
  For now, no: the activity list beside them has the titles.
256
- Organisation pages could have their own sky (the organisation's
257
  repositories). That is cheap once Your sky exists.