Functional Weave
Code in Python

charts.interpolate-color@1.0.0

impl/rust.rs

2,704 bytes · the Rust 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::charts_color::Rgb;  ← from charts.color ^1.0.0 · built alongside by fune
use super::charts_color_linear_to_srgb::linear_to_srgb;
use super::charts_color_parse_hex::parse_hex;
use super::charts_color_srgb_to_linear::srgb_to_linear;
use super::charts_color_to_hex::to_hex;
use super::math_pow::pow;  ← from math.pow ^1.0.0 · built alongside by fune

// Björn Ottosson's matrices, "A perceptual color space for image processing"
// (2020), https://bottosson.github.io/posts/oklab/, applied to linear sRGB.
fn to_oklab(hex: &str) -> [f64; 3] {
    let c = parse_hex(hex);
    let r = srgb_to_linear(c.r);
    let g = srgb_to_linear(c.g);
    let b = srgb_to_linear(c.b);
    let l = pow(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b, 1.0 / 3.0);
    let m = pow(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b, 1.0 / 3.0);
    let s = pow(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b, 1.0 / 3.0);
    [
        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,
    ]
}

fn from_oklab(lab: [f64; 3]) -> String {
    let [cl, ca, cb] = lab;
    let l0 = cl + 0.3963377774 * ca + 0.2158037573 * cb;
    let m0 = cl - 0.1055613458 * ca - 0.0638541728 * cb;
    let s0 = cl - 0.0894841775 * ca - 1.2914855480 * cb;
    let l = l0 * l0 * l0;
    let m = m0 * m0 * m0;
    let s = s0 * s0 * s0;
    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),
    })
}

/// The colour a fraction t of the way along a ramp of evenly spaced stops,
/// mixed in OKLab so equal steps look equal.
///
/// # Panics
/// Panics on fewer than 2 stops, a bad hex colour, or t outside 0..=1.
pub fn interpolate_color(ramp: &[String], t: f64) -> String {
    if ramp.len() < 2 {
        panic!("ramp needs at least 2 colours, received {}", ramp.len());
    }
    if !t.is_finite() || t < 0.0 || t > 1.0 {
        panic!("t must be between 0 and 1, received {}", t);
    }
    let position = t * (ramp.len() - 1) as f64;
    let mut i = position.floor() as usize;
    if i > ramp.len() - 2 {
        i = ramp.len() - 2;
    }
    let u = position - i as f64;
    let a = to_oklab(&ramp[i]);
    let b = to_oklab(&ramp[i + 1]);
    from_oklab([a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let ramp: Vec<String> = args[0].as_arr().iter().map(|v| v.as_str().to_string()).collect();
    Value::str(&interpolate_color(&ramp, args[1].as_f64()))
}