mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
116 lines
3.8 KiB
Rust
116 lines
3.8 KiB
Rust
use super::{ParamUi, slider};
|
|
|
|
// ---------------------------------------------------------------- ASC CDL
|
|
|
|
/// An ASC Colour Decision List correction.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Cdl {
|
|
pub slope: [f64; 3],
|
|
pub offset: [f64; 3],
|
|
pub power: [f64; 3],
|
|
pub sat: f64,
|
|
}
|
|
|
|
impl Default for Cdl {
|
|
fn default() -> Self {
|
|
Cdl { slope: [1.0; 3], offset: [0.0; 3], power: [1.0; 3], sat: 1.0 }
|
|
}
|
|
}
|
|
|
|
const LUMA: [f64; 3] = [0.2126, 0.7152, 0.0722];
|
|
|
|
impl Cdl {
|
|
/// Forward CDL. `clamp` = the v1.2 style (results clamped to 0..1 after SOP and saturation).
|
|
pub fn apply(&self, c: [f64; 3], clamp: bool) -> [f64; 3] {
|
|
let mut o = [0.0; 3];
|
|
for i in 0..3 {
|
|
let v = c[i] * self.slope[i] + self.offset[i];
|
|
o[i] = if clamp {
|
|
v.clamp(0.0, 1.0).powf(self.power[i])
|
|
} else if v >= 0.0 {
|
|
v.powf(self.power[i])
|
|
} else {
|
|
v
|
|
};
|
|
}
|
|
let l = o[0] * LUMA[0] + o[1] * LUMA[1] + o[2] * LUMA[2];
|
|
let o = o.map(|v| l + self.sat * (v - l));
|
|
if clamp { o.map(|v| v.clamp(0.0, 1.0)) } else { o }
|
|
}
|
|
/// Exact inverse of [`Cdl::apply`] (inside the clamped range).
|
|
pub fn invert(&self, c: [f64; 3], clamp: bool) -> [f64; 3] {
|
|
let c = if clamp { c.map(|v| v.clamp(0.0, 1.0)) } else { c };
|
|
let l = c[0] * LUMA[0] + c[1] * LUMA[1] + c[2] * LUMA[2];
|
|
let s = if self.sat.abs() < 1e-9 { 1e-9 } else { self.sat };
|
|
let o = c.map(|v| l + (v - l) / s);
|
|
let mut r = [0.0; 3];
|
|
for i in 0..3 {
|
|
let v = if clamp { o[i].clamp(0.0, 1.0) } else { o[i] };
|
|
let pw = self.power[i].max(1e-9);
|
|
let u = if v >= 0.0 { v.powf(1.0 / pw) } else { v };
|
|
let sl = if self.slope[i].abs() < 1e-9 { 1e-9 } else { self.slope[i] };
|
|
r[i] = (u - self.offset[i]) / sl;
|
|
}
|
|
if clamp { r.map(|v| v.clamp(0.0, 1.0)) } else { r }
|
|
}
|
|
}
|
|
|
|
fn tag<'a>(xml: &'a str, name: &str) -> Option<&'a str> {
|
|
let open = xml.find(&format!("<{name}"))?;
|
|
let gt = open + xml[open..].find('>')?;
|
|
if xml[..gt].ends_with('/') {
|
|
return Some("");
|
|
}
|
|
let close = xml[gt..].find(&format!("</{name}>"))? + gt;
|
|
Some(&xml[gt + 1..close])
|
|
}
|
|
|
|
fn nums3(s: &str) -> Option<[f64; 3]> {
|
|
let v: Vec<f64> = s.split_whitespace().filter_map(|t| t.parse().ok()).collect();
|
|
(v.len() >= 3).then(|| [v[0], v[1], v[2]])
|
|
}
|
|
|
|
fn parse_cc(block: &str) -> Cdl {
|
|
let mut c = Cdl::default();
|
|
if let Some(s) = tag(block, "Slope").and_then(nums3) {
|
|
c.slope = s;
|
|
}
|
|
if let Some(s) = tag(block, "Offset").and_then(nums3) {
|
|
c.offset = s;
|
|
}
|
|
if let Some(s) = tag(block, "Power").and_then(nums3) {
|
|
c.power = s;
|
|
}
|
|
if let Some(s) = tag(block, "Saturation").and_then(|t| t.trim().parse().ok()) {
|
|
c.sat = s;
|
|
}
|
|
c
|
|
}
|
|
|
|
/// Parse ASC CDL XML (`.cc`, `.ccc`, `.cdl`): every `<ColorCorrection>` with its `id`.
|
|
pub fn parse_cdl_xml(xml: &str) -> Vec<(String, Cdl)> {
|
|
let mut out = vec![];
|
|
let mut rest = xml;
|
|
while let Some(i) = rest.find("<ColorCorrection") {
|
|
let after = &rest[i..];
|
|
let head_end = after.find('>').unwrap_or(after.len());
|
|
let head = &after[..head_end];
|
|
let id = head
|
|
.find("id=")
|
|
.and_then(|k| {
|
|
let q = head[k + 3..].chars().next()?;
|
|
let s = &head[k + 4..];
|
|
s.find(q).map(|e| s[..e].to_string())
|
|
})
|
|
.unwrap_or_default();
|
|
let end = after.find("</ColorCorrection>").map(|e| e + "</ColorCorrection>".len()).unwrap_or(after.len());
|
|
out.push((id, parse_cc(&after[..end])));
|
|
rest = &after[end..];
|
|
}
|
|
out
|
|
}
|
|
|
|
pub(super) fn sl(lo: f64, hi: f64, smin: f64, smax: f64) -> ParamUi {
|
|
slider(lo, hi, smin, smax, 3)
|
|
}
|