Functional Weave
Code in Python

charts.interpolate-color@1.0.0

impl/python.py

2,439 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import math
from typing import Sequence, Tuple

from .charts_color_linear_to_srgb import linear_to_srgb
from .charts_color_parse_hex import parse_hex
from .charts_color_srgb_to_linear import srgb_to_linear
from .charts_color_to_hex import to_hex
from .charts_color_types import Rgb
from .math_pow import pow  ← from math.pow ^1.0.0 · built alongside by fune

Lab = Tuple[float, float, float]


# Björn Ottosson's matrices, "A perceptual color space for image processing"
# (2020), https://bottosson.github.io/posts/oklab/, applied to linear sRGB.
def _to_oklab(hex: str) -> Lab:  # noqa: A002
    c = parse_hex(hex)
    r = srgb_to_linear(c.r)
    g = srgb_to_linear(c.g)
    b = srgb_to_linear(c.b)
    l = pow(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b, 1 / 3)
    m = pow(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b, 1 / 3)
    s = pow(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b, 1 / 3)
    return (
        0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
        1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
        0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
    )


def _from_oklab(lab: Lab) -> str:
    L, A, B = lab
    l0 = L + 0.3963377774 * A + 0.2158037573 * B
    m0 = L - 0.1055613458 * A - 0.0638541728 * B
    s0 = L - 0.0894841775 * A - 1.2914855480 * B
    l = l0 * l0 * l0
    m = m0 * m0 * m0
    s = s0 * s0 * s0
    return to_hex(
        Rgb(
            r=linear_to_srgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
            g=linear_to_srgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
            b=linear_to_srgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s),
        )
    )


def interpolate_color(ramp: Sequence[str], t: float) -> str:
    """The colour a fraction t of the way along a ramp of evenly spaced stops,
    mixed in OKLab so equal steps look equal."""
    if len(ramp) < 2:
        raise ValueError("ramp needs at least 2 colours, received %d" % len(ramp))
    if isinstance(t, bool) or not isinstance(t, (int, float)) or not math.isfinite(t) or t < 0 or t > 1:
        raise ValueError("t must be between 0 and 1, received %r" % (t,))
    position = float(t) * (len(ramp) - 1)
    i = math.floor(position)
    if i > len(ramp) - 2:
        i = len(ramp) - 2
    u = position - i
    a = _to_oklab(ramp[i])
    b = _to_oklab(ramp[i + 1])
    return _from_oklab((a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u))