cutan.characters.colour_roles¶
Colour roles: which colour literal in which part is skin, clothing, hair…
A StylePack maps a role to a colour. For the procedural rig the compiler
decides every colour, so the mapping is a lookup. For SVG art the colours live
inside the drawings, and a pack could only reach them by guessing which literal
means what — inferring a role from a pixel, which is what produced an#99’s
wrong-tone lid. So the roles are recorded at the SOURCE instead: the character
factory knows which fill it drew as skin and which as clothing, and it writes
that down in the descriptor’s colour_roles:
{"parts/torso.svg": {"#a83249": "clothing", "#3b2a1a": "hair"}}
This is palette swapping, the indexed-colour technique 2D games have used
since sprites: a part is keyed by the literal it was drawn in, and a swap
rewrites that literal. Keyed per PART, not per character, because one literal
can mean two things in two drawings (the default pupil and a near-black hair
are both #1a1a1a). The one limit it inherits is the classic one: within a
single part, two roles must be drawn in two distinct literals — the factory
guarantees it (distinct_literal()), and an illustrator tagging their own
art is told so by the descriptor validator.
The compiler applies it (recolour_svg()) at compile time: the tagged
part’s SVG text is rewritten, and the result becomes a new, content-addressed
inline texture. Art with no roles — hand-drawn, DiceBear — is untouched, and
the compiler says so.
>>> recolour_svg('<rect fill="#A83249" stroke="#222"/>', {"#a83249": "#123456"})
'<rect fill="#123456" stroke="#222"/>'
Module Attributes
|
Functions
|
|
|
|
|
|
|
|
- cutan.characters.colour_roles.ColourRoles¶
{part path: {"#rrggbb": role}}— the descriptor field’s shape.
- cutan.characters.colour_roles.distinct_literal(colour, taken)[source]¶
colour, nudged by the smallest step until no literal intakenhas it — the one rule palette swapping needs within a part.Two roles drawn in one literal cannot be told apart by a swap, and neither can a role and an untagged detail (a shoe, an outline) that happens to share it. One step in one channel is invisible and makes the key exact.
>>> distinct_literal("#222222", {"#222222"}) '#222223' >>> distinct_literal("#ffffff", {"#ffffff", "#fffffe"}) '#fffffd' >>> distinct_literal("#123456", {"#abcdef"}) '#123456'- Return type:
- cutan.characters.colour_roles.normalise_hex(colour)[source]¶
'#ABC'->'#aabbcc': the one spelling a literal is keyed by.>>> normalise_hex("#A83249"), normalise_hex("#fa0") ('#a83249', '#ffaa00') >>> normalise_hex("red") Traceback (most recent call last): ... ValueError: 'red' is not a #rgb or #rrggbb colour- Return type:
- cutan.characters.colour_roles.recolour_svg(svg, swaps)[source]¶
svgwith every paint use of an old literal replaced by its new one.Only paint colours are touched (
fill,stroke, gradient stops…, as an attribute or a style declaration); everything else — geometry, ids, fragment references — stays byte-for-byte. Literals match case- and length-insensitively (#FA0is#ffaa00). Deterministic, and cached by content: the same text and swaps are rewritten once per process.>>> recolour_svg('<g style="fill:#fa0;stroke:#000"><use href="#fa0"/></g>', ... {"#ffaa00": "#010203"}) '<g style="fill:#010203;stroke:#000"><use href="#fa0"/></g>' >>> recolour_svg('<rect data-fill="#aaa" fill="#aaa"/>', {"#aaaaaa": "#bbbbbb"}) '<rect data-fill="#aaa" fill="#bbbbbb"/>'- Return type:
- cutan.characters.colour_roles.role_recolouring(roles, colour_for)[source]¶
{old literal: new colour}for the rolescolour_forsets.colour_for(role)is the pack’s lookup (per entity); a role it leaves unset keeps its literal, and a role set to the colour it already has is not a swap — so a pack that changes nothing produces nothing to rewrite.>>> role_recolouring({"#a83249": "clothing", "#3b2a1a": "hair"}, ... {"clothing": "#202028"}.get) {'#a83249': '#202028'}