an.bench.palette
Derive the set of colours a compiled shot declared — never hand-pin it.
off_palette_pixel_fraction’s whole meaning is “not one of the colours the
compiler declared for this shot”. A hand-tuned palette constant would make it a
per-scene magic number, which is exactly what killed its predecessor
(blend_pixel_fraction: a 12-flat-fill, anti-aliasing-free frame read 2.46%
at K=10, i.e. 4.8x the entire claimed signal).
So the palette is read out of the artifacts the browser actually loaded — the
staged scene.json and the staged SVG files beside it — never from a
re-compile. A re-compile can differ from what ran: a missing character
descriptor makes the compiler fall back to the procedural rig, and before
an#33 it did so without a word.
Three things this module must get right, each a silent failure otherwise:
parse_coloris a verbatim mirror of the runtime’s rule (hex.padEnd(6,'0').slice(0,6)), not a CSS parser. A CSS-correct 3-digit expander maps"#222"to0x222222; the runtime paints0x222000.Some colours are runtime constants, never present in the JSON — the eye whites, the four mouth colours. A JSON-only sweep under-collects them and the metric reads high with no error anywhere.
Some
visual.colorvalues are inert:drawMouthShapenever reads its node’s colour, and everysvg_spritecarries the#888888schema default. Collecting those over-collects.
Both directions are recorded in palette_sources so a reviewer can see which
half moved when the number does.
- an.bench.palette.COLOURED_KINDS: frozenset[str] = frozenset({'ellipse', 'rect'})
visual.kind values whose visual.color the runtime actually paints.
- an.bench.palette.INERT_COLOUR_KINDS: frozenset[str] = frozenset({'mouth', 'sprite', 'svg_sprite'})
visual.kind values whose visual.color is inert.
- an.bench.palette.RUNTIME_DEFAULT_COLOUR: int = 8947848
The runtime’s fallback when a visual.color is absent or not a string.
- an.bench.palette.RUNTIME_EYE_COLOURS: tuple[int, ...] = (16777215, 2236962)
the sclera fill and the 0.6-alpha outline. The outline’s alpha means it paints BLENDS, so it is a lower bound on that node’s contribution — the safe direction.
- Type:
Painted by makeEye regardless of visual.color
- an.bench.palette.RUNTIME_EYE_PUPIL_DEFAULT: str = '#1a1a1a'
The runtime’s fallback pupil colour when a visual.color is absent.
- an.bench.palette.RUNTIME_JS_RELPATH: str = 'an/data/cutout_runtime/runtime.js'
Where the runtime’s own hard-coded colours live, for the source cross-check.
- an.bench.palette.RUNTIME_MOUTH_COLOURS: tuple[int, ...] = (7023403, 2756624, 16448250, 11552840)
lip, fill, teeth, tongue. The mouth node’s own visual.color is never read.
- Type:
Painted by drawMouthShape
- an.bench.palette.palette_for_scene(scene_json: dict, *, runtime_dir: Path) dict[source]
The declared palette of one staged shot.
scene_jsonis the staged compiled scene (what the browser loaded);runtime_diris the directory it was served from, which is where its SVGs were staged.Returns a dict carrying the palette itself plus enough provenance to explain a move: which half of the derivation each colour came from, whether the palette is a superset of what was painted, and every token that could not be resolved.
- an.bench.palette.parse_color(value: Any) int[source]
The runtime’s own colour rule, mirrored exactly.
runtime.js:function parseColor(s) { if (typeof s !== 'string') return 0x888888; const hex = s.startsWith('#') ? s.slice(1) : s; return parseInt(hex.padEnd(6, '0').slice(0, 6), 16); }Note what that is not: a 3-digit CSS shorthand expander.
"#222"pads to"222000", so the runtime paints0x222000and so must this.>>> hex(parse_color("#222")) '0x222000' >>> hex(parse_color("#ffffffaa")) '0xffffff' >>> hex(parse_color(None)) '0x888888'
- an.bench.palette.runtime_literal_colours(runtime_js: Path) set[int][source]
Every 6-digit hex literal the runtime source paints.
Used to cross-check
RUNTIME_EYE_COLOURSandRUNTIME_MOUTH_COLOURSagainst the file that actually paints them, so adding a fifth mouth colour reddens a test instead of silently inflatingoff_palette_pixel_fraction.
- an.bench.palette.svg_colours(svg_path: Path) tuple[set[int], set[str]][source]
Every colour literal an SVG paints, plus the tokens that could not be resolved.
Parsed as XML rather than scraped with a regex, so
style="fill:#abc"and adisplay:nonesubtree are both handled — the first is a spelling a regex overfill="…"misses entirely, and the second contributes colours to a palette that never reach a pixel.An unresolvable token (a named colour, a
url(#gradient)reference) is returned rather than guessed: guessing puts a wrong colour in the palette, and the metric then reads low with no error anywhere.