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}