mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 16:33:01 -06:00
M0.1–M0.7: workspace, time/geom/color/keyframe/project/raster/path/text/effects/render crates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9f19158a59
commit
808cd490cd
@@ -0,0 +1,455 @@
|
||||
//! Color Correction effects.
|
||||
|
||||
use effectcraft_color::{hsl_to_rgb, luminance, rgb_to_hsl};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Color Correction", params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
fn mix(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
|
||||
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
||||
}
|
||||
|
||||
fn tint(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let black = ctx.params.color("black");
|
||||
let white = ctx.params.color("white");
|
||||
let amt = ctx.params.f("amount") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let t = [black[0] + (white[0] - black[0]) * l, black[1] + (white[1] - black[1]) * l, black[2] + (white[2] - black[2]) * l];
|
||||
mix(c, t, amt)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn tritone(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let hi = ctx.params.color("highlights");
|
||||
let mid = ctx.params.color("midtones");
|
||||
let sh = ctx.params.color("shadows");
|
||||
let blend = 1.0 - ctx.params.f("blend") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let t = if l < 0.5 { mix([sh[0], sh[1], sh[2]], [mid[0], mid[1], mid[2]], l * 2.0) } else { mix([mid[0], mid[1], mid[2]], [hi[0], hi[1], hi[2]], (l - 0.5) * 2.0) };
|
||||
mix(c, t, blend)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn brightness_contrast(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let br = ctx.params.f("brightness") as f32 / 100.0;
|
||||
let ct = ctx.params.f("contrast") as f32 / 100.0;
|
||||
let k = if ct >= 0.0 { 1.0 / (1.0 - ct * 0.99) } else { 1.0 + ct };
|
||||
b.img.map_straight(|c| c.map(|v| ((v + br - 0.5) * k + 0.5).max(0.0)));
|
||||
b
|
||||
}
|
||||
|
||||
fn hue_saturation(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let hue = ctx.params.f("hue") as f32 / 360.0;
|
||||
let sat = ctx.params.f("saturation") as f32 / 100.0;
|
||||
let light = ctx.params.f("lightness") as f32 / 100.0;
|
||||
let colorize = ctx.params.b("colorize");
|
||||
let ch = ctx.params.f("colorizeHue") as f32 / 360.0;
|
||||
let cs = ctx.params.f("colorizeSaturation") as f32 / 100.0;
|
||||
let cl = ctx.params.f("colorizeLightness") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let (h, s, l) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
let (h, s, l) = if colorize {
|
||||
(ch, cs, (l + cl * if cl > 0.0 { 1.0 - l } else { l }).clamp(0.0, 1.0))
|
||||
} else {
|
||||
let s2 = if sat >= 0.0 { s + (1.0 - s) * sat * s.min(1.0) } else { s * (1.0 + sat) };
|
||||
let l2 = if light >= 0.0 { l + (1.0 - l) * light } else { l * (1.0 + light) };
|
||||
((h + hue).rem_euclid(1.0), s2.clamp(0.0, 1.0), l2)
|
||||
};
|
||||
let (r, g, bl) = hsl_to_rgb(h, s, l);
|
||||
[r, g, bl]
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn levels(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ib = ctx.params.f("inBlack") as f32;
|
||||
let iw = ctx.params.f("inWhite") as f32;
|
||||
let g = ctx.params.f("gamma").max(0.01) as f32;
|
||||
let ob = ctx.params.f("outBlack") as f32;
|
||||
let ow = ctx.params.f("outWhite") as f32;
|
||||
let clip = !ctx.params.b("noClip");
|
||||
b.img.map_straight(|c| {
|
||||
c.map(|v| {
|
||||
let mut t = (v - ib) / (iw - ib).max(1e-6);
|
||||
if clip {
|
||||
t = t.clamp(0.0, 1.0);
|
||||
}
|
||||
let t = t.max(0.0).powf(1.0 / g);
|
||||
ob + (ow - ob) * t
|
||||
})
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn exposure(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let e = 2f32.powf(ctx.params.f("exposure") as f32);
|
||||
let off = ctx.params.f("offset") as f32;
|
||||
let g = ctx.params.f("gamma").max(0.01) as f32;
|
||||
b.img.map_straight(|c| {
|
||||
c.map(|v| {
|
||||
let lin = effectcraft_color::srgb_to_linear(v.max(0.0));
|
||||
let o = (lin * e + off).max(0.0).powf(1.0 / g);
|
||||
effectcraft_color::linear_to_srgb(o)
|
||||
})
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn black_white(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let w = [ctx.params.f("reds"), ctx.params.f("yellows"), ctx.params.f("greens"), ctx.params.f("cyans"), ctx.params.f("blues"), ctx.params.f("magentas")].map(|v| v as f32 / 100.0);
|
||||
let tint_on = ctx.params.b("tint");
|
||||
let tc = ctx.params.color("tintColor");
|
||||
b.img.map_straight(|c| {
|
||||
let (h, s, _) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
// Interpolate the six hue weights around the colour wheel.
|
||||
let hp = h * 6.0;
|
||||
let i = hp.floor() as usize % 6;
|
||||
let f = hp - hp.floor();
|
||||
let wt = w[i] + (w[(i + 1) % 6] - w[i]) * f;
|
||||
let base = luminance(c[0], c[1], c[2]);
|
||||
let gray = (base + (wt - 0.5) * s * 0.6).max(0.0);
|
||||
if tint_on {
|
||||
let (th, ts, _) = rgb_to_hsl(tc[0], tc[1], tc[2]);
|
||||
let (r, g, bl) = hsl_to_rgb(th, ts, gray.min(1.0));
|
||||
[r, g, bl]
|
||||
} else {
|
||||
[gray, gray, gray]
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn fill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = ctx.params.color("color");
|
||||
let invert = ctx.params.b("invert");
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
b.img.data.iter_mut().for_each(|px| {
|
||||
let a = if invert { 1.0 - px[3] } else { px[3] };
|
||||
let f = [c[0] * a, c[1] * a, c[2] * a];
|
||||
for i in 0..3 {
|
||||
px[i] += (f[i] - px[i]) * opacity;
|
||||
}
|
||||
if invert {
|
||||
px[3] += (a - px[3]) * opacity;
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn change_to_color(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let from = ctx.params.color("from");
|
||||
let to = ctx.params.color("to");
|
||||
let tol = ctx.params.f("tolerance") as f32 / 100.0;
|
||||
let soft = (ctx.params.f("softness") as f32 / 100.0).max(1e-3);
|
||||
let (fh, fs, fl) = rgb_to_hsl(from[0], from[1], from[2]);
|
||||
let (th, ts, tl) = rgb_to_hsl(to[0], to[1], to[2]);
|
||||
b.img.map_straight(|c| {
|
||||
let (h, s, l) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
let dh = ((h - fh + 0.5).rem_euclid(1.0) - 0.5).abs() * 2.0;
|
||||
let d = dh.max((s - fs).abs() * 0.5).max((l - fl).abs() * 0.5);
|
||||
let k = (1.0 - ((d - tol) / soft).clamp(0.0, 1.0)).clamp(0.0, 1.0);
|
||||
let (r, g, bl) = hsl_to_rgb((h + (th - fh)).rem_euclid(1.0), (s + (ts - fs)).clamp(0.0, 1.0), (l + (tl - fl)).clamp(0.0, 1.0));
|
||||
mix(c, [r, g, bl], k)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn leave_color(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let keep = ctx.params.color("color");
|
||||
let amt = ctx.params.f("amount") as f32 / 100.0;
|
||||
let tol = ctx.params.f("tolerance") as f32 / 100.0;
|
||||
let (kh, _, _) = rgb_to_hsl(keep[0], keep[1], keep[2]);
|
||||
b.img.map_straight(|c| {
|
||||
let (h, s, _) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
let dh = ((h - kh + 0.5).rem_euclid(1.0) - 0.5).abs() * 2.0;
|
||||
let keepk = if s > 0.05 && dh <= tol { 1.0 } else { (1.0 - (dh - tol) * 8.0).clamp(0.0, 1.0) * s.min(1.0) };
|
||||
let g = luminance(c[0], c[1], c[2]);
|
||||
mix(c, [g, g, g], amt * (1.0 - keepk))
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn photo_filter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = ctx.params.color("color");
|
||||
let d = ctx.params.f("density") as f32 / 100.0;
|
||||
let keep = ctx.params.b("preserveLuminosity");
|
||||
b.img.map_straight(|x| {
|
||||
let f = [x[0] * c[0], x[1] * c[1], x[2] * c[2]];
|
||||
let mut o = mix(x, f, d);
|
||||
if keep {
|
||||
let l0 = luminance(x[0], x[1], x[2]);
|
||||
let l1 = luminance(o[0], o[1], o[2]).max(1e-6);
|
||||
o = o.map(|v| v * l0 / l1);
|
||||
}
|
||||
o
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn vibrance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let vib = ctx.params.f("vibrance") as f32 / 100.0;
|
||||
let sat = ctx.params.f("saturation") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let (h, s, l) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
let s = (s * (1.0 + sat) + vib * (1.0 - s) * s.min(0.5)).clamp(0.0, 1.0);
|
||||
let (r, g, bl) = hsl_to_rgb(h, s, l);
|
||||
[r, g, bl]
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn color_balance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let g = |k: &str| ctx.params.f(k) as f32 / 100.0;
|
||||
let sh = [g("shadowRed"), g("shadowGreen"), g("shadowBlue")];
|
||||
let md = [g("midRed"), g("midGreen"), g("midBlue")];
|
||||
let hl = [g("hiRed"), g("hiGreen"), g("hiBlue")];
|
||||
b.img.map_straight(|c| {
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let ws = (1.0 - l * 2.0).clamp(0.0, 1.0);
|
||||
let wh = (l * 2.0 - 1.0).clamp(0.0, 1.0);
|
||||
let wm = 1.0 - ws - wh;
|
||||
let mut o = c;
|
||||
for i in 0..3 {
|
||||
o[i] = (c[i] + (sh[i] * ws + md[i] * wm + hl[i] * wh) * 0.5).max(0.0);
|
||||
}
|
||||
o
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn channel_mixer(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let g = |k: &str| ctx.params.f(k) as f32 / 100.0;
|
||||
let m = [[g("rr"), g("rg"), g("rb"), g("rc")], [g("gr"), g("gg"), g("gb"), g("gc")], [g("br"), g("bg"), g("bb"), g("bc")]];
|
||||
let mono = ctx.params.b("monochrome");
|
||||
b.img.map_straight(|c| {
|
||||
let row = |r: [f32; 4]| (c[0] * r[0] + c[1] * r[1] + c[2] * r[2] + r[3]).max(0.0);
|
||||
if mono {
|
||||
let v = row(m[0]);
|
||||
[v, v, v]
|
||||
} else {
|
||||
[row(m[0]), row(m[1]), row(m[2])]
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn gamma_pg(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ch = |n: &str| (ctx.params.f(&format!("{n}Gamma")).max(0.01) as f32, ctx.params.f(&format!("{n}Pedestal")) as f32, ctx.params.f(&format!("{n}Gain")) as f32);
|
||||
let r = ch("red");
|
||||
let g = ch("green");
|
||||
let bl = ch("blue");
|
||||
b.img.map_straight(|c| {
|
||||
let f = |v: f32, (gm, pd, gn): (f32, f32, f32)| (pd + (gn - pd) * v.max(0.0).powf(gm)).max(0.0);
|
||||
[f(c[0], r), f(c[1], g), f(c[2], bl)]
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn colorama(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let shift = ctx.params.f("phaseShift") as f32 / 360.0;
|
||||
let cycles = ctx.params.f("cycles").max(0.0) as f32;
|
||||
let blend = 1.0 - ctx.params.f("blend") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let l = luminance(c[0], c[1], c[2]);
|
||||
let (r, g, bl) = effectcraft_color::hsv_to_rgb((l * cycles + shift).rem_euclid(1.0), 1.0, 1.0);
|
||||
mix(c, [r, g, bl], blend)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let pct = || slider(-100.0, 100.0, -100.0, 100.0, 1);
|
||||
let mut gpg = Vec::new();
|
||||
for (n, label) in [("red", "Red"), ("green", "Green"), ("blue", "Blue")] {
|
||||
let leak = |s: String| -> &'static str { Box::leak(s.into_boxed_str()) };
|
||||
gpg.push(p(leak(format!("{n}Gamma")), leak(format!("{label} Gamma")), num(1.0), slider(0.1, 10.0, 0.1, 4.0, 2)));
|
||||
gpg.push(p(leak(format!("{n}Pedestal")), leak(format!("{label} Pedestal")), num(0.0), slider(-2.0, 2.0, -1.0, 1.0, 2)));
|
||||
gpg.push(p(leak(format!("{n}Gain")), leak(format!("{label} Gain")), num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)));
|
||||
}
|
||||
vec![
|
||||
spec(
|
||||
"ec.color.tint",
|
||||
"Tint",
|
||||
vec![
|
||||
p("black", "Map Black To", col(0.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("white", "Map White To", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("amount", "Amount to Tint", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
tint,
|
||||
),
|
||||
spec(
|
||||
"ec.color.tritone",
|
||||
"Tritone",
|
||||
vec![
|
||||
p("highlights", "Highlights", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("midtones", "Midtones", col(0.5, 0.4, 0.3), ParamUi::Color),
|
||||
p("shadows", "Shadows", col(0.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
tritone,
|
||||
),
|
||||
spec(
|
||||
"ec.color.brightnesscontrast",
|
||||
"Brightness & Contrast",
|
||||
vec![p("brightness", "Brightness", num(0.0), slider(-150.0, 150.0, -150.0, 150.0, 1)), p("contrast", "Contrast", num(0.0), pct())],
|
||||
brightness_contrast,
|
||||
),
|
||||
spec(
|
||||
"ec.color.huesaturation",
|
||||
"Hue/Saturation",
|
||||
vec![
|
||||
p("hue", "Master Hue", num(0.0), ParamUi::Angle),
|
||||
p("saturation", "Master Saturation", num(0.0), pct()),
|
||||
p("lightness", "Master Lightness", num(0.0), pct()),
|
||||
p("colorize", "Colorize", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("colorizeHue", "Colorize Hue", num(0.0), ParamUi::Angle),
|
||||
p("colorizeSaturation", "Colorize Saturation", num(25.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("colorizeLightness", "Colorize Lightness", num(0.0), pct()),
|
||||
],
|
||||
hue_saturation,
|
||||
),
|
||||
spec(
|
||||
"ec.color.levels",
|
||||
"Levels",
|
||||
vec![
|
||||
p("inBlack", "Input Black", num(0.0), slider(-1.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("inWhite", "Input White", num(1.0), slider(-1.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("gamma", "Gamma", num(1.0), slider(0.1, 10.0, 0.1, 3.0, 2)),
|
||||
p("outBlack", "Output Black", num(0.0), slider(-1.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("outWhite", "Output White", num(1.0), slider(-1.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("noClip", "Don't Clip", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
levels,
|
||||
),
|
||||
spec(
|
||||
"ec.color.exposure",
|
||||
"Exposure",
|
||||
vec![
|
||||
p("exposure", "Exposure", num(0.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||||
p("offset", "Offset", num(0.0), slider(-2.0, 2.0, -0.5, 0.5, 4)),
|
||||
p("gamma", "Gamma Correction", num(1.0), slider(0.01, 9.99, 0.1, 3.0, 2)),
|
||||
],
|
||||
exposure,
|
||||
),
|
||||
spec(
|
||||
"ec.color.blackwhite",
|
||||
"Black & White",
|
||||
vec![
|
||||
p("reds", "Reds", num(40.0), slider(-200.0, 300.0, -200.0, 300.0, 0)),
|
||||
p("yellows", "Yellows", num(60.0), slider(-200.0, 300.0, -200.0, 300.0, 0)),
|
||||
p("greens", "Greens", num(40.0), slider(-200.0, 300.0, -200.0, 300.0, 0)),
|
||||
p("cyans", "Cyans", num(60.0), slider(-200.0, 300.0, -200.0, 300.0, 0)),
|
||||
p("blues", "Blues", num(20.0), slider(-200.0, 300.0, -200.0, 300.0, 0)),
|
||||
p("magentas", "Magentas", num(80.0), slider(-200.0, 300.0, -200.0, 300.0, 0)),
|
||||
p("tint", "Tint", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("tintColor", "Tint Color", col(0.88, 0.76, 0.6), ParamUi::Color),
|
||||
],
|
||||
black_white,
|
||||
),
|
||||
EffectSpec {
|
||||
id: "ec.generate.fill",
|
||||
name: "Fill",
|
||||
category: "Generate",
|
||||
params: vec![
|
||||
p("color", "Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
render: fill,
|
||||
gpu: false,
|
||||
float: true,
|
||||
},
|
||||
spec(
|
||||
"ec.color.changetocolor",
|
||||
"Change to Color",
|
||||
vec![
|
||||
p("from", "From", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("to", "To", col(0.0, 0.0, 1.0), ParamUi::Color),
|
||||
p("tolerance", "Tolerance", num(10.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("softness", "Softness", num(10.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
change_to_color,
|
||||
),
|
||||
spec(
|
||||
"ec.color.leavecolor",
|
||||
"Leave Color",
|
||||
vec![
|
||||
p("amount", "Amount to Decolor", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("color", "Color To Leave", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("tolerance", "Tolerance", num(15.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
leave_color,
|
||||
),
|
||||
spec(
|
||||
"ec.color.photofilter",
|
||||
"Photo Filter",
|
||||
vec![
|
||||
p("color", "Color", col(0.93, 0.54, 0.09), ParamUi::Color),
|
||||
p("density", "Density", num(25.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("preserveLuminosity", "Preserve Luminosity", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
photo_filter,
|
||||
),
|
||||
spec("ec.color.vibrance", "Vibrance", vec![p("vibrance", "Vibrance", num(0.0), pct()), p("saturation", "Saturation", num(0.0), pct())], vibrance),
|
||||
spec(
|
||||
"ec.color.colorbalance",
|
||||
"Color Balance",
|
||||
["shadowRed", "shadowGreen", "shadowBlue", "midRed", "midGreen", "midBlue", "hiRed", "hiGreen", "hiBlue"]
|
||||
.iter()
|
||||
.zip([
|
||||
"Shadow Red Balance",
|
||||
"Shadow Green Balance",
|
||||
"Shadow Blue Balance",
|
||||
"Midtone Red Balance",
|
||||
"Midtone Green Balance",
|
||||
"Midtone Blue Balance",
|
||||
"Highlight Red Balance",
|
||||
"Highlight Green Balance",
|
||||
"Highlight Blue Balance",
|
||||
])
|
||||
.map(|(id, n)| p(id, n, num(0.0), pct()))
|
||||
.collect(),
|
||||
color_balance,
|
||||
),
|
||||
spec(
|
||||
"ec.color.channelmixer",
|
||||
"Channel Mixer",
|
||||
{
|
||||
let ids = ["rr", "rg", "rb", "rc", "gr", "gg", "gb", "gc", "br", "bg", "bb", "bc"];
|
||||
let names = [
|
||||
"Red-Red", "Red-Green", "Red-Blue", "Red-Const", "Green-Red", "Green-Green", "Green-Blue", "Green-Const", "Blue-Red", "Blue-Green", "Blue-Blue",
|
||||
"Blue-Const",
|
||||
];
|
||||
let mut v: Vec<_> = ids
|
||||
.iter()
|
||||
.zip(names)
|
||||
.map(|(id, n)| p(id, n, num(if matches!(*id, "rr" | "gg" | "bb") { 100.0 } else { 0.0 }), slider(-200.0, 200.0, -200.0, 200.0, 0)))
|
||||
.collect();
|
||||
v.push(p("monochrome", "Monochrome", Value::Bool(false), ParamUi::Checkbox));
|
||||
v
|
||||
},
|
||||
channel_mixer,
|
||||
),
|
||||
spec("ec.color.gammapedestalgain", "Gamma/Pedestal/Gain", gpg, gamma_pg),
|
||||
spec(
|
||||
"ec.color.colorama",
|
||||
"Colorama",
|
||||
vec![
|
||||
p("phaseShift", "Phase Shift", num(0.0), ParamUi::Angle),
|
||||
p("cycles", "Cycle Repetitions", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 1)),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("mode", "Get Phase From", Value::Enum(0), popup(&["Intensity", "Red", "Green", "Blue", "Hue", "Lightness", "Saturation", "Value", "Alpha"])),
|
||||
],
|
||||
colorama,
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! Expression Controls: parameter-only effects whose values expressions read. They pass pixels
|
||||
//! through unchanged.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
fn passthrough(_: &EffectCtx, b: Buf) -> Buf {
|
||||
b
|
||||
}
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Expression Controls", params, render: passthrough, gpu: true, float: true }
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
vec![
|
||||
spec("ec.control.slider", "Slider Control", vec![p("slider", "Slider", num(0.0), slider(-1_000_000.0, 1_000_000.0, 0.0, 100.0, 2))]),
|
||||
spec("ec.control.angle", "Angle Control", vec![p("angle", "Angle", num(0.0), ParamUi::Angle)]),
|
||||
spec("ec.control.checkbox", "Checkbox Control", vec![p("checkbox", "Checkbox", Value::Bool(false), ParamUi::Checkbox)]),
|
||||
spec("ec.control.color", "Color Control", vec![p("color", "Color", col(1.0, 0.0, 0.0), ParamUi::Color)]),
|
||||
spec("ec.control.point", "Point Control", vec![p("point", "Point", Value::Vec2([0.5, 0.5]), ParamUi::Point)]),
|
||||
spec("ec.control.point3d", "3D Point Control", vec![p("point", "3D Point", Value::Vec3([0.0, 0.0, 0.0]), ParamUi::Point3)]),
|
||||
spec("ec.control.layer", "Layer Control", vec![p("layer", "Layer", Value::Layer(None), ParamUi::Layer)]),
|
||||
spec("ec.control.dropdown", "Dropdown Menu Control", vec![p("menu", "Menu", Value::Enum(0), popup(&["Item 1", "Item 2", "Item 3"]))]),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
//! Distort effects: inverse-mapped warps (each output pixel samples the source).
|
||||
|
||||
use effectcraft_geom::{Mat3, vec2};
|
||||
use rayon::prelude::*;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::{Buf, EffectCtx, EffectSpec, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Distort", params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
/// Resample through `f(x, y) -> source point` (pixel coordinates, centres at +0.5).
|
||||
fn remap(src: &Image, repeat: bool, f: impl Fn(f64, f64) -> (f64, f64) + Sync) -> Image {
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
out.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (sx, sy) = f(x as f64 + 0.5, y as f64 + 0.5);
|
||||
*px = if repeat { src.sample_bilinear_clamped(sx, sy) } else { src.sample_bilinear(sx, sy) };
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
fn twirl(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ang = ctx.params.f("angle").to_radians();
|
||||
let r = (ctx.params.f("radius") / 100.0 * b.img.width.min(b.img.height) as f64 * 0.5).max(1.0);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let (dx, dy) = (x - c.0, y - c.1);
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d >= r {
|
||||
return (x, y);
|
||||
}
|
||||
let t = 1.0 - d / r;
|
||||
let a = -ang * t * t;
|
||||
let (s, co) = a.sin_cos();
|
||||
(c.0 + dx * co - dy * s, c.1 + dx * s + dy * co)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn spherize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = (ctx.params.f("radius") * b.scale).max(1.0);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let (dx, dy) = (x - c.0, y - c.1);
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d >= r || d == 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let nd = d / r;
|
||||
let k = (nd.asin() / std::f64::consts::FRAC_PI_2) / nd;
|
||||
(c.0 + dx * k, c.1 + dy * k)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn bulge(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let rx = (ctx.params.f("hradius") * b.scale).max(1.0);
|
||||
let ry = (ctx.params.f("vradius") * b.scale).max(1.0);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let h = ctx.params.f("height");
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let (dx, dy) = ((x - c.0) / rx, (y - c.1) / ry);
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if d2 >= 1.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let k = 1.0 - h * (1.0 - d2) * 0.5;
|
||||
(c.0 + dx * rx * k, c.1 + dy * ry * k)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn wave_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let h = ctx.params.f("height") * b.scale;
|
||||
let w = (ctx.params.f("width") * b.scale).max(1.0);
|
||||
let dir = ctx.params.f("direction").to_radians();
|
||||
let speed = ctx.params.f("speed");
|
||||
let phase = ctx.params.f("phase").to_radians() + ctx.time * speed * std::f64::consts::TAU;
|
||||
let kind = ctx.params.e("waveType");
|
||||
let (dx, dy) = (dir.sin(), -dir.cos());
|
||||
if !ctx.adjustment {
|
||||
b.pad(h.abs().ceil() as u32 + 1);
|
||||
}
|
||||
let wave = move |t: f64| -> f64 {
|
||||
let ph = t.rem_euclid(std::f64::consts::TAU) / std::f64::consts::TAU;
|
||||
match kind {
|
||||
1 => if ph < 0.5 { 1.0 } else { -1.0 },
|
||||
2 => 1.0 - 4.0 * (ph - 0.5).abs(),
|
||||
3 => 2.0 * ph - 1.0,
|
||||
_ => t.sin(),
|
||||
}
|
||||
};
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
// Waves travel along `dir`; displacement is perpendicular to it.
|
||||
let along = x * dx + y * dy;
|
||||
let disp = wave(along / w * std::f64::consts::TAU + phase) * h;
|
||||
(x + dy * disp, y - dx * disp)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn ripple(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = (ctx.params.f("radius") / 100.0 * b.img.width.max(b.img.height) as f64).max(1.0);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let w = (ctx.params.f("waveWidth") * b.scale).max(1.0);
|
||||
let h = ctx.params.f("waveHeight") * b.scale;
|
||||
let phase = ctx.params.f("phase").to_radians() - ctx.time * ctx.params.f("speed") * std::f64::consts::TAU;
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let (dx, dy) = (x - c.0, y - c.1);
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d >= r || d == 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let fall = 1.0 - d / r;
|
||||
let o = (d / w * std::f64::consts::TAU + phase).sin() * h * fall;
|
||||
(x + dx / d * o, y + dy / d * o)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn mirror(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let a = ctx.params.f("angle").to_radians();
|
||||
let n = (a.cos(), a.sin());
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let d = (x - c.0) * n.0 + (y - c.1) * n.1;
|
||||
if d > 0.0 { (x - 2.0 * d * n.0, y - 2.0 * d * n.1) } else { (x, y) }
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn offset(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let shift = ctx.params.v2("shift");
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
let blend = ctx.params.f("blend") as f32 / 100.0;
|
||||
let (cx, cy) = (w / 2.0, h / 2.0);
|
||||
let (sx, sy) = b.to_px(shift);
|
||||
let orig = b.img.clone();
|
||||
let mut o = remap(&b.img, false, |x, y| ((x - (sx - cx)).rem_euclid(w), (y - (sy - cy)).rem_euclid(h)));
|
||||
if blend > 0.0 {
|
||||
for (p, q) in o.data.iter_mut().zip(&orig.data) {
|
||||
for i in 0..4 {
|
||||
p[i] += (q[i] - p[i]) * blend;
|
||||
}
|
||||
}
|
||||
}
|
||||
b.img = o;
|
||||
b
|
||||
}
|
||||
|
||||
fn polar(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let amt = ctx.params.f("interpolation") / 100.0;
|
||||
let to_polar = ctx.params.e("conversion") == 1;
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
let (cx, cy) = (w / 2.0, h / 2.0);
|
||||
let rmax = cx.min(cy);
|
||||
b.img = remap(&b.img, true, |x, y| {
|
||||
let (px, py) = if to_polar {
|
||||
// output is polar: angle → x, radius → y
|
||||
let dx = x - cx;
|
||||
let dy = y - cy;
|
||||
let a = dx.atan2(-dy).rem_euclid(std::f64::consts::TAU);
|
||||
let r = (dx * dx + dy * dy).sqrt() / rmax;
|
||||
(a / std::f64::consts::TAU * w, h - r * h)
|
||||
} else {
|
||||
let a = x / w * std::f64::consts::TAU;
|
||||
let r = (h - y) / h * rmax;
|
||||
(cx + r * a.sin(), cy - r * a.cos())
|
||||
};
|
||||
(x + (px - x) * amt, y + (py - y) * amt)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn transform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let anchor = b.to_px(ctx.params.v2("anchor"));
|
||||
let pos = b.to_px(ctx.params.v2("position"));
|
||||
let uniform = ctx.params.b("uniform");
|
||||
let sh = ctx.params.f("scaleHeight");
|
||||
let sw = if uniform { sh } else { ctx.params.f("scaleWidth") };
|
||||
let skew = ctx.params.f("skew");
|
||||
let skew_axis = ctx.params.f("skewAxis");
|
||||
let rot = ctx.params.f("rotation");
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let m = Mat3::translate(vec2(pos.0, pos.1))
|
||||
* Mat3::rotate_deg(rot)
|
||||
* Mat3::skew_deg(skew, skew_axis)
|
||||
* Mat3::scale(vec2(sw / 100.0, sh / 100.0))
|
||||
* Mat3::translate(vec2(-anchor.0, -anchor.1));
|
||||
let mut out = Image::new(b.img.width, b.img.height);
|
||||
effectcraft_raster::composite_warp(&mut out, &b.img, &m, &effectcraft_raster::WarpOpts { opacity, ..Default::default() });
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
|
||||
fn corner_pin(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (w, h) = (b.layer_w(ctx), b.layer_h(ctx));
|
||||
let q = ["ul", "ur", "lr", "ll"].map(|k| {
|
||||
let p = b.to_px(ctx.params.v2(k));
|
||||
vec2(p.0, p.1)
|
||||
});
|
||||
let quad = Mat3::square_to_quad(q);
|
||||
let o = b.to_px([0.0, 0.0]);
|
||||
let m = quad * Mat3::scale(vec2(1.0 / (w * b.scale), 1.0 / (h * b.scale))) * Mat3::translate(vec2(-o.0, -o.1));
|
||||
let mut out = Image::new(b.img.width, b.img.height);
|
||||
effectcraft_raster::composite_warp(&mut out, &b.img, &m, &Default::default());
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
|
||||
impl Buf {
|
||||
fn layer_w(&self, ctx: &EffectCtx) -> f64 {
|
||||
ctx.layer_size[0].max(1.0)
|
||||
}
|
||||
fn layer_h(&self, ctx: &EffectCtx) -> f64 {
|
||||
ctx.layer_size[1].max(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let pt = |x, y| Value::Vec2([x, y]);
|
||||
vec![
|
||||
spec(
|
||||
"ec.distort.twirl",
|
||||
"Twirl",
|
||||
vec![
|
||||
p("angle", "Angle", num(0.0), ParamUi::Angle),
|
||||
p("radius", "Twirl Radius", num(30.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("center", "Twirl Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
],
|
||||
twirl,
|
||||
),
|
||||
spec(
|
||||
"ec.distort.spherize",
|
||||
"Spherize",
|
||||
vec![p("radius", "Radius", num(0.0), slider(0.0, 2500.0, 0.0, 2500.0, 1)), p("center", "Center of Sphere", pt(0.5, 0.5), ParamUi::Point)],
|
||||
spherize,
|
||||
),
|
||||
spec(
|
||||
"ec.distort.bulge",
|
||||
"Bulge",
|
||||
vec![
|
||||
p("hradius", "Horizontal Radius", num(50.0), slider(0.0, 4000.0, 0.0, 1000.0, 1)),
|
||||
p("vradius", "Vertical Radius", num(50.0), slider(0.0, 4000.0, 0.0, 1000.0, 1)),
|
||||
p("center", "Bulge Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("height", "Bulge Height", num(1.0), slider(-4.0, 4.0, -4.0, 4.0, 2)),
|
||||
],
|
||||
bulge,
|
||||
),
|
||||
spec(
|
||||
"ec.distort.wavewarp",
|
||||
"Wave Warp",
|
||||
vec![
|
||||
p("waveType", "Wave Type", Value::Enum(0), popup(&["Sine", "Square", "Triangle", "Sawtooth"])),
|
||||
p("height", "Wave Height", num(10.0), slider(-1000.0, 1000.0, -100.0, 100.0, 0)),
|
||||
p("width", "Wave Width", num(40.0), slider(1.0, 10000.0, 1.0, 500.0, 0)),
|
||||
p("direction", "Direction", num(90.0), ParamUi::Angle),
|
||||
p("speed", "Wave Speed", num(1.0), slider(-100.0, 100.0, -5.0, 5.0, 1)),
|
||||
p("phase", "Phase", num(0.0), ParamUi::Angle),
|
||||
],
|
||||
wave_warp,
|
||||
),
|
||||
spec(
|
||||
"ec.distort.ripple",
|
||||
"Ripple",
|
||||
vec![
|
||||
p("radius", "Radius", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("center", "Center of Ripple", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("waveWidth", "Wave Width", num(20.0), slider(1.0, 1000.0, 1.0, 100.0, 1)),
|
||||
p("waveHeight", "Wave Height", num(10.0), slider(0.0, 400.0, 0.0, 100.0, 1)),
|
||||
p("speed", "Wave Speed", num(1.0), slider(-15.0, 15.0, -5.0, 5.0, 1)),
|
||||
p("phase", "Ripple Phase", num(0.0), ParamUi::Angle),
|
||||
],
|
||||
ripple,
|
||||
),
|
||||
spec("ec.distort.mirror", "Mirror", vec![p("center", "Reflection Center", pt(0.5, 0.5), ParamUi::Point), p("angle", "Reflection Angle", num(0.0), ParamUi::Angle)], mirror),
|
||||
spec(
|
||||
"ec.distort.offset",
|
||||
"Offset",
|
||||
vec![p("shift", "Shift Center To", pt(0.5, 0.5), ParamUi::Point), p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1))],
|
||||
offset,
|
||||
),
|
||||
spec(
|
||||
"ec.distort.polar",
|
||||
"Polar Coordinates",
|
||||
vec![
|
||||
p("interpolation", "Interpolation", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("conversion", "Type of Conversion", Value::Enum(0), popup(&["Rect to Polar", "Polar to Rect"])),
|
||||
],
|
||||
polar,
|
||||
),
|
||||
spec(
|
||||
"ec.distort.transform",
|
||||
"Transform",
|
||||
vec![
|
||||
p("anchor", "Anchor Point", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("uniform", "Uniform Scale", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("scaleHeight", "Scale Height", num(100.0), slider(-10000.0, 10000.0, 0.0, 600.0, 1)),
|
||||
p("scaleWidth", "Scale Width", num(100.0), slider(-10000.0, 10000.0, 0.0, 600.0, 1)),
|
||||
p("skew", "Skew", num(0.0), slider(-70.0, 70.0, -70.0, 70.0, 1)),
|
||||
p("skewAxis", "Skew Axis", num(0.0), ParamUi::Angle),
|
||||
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
transform,
|
||||
),
|
||||
spec(
|
||||
"ec.distort.cornerpin",
|
||||
"Corner Pin",
|
||||
vec![
|
||||
p("ul", "Upper Left", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("ur", "Upper Right", pt(1.0, 0.0), ParamUi::Point),
|
||||
p("ll", "Lower Left", pt(0.0, 1.0), ParamUi::Point),
|
||||
p("lr", "Lower Right", pt(1.0, 1.0), ParamUi::Point),
|
||||
],
|
||||
corner_pin,
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
//! Generate and Noise effects that synthesise images.
|
||||
|
||||
use rayon::prelude::*;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::Px;
|
||||
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, category: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category, params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
/// Blend a generated straight colour `g` (with alpha `ga`) over the original pixel.
|
||||
fn put(px: &mut Px, g: [f32; 3], ga: f32, blend_orig: f32) {
|
||||
let g2 = [g[0] * ga, g[1] * ga, g[2] * ga, ga];
|
||||
let k = 1.0 - blend_orig;
|
||||
for c in 0..4 {
|
||||
px[c] = px[c] * blend_orig + g2[c] * k;
|
||||
}
|
||||
}
|
||||
|
||||
fn gradient_ramp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let s = b.to_px(ctx.params.v2("start"));
|
||||
let e = b.to_px(ctx.params.v2("end"));
|
||||
let c0 = ctx.params.color("startColor");
|
||||
let c1 = ctx.params.color("endColor");
|
||||
let radial = ctx.params.e("shape") == 1;
|
||||
let blend = ctx.params.f("blend") as f32 / 100.0;
|
||||
let scatter = ctx.params.f("scatter") as f32 / 512.0;
|
||||
let (dx, dy) = (e.0 - s.0, e.1 - s.1);
|
||||
let len2 = (dx * dx + dy * dy).max(1e-9);
|
||||
let seed = ctx.seed;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (vx, vy) = (x as f64 + 0.5 - s.0, y as f64 + 0.5 - s.1);
|
||||
let mut t = if radial { ((vx * vx + vy * vy) / len2).sqrt() } else { (vx * dx + vy * dy) / len2 } as f32;
|
||||
if scatter > 0.0 {
|
||||
t += (effectcraft_raster::hash_noise(x as u32, y as u32, seed) - 0.5) * scatter;
|
||||
}
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let c = [c0[0] + (c1[0] - c0[0]) * t, c0[1] + (c1[1] - c0[1]) * t, c0[2] + (c1[2] - c0[2]) * t];
|
||||
put(px, c, 1.0, blend);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn four_color(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pts: Vec<((f64, f64), [f32; 4])> = (1..=4).map(|i| (b.to_px(ctx.params.v2(&format!("point{i}"))), ctx.params.color(&format!("color{i}")))).collect();
|
||||
let blend = ctx.params.f("blend").max(1.0);
|
||||
let op = ctx.params.f("opacity") as f32 / 100.0;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let mut acc = [0.0f64; 3];
|
||||
let mut wsum = 0.0;
|
||||
for ((qx, qy), c) in &pts {
|
||||
let d2 = (x as f64 + 0.5 - qx).powi(2) + (y as f64 + 0.5 - qy).powi(2);
|
||||
let w = 1.0 / (d2 / (blend * 100.0) + 1e-6).powf(1.5);
|
||||
for i in 0..3 {
|
||||
acc[i] += c[i] as f64 * w;
|
||||
}
|
||||
wsum += w;
|
||||
}
|
||||
let c = acc.map(|v| (v / wsum) as f32);
|
||||
put(px, c, 1.0, 1.0 - op);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn checkerboard(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let anchor = b.to_px(ctx.params.v2("anchor"));
|
||||
let w = (ctx.params.f("width") * b.scale).max(1.0);
|
||||
let h = (ctx.params.f("height") * b.scale).max(1.0);
|
||||
let c = ctx.params.color("color");
|
||||
let op = ctx.params.f("opacity") as f32 / 100.0;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let cx = ((x as f64 + 0.5 - anchor.0) / w).floor() as i64;
|
||||
let cy = ((y as f64 + 0.5 - anchor.1) / h).floor() as i64;
|
||||
if (cx + cy).rem_euclid(2) == 0 {
|
||||
put(px, [c[0], c[1], c[2]], op, 0.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn grid(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let anchor = b.to_px(ctx.params.v2("anchor"));
|
||||
let w = (ctx.params.f("width") * b.scale).max(1.0);
|
||||
let h = (ctx.params.f("height") * b.scale).max(1.0);
|
||||
let border = (ctx.params.f("border") * b.scale).max(0.0);
|
||||
let c = ctx.params.color("color");
|
||||
let op = ctx.params.f("opacity") as f32 / 100.0;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let fx = (x as f64 + 0.5 - anchor.0).rem_euclid(w);
|
||||
let fy = (y as f64 + 0.5 - anchor.1).rem_euclid(h);
|
||||
let d = fx.min(w - fx).min(fy.min(h - fy));
|
||||
let cov = ((border / 2.0 - d) + 0.5).clamp(0.0, 1.0) as f32;
|
||||
if cov > 0.0 {
|
||||
let mut g = *px;
|
||||
put(&mut g, [c[0], c[1], c[2]], op, 0.0);
|
||||
for i in 0..4 {
|
||||
px[i] += (g[i] - px[i]) * cov;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn circle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let r = ctx.params.f("radius") * b.scale;
|
||||
let feather = (ctx.params.f("feather") * b.scale).max(0.5);
|
||||
let color = ctx.params.color("color");
|
||||
let op = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let invert = ctx.params.b("invert");
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let d = ((x as f64 + 0.5 - c.0).powi(2) + (y as f64 + 0.5 - c.1).powi(2)).sqrt();
|
||||
let mut cov = ((r - d) / feather + 0.5).clamp(0.0, 1.0) as f32;
|
||||
if invert {
|
||||
cov = 1.0 - cov;
|
||||
}
|
||||
if cov > 0.0 {
|
||||
let mut g = *px;
|
||||
put(&mut g, [color[0], color[1], color[2]], op, 0.0);
|
||||
for i in 0..4 {
|
||||
px[i] += (g[i] - px[i]) * cov;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ---- value noise / fBm (our own lattice noise with quintic fade) ----
|
||||
|
||||
#[inline]
|
||||
fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 {
|
||||
let mut h = (ix as u32).wrapping_mul(0x27d4_eb2d) ^ (iy as u32).wrapping_mul(0x1656_67b1) ^ (iz as u32).wrapping_mul(0x9e37_79b9) ^ seed.wrapping_mul(0x85eb_ca6b);
|
||||
h ^= h >> 15;
|
||||
h = h.wrapping_mul(0x2c1b_3c6d);
|
||||
h ^= h >> 12;
|
||||
h = h.wrapping_mul(0x297a_2d39);
|
||||
h ^= h >> 15;
|
||||
(h & 0xffff) as f32 / 65535.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn fade(t: f32) -> f32 {
|
||||
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
|
||||
}
|
||||
|
||||
/// 3D value noise in 0..1 (z = evolution).
|
||||
pub fn value_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 {
|
||||
let (x0, y0, z0) = (x.floor(), y.floor(), z.floor());
|
||||
let (fx, fy, fz) = (fade(x - x0), fade(y - y0), fade(z - z0));
|
||||
let (ix, iy, iz) = (x0 as i32, y0 as i32, z0 as i32);
|
||||
let l = |a: f32, b: f32, t: f32| a + (b - a) * t;
|
||||
let c = |dx: i32, dy: i32, dz: i32| lattice(ix + dx, iy + dy, iz + dz, seed);
|
||||
let a = l(l(c(0, 0, 0), c(1, 0, 0), fx), l(c(0, 1, 0), c(1, 1, 0), fx), fy);
|
||||
let b = l(l(c(0, 0, 1), c(1, 0, 1), fx), l(c(0, 1, 1), c(1, 1, 1), fx), fy);
|
||||
l(a, b, fz)
|
||||
}
|
||||
|
||||
fn fractal_noise(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let kind = ctx.params.e("fractalType");
|
||||
let contrast = ctx.params.f("contrast") as f32 / 100.0;
|
||||
let brightness = ctx.params.f("brightness") as f32 / 100.0;
|
||||
let invert = ctx.params.b("invert");
|
||||
let scale = (ctx.params.f("scale") * b.scale).max(1.0) as f32;
|
||||
let off = ctx.params.v2("offset");
|
||||
let rot = (ctx.params.f("rotation") as f32).to_radians();
|
||||
let octaves = ctx.params.f("complexity").clamp(1.0, 20.0);
|
||||
let evo = ctx.params.f("evolution") as f32 / 360.0;
|
||||
let seed = ctx.params.f("seed") as u32 ^ 0x51ed;
|
||||
let blend = ctx.params.f("blend") as f32 / 100.0;
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let (sr, cr) = rot.sin_cos();
|
||||
let n_oct = octaves.ceil() as usize;
|
||||
let frac = (octaves - octaves.floor()) as f32;
|
||||
let (ox, oy) = b.to_px(off);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (dx, dy) = (x as f32 + 0.5 - ox as f32, y as f32 + 0.5 - oy as f32);
|
||||
let (u, v) = ((dx * cr + dy * sr) / scale, (-dx * sr + dy * cr) / scale);
|
||||
let mut sum = 0.0;
|
||||
let mut amp = 1.0;
|
||||
let mut norm = 0.0;
|
||||
let mut f = 1.0;
|
||||
for o in 0..n_oct {
|
||||
let w = if o + 1 == n_oct && frac > 0.0 { frac } else { 1.0 };
|
||||
let mut n = value_noise(u * f, v * f, evo + o as f32 * 7.31, seed + o as u32);
|
||||
if kind == 1 {
|
||||
n = 1.0 - (n * 2.0 - 1.0).abs(); // turbulent / ridged
|
||||
}
|
||||
sum += n * amp * w;
|
||||
norm += amp * w;
|
||||
amp *= 0.5;
|
||||
f *= 2.0;
|
||||
}
|
||||
let mut val = sum / norm.max(1e-6);
|
||||
val = ((val - 0.5) * contrast + 0.5 + brightness).clamp(0.0, 1.0);
|
||||
if invert {
|
||||
val = 1.0 - val;
|
||||
}
|
||||
put(px, [val, val, val], opacity, blend);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let pt = |x, y| Value::Vec2([x, y]);
|
||||
vec![
|
||||
spec(
|
||||
"ec.generate.gradientramp",
|
||||
"Gradient Ramp",
|
||||
"Generate",
|
||||
vec![
|
||||
p("start", "Start of Ramp", pt(0.5, 0.0), ParamUi::Point),
|
||||
p("startColor", "Start Color", col(0.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("end", "End of Ramp", pt(0.5, 1.0), ParamUi::Point),
|
||||
p("endColor", "End Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("shape", "Ramp Shape", Value::Enum(0), popup(&["Linear Ramp", "Radial Ramp"])),
|
||||
p("scatter", "Ramp Scatter", num(0.0), slider(0.0, 512.0, 0.0, 512.0, 1)),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
gradient_ramp,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.fourcolor",
|
||||
"4-Color Gradient",
|
||||
"Generate",
|
||||
vec![
|
||||
p("point1", "Point 1", pt(0.1, 0.1), ParamUi::Point),
|
||||
p("color1", "Color 1", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("point2", "Point 2", pt(0.9, 0.1), ParamUi::Point),
|
||||
p("color2", "Color 2", col(0.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("point3", "Point 3", pt(0.1, 0.9), ParamUi::Point),
|
||||
p("color3", "Color 3", col(1.0, 0.0, 1.0), ParamUi::Color),
|
||||
p("point4", "Point 4", pt(0.9, 0.9), ParamUi::Point),
|
||||
p("color4", "Color 4", col(0.0, 0.0, 1.0), ParamUi::Color),
|
||||
p("blend", "Blend", num(100.0), slider(1.0, 1000.0, 1.0, 1000.0, 1)),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
four_color,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.checkerboard",
|
||||
"Checkerboard",
|
||||
"Generate",
|
||||
vec![
|
||||
p("anchor", "Anchor", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("width", "Width", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)),
|
||||
p("height", "Height", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
checkerboard,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.grid",
|
||||
"Grid",
|
||||
"Generate",
|
||||
vec![
|
||||
p("anchor", "Anchor", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("width", "Width", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)),
|
||||
p("height", "Height", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)),
|
||||
p("border", "Border", num(2.0), slider(0.0, 400.0, 0.0, 40.0, 1)),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
grid,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.circle",
|
||||
"Circle",
|
||||
"Generate",
|
||||
vec![
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("radius", "Radius", num(75.0), slider(0.0, 4000.0, 0.0, 1000.0, 1)),
|
||||
p("feather", "Feather Outer Edge", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("invert", "Invert Circle", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
circle,
|
||||
),
|
||||
spec(
|
||||
"ec.noise.fractal",
|
||||
"Fractal Noise",
|
||||
"Noise & Grain",
|
||||
vec![
|
||||
p("fractalType", "Fractal Type", Value::Enum(0), popup(&["Basic", "Turbulent Smooth"])),
|
||||
p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("contrast", "Contrast", num(100.0), slider(0.0, 10000.0, 0.0, 400.0, 1)),
|
||||
p("brightness", "Brightness", num(0.0), slider(-10000.0, 10000.0, -200.0, 200.0, 1)),
|
||||
p("scale", "Scale", num(100.0), slider(1.0, 10000.0, 20.0, 600.0, 1)),
|
||||
p("offset", "Offset Turbulence", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||||
p("complexity", "Complexity", num(6.0), slider(1.0, 20.0, 1.0, 10.0, 1)),
|
||||
p("evolution", "Evolution", num(0.0), ParamUi::Angle),
|
||||
p("seed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
fractal_noise,
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
//! The effects library.
|
||||
//!
|
||||
//! Every effect is an [`EffectSpec`]: a stable id (`ec.blur.gaussian`), its display name and
|
||||
//! category (the Effects & Presets tree), typed parameters with defaults and UI hints, and a CPU
|
||||
//! render function. Effect instances on layers are ordinary property groups (see
|
||||
//! [`instantiate`]), so parameters animate, take expressions and are addressable like any other
|
||||
//! property.
|
||||
|
||||
mod color_fx;
|
||||
mod controls;
|
||||
mod distort;
|
||||
mod generate;
|
||||
mod misc;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
|
||||
pub use effectcraft_raster::Image;
|
||||
|
||||
/// Effect categories in Effects & Presets order.
|
||||
pub const CATEGORIES: &[&str] = &[
|
||||
"3D Channel",
|
||||
"Audio",
|
||||
"Blur & Sharpen",
|
||||
"Channel",
|
||||
"Color Correction",
|
||||
"Distort",
|
||||
"Expression Controls",
|
||||
"Generate",
|
||||
"Immersive Video",
|
||||
"Keying",
|
||||
"Matte",
|
||||
"Noise & Grain",
|
||||
"Obsolete",
|
||||
"Perspective",
|
||||
"Simulation",
|
||||
"Stylize",
|
||||
"Text",
|
||||
"Time",
|
||||
"Transition",
|
||||
"Utility",
|
||||
];
|
||||
|
||||
pub struct ParamSpec {
|
||||
/// Point defaults are fractions of the layer size (0.5, 0.5 = layer centre).
|
||||
pub id: &'static str,
|
||||
pub name: &'static str,
|
||||
pub default: Value,
|
||||
pub ui: ParamUi,
|
||||
}
|
||||
|
||||
/// Parameter values evaluated at the current time.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Params {
|
||||
pub values: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
pub fn get(&self, id: &str) -> Option<&Value> {
|
||||
self.values.get(id)
|
||||
}
|
||||
pub fn f(&self, id: &str) -> f64 {
|
||||
self.values.get(id).map(Value::as_f64).unwrap_or(0.0)
|
||||
}
|
||||
pub fn v2(&self, id: &str) -> [f64; 2] {
|
||||
self.values.get(id).map(Value::as_vec2).unwrap_or([0.0; 2])
|
||||
}
|
||||
pub fn color(&self, id: &str) -> [f32; 4] {
|
||||
self.values.get(id).map(Value::as_color).unwrap_or([1.0; 4])
|
||||
}
|
||||
pub fn b(&self, id: &str) -> bool {
|
||||
self.values.get(id).map(Value::as_bool).unwrap_or(false)
|
||||
}
|
||||
pub fn e(&self, id: &str) -> u32 {
|
||||
self.values.get(id).map(Value::as_enum).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer image in flight: layer-space point `p` sits at pixel `p * scale + offset`.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Buf {
|
||||
pub img: Image,
|
||||
pub offset: [f64; 2],
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
impl Buf {
|
||||
pub fn to_px(&self, p: [f64; 2]) -> (f64, f64) {
|
||||
(p[0] * self.scale + self.offset[0], p[1] * self.scale + self.offset[1])
|
||||
}
|
||||
/// Grow by `pad` transparent pixels on every side.
|
||||
pub fn pad(&mut self, pad: u32) {
|
||||
if pad == 0 {
|
||||
return;
|
||||
}
|
||||
self.img = self.img.padded(pad);
|
||||
self.offset[0] += pad as f64;
|
||||
self.offset[1] += pad as f64;
|
||||
}
|
||||
}
|
||||
|
||||
/// What an effect gets to render with.
|
||||
pub struct EffectCtx<'a> {
|
||||
pub params: &'a Params,
|
||||
/// Layer time in seconds.
|
||||
pub time: f64,
|
||||
/// Layer source size in layer pixels (the effect's "layer bounds").
|
||||
pub layer_size: [f64; 2],
|
||||
/// Instance seed (stable per effect instance).
|
||||
pub seed: u32,
|
||||
/// Rendering for an adjustment layer (operating on the comp below).
|
||||
pub adjustment: bool,
|
||||
}
|
||||
|
||||
pub type RenderFn = fn(&EffectCtx, Buf) -> Buf;
|
||||
|
||||
pub struct EffectSpec {
|
||||
pub id: &'static str,
|
||||
pub name: &'static str,
|
||||
pub category: &'static str,
|
||||
pub params: Vec<ParamSpec>,
|
||||
pub render: RenderFn,
|
||||
/// Implemented on the GPU path too (shown with a badge).
|
||||
pub gpu: bool,
|
||||
/// Supports 32 bpc float (badge).
|
||||
pub float: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn p(id: &'static str, name: &'static str, default: Value, ui: ParamUi) -> ParamSpec {
|
||||
ParamSpec { id, name, default, ui }
|
||||
}
|
||||
pub(crate) fn slider(min: f64, max: f64, smin: f64, smax: f64, decimals: u8) -> ParamUi {
|
||||
ParamUi::Slider { min, max, slider_min: smin, slider_max: smax, decimals }
|
||||
}
|
||||
pub(crate) fn popup(opts: &[&str]) -> ParamUi {
|
||||
ParamUi::Popup { options: opts.iter().map(|s| s.to_string()).collect() }
|
||||
}
|
||||
pub(crate) fn num(v: f64) -> Value {
|
||||
Value::Scalar(v)
|
||||
}
|
||||
pub(crate) fn col(r: f64, g: f64, b: f64) -> Value {
|
||||
Value::Color([r, g, b, 1.0])
|
||||
}
|
||||
|
||||
/// All effects, sorted by category then name.
|
||||
pub fn registry() -> &'static [EffectSpec] {
|
||||
static R: OnceLock<Vec<EffectSpec>> = OnceLock::new();
|
||||
R.get_or_init(|| {
|
||||
let mut v = Vec::new();
|
||||
v.extend(misc::specs());
|
||||
v.extend(color_fx::specs());
|
||||
v.extend(generate::specs());
|
||||
v.extend(distort::specs());
|
||||
v.extend(controls::specs());
|
||||
v.sort_by(|a, b| a.category.cmp(b.category).then(a.name.cmp(b.name)));
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find(id: &str) -> Option<&'static EffectSpec> {
|
||||
registry().iter().find(|s| s.id == id)
|
||||
}
|
||||
|
||||
/// Find by id or (case-insensitive) display name.
|
||||
pub fn lookup(name_or_id: &str) -> Option<&'static EffectSpec> {
|
||||
find(name_or_id).or_else(|| registry().iter().find(|s| s.name.eq_ignore_ascii_case(name_or_id)))
|
||||
}
|
||||
|
||||
/// Build the property group for a new instance of an effect.
|
||||
pub fn instantiate(spec: &EffectSpec, ids: &mut Ids, instance_name: &str, layer_size: [f64; 2]) -> PropGroup {
|
||||
let mut g = ids.group(spec.id, instance_name);
|
||||
g.kind = GroupKind::Effect { effect: spec.id.to_string() };
|
||||
for ps in &spec.params {
|
||||
let default = match (&ps.ui, &ps.default) {
|
||||
(ParamUi::Point, Value::Vec2(f)) => Value::Vec2([f[0] * layer_size[0], f[1] * layer_size[1]]),
|
||||
_ => ps.default.clone(),
|
||||
};
|
||||
let mut pr = Property::new(ids.alloc(), ps.id, ps.name, default).with_ui(ps.ui.clone());
|
||||
if matches!(ps.ui, ParamUi::Point) {
|
||||
pr.spatial = true;
|
||||
}
|
||||
if matches!(ps.ui, ParamUi::Checkbox | ParamUi::Popup { .. } | ParamUi::Layer) {
|
||||
pr.hold_only = true;
|
||||
}
|
||||
g.children.push(pr.into());
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Run an effect on a buffer.
|
||||
pub fn apply(spec: &EffectSpec, ctx: &EffectCtx, buf: Buf) -> Buf {
|
||||
(spec.render)(ctx, buf)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn registry_is_consistent() {
|
||||
let r = registry();
|
||||
assert!(r.len() >= 40, "{}", r.len());
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
for s in r {
|
||||
assert!(ids.insert(s.id), "duplicate {}", s.id);
|
||||
assert!(CATEGORIES.contains(&s.category), "{} in unknown category {}", s.id, s.category);
|
||||
let mut pids = std::collections::HashSet::new();
|
||||
for p in &s.params {
|
||||
assert!(pids.insert(p.id), "{}: duplicate param {}", s.id, p.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every effect runs on a small image with its default parameters without panicking and
|
||||
/// keeps alpha in range.
|
||||
#[test]
|
||||
fn all_effects_run_with_defaults() {
|
||||
let mut img = Image::new(48, 32);
|
||||
for y in 0..32 {
|
||||
for x in 0..48 {
|
||||
let a = if (8..40).contains(&x) && (6..26).contains(&y) { 1.0 } else { 0.0 };
|
||||
img.set(x, y, [x as f32 / 48.0 * a, y as f32 / 32.0 * a, 0.5 * a, a]);
|
||||
}
|
||||
}
|
||||
for s in registry() {
|
||||
let params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: [48.0, 32.0], seed: 1, adjustment: false };
|
||||
let out = apply(s, &ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 });
|
||||
assert!(!out.img.is_empty(), "{}", s.id);
|
||||
assert!(out.img.data.iter().all(|p| p[3].is_finite() && p[3] >= -1e-4 && p[3] <= 1.0001), "{} alpha out of range", s.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
//! Blur & Sharpen, Stylize, Perspective, Channel, Noise and Transition effects.
|
||||
|
||||
use effectcraft_color::luminance;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::{Image, gaussian_blur, hash_noise};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, category: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category, params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
/// AE's Blurriness is roughly a radius; our Gaussian uses sigma = blurriness / 2.
|
||||
fn blur_sigma(blurriness: f64) -> f64 {
|
||||
blurriness.max(0.0) * 0.5
|
||||
}
|
||||
|
||||
fn dims_xy(dim: u32) -> (f64, f64) {
|
||||
match dim {
|
||||
1 => (1.0, 0.0),
|
||||
2 => (0.0, 1.0),
|
||||
_ => (1.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn gaussian(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let s = blur_sigma(ctx.params.f("blurriness")) * b.scale;
|
||||
if s <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let (kx, ky) = dims_xy(ctx.params.e("dimensions"));
|
||||
let repeat = ctx.params.b("repeatEdge");
|
||||
if !repeat && !ctx.adjustment {
|
||||
b.pad((s * 3.0).ceil() as u32);
|
||||
}
|
||||
b.img = gaussian_blur(&b.img, s * kx, s * ky, repeat || ctx.adjustment);
|
||||
b
|
||||
}
|
||||
|
||||
fn box_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize;
|
||||
if r == 0 {
|
||||
return b;
|
||||
}
|
||||
let it = ctx.params.f("iterations").round().clamp(1.0, 50.0) as usize;
|
||||
let (kx, ky) = dims_xy(ctx.params.e("dimensions"));
|
||||
let repeat = ctx.params.b("repeatEdge");
|
||||
if !repeat && !ctx.adjustment {
|
||||
b.pad((r * it) as u32 + 1);
|
||||
}
|
||||
b.img = effectcraft_raster::box_blur(&b.img, r * kx as usize, r * ky as usize, it, repeat || ctx.adjustment);
|
||||
b
|
||||
}
|
||||
|
||||
fn directional(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let len = ctx.params.f("length") * b.scale;
|
||||
if len < 0.5 {
|
||||
return b;
|
||||
}
|
||||
if !ctx.adjustment {
|
||||
b.pad(len.ceil() as u32 + 1);
|
||||
}
|
||||
b.img = effectcraft_raster::directional_blur(&b.img, ctx.params.f("direction"), len);
|
||||
b
|
||||
}
|
||||
|
||||
fn radial(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let amt = ctx.params.f("amount");
|
||||
let zoom = ctx.params.e("type") == 1;
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
b.img = effectcraft_raster::radial_blur(&b.img, c, if zoom { amt / 100.0 } else { amt }, zoom);
|
||||
b
|
||||
}
|
||||
|
||||
fn unsharp_core(img: &Image, amount: f32, sigma: f64, threshold: f32) -> Image {
|
||||
let blurred = gaussian_blur(img, sigma, sigma, true);
|
||||
let mut out = img.clone();
|
||||
out.data.par_iter_mut().zip(blurred.data.par_iter()).for_each(|(o, bl)| {
|
||||
for c in 0..3 {
|
||||
let d = o[c] - bl[c];
|
||||
if d.abs() * 255.0 >= threshold {
|
||||
o[c] = (o[c] + d * amount).max(0.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
fn sharpen(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let a = ctx.params.f("amount") as f32 / 50.0;
|
||||
b.img = unsharp_core(&b.img, a, 1.0 * b.scale, 0.0);
|
||||
b
|
||||
}
|
||||
|
||||
fn unsharp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let a = ctx.params.f("amount") as f32 / 100.0;
|
||||
b.img = unsharp_core(&b.img, a, (ctx.params.f("radius") * b.scale).max(0.1), ctx.params.f("threshold") as f32);
|
||||
b
|
||||
}
|
||||
|
||||
fn glow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let thr = ctx.params.f("threshold") as f32 / 100.0;
|
||||
let radius = ctx.params.f("radius") * b.scale;
|
||||
let intensity = ctx.params.f("intensity") as f32;
|
||||
let use_colors = ctx.params.e("colors") == 1;
|
||||
let ca = ctx.params.color("colorA");
|
||||
let cb = ctx.params.color("colorB");
|
||||
if !ctx.adjustment {
|
||||
b.pad((radius * 1.5).ceil() as u32 + 2);
|
||||
}
|
||||
// Bright pass.
|
||||
let mut bright = b.img.clone();
|
||||
bright.data.par_iter_mut().for_each(|px| {
|
||||
let a = px[3];
|
||||
if a <= 0.0 {
|
||||
*px = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
let l = luminance(px[0] / a, px[1] / a, px[2] / a);
|
||||
let k = ((l - thr) / (1.0 - thr).max(1e-3)).clamp(0.0, 1.0);
|
||||
if use_colors {
|
||||
let t = l.clamp(0.0, 1.0);
|
||||
let c = [ca[0] + (cb[0] - ca[0]) * t, ca[1] + (cb[1] - ca[1]) * t, ca[2] + (cb[2] - ca[2]) * t];
|
||||
*px = [c[0] * k * a, c[1] * k * a, c[2] * k * a, k * a];
|
||||
} else {
|
||||
*px = px.map(|v| v * k);
|
||||
}
|
||||
});
|
||||
let s = (radius / 2.0).max(0.5);
|
||||
let blurred = gaussian_blur(&bright, s, s, false);
|
||||
let composite_behind = ctx.params.e("operation") == 1;
|
||||
b.img.data.par_iter_mut().zip(blurred.data.par_iter()).for_each(|(o, g)| {
|
||||
let g = g.map(|v| v * intensity);
|
||||
if composite_behind {
|
||||
let k = 1.0 - o[3];
|
||||
for c in 0..4 {
|
||||
o[c] += g[c] * k;
|
||||
}
|
||||
} else {
|
||||
// Add (screen-like add on premultiplied colour; alpha grows by the glow's alpha).
|
||||
for c in 0..3 {
|
||||
o[c] += g[c];
|
||||
}
|
||||
o[3] = (o[3] + g[3] * (1.0 - o[3])).min(1.0);
|
||||
}
|
||||
o[3] = o[3].clamp(0.0, 1.0);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn drop_shadow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let color = ctx.params.color("color");
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let dir = ctx.params.f("direction").to_radians();
|
||||
let dist = ctx.params.f("distance") * b.scale;
|
||||
let soft = ctx.params.f("softness") * b.scale;
|
||||
let only = ctx.params.b("shadowOnly");
|
||||
let pad = (dist + soft * 1.5).ceil() as u32 + 2;
|
||||
if !ctx.adjustment {
|
||||
b.pad(pad);
|
||||
}
|
||||
let (dx, dy) = (dir.sin() * dist, -dir.cos() * dist);
|
||||
let mut sh = Image::new(b.img.width, b.img.height);
|
||||
let src = &b.img;
|
||||
sh.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let a = src.sample_bilinear(x as f64 + 0.5 - dx, y as f64 + 0.5 - dy)[3] * opacity * color[3];
|
||||
*px = [color[0] * a, color[1] * a, color[2] * a, a];
|
||||
}
|
||||
});
|
||||
if soft > 0.0 {
|
||||
sh = gaussian_blur(&sh, soft / 2.0, soft / 2.0, false);
|
||||
}
|
||||
if !only {
|
||||
sh.data.par_iter_mut().zip(b.img.data.par_iter()).for_each(|(s, o)| {
|
||||
let k = 1.0 - o[3];
|
||||
for c in 0..4 {
|
||||
s[c] = o[c] + s[c] * k;
|
||||
}
|
||||
});
|
||||
}
|
||||
b.img = sh;
|
||||
b
|
||||
}
|
||||
|
||||
fn invert(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ch = ctx.params.e("channel");
|
||||
let blend = 1.0 - ctx.params.f("blend") as f32 / 100.0;
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let a = px[3];
|
||||
if ch == 4 {
|
||||
let na = 1.0 - a;
|
||||
let k = if a > 0.0 { na / a } else { 0.0 };
|
||||
let inv = [px[0] * k, px[1] * k, px[2] * k, na];
|
||||
for c in 0..4 {
|
||||
px[c] += (inv[c] - px[c]) * blend;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if a <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let s = [px[0] / a, px[1] / a, px[2] / a];
|
||||
let mut o = s;
|
||||
match ch {
|
||||
1 => o[0] = 1.0 - s[0],
|
||||
2 => o[1] = 1.0 - s[1],
|
||||
3 => o[2] = 1.0 - s[2],
|
||||
_ => o = [1.0 - s[0], 1.0 - s[1], 1.0 - s[2]],
|
||||
}
|
||||
for c in 0..3 {
|
||||
px[c] = (s[c] + (o[c] - s[c]) * blend) * a;
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn mosaic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let hb = ctx.params.f("horizontal").round().max(1.0) as usize;
|
||||
let vb = ctx.params.f("vertical").round().max(1.0) as usize;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let cw = (w as f64 / hb as f64).max(1.0);
|
||||
let ch = (h as f64 / vb as f64).max(1.0);
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
let cy = ((y as f64 / ch).floor() + 0.5) * ch;
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let cx = ((x as f64 / cw).floor() + 0.5) * cw;
|
||||
*px = src.get_clamped(cx as i64, cy as i64);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn posterize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let lv = ctx.params.f("level").round().clamp(2.0, 255.0) as f32 - 1.0;
|
||||
b.img.map_straight(|c| c.map(|v| (v.clamp(0.0, 1.0) * lv).round() / lv));
|
||||
b
|
||||
}
|
||||
|
||||
fn threshold(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let t = ctx.params.f("level") as f32 / 255.0;
|
||||
b.img.map_straight(|c| {
|
||||
let v = if luminance(c[0], c[1], c[2]) >= t { 1.0 } else { 0.0 };
|
||||
[v, v, v]
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn find_edges(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let inv = ctx.params.b("invert");
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let l = |dx: i64, dy: i64| {
|
||||
let p = src.get_clamped(x as i64 + dx, y as i64 + dy);
|
||||
luminance(p[0], p[1], p[2])
|
||||
};
|
||||
let gx = l(1, -1) + 2.0 * l(1, 0) + l(1, 1) - l(-1, -1) - 2.0 * l(-1, 0) - l(-1, 1);
|
||||
let gy = l(-1, 1) + 2.0 * l(0, 1) + l(1, 1) - l(-1, -1) - 2.0 * l(0, -1) - l(1, -1);
|
||||
let e = (gx * gx + gy * gy).sqrt().min(1.0);
|
||||
let v = if inv { e } else { 1.0 - e };
|
||||
let a = px[3];
|
||||
*px = [v * a, v * a, v * a, a];
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn emboss(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ang = ctx.params.f("direction").to_radians();
|
||||
let relief = ctx.params.f("relief") * b.scale;
|
||||
let contrast = ctx.params.f("contrast") as f32 / 100.0;
|
||||
let (dx, dy) = (ang.cos() * relief, -ang.sin() * relief);
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let a = src.sample_bilinear_clamped(x as f64 + 0.5 + dx, y as f64 + 0.5 + dy);
|
||||
let c = src.sample_bilinear_clamped(x as f64 + 0.5 - dx, y as f64 + 0.5 - dy);
|
||||
let d = luminance(a[0], a[1], a[2]) - luminance(c[0], c[1], c[2]);
|
||||
let v = (0.5 + d * contrast).clamp(0.0, 1.0);
|
||||
let al = px[3];
|
||||
*px = [v * al, v * al, v * al, al];
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn noise(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let amt = ctx.params.f("amount") as f32 / 100.0;
|
||||
let color = ctx.params.b("color");
|
||||
let frame_seed = (ctx.time * 1000.0) as u32 ^ ctx.seed;
|
||||
let w = b.img.width;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let a = px[3];
|
||||
if a <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let n = |k: u32| (hash_noise(x as u32 + k * w, y as u32, frame_seed) - 0.5) * amt;
|
||||
let (r, g, bl) = if color { (n(0), n(1), n(2)) } else { let v = n(0); (v, v, v) };
|
||||
px[0] = (px[0] + r * a).clamp(0.0, a);
|
||||
px[1] = (px[1] + g * a).clamp(0.0, a);
|
||||
px[2] = (px[2] + bl * a).clamp(0.0, a);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn linear_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let done = ctx.params.f("completion") / 100.0;
|
||||
let ang = ctx.params.f("angle").to_radians();
|
||||
let feather = (ctx.params.f("feather") * b.scale).max(0.001);
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
// Direction the wipe travels: angle 90° wipes left→right.
|
||||
let (dx, dy) = (ang.sin(), -ang.cos());
|
||||
let corners = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)];
|
||||
let proj: Vec<f64> = corners.iter().map(|(x, y)| x * dx + y * dy).collect();
|
||||
let (lo, hi) = (proj.iter().cloned().fold(f64::INFINITY, f64::min), proj.iter().cloned().fold(f64::NEG_INFINITY, f64::max));
|
||||
let edge = lo - feather + (hi - lo + 2.0 * feather) * done;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let d = (x as f64 + 0.5) * dx + (y as f64 + 0.5) * dy;
|
||||
let k = ((d - edge) / feather + 0.5).clamp(0.0, 1.0) as f32;
|
||||
for c in px.iter_mut() {
|
||||
*c *= k;
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn radial_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let done = ctx.params.f("completion") / 100.0;
|
||||
let start = ctx.params.f("startAngle");
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let dir = ctx.params.e("wipe");
|
||||
let feather = (ctx.params.f("feather")).max(0.01);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let a = ((x as f64 + 0.5 - c.0).atan2(-(y as f64 + 0.5 - c.1)).to_degrees() - start).rem_euclid(360.0);
|
||||
let a = if dir == 1 { 360.0 - a } else { a };
|
||||
let edge = done * 360.0;
|
||||
let k = ((a - edge) / feather + 0.5).clamp(0.0, 1.0) as f32;
|
||||
for ch in px.iter_mut() {
|
||||
*ch *= k;
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn venetian(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let done = ctx.params.f("completion") / 100.0;
|
||||
let ang = ctx.params.f("direction").to_radians();
|
||||
let width = (ctx.params.f("width") * b.scale).max(1.0);
|
||||
let feather = (ctx.params.f("feather") * b.scale).max(0.001);
|
||||
let (dx, dy) = (ang.cos(), ang.sin());
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let d = ((x as f64 + 0.5) * dx + (y as f64 + 0.5) * dy).rem_euclid(width);
|
||||
let k = ((d - done * width) / feather + 0.5).clamp(0.0, 1.0) as f32;
|
||||
for c in px.iter_mut() {
|
||||
*c *= k;
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let dims = || popup(&["Horizontal and Vertical", "Horizontal", "Vertical"]);
|
||||
vec![
|
||||
spec(
|
||||
"ec.blur.gaussian",
|
||||
"Gaussian Blur",
|
||||
"Blur & Sharpen",
|
||||
vec![
|
||||
p("blurriness", "Blurriness", num(0.0), slider(0.0, 3000.0, 0.0, 50.0, 1)),
|
||||
p("dimensions", "Blur Dimensions", Value::Enum(0), dims()),
|
||||
p("repeatEdge", "Repeat Edge Pixels", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
gaussian,
|
||||
),
|
||||
spec(
|
||||
"ec.blur.fastbox",
|
||||
"Fast Box Blur",
|
||||
"Blur & Sharpen",
|
||||
vec![
|
||||
p("radius", "Blur Radius", num(0.0), slider(0.0, 3000.0, 0.0, 50.0, 1)),
|
||||
p("iterations", "Iterations", num(3.0), slider(1.0, 50.0, 1.0, 5.0, 0)),
|
||||
p("dimensions", "Blur Dimensions", Value::Enum(0), dims()),
|
||||
p("repeatEdge", "Repeat Edge Pixels", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
box_blur,
|
||||
),
|
||||
spec(
|
||||
"ec.blur.directional",
|
||||
"Directional Blur",
|
||||
"Blur & Sharpen",
|
||||
vec![p("direction", "Direction", num(0.0), ParamUi::Angle), p("length", "Blur Length", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1))],
|
||||
directional,
|
||||
),
|
||||
spec(
|
||||
"ec.blur.radial",
|
||||
"Radial Blur",
|
||||
"Blur & Sharpen",
|
||||
vec![
|
||||
p("amount", "Amount", num(10.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("center", "Center", Value::Vec2([0.5, 0.5]), ParamUi::Point),
|
||||
p("type", "Type", Value::Enum(0), popup(&["Spin", "Zoom"])),
|
||||
],
|
||||
radial,
|
||||
),
|
||||
spec("ec.blur.sharpen", "Sharpen", "Blur & Sharpen", vec![p("amount", "Sharpen Amount", num(0.0), slider(0.0, 500.0, 0.0, 100.0, 0))], sharpen),
|
||||
spec(
|
||||
"ec.blur.unsharp",
|
||||
"Unsharp Mask",
|
||||
"Blur & Sharpen",
|
||||
vec![
|
||||
p("amount", "Amount", num(50.0), slider(0.0, 500.0, 0.0, 500.0, 0)),
|
||||
p("radius", "Radius", num(1.0), slider(0.1, 500.0, 0.1, 50.0, 1)),
|
||||
p("threshold", "Threshold", num(0.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||||
],
|
||||
unsharp,
|
||||
),
|
||||
spec(
|
||||
"ec.stylize.glow",
|
||||
"Glow",
|
||||
"Stylize",
|
||||
vec![
|
||||
p("based", "Glow Based On", Value::Enum(1), popup(&["Alpha Channel", "Color Channels"])),
|
||||
p("threshold", "Glow Threshold", num(60.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("radius", "Glow Radius", num(10.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("intensity", "Glow Intensity", num(1.0), slider(0.0, 255.0, 0.0, 4.0, 1)),
|
||||
p("operation", "Composite Original", Value::Enum(0), popup(&["On Top", "Behind", "None"])),
|
||||
p("colors", "Glow Colors", Value::Enum(0), popup(&["Original Colors", "A & B Colors"])),
|
||||
p("colorA", "Color A", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("colorB", "Color B", col(0.0, 0.0, 0.0), ParamUi::Color),
|
||||
],
|
||||
glow,
|
||||
),
|
||||
spec(
|
||||
"ec.stylize.mosaic",
|
||||
"Mosaic",
|
||||
"Stylize",
|
||||
vec![
|
||||
p("horizontal", "Horizontal Blocks", num(10.0), slider(1.0, 4000.0, 1.0, 200.0, 0)),
|
||||
p("vertical", "Vertical Blocks", num(10.0), slider(1.0, 4000.0, 1.0, 200.0, 0)),
|
||||
],
|
||||
mosaic,
|
||||
),
|
||||
spec("ec.stylize.posterize", "Posterize", "Stylize", vec![p("level", "Level", num(6.0), slider(2.0, 255.0, 2.0, 32.0, 0))], posterize),
|
||||
spec("ec.stylize.threshold", "Threshold", "Stylize", vec![p("level", "Level", num(128.0), slider(0.0, 255.0, 0.0, 255.0, 0))], threshold),
|
||||
spec("ec.stylize.findedges", "Find Edges", "Stylize", vec![p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox)], find_edges),
|
||||
spec(
|
||||
"ec.stylize.emboss",
|
||||
"Emboss",
|
||||
"Stylize",
|
||||
vec![
|
||||
p("direction", "Direction", num(45.0), ParamUi::Angle),
|
||||
p("relief", "Relief", num(1.5), slider(0.0, 10.0, 0.0, 10.0, 2)),
|
||||
p("contrast", "Contrast", num(100.0), slider(0.0, 500.0, 0.0, 500.0, 0)),
|
||||
],
|
||||
emboss,
|
||||
),
|
||||
spec(
|
||||
"ec.perspective.dropshadow",
|
||||
"Drop Shadow",
|
||||
"Perspective",
|
||||
vec![
|
||||
p("color", "Shadow Color", col(0.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("direction", "Direction", num(135.0), ParamUi::Angle),
|
||||
p("distance", "Distance", num(5.0), slider(0.0, 32000.0, 0.0, 120.0, 1)),
|
||||
p("softness", "Softness", num(0.0), slider(0.0, 1000.0, 0.0, 250.0, 1)),
|
||||
p("shadowOnly", "Shadow Only", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
drop_shadow,
|
||||
),
|
||||
spec(
|
||||
"ec.channel.invert",
|
||||
"Invert",
|
||||
"Channel",
|
||||
vec![
|
||||
p("channel", "Channel", Value::Enum(0), popup(&["RGB", "Red", "Green", "Blue", "Alpha"])),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
invert,
|
||||
),
|
||||
spec(
|
||||
"ec.noise.noise",
|
||||
"Noise",
|
||||
"Noise & Grain",
|
||||
vec![p("amount", "Amount of Noise", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), p("color", "Use Color Noise", Value::Bool(true), ParamUi::Checkbox)],
|
||||
noise,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.linearwipe",
|
||||
"Linear Wipe",
|
||||
"Transition",
|
||||
vec![
|
||||
p("completion", "Transition Completion", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("angle", "Wipe Angle", num(90.0), ParamUi::Angle),
|
||||
p("feather", "Feather", num(0.0), slider(0.0, 32000.0, 0.0, 500.0, 1)),
|
||||
],
|
||||
linear_wipe,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.radialwipe",
|
||||
"Radial Wipe",
|
||||
"Transition",
|
||||
vec![
|
||||
p("completion", "Transition Completion", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("startAngle", "Start Angle", num(0.0), ParamUi::Angle),
|
||||
p("center", "Wipe Center", Value::Vec2([0.5, 0.5]), ParamUi::Point),
|
||||
p("wipe", "Wipe", Value::Enum(0), popup(&["Clockwise", "Counterclockwise", "Both"])),
|
||||
p("feather", "Feather", num(0.0), slider(0.0, 360.0, 0.0, 50.0, 1)),
|
||||
],
|
||||
radial_wipe,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.venetian",
|
||||
"Venetian Blinds",
|
||||
"Transition",
|
||||
vec![
|
||||
p("completion", "Transition Completion", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("direction", "Direction", num(90.0), ParamUi::Angle),
|
||||
p("width", "Width", num(30.0), slider(2.0, 1000.0, 2.0, 200.0, 0)),
|
||||
p("feather", "Feather", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
venetian,
|
||||
),
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user