Raxol.UI.Theming.Salience (Raxol v2.6.1)

View Source

Salience-tier color solver: OKLCH colors leveled through a two-lobe Helmholtz-Kohlrausch apparent-lightness model.

Instead of hand-picking lightness per color, a palette is expressed as (hue, chroma, tier) seeds. Each tier is an apparent-lightness contrast delta away from the ground (background) lightness; the solver compensates for the H-K effect (chromatic colors — especially blues — read darker than their nominal OKLCH L suggests) so every color on one tier appears equally bright to the eye.

The two-lobe formulation:

apparent_L = L - 0.14 * C * hue_factor(h)

Tiers generalize to any ground: on dark grounds tiers solve up (lighter than ground), on light grounds down, chosen automatically by headroom. Deltas compress proportionally when the ground leaves too little headroom (mid-gray grounds), preserving tier ordering.

Ground lightness is a runtime input — pair with an OSC 11 background query to solve the palette against the terminal's actual background.

Summary

Functions

Apparent lightness of an OKLCH color under the two-lobe H-K model.

Converts a #rrggbb hex string (or any struct/map with integer r, g, b components, e.g. Raxol.Style.Colors.Color) to {l, c, h} OKLCH.

Two-lobe H-K hue factor: warm cosine fundamental plus a Gaussian bump around blue (h = 255), clamped to [0.3, 1.2].

Converts OKLCH to a sRGB hex string, clamping L into displayable range and shrinking chroma until the color fits the sRGB gamut.

Converts OKLCH to a gamut-mapped sRGB {r, g, b} tuple (0-255 per channel), clamping L into displayable range and shrinking chroma until the color fits the sRGB gamut.

The reference near-black ground OKLCH lightness the tier deltas were derived against (0.2). Exposed so callers that need a fallback ground (e.g. no OSC 11 detection) don't duplicate the number.

sRGB relative luminance (Rec. 709 coefficients) of a #rrggbb hex color, per the WCAG contrast-ratio formula ((L1 + 0.05) / (L2 + 0.05)).

Converts sRGB components in 0.0..1.0 to {l, c, h} OKLCH (h in degrees, 0.0 <= h < 360.0).

Solves a (C, h) seed on a salience tier against a ground lightness and returns a hex color.

Nominal OKLCH L that lands a (C, h) color on a target apparent lightness.

Solves a seed list into a palette map.

Apparent-lightness contrast delta for a tier.

Target apparent lightness for a tier against a ground, with headroom compression: when the ground leaves less than the full delta range on the chosen side, all deltas scale down proportionally (ordering preserved).

Tier names ordered by increasing contrast from ground.

Types

polarity()

@type polarity() :: :auto | :up | :down

tier()

@type tier() :: :alarm | :recede | :differentiate | :baseline | :anchor

Functions

apparent_lightness(l, c, h)

@spec apparent_lightness(number(), number(), number()) :: float()

Apparent lightness of an OKLCH color under the two-lobe H-K model.

hex_to_oklch(arg1)

@spec hex_to_oklch(String.t() | %{r: 0..255, g: 0..255, b: 0..255}) ::
  {float(), float(), float()}

Converts a #rrggbb hex string (or any struct/map with integer r, g, b components, e.g. Raxol.Style.Colors.Color) to {l, c, h} OKLCH.

hue_factor(h)

@spec hue_factor(number()) :: float()

Two-lobe H-K hue factor: warm cosine fundamental plus a Gaussian bump around blue (h = 255), clamped to [0.3, 1.2].

oklch_to_hex(l, c, h_deg)

@spec oklch_to_hex(number(), number(), number()) :: String.t()

Converts OKLCH to a sRGB hex string, clamping L into displayable range and shrinking chroma until the color fits the sRGB gamut.

oklch_to_rgb(l, c, h_deg)

@spec oklch_to_rgb(number(), number(), number()) :: {0..255, 0..255, 0..255}

Converts OKLCH to a gamut-mapped sRGB {r, g, b} tuple (0-255 per channel), clamping L into displayable range and shrinking chroma until the color fits the sRGB gamut.

reference_ground()

@spec reference_ground() :: float()

The reference near-black ground OKLCH lightness the tier deltas were derived against (0.2). Exposed so callers that need a fallback ground (e.g. no OSC 11 detection) don't duplicate the number.

relative_luminance(hex)

@spec relative_luminance(String.t()) :: float()

sRGB relative luminance (Rec. 709 coefficients) of a #rrggbb hex color, per the WCAG contrast-ratio formula ((L1 + 0.05) / (L2 + 0.05)).

This is intentionally a different lens than apparent_lightness/3: the H-K apparent-lightness model is the solver's internal notion of equal brightness (used for tier uniformity/monotonicity), while relative luminance is the external, standards-based check a caller can use for a legibility floor that the solver's own model cannot self-certify.

Raises ArgumentError (with the malformed value in the message) when hex isn't exactly 6 hex digits, optionally #-prefixed -- a programming-error contract, not a runtime-input one: callers are expected to pass already-resolved hex colors, not raw user input.

rgb_to_oklch(r, g, b)

@spec rgb_to_oklch(number(), number(), number()) :: {float(), float(), float()}

Converts sRGB components in 0.0..1.0 to {l, c, h} OKLCH (h in degrees, 0.0 <= h < 360.0).

solve(tier, c, h, opts \\ [])

@spec solve(tier(), number(), number(), keyword()) :: String.t()

Solves a (C, h) seed on a salience tier against a ground lightness and returns a hex color.

Options

  • :ground - ground (background) OKLCH lightness. Default 0.2 (the reference near-black ground the tier deltas were derived against).
  • :polarity - :up (tiers lighter than ground), :down (darker), or :auto (default): whichever side of the ground has more headroom.

Examples

iex> Raxol.UI.Theming.Salience.solve(:differentiate, 0.13, 57)
"#c1712c"

iex> Raxol.UI.Theming.Salience.solve(:baseline, 0.0, 250)
"#b4b4b4"

solve_lightness(target_al, c, h)

@spec solve_lightness(number(), number(), number()) :: float()

Nominal OKLCH L that lands a (C, h) color on a target apparent lightness.

solve_palette(seeds, opts \\ [])

@spec solve_palette(
  [map()],
  keyword()
) :: %{required(atom()) => String.t()}

Solves a seed list into a palette map.

Seeds are maps with :name, :h, :c, :tier (extra keys are ignored). Returns %{name => hex}.

tier_delta(tier)

@spec tier_delta(tier()) :: float()

Apparent-lightness contrast delta for a tier.

tier_target(tier, ground, polarity \\ :auto)

@spec tier_target(tier(), number(), polarity()) :: float()

Target apparent lightness for a tier against a ground, with headroom compression: when the ground leaves less than the full delta range on the chosen side, all deltas scale down proportionally (ordering preserved).

tiers()

@spec tiers() :: [tier()]

Tier names ordered by increasing contrast from ground.