M2.2: base profiles (Color/Neutral/Vivid/Landscape/Portrait/Monochrome) with amount

Profiles are our own looks applied underneath the user's sliders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-09-30 23:28:51 -04:00
parent ac14995c75
commit ca35e7f888
3 changed files with 78 additions and 1 deletions

View File

@ -101,7 +101,11 @@ fn grain_noise(x: f32, y: f32, seed: u32) -> f32 {
pub fn finish(p: &Prepared, s: &DevelopSettings, frame: &Frame, info: &SourceInfo) -> Rgba8 {
let (w, h) = (p.img.width, p.img.height);
let tone = if info.raw { ToneMap::new(s.light.contrast, s.light.whites, s.light.blacks) } else { ToneMap::display(s.light.contrast, s.light.whites, s.light.blacks) };
let tone = if info.raw {
ToneMap::new(s.light.contrast, s.light.whites, s.light.blacks)
} else {
ToneMap::display(s.light.contrast, s.light.whites, s.light.blacks)
};
let ops = ColorOps::new(s);
let curves = curve_luts(&s.curve);
let vig = if s.section_enabled("effects") { vignette(s) } else { None };

View File

@ -22,6 +22,7 @@ mod finish;
pub mod geometry;
mod local;
pub mod masks;
pub mod profiles;
mod tone;
use lightcraft_develop::{DevelopSettings, Treatment};
@ -104,6 +105,7 @@ pub fn render(src: &Rgb32f, info: &SourceInfo, s: &DevelopSettings, req: &Render
}
};
let mut t = t0;
let s = &*profiles::effective(s);
let frame = geometry::Frame::new(src.width, src.height, s, req.apply_crop);
let (w, h) = frame.fit(req.max_w, req.max_h);
let mut img = frame.sample(src, w, h);

View File

@ -0,0 +1,71 @@
//! Base rendering profiles. A profile is a look applied *underneath* the user's sliders (the sliders
//! stay at their displayed values); `profile.amount` (0–200 %) scales it. All looks are our own.
use std::borrow::Cow;
use lightcraft_develop::{DevelopSettings, Treatment};
/// The settings the pipeline actually renders: user settings plus the profile's look.
pub fn effective(s: &DevelopSettings) -> Cow<'_, DevelopSettings> {
let k = (s.profile.amount / 100.0).clamp(0.0, 2.0);
let id = s.profile.id.as_str();
if id == "lc.color" || id.is_empty() || k == 0.0 {
return Cow::Borrowed(s);
}
let mut e = s.clone();
let add = |v: &mut f64, d: f64, lo: f64, hi: f64| *v = (*v + d * k).clamp(lo, hi);
match id {
"lc.neutral" => {
add(&mut e.light.contrast, -18.0, -100.0, 100.0);
add(&mut e.color.saturation, -10.0, -100.0, 100.0);
add(&mut e.light.highlights, -10.0, -100.0, 100.0);
add(&mut e.light.shadows, 8.0, -100.0, 100.0);
}
"lc.vivid" => {
add(&mut e.light.contrast, 16.0, -100.0, 100.0);
add(&mut e.color.saturation, 14.0, -100.0, 100.0);
add(&mut e.color.vibrance, 14.0, -100.0, 100.0);
add(&mut e.light.blacks, -6.0, -100.0, 100.0);
}
"lc.landscape" => {
add(&mut e.light.contrast, 10.0, -100.0, 100.0);
add(&mut e.color.vibrance, 10.0, -100.0, 100.0);
add(&mut e.mixer.green.sat, 14.0, -100.0, 100.0);
add(&mut e.mixer.aqua.sat, 10.0, -100.0, 100.0);
add(&mut e.mixer.blue.sat, 14.0, -100.0, 100.0);
add(&mut e.mixer.blue.lum, -6.0, -100.0, 100.0);
add(&mut e.effects.clarity, 6.0, -100.0, 100.0);
}
"lc.portrait" => {
add(&mut e.light.contrast, -6.0, -100.0, 100.0);
add(&mut e.mixer.orange.sat, -8.0, -100.0, 100.0);
add(&mut e.mixer.orange.lum, 6.0, -100.0, 100.0);
add(&mut e.mixer.red.sat, -5.0, -100.0, 100.0);
add(&mut e.effects.texture, -6.0, -100.0, 100.0);
}
"lc.mono" => {
e.treatment = Treatment::Bw;
add(&mut e.light.contrast, 8.0, -100.0, 100.0);
}
_ => return Cow::Borrowed(s),
}
Cow::Owned(e)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_profile_is_borrowed_and_others_change_things() {
let s = DevelopSettings::default();
assert!(matches!(effective(&s), Cow::Borrowed(_)));
for id in ["lc.neutral", "lc.vivid", "lc.landscape", "lc.portrait", "lc.mono"] {
let mut t = s.clone();
t.profile.id = id.into();
assert_ne!(*effective(&t), s, "{id}");
t.profile.amount = 0.0;
assert_eq!(*effective(&t), t, "{id} at 0%");
}
}
}