#![allow(clippy::excessive_precision)]
const WGM_EPSILON: f32 = 0.001;
#[inline]
fn fastlog2(x: f32) -> f32 {
let vx = x.to_bits();
let mx = f32::from_bits((vx & 0x007F_FFFF) | 0x3F00_0000);
let y = vx as f32 * 1.192_092_9e-7;
y - 124.225_52 - 1.498_030_3 * mx - 1.725_88 / (0.352_088_7 + mx)
}
#[inline]
fn fastpow2(p: f32) -> f32 {
let offset = if p < 0.0 { 1.0 } else { 0.0 };
let clipp = if p < -126.0 { -126.0 } else { p };
let w = clipp as i32;
let z = clipp - w as f32 + offset;
let bits = ((1 << 23) as f32
* (clipp + 121.274_055 + 27.728_022 / (4.842_525_7 - z) - 1.490_129_1 * z))
as u32;
f32::from_bits(bits)
}
#[inline]
pub fn fastpow(x: f32, p: f32) -> f32 {
fastpow2(p * fastlog2(x))
}
const SPECTRAL_R: [f32; 10] = [
0.009281362787953,
0.009732627042016,
0.011254252737167,
0.015105578649573,
0.024797924177217,
0.083622585502406,
0.977865045723212,
1.000000000000000,
0.999961046144372,
0.999999992756822,
];
const SPECTRAL_G: [f32; 10] = [
0.002854127435775,
0.003917589679914,
0.012132151699187,
0.748259205918013,
1.000000000000000,
0.865695937531795,
0.037477469241101,
0.022816789725717,
0.021747419446456,
0.021384940572308,
];
const SPECTRAL_B: [f32; 10] = [
0.537052150373386,
0.546646402401469,
0.575501819073983,
0.258778829633924,
0.041709923751716,
0.012662638828324,
0.007485593127390,
0.006766900622462,
0.006699764779016,
0.006676219883241,
];
const T_MATRIX: [[f32; 10]; 3] = [
[
0.026595621243689,
0.049779426257903,
0.022449850859496,
-0.218453689278271,
-0.256894883201278,
0.445881722194840,
0.772365886289756,
0.194498761382537,
0.014038157587820,
0.007687264480513,
],
[
-0.032601672674412,
-0.061021043498478,
-0.052490001018404,
0.206659098273522,
0.572496335158169,
0.317837248815438,
-0.021216624031211,
-0.019387668756117,
-0.001521339050858,
-0.000835181622534,
],
[
0.339475473216284,
0.635401374177222,
0.771520797089589,
0.113222640692379,
-0.055251113343776,
-0.048222578468680,
-0.012966666339586,
-0.001523814504223,
-0.000094718948810,
-0.000051604594741,
],
];
pub fn rgb_to_spectral(r: f32, g: f32, b: f32) -> [f32; 10] {
let offset = 1.0 - WGM_EPSILON;
let r = r * offset + WGM_EPSILON;
let g = g * offset + WGM_EPSILON;
let b = b * offset + WGM_EPSILON;
let mut out = [0.0_f32; 10];
for i in 0..10 {
out[i] = SPECTRAL_R[i] * r + SPECTRAL_G[i] * g + SPECTRAL_B[i] * b;
}
out
}
pub fn spectral_to_rgb(spec: &[f32; 10]) -> (f32, f32, f32) {
let offset = 1.0 - WGM_EPSILON;
let mut tmp = [0.0_f32; 3];
for i in 0..10 {
tmp[0] += T_MATRIX[0][i] * spec[i];
tmp[1] += T_MATRIX[1][i] * spec[i];
tmp[2] += T_MATRIX[2][i] * spec[i];
}
let out = |v: f32| ((v - WGM_EPSILON) / offset).clamp(0.0, 1.0);
(out(tmp[0]), out(tmp[1]), out(tmp[2]))
}
pub fn mix_colors(a: [f32; 4], b: [f32; 4], fac: f32, paint_mode: f32) -> [f32; 4] {
let opa_a = fac;
let opa_b = 1.0 - opa_a;
let out_a = (opa_a * a[3] + opa_b * b[3]).clamp(0.0, 1.0);
let sfac_a = if a[3] == 0.0 {
0.0
} else {
opa_a * a[3] / (a[3] + b[3] * opa_b)
};
let sfac_b = 1.0 - sfac_a;
let mut result = [0.0_f32; 4];
if paint_mode > 0.0 {
let spec_a = rgb_to_spectral(a[0], a[1], a[2]);
let spec_b = rgb_to_spectral(b[0], b[1], b[2]);
let mut mix = [0.0_f32; 10];
for i in 0..10 {
mix[i] = fastpow(spec_a[i].max(1e-6), sfac_a) * fastpow(spec_b[i].max(1e-6), sfac_b);
}
let (r, g, b_) = spectral_to_rgb(&mix);
result[0] = r;
result[1] = g;
result[2] = b_;
}
if paint_mode < 1.0 {
for i in 0..3 {
let lin = a[i] * opa_a + b[i] * opa_b;
result[i] = result[i] * paint_mode + (1.0 - paint_mode) * lin;
}
}
result[3] = out_a;
result
}
pub fn spectral_blend_factor(x: f32) -> f32 {
const VER_FAC: f32 = 1.65;
const HOR_FAC: f32 = 8.0;
const HOR_OFFS: f32 = 3.0;
let b = x * HOR_FAC - HOR_OFFS;
(0.5 + b / (1.0 + b.abs() * VER_FAC)).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn primaries_round_trip_approximately() {
for &(r, g, b) in &[(1.0_f32, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)] {
let s = rgb_to_spectral(r, g, b);
let (r2, g2, b2) = spectral_to_rgb(&s);
assert!((r2 - r).abs() < 0.05, "r: {r} → {r2}");
assert!((g2 - g).abs() < 0.05, "g: {g} → {g2}");
assert!((b2 - b).abs() < 0.05, "b: {b} → {b2}");
}
}
#[test]
fn blue_plus_yellow_is_greenish() {
let blue = rgb_to_spectral(0.0, 0.0, 1.0);
let yellow = rgb_to_spectral(1.0, 1.0, 0.0);
let mut mix = [0.0_f32; 10];
for i in 0..10 {
mix[i] = blue[i].powf(0.5) * yellow[i].powf(0.5);
}
let (r, g, b) = spectral_to_rgb(&mix);
assert!(g > r, "green {g} should beat red {r}");
assert!(g > b, "green {g} should beat blue {b}");
}
}