use super::funejson::Value; use super::charts_color::Rgb; 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; // 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 = args[0].as_arr().iter().map(|v| v.as_str().to_string()).collect(); Value::str(&interpolate_color(&ramp, args[1].as_f64())) }