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))