mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
M9.13: Camera Lens Blur fringe / blur map, Colorama full controls, Selective Color Details
- Camera Lens Blur: Diffraction Fringe, Blur Map (layer, channel, placement, focal distance, invert) with per-pixel radius. - Colorama: Add Phase (layer, from, mode), preset / custom Output Cycle palettes, Modify, Modify Alpha, Change Empty Pixels, Pixel Selection, Masking, Composite Over Layer. - Selective Color: Colors popup and Details twirl-downs (older saves migrate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3a7c9c5f61
commit
413e3ea97d
+176
-52
@@ -284,15 +284,9 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
}
|
||||
let shape = ctx.params.e("irisProperties/irisShape");
|
||||
let sides = if shape >= 8 { 0 } else { shape + 3 };
|
||||
let mut spans = iris_spans(r, sides, ctx.params.f("irisProperties/irisRotation"), ctx.params.f("irisProperties/irisRoundness").clamp(0.0, 100.0) / 100.0);
|
||||
let roundness = ctx.params.f("irisProperties/irisRoundness").clamp(0.0, 100.0) / 100.0;
|
||||
// Aspect Ratio stretches the iris horizontally (> 1) or squeezes it (< 1).
|
||||
let aspect = ctx.params.f("irisProperties/irisAspectRatio");
|
||||
if aspect > 0.0 && (aspect - 1.0).abs() > 1e-6 {
|
||||
for s in spans.iter_mut() {
|
||||
s.1 = (s.1 as f64 * aspect).round() as i64;
|
||||
s.2 = (s.2 as f64 * aspect).round() as i64;
|
||||
}
|
||||
}
|
||||
let reach = r * aspect.max(1.0);
|
||||
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
|
||||
if !repeat {
|
||||
@@ -342,58 +336,148 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
let count: i64 = spans.iter().map(|(_, l, r)| r - l + 1).sum();
|
||||
let norm = 1.0 / count as f64;
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
out.rows_mut().for_each(|(y, row)| {
|
||||
for (x, o) in row.iter_mut().enumerate() {
|
||||
let mut acc = [0.0f64; 4];
|
||||
for &(dy, l, rr) in &spans {
|
||||
let mut yy = y as i64 + dy;
|
||||
if yy < 0 || yy >= h as i64 {
|
||||
if !repeat {
|
||||
continue;
|
||||
}
|
||||
yy = yy.clamp(0, h as i64 - 1);
|
||||
// Sum of the source over the iris spans centred on (x, y).
|
||||
let span_sum = |spans: &[(i64, i64, i64)], x: usize, y: usize| -> [f64; 4] {
|
||||
let mut acc = [0.0f64; 4];
|
||||
for &(dy, l, rr) in spans {
|
||||
let mut yy = y as i64 + dy;
|
||||
if yy < 0 || yy >= h as i64 {
|
||||
if !repeat {
|
||||
continue;
|
||||
}
|
||||
let pre = &prefix[yy as usize];
|
||||
let (mut a, mut bnd) = (x as i64 + l, x as i64 + rr);
|
||||
if repeat {
|
||||
if a < 0 {
|
||||
let n = (-a).min(bnd - a + 1) as f64;
|
||||
let first = src.data[yy as usize * w];
|
||||
for c in 0..4 {
|
||||
acc[c] += first[c] as f64 * n;
|
||||
}
|
||||
a = 0;
|
||||
}
|
||||
if bnd >= w as i64 {
|
||||
let n = (bnd - (w as i64 - 1)).min(bnd - a + 1) as f64;
|
||||
let last = src.data[yy as usize * w + w - 1];
|
||||
for c in 0..4 {
|
||||
acc[c] += last[c] as f64 * n;
|
||||
}
|
||||
bnd = w as i64 - 1;
|
||||
}
|
||||
} else {
|
||||
a = a.max(0);
|
||||
bnd = bnd.min(w as i64 - 1);
|
||||
}
|
||||
if a <= bnd {
|
||||
yy = yy.clamp(0, h as i64 - 1);
|
||||
}
|
||||
let pre = &prefix[yy as usize];
|
||||
let (mut a, mut bnd) = (x as i64 + l, x as i64 + rr);
|
||||
if repeat {
|
||||
if a < 0 {
|
||||
let n = (-a).min(bnd - a + 1) as f64;
|
||||
let first = src.data[yy as usize * w];
|
||||
for c in 0..4 {
|
||||
acc[c] += pre[bnd as usize + 1][c] - pre[a as usize][c];
|
||||
acc[c] += first[c] as f64 * n;
|
||||
}
|
||||
a = 0;
|
||||
}
|
||||
if bnd >= w as i64 {
|
||||
let n = (bnd - (w as i64 - 1)).min(bnd - a + 1) as f64;
|
||||
let last = src.data[yy as usize * w + w - 1];
|
||||
for c in 0..4 {
|
||||
acc[c] += last[c] as f64 * n;
|
||||
}
|
||||
bnd = w as i64 - 1;
|
||||
}
|
||||
} else {
|
||||
a = a.max(0);
|
||||
bnd = bnd.min(w as i64 - 1);
|
||||
}
|
||||
if a <= bnd {
|
||||
for c in 0..4 {
|
||||
acc[c] += pre[bnd as usize + 1][c] - pre[a as usize][c];
|
||||
}
|
||||
}
|
||||
let mut px = acc.map(|v| (v * norm) as f32);
|
||||
px[3] = px[3].clamp(0.0, 1.0);
|
||||
if linear {
|
||||
let (c, a) = unpremul(px);
|
||||
px = premul(c.map(|v| effectcraft_color::linear_to_srgb(v.max(0.0))), a);
|
||||
}
|
||||
*o = px;
|
||||
}
|
||||
acc
|
||||
};
|
||||
// Diffraction Fringe: the iris edge (outer 20 % of the radius) gets more of the energy:
|
||||
// 0 = an even disc, 100 = a natural halo, 500 = all of it in the ring.
|
||||
let fringe = (ctx.params.f("irisProperties/diffractionFringe") / 500.0).clamp(0.0, 1.0);
|
||||
let (w_in, w_ring) = (1.0 - fringe, 1.0 + 4.0 * fringe);
|
||||
let iris = |radius: f64| -> (Vec<(i64, i64, i64)>, Vec<(i64, i64, i64)>) {
|
||||
let stretch = |mut s: Vec<(i64, i64, i64)>| {
|
||||
if aspect > 0.0 && (aspect - 1.0).abs() > 1e-6 {
|
||||
for v in s.iter_mut() {
|
||||
v.1 = (v.1 as f64 * aspect).round() as i64;
|
||||
v.2 = (v.2 as f64 * aspect).round() as i64;
|
||||
}
|
||||
}
|
||||
s
|
||||
};
|
||||
let outer = stretch(iris_spans(radius, sides, ctx.params.f("irisProperties/irisRotation"), roundness));
|
||||
let inner = if fringe > 0.0 && radius * 0.8 >= 1.0 {
|
||||
stretch(iris_spans(radius * 0.8, sides, ctx.params.f("irisProperties/irisRotation"), roundness))
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
(outer, inner)
|
||||
};
|
||||
let count = |s: &[(i64, i64, i64)]| s.iter().map(|(_, l, r)| (r - l + 1) as f64).sum::<f64>();
|
||||
// One blurred pixel with the given iris.
|
||||
let blur_px = |sp: &(Vec<(i64, i64, i64)>, Vec<(i64, i64, i64)>), x: usize, y: usize| -> [f32; 4] {
|
||||
let (outer, inner) = sp;
|
||||
let acc = if inner.is_empty() {
|
||||
let n = count(outer).max(1.0);
|
||||
span_sum(outer, x, y).map(|v| v / n)
|
||||
} else {
|
||||
let (so, si) = (span_sum(outer, x, y), span_sum(inner, x, y));
|
||||
let (no, ni) = (count(outer), count(inner));
|
||||
let norm = (w_in * ni + w_ring * (no - ni)).max(1e-9);
|
||||
std::array::from_fn(|c| (w_in * si[c] + w_ring * (so[c] - si[c])) / norm)
|
||||
};
|
||||
let mut px = acc.map(|v| v as f32);
|
||||
px[3] = px[3].clamp(0.0, 1.0);
|
||||
if linear {
|
||||
let (c, a) = unpremul(px);
|
||||
px = premul(c.map(|v| effectcraft_color::linear_to_srgb(v.max(0.0))), a);
|
||||
}
|
||||
px
|
||||
};
|
||||
// Blur Map: per-pixel radius from a map layer (no blur at Blur Focal Distance, full radius
|
||||
// at the farthest value).
|
||||
let map = ctx.layer_param("blurMap/blurMapLayer", true).map(|o| {
|
||||
let stretch = ctx.params.e("blurMap/placement") == 1;
|
||||
let img = crate::util::fit_layer(ctx, &b, &o, stretch);
|
||||
let ch = ctx.params.e("blurMap/channel");
|
||||
let invert = ctx.params.b("blurMap/invertBlurMap");
|
||||
let focal = (ctx.params.f("blurMap/blurFocalDistance") / 255.0).clamp(0.0, 1.0) as f32;
|
||||
let span = focal.max(1.0 - focal).max(1e-3);
|
||||
img.data
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (c, a) = unpremul(*p);
|
||||
let mut v = match ch {
|
||||
1 => a,
|
||||
2 => c[0].max(c[1]).max(c[2]) * a,
|
||||
_ => luminance(c[0], c[1], c[2]) * a,
|
||||
};
|
||||
if invert {
|
||||
v = 1.0 - v;
|
||||
}
|
||||
((v - focal).abs() / span).clamp(0.0, 1.0)
|
||||
})
|
||||
.collect::<Vec<f32>>()
|
||||
});
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
match &map {
|
||||
None => {
|
||||
let sp = iris(r);
|
||||
out.rows_mut().for_each(|(y, row)| {
|
||||
for (x, o) in row.iter_mut().enumerate() {
|
||||
*o = blur_px(&sp, x, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(m) => {
|
||||
// Radii quantised to levels; each pixel blends the two nearest.
|
||||
let levels = (r.ceil() as usize).clamp(1, 8);
|
||||
let irises: Vec<_> = (0..=levels).map(|k| iris(r * k as f64 / levels as f64)).collect();
|
||||
let srcd = &b.img.data;
|
||||
out.rows_mut().for_each(|(y, row)| {
|
||||
for (x, o) in row.iter_mut().enumerate() {
|
||||
let f = m[y * w + x] * levels as f32;
|
||||
let k0 = (f.floor() as usize).min(levels);
|
||||
let t = f - k0 as f32;
|
||||
let at = |k: usize| if k == 0 || r * k as f64 / (levels as f64) < 0.5 { srcd[y * w + x] } else { blur_px(&irises[k], x, y) };
|
||||
let a = at(k0);
|
||||
*o = if t > 1e-4 && k0 < levels {
|
||||
let bq = at(k0 + 1);
|
||||
std::array::from_fn(|c| a[c] + (bq[c] - a[c]) * t)
|
||||
} else {
|
||||
a
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
@@ -598,6 +682,12 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("irisProperties/irisRoundness", "Roundness", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("irisProperties/irisAspectRatio", "Aspect Ratio", num(1.0), slider(0.01, 100.0, 0.25, 4.0, 2)),
|
||||
p("irisProperties/irisRotation", "Rotation", num(0.0), ParamUi::Angle),
|
||||
p("irisProperties/diffractionFringe", "Diffraction Fringe", num(0.0), slider(0.0, 500.0, 0.0, 500.0, 1)),
|
||||
p("blurMap/blurMapLayer", "Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("blurMap/channel", "Channel", Value::Enum(0), popup(&["Luminance", "Alpha", "Color"])),
|
||||
p("blurMap/placement", "Placement", Value::Enum(1), popup(&["Center", "Stretch"])),
|
||||
p("blurMap/blurFocalDistance", "Blur Focal Distance", num(0.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||||
p("blurMap/invertBlurMap", "Invert Blur Map", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("highlight/specularBrightness", "Gain", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("highlight/specularThreshold", "Threshold", num(255.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||||
p("highlight/highlightSaturation", "Saturation", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
@@ -816,4 +906,38 @@ mod tests {
|
||||
let pad = (out.img.width - 16) / 2;
|
||||
assert!(out.img.get(pad as i64, pad as i64)[0] < 0.005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_lens_blur_diffraction_fringe_and_blur_map() {
|
||||
use crate::{EffectEnv, EffectHost, LayerPixels};
|
||||
// A single bright point spreads into a disc; with Diffraction Fringe its rim is brighter.
|
||||
let mut dot = Image::filled(41, 41, [0.0, 0.0, 0.0, 1.0]);
|
||||
dot.set(20, 20, [1.0, 1.0, 1.0, 1.0]);
|
||||
let v = [("blurRadius", num(10.0)), ("irisProperties/irisShape", Value::Enum(8)), ("repeatEdge", Value::Bool(true))];
|
||||
let plain = run("ec.blur.cameralens", &dot, &v).img;
|
||||
let ring = run("ec.blur.cameralens", &dot, &[v.as_slice(), &[("irisProperties/diffractionFringe", num(300.0))]].concat()).img;
|
||||
assert!((plain.get(20, 20)[0] - plain.get(29, 20)[0]).abs() < 1e-5, "even disc");
|
||||
assert!(ring.get(29, 20)[0] > ring.get(20, 20)[0] * 2.0, "{:?} {:?}", ring.get(29, 20), ring.get(20, 20));
|
||||
// Blur Map: black (focal distance 0) stays sharp, white gets the full radius.
|
||||
struct Half;
|
||||
impl EffectHost for Half {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||||
let mut m = Image::filled(40, 20, [0.0, 0.0, 0.0, 1.0]);
|
||||
for y in 0..20 {
|
||||
for x in 20..40 {
|
||||
m.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
Some(LayerPixels { buf: Buf { img: m, offset: [0.0; 2], scale: 1.0 }, size: [40.0, 20.0] })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
let stripes = crate::util::gen_image(40, 20, |x, _| if x % 4 < 2 { [1.0, 1.0, 1.0, 1.0] } else { [0.0, 0.0, 0.0, 1.0] });
|
||||
let vals = [("blurRadius", num(6.0)), ("repeatEdge", Value::Bool(true)), ("blurMap/blurMapLayer", Value::Layer(Some(1)))];
|
||||
let out = crate::run_fx("ec.blur.cameralens", &vals, stripes.clone(), 0.0, EffectEnv { host: Some(&Half), ..Default::default() }).img;
|
||||
assert_eq!(out.get(4, 10), stripes.get(4, 10), "sharp where the map is at the focal distance");
|
||||
assert!((out.get(30, 10)[0] - 0.5).abs() < 0.2, "{:?}", out.get(30, 10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ use crate::{CATEGORIES, registry};
|
||||
/// `(effect id, what is missing)`. Everything else in the registry is implemented in full as far
|
||||
/// as the public behaviour documentation describes it.
|
||||
pub const PARTIAL: &[(&str, &str)] = &[
|
||||
("ec.blur.cameralens", "no Diffraction Fringe or Blur Map layer"),
|
||||
("ec.color.colorama", "no Add Phase, editable Output Cycle, Modify, Pixel Selection or Masking"),
|
||||
("ec.color.selectivecolor", "simplified Colors / Details layout"),
|
||||
("ec.distort.reshape", "no correspondence points"),
|
||||
("ec.distort.rollingshutterrepair", "Pixel Motion Detail has no effect"),
|
||||
("ec.generate.advancedlightning", "no Alpha Obstacle; most Expert Settings missing"),
|
||||
@@ -61,6 +58,13 @@ pub fn param_shown(effect: &str, param: &str, value: &dyn Fn(&str) -> Option<eff
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
"ec.color.selectivecolor" => match param.strip_prefix("details/") {
|
||||
Some(rest) => {
|
||||
let g = rest.split('/').next().unwrap_or("");
|
||||
crate::color2::SC_GROUPS.iter().position(|x| *x == g).is_none_or(|i| i == e("colors") as usize)
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
"ec.sim.carddance" | "ec.transition.cardwipe" => crate::card3d::shown(param, &e).unwrap_or(true),
|
||||
"ec.sim.shatter" => match param {
|
||||
"shape/customShatterMap" | "shape/whiteTilesFixed" => e("shape/pattern") == 5,
|
||||
@@ -143,6 +147,34 @@ mod tests {
|
||||
assert!(have == want, "docs/effects.md is stale: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn popups_choose_the_shown_controls() {
|
||||
use effectcraft_keyframe::Value;
|
||||
let with = |pairs: &'static [(&'static str, u32)]| move |id: &str| pairs.iter().find(|(k, _)| *k == id).map(|(_, v)| Value::Enum(*v));
|
||||
let shown = |e: &str, p: &str, pairs: &'static [(&'static str, u32)]| super::param_shown(e, p, &with(pairs));
|
||||
// Levels: RGB shows the master controls only; Red shows the red ones.
|
||||
assert!(shown("ec.color.levels", "inBlack", &[]));
|
||||
assert!(!shown("ec.color.levels", "redInBlack", &[]));
|
||||
assert!(shown("ec.color.levels", "redInBlack", &[("channel", 1)]));
|
||||
assert!(!shown("ec.color.levels", "inBlack", &[("channel", 1)]));
|
||||
assert!(shown("ec.color.levels", "channel", &[("channel", 1)]));
|
||||
// Hue/Saturation: Master or one colour range.
|
||||
assert!(shown("ec.color.huesaturation", "hue", &[]));
|
||||
assert!(shown("ec.color.huesaturation", "bluesHue", &[("channelControl", 5)]));
|
||||
assert!(!shown("ec.color.huesaturation", "redsHue", &[("channelControl", 5)]));
|
||||
// Card effects: the camera system's group.
|
||||
assert!(shown("ec.sim.carddance", "cameraPosition", &[]));
|
||||
assert!(!shown("ec.sim.carddance", "cornerPins", &[]));
|
||||
assert!(shown("ec.transition.cardwipe", "cornerPins/upperLeftCorner", &[("cameraSystem", 1)]));
|
||||
assert!(!shown("ec.sim.shatter", "shape/customShatterMap", &[]));
|
||||
// Selective Color: the chosen range's Details group.
|
||||
assert!(shown("ec.color.selectivecolor", "details/reds", &[]));
|
||||
assert!(!shown("ec.color.selectivecolor", "details/blues/bluesCyan", &[]));
|
||||
assert!(shown("ec.color.selectivecolor", "details", &[]));
|
||||
// Everything else is always shown.
|
||||
assert!(shown("ec.blur.gaussian", "blurriness", &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_notes_name_registered_effects() {
|
||||
for (id, note) in super::PARTIAL {
|
||||
|
||||
@@ -504,6 +504,16 @@ const SC_IDS: [[&str; 4]; 9] = [
|
||||
["neutralsCyan", "neutralsMagenta", "neutralsYellow", "neutralsBlack"],
|
||||
["blacksCyan", "blacksMagenta", "blacksYellow", "blacksBlack"],
|
||||
];
|
||||
/// Selective Color's Colors popup (the range whose controls Effect Controls shows) and the
|
||||
/// Details twirl-down group of each range.
|
||||
pub const SC_COLORS: [&str; 9] = ["Reds", "Yellows", "Greens", "Cyans", "Blues", "Magentas", "Whites", "Neutrals", "Blacks"];
|
||||
pub const SC_GROUPS: [&str; 9] = ["reds", "yellows", "greens", "cyans", "blues", "magentas", "whites", "neutrals", "blacks"];
|
||||
|
||||
/// Spec id of range `r`'s control `leaf` (`details/reds/redsCyan`).
|
||||
fn sc_param_id(r: usize, leaf: &str) -> String {
|
||||
format!("details/{}/{leaf}", SC_GROUPS[r])
|
||||
}
|
||||
|
||||
const SC_NAMES: [[&str; 4]; 9] = [
|
||||
["Reds Cyan", "Reds Magenta", "Reds Yellow", "Reds Black"],
|
||||
["Yellows Cyan", "Yellows Magenta", "Yellows Yellow", "Yellows Black"],
|
||||
@@ -534,7 +544,7 @@ fn sc_weights(c: [f32; 3]) -> [f32; 9] {
|
||||
}
|
||||
|
||||
fn selective_color(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let adj = SC_IDS.map(|ids| ids.map(|id| ctx.params.f(id) as f32 / 100.0));
|
||||
let adj: [[f32; 4]; 9] = std::array::from_fn(|r| SC_IDS[r].map(|id| ctx.params.f(&sc_param_id(r, id)) as f32 / 100.0));
|
||||
if adj.iter().all(|r| r.iter().all(|v| *v == 0.0)) {
|
||||
return b;
|
||||
}
|
||||
@@ -882,10 +892,12 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
}
|
||||
lv_params.push(p("clipToOutputBlack", "Clip To Output Black", Value::Enum(2), popup(&crate::color_fx::LEVELS_CLIP)));
|
||||
lv_params.push(p("clipToOutputWhite", "Clip To Output White", Value::Enum(2), popup(&crate::color_fx::LEVELS_CLIP)));
|
||||
let mut sc_params = vec![p("method", "Method", Value::Enum(0), popup(&["Relative", "Absolute"]))];
|
||||
for (ids, names) in SC_IDS.iter().zip(SC_NAMES.iter()) {
|
||||
let mut sc_params = vec![p("method", "Method", Value::Enum(0), popup(&["Relative", "Absolute"])), p("colors", "Colors", Value::Enum(0), popup(&SC_COLORS))];
|
||||
// Details: every range's Cyan / Magenta / Yellow / Black in its own twirl-down.
|
||||
let leak = |s: String| -> &'static str { Box::leak(s.into_boxed_str()) };
|
||||
for (r, (ids, names)) in SC_IDS.iter().zip(SC_NAMES.iter()).enumerate() {
|
||||
for k in 0..4 {
|
||||
sc_params.push(p(ids[k], names[k], num(0.0), pct()));
|
||||
sc_params.push(p(leak(sc_param_id(r, ids[k])), names[k], num(0.0), pct()));
|
||||
}
|
||||
}
|
||||
let clip_params = |extra: bool| {
|
||||
@@ -1146,10 +1158,10 @@ mod tests {
|
||||
let img = ramp();
|
||||
assert_eq!(run("ec.color.selectivecolor", &img, &[]), img);
|
||||
let red = Image::filled(2, 2, [1.0, 0.1, 0.1, 1.0]);
|
||||
let out = run("ec.color.selectivecolor", &red, &[("redsCyan", num(100.0)), ("method", Value::Enum(1))]);
|
||||
let out = run("ec.color.selectivecolor", &red, &[("details/reds/redsCyan", num(100.0)), ("method", Value::Enum(1))]);
|
||||
assert!(out.data[0][0] < 0.3, "{:?}", out.data[0]);
|
||||
let blue = Image::filled(2, 2, [0.1, 0.1, 1.0, 1.0]);
|
||||
assert_eq!(run("ec.color.selectivecolor", &blue, &[("redsCyan", num(100.0))]), blue);
|
||||
assert_eq!(run("ec.color.selectivecolor", &blue, &[("details/reds/redsCyan", num(100.0))]), blue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+254
-17
@@ -634,28 +634,204 @@ fn colorama_phase(c: [f32; 3], a: f32, mode: u32) -> f32 {
|
||||
v.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// Colorama with the built-in hue-cycle output palette: the chosen input attribute, shifted by
|
||||
/// Phase Shift and repeated Cycle Repetitions times, picks a colour on the cycle (Interpolate
|
||||
/// Palette off posterizes to the palette's six colours).
|
||||
/// Colorama's Output Cycle presets (our own palettes; Custom uses the Palette parameter).
|
||||
pub const COLORAMA_PRESETS: [&str; 9] = ["Hue Cycle", "Ramp Grayscale", "Fire", "Ice", "Ramp Red", "Ramp Green", "Ramp Blue", "Pastel Cycle", "Custom"];
|
||||
/// Colorama's Modify options: which channels of the pixel take the palette colour.
|
||||
const COLORAMA_MODIFY: [&str; 12] =
|
||||
["All", "Red", "Green", "Blue", "Hue", "Lightness", "Saturation", "Value", "Red & Green", "Green & Blue", "Blue & Red", "None"];
|
||||
|
||||
/// An output cycle: colour stops around the wheel (position 0..1, straight RGBA), cyclic.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ColoramaPalette {
|
||||
pub stops: Vec<(f32, [f32; 4])>,
|
||||
}
|
||||
|
||||
impl ColoramaPalette {
|
||||
/// Built-in palette `i` of [`COLORAMA_PRESETS`].
|
||||
pub fn preset(i: u32) -> ColoramaPalette {
|
||||
let s = |v: &[(f32, [f32; 3])]| ColoramaPalette { stops: v.iter().map(|(p, c)| (*p, [c[0], c[1], c[2], 1.0])).collect() };
|
||||
match i {
|
||||
1 => s(&[(0.0, [0.0; 3]), (0.999, [1.0; 3])]),
|
||||
2 => s(&[(0.0, [0.0, 0.0, 0.0]), (0.3, [0.7, 0.05, 0.0]), (0.6, [1.0, 0.55, 0.0]), (0.85, [1.0, 0.95, 0.4]), (0.999, [1.0, 1.0, 1.0])]),
|
||||
3 => s(&[(0.0, [0.0, 0.0, 0.1]), (0.4, [0.1, 0.3, 0.8]), (0.75, [0.5, 0.85, 1.0]), (0.999, [1.0, 1.0, 1.0])]),
|
||||
4 => s(&[(0.0, [0.0; 3]), (0.999, [1.0, 0.0, 0.0])]),
|
||||
5 => s(&[(0.0, [0.0; 3]), (0.999, [0.0, 1.0, 0.0])]),
|
||||
6 => s(&[(0.0, [0.0; 3]), (0.999, [0.0, 0.0, 1.0])]),
|
||||
7 => s(&[(0.0, [1.0, 0.7, 0.7]), (1.0 / 3.0, [0.7, 1.0, 0.7]), (2.0 / 3.0, [0.7, 0.7, 1.0])]),
|
||||
_ => s(&[
|
||||
(0.0, [1.0, 0.0, 0.0]),
|
||||
(1.0 / 6.0, [1.0, 1.0, 0.0]),
|
||||
(2.0 / 6.0, [0.0, 1.0, 0.0]),
|
||||
(3.0 / 6.0, [0.0, 1.0, 1.0]),
|
||||
(4.0 / 6.0, [0.0, 0.0, 1.0]),
|
||||
(5.0 / 6.0, [1.0, 0.0, 1.0]),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse "pos:r,g,b[,a] pos:r,g,b …" (positions 0..1, channels 0..1). `None` without stops.
|
||||
pub fn parse(s: &str) -> Option<ColoramaPalette> {
|
||||
let mut stops: Vec<(f32, [f32; 4])> = s
|
||||
.split_whitespace()
|
||||
.filter_map(|t| {
|
||||
let (pos, c) = t.split_once(':')?;
|
||||
let v: Vec<f32> = c.split(',').filter_map(|x| x.trim().parse().ok()).collect();
|
||||
(v.len() >= 3).then(|| (pos.trim().parse::<f32>().ok().map(|p| p.rem_euclid(1.0)), [v[0], v[1], v[2], v.get(3).copied().unwrap_or(1.0)]))
|
||||
})
|
||||
.filter_map(|(p, c)| p.map(|p| (p, c)))
|
||||
.collect();
|
||||
stops.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
(!stops.is_empty()).then_some(ColoramaPalette { stops })
|
||||
}
|
||||
|
||||
/// The colour at phase `ph` (0..1). Interpolate Palette blends between neighbouring stops
|
||||
/// (around the wheel); off, each phase takes the stop at or before it.
|
||||
pub fn eval(&self, ph: f32, interpolate: bool) -> [f32; 4] {
|
||||
let n = self.stops.len();
|
||||
let ph = ph.rem_euclid(1.0);
|
||||
let k = self.stops.iter().rposition(|s| s.0 <= ph).unwrap_or(n - 1);
|
||||
let (p0, c0) = self.stops[k];
|
||||
if !interpolate || n == 1 {
|
||||
return c0;
|
||||
}
|
||||
let (p1, c1) = self.stops[(k + 1) % n];
|
||||
let span = (p1 - p0).rem_euclid(1.0).max(1e-6);
|
||||
let t = ((ph - p0).rem_euclid(1.0) / span).clamp(0.0, 1.0);
|
||||
std::array::from_fn(|i| c0[i] + (c1[i] - c0[i]) * t)
|
||||
}
|
||||
}
|
||||
|
||||
/// How well colour `c` matches `key` (1 = matches) for Pixel Selection's Matching Mode
|
||||
/// (1 RGB, 2 Hue, 3 Chroma), with Matching Tolerance / Softness (0..1).
|
||||
fn colorama_match(c: [f32; 3], key: [f32; 3], mode: u32, tol: f32, soft: f32) -> f32 {
|
||||
let d = match mode {
|
||||
2 => {
|
||||
let (h1, s1, _) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
let (h2, _, _) = rgb_to_hsl(key[0], key[1], key[2]);
|
||||
let dh = (h1 - h2).abs();
|
||||
dh.min(1.0 - dh) * 2.0 + (1.0 - s1.min(1.0)) * 0.5
|
||||
}
|
||||
3 => {
|
||||
// Chroma: the colour without its brightness.
|
||||
let n = |c: [f32; 3]| {
|
||||
let s = c[0] + c[1] + c[2];
|
||||
if s > 1e-5 { [c[0] / s, c[1] / s, c[2] / s] } else { [1.0 / 3.0; 3] }
|
||||
};
|
||||
let (a, b) = (n(c), n(key));
|
||||
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt() * 1.5
|
||||
}
|
||||
_ => ((c[0] - key[0]).powi(2) + (c[1] - key[1]).powi(2) + (c[2] - key[2]).powi(2)).sqrt() / 3f32.sqrt(),
|
||||
};
|
||||
if d <= tol {
|
||||
1.0
|
||||
} else if soft > 0.0 {
|
||||
(1.0 - (d - tol) / soft).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Colorama: a phase taken from the pixel (Get Phase From, optionally combined with an Add
|
||||
/// Phase layer), shifted and repeated around the Output Cycle, picks a palette colour, which
|
||||
/// replaces the channels Modify names. Pixel Selection limits it to colours near Matching
|
||||
/// Color; Masking to where a mask layer allows; Blend With Original mixes the result back.
|
||||
fn colorama(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let shift = ctx.params.f("inputPhase/phaseShift") as f32 / 360.0;
|
||||
let mode = ctx.params.e("inputPhase/mode");
|
||||
let cycles = ctx.params.f("outputCycle/cycles").max(0.0) as f32;
|
||||
let smooth = ctx.params.get("outputCycle/interpolatePalette").is_none_or(|v| v.as_bool());
|
||||
let blend = 1.0 - ctx.params.f("blend") as f32 / 100.0;
|
||||
b.img.data.iter_mut().for_each(|px| {
|
||||
let pr = ctx.params;
|
||||
let shift = pr.f("inputPhase/phaseShift") as f32 / 360.0;
|
||||
let mode = pr.e("inputPhase/mode");
|
||||
let cycles = pr.f("outputCycle/cycles").max(0.0) as f32;
|
||||
let smooth = pr.get("outputCycle/interpolatePalette").is_none_or(|v| v.as_bool());
|
||||
let preset = pr.e("outputCycle/usePresetPalette");
|
||||
let palette = if preset as usize == COLORAMA_PRESETS.len() - 1 { ColoramaPalette::parse(pr.s("outputCycle/palette")) } else { None }
|
||||
.unwrap_or_else(|| ColoramaPalette::preset(preset));
|
||||
let blend = 1.0 - pr.f("blend") as f32 / 100.0;
|
||||
// Add Phase.
|
||||
let add = ctx.layer_param("inputPhase/addPhase", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true));
|
||||
let add_from = pr.e("inputPhase/addPhaseFrom");
|
||||
let add_mode = pr.e("inputPhase/addMode");
|
||||
// Modify.
|
||||
let modify = pr.e("modify/modify");
|
||||
let modify_alpha = pr.b("modify/modifyAlpha");
|
||||
let change_empty = pr.b("modify/changeEmptyPixels");
|
||||
// Pixel Selection.
|
||||
let match_mode = pr.e("pixelSelection/matchingMode");
|
||||
let key = pr.color("pixelSelection/matchingColor");
|
||||
let tol = (pr.f("pixelSelection/matchingTolerance") / 100.0) as f32;
|
||||
let soft = (pr.f("pixelSelection/matchingSoftness") / 100.0) as f32;
|
||||
// Masking.
|
||||
let mask_mode = pr.e("masking/maskingMode");
|
||||
let mask = (mask_mode != 0).then(|| ctx.layer_param("masking/maskLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true))).flatten();
|
||||
let composite = pr.get("masking/compositeOverLayer").is_none_or(|v| v.as_bool());
|
||||
use rayon::prelude::*;
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let a = px[3];
|
||||
if a <= 0.0 {
|
||||
if a <= 0.0 && !change_empty {
|
||||
return;
|
||||
}
|
||||
let c = [px[0] / a, px[1] / a, px[2] / a];
|
||||
let mut ph = (colorama_phase(c, a, mode) * cycles + shift).rem_euclid(1.0);
|
||||
if !smooth {
|
||||
ph = (ph * 6.0).floor() / 6.0;
|
||||
let c = if a > 0.0 { [px[0] / a, px[1] / a, px[2] / a] } else { [0.0; 3] };
|
||||
let mut p = colorama_phase(c, a, mode);
|
||||
if let Some(img) = &add {
|
||||
let (ac, aa) = crate::util::unpremul(img.data[i]);
|
||||
let q = colorama_phase(ac, aa, add_from);
|
||||
p = match add_mode {
|
||||
1 => (p + q).min(1.0),
|
||||
2 => (p + q) * 0.5,
|
||||
3 => p + q - p * q,
|
||||
_ => (p + q).rem_euclid(1.0),
|
||||
};
|
||||
}
|
||||
let (r, g, bl) = effectcraft_color::hsv_to_rgb(ph, 1.0, 1.0);
|
||||
let o = mix(c, [r, g, bl], blend);
|
||||
*px = [o[0] * a, o[1] * a, o[2] * a, a];
|
||||
let ph = (p * cycles + shift).rem_euclid(1.0);
|
||||
let pc = palette.eval(ph, smooth);
|
||||
let (h, s, l) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
let (ph_, ps, pl) = rgb_to_hsl(pc[0], pc[1], pc[2]);
|
||||
let hl = |h: f32, s: f32, l: f32| {
|
||||
let (r, g, bb) = hsl_to_rgb(h, s, l);
|
||||
[r, g, bb]
|
||||
};
|
||||
let new: [f32; 3] = match modify {
|
||||
1 => [pc[0], c[1], c[2]],
|
||||
2 => [c[0], pc[1], c[2]],
|
||||
3 => [c[0], c[1], pc[2]],
|
||||
4 => hl(ph_, s, l),
|
||||
5 => hl(h, s, pl),
|
||||
6 => hl(h, ps, l),
|
||||
7 => {
|
||||
// Value: scale the colour to the palette colour's maximum.
|
||||
let (m, pm) = (c[0].max(c[1]).max(c[2]), pc[0].max(pc[1]).max(pc[2]));
|
||||
if m > 1e-6 { c.map(|v| v * pm / m) } else { [pm; 3] }
|
||||
}
|
||||
8 => [pc[0], pc[1], c[2]],
|
||||
9 => [c[0], pc[1], pc[2]],
|
||||
10 => [pc[0], c[1], pc[2]],
|
||||
11 => c,
|
||||
_ => [pc[0], pc[1], pc[2]],
|
||||
};
|
||||
let new_a = if modify_alpha {
|
||||
pc[3]
|
||||
} else if a > 0.0 {
|
||||
a
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
// Where the effect applies: Pixel Selection × Masking.
|
||||
let mut w = if match_mode == 0 { 1.0 } else { colorama_match(c, [key[0], key[1], key[2]], match_mode, tol, soft) };
|
||||
if let Some(m) = &mask {
|
||||
let (mc, ma) = crate::util::unpremul(m.data[i]);
|
||||
let lum = luminance(mc[0], mc[1], mc[2]) * ma;
|
||||
w *= match mask_mode {
|
||||
1 => lum,
|
||||
2 => 1.0 - lum,
|
||||
3 => ma,
|
||||
_ => 1.0 - ma,
|
||||
}
|
||||
.clamp(0.0, 1.0);
|
||||
}
|
||||
let k = w * blend;
|
||||
let o = mix(c, new, k);
|
||||
let oa = a + (new_a - a) * k;
|
||||
// Composite Over Layer off: only the affected pixels remain.
|
||||
let oa = if composite { oa } else { oa * w };
|
||||
*px = [o[0] * oa, o[1] * oa, o[2] * oa, oa.clamp(0.0, 1.0)];
|
||||
});
|
||||
b
|
||||
}
|
||||
@@ -919,9 +1095,30 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
"Colorama",
|
||||
vec![
|
||||
p("inputPhase/mode", "Get Phase From", Value::Enum(0), popup(&COLORAMA_PHASE)),
|
||||
p("inputPhase/addPhase", "Add Phase", Value::Layer(None), ParamUi::Layer),
|
||||
p("inputPhase/addPhaseFrom", "Add Phase From", Value::Enum(0), popup(&COLORAMA_PHASE)),
|
||||
p("inputPhase/addMode", "Add Mode", Value::Enum(0), popup(&["Wrap", "Clamp", "Average", "Screen"])),
|
||||
p("inputPhase/phaseShift", "Phase Shift", num(0.0), ParamUi::Angle),
|
||||
p("outputCycle/usePresetPalette", "Use Preset Palette", Value::Enum(0), popup(&COLORAMA_PRESETS)),
|
||||
// Use Preset Palette = Custom: "position:r,g,b[,a] …" around the wheel.
|
||||
p(
|
||||
"outputCycle/palette",
|
||||
"Output Cycle",
|
||||
Value::Str("0:1,0,0 0.1667:1,1,0 0.3333:0,1,0 0.5:0,1,1 0.6667:0,0,1 0.8333:1,0,1".into()),
|
||||
ParamUi::Text,
|
||||
),
|
||||
p("outputCycle/cycles", "Cycle Repetitions", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 1)),
|
||||
p("outputCycle/interpolatePalette", "Interpolate Palette", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("modify/modify", "Modify", Value::Enum(0), popup(&COLORAMA_MODIFY)),
|
||||
p("modify/modifyAlpha", "Modify Alpha", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("modify/changeEmptyPixels", "Change Empty Pixels", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("pixelSelection/matchingColor", "Matching Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("pixelSelection/matchingTolerance", "Matching Tolerance", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("pixelSelection/matchingSoftness", "Matching Softness", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("pixelSelection/matchingMode", "Matching Mode", Value::Enum(0), popup(&["Off", "RGB", "Hue", "Chroma"])),
|
||||
p("masking/maskLayer", "Mask Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("masking/maskingMode", "Masking Mode", Value::Enum(0), popup(&["Off", "Luminance", "Inverted Luminance", "Alpha", "Inverted Alpha"])),
|
||||
p("masking/compositeOverLayer", "Composite Over Layer", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
colorama,
|
||||
@@ -1158,4 +1355,44 @@ mod tests {
|
||||
let plain = run_fx("ec.generate.fill", &[("fillMask", num(1.0))], grey.clone(), 0.0, EffectEnv::default()).img;
|
||||
assert_eq!(plain.get(10, 5), [1.0, 0.0, 0.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colorama_palettes_modify_selection_masking_and_add_phase() {
|
||||
let grey = [0.5, 0.5, 0.5, 1.0];
|
||||
let red = [1.0, 0.0, 0.0, 1.0];
|
||||
// Ramp Grayscale: intensity 0.5 maps to mid grey.
|
||||
let g = px("ec.color.colorama", &[("outputCycle/usePresetPalette", Value::Enum(1))], grey);
|
||||
assert!((g[0] - 0.5).abs() < 0.01 && (g[0] - g[2]).abs() < 1e-5, "{g:?}");
|
||||
// Custom palette: two stops, black at 0 and blue at 0.5.
|
||||
let custom = [("outputCycle/usePresetPalette", Value::Enum(8)), ("outputCycle/palette", Value::Str("0:0,0,0 0.5:0,0,1".into()))];
|
||||
let c = px("ec.color.colorama", &custom, grey);
|
||||
assert!(c[2] > 0.99 && c[0] < 0.01, "{c:?}");
|
||||
// Modify Lightness keeps the hue of the pixel (red stays red).
|
||||
let l = px("ec.color.colorama", &[("modify/modify", Value::Enum(5)), ("outputCycle/usePresetPalette", Value::Enum(1))], red);
|
||||
assert!(l[0] > l[1] + 0.2 && l[1] < 0.01 + l[2] + 1e-4, "{l:?}");
|
||||
// Pixel Selection: only colours near Matching Color change.
|
||||
let sel = [
|
||||
("pixelSelection/matchingMode", Value::Enum(1)),
|
||||
("pixelSelection/matchingColor", col(1.0, 0.0, 0.0)),
|
||||
("pixelSelection/matchingTolerance", num(10.0)),
|
||||
];
|
||||
assert!(close(px("ec.color.colorama", &sel, grey), grey, 1e-6));
|
||||
assert!(!close(px("ec.color.colorama", &sel, red), red, 1e-3));
|
||||
// Composite Over Layer off: unselected pixels vanish.
|
||||
let mut off = sel.to_vec();
|
||||
off.push(("masking/compositeOverLayer", Value::Bool(false)));
|
||||
assert_eq!(px("ec.color.colorama", &off, grey)[3], 0.0);
|
||||
// Modify Alpha takes the palette alpha.
|
||||
let alpha = [
|
||||
("outputCycle/usePresetPalette", Value::Enum(8)),
|
||||
("outputCycle/palette", Value::Str("0:1,1,1,0.25".into())),
|
||||
("modify/modifyAlpha", Value::Bool(true)),
|
||||
];
|
||||
assert!((px("ec.color.colorama", &alpha, grey)[3] - 0.25).abs() < 1e-5);
|
||||
// Palette parsing and evaluation.
|
||||
let p = ColoramaPalette::parse("0.5:0,0,1 0:1,0,0").unwrap();
|
||||
assert_eq!(p.eval(0.25, true), [0.5, 0.0, 0.5, 1.0]);
|
||||
assert_eq!(p.eval(0.75, true), [0.5, 0.0, 0.5, 1.0], "wraps around");
|
||||
assert_eq!(p.eval(0.25, false), [1.0, 0.0, 0.0, 1.0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,6 +514,19 @@ pub const PARAM_GROUPS: &[(&str, &str)] = &[
|
||||
("toleranceGroup", "Tolerance"),
|
||||
("inputPhase", "Input Phase"),
|
||||
("outputCycle", "Output Cycle"),
|
||||
("modify", "Modify"),
|
||||
("pixelSelection", "Pixel Selection"),
|
||||
("masking", "Masking"),
|
||||
("details", "Details"),
|
||||
("reds", "Reds"),
|
||||
("yellows", "Yellows"),
|
||||
("greens", "Greens"),
|
||||
("cyans", "Cyans"),
|
||||
("blues", "Blues"),
|
||||
("magentas", "Magentas"),
|
||||
("whites", "Whites"),
|
||||
("neutrals", "Neutrals"),
|
||||
("blacks", "Blacks"),
|
||||
("moreOptions", "More Options"),
|
||||
("basicCorrection", "Basic Correction"),
|
||||
("whiteBalance", "White Balance"),
|
||||
@@ -532,6 +545,7 @@ pub const PARAM_GROUPS: &[(&str, &str)] = &[
|
||||
// Camera Lens Blur.
|
||||
("irisProperties", "Iris Properties"),
|
||||
("highlight", "Highlight"),
|
||||
("blurMap", "Blur Map"),
|
||||
// Numbers.
|
||||
("format", "Format"),
|
||||
("fillAndStroke", "Fill and Stroke"),
|
||||
|
||||
@@ -421,4 +421,18 @@ mod tests {
|
||||
assert!(upgrade_instance(spec, &mut g, &mut Ids(&mut next), [100.0, 50.0]));
|
||||
assert_eq!(prop_at(&mut g, "persistentPropertyMapper/mapRedTo").unwrap().value, Value::Enum(15), "X Speed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selective_color_ranges_move_into_details() {
|
||||
let spec = find("ec.color.selectivecolor").unwrap();
|
||||
let mut next = 1;
|
||||
let mut g = instantiate(spec, &mut Ids(&mut next), "Selective Color", [100.0, 50.0]);
|
||||
let mut pr = take_at(&mut g, "details/reds/redsCyan").unwrap();
|
||||
pr.value = Value::Scalar(40.0);
|
||||
g.children.push(pr.into());
|
||||
assert!(upgrade_instance(spec, &mut g, &mut Ids(&mut next), [100.0, 50.0]));
|
||||
assert!(g.get("redsCyan").is_none());
|
||||
assert_eq!(prop_at(&mut g, "details/reds/redsCyan").unwrap().value, Value::Scalar(40.0));
|
||||
assert_eq!(g.sub("details").unwrap().sub("reds").unwrap().name, "Reds");
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -3,11 +3,11 @@
|
||||
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
|
||||
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
|
||||
|
||||
EffectCraft ships 303 effects with 2751 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
EffectCraft ships 303 effects with 2773 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 24 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 284 implemented in full, 19 partial (what is missing is listed).
|
||||
- **Status**: 287 implemented in full, 16 partial (what is missing is listed).
|
||||
|
||||
Effects are addressed by id (`ec.<category>.<name>`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`).
|
||||
|
||||
@@ -51,7 +51,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented |
|
||||
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | | 32 | Implemented |
|
||||
| CC Vector Blur | `ec.blur.ccvector` | 4 | | 32 | Implemented |
|
||||
| Camera Lens Blur | `ec.blur.cameralens` | 10 | | 32 | Partial: no Diffraction Fringe or Blur Map layer |
|
||||
| Camera Lens Blur | `ec.blur.cameralens` | 16 | | 32 | Implemented |
|
||||
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
|
||||
| Channel Blur | `ec.blur.channel` | 6 | | 32 | Implemented |
|
||||
| Compound Blur | `ec.blur.compound` | 4 | | 32 | Implemented |
|
||||
@@ -102,7 +102,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | | 32 | Implemented |
|
||||
| Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented |
|
||||
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | | 32 | Implemented |
|
||||
| Colorama | `ec.color.colorama` | 5 | | 32 | Partial: no Add Phase, editable Output Cycle, Modify, Pixel Selection or Masking |
|
||||
| Colorama | `ec.color.colorama` | 20 | | 32 | Implemented |
|
||||
| Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented |
|
||||
| Equalize | `ec.color.equalize` | 2 | | 32 | Implemented |
|
||||
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
|
||||
@@ -119,7 +119,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented |
|
||||
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | | 32 | Implemented |
|
||||
| Photo Filter | `ec.color.photofilter` | 4 | | 32 | Implemented |
|
||||
| Selective Color | `ec.color.selectivecolor` | 37 | | 32 | Partial: simplified Colors / Details layout |
|
||||
| Selective Color | `ec.color.selectivecolor` | 38 | | 32 | Implemented |
|
||||
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | | 32 | Implemented |
|
||||
| Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented |
|
||||
| Tritone | `ec.color.tritone` | 4 | | 32 | Implemented |
|
||||
|
||||
Reference in New Issue
Block a user