Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/FACE-VEIL-ASSETS.md
Fimeg b47c17075e 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.
2026-08-15 21:42:54 -04:00

9.7 KiB
Raw Permalink Blame History

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).