mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-12 21:32:52 +00:00
Input/Output Black and White of every channel take -10000..10000 and Gamma goes down to 0 (it keeps its wider maximum, 10), After Effects' recorded valid ranges in the same units (the .aep mapping is Conv::Plain). Defaults and the 0..1 / 0.1..3 sliders are unchanged. Gamma 0 renders as the existing 0.01 floor, an exponent of 100, so an HDR value a few times Input White would overflow f32 and an infinite curve times an empty output range is NaN. The curve is now bounded at +-1e6 on the CPU (Lv::apply) and the GPU (fxt_lv), so the output stays finite over the whole range; values below the bound are unchanged. The engine regression (tests/effect_ranges.rs) sets both ends of each row through prop.set, checks undo/redo, save and reopen, and that a project saved with the narrower ranges opens with the new ones and its values.
149 lines
7.8 KiB
Rust
149 lines
7.8 KiB
Rust
//! Effect parameters take the whole valid range After Effects records for them (fx_defs in the
|
|
//! aep crate), end to end: `prop.set` keeps both ends, undo and redo restore them, and a saved
|
|
//! project reopens with them. The aep crate's
|
|
//! `effect_hard_ranges_cover_recorded_after_effects_ranges` checks the ranges themselves
|
|
//! against that table.
|
|
|
|
use effectcraft_project::{LayerId, Node, ParamUi, PropGroup};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
|
|
/// (issue, effect, parameter path, lowest, highest): the ends of After Effects' valid range,
|
|
/// in EffectCraft's units, each outside the range the effect accepted before the issue.
|
|
#[rustfmt::skip]
|
|
const ROWS: &[(u32, &str, &str, f64, f64)] = &[
|
|
(3487, "ec.color.levelsic", "rgb/rgbInBlack", -10000.0, 10000.0),
|
|
(3488, "ec.color.levelsic", "rgb/rgbInWhite", -10000.0, 10000.0),
|
|
(3489, "ec.color.levelsic", "rgb/rgbOutBlack", -10000.0, 10000.0),
|
|
(3490, "ec.color.levelsic", "rgb/rgbOutWhite", -10000.0, 10000.0),
|
|
(3491, "ec.color.levelsic", "rgb/rgbGamma", 0.0, 10.0),
|
|
(3487, "ec.color.levelsic", "red/redInBlack", -10000.0, 10000.0),
|
|
(3488, "ec.color.levelsic", "red/redInWhite", -10000.0, 10000.0),
|
|
(3489, "ec.color.levelsic", "red/redOutBlack", -10000.0, 10000.0),
|
|
(3490, "ec.color.levelsic", "red/redOutWhite", -10000.0, 10000.0),
|
|
(3491, "ec.color.levelsic", "red/redGamma", 0.0, 10.0),
|
|
(3487, "ec.color.levelsic", "green/greenInBlack", -10000.0, 10000.0),
|
|
(3488, "ec.color.levelsic", "green/greenInWhite", -10000.0, 10000.0),
|
|
(3489, "ec.color.levelsic", "green/greenOutBlack", -10000.0, 10000.0),
|
|
(3490, "ec.color.levelsic", "green/greenOutWhite", -10000.0, 10000.0),
|
|
(3491, "ec.color.levelsic", "green/greenGamma", 0.0, 10.0),
|
|
(3487, "ec.color.levelsic", "blue/blueInBlack", -10000.0, 10000.0),
|
|
(3488, "ec.color.levelsic", "blue/blueInWhite", -10000.0, 10000.0),
|
|
(3489, "ec.color.levelsic", "blue/blueOutBlack", -10000.0, 10000.0),
|
|
(3490, "ec.color.levelsic", "blue/blueOutWhite", -10000.0, 10000.0),
|
|
(3491, "ec.color.levelsic", "blue/blueGamma", 0.0, 10.0),
|
|
(3487, "ec.color.levelsic", "alpha/alphaInBlack", -10000.0, 10000.0),
|
|
(3488, "ec.color.levelsic", "alpha/alphaInWhite", -10000.0, 10000.0),
|
|
(3489, "ec.color.levelsic", "alpha/alphaOutBlack", -10000.0, 10000.0),
|
|
(3490, "ec.color.levelsic", "alpha/alphaOutWhite", -10000.0, 10000.0),
|
|
(3491, "ec.color.levelsic", "alpha/alphaGamma", 0.0, 10.0),
|
|
];
|
|
|
|
fn path(effect: &str, param: &str) -> String {
|
|
format!("effects/{effect}/{param}")
|
|
}
|
|
|
|
fn read(s: &mut Session, layer: u64, effect: &str, param: &str) -> f64 {
|
|
let got = s.execute("prop.get", json!({"layer": layer, "path": path(effect, param)})).unwrap();
|
|
got["value"].as_f64().unwrap_or_else(|| panic!("{effect} {param}: {got}"))
|
|
}
|
|
|
|
fn set(s: &mut Session, layer: u64, effect: &str, param: &str, v: f64) -> Value {
|
|
s.execute("prop.set", json!({"layer": layer, "path": path(effect, param), "value": v})).unwrap()
|
|
}
|
|
|
|
/// A composition with two solids, each with one instance of every effect in [`ROWS`].
|
|
fn session() -> (Session, u64, u64) {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "frameRate": 30, "duration": 2})).unwrap();
|
|
let mut layers = [0; 2];
|
|
for l in &mut layers {
|
|
*l = s.execute("layer.newSolid", json!({"color": "#808080", "width": 64, "height": 36})).unwrap()["layer"].as_u64().unwrap();
|
|
let mut effects: Vec<&str> = ROWS.iter().map(|r| r.1).collect();
|
|
effects.dedup();
|
|
for e in effects {
|
|
s.execute("effect.apply", json!({"layers": [*l], "effect": e})).unwrap();
|
|
}
|
|
}
|
|
(s, layers[0], layers[1])
|
|
}
|
|
|
|
/// Both ends of every row survive `prop.set`, undo / redo, and a save and reopen.
|
|
#[test]
|
|
fn effect_parameters_keep_both_ends_of_the_after_effects_range() {
|
|
let (mut s, low, high) = session();
|
|
for &(issue, e, p, lo, hi) in ROWS {
|
|
assert_eq!(set(&mut s, high, e, p, lo), json!(lo), "#{issue} {e} {p}: prop.set reply");
|
|
assert_eq!(read(&mut s, high, e, p), lo, "#{issue} {e} {p}");
|
|
set(&mut s, high, e, p, hi);
|
|
assert_eq!(read(&mut s, high, e, p), hi, "#{issue} {e} {p}");
|
|
assert!(s.undo());
|
|
assert_eq!(read(&mut s, high, e, p), lo, "#{issue} {e} {p}: undo");
|
|
assert!(s.redo());
|
|
assert_eq!(read(&mut s, high, e, p), hi, "#{issue} {e} {p}: redo");
|
|
set(&mut s, low, e, p, lo);
|
|
}
|
|
let dir = std::env::temp_dir().join(format!("ec-effect-ranges-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let file = dir.join("ranges.ecproj").to_string_lossy().to_string();
|
|
s.execute("file.save", json!({"path": file})).unwrap();
|
|
let mut s2 = Session::default();
|
|
s2.execute("file.open", json!({"path": file})).unwrap();
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
for &(issue, e, p, lo, hi) in ROWS {
|
|
assert_eq!(read(&mut s2, low, e, p), lo, "#{issue} {e} {p}: reopened");
|
|
assert_eq!(read(&mut s2, high, e, p), hi, "#{issue} {e} {p}: reopened");
|
|
}
|
|
}
|
|
|
|
/// A project saved while these ranges were narrower stored that range with each instance
|
|
/// (`ParamUi`) and values clamped to it: opening it takes the effect's current range and keeps
|
|
/// the stored values, so the new ends can be set.
|
|
#[test]
|
|
fn projects_saved_with_the_narrower_ranges_open_with_the_new_ones() {
|
|
let (mut s, low, _) = session();
|
|
let cid = s.active_comp_id().unwrap();
|
|
let mut old = (*s.project).clone();
|
|
let stored: Vec<f64> = ROWS.iter().map(|&(_, e, p, ..)| read(&mut s, low, e, p)).collect();
|
|
{
|
|
let effectcraft_project::ItemKind::Comp(comp) = &mut old.items.get_mut(&cid).unwrap().kind else { panic!("not a composition") };
|
|
let l = std::sync::Arc::make_mut(comp).layers.find_mut(|l| l.id == LayerId(low)).unwrap();
|
|
for (&(_, e, p, ..), v) in ROWS.iter().zip(&stored) {
|
|
let g = l.props.sub_mut("effects").unwrap().children.iter_mut().find_map(|n| match n {
|
|
Node::Group(g) if g.match_id == e => Some(g),
|
|
_ => None,
|
|
});
|
|
let pr = prop_mut(g.unwrap(), p).unwrap_or_else(|| panic!("{e} {p}"));
|
|
// The narrowest stored range that still holds the stored value.
|
|
pr.ui = ParamUi::Slider { min: *v, max: *v, slider_min: *v, slider_max: *v, decimals: 1 };
|
|
}
|
|
}
|
|
let dir = std::env::temp_dir().join(format!("ec-effect-ranges-old-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let file = dir.join("old.ecproj").to_string_lossy().to_string();
|
|
std::fs::write(&file, old.to_json()).unwrap();
|
|
let mut s2 = Session::default();
|
|
s2.execute("file.open", json!({"path": file})).unwrap();
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
for (&(issue, e, p, lo, hi), v) in ROWS.iter().zip(&stored) {
|
|
assert_eq!(read(&mut s2, low, e, p), *v, "#{issue} {e} {p}: the stored value is kept");
|
|
let spec = effectcraft_effects::find(e).unwrap().params.iter().find(|q| q.id == p).unwrap();
|
|
let uid = s2.execute("prop.get", json!({"layer": low, "path": path(e, p)})).unwrap()["uid"].as_u64().unwrap();
|
|
let layer = s2.project.comp(cid).unwrap().layer(LayerId(low)).unwrap();
|
|
assert_eq!(layer.props.find(uid).unwrap().ui, spec.ui, "#{issue} {e} {p}: the range is refreshed");
|
|
set(&mut s2, low, e, p, lo);
|
|
assert_eq!(read(&mut s2, low, e, p), lo, "#{issue} {e} {p}");
|
|
set(&mut s2, low, e, p, hi);
|
|
assert_eq!(read(&mut s2, low, e, p), hi, "#{issue} {e} {p}");
|
|
}
|
|
}
|
|
|
|
/// The property at `path` (`group/param`) in an effect instance.
|
|
fn prop_mut<'a>(g: &'a mut PropGroup, path: &str) -> Option<&'a mut effectcraft_project::Property> {
|
|
match path.split_once('/') {
|
|
None => g.get_mut(path),
|
|
Some((first, rest)) => prop_mut(g.sub_mut(first)?, rest),
|
|
}
|
|
}
|