On this page
Byte-exact Python port of the client's culori-backed variant palette math: color conversions, palette extraction, palette swaps, and the seeded HSV randomizer.
#server.src.pixelweaver.variant_palette
#server.src.pixelweaver.variant_palette
Faithful Python port of the client variant palette math.
This is a BYTE-EXACT port of the browser's culori-backed color pipeline used by the variant subsystem:
- color conversions (src/lib/color/color-utils.ts): rgb<->hex, rgb<->hsv, and the OKLab lerpColor interpolation; - palette extraction (palette-extraction.ts); - palette swap application (palette-swap.ts) -- the baking primitive; - the seeded HSV randomizer (variant-randomizer.ts + seeded-rng.ts).
Parity is pinned by server/tests/test_variant_palette.py against golden fixtures generated by RUNNING the actual TypeScript (see src/lib/variants/golden-fixtures.gen.test.ts). Every float op mirrors culori's source (matrices, transfer functions, rounding) so the 8-bit output matches to the byte. The one place JS and Python diverge -- rounding -- is handled by _js_round (JS Math.round is half-up toward +Infinity, unlike Python's banker's rounding).
#_js_round
def _js_round(x: float) -> intJS Math.round: round half up toward +Infinity (not banker's).
#_clamp_round
def _clamp_round(value: float, lo: float, hi: float) -> intcolor-utils clampRound: clamp to [lo, hi] THEN JS-round.
#_fixup
def _fixup(c: float) -> intculori serializeHex fixup: Math.round(clamp(c) * 255) on a 0-1 value.
#rgb_to_hex
def rgb_to_hex(r: float, g: float, b: float) -> strRGB (0-255) -> uppercase #RRGGBB (mirror of rgbToHex).
#hex_to_rgb
def hex_to_rgb(hex_color: str) -> tuple[int, int, int]#rgb / #rrggbb (optional #rrggbbaa) -> RGB ints (mirror of hexToRgb: culori parses to 0-1 then clampRound(*255), which for these hex forms is exactly the byte value).
#rgb_to_hsv
def rgb_to_hsv(r: float, g: float, b: float) -> tuple[int, int, int]RGB (0-255) -> HSV (H 0-360, S 0-100, V 0-100) as clampRound ints.
#_normalize_hue
def _normalize_hue(h: float) -> floatculori normalizeHue: (h % 360) < 0 ? +360.
#hsv_to_rgb
def hsv_to_rgb(h: float, s: float, v: float) -> tuple[int, int, int]HSV (H 0-360, S 0-100, V 0-100) -> RGB (0-255) as clampRound ints.
h may be a float (the randomizer feeds unrounded shifted hues); s/v are on the 0-100 scale (divided by 100 before the culori conversion).
#lerp_color
def lerp_color(color1: str, color2: str, t: float) -> strInterpolate two hex colors in OKLab (mirror of lerpColor).
culori's 2-sample linear interpolator is a + t*(b-a) per channel.
#extract_buffer_palette
def extract_buffer_palette(data: bytes | bytearray | Iterable[int]) -> list[str]Extract unique opaque colors (uppercase hex, insertion order) from an RGBA buffer. Transparent pixels (alpha == 0) are skipped (mirror of extractBufferPalette).
#apply_palette_swap
def apply_palette_swap(data: bytes | bytearray, color_map: dict[str, str]) -> bytesApply an exact-hex-match color swap to an RGBA buffer, preserving alpha.
Returns a new buffer (never mutates the input). Transparent pixels are skipped. An empty map returns an unchanged copy (mirror of applyPaletteSwap).
#interpolate_presets
def interpolate_presets(preset_a: _GroupOverrides, preset_b: _GroupOverrides, t: float) -> _GroupOverridesInterpolate two presets' groupOverrides at factor t (mirror of interpolatePresets). A missing override in one endpoint is treated as the original color (identity).
#generate_interpolated_series
def generate_interpolated_series(preset_a: _GroupOverrides, preset_b: _GroupOverrides, count: int) -> list[_GroupOverrides]N evenly-spaced interpolations (mirror of generateInterpolatedSeries).
#create_rng
def create_rng(seed: int)xorshift32 PRNG (mirror of createRng): floats in [0, 1); zero seed coerced to 1. Every op is truncated to signed int32 like JS bitwise ops.
#generate_random_variant
def generate_random_variant(group_palettes: dict[str, list[str]], *, seed: int, hue_range: float=360, sat_range: float=30, val_range: float=20) -> _GroupOverridesSeeded HSV randomization of each group's palette (mirror of generateRandomVariant with a required seed).
One uniform hue rotation per group (all colors shift together); per-color saturation + value jitter. The RNG stream order is: one draw for the group hue, then two draws (sat, val) per color -- exactly the client's loop order, so the same seed yields identical overrides.