Skip to main content

pfx_post/
tone.rs

1#[derive(Clone, Copy, Debug)]
2pub enum Tone {
3    Aces {
4        gain: f32,
5    },
6    Neutral {
7        start: f32,
8        toe: f32,
9        desaturation: f32,
10        clamp: bool,
11    },
12    Agx,
13}
14
15impl Tone {
16    pub fn aces() -> Self {
17        Self::Aces { gain: 1.0 }
18    }
19
20    pub fn neutral(start: f32, clamp: bool) -> Self {
21        Self::Neutral {
22            start,
23            toe: 0.04,
24            desaturation: 0.15,
25            clamp,
26        }
27    }
28
29    pub fn map(&self, c: [f32; 3]) -> [f32; 3] {
30        match *self {
31            Self::Aces { gain } => aces(c, gain),
32            Self::Neutral {
33                start,
34                toe,
35                desaturation,
36                clamp,
37            } => neutral(c, start, toe, desaturation, clamp),
38            Self::Agx => agx(c),
39        }
40    }
41}
42
43pub fn aces(c: [f32; 3], gain: f32) -> [f32; 3] {
44    c.map(|v| aces_channel(v * gain))
45}
46
47fn aces_channel(x: f32) -> f32 {
48    let a = x * (x * 2.51 + 0.03);
49    let b = x * (x * 2.43 + 0.59) + 0.14;
50    if b.abs() < 1e-8 {
51        0.0
52    } else {
53        (a / b).clamp(0.0, 1.0)
54    }
55}
56
57pub fn neutral(c: [f32; 3], start: f32, toe: f32, desaturation: f32, clamp_hi: bool) -> [f32; 3] {
58    let low = c[0].min(c[1]).min(c[2]);
59    let offset = if low < 0.08 {
60        low - 6.25 * low * low
61    } else {
62        toe
63    };
64    let mut v = [c[0] - offset, c[1] - offset, c[2] - offset];
65    let peak = v[0].max(v[1]).max(v[2]);
66    if peak >= start && peak > 1e-8 {
67        let d = 1.0 - start;
68        let denom = peak + d - start;
69        let mapped = if denom.abs() < 1e-8 {
70            start
71        } else {
72            1.0 - d * d / denom
73        };
74        let scale = mapped / peak;
75        v[0] *= scale;
76        v[1] *= scale;
77        v[2] *= scale;
78        let g = 1.0 - 1.0 / (desaturation * (peak - mapped) + 1.0);
79        v[0] = crate::color::lerp(v[0], mapped, g);
80        v[1] = crate::color::lerp(v[1], mapped, g);
81        v[2] = crate::color::lerp(v[2], mapped, g);
82    }
83    if clamp_hi {
84        v.map(|x| x.clamp(0.0, 1.0))
85    } else {
86        v
87    }
88}
89
90#[allow(clippy::excessive_precision)]
91const AGX_MAT: [[f32; 3]; 3] = [
92    [0.842479062253094, 0.0784335999999992, 0.0792237451477643],
93    [0.0423282422610123, 0.878468636469772, 0.0791661274605434],
94    [0.0423756549057051, 0.0784336, 0.879142973793104],
95];
96
97const AGX_MIN_EV: f32 = -12.47393;
98const AGX_MAX_EV: f32 = 4.026069;
99
100fn mul_row(m: [f32; 3], v: [f32; 3]) -> f32 {
101    m[0] * v[0] + m[1] * v[1] + m[2] * v[2]
102}
103
104fn agx_contrast(x: f32) -> f32 {
105    let x2 = x * x;
106    let x4 = x2 * x2;
107    15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x
108        - 0.00232
109}
110
111pub fn agx(c: [f32; 3]) -> [f32; 3] {
112    let v = [
113        mul_row(AGX_MAT[0], c),
114        mul_row(AGX_MAT[1], c),
115        mul_row(AGX_MAT[2], c),
116    ];
117    let span = AGX_MAX_EV - AGX_MIN_EV;
118    v.map(|channel| {
119        let x = channel.max(1e-10).log2();
120        let t = ((x - AGX_MIN_EV) / span).clamp(0.0, 1.0);
121        agx_contrast(t).clamp(0.0, 1.0)
122    })
123}