File size: 1,975 Bytes
414dc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"""Deterministic noir pixel palettes derived from a descriptor accent + seed.

Calm, professional, muted - dark slate base, warm parchment, a single accent. No
neon. Everything is computed (no assets), so portraits are instant and offline.
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass

RGBA = tuple[int, int, int, int]

_INK: RGBA = (18, 20, 28, 255)
_BG: RGBA = (20, 22, 31, 255)
_PANEL: RGBA = (33, 36, 49, 255)
_PARCHMENT: RGBA = (217, 200, 160, 255)

_SKIN_TONES: tuple[RGBA, ...] = (
    (224, 187, 153, 255),
    (198, 156, 121, 255),
    (165, 124, 92, 255),
    (122, 90, 66, 255),
    (90, 66, 50, 255),
)
_HAIR_TONES: tuple[RGBA, ...] = (
    (35, 28, 24, 255),
    (62, 44, 32, 255),
    (120, 110, 105, 255),
    (95, 70, 45, 255),
)


@dataclass(frozen=True)
class Palette:
    bg: RGBA
    panel: RGBA
    ink: RGBA
    parchment: RGBA
    skin: RGBA
    hair: RGBA
    cloth: RGBA
    cloth_dark: RGBA
    accent: RGBA


def seed_from(*parts: str) -> int:
    digest = hashlib.sha256("|".join(parts).encode("utf-8")).digest()
    return int.from_bytes(digest[:4], "big")


def _hex_to_rgba(value: str, fallback: RGBA) -> RGBA:
    value = value.strip().lstrip("#")
    if len(value) != 6:
        return fallback
    try:
        return (int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16), 255)
    except ValueError:
        return fallback


def _darken(color: RGBA, factor: float = 0.6) -> RGBA:
    return (int(color[0] * factor), int(color[1] * factor), int(color[2] * factor), 255)


def build_palette(seed: int, accent_hex: str | None) -> Palette:
    accent = _hex_to_rgba(accent_hex or "", (184, 134, 11, 255))
    skin = _SKIN_TONES[seed % len(_SKIN_TONES)]
    hair = _HAIR_TONES[(seed >> 3) % len(_HAIR_TONES)]
    return Palette(
        bg=_BG, panel=_PANEL, ink=_INK, parchment=_PARCHMENT,
        skin=skin, hair=hair, cloth=accent, cloth_dark=_darken(accent, 0.55), accent=accent,
    )