Watch
1
0
Fork
You've already forked SouveraineOS
0

docs: what a veil frame has to be

The twenty frames in AniAvatar carry no alpha, so one composited at the blink's
0.12 is a 12% black wash over the whole panel — indistinguishable from expose
dimming the screen. At that alpha the image lands inside 30 of the panel's 255
code values, so anything under 8/255 in the file is simply not there.
This commit is contained in:
Fimeg 2026-08-15 21:42:54 -04:00
commit b47c17075e

240
docs/FACE-VEIL-ASSETS.md Normal file
View file

@ -0,0 +1,240 @@
# The veil — asset specification
What a frame of her has to be before the compositor can flash it across the
whole panel at low alpha. Written 2026-08-15 against the blink curve landed in
`souveraine-viewtop` `b7ef258` and the panel facts in TASK-61.
This is a production spec for a person with an image editor open. Every number
below either comes from the source or is derived from it, and each one says
which.
**The one-line version:** cut her out on a transparent 1080×2160 canvas, export
straight-alpha PNG-24, decontaminate the edges, and author her in the **top
third of her tonal range** — because after compositing she gets about 30 code
values of the panel's 255, and everything darker than that simply is not there.
---
## 1. Why the current frames cannot be used
Measured 2026-08-15 in `~/Projects/AniAvatar/`:
| file | size | PNG colour type |
|---|---|---|
| `ani-idle.png` | 1024×1024 | 2 — truecolour, **no alpha** |
| `ani-idle-blink.png` | 1024×1024 | 2 — **no alpha** |
| `ani-affectionate.png` | 465×388 | 2 — **no alpha** |
| `ani-alert.png` | 1024×1024 | 2 — **no alpha** |
Twenty frames, none with an alpha channel, in two different sizes.
The black behind her is not absence, it is opaque black. Composited at the
blink's 0.12 it produces a **12 % black wash over the entire panel** with a faint
her inside it. That reads as the screen dimming — which is exactly what `expose`
does, and `expose` is a *disclosure* decision the gate makes. Two different
mechanisms must never look the same on the glass.
So the veil needs the fourth channel, and it needs it as a real gradient: the
soft fall-off at a pigtail, the edge of her jaw, the places she is barely there.
A hard 1-bit cut-out will read as a sticker.
---
## 2. Canvas
| property | value | why |
|---|---|---|
| Size | **1080 × 2160 px** | blueline's panel — `scale.rs:238`, `BLUELINE_MODE` |
| Aspect | 1:2 (18:9) | ditto |
| Colour mode | **RGB/8** | `kms.rs` scans out `Xrgb8888`/`Argb8888`; 10-bit exists behind `VIEWTOP_10BIT` and is off |
| Colour profile | **sRGB IEC61966-2.1** | see §5 |
| Background | **transparent** | no matte, no black layer, no white layer |
Do **not** author at 540×1080 logical. The compositor renders at physical scale
2 and a logical-sized asset arrives at half resolution on a 440 dpi panel.
Her art does not have to fill the canvas. Compose her within it — the canvas is
the panel, and where she sits on it is a composition decision, not a crop.
---
## 3. The tonal budget, which is the whole difficulty
This is the number that decides whether the art works, and it is not obvious.
The blink's peak alpha is **0.12** (`atmosphere.rs`, `Blink::PEAK`). Over the
dark desktop the composited output is `0.12 × her value`, so:
| her pixel | what lands on the panel (of 255) |
|---|---|
| 255 | 30 |
| 200 | 24 |
| 128 | 15 |
| 64 | 7 |
| 32 | 3 |
| 16 | **1** |
| 8 | **0 — gone** |
**The entire image compresses into roughly 30 of 255 code values.** Consequences,
all of them real:
- **Anything below ~8/255 in your file does not exist on the glass.** Shadow
detail, dark linework, the black parts of her jacket: gone.
- **Work in the top third.** Her useful range is about 170255. Push midtones up;
what looks blown-out in Photoshop is *correct* here.
- **Silhouette and highlight, not portrait detail.** A 1 px circuit trace at
60 % grey lands at 9/255 and is invisible. Make the tracery bright and at
least 3 px at 1080 width if it is meant to read.
- **Banding is a live risk.** 30 levels across a gradient will step visibly, and
the 10-bit path is off by default (TASK-61 Part 4). Add a small amount of
noise/dither to large soft gradients — 12 % monochromatic — rather than
relying on smooth ramps.
- **This is per-frame, not per-project.** If a specific moment wants to be more
present, the verb can name a higher peak; the art should be authored for 0.12
and simply get stronger, never authored for 0.4 and vanish at 0.12.
The alpha channel is *not* subject to this budget — a pixel at alpha 1.0 in the
file still only reaches 0.12 on the glass, because the blink multiplies the whole
surface. Use alpha for **shape**, use luminance for **presence**.
---
## 4. Alpha, and the one trap that will bite
Wayland's `Argb8888` is **premultiplied alpha**. The fork's fragment shader says
so and un-premultiplies before any colour work
(`smithay/src/backend/renderer/gles/shaders/implicit/texture.frag`).
Photoshop exports **straight** (unpremultiplied) alpha. That is the correct thing
to export — the client that uploads the texture is responsible for premultiplying
— but it means two things for you:
1. **Do not pre-darken her against black to "bake in" the transparency.** That is
premultiplying by hand, and it will be done again downstream, squaring the
alpha and producing a ghost with dark edges.
2. **Decontaminate the edges.** These frames were painted on black. Any
semi-transparent edge pixel produced by keying that background out still
carries black in its RGB, and straight-alpha compositing will show it as a
dark fringe around every soft edge — worst on the hair, which is where she is
softest.
In Photoshop: `Select → Select and Mask`, turn on **Decontaminate Colors**,
output to a new layer with layer mask. Or repaint the edge pixels. Check by
putting her over a mid-grey layer, not over black — black hides exactly the
defect you are looking for.
**Export:** `File → Export → Export As…` → PNG, **Transparency on**, no
"smaller file (8-bit)". The result must be PNG colour type **6** (truecolour +
alpha). Verify:
```
python3 -c "import sys;d=open(sys.argv[1],'rb').read(33);print('colour type',d[25])" frame.png
```
`6` is correct. `2` means the alpha was flattened and the file is unusable.
---
## 5. Colour
The panel is the LG SW43408 — approximately DCI-P3, **no EDID**, 24-bit
(TASK-61). viewtop scans out **byte-through sRGB by default**: colour management
is off unless `VIEWTOP_COLOR=natural|vivid` is set.
So, plainly: **author in sRGB, and expect the phone to render it more saturated
than your monitor does**, because raw sRGB code values land on P3 primaries. That
is a known open defect, not a surprise, and TASK-61 Part 2 is the fix. Do not
compensate for it by desaturating the source — that would make the art wrong the
day colour management is switched on.
Saturated colour survives the tonal budget better than neutral colour does, which
works in your favour. Her established palette is already there:
| preset | primary | secondary |
|---|---|---|
| `NeonGlow` | `#FF1493` | `#7FFF00` |
| `AuroraBorealis` | `#00FFFF` | `#7FFFD4` |
| `CherryBlossom` | `#FFB7C5` | `#FF69B4` |
`#FF1493` and `#00FFFF` are also the two fringe colours a chromatic split throws
(magenta leading, cyan trailing), which is why art in those two reads as
belonging to this compositor rather than sitting on top of it.
---
## 6. Frames, naming, and how many
Keep the existing convention — `ani-<posture>[-<variant>].png`, lowercase,
hyphenated.
**Postures.** `FELT_STATE_ARCHITECTURE.md` defines nine, and the current art does
not match them:
| posture (canonical) | art exists? |
|---|---|
| `idle` | yes |
| `alert` | yes |
| `thinking` | **no** |
| `processing` | yes |
| `affectionate` | yes |
| `straining` | yes |
| `yawning` | yes |
| `listening` | **no** |
| `speaking` | **no** |
| — | `engaged` exists and is **not a posture** |
Decide before redrawing: either add `thinking`/`listening`/`speaking`, or retire
`engaged`. A frame named for a state the system cannot enter will never be shown,
and a posture with no frame falls back silently.
**Variants per posture:** `-blink` is required (it is the technique's name).
`-wink` exists for `affectionate` and is welcome anywhere it is in character.
---
## 7. Memory — the constraint that caps the set size
A 1080×2160 RGBA8 texture is **8.9 MiB resident on the GPU**
(1080 × 2160 × 4 = 9,331,200 bytes). PNG compression does not help; that is the
uploaded size.
TASK-75's measurements, on a 3.5 GB phone: QuickShell alone at **610 MB RSS**,
the WebKit face adding **283 MB**, and a rendering storm reaching **952 MB**
before the kernel OOM-killed the shell.
So: **twenty full-panel frames resident is 178 MB, and that is not affordable.**
- Load **one posture's pair at a time** — base plus `-blink` — 17.8 MiB. Swap on
posture change.
- Or author her smaller than the canvas and place her: a 1080×1200 cut-out is
4.9 MiB, and most compositions do not need the full 2160.
- Either way the *client* owns this budget, not the compositor. The compositor
gets one surface and drives its alpha.
Say which choice was made in TASK-59 when it lands, because "why is the shell
180 MB heavier" is a question somebody will ask three weeks later.
---
## 8. Checklist before handing frames over
- [ ] 1080 × 2160, RGB/8, sRGB profile embedded
- [ ] Transparent background — no matte layer of any colour
- [ ] Edges decontaminated; checked over mid-grey, not over black
- [ ] Not pre-darkened against black (straight alpha, not premultiplied)
- [ ] Luminance authored in the top third; nothing meaningful below 8/255
- [ ] Fine detail ≥ 3 px and bright, or removed
- [ ] Large gradients carry 12 % dither
- [ ] Exported PNG colour type **6** — verified with the snippet in §4
- [ ] Named `ani-<posture>[-blink|-wink].png`, posture from the canonical nine
- [ ] Frame count × 8.9 MiB is a number somebody has agreed to
---
## Connects to
TASK-59 (her face on the glass — owns the surface and the role), TASK-52
(atmosphere; `Blink` is #15 and the curve is landed), TASK-61 (why the panel
renders saturated and when that stops being true), TASK-75 (the memory budget
these frames spend from), `FELT_STATE_ARCHITECTURE.md` (the nine postures and the
fourteen presets, both hers).