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:
parent
7086b4a41e
commit
b47c17075e
1 changed files with 240 additions and 0 deletions
240
docs/FACE-VEIL-ASSETS.md
Normal file
240
docs/FACE-VEIL-ASSETS.md
Normal 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 170–255. 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 — 1–2 % 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 1–2 % 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).
|
||||
Loading…
Reference in a new issue