Skip to main content

rust_pixel/render/style/color_pro/
hct.rs

1// RustPixel
2// copyright zipxing@hotmail.com 2022~2026
3
4#![allow(non_snake_case)]
5use crate::render::style::color_pro::*;
6use lazy_static::lazy_static;
7use std::f64::consts::PI;
8
9lazy_static! {
10    static ref ENVS: Vec<Environment> = {
11        let v1 = environment(WHITE, 64.0 / PI * 0.2, 20.0, &SURROUND_MAP[2], false);
12        let v2 = environment(
13            WHITE,
14            200.0 / PI * from_lstar(50.0),
15            from_lstar(50.0) * 100.0,
16            &SURROUND_MAP[2],
17            false,
18        );
19        vec![v1, v2]
20        // let mut rv = vec![];
21        // rv.push(v1);
22        // rv.push(v2);
23        // rv
24    };
25}
26
27const ADAPTED_COEF: f64 = 0.42;
28const ADAPTED_COEF_INV: f64 = 1.0 / ADAPTED_COEF;
29const TAU: f64 = 2.0 * PI;
30
31const CAT16: [[f64; 3]; 3] = [
32    [0.401288, 0.650173, -0.051461],
33    [-0.250268, 1.204414, 0.045854],
34    [-0.002079, 0.048952, 0.953127],
35];
36
37const CAT16_INV: [[f64; 3]; 3] = [
38    [1.8620678550872327, -1.0112546305316843, 0.14918677544445175],
39    [
40        0.38752654323613717,
41        0.6214474419314753,
42        -0.008973985167612518,
43    ],
44    [
45        -0.015841498849333856,
46        -0.03412293802851557,
47        1.0499644368778496,
48    ],
49];
50
51const M1: [[f64; 3]; 3] = [
52    [460.0, 451.0, 288.0],
53    [460.0, -891.0, -261.0],
54    [460.0, -220.0, -6300.0],
55];
56
57const SURROUND_MAP: [[f64; 3]; 3] = [[0.8, 0.525, 0.8], [0.9, 0.59, 0.9], [1.0, 0.69, 1.0]];
58
59const HUE_QUAD_MAP: ([f64; 5], [f64; 5], [f64; 5]) = (
60    [20.14, 90.00, 164.25, 237.53, 380.14],
61    [0.8, 0.7, 1.0, 1.2, 0.8],
62    [0.0, 100.0, 200.0, 300.0, 400.0],
63);
64
65const RAD2DEG: f64 = 180.0 / PI;
66const DEG2RAD: f64 = PI / 180.0;
67
68fn spow(x: f64, y: f64) -> f64 {
69    x.powf(y)
70}
71
72fn copy_sign(x: f64, y: f64) -> f64 {
73    x.copysign(y)
74}
75
76fn multiply_matrices(a: [[f64; 3]; 3], b: [f64; 3]) -> [f64; 3] {
77    let mut result = [0.0; 3];
78    for i in 0..3 {
79        for (j, item) in b.iter().enumerate() {
80            result[i] += a[i][j] * item;
81        }
82    }
83    result
84}
85
86fn interpolate(a: f64, b: f64, t: f64) -> f64 {
87    a + t * (b - a)
88}
89
90fn zdiv(a: f64, b: f64) -> f64 {
91    if b == 0.0 {
92        0.0
93    } else {
94        a / b
95    }
96}
97
98fn constrain(v: f64) -> f64 {
99    if v < 0.0 {
100        v + 360.0
101    } else {
102        v % 360.0
103    }
104}
105
106fn adapt(coords: [f64; 3], fl: f64) -> [f64; 3] {
107    coords.map(|c| {
108        let x = spow(fl * c.abs() * 0.01, ADAPTED_COEF);
109        400.0 * copy_sign(x, c) / (x + 27.13)
110    })
111}
112
113fn unadapt(adapted: [f64; 3], fl: f64) -> [f64; 3] {
114    let constant = 100.0 / fl * (27.13_f64).powf(ADAPTED_COEF_INV);
115    adapted.map(|c| {
116        let cabs = c.abs();
117        copy_sign(constant * spow(cabs / (400.0 - cabs), ADAPTED_COEF_INV), c)
118    })
119}
120
121fn hue_quadrature(h: f64) -> f64 {
122    let mut hp = constrain(h);
123    if hp <= HUE_QUAD_MAP.0[0] {
124        hp += 360.0;
125    }
126
127    let i = HUE_QUAD_MAP.0.iter().position(|&x| x > hp).unwrap() - 1;
128    let hi = HUE_QUAD_MAP.0[i];
129    let hii = HUE_QUAD_MAP.0[i + 1];
130    let ei = HUE_QUAD_MAP.1[i];
131    let eii = HUE_QUAD_MAP.1[i + 1];
132    let Hi = HUE_QUAD_MAP.2[i];
133
134    let t = (hp - hi) / ei;
135    Hi + (100.0 * t) / (t + (hii - hp) / eii)
136}
137
138fn inv_hue_quadrature(H: f64) -> f64 {
139    let Hp = (H % 400.0 + 400.0) % 400.0;
140    let i = (Hp / 100.0).floor() as usize;
141    let Hp = Hp % 100.0;
142    let hi = HUE_QUAD_MAP.0[i];
143    let hii = HUE_QUAD_MAP.0[i + 1];
144    let ei = HUE_QUAD_MAP.1[i];
145    let eii = HUE_QUAD_MAP.1[i + 1];
146
147    constrain((Hp * (eii * hi - ei * hii) - 100.0 * hi * eii) / (Hp * (eii - ei) - 100.0 * eii))
148}
149
150#[derive(Debug)]
151struct Environment {
152    fl: f64,
153    fl_root: f64,
154    n: f64,
155    z: f64,
156    nbb: f64,
157    ncb: f64,
158    c: f64,
159    nc: f64,
160    d_rgb: [f64; 3],
161    d_rgb_inv: [f64; 3],
162    a_w: f64,
163}
164
165fn environment(
166    ref_white: [f64; 3],
167    adapting_luminance: f64,
168    background_luminance: f64,
169    surround: &'static [f64; 3],
170    discounting: bool,
171) -> Environment {
172    let xyz_w = ref_white.map(|c| c * 100.0);
173    let la = adapting_luminance;
174    let yb = background_luminance;
175    let yw = xyz_w[1];
176    let rgb_w = multiply_matrices(CAT16, xyz_w);
177
178    let f = surround[0];
179    let c = surround[1];
180    let nc = surround[2];
181
182    let k = 1.0 / (5.0 * la + 1.0);
183    let k4 = k.powi(4);
184
185    let fl = k4 * la + 0.1 * (1.0 - k4) * (1.0 - k4) * (5.0 * la).cbrt();
186    let fl_root = fl.powf(0.25);
187
188    let n = yb / yw;
189    let z = 1.48 + n.sqrt();
190    let nbb = 0.725 * n.powf(-0.2);
191    let ncb = nbb;
192
193    let d = if discounting {
194        1.0
195    } else {
196        f * (1.0 - 1.0 / 3.6 * ((-la - 42.0) / 92.0).exp()).clamp(0.0, 1.0)
197    };
198    let d_rgb = rgb_w.map(|c| interpolate(1.0, yw / c, d));
199    let d_rgb_inv = d_rgb.map(|c| 1.0 / c);
200
201    let rgb_cw = [
202        rgb_w[0] * d_rgb[0],
203        rgb_w[1] * d_rgb[1],
204        rgb_w[2] * d_rgb[2],
205    ];
206    let rgb_aw = adapt(rgb_cw, fl);
207    let a_w = nbb * (2.0 * rgb_aw[0] + rgb_aw[1] + 0.05 * rgb_aw[2]);
208
209    Environment {
210        fl,
211        fl_root,
212        n,
213        z,
214        nbb,
215        ncb,
216        c,
217        nc,
218        d_rgb,
219        d_rgb_inv,
220        a_w,
221    }
222}
223
224fn from_cam16(cam16: &Cam16Object, env: &Environment) -> [f64; 3] {
225    if !(cam16.J.is_some() ^ cam16.Q.is_some()) {
226        panic!("Conversion requires one and only one: 'J' or 'Q'");
227    }
228
229    if !(cam16.C.is_some() ^ cam16.M.is_some() ^ cam16.s.is_some()) {
230        panic!("Conversion requires one and only one: 'C', 'M' or 's'");
231    }
232
233    if !(cam16.h.is_some() ^ cam16.H.is_some()) {
234        panic!("Conversion requires one and only one: 'h' or 'H'");
235    }
236
237    if cam16.J == Some(0.0) || cam16.Q == Some(0.0) {
238        return [0.0, 0.0, 0.0];
239    }
240
241    let h_rad = if let Some(h) = cam16.h {
242        constrain(h) * DEG2RAD
243    } else {
244        inv_hue_quadrature(cam16.H.unwrap()) * DEG2RAD
245    };
246
247    let cos_h = h_rad.cos();
248    let sin_h = h_rad.sin();
249
250    let j_root = if let Some(J) = cam16.J {
251        spow(J, 0.5) * 0.1
252    } else {
253        0.25 * env.c * cam16.Q.unwrap() / ((env.a_w + 4.0) * env.fl_root)
254    };
255
256    let alpha = if let Some(C) = cam16.C {
257        C / j_root
258    } else if let Some(M) = cam16.M {
259        (M / env.fl_root) / j_root
260    } else {
261        0.0004 * spow(cam16.s.unwrap(), 2.0) * (env.a_w + 4.0) / env.c
262    };
263
264    let t = spow(alpha * spow(1.64 - spow(0.29, env.n), -0.73), 10.0 / 9.0);
265
266    let et = 0.25 * ((h_rad + 2.0).cos() + 3.8);
267
268    let A = env.a_w * spow(j_root, 2.0 / (env.c * env.z));
269
270    let p1 = 50000.0 / 13.0 * env.nc * env.ncb * et;
271    let p2 = A / env.nbb;
272
273    let r = 23.0 * (p2 + 0.305) * zdiv(t, 23.0 * p1 + t * (11.0 * cos_h + 108.0 * sin_h));
274    let a = r * cos_h;
275    let b = r * sin_h;
276
277    let rgb_c = unadapt(
278        multiply_matrices(M1, [p2, a, b]).map(|c| c / 1403.0),
279        env.fl,
280    );
281    let xyz = multiply_matrices(
282        CAT16_INV,
283        [
284            rgb_c[0] * env.d_rgb_inv[0],
285            rgb_c[1] * env.d_rgb_inv[1],
286            rgb_c[2] * env.d_rgb_inv[2],
287        ],
288    );
289
290    [xyz[0] / 100.0, xyz[1] / 100.0, xyz[2] / 100.0]
291}
292
293fn to_cam16(xyzd65: [f64; 3], env: &Environment) -> Cam16Object {
294    let xyz100 = xyzd65.map(|c| c * 100.0);
295    let mut tmp: [f64; 3] = [0.0, 0.0, 0.0];
296    let tmpmm = multiply_matrices(CAT16, xyz100);
297    for i in 0..tmpmm.len() {
298        tmp[i] = tmpmm[i] * env.d_rgb[i];
299    }
300    let rgb_a = adapt(tmp, env.fl);
301
302    let a = rgb_a[0] + (-12.0 * rgb_a[1] + rgb_a[2]) / 11.0;
303    let b = (rgb_a[0] + rgb_a[1] - 2.0 * rgb_a[2]) / 9.0;
304    let h_rad = ((b.atan2(a) % TAU) + TAU) % TAU;
305
306    let et = 0.25 * ((h_rad + 2.0).cos() + 3.8);
307
308    let t = 50000.0 / 13.0
309        * env.nc
310        * env.ncb
311        * zdiv(
312            et * (a * a + b * b).sqrt(),
313            rgb_a[0] + rgb_a[1] + 1.05 * rgb_a[2] + 0.305,
314        );
315    let alpha = spow(t, 0.9) * spow(1.64 - spow(0.29, env.n), 0.73);
316
317    let a = env.nbb * (2.0 * rgb_a[0] + rgb_a[1] + 0.05 * rgb_a[2]);
318
319    let j_root = spow(a / env.a_w, 0.5 * env.c * env.z);
320
321    let j = 100.0 * spow(j_root, 2.0);
322
323    let q = 4.0 / env.c * j_root * (env.a_w + 4.0) * env.fl_root;
324
325    let c = alpha * j_root;
326
327    let m = c * env.fl_root;
328
329    let h = constrain(h_rad * RAD2DEG);
330
331    let H = hue_quadrature(h);
332
333    let s = 50.0 * spow(env.c * alpha / (env.a_w + 4.0), 0.5);
334
335    Cam16Object {
336        J: Some(j),
337        C: Some(c),
338        h: Some(h),
339        s: Some(s),
340        Q: Some(q),
341        M: Some(m),
342        H: Some(H),
343    }
344}
345
346#[derive(Debug)]
347struct Cam16Object {
348    J: Option<f64>,
349    C: Option<f64>,
350    h: Option<f64>,
351    s: Option<f64>,
352    Q: Option<f64>,
353    M: Option<f64>,
354    H: Option<f64>,
355}
356
357fn to_lstar(y: f64) -> f64 {
358    let fy = if y > EPSILON_LSTAR {
359        y.cbrt()
360    } else {
361        (KAPPA * y + 16.0) / 116.0
362    };
363    (116.0 * fy) - 16.0
364}
365
366fn from_lstar(lstar: f64) -> f64 {
367    if lstar > 8.0 {
368        ((lstar + 16.0) / 116.0).powi(3)
369    } else {
370        lstar / KAPPA
371    }
372}
373
374fn from_hct(coords: [f64; 3], env: &Environment) -> [f64; 3] {
375    let [h, c, t] = coords;
376    let mut xyz;
377    let mut j;
378
379    if t == 0.0 {
380        return [0.0, 0.0, 0.0];
381    }
382
383    let y = from_lstar(t);
384
385    if t > 0.0 {
386        j = 0.00379058511492914 * t.powi(2) + 0.608983189401032 * t + 0.9155088574762233;
387    } else {
388        j = 9.514_440_756_550_361e-6 * t.powi(2) + 0.08693057439788597 * t - 21.928975842194614;
389    }
390
391    const THRESHOLD: f64 = 2e-12;
392    const MAX_ATTEMPTS: usize = 15;
393
394    let mut attempt = 0;
395    let mut last = f64::INFINITY;
396    let mut best = j;
397
398    while attempt <= MAX_ATTEMPTS {
399        let cam16 = Cam16Object {
400            J: Some(j),
401            C: Some(c),
402            H: None,
403            s: None,
404            Q: None,
405            M: None,
406            h: Some(h),
407        };
408
409        xyz = from_cam16(&cam16, env);
410
411        let delta = (xyz[1] - y).abs();
412        if delta < last {
413            if delta <= THRESHOLD {
414                return xyz;
415            }
416            best = j;
417            last = delta;
418        }
419
420        j -= (xyz[1] - y) * j / (2.0 * xyz[1]);
421
422        attempt += 1;
423    }
424
425    let cam16 = Cam16Object {
426        J: Some(best),
427        C: Some(c),
428        H: None,
429        s: None,
430        Q: None,
431        M: None,
432        h: Some(h),
433    };
434
435    from_cam16(&cam16, env)
436}
437
438fn to_hct(xyz: [f64; 3], env: &Environment) -> [f64; 3] {
439    let t = to_lstar(xyz[1]);
440    if t == 0.0 {
441        return [0.0, 0.0, 0.0];
442    }
443    let cam16 = to_cam16(xyz, env);
444    [constrain(cam16.h.unwrap()), cam16.C.unwrap(), t]
445}
446
447#[inline(always)]
448pub fn cam16_to_xyz(l: ColorData) -> ColorData {
449    let j = l.v[0];
450    let m = l.v[1];
451    let h = l.v[2];
452    let cam16 = Cam16Object {
453        J: Some(j),
454        C: None,
455        H: None,
456        s: None,
457        Q: None,
458        M: Some(m),
459        h: Some(h),
460    };
461    let xyz = from_cam16(&cam16, &ENVS[0]);
462    ColorData {
463        v: [xyz[0], xyz[1], xyz[2], l.v[3]],
464    }
465}
466
467#[inline(always)]
468pub fn xyz_to_cam16(l: ColorData) -> ColorData {
469    let x = l.v[0];
470    let y = l.v[1];
471    let z = l.v[2];
472    let cam16 = to_cam16([x, y, z], &ENVS[0]);
473    ColorData {
474        v: [cam16.J.unwrap(), cam16.M.unwrap(), cam16.h.unwrap(), l.v[3]],
475    }
476}
477
478#[inline(always)]
479pub fn hct_to_xyz(l: ColorData) -> ColorData {
480    let hct = [l.v[0], l.v[1], l.v[2]];
481    let xyz = from_hct(hct, &ENVS[1]);
482    ColorData {
483        v: [xyz[0], xyz[1], xyz[2], l.v[3]],
484    }
485}
486
487#[inline(always)]
488pub fn xyz_to_hct(l: ColorData) -> ColorData {
489    let xyz = [l.v[0], l.v[1], l.v[2]];
490    let hct = to_hct(xyz, &ENVS[1]);
491    ColorData {
492        v: [hct[0], hct[1], hct[2], l.v[3]],
493    }
494}
495
496// fn main() {
497//     let viewing_conditions = environment(WHITE, 64.0 / PI * 0.2, 20.0, &SURROUND_MAP[2], false);
498//     let viewing_conditions2 = environment(WHITE, 200.0 / PI * from_lstar(50.0), from_lstar(50.0) * 100.0, &SURROUND_MAP[2], false);
499
500//     // [79.10134572991937, 78.2155216870714, 142.22342095435386]
501//     let cam16 = Cam16Object {
502//         J: Some(79.10134572991937),
503//         C: None,
504//         H: None,
505//         s: None,
506//         Q: None,
507//         M: Some(78.2155216870714),
508//         h: Some(142.22342095435386),
509//     };
510
511//     let xyz = from_cam16(&cam16, &viewing_conditions);
512//     println!("XYZ: {:?}", xyz);
513
514//     let hct = to_hct(xyz, &viewing_conditions2);
515//     println!("HCT: {:?}", hct);
516
517//     let xyz = from_hct(hct, &viewing_conditions2);
518//     println!("XYZ2: {:?}", xyz);
519
520//     // let cam16_converted = to_cam16(xyz, &viewing_conditions);
521//     // println!("CAM16: {:?}", cam16_converted);
522// }