Merge main into M13.5 long-tail polish
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d0197a084c
|
|
@ -14,8 +14,10 @@
|
|||
//! all-passes, as in the published Freeverb description); chorus/flange/vibrato are modulated
|
||||
//! fractional delay lines. Compressor and Gate are feed-forward dynamics processors on a
|
||||
//! stereo-linked peak envelope (one-pole attack/release smoothing, the textbook quadratic soft-knee
|
||||
//! gain computer); Distortion is a static waveshaper (soft clip, hard clip, fold-back, bit crush)
|
||||
//! followed by a one-pole tone filter.
|
||||
//! gain computer, an optional program-dependent "auto" release and an output ceiling); Gate mutes
|
||||
//! below its threshold with attack / hold / release; Distortion is a static waveshaper (soft and
|
||||
//! hard clip, two saturation curves, tube, fuzz) with pre-gain, bit reduction, sample-and-hold
|
||||
//! downsampling, dry/wet mix and output volume.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
|
|
@ -40,11 +42,14 @@ fn pp(id: &'static str, name: &'static str, v: (Value, ParamUi)) -> crate::Param
|
|||
p(id, name, v.0, v.1)
|
||||
}
|
||||
|
||||
/// Distortion's Type options.
|
||||
pub const DISTORTION_TYPES: [&str; 6] = ["Soft Clip", "Hard Clip", "Saturation 1", "Saturation 2", "Tube", "Fuzz"];
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
const BANDS: [[&str; 8]; 3] = [
|
||||
["band1Enable", "Band 1 Enable", "band1Frequency", "Band 1 Frequency", "band1Bandwidth", "Band 1 Bandwidth", "band1BoostCut", "Band 1 Boost/Cut"],
|
||||
["band2Enable", "Band 2 Enable", "band2Frequency", "Band 2 Frequency", "band2Bandwidth", "Band 2 Bandwidth", "band2BoostCut", "Band 2 Boost/Cut"],
|
||||
["band3Enable", "Band 3 Enable", "band3Frequency", "Band 3 Frequency", "band3Bandwidth", "Band 3 Bandwidth", "band3BoostCut", "Band 3 Boost/Cut"],
|
||||
["band1Enable", "Band 1 Enabled", "band1Frequency", "Band 1 Frequency", "band1Bandwidth", "Band 1 Bandwidth", "band1BoostCut", "Band 1 Boost/Cut"],
|
||||
["band2Enable", "Band 2 Enabled", "band2Frequency", "Band 2 Frequency", "band2Bandwidth", "Band 2 Bandwidth", "band2BoostCut", "Band 2 Boost/Cut"],
|
||||
["band3Enable", "Band 3 Enabled", "band3Frequency", "Band 3 Frequency", "band3Bandwidth", "Band 3 Bandwidth", "band3BoostCut", "Band 3 Boost/Cut"],
|
||||
];
|
||||
let mut eq = vec![];
|
||||
for (b, (f, on)) in BANDS.iter().zip([(200.0, true), (1000.0, false), (5000.0, false)]) {
|
||||
|
|
@ -67,14 +72,14 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.audio.compressor",
|
||||
"Compressor",
|
||||
vec![
|
||||
p("threshold", "Threshold (dB)", num(-20.0), slider(-60.0, 0.0, -60.0, 0.0, 2)),
|
||||
p("ratio", "Ratio", num(4.0), slider(1.0, 30.0, 1.0, 20.0, 2)),
|
||||
p("attack", "Attack (ms)", num(10.0), slider(0.0, 500.0, 0.0, 200.0, 2)),
|
||||
p("release", "Release (ms)", num(100.0), slider(1.0, 5000.0, 1.0, 1000.0, 2)),
|
||||
p("knee", "Knee (dB)", num(0.0), slider(0.0, 24.0, 0.0, 24.0, 2)),
|
||||
p("makeupGain", "Makeup Gain (dB)", num(0.0), slider(-24.0, 36.0, 0.0, 24.0, 2)),
|
||||
pp("dryOut", "Dry Out", pct(0.0)),
|
||||
pp("wetOut", "Wet Out", pct(100.0)),
|
||||
p("threshold", "Threshold (dB)", num(-16.0), slider(-60.0, 0.0, -60.0, 0.0, 2)),
|
||||
p("ratio", "Ratio (x:1)", num(3.0), slider(0.4, 30.0, 0.4, 30.0, 2)),
|
||||
p("knee", "Knee (dB)", num(15.0), slider(0.0, 30.0, 0.0, 30.0, 2)),
|
||||
p("attack", "Attack (ms)", num(6.0), slider(0.0, 400.0, 0.0, 400.0, 2)),
|
||||
p("release", "Release (ms)", num(440.0), slider(1.0, 4000.0, 1.0, 4000.0, 2)),
|
||||
p("autoRelease", "Auto Release", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("makeupGain", "Makeup Gain (dB)", num(0.0), slider(-30.0, 30.0, -30.0, 30.0, 2)),
|
||||
p("outputLimit", "Output Limit (dB)", num(0.0), slider(-30.0, 0.0, -30.0, 0.0, 2)),
|
||||
],
|
||||
),
|
||||
spec(
|
||||
|
|
@ -92,13 +97,13 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.audio.distortion",
|
||||
"Distortion",
|
||||
vec![
|
||||
p("distortionType", "Distortion Type", Value::Enum(0), popup(&["Soft Clip", "Hard Clip", "Fold Back", "Bit Crush"])),
|
||||
p("drive", "Drive (dB)", num(12.0), slider(0.0, 48.0, 0.0, 48.0, 2)),
|
||||
p("bitDepth", "Bit Depth", num(8.0), slider(1.0, 16.0, 1.0, 16.0, 0)),
|
||||
p("tone", "Tone (Hz)", num(8000.0), slider(200.0, 20000.0, 200.0, 20000.0, 1)),
|
||||
p("outputGain", "Output Gain (dB)", num(-6.0), slider(-48.0, 12.0, -24.0, 12.0, 2)),
|
||||
pp("dryOut", "Dry Out", pct(0.0)),
|
||||
pp("wetOut", "Wet Out", pct(100.0)),
|
||||
p("distortionType", "Type", Value::Enum(0), popup(&DISTORTION_TYPES)),
|
||||
pp("drive", "Drive", pct(25.0)),
|
||||
p("gain", "Gain", num(25.0), slider(0.0, 300.0, 0.0, 300.0, 2)),
|
||||
pp("mix", "Mix", pct(100.0)),
|
||||
p("volume", "Volume", num(11.0), slider(0.0, 100.0, 0.0, 100.0, 2)),
|
||||
p("bitDepth", "Resolution (bit)", num(24.0), slider(1.0, 24.0, 1.0, 24.0, 0)),
|
||||
p("downsample", "Downsample (x)", num(1.0), slider(1.0, 64.0, 1.0, 32.0, 0)),
|
||||
],
|
||||
),
|
||||
spec(
|
||||
|
|
@ -120,11 +125,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.audio.gate",
|
||||
"Gate",
|
||||
vec![
|
||||
p("threshold", "Threshold (dB)", num(-40.0), slider(-90.0, 0.0, -80.0, 0.0, 2)),
|
||||
p("attack", "Attack (ms)", num(1.0), slider(0.0, 500.0, 0.0, 100.0, 2)),
|
||||
p("hold", "Hold (ms)", num(50.0), slider(0.0, 5000.0, 0.0, 500.0, 2)),
|
||||
p("release", "Release (ms)", num(100.0), slider(1.0, 5000.0, 1.0, 1000.0, 2)),
|
||||
p("range", "Range (dB)", num(-80.0), slider(-120.0, 0.0, -90.0, 0.0, 2)),
|
||||
p("threshold", "Threshold (dB)", num(-60.0), slider(-60.0, 0.0, -60.0, 0.0, 2)),
|
||||
p("attack", "Attack (ms)", num(10.0), slider(0.0, 1000.0, 0.0, 1000.0, 2)),
|
||||
p("hold", "Hold (ms)", num(40.0), slider(0.0, 2500.0, 0.0, 2500.0, 2)),
|
||||
p("release", "Release (ms)", num(220.0), slider(5.0, 4000.0, 5.0, 4000.0, 2)),
|
||||
],
|
||||
),
|
||||
spec(
|
||||
|
|
@ -175,7 +179,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.audio.tone",
|
||||
"Tone",
|
||||
vec![
|
||||
p("waveformOptions", "Waveform Options", Value::Enum(0), popup(&["Sine", "Triangle", "Saw", "Square"])),
|
||||
p("waveformOptions", "Waveform Options", Value::Enum(0), popup(&["Sine", "Triangle", "Saw", "Square", "White Noise"])),
|
||||
p("frequency1", "Frequency 1", num(440.0), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
|
||||
p("frequency2", "Frequency 2", num(493.88), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
|
||||
p("frequency3", "Frequency 3", num(587.33), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
|
||||
|
|
@ -211,7 +215,8 @@ pub fn preroll(id: &str, params: &Params) -> f64 {
|
|||
"ec.audio.distortion" => 0.01,
|
||||
// The envelope settles within a few attack/release time constants.
|
||||
"ec.audio.compressor" | "ec.audio.gate" => {
|
||||
let rel = params.f("release").max(1.0) / 1000.0;
|
||||
let auto = id == "ec.audio.compressor" && params.b("autoRelease");
|
||||
let rel = if auto { AUTO_RELEASE_SLOW_MS } else { params.f("release").max(1.0) } / 1000.0;
|
||||
let att = params.f("attack").max(0.0) / 1000.0;
|
||||
let hold = if id == "ec.audio.gate" { params.f("hold").max(0.0) / 1000.0 } else { 0.0 };
|
||||
(9.0 * rel + 9.0 * att + hold + 0.01).min(10.0)
|
||||
|
|
@ -281,7 +286,7 @@ pub fn process(id: &str, params: &Params, buf: &mut [f32], rate: u32, start: f64
|
|||
}
|
||||
"ec.audio.delay" => delay(params, buf, sr),
|
||||
"ec.audio.compressor" => compressor(params, buf, sr),
|
||||
"ec.audio.distortion" => distortion(params, buf, sr),
|
||||
"ec.audio.distortion" => distortion(params, buf, sr, start),
|
||||
"ec.audio.gate" => gate(params, buf, sr),
|
||||
"ec.audio.flangechorus" => flange_chorus(params, buf, sr, start),
|
||||
"ec.audio.modulator" => modulator(params, buf, sr, start),
|
||||
|
|
@ -432,7 +437,8 @@ fn smoothing(ms: f64, sr: f64) -> f64 {
|
|||
/// Static compressor curve: gain change (dB, ≤ 0) for an input level `x` dB (quadratic soft
|
||||
/// knee of width `knee` around the threshold).
|
||||
pub fn compressor_gain_db(x: f64, threshold: f64, ratio: f64, knee: f64) -> f64 {
|
||||
let r = ratio.max(1.0);
|
||||
// Ratios below 1:1 expand above the threshold.
|
||||
let r = ratio.max(0.1);
|
||||
let over = x - threshold;
|
||||
let y = if knee > 0.0 && 2.0 * over.abs() <= knee {
|
||||
x + (1.0 / r - 1.0) * (over + knee / 2.0).powi(2) / (2.0 * knee)
|
||||
|
|
@ -444,25 +450,41 @@ pub fn compressor_gain_db(x: f64, threshold: f64, ratio: f64, knee: f64) -> f64
|
|||
y - x
|
||||
}
|
||||
|
||||
/// Auto Release: a program-dependent release from two envelopes. A fast one (the Attack time, a
|
||||
/// short release) follows transients; a slow one (slow attack and release) only builds up under
|
||||
/// sustained compression. The deeper of the two applies, so brief peaks recover quickly and long
|
||||
/// loud passages slowly.
|
||||
const AUTO_RELEASE_FAST_MS: f64 = 60.0;
|
||||
const AUTO_RELEASE_SLOW_ATTACK_MS: f64 = 150.0;
|
||||
const AUTO_RELEASE_SLOW_MS: f64 = 900.0;
|
||||
|
||||
fn compressor(params: &Params, buf: &mut [f32], sr: f64) {
|
||||
let th = params.f("threshold");
|
||||
let ratio = params.f("ratio").max(1.0);
|
||||
let ratio = params.f("ratio").max(0.1);
|
||||
let knee = params.f("knee").max(0.0);
|
||||
let att = smoothing(params.f("attack"), sr);
|
||||
let rel = smoothing(params.f("release").max(0.001), sr);
|
||||
let auto = params.b("autoRelease");
|
||||
let rel = smoothing(if auto { AUTO_RELEASE_FAST_MS } else { params.f("release").max(0.001) }, sr);
|
||||
let (att_s, rel_s) = (smoothing(AUTO_RELEASE_SLOW_ATTACK_MS, sr), smoothing(AUTO_RELEASE_SLOW_MS, sr));
|
||||
let makeup = params.f("makeupGain");
|
||||
let dry = (params.f("dryOut") / 100.0) as f32;
|
||||
let wet = (params.f("wetOut") / 100.0) as f32;
|
||||
// Smoothed gain change in dB (≤ 0): attack while the reduction grows, release as it shrinks.
|
||||
let mut g = 0.0f64;
|
||||
// Output Limit: a final ceiling (brick-wall clip) so the result never exceeds it.
|
||||
let limit = db_to_lin(params.f("outputLimit").min(0.0)) as f32;
|
||||
// Smoothed gain change in dB: attack while the reduction grows, release as it shrinks.
|
||||
let (mut g, mut gs) = (0.0f64, 0.0f64);
|
||||
for f in buf.chunks_exact_mut(2) {
|
||||
let peak = f[0].abs().max(f[1].abs()) as f64;
|
||||
let target = compressor_gain_db(lin_to_db(peak), th, ratio, knee);
|
||||
let k = if target < g { att } else { rel };
|
||||
g = target + (g - target) * k;
|
||||
let lin = db_to_lin(g + makeup) as f32;
|
||||
let mut eff = g;
|
||||
if auto {
|
||||
let k = if target < gs { att_s } else { rel_s };
|
||||
gs = target + (gs - target) * k;
|
||||
eff = g.min(gs);
|
||||
}
|
||||
let lin = db_to_lin(eff + makeup) as f32;
|
||||
for v in f.iter_mut() {
|
||||
*v = *v * dry + *v * lin * wet;
|
||||
*v = (*v * lin).clamp(-limit, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -472,8 +494,7 @@ fn gate(params: &Params, buf: &mut [f32], sr: f64) {
|
|||
let att = smoothing(params.f("attack"), sr);
|
||||
let rel = smoothing(params.f("release").max(0.001), sr);
|
||||
let hold = (params.f("hold").max(0.0) / 1000.0 * sr) as usize;
|
||||
let floor = db_to_lin(params.f("range").min(0.0));
|
||||
// Openness 0..1 (closed..open), mapped to floor..1.
|
||||
// Openness 0..1 (closed..open) is the gain: a closed gate mutes.
|
||||
let mut g = 0.0f64;
|
||||
let mut held = 0usize;
|
||||
for f in buf.chunks_exact_mut(2) {
|
||||
|
|
@ -490,46 +511,67 @@ fn gate(params: &Params, buf: &mut [f32], sr: f64) {
|
|||
let target = if open { 1.0 } else { 0.0 };
|
||||
let k = if target > g { att } else { rel };
|
||||
g = target + (g - target) * k;
|
||||
let lin = (floor + (1.0 - floor) * g) as f32;
|
||||
let lin = g as f32;
|
||||
f[0] *= lin;
|
||||
f[1] *= lin;
|
||||
}
|
||||
}
|
||||
|
||||
/// The Distortion effect's waveshaper for one sample (`drive` linear gain, `bits` for Bit Crush).
|
||||
pub fn distort_sample(kind: u32, x: f32, drive: f32, bits: f32) -> f32 {
|
||||
let v = x * drive;
|
||||
/// The Distortion effect's waveshaper for one sample: `kind` indexes [`DISTORTION_TYPES`],
|
||||
/// `drive` (0..1) sets how hard the curve bends. The result stays within ±1.
|
||||
pub fn distort_sample(kind: u32, x: f32, drive: f32) -> f32 {
|
||||
let v = x * (1.0 + drive.clamp(0.0, 1.0) * 24.0);
|
||||
match kind {
|
||||
1 => v.clamp(-1.0, 1.0),
|
||||
2 => {
|
||||
// Fold back: reflect at ±1 (a triangle function of the input).
|
||||
let t = (v + 1.0).rem_euclid(4.0);
|
||||
if t < 2.0 { t - 1.0 } else { 3.0 - t }
|
||||
}
|
||||
// Hard clip: linear up to a flat ceiling.
|
||||
1 => v,
|
||||
// Saturation 1: rational curve, gentler knee than tanh.
|
||||
2 => v / (1.0 + v.abs()),
|
||||
// Saturation 2: cubic soft saturator (odd harmonics), flat beyond ±1.5.
|
||||
3 => {
|
||||
let q = 2f32.powf(bits.clamp(1.0, 16.0) - 1.0);
|
||||
(v.clamp(-1.0, 1.0) * q).round() / q
|
||||
let c = v.clamp(-1.5, 1.5);
|
||||
c - c * c * c * 4.0 / 27.0
|
||||
}
|
||||
// Tube: asymmetric — the positive half saturates harder (even harmonics).
|
||||
4 => {
|
||||
if v >= 0.0 {
|
||||
v.tanh()
|
||||
} else {
|
||||
(0.6 * v).tanh()
|
||||
}
|
||||
}
|
||||
// Fuzz: near-square exponential clipper.
|
||||
5 => v.signum() * (1.0 - (-3.0 * v.abs()).exp()),
|
||||
// Soft clip.
|
||||
_ => v.tanh(),
|
||||
}
|
||||
.clamp(-1.0, 1.0)
|
||||
}
|
||||
|
||||
fn distortion(params: &Params, buf: &mut [f32], sr: f64) {
|
||||
fn distortion(params: &Params, buf: &mut [f32], sr: f64, start: f64) {
|
||||
let kind = params.e("distortionType");
|
||||
let drive = db_to_lin(params.f("drive")) as f32;
|
||||
let bits = params.f("bitDepth") as f32;
|
||||
let out = db_to_lin(params.f("outputGain")) as f32;
|
||||
let dry = (params.f("dryOut") / 100.0) as f32;
|
||||
let wet = (params.f("wetOut") / 100.0) as f32;
|
||||
let fc = params.f("tone").clamp(20.0, sr * 0.49);
|
||||
let a = (-2.0 * PI * fc / sr).exp() as f32;
|
||||
let mut lp = [0.0f32; 2];
|
||||
for f in buf.chunks_exact_mut(2) {
|
||||
let drive = (params.f("drive") / 100.0) as f32;
|
||||
// Gain: pre-distortion amplification (25 = unity); Volume: output level (11 = unity).
|
||||
let pre = (params.f("gain").max(0.0) / 25.0) as f32;
|
||||
let mix = (params.f("mix") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let vol = (params.f("volume").max(0.0) / 11.0) as f32;
|
||||
let bits = params.f("bitDepth").round().clamp(1.0, 24.0);
|
||||
let q = (bits < 24.0).then(|| 2f32.powf(bits as f32 - 1.0));
|
||||
let down = params.f("downsample").round().clamp(1.0, 64.0) as i64;
|
||||
// Sample-and-hold on absolute sample positions, so blocks agree with one long run.
|
||||
let first = (start * sr).round() as i64;
|
||||
let mut held = [0.0f32; 2];
|
||||
for (i, f) in buf.chunks_exact_mut(2).enumerate() {
|
||||
let take = i == 0 || (first + i as i64).rem_euclid(down) == 0;
|
||||
for ch in 0..2 {
|
||||
let x = f[ch];
|
||||
let y = distort_sample(kind, x, drive, bits);
|
||||
lp[ch] = y + (lp[ch] - y) * a;
|
||||
f[ch] = x * dry + lp[ch] * out * wet;
|
||||
let mut y = distort_sample(kind, x * pre, drive);
|
||||
if let Some(q) = q {
|
||||
y = (y * q).round() / q;
|
||||
}
|
||||
if take {
|
||||
held[ch] = y;
|
||||
}
|
||||
f[ch] = (x * (1.0 - mix) + held[ch] * mix) * vol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -681,6 +723,12 @@ fn tone(params: &Params, buf: &mut [f32], sr: f64, start: f64) {
|
|||
-1.0
|
||||
}
|
||||
}
|
||||
// White noise: a hash of the absolute sample position (every frequency alike).
|
||||
4 => {
|
||||
let h = ((t * sr).round() as i64 as u64 ^ f.to_bits()).wrapping_mul(0x9e37_79b9_7f4a_7c15);
|
||||
let h = (h ^ (h >> 29)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||
((h ^ (h >> 32)) >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0
|
||||
}
|
||||
_ => (2.0 * PI * ph).sin(),
|
||||
};
|
||||
}
|
||||
|
|
@ -866,7 +914,10 @@ mod tests {
|
|||
assert!((compressor_gain_db(-17.0, -20.0, 4.0, k) - compressor_gain_db(-17.0, -20.0, 4.0, 0.0)).abs() < 1e-9);
|
||||
let sr = 48000.0;
|
||||
let mut p = defaults("ec.audio.compressor");
|
||||
p.values.insert("attack".into(), num(1.0));
|
||||
for (k, v) in [("attack", 1.0), ("threshold", -20.0), ("ratio", 4.0), ("knee", 0.0), ("release", 100.0)] {
|
||||
p.values.insert(k.into(), num(v));
|
||||
}
|
||||
p.values.insert("autoRelease".into(), Value::Bool(false));
|
||||
let mut loud = sine(1000.0, sr, 9600);
|
||||
process("ec.audio.compressor", &p, &mut loud, 48000, 0.0);
|
||||
// A full-scale sine (peak 0 dB) is pulled down by ~15 dB once the envelope settles.
|
||||
|
|
@ -883,8 +934,9 @@ mod tests {
|
|||
#[test]
|
||||
fn gate_mutes_below_threshold() {
|
||||
let sr = 48000.0;
|
||||
let p = defaults("ec.audio.gate");
|
||||
// A −60 dB signal under the −40 dB threshold is attenuated by the −80 dB range.
|
||||
let mut p = defaults("ec.audio.gate");
|
||||
p.values.insert("threshold".into(), num(-40.0));
|
||||
// A −60 dB signal under the −40 dB threshold is muted.
|
||||
let mut quiet: Vec<f32> = sine(500.0, sr, 9600).iter().map(|v| v * 0.001).collect();
|
||||
process("ec.audio.gate", &p, &mut quiet, 48000, 0.0);
|
||||
assert!(rms(&quiet, 4800) < 1e-6, "{}", rms(&quiet, 4800));
|
||||
|
|
@ -903,10 +955,16 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn distortion_shapes_and_bounds() {
|
||||
assert!((distort_sample(0, 0.5, 1.0, 8.0) - 0.5f32.tanh()).abs() < 1e-6);
|
||||
assert_eq!(distort_sample(1, 0.8, 2.0, 8.0), 1.0);
|
||||
assert!((distort_sample(2, 0.7, 2.0, 8.0) - 0.6).abs() < 1e-5);
|
||||
assert_eq!(distort_sample(3, 0.3, 1.0, 2.0), 0.5);
|
||||
assert!((distort_sample(0, 0.5, 0.0) - 0.5f32.tanh()).abs() < 1e-6);
|
||||
assert_eq!(distort_sample(1, 0.8, 1.0), 1.0);
|
||||
for k in 0..DISTORTION_TYPES.len() as u32 {
|
||||
for x in [-2.0f32, -0.3, 0.0, 0.2, 1.7] {
|
||||
let y = distort_sample(k, x, 0.5);
|
||||
assert!(y.abs() <= 1.0 && y.signum() * x.signum() >= 0.0, "{k} {x} {y}");
|
||||
}
|
||||
}
|
||||
// Tube is asymmetric.
|
||||
assert!(distort_sample(4, 0.1, 0.5) > -distort_sample(4, -0.1, 0.5));
|
||||
let p = defaults("ec.audio.distortion");
|
||||
let mut b = sine(200.0, 48000.0, 4800);
|
||||
process("ec.audio.distortion", &p, &mut b, 48000, 0.0);
|
||||
|
|
@ -914,4 +972,54 @@ mod tests {
|
|||
assert!(rms(&b, 100) > 0.1);
|
||||
assert!(preroll("ec.audio.compressor", &defaults("ec.audio.compressor")) > 0.4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distortion_mix_resolution_and_downsample() {
|
||||
let sr = 48000.0;
|
||||
let src: Vec<f32> = sine(300.0, sr, 480).iter().map(|v| v * 0.5).collect();
|
||||
// Mix 0 with unity volume passes the input through.
|
||||
let mut p = defaults("ec.audio.distortion");
|
||||
p.values.insert("mix".into(), num(0.0));
|
||||
let mut b = src.clone();
|
||||
process("ec.audio.distortion", &p, &mut b, 48000, 0.0);
|
||||
assert!(b.iter().zip(&src).all(|(a, s)| (a - s).abs() < 1e-6));
|
||||
// 2-bit resolution leaves at most 5 distinct levels (−1, −½, 0, ½, 1).
|
||||
let mut p = defaults("ec.audio.distortion");
|
||||
p.values.insert("bitDepth".into(), num(2.0));
|
||||
let mut b = src.clone();
|
||||
process("ec.audio.distortion", &p, &mut b, 48000, 0.0);
|
||||
let mut levels: Vec<i32> = b.iter().map(|v| (v * 1000.0).round() as i32).collect();
|
||||
levels.sort();
|
||||
levels.dedup();
|
||||
assert!(levels.len() <= 5, "{levels:?}");
|
||||
// Downsample ×4 holds each value for 4 frames.
|
||||
let mut p = defaults("ec.audio.distortion");
|
||||
p.values.insert("downsample".into(), num(4.0));
|
||||
let mut b = src.clone();
|
||||
process("ec.audio.distortion", &p, &mut b, 48000, 0.0);
|
||||
assert_eq!(b[0], b[6]);
|
||||
assert_ne!(b[6], b[8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressor_output_limit_and_auto_release() {
|
||||
let sr = 48000.0;
|
||||
let mut p = defaults("ec.audio.compressor");
|
||||
p.values.insert("outputLimit".into(), num(-12.0));
|
||||
p.values.insert("makeupGain".into(), num(20.0));
|
||||
let mut b = sine(1000.0, sr, 4800);
|
||||
process("ec.audio.compressor", &p, &mut b, 48000, 0.0);
|
||||
let ceiling = db_to_lin(-12.0) as f32;
|
||||
assert!(b.iter().all(|v| v.abs() <= ceiling + 1e-6));
|
||||
// After a short burst, auto release recovers faster than a long manual release.
|
||||
let burst = |auto: bool| {
|
||||
let mut p = defaults("ec.audio.compressor");
|
||||
p.values.insert("autoRelease".into(), Value::Bool(auto));
|
||||
p.values.insert("release".into(), num(4000.0));
|
||||
let mut b: Vec<f32> = sine(1000.0, sr, 9600).iter().enumerate().map(|(i, v)| if i < 960 { *v } else { v * 0.2 }).collect();
|
||||
process("ec.audio.compressor", &p, &mut b, 48000, 0.0);
|
||||
rms(&b, 4800)
|
||||
};
|
||||
assert!(burst(true) > burst(false) * 1.2, "{} {}", burst(true), burst(false));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,11 +193,12 @@ fn compound_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
return b;
|
||||
}
|
||||
let invert = ctx.params.b("invertBlur");
|
||||
// The blur map is the layer's own luminance (sampled before padding grows the buffer).
|
||||
if !ctx.adjustment {
|
||||
b.pad((max_sigma * 3.0).ceil() as u32 + 1);
|
||||
}
|
||||
let map = Plane::from_image(&b.img, |p| {
|
||||
// The blur map: the Blur Layer's luminance (stretched to fit or centred), else the layer's own.
|
||||
let map_img = crate::util::layer_or_self(ctx, &b, "blurLayer", true, ctx.params.b("stretchMapToFit"));
|
||||
let map = Plane::from_image(&map_img, |p| {
|
||||
let (c, a) = unpremul(p);
|
||||
let l = luminance(c[0], c[1], c[2]) * a;
|
||||
if invert { 1.0 - l } else { l }
|
||||
|
|
@ -281,23 +282,44 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if r < 0.5 {
|
||||
return b;
|
||||
}
|
||||
let shape = ctx.params.e("irisShape");
|
||||
let shape = ctx.params.e("irisProperties/irisShape");
|
||||
let sides = if shape >= 8 { 0 } else { shape + 3 };
|
||||
let spans = iris_spans(r, sides, ctx.params.f("irisRotation"), ctx.params.f("irisRoundness").clamp(0.0, 100.0) / 100.0);
|
||||
let mut spans = iris_spans(r, sides, ctx.params.f("irisProperties/irisRotation"), 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 {
|
||||
b.pad(r.ceil() as u32 + 1);
|
||||
b.pad(reach.ceil() as u32 + 1);
|
||||
}
|
||||
// Specular highlights: boost bright pixels before the blur so they bloom into bokeh shapes.
|
||||
let thr = ctx.params.f("specularThreshold") as f32 / 255.0;
|
||||
let boost = 1.0 + ctx.params.f("specularBrightness").max(0.0) as f32 / 100.0 * 4.0;
|
||||
let linear = ctx.params.b("useLinear");
|
||||
let to_lin = |px: &mut [f32; 4]| {
|
||||
let (c, a) = unpremul(*px);
|
||||
*px = premul(c.map(effectcraft_color::srgb_to_linear), a);
|
||||
};
|
||||
// Highlights: boost bright pixels before the blur so they bloom into bokeh shapes; Saturation
|
||||
// keeps their colour (100) or pushes the added energy towards white (0).
|
||||
let thr = ctx.params.f("highlight/specularThreshold") as f32 / 255.0;
|
||||
let boost = 1.0 + ctx.params.f("highlight/specularBrightness").max(0.0) as f32 / 100.0 * 4.0;
|
||||
let sat = (ctx.params.f("highlight/highlightSaturation") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let mut src = b.img.clone();
|
||||
if linear {
|
||||
src.data.par_iter_mut().for_each(to_lin);
|
||||
}
|
||||
if boost > 1.0 {
|
||||
src.data.par_iter_mut().for_each(|px| {
|
||||
let (c, a) = unpremul(*px);
|
||||
if a > 0.0 && luminance(c[0], c[1], c[2]) >= thr {
|
||||
let m = c[0].max(c[1]).max(c[2]);
|
||||
for k in 0..3 {
|
||||
px[k] *= boost;
|
||||
let tinted = c[k] + (m - c[k]) * (1.0 - sat);
|
||||
px[k] = (c[k] + tinted * (boost - 1.0)) * a;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -365,6 +387,10 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
|
@ -530,7 +556,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("radius", "Radius", num(3.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("threshold", "Threshold", num(25.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("mode", "Mode", Value::Enum(0), popup(&["Normal", "Edge Only", "Edge Overlay"])),
|
||||
p("mode", "Mode", Value::Enum(0), popup(&["Normal", "Edge Only", "Overlay Edge"])),
|
||||
],
|
||||
smart_blur,
|
||||
),
|
||||
|
|
@ -551,7 +577,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.blur.compound",
|
||||
"Compound Blur",
|
||||
vec![
|
||||
p("blurLayer", "Blur Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("maximumBlur", "Maximum Blur", num(20.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("stretchMapToFit", "Stretch Map to Fit", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("invertBlur", "Invert Blur", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
compound_blur,
|
||||
|
|
@ -562,16 +590,19 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("blurRadius", "Blur Radius", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||||
p(
|
||||
"irisShape",
|
||||
"Iris Shape",
|
||||
"irisProperties/irisShape",
|
||||
"Shape",
|
||||
Value::Enum(3),
|
||||
popup(&["Triangle", "Square", "Pentagon", "Hexagon", "Heptagon", "Octagon", "Nonagon", "Decagon", "Circle"]),
|
||||
),
|
||||
p("irisRotation", "Iris Rotation", num(0.0), ParamUi::Angle),
|
||||
p("irisRoundness", "Iris Roundness", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("specularThreshold", "Specular Threshold", num(255.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||||
p("specularBrightness", "Specular Brightness", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
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("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)),
|
||||
p("repeatEdge", "Repeat Edge Pixels", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("useLinear", "Use Linear Working Space", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
camera_lens_blur,
|
||||
),
|
||||
|
|
@ -685,6 +716,73 @@ mod tests {
|
|||
assert!(out.img.width > 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lens_blur_aspect_ratio_widens_the_bokeh() {
|
||||
let mut dot = Image::new(41, 41);
|
||||
dot.set(20, 20, [1.0, 1.0, 1.0, 1.0]);
|
||||
let wide = run("ec.blur.cameralens", &dot, &[("blurRadius", num(5.0)), ("irisProperties/irisAspectRatio", num(2.0))]);
|
||||
let (o, d) = (wide.offset[0] as i64, wide.offset[1] as i64);
|
||||
// Lit 8 px to the side but not 8 px above.
|
||||
assert!(wide.img.get(20 + o + 8, 20 + d)[3] > 0.0);
|
||||
assert_eq!(wide.img.get(20 + o, 20 + d - 8)[3], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lens_blur_highlight_saturation_whitens_boosted_colour() {
|
||||
let mut img = Image::new(21, 21);
|
||||
img.set(10, 10, [1.0, 0.2, 0.2, 1.0]);
|
||||
let over = |s: f64| {
|
||||
[
|
||||
("blurRadius", num(3.0)),
|
||||
("highlight/specularBrightness", num(100.0)),
|
||||
("highlight/specularThreshold", num(0.0)),
|
||||
("highlight/highlightSaturation", num(s)),
|
||||
]
|
||||
};
|
||||
let keep = run("ec.blur.cameralens", &img, &over(100.0));
|
||||
let white = run("ec.blur.cameralens", &img, &over(0.0));
|
||||
let c = |b: &Buf| b.img.get(10 + b.offset[0] as i64, 10 + b.offset[1] as i64);
|
||||
// Same red energy, more green with Saturation 0.
|
||||
assert!((c(&keep)[0] - c(&white)[0]).abs() < 1e-5);
|
||||
assert!(c(&white)[1] > c(&keep)[1] + 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_blur_reads_the_blur_layer() {
|
||||
use crate::{EffectEnv, EffectHost, LayerPixels};
|
||||
struct Map;
|
||||
impl EffectHost for Map {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||||
// Left half black (sharp), right half white (full blur).
|
||||
let mut m = Image::new(20, 20);
|
||||
for y in 0..20 {
|
||||
for x in 10..20 {
|
||||
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: [20.0, 20.0] })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
let mut img = Image::filled(20, 20, [0.0, 0.0, 0.0, 1.0]);
|
||||
for y in 0..20 {
|
||||
img.set(4, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
img.set(15, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
let out = crate::run_fx(
|
||||
"ec.blur.compound",
|
||||
&[("blurLayer", Value::Layer(Some(1))), ("maximumBlur", num(6.0))],
|
||||
img,
|
||||
0.0,
|
||||
EffectEnv { host: Some(&Map), ..Default::default() },
|
||||
);
|
||||
let (o, d) = (out.offset[0] as i64, out.offset[1] as i64);
|
||||
assert!((out.img.get(4 + o, 10 + d)[0] - 1.0).abs() < 1e-4, "dark map keeps the line sharp");
|
||||
assert!(out.img.get(15 + o, 10 + d)[0] < 0.9, "bright map blurs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iris_spans_hexagon_is_symmetric() {
|
||||
let s = iris_spans(10.0, 6, 0.0, 0.0);
|
||||
|
|
|
|||
|
|
@ -131,9 +131,17 @@ fn align(a: &Plane, bp: &Plane, x0: usize, y0: usize, x1: usize, y1: usize, r: i
|
|||
fn camera_shake_deblur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let Some(host) = ctx.env.host else { return b };
|
||||
let dur = ctx.params.f("blurDuration").clamp(1.0, 10.0).round() as i64;
|
||||
let search = (ctx.params.f("searchRange").max(0.0) * b.scale).round() as i64;
|
||||
// Standard (index 1) aligns substitutes over a smaller search window than High Quality.
|
||||
let mut search = (ctx.params.f("searchRange").max(0.0) * b.scale).round() as i64;
|
||||
if ctx.params.e("deblurMethod") == 1 {
|
||||
search /= 2;
|
||||
}
|
||||
let patch = (ctx.params.f("patchSize").max(8.0) * b.scale).round().max(4.0) as usize;
|
||||
let thresh = 1.0 + ctx.params.f("threshold").max(0.0) / 100.0;
|
||||
// Shake Sensitivity scales how much sharper a neighbour must be (50 = the Threshold as set,
|
||||
// 100 = any sharper neighbour counts, 0 = twice the Threshold).
|
||||
let sens = ctx.params.f("shakeSensitivity").clamp(0.0, 100.0);
|
||||
let thresh = 1.0 + ctx.params.f("threshold").max(0.0) / 100.0 * (100.0 - sens) / 50.0;
|
||||
let strength = (ctx.params.f("strength") / 100.0).clamp(1.0, 2.0) as f32;
|
||||
let show = ctx.params.b("showBlurryFrames");
|
||||
let fps = ctx.fps();
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
|
|
@ -214,6 +222,17 @@ fn camera_shake_deblur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
acc[c] += q[c] * wgt;
|
||||
}
|
||||
}
|
||||
if strength != 1.0 {
|
||||
// Strength above 100 % pushes past the substitute (away from the blurry pixel).
|
||||
let s0 = src.data[y * w + x];
|
||||
for c in 0..4 {
|
||||
acc[c] = s0[c] + (acc[c] - s0[c]) * strength;
|
||||
}
|
||||
acc[3] = acc[3].clamp(0.0, 1.0);
|
||||
for c in 0..3 {
|
||||
acc[c] = acc[c].max(0.0);
|
||||
}
|
||||
}
|
||||
if show {
|
||||
acc[0] = (acc[0] + 0.3 * acc[3]).min(acc[3].max(acc[0]));
|
||||
}
|
||||
|
|
@ -241,6 +260,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Camera-Shake Deblur",
|
||||
vec![
|
||||
p("blurDuration", "Blur Duration", num(2.0), slider(1.0, 10.0, 1.0, 10.0, 0)),
|
||||
p("deblurMethod", "Deblur Method", Value::Enum(0), popup(&["High Quality (slower)", "Standard"])),
|
||||
p("strength", "Strength", num(100.0), slider(100.0, 200.0, 100.0, 200.0, 0)),
|
||||
p("shakeSensitivity", "Shake Sensitivity", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("searchRange", "Search Range", num(8.0), slider(0.0, 64.0, 0.0, 32.0, 0)),
|
||||
p("patchSize", "Patch Size", num(32.0), slider(8.0, 256.0, 8.0, 128.0, 0)),
|
||||
p("threshold", "Threshold", num(20.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||||
|
|
@ -335,4 +357,34 @@ mod tests {
|
|||
let none = run_fx("ec.blur.camerashakedeblur", &[], blurry.clone(), 0.0, EffectEnv::default());
|
||||
assert_eq!(none.img.data, blurry.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_shake_deblur_sensitivity_and_strength() {
|
||||
let host = Shaky;
|
||||
let env = EffectEnv { host: Some(&host), frame_rate: 30.0, ..Default::default() };
|
||||
let blurry = frame(1);
|
||||
let base = [("patchSize", num(16.0)), ("searchRange", num(3.0)), ("threshold", num(500.0))];
|
||||
let dist = |a: &Image| a.data.iter().zip(&blurry.data).map(|(p, q)| (p[0] - q[0]).abs()).sum::<f32>();
|
||||
// Full sensitivity deblurs whatever the threshold; lower sensitivity never deblurs more.
|
||||
let low = run_fx(
|
||||
"ec.blur.camerashakedeblur",
|
||||
&[base[0].clone(), base[1].clone(), base[2].clone(), ("shakeSensitivity", num(0.0))],
|
||||
blurry.clone(),
|
||||
1.0 / 30.0,
|
||||
env,
|
||||
);
|
||||
let high = run_fx(
|
||||
"ec.blur.camerashakedeblur",
|
||||
&[base[0].clone(), base[1].clone(), base[2].clone(), ("shakeSensitivity", num(100.0))],
|
||||
blurry.clone(),
|
||||
1.0 / 30.0,
|
||||
env,
|
||||
);
|
||||
assert_ne!(high.img.data, blurry.data);
|
||||
assert!(dist(&high.img) >= dist(&low.img));
|
||||
// Strength 200 % moves further from the blurry frame than 100 %.
|
||||
let s1 = run_fx("ec.blur.camerashakedeblur", &[base[0].clone(), base[1].clone()], blurry.clone(), 1.0 / 30.0, env);
|
||||
let s2 = run_fx("ec.blur.camerashakedeblur", &[base[0].clone(), base[1].clone(), ("strength", num(200.0))], blurry.clone(), 1.0 / 30.0, env);
|
||||
assert!(dist(&s2.img) > dist(&s1.img) * 1.05, "{} vs {}", dist(&s2.img), dist(&s1.img));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
//! The public effects catalogue (`docs/effects.md`), generated from the registry.
|
||||
//!
|
||||
//! Regenerate with `UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current`.
|
||||
|
||||
use crate::{CATEGORIES, registry};
|
||||
|
||||
/// Effects whose implementation is known to fall short of the reference behaviour:
|
||||
/// `(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.channel.minimax", "no Don't Shrink Edges option"),
|
||||
("ec.channel.combiner", "no Saturation Multiplied target"),
|
||||
("ec.3d.channelextract", "no anti-alias option"),
|
||||
("ec.color.huesaturation", "no per-range Channel Control"),
|
||||
("ec.color.levels", "master channel only (no Channel popup)"),
|
||||
("ec.color.curves", "no Channel popup"),
|
||||
("ec.color.colorama", "no Add Phase, editable Output Cycle, Modify, Pixel Selection or Masking"),
|
||||
("ec.color.lumetri", "no HSL Secondary, hue/saturation curves, HDR mode or look files"),
|
||||
("ec.color.selectivecolor", "simplified Colors / Details layout"),
|
||||
("ec.distort.turbulentdisplace", "no locked pinning variants"),
|
||||
("ec.distort.opticscompensation", "no Optimal Pixels"),
|
||||
("ec.distort.transform", "no shutter-angle motion blur"),
|
||||
("ec.distort.reshape", "no correspondence points"),
|
||||
("ec.distort.rollingshutterrepair", "Pixel Motion Detail has no effect"),
|
||||
("ec.generate.fill", "no Fill Mask / All Masks / feather"),
|
||||
("ec.generate.advancedlightning", "no Alpha Obstacle; most Expert Settings missing"),
|
||||
("ec.generate.radiowaves", "no Image Contours or Mask wave types, Parameters Are Set At, reflection or stroke profile"),
|
||||
("ec.generate.scribble", "no caps, joins or Start/End Apply To"),
|
||||
("ec.generate.cellpattern", "HQ variants render like the standard ones"),
|
||||
("ec.key.innerouter", "one additional mask per side; no cleanup strokes"),
|
||||
("ec.key.advancedspill", "no Tolerance, Desaturate or Spill Color Correction"),
|
||||
("ec.keying.keylight", "no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing"),
|
||||
("ec.matte.refinesoft", "no Reduce Chatter or motion blur"),
|
||||
("ec.matte.refinehard", "no Reduce Chatter or motion blur"),
|
||||
("ec.obsolete.pathtext", "no kerning, shear/scale, line spacing, fade time or jitter"),
|
||||
("ec.noise.addgrain", "no preview region, presets, channel balance or temporal controls"),
|
||||
("ec.noise.matchgrain", "no preview region, presets, sampling or temporal controls"),
|
||||
("ec.noise.removegrain", "no preview region, sampling or temporal filtering"),
|
||||
("ec.vr.digitalglitch", "reduced control set"),
|
||||
("ec.vr.converter", "common layouts only"),
|
||||
("ec.time.timewarp", "Pixel Motion blends frames (no optical flow); no matte, warp or crop controls"),
|
||||
("ec.sim.carddance", "no camera system, corner pins or lighting/material controls"),
|
||||
("ec.sim.shatter", "no custom shatter map, gradient, textures or extrusion rendering"),
|
||||
("ec.sim.caustics", "no Sky group or light type"),
|
||||
("ec.sim.foam", "no wobble, pop velocity, custom bubble texture, environment map or flow map"),
|
||||
("ec.sim.particleplayground", "no Particle Exploder, Ephemeral Property Mapper, Affects groups or text particles"),
|
||||
("ec.sim.waveworld", "no wireframe controls, dry-area rendering or ground group"),
|
||||
("ec.stylize.glow", "no Glow Operation or arbitrary colour map"),
|
||||
("ec.stylize.cartoon", "no Edge Enhancement"),
|
||||
("ec.transition.cardwipe", "2D flip only; no back layer picker, camera, lighting or jitter"),
|
||||
("ec.transition.blockdissolve", "no Soft Edges option"),
|
||||
];
|
||||
|
||||
/// Implementation status of effect `id` (`Implemented` or `Partial: …`).
|
||||
pub fn status(id: &str) -> String {
|
||||
match PARTIAL.iter().find(|(e, _)| *e == id) {
|
||||
Some((_, note)) => format!("Partial: {note}"),
|
||||
None => "Implemented".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `docs/effects.md`.
|
||||
pub fn effects_markdown() -> String {
|
||||
let r = registry();
|
||||
let mut s = String::new();
|
||||
s.push_str("# Effects\n\n");
|
||||
s.push_str("<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.\n");
|
||||
s.push_str(" Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->\n\n");
|
||||
let params: usize = r.iter().map(|e| e.params.len()).sum();
|
||||
let gpu = r.iter().filter(|e| e.gpu).count();
|
||||
let float = r.iter().filter(|e| e.float).count();
|
||||
let partial = r.iter().filter(|e| PARTIAL.iter().any(|(id, _)| *id == e.id)).count();
|
||||
s.push_str(&format!(
|
||||
"EffectCraft ships {} effects with {} 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.\n\n",
|
||||
r.len(),
|
||||
params
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"- **GPU**: {gpu} effects also run on the GPU compositor with identical results.\n\
|
||||
- **32**: {float} effects process 32-bit float (HDR, overbright) pixels without clamping.\n\
|
||||
- **Status**: {} implemented in full, {partial} partial (what is missing is listed).\n\n",
|
||||
r.len() - partial
|
||||
));
|
||||
s.push_str("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`).\n\n");
|
||||
for cat in CATEGORIES {
|
||||
let list: Vec<_> = r.iter().filter(|e| e.category == *cat).collect();
|
||||
if list.is_empty() {
|
||||
continue;
|
||||
}
|
||||
s.push_str(&format!("## {cat}\n\n| Effect | Id | Params | GPU | 32 | Status |\n|---|---|---:|:-:|:-:|---|\n"));
|
||||
for e in list {
|
||||
s.push_str(&format!(
|
||||
"| {} | `{}` | {} | {} | {} | {} |\n",
|
||||
e.name.replace('|', "\\|"),
|
||||
e.id,
|
||||
e.params.iter().filter(|p| !matches!(p.ui, effectcraft_project::ParamUi::Hidden)).count(),
|
||||
if e.gpu { "GPU" } else { "" },
|
||||
if e.float { "32" } else { "" },
|
||||
status(e.id).replace('|', "\\|"),
|
||||
));
|
||||
}
|
||||
s.push('\n');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn effects_doc_is_current() {
|
||||
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/effects.md");
|
||||
let want = super::effects_markdown();
|
||||
if std::env::var_os("UPDATE_DOCS").is_some() {
|
||||
std::fs::write(path, &want).unwrap();
|
||||
return;
|
||||
}
|
||||
let have = std::fs::read_to_string(path).unwrap_or_default();
|
||||
assert!(have == want, "docs/effects.md is stale: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_notes_name_registered_effects() {
|
||||
for (id, note) in super::PARTIAL {
|
||||
assert!(crate::find(id).is_some(), "PARTIAL names unknown effect {id}");
|
||||
assert!(!note.trim().is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ use effectcraft_keyframe::Value;
|
|||
use effectcraft_project::ParamUi;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::util::{Plane, SRC_NAMES, Src, join, morph_plane, pick, premul, src_at, unpremul};
|
||||
use crate::util::{Plane, SRC_NAMES, Src, join, layer_or_self, morph_plane, pick, premul, src_at, unpremul};
|
||||
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 {
|
||||
|
|
@ -25,9 +25,27 @@ fn route(b: &mut Buf, srcs: [Src; 4]) {
|
|||
});
|
||||
}
|
||||
|
||||
const SET_CHANNEL_LAYERS: [&str; 4] = ["sourceLayer1", "sourceLayer2", "sourceLayer3", "sourceLayer4"];
|
||||
|
||||
fn set_channels(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let s = ["setRedTo", "setGreenTo", "setBlueTo", "setAlphaTo"].map(|id| src_at(ctx.params.e(id)));
|
||||
route(&mut b, s);
|
||||
// Source layers default to the effect's own layer ("None").
|
||||
let stretch = ctx.params.b("stretchLayersToFit");
|
||||
let layers: Vec<Option<crate::Image>> =
|
||||
SET_CHANNEL_LAYERS.iter().map(|id| ctx.layer_param(id, true).map(|o| crate::util::fit_layer(ctx, &b, &o, stretch))).collect();
|
||||
if layers.iter().all(Option::is_none) {
|
||||
route(&mut b, s);
|
||||
return b;
|
||||
}
|
||||
let own = b.img.clone();
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let v: [f32; 4] = std::array::from_fn(|k| {
|
||||
let q = layers[k].as_ref().map(|l| l.data[i]).unwrap_or(own.data[i]);
|
||||
let (c, a) = unpremul(q);
|
||||
pick(s[k], c, a)
|
||||
});
|
||||
*px = premul([v[0], v[1], v[2]], v[3].clamp(0.0, 1.0));
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
|
|
@ -54,13 +72,43 @@ fn yuv_to_rgb(c: [f32; 3]) -> [f32; 3] {
|
|||
[r, g, b]
|
||||
}
|
||||
|
||||
/// Channel Combiner's From options.
|
||||
const COMBINER_FROM: [&str; 14] = [
|
||||
"RGB to HLS",
|
||||
"HLS to RGB",
|
||||
"RGB to YUV",
|
||||
"YUV to RGB",
|
||||
"Red",
|
||||
"Green",
|
||||
"Blue",
|
||||
"Alpha",
|
||||
"Lightness",
|
||||
"Hue",
|
||||
"Saturation",
|
||||
"Luminance",
|
||||
"Max RGB",
|
||||
"Min RGB",
|
||||
];
|
||||
/// Channel Combiner's To options (a single value goes into one channel).
|
||||
const COMBINER_TO: [&str; 8] = ["Red Only", "Green Only", "Blue Only", "Alpha Only", "Lightness Only", "Hue Only", "Saturation Only", "Grayscale"];
|
||||
|
||||
fn channel_combiner(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let from = ctx.params.e("from");
|
||||
let to = ctx.params.e("to");
|
||||
let inv = ctx.params.b("invert");
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let solid = ctx.params.b("solidAlpha");
|
||||
// Use 2nd Layer: read the input values from the source layer instead.
|
||||
let second = if ctx.params.b("useSecondLayer") { Some(layer_or_self(ctx, &b, "sourceLayer", true, true)) } else { None };
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (c, mut a) = unpremul(*px);
|
||||
if a <= 0.0 && from != 7 {
|
||||
let (sc, sa) = match &second {
|
||||
Some(img) => unpremul(img.data[i]),
|
||||
None => (c, a),
|
||||
};
|
||||
if sa <= 0.0 && from != 7 && second.is_none() {
|
||||
if solid {
|
||||
*px = premul(c, 1.0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let iv = |v: f32| if inv { 1.0 - v } else { v };
|
||||
|
|
@ -68,57 +116,65 @@ fn channel_combiner(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
0..=3 => {
|
||||
let o = match from {
|
||||
0 => {
|
||||
let (h, s, l) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
let (h, s, l) = rgb_to_hsl(sc[0], sc[1], sc[2]);
|
||||
[h, l, s]
|
||||
}
|
||||
1 => {
|
||||
let (r, g, bb) = hsl_to_rgb(c[0], c[2], c[1]);
|
||||
let (r, g, bb) = hsl_to_rgb(sc[0], sc[2], sc[1]);
|
||||
[r, g, bb]
|
||||
}
|
||||
2 => rgb_to_yuv(c),
|
||||
_ => yuv_to_rgb(c),
|
||||
2 => rgb_to_yuv(sc),
|
||||
_ => yuv_to_rgb(sc),
|
||||
};
|
||||
o.map(iv)
|
||||
}
|
||||
_ => {
|
||||
let v = iv(match from {
|
||||
4 => c[0],
|
||||
5 => c[1],
|
||||
6 => c[2],
|
||||
7 => a,
|
||||
8 => rgb_to_hsl(c[0], c[1], c[2]).2,
|
||||
9 => rgb_to_hsl(c[0], c[1], c[2]).0,
|
||||
10 => rgb_to_hsl(c[0], c[1], c[2]).1,
|
||||
11 => luminance(c[0], c[1], c[2]),
|
||||
12 => c[0].max(c[1]).max(c[2]),
|
||||
_ => c[0].min(c[1]).min(c[2]),
|
||||
4 => sc[0],
|
||||
5 => sc[1],
|
||||
6 => sc[2],
|
||||
7 => sa,
|
||||
8 => rgb_to_hsl(sc[0], sc[1], sc[2]).2,
|
||||
9 => rgb_to_hsl(sc[0], sc[1], sc[2]).0,
|
||||
10 => rgb_to_hsl(sc[0], sc[1], sc[2]).1,
|
||||
11 => luminance(sc[0], sc[1], sc[2]),
|
||||
12 => sc[0].max(sc[1]).max(sc[2]),
|
||||
_ => sc[0].min(sc[1]).min(sc[2]),
|
||||
});
|
||||
let mut o = c;
|
||||
// The single value goes to one channel; the others are set to fixed values
|
||||
// (zero for the other colour channels, white for Alpha Only, 50 % lightness /
|
||||
// full saturation for Hue Only, …).
|
||||
match to {
|
||||
0 => o[0] = v,
|
||||
1 => o[1] = v,
|
||||
2 => o[2] = v,
|
||||
3 => a = v.clamp(0.0, 1.0),
|
||||
4..=6 => {
|
||||
let (mut h, mut s, mut l) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
match to {
|
||||
4 => l = v,
|
||||
5 => h = v,
|
||||
_ => s = v,
|
||||
}
|
||||
let (r, g, bb) = hsl_to_rgb(h, s.clamp(0.0, 1.0), l);
|
||||
o = [r, g, bb];
|
||||
0 => [v, 0.0, 0.0],
|
||||
1 => [0.0, v, 0.0],
|
||||
2 => [0.0, 0.0, v],
|
||||
3 => {
|
||||
a = v.clamp(0.0, 1.0);
|
||||
[1.0, 1.0, 1.0]
|
||||
}
|
||||
_ => o = [v, v, v],
|
||||
5 => {
|
||||
let (r, g, bb) = hsl_to_rgb(v, 1.0, 0.5);
|
||||
[r, g, bb]
|
||||
}
|
||||
6 => {
|
||||
let (r, g, bb) = hsl_to_rgb(0.0, v.clamp(0.0, 1.0), 0.5);
|
||||
[r, g, bb]
|
||||
}
|
||||
_ => [v, v, v],
|
||||
}
|
||||
o
|
||||
}
|
||||
};
|
||||
if solid {
|
||||
a = 1.0;
|
||||
}
|
||||
*px = premul(out.map(|v| v.max(0.0)), a);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Minimax's Direction options.
|
||||
pub(crate) const MINIMAX_DIRECTIONS: [&str; 3] = ["Horizontal & Vertical", "Just Horizontal", "Just Vertical"];
|
||||
|
||||
fn minimax(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize;
|
||||
if r == 0 {
|
||||
|
|
@ -277,16 +333,23 @@ fn set_matte(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let inv = ctx.params.b("invertMatte");
|
||||
let comp = ctx.params.b("compositeMatteWithOriginal");
|
||||
let pre = ctx.params.b("premultiplyMatteLayer");
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
// Take Matte From Layer: another layer (stretched or centred), else the layer itself.
|
||||
let matte = ctx.layer_param("takeMatteFromLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, ctx.params.b("stretchMatteToFit")));
|
||||
let own_matte = matte.is_none();
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (c, a) = unpremul(*px);
|
||||
let mut m = pick(src, c, a);
|
||||
let (mc, ma) = match &matte {
|
||||
Some(m) => unpremul(m.data[i]),
|
||||
None => (c, a),
|
||||
};
|
||||
let mut m = pick(src, mc, ma);
|
||||
if pre && !matches!(src, Src::Alpha | Src::Full | Src::Off) {
|
||||
m *= a;
|
||||
m *= ma;
|
||||
}
|
||||
let m = m.clamp(0.0, 1.0);
|
||||
let m = if inv { 1.0 - m } else { m };
|
||||
// The layer's own (non-inverted) alpha already is the original matte: don't square it.
|
||||
let na = if comp && (src != Src::Alpha || inv) { a * m } else { m };
|
||||
let na = if comp && (!own_matte || src != Src::Alpha || inv) { a * m } else { m };
|
||||
*px = premul(c, na.clamp(0.0, 1.0));
|
||||
});
|
||||
b
|
||||
|
|
@ -301,10 +364,15 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.channel.setchannels",
|
||||
"Set Channels",
|
||||
vec![
|
||||
p("sourceLayer1", "Source Layer 1", Value::Layer(None), ParamUi::Layer),
|
||||
srcp("setRedTo", "Set Red To Source 1's", 0),
|
||||
p("sourceLayer2", "Source Layer 2", Value::Layer(None), ParamUi::Layer),
|
||||
srcp("setGreenTo", "Set Green To Source 2's", 1),
|
||||
p("sourceLayer3", "Source Layer 3", Value::Layer(None), ParamUi::Layer),
|
||||
srcp("setBlueTo", "Set Blue To Source 3's", 2),
|
||||
p("sourceLayer4", "Source Layer 4", Value::Layer(None), ParamUi::Layer),
|
||||
srcp("setAlphaTo", "Set Alpha To Source 4's", 3),
|
||||
p("stretchLayersToFit", "Stretch Layers to Fit", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
set_channels,
|
||||
),
|
||||
|
|
@ -323,29 +391,12 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.channel.combiner",
|
||||
"Channel Combiner",
|
||||
vec![
|
||||
p(
|
||||
"from",
|
||||
"From",
|
||||
Value::Enum(8),
|
||||
popup(&[
|
||||
"RGB to HLS",
|
||||
"HLS to RGB",
|
||||
"RGB to YUV",
|
||||
"YUV to RGB",
|
||||
"Red",
|
||||
"Green",
|
||||
"Blue",
|
||||
"Alpha",
|
||||
"Lightness",
|
||||
"Hue",
|
||||
"Saturation",
|
||||
"Luminance",
|
||||
"Max RGB",
|
||||
"Min RGB",
|
||||
]),
|
||||
),
|
||||
p("to", "To", Value::Enum(7), popup(&["Red Only", "Green Only", "Blue Only", "Alpha Only", "Lightness", "Hue", "Saturation", "Grayscale"])),
|
||||
p("useSecondLayer", "Use 2nd Layer", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("sourceLayer", "Source Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("from", "From", Value::Enum(8), popup(&COMBINER_FROM)),
|
||||
p("to", "To", Value::Enum(4), popup(&COMBINER_TO)),
|
||||
p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("solidAlpha", "Solid Alpha", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
channel_combiner,
|
||||
),
|
||||
|
|
@ -353,10 +404,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.channel.minimax",
|
||||
"Minimax",
|
||||
vec![
|
||||
p("operation", "Operation", Value::Enum(1), popup(&["Minimum", "Maximum", "Minimum then Maximum", "Maximum then Minimum"])),
|
||||
p("operation", "Operation", Value::Enum(1), popup(&["Minimum", "Maximum", "Minimum Then Maximum", "Maximum Then Minimum"])),
|
||||
p("radius", "Radius", num(0.0), slider(0.0, 500.0, 0.0, 100.0, 0)),
|
||||
p("channel", "Channel", Value::Enum(0), popup(&["Color", "Alpha", "Alpha and Color", "Red", "Green", "Blue"])),
|
||||
p("direction", "Direction", Value::Enum(0), popup(&["Horizontal & Vertical", "Horizontal Only", "Vertical Only"])),
|
||||
p("direction", "Direction", Value::Enum(0), popup(&MINIMAX_DIRECTIONS)),
|
||||
],
|
||||
minimax,
|
||||
),
|
||||
|
|
@ -391,13 +442,17 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
spec(
|
||||
"ec.channel.removecolormatting",
|
||||
"Remove Color Matting",
|
||||
vec![p("backgroundColor", "Background Color", col(0.0, 0.0, 0.0), ParamUi::Color), p("clipHdr", "Clipping", Value::Bool(true), ParamUi::Checkbox)],
|
||||
vec![
|
||||
p("backgroundColor", "Background Color", col(0.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("clipHdr", "Clip HDR Values", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
remove_color_matting,
|
||||
),
|
||||
spec(
|
||||
"ec.channel.setmatte",
|
||||
"Set Matte",
|
||||
vec![
|
||||
p("takeMatteFromLayer", "Take Matte From Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p(
|
||||
"takeMatteFrom",
|
||||
"Use For Matte",
|
||||
|
|
@ -405,6 +460,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
popup(&["Red", "Green", "Blue", "Alpha", "Luminance", "Hue", "Lightness", "Saturation", "Full", "Off"]),
|
||||
),
|
||||
p("invertMatte", "Invert Matte", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("stretchMatteToFit", "Stretch Matte to Fit", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("compositeMatteWithOriginal", "Composite Matte with Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("premultiplyMatteLayer", "Premultiply Matte Layer", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
|
|
@ -510,6 +566,90 @@ mod tests {
|
|||
assert_eq!(out.get(1, 0)[3], 0.0);
|
||||
}
|
||||
|
||||
/// A host whose every layer is `img`.
|
||||
struct OneLayer(Image);
|
||||
impl crate::EffectHost for OneLayer {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
let (w, h) = (self.0.width as f64, self.0.height as f64);
|
||||
Some(crate::LayerPixels { buf: Buf { img: self.0.clone(), offset: [0.0; 2], scale: 1.0 }, size: [w, h] })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn run_host(id: &str, over: &[(&str, Value)], img: Image, other: Image) -> Image {
|
||||
let s = find(id).unwrap();
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
for (k, v) in over {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let host = OneLayer(other);
|
||||
let env = crate::EffectEnv { host: Some(&host), ..Default::default() };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: true, env };
|
||||
apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_channels_reads_source_layers() {
|
||||
let other = Image::filled(8, 8, [0.7, 0.1, 0.3, 1.0]);
|
||||
let out = run_host("ec.channel.setchannels", &[("sourceLayer2", Value::Layer(Some(9)))], sample(), other);
|
||||
// Green from the other layer's green; red, blue, alpha from the layer itself.
|
||||
assert!(close(out.get(0, 0), [0.2, 0.1, 0.9, 1.0]), "{:?}", out.get(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_matte_takes_matte_from_another_layer() {
|
||||
let mut other = Image::new(2, 1);
|
||||
other.set(0, 0, [1.0, 1.0, 1.0, 1.0]);
|
||||
let img = Image::filled(2, 1, [0.5, 0.5, 0.5, 1.0]);
|
||||
let out = run_host("ec.channel.setmatte", &[("takeMatteFromLayer", Value::Layer(Some(9)))], img, other);
|
||||
assert!((out.get(0, 0)[3] - 1.0).abs() < 1e-5);
|
||||
assert_eq!(out.get(1, 0)[3], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_combiner_single_channel_targets() {
|
||||
let img = Image::filled(1, 1, [0.2, 0.5, 0.9, 1.0]);
|
||||
// Red → Green Only: green gets red's value, the other colour channels go to zero.
|
||||
let out = run("ec.channel.combiner", &[("from", Value::Enum(4)), ("to", Value::Enum(1))], img.clone());
|
||||
assert!(close(out.get(0, 0), [0.0, 0.2, 0.0, 1.0]), "{:?}", out.get(0, 0));
|
||||
// Lightness Only (the default) gives a grey image.
|
||||
let out = run("ec.channel.combiner", &[], img.clone());
|
||||
let px = out.get(0, 0);
|
||||
assert!((px[0] - px[1]).abs() < 1e-6 && (px[1] - px[2]).abs() < 1e-6 && (px[0] - 0.55).abs() < 1e-5, "{px:?}");
|
||||
// Alpha Only: white with the value as alpha; Solid Alpha forces full opacity.
|
||||
let out = run("ec.channel.combiner", &[("from", Value::Enum(5)), ("to", Value::Enum(3))], img.clone());
|
||||
assert!(close(out.get(0, 0), [0.5, 0.5, 0.5, 0.5]), "{:?}", out.get(0, 0));
|
||||
let out = run("ec.channel.combiner", &[("from", Value::Enum(5)), ("to", Value::Enum(3)), ("solidAlpha", Value::Bool(true))], img);
|
||||
assert!(close(out.get(0, 0), [1.0, 1.0, 1.0, 1.0]), "{:?}", out.get(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invert_hls_and_yiq_channels() {
|
||||
let img = Image::filled(1, 1, [0.2, 0.4, 0.6, 1.0]);
|
||||
let idx = |n: &str| Value::Enum(crate::INVERT_CHANNELS.iter().position(|o| *o == n).unwrap() as u32);
|
||||
// Lightness 0.4 → 0.6 keeps hue and saturation: (0.4, 0.6, 0.8).
|
||||
let out = run("ec.channel.invert", &[("channel", idx("Lightness"))], img.clone());
|
||||
assert!(close(out.get(0, 0), [0.4, 0.6, 0.8, 1.0]), "{:?}", out.get(0, 0));
|
||||
// Luminance inverts Y and keeps chrominance: Y + Y' = 1.
|
||||
let out = run("ec.channel.invert", &[("channel", idx("Luminance"))], img.clone());
|
||||
let y = |p: [f32; 4]| 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
|
||||
assert!((y(out.get(0, 0)) + y(img.get(0, 0)) - 1.0).abs() < 1e-3, "{:?}", out.get(0, 0));
|
||||
// Alpha keeps working at its new index.
|
||||
let out = run("ec.channel.invert", &[("channel", Value::Enum(crate::INVERT_ALPHA))], img);
|
||||
assert_eq!(out.get(0, 0)[3], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_combiner_uses_second_layer() {
|
||||
let img = Image::filled(1, 1, [0.2, 0.5, 0.9, 1.0]);
|
||||
let other = Image::filled(1, 1, [0.6, 0.0, 0.0, 1.0]);
|
||||
let over = [("useSecondLayer", Value::Bool(true)), ("sourceLayer", Value::Layer(Some(3))), ("from", Value::Enum(4)), ("to", Value::Enum(0))];
|
||||
let out = run_host("ec.channel.combiner", &over, img, other);
|
||||
assert!(close(out.get(0, 0), [0.6, 0.0, 0.0, 1.0]), "{:?}", out.get(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_composite_fills_behind() {
|
||||
let mut img = Image::new(2, 1);
|
||||
|
|
|
|||
|
|
@ -62,8 +62,12 @@ fn channel_extract(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let Some(a) = aux(ctx) else { return b };
|
||||
let idx = index_map(&b, &a);
|
||||
let (bp, wp) = (ctx.params.f("blackPoint") as f32, ctx.params.f("whitePoint") as f32);
|
||||
let lv = |v: f32| ((v - bp) / (wp - bp).abs().max(1e-9) * (wp - bp).signum()).clamp(0.0, 1.0);
|
||||
let lv = |v: f32| if wp >= bp { lv(v) } else { 1.0 - ((v - wp) / (bp - wp).max(1e-9)).clamp(0.0, 1.0) };
|
||||
// Clamp Output limits the mapped values to 0..1; Invert Depth Map flips them.
|
||||
let (clamp, invert) = (ctx.params.b("clampOutput"), ctx.params.b("invertDepthMap"));
|
||||
let cl = move |v: f32| if clamp { v.clamp(0.0, 1.0) } else { v };
|
||||
let lv = |v: f32| cl((v - bp) / (wp - bp).abs().max(1e-9) * (wp - bp).signum());
|
||||
let lv = |v: f32| if wp >= bp { lv(v) } else { 1.0 - cl((v - wp) / (bp - wp).max(1e-9)) };
|
||||
let lv = |v: f32| if invert { 1.0 - lv(v) } else { lv(v) };
|
||||
let get3 = |names: [&[&str]; 3]| -> Option<[&[f32]; 3]> { Some([a.find(names[0])?, a.find(names[1])?, a.find(names[2])?]) };
|
||||
let img: Option<Image> = match ctx.params.e("channel") {
|
||||
0 => a.depth().map(|z| map_grey(&b, &idx, |i| lv(z[i]))),
|
||||
|
|
@ -437,6 +441,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("channel", "3D Channel", Value::Enum(0), popup(&EXTRACT_CHANNELS)),
|
||||
p("blackPoint", "Black Point", num(0.0), slider(-1.0e6, 1.0e6, -10000.0, 10000.0, 2)),
|
||||
p("whitePoint", "White Point", num(10000.0), slider(-1.0e6, 1.0e6, -10000.0, 10000.0, 2)),
|
||||
p("clampOutput", "Clamp Output", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("invertDepthMap", "Invert Depth Map", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
channel_extract,
|
||||
),
|
||||
|
|
@ -614,6 +620,11 @@ mod tests {
|
|||
assert_eq!(o.get(17, 1)[0], 1.0);
|
||||
let ids = run("ec.3d.channelextract", &[("channel", Value::Enum(1)), ("whitePoint", num(2.0))], &h);
|
||||
assert_eq!(ids.get(2, 5)[0], 0.5);
|
||||
// Invert Depth Map flips the ramp; without Clamp Output values run past 1.
|
||||
let inv = run("ec.3d.channelextract", &[("blackPoint", num(0.0)), ("whitePoint", num(1000.0)), ("invertDepthMap", Value::Bool(true))], &h);
|
||||
assert!((inv.get(2, 5)[0] - 0.9).abs() < 1e-6);
|
||||
let raw = run("ec.3d.channelextract", &[("blackPoint", num(0.0)), ("whitePoint", num(100.0)), ("clampOutput", Value::Bool(false))], &h);
|
||||
assert!(raw.get(12, 5)[0] > 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -160,12 +160,14 @@ fn curves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
// ---------------------------------------------------------------------------------------------
|
||||
// Levels (Individual Controls)
|
||||
|
||||
/// Levels (Individual Controls) parameter ids: each channel's controls sit in its own
|
||||
/// twirl-down group (RGB, Red, Green, Blue, Alpha).
|
||||
const LV_IDS: [[&str; 5]; 5] = [
|
||||
["rgbInBlack", "rgbInWhite", "rgbGamma", "rgbOutBlack", "rgbOutWhite"],
|
||||
["redInBlack", "redInWhite", "redGamma", "redOutBlack", "redOutWhite"],
|
||||
["greenInBlack", "greenInWhite", "greenGamma", "greenOutBlack", "greenOutWhite"],
|
||||
["blueInBlack", "blueInWhite", "blueGamma", "blueOutBlack", "blueOutWhite"],
|
||||
["alphaInBlack", "alphaInWhite", "alphaGamma", "alphaOutBlack", "alphaOutWhite"],
|
||||
["rgb/rgbInBlack", "rgb/rgbInWhite", "rgb/rgbGamma", "rgb/rgbOutBlack", "rgb/rgbOutWhite"],
|
||||
["red/redInBlack", "red/redInWhite", "red/redGamma", "red/redOutBlack", "red/redOutWhite"],
|
||||
["green/greenInBlack", "green/greenInWhite", "green/greenGamma", "green/greenOutBlack", "green/greenOutWhite"],
|
||||
["blue/blueInBlack", "blue/blueInWhite", "blue/blueGamma", "blue/blueOutBlack", "blue/blueOutWhite"],
|
||||
["alpha/alphaInBlack", "alpha/alphaInWhite", "alpha/alphaGamma", "alpha/alphaOutBlack", "alpha/alphaOutWhite"],
|
||||
];
|
||||
const LV_NAMES: [[&str; 5]; 5] = [
|
||||
["Input Black", "Input White", "Gamma", "Output Black", "Output White"],
|
||||
|
|
@ -188,12 +190,18 @@ impl Lv {
|
|||
fn identity(&self) -> bool {
|
||||
self.ib == 0.0 && self.iw == 1.0 && self.g == 1.0 && self.ob == 0.0 && self.ow == 1.0
|
||||
}
|
||||
fn apply(&self, v: f32) -> f32 {
|
||||
fn apply(&self, v: f32, (clip_b, clip_w): (bool, bool)) -> f32 {
|
||||
if self.identity() {
|
||||
return v;
|
||||
}
|
||||
let t = ((v - self.ib) / (self.iw - self.ib).max(1e-6)).clamp(0.0, 1.0);
|
||||
self.ob + (self.ow - self.ob) * t.powf(1.0 / self.g.max(0.01))
|
||||
let mut t = (v - self.ib) / (self.iw - self.ib).max(1e-6);
|
||||
if clip_b {
|
||||
t = t.max(0.0);
|
||||
}
|
||||
if clip_w {
|
||||
t = t.min(1.0);
|
||||
}
|
||||
self.ob + (self.ow - self.ob) * t.max(0.0).powf(1.0 / self.g.max(0.01))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +216,8 @@ fn levels_ic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if lv.iter().all(Lv::identity) {
|
||||
return b;
|
||||
}
|
||||
map_ca(&mut b.img, |c, a| ([0, 1, 2].map(|i| lv[i + 1].apply(lv[0].apply(c[i]))), lv[4].apply(a)));
|
||||
let clip = crate::color_fx::levels_clip(ctx);
|
||||
map_ca(&mut b.img, |c, a| ([0, 1, 2].map(|i| lv[i + 1].apply(lv[0].apply(c[i], clip), clip)), lv[4].apply(a, clip).clamp(0.0, 1.0)));
|
||||
b
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +286,44 @@ fn clip_points(h: &[f64], black: f64, white: f64) -> (f32, f32) {
|
|||
if hi - lo < 1e-3 { (0.0, 1.0) } else { (lo, hi) }
|
||||
}
|
||||
|
||||
/// Alpha-weighted mean (0..1) of a histogram.
|
||||
fn hist_mean(h: &[f64]) -> f64 {
|
||||
let (mut s, mut w) = (0.0, 0.0);
|
||||
for (i, v) in h.iter().enumerate() {
|
||||
s += v * i as f64;
|
||||
w += v;
|
||||
}
|
||||
if w > 0.0 { s / w / (BINS - 1) as f64 } else { 0.5 }
|
||||
}
|
||||
|
||||
/// The histograms the automatic corrections analyse: the current frame's, or with Temporal
|
||||
/// Smoothing (seconds) the sum over the frames within that range on either side. Scene Detect
|
||||
/// stops at a frame whose brightness jumps (a cut).
|
||||
fn smoothed_histograms(ctx: &EffectCtx, img: &Image) -> [Vec<f64>; 4] {
|
||||
let mut h = histograms(img);
|
||||
let secs = ctx.params.f("temporalSmoothing");
|
||||
let Some(host) = ctx.env.host.filter(|_| secs > 0.0) else { return h };
|
||||
let fps = ctx.fps();
|
||||
let n = (secs * fps).round().clamp(0.0, 240.0) as i64;
|
||||
let scene = ctx.params.b("sceneDetect");
|
||||
let m0 = hist_mean(&h[3]);
|
||||
for dir in [-1i64, 1] {
|
||||
for k in 1..=n {
|
||||
let Some(nb) = host.self_at(ctx.time + (dir * k) as f64 / fps, ctx.env.effect_index) else { break };
|
||||
let hn = histograms(&nb.img);
|
||||
if scene && (hist_mean(&hn[3]) - m0).abs() > 0.15 {
|
||||
break;
|
||||
}
|
||||
for c in 0..4 {
|
||||
for i in 0..BINS {
|
||||
h[c][i] += hn[c][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
fn stretch(v: f32, lo: f32, hi: f32) -> f32 {
|
||||
((v - lo) / (hi - lo)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
|
@ -286,7 +333,7 @@ fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
|
|||
let black = ctx.params.f("blackClip").clamp(0.0, 49.0) / 100.0;
|
||||
let white = ctx.params.f("whiteClip").clamp(0.0, 49.0) / 100.0;
|
||||
let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let h = histograms(&b.img);
|
||||
let h = smoothed_histograms(ctx, &b.img);
|
||||
let ranges: [(f32, f32); 3] = if kind == 1 {
|
||||
let mut combined = vec![0.0; BINS];
|
||||
for k in 0..3 {
|
||||
|
|
@ -392,22 +439,20 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (mut s_amt, mut h_amt) = (ctx.params.f("shadowAmount") as f32 / 100.0, ctx.params.f("highlightAmount") as f32 / 100.0);
|
||||
let luma = Plane::luma(&b.img);
|
||||
let src_hist = histograms(&b.img);
|
||||
if ctx.params.b("autoAmounts") {
|
||||
let (mut sum, mut wsum) = (0.0f64, 0.0f64);
|
||||
for (px, l) in b.img.data.iter().zip(&luma.data) {
|
||||
sum += (*l * px[3]) as f64;
|
||||
wsum += px[3] as f64;
|
||||
}
|
||||
let mean = if wsum > 0.0 { (sum / wsum) as f32 } else { 0.5 };
|
||||
// Temporal Smoothing / Scene Detect average the analysis over neighbouring frames.
|
||||
let mean =
|
||||
if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, &b.img)[3]) as f32 } else { hist_mean(&src_hist[3]) as f32 };
|
||||
s_amt = ((0.6 - mean) * 1.5).clamp(0.0, 1.0) * 0.8 + 0.1;
|
||||
h_amt = ((mean - 0.6) * 1.5).clamp(0.0, 1.0) * 0.8;
|
||||
}
|
||||
let s_tw = (ctx.params.f("shadowTonalWidth") as f32 / 100.0).max(0.01);
|
||||
let h_tw = (ctx.params.f("highlightTonalWidth") as f32 / 100.0).max(0.01);
|
||||
let s_r = ctx.params.f("shadowRadius").max(0.0) * b.scale / 2.0;
|
||||
let h_r = ctx.params.f("highlightRadius").max(0.0) * b.scale / 2.0;
|
||||
let cc = ctx.params.f("colorCorrection") as f32 / 100.0;
|
||||
let mc = ctx.params.f("midtoneContrast") as f32 / 100.0;
|
||||
let s_tw = (ctx.params.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01);
|
||||
let h_tw = (ctx.params.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01);
|
||||
let s_r = ctx.params.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
|
||||
let h_r = ctx.params.f("moreOptions/highlightRadius").max(0.0) * b.scale / 2.0;
|
||||
let cc = ctx.params.f("moreOptions/colorCorrection") as f32 / 100.0;
|
||||
let mc = ctx.params.f("moreOptions/midtoneContrast") as f32 / 100.0;
|
||||
let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let base_s = gauss_plane(&luma, s_r, s_r);
|
||||
let base_h = if (h_r - s_r).abs() < 1e-9 { base_s.clone() } else { gauss_plane(&luma, h_r, h_r) };
|
||||
|
|
@ -430,6 +475,18 @@ fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let o = lerp3(o.map(|v| v.max(0.0)), c, blend);
|
||||
*px = premul(o, a);
|
||||
});
|
||||
// Black / White Clip: the adjusted image's extremes (ignoring those fractions of pixels)
|
||||
// are mapped back onto the original's, so lifting shadows or recovering highlights doesn't
|
||||
// flatten the ends of the tonal range.
|
||||
let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
|
||||
if bc > 0.0 || wc > 0.0 {
|
||||
let (lo0, hi0) = clip_points(&src_hist[3], bc, wc);
|
||||
let (lo1, hi1) = clip_points(&histograms(&b.img)[3], bc, wc);
|
||||
if (lo0, hi0) != (lo1, hi1) && hi1 - lo1 > 1e-3 {
|
||||
let k = (hi0 - lo0) / (hi1 - lo1);
|
||||
map_ca(&mut b.img, |c, a| (c.map(|v| (lo0 + (v - lo1) * k).max(0.0)), a));
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
|
|
@ -538,48 +595,112 @@ const LINK_MODES: [(&str, BlendMode); 13] = [
|
|||
("Luminosity", BlendMode::Luminosity),
|
||||
];
|
||||
|
||||
/// Representative colour of an image: 0 average, 1 brightest, 2 darkest, 3 max RGB, 4 min RGB.
|
||||
fn sample_color(img: &Image, how: u32) -> [f32; 3] {
|
||||
let mut sum = [0.0f64; 4];
|
||||
let mut best: Option<([f32; 3], f32)> = None;
|
||||
let mut mx = [0.0f32; 3];
|
||||
let mut mn = [f32::INFINITY; 3];
|
||||
/// Color Link's Sample options.
|
||||
pub const LINK_SAMPLES: [&str; 10] =
|
||||
["Average", "Median", "Brightest", "Darkest", "Max RGB", "Min RGB", "Average Alpha", "Median Alpha", "Max Alpha", "Min Alpha"];
|
||||
|
||||
/// Element of sorted `v` at fraction `q`.
|
||||
fn quantile(v: &[f32], q: f64) -> f32 {
|
||||
if v.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
v[((q * (v.len() - 1) as f64).round() as usize).min(v.len() - 1)]
|
||||
}
|
||||
|
||||
/// Mean of `v` without the fraction `clip` at each end.
|
||||
fn trimmed_mean(v: &mut [f32], clip: f64) -> f32 {
|
||||
if v.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
v.sort_by(f32::total_cmp);
|
||||
let cut = (v.len() as f64 * clip) as usize;
|
||||
let s = &v[cut..v.len() - cut];
|
||||
if s.is_empty() { quantile(v, 0.5) } else { (s.iter().map(|x| *x as f64).sum::<f64>() / s.len() as f64) as f32 }
|
||||
}
|
||||
|
||||
/// Representative colour (RGB) and alpha of an image for a Sample option (see [`LINK_SAMPLES`]),
|
||||
/// ignoring the fraction `clip` of pixels at each extreme. Fully transparent pixels are skipped
|
||||
/// for the colour samples.
|
||||
fn sample_color(img: &Image, how: u32, clip: f64) -> ([f32; 3], f32) {
|
||||
let clip = clip.clamp(0.0, 0.49);
|
||||
let alpha_mode = how >= 6;
|
||||
let mut ch: [Vec<f32>; 3] = Default::default();
|
||||
let mut lum: Vec<(f32, [f32; 3])> = vec![];
|
||||
let mut alphas: Vec<f32> = Vec::with_capacity(img.data.len());
|
||||
let mut wsum = [0.0f64; 4];
|
||||
for px in &img.data {
|
||||
let (c, a) = unpremul(*px);
|
||||
if a <= 1e-3 {
|
||||
alphas.push(a);
|
||||
if alpha_mode || a <= 1e-3 {
|
||||
continue;
|
||||
}
|
||||
for k in 0..3 {
|
||||
sum[k] += (c[k] * a) as f64;
|
||||
mx[k] = mx[k].max(c[k]);
|
||||
mn[k] = mn[k].min(c[k]);
|
||||
ch[k].push(c[k]);
|
||||
}
|
||||
sum[3] += a as f64;
|
||||
let l = luminance(c[0], c[1], c[2]);
|
||||
let better = match best {
|
||||
None => true,
|
||||
Some((_, bl)) => (how == 1 && l > bl) || (how == 2 && l < bl),
|
||||
lum.push((luminance(c[0], c[1], c[2]), c));
|
||||
for k in 0..3 {
|
||||
wsum[k] += (c[k] * a) as f64;
|
||||
}
|
||||
wsum[3] += a as f64;
|
||||
}
|
||||
if alpha_mode {
|
||||
alphas.sort_by(f32::total_cmp);
|
||||
let a = match how {
|
||||
7 => quantile(&alphas, 0.5),
|
||||
8 => quantile(&alphas, 1.0 - clip),
|
||||
9 => quantile(&alphas, clip),
|
||||
_ => trimmed_mean(&mut alphas, clip),
|
||||
};
|
||||
if better {
|
||||
best = Some((c, l));
|
||||
return ([0.0; 3], a);
|
||||
}
|
||||
if lum.is_empty() {
|
||||
return ([0.0; 3], 1.0);
|
||||
}
|
||||
let c = match how {
|
||||
1 => ch.each_mut().map(|v| {
|
||||
v.sort_by(f32::total_cmp);
|
||||
quantile(v, 0.5)
|
||||
}),
|
||||
2 | 3 => {
|
||||
lum.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let q = if how == 2 { 1.0 - clip } else { clip };
|
||||
lum[((q * (lum.len() - 1) as f64).round() as usize).min(lum.len() - 1)].1
|
||||
}
|
||||
}
|
||||
if sum[3] <= 0.0 {
|
||||
return [0.0; 3];
|
||||
}
|
||||
match how {
|
||||
1 | 2 => best.map(|b| b.0).unwrap_or([0.0; 3]),
|
||||
3 => mx,
|
||||
4 => mn,
|
||||
_ => [0, 1, 2].map(|k| (sum[k] / sum[3]) as f32),
|
||||
}
|
||||
4 | 5 => ch.each_mut().map(|v| {
|
||||
v.sort_by(f32::total_cmp);
|
||||
quantile(v, if how == 4 { 1.0 - clip } else { clip })
|
||||
}),
|
||||
// Plain Average is alpha-weighted.
|
||||
0 if clip <= 0.0 => [0, 1, 2].map(|k| (wsum[k] / wsum[3]) as f32),
|
||||
_ => ch.each_mut().map(|v| trimmed_mean(v, clip)),
|
||||
};
|
||||
(c, 1.0)
|
||||
}
|
||||
|
||||
/// Color Link: colours the layer with a colour sampled from the Source Layer (its source, no
|
||||
/// masks or effects) or, with no source layer, from the layer itself.
|
||||
fn color_link(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = sample_color(&b.img, ctx.params.e("sampleSource"));
|
||||
let how = ctx.params.e("sampleSource");
|
||||
let clip = ctx.params.f("clip") / 100.0;
|
||||
let (c, sa) = match ctx.layer_param("sourceLayer", false) {
|
||||
Some(lp) => sample_color(&lp.buf.img, how, clip),
|
||||
None => sample_color(&b.img, how, clip),
|
||||
};
|
||||
let op = (ctx.params.f("opacity") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let mode = LINK_MODES[(ctx.params.e("blendingMode") as usize).min(LINK_MODES.len() - 1)].1;
|
||||
let stencil = ctx.params.b("stencilOriginalAlpha");
|
||||
if how >= 6 {
|
||||
// Alpha samples set the layer's alpha (blending modes don't apply).
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let (c0, a0) = unpremul(*px);
|
||||
let mut na = a0 + (sa - a0) * op;
|
||||
if stencil {
|
||||
na = na.min(a0);
|
||||
}
|
||||
*px = premul(c0, na.clamp(0.0, 1.0));
|
||||
});
|
||||
return b;
|
||||
}
|
||||
let mode = LINK_MODES[(ctx.params.e("blendingMode") as usize).min(LINK_MODES.len() - 1)].1;
|
||||
let src = [c[0] * op, c[1] * op, c[2] * op, op];
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let a0 = px[3];
|
||||
|
|
@ -759,6 +880,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
lv_params.push(p(ids[3], names[3], num(0.0), slider(-1.0, 2.0, 0.0, 1.0, 3)));
|
||||
lv_params.push(p(ids[4], names[4], num(1.0), slider(-1.0, 2.0, 0.0, 1.0, 3)));
|
||||
}
|
||||
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()) {
|
||||
for k in 0..4 {
|
||||
|
|
@ -767,12 +890,14 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
}
|
||||
let clip_params = |extra: bool| {
|
||||
let mut v = vec![
|
||||
p("temporalSmoothing", "Temporal Smoothing (seconds)", num(0.0), slider(0.0, 4.0, 0.0, 4.0, 2)),
|
||||
p("sceneDetect", "Scene Detect", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("blackClip", "Black Clip", num(0.1), slider(0.0, 49.0, 0.0, 10.0, 2)),
|
||||
p("whiteClip", "White Clip", num(0.1), slider(0.0, 49.0, 0.0, 10.0, 2)),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
];
|
||||
if extra {
|
||||
v.insert(0, p("snapNeutralMidtones", "Snap Neutral Midtones", Value::Bool(true), ParamUi::Checkbox));
|
||||
v.insert(4, p("snapNeutralMidtones", "Snap Neutral Midtones", Value::Bool(true), ParamUi::Checkbox));
|
||||
}
|
||||
v
|
||||
};
|
||||
|
|
@ -809,12 +934,16 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("autoAmounts", "Auto Amounts", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("shadowAmount", "Shadow Amount", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("highlightAmount", "Highlight Amount", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("shadowTonalWidth", "Shadow Tonal Width", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("shadowRadius", "Shadow Radius", num(30.0), slider(0.0, 2500.0, 0.0, 100.0, 0)),
|
||||
p("highlightTonalWidth", "Highlight Tonal Width", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("highlightRadius", "Highlight Radius", num(30.0), slider(0.0, 2500.0, 0.0, 100.0, 0)),
|
||||
p("colorCorrection", "Color Correction", num(20.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
p("midtoneContrast", "Midtone Contrast", num(0.0), pct()),
|
||||
p("temporalSmoothing", "Temporal Smoothing (seconds)", num(0.0), slider(0.0, 4.0, 0.0, 4.0, 2)),
|
||||
p("sceneDetect", "Scene Detect", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("moreOptions/shadowTonalWidth", "Shadow Tonal Width", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("moreOptions/shadowRadius", "Shadow Radius", num(30.0), slider(0.0, 2500.0, 0.0, 100.0, 0)),
|
||||
p("moreOptions/highlightTonalWidth", "Highlight Tonal Width", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("moreOptions/highlightRadius", "Highlight Radius", num(30.0), slider(0.0, 2500.0, 0.0, 100.0, 0)),
|
||||
p("moreOptions/colorCorrection", "Color Correction", num(20.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
p("moreOptions/midtoneContrast", "Midtone Contrast", num(0.0), pct()),
|
||||
p("moreOptions/blackClip", "Black Clip", num(0.01), slider(0.0, 49.0, 0.0, 10.0, 2)),
|
||||
p("moreOptions/whiteClip", "White Clip", num(0.01), slider(0.0, 49.0, 0.0, 10.0, 2)),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
shadow_highlight,
|
||||
|
|
@ -830,7 +959,11 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.color.colorlink",
|
||||
"Color Link",
|
||||
vec![
|
||||
p("sampleSource", "Sample", Value::Enum(0), popup(&["Average", "Brightest", "Darkest", "Max RGB", "Min RGB"])),
|
||||
p("sourceLayer", "Source Layer", Value::Layer(None), ParamUi::Layer),
|
||||
// A chosen source layer is sampled without its masks and effects.
|
||||
p("sourceLayerSource", "Source Layer Source", Value::Enum(0), ParamUi::Hidden),
|
||||
p("sampleSource", "Sample", Value::Enum(0), popup(&LINK_SAMPLES)),
|
||||
p("clip", "Clip", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("stencilOriginalAlpha", "Stencil Original Alpha", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(0), popup(&LINK_MODES.map(|m| m.0))),
|
||||
|
|
@ -844,7 +977,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("locale", "Broadcast Locale", Value::Enum(0), popup(&["NTSC", "PAL"])),
|
||||
p(
|
||||
"howToMakeSafe",
|
||||
"How to Make Color Safe",
|
||||
"How To Make Color Safe",
|
||||
Value::Enum(0),
|
||||
popup(&["Reduce Luminance", "Reduce Saturation", "Key Out Unsafe", "Key Out Safe"]),
|
||||
),
|
||||
|
|
@ -950,7 +1083,7 @@ mod tests {
|
|||
fn levels_ic_identity_and_red_only() {
|
||||
let img = ramp();
|
||||
assert_eq!(run("ec.color.levelsic", &img, &[]), img);
|
||||
let out = run("ec.color.levelsic", &img, &[("redOutWhite", num(0.5))]);
|
||||
let out = run("ec.color.levelsic", &img, &[("red/redOutWhite", num(0.5))]);
|
||||
let p = out.get(15, 3);
|
||||
assert!((p[0] - 0.5).abs() < 1e-4 && (p[1] - 0.0).abs() < 1e-4);
|
||||
}
|
||||
|
|
@ -1066,4 +1199,83 @@ mod tests {
|
|||
let out = run("ec.color.shadowhighlight", &img, &[("autoAmounts", Value::Bool(false))]);
|
||||
assert!(out.data[0][0] > 0.15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn levels_ic_clip_popups() {
|
||||
let img = Image::filled(2, 2, [0.9, 0.9, 0.9, 1.0]);
|
||||
let lv = [("rgb/rgbInWhite", num(0.5))];
|
||||
assert!((run("ec.color.levelsic", &img, &lv).data[0][0] - 1.0).abs() < 1e-6);
|
||||
let off = [("rgb/rgbInWhite", num(0.5)), ("clipToOutputWhite", Value::Enum(0))];
|
||||
assert!(run("ec.color.levelsic", &img, &off).data[0][0] > 1.5);
|
||||
}
|
||||
|
||||
/// A host whose other layers and other frames are a fixed image.
|
||||
struct ImgHost(Image);
|
||||
impl crate::EffectHost for ImgHost {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
Some(crate::LayerPixels { buf: Buf { img: self.0.clone(), offset: [0.0; 2], scale: 1.0 }, size: [2.0, 2.0] })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn self_at(&self, _: f64, _: usize) -> Option<Buf> {
|
||||
Some(Buf { img: self.0.clone(), offset: [0.0; 2], scale: 1.0 })
|
||||
}
|
||||
}
|
||||
|
||||
fn run_host(id: &str, img: &Image, over: &[(&str, Value)], host: &ImgHost) -> Image {
|
||||
let env = crate::EffectEnv { host: Some(host), frame_rate: 10.0, ..Default::default() };
|
||||
crate::run_fx(id, over, img.clone(), 1.0, env).img
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_link_samples_clip_and_source_layer() {
|
||||
let mut img = Image::new(5, 1);
|
||||
for (x, v) in [0.0f32, 0.1, 0.2, 0.3, 1.0].iter().enumerate() {
|
||||
img.set(x as u32, 0, [*v, *v, *v, 1.0]);
|
||||
}
|
||||
// Median ignores the outlier the average follows.
|
||||
let med = run("ec.color.colorlink", &img, &[("sampleSource", Value::Enum(1))]);
|
||||
assert!((med.data[0][0] - 0.2).abs() < 1e-5);
|
||||
// Brightest with 20 % clip skips the brightest pixel.
|
||||
let br = run("ec.color.colorlink", &img, &[("sampleSource", Value::Enum(2)), ("clip", num(20.0))]);
|
||||
assert!((br.data[0][0] - 0.3).abs() < 1e-5);
|
||||
// Min Alpha of a half-transparent source layer sets the alpha.
|
||||
let host = ImgHost(Image::filled(2, 2, [0.25, 0.25, 0.25, 0.5]));
|
||||
let out = run_host("ec.color.colorlink", &img, &[("sourceLayer", Value::Layer(Some(9))), ("sampleSource", Value::Enum(9))], &host);
|
||||
assert!((out.data[4][3] - 0.5).abs() < 1e-5);
|
||||
// Average of the source layer (not the effect's own pixels).
|
||||
let out = run_host("ec.color.colorlink", &img, &[("sourceLayer", Value::Layer(Some(9)))], &host);
|
||||
assert!((out.data[4][0] - 0.5).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_levels_temporal_smoothing_reads_neighbour_frames() {
|
||||
let mut img = Image::new(4, 1);
|
||||
for x in 0..4 {
|
||||
let v = 0.3 + 0.1 * x as f32;
|
||||
img.set(x, 0, [v, v, v, 1.0]);
|
||||
}
|
||||
let wide = ImgHost(ramp());
|
||||
let p0 = [("blackClip", num(0.0)), ("whiteClip", num(0.0))];
|
||||
let alone = run_host("ec.color.autolevels", &img, &p0, &wide);
|
||||
let smooth = run_host("ec.color.autolevels", &img, &[p0[0].clone(), p0[1].clone(), ("temporalSmoothing", num(0.2))], &wide);
|
||||
// Alone the frame is stretched to full range; with full-range neighbours much less.
|
||||
assert!(alone.data[0][0] < 0.01);
|
||||
assert!(smooth.data[0][0] > 0.1, "{:?}", smooth.data[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadow_highlight_clip_keeps_the_tonal_range() {
|
||||
let img = ramp();
|
||||
let lift = [("autoAmounts", Value::Bool(false)), ("shadowAmount", num(80.0))];
|
||||
let unclipped = run(
|
||||
"ec.color.shadowhighlight",
|
||||
&img,
|
||||
&[lift[0].clone(), lift[1].clone(), ("moreOptions/blackClip", num(0.0)), ("moreOptions/whiteClip", num(0.0))],
|
||||
);
|
||||
let clipped = run("ec.color.shadowhighlight", &img, &[lift[0].clone(), lift[1].clone(), ("moreOptions/blackClip", num(1.0))]);
|
||||
let min = |im: &Image| im.data.iter().filter(|p| p[3] > 0.99).map(|p| luminance(p[0], p[1], p[2])).fold(f32::MAX, f32::min);
|
||||
assert!(min(&clipped) < min(&unclipped));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,32 +251,40 @@ fn tint_offset(c: [f32; 4]) -> [f32; 3] {
|
|||
|
||||
fn lumetri(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let temp = (pr.f("temperature") / 100.0) as f32;
|
||||
let tint = (pr.f("tint") / 100.0) as f32;
|
||||
let expo = 2f32.powf(pr.f("exposure") as f32);
|
||||
let contrast = (pr.f("contrast") / 100.0) as f32;
|
||||
let (hi, sh, wh, bl) =
|
||||
((pr.f("highlights") / 100.0) as f32, (pr.f("shadows") / 100.0) as f32, (pr.f("whites") / 100.0) as f32, (pr.f("blacks") / 100.0) as f32);
|
||||
let sat = (pr.f("saturation") / 100.0) as f32;
|
||||
let intensity = (pr.f("lookIntensity") / 100.0) as f32;
|
||||
let faded = (pr.f("fadedFilm") / 100.0) as f32;
|
||||
let vib = (pr.f("vibrance") / 100.0) as f32;
|
||||
let csat = (pr.f("creativeSaturation") / 100.0) as f32;
|
||||
let st = tint_offset(pr.color("shadowTint"));
|
||||
let ht = tint_offset(pr.color("highlightTint"));
|
||||
let tbal = (pr.f("tintBalance") / 100.0) as f32;
|
||||
let temp = (pr.f("basicCorrection/whiteBalance/temperature") / 100.0) as f32;
|
||||
let tint = (pr.f("basicCorrection/whiteBalance/tint") / 100.0) as f32;
|
||||
let expo = 2f32.powf(pr.f("basicCorrection/tone/exposure") as f32);
|
||||
let contrast = (pr.f("basicCorrection/tone/contrast") / 100.0) as f32;
|
||||
let (hi, sh, wh, bl) = (
|
||||
(pr.f("basicCorrection/tone/highlights") / 100.0) as f32,
|
||||
(pr.f("basicCorrection/tone/shadows") / 100.0) as f32,
|
||||
(pr.f("basicCorrection/tone/whites") / 100.0) as f32,
|
||||
(pr.f("basicCorrection/tone/blacks") / 100.0) as f32,
|
||||
);
|
||||
let sat = (pr.f("basicCorrection/saturation") / 100.0) as f32;
|
||||
let intensity = (pr.f("creative/lookIntensity") / 100.0) as f32;
|
||||
let faded = (pr.f("creative/adjustments/fadedFilm") / 100.0) as f32;
|
||||
let vib = (pr.f("creative/adjustments/vibrance") / 100.0) as f32;
|
||||
let csat = (pr.f("creative/adjustments/creativeSaturation") / 100.0) as f32;
|
||||
let st = tint_offset(pr.color("creative/adjustments/shadowTint"));
|
||||
let ht = tint_offset(pr.color("creative/adjustments/highlightTint"));
|
||||
let tbal = (pr.f("creative/adjustments/tintBalance") / 100.0) as f32;
|
||||
let curve = |k: &str| {
|
||||
[(pr.f(&format!("{k}Shadows")) / 100.0) as f32, (pr.f(&format!("{k}Midtones")) / 100.0) as f32, (pr.f(&format!("{k}Highlights")) / 100.0) as f32]
|
||||
[
|
||||
(pr.f(&format!("curves/{k}Shadows")) / 100.0) as f32,
|
||||
(pr.f(&format!("curves/{k}Midtones")) / 100.0) as f32,
|
||||
(pr.f(&format!("curves/{k}Highlights")) / 100.0) as f32,
|
||||
]
|
||||
};
|
||||
let (cm, cr, cg, cb) = (curve("curveMaster"), curve("curveRed"), curve("curveGreen"), curve("curveBlue"));
|
||||
let ws = tint_offset(pr.color("shadowsWheel"));
|
||||
let wm = tint_offset(pr.color("midtonesWheel"));
|
||||
let wh_ = tint_offset(pr.color("highlightsWheel"));
|
||||
let vig = pr.f("vignetteAmount") as f32;
|
||||
let vmid = (pr.f("vignetteMidpoint") / 100.0) as f32;
|
||||
let vround = (pr.f("vignetteRoundness") / 100.0) as f32;
|
||||
let vfeather = (pr.f("vignetteFeather") / 100.0) as f32;
|
||||
let sharpen = (pr.f("sharpen") / 100.0) as f32;
|
||||
let ws = tint_offset(pr.color("colorWheels/shadowsWheel"));
|
||||
let wm = tint_offset(pr.color("colorWheels/midtonesWheel"));
|
||||
let wh_ = tint_offset(pr.color("colorWheels/highlightsWheel"));
|
||||
let vig = pr.f("vignette/vignetteAmount") as f32;
|
||||
let vmid = (pr.f("vignette/vignetteMidpoint") / 100.0) as f32;
|
||||
let vround = (pr.f("vignette/vignetteRoundness") / 100.0) as f32;
|
||||
let vfeather = (pr.f("vignette/vignetteFeather") / 100.0) as f32;
|
||||
let sharpen = (pr.f("creative/adjustments/sharpen") / 100.0) as f32;
|
||||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||||
let w = b.img.width as usize;
|
||||
let (cx, cy) = (lx + lw * 0.5, ly + lh * 0.5);
|
||||
|
|
@ -376,51 +384,51 @@ fn lumetri(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let grey = || col(0.5, 0.5, 0.5);
|
||||
let mut lum = vec![
|
||||
p("inputLut", "Input LUT", Value::Enum(0), popup(&["None"])),
|
||||
p("temperature", "Temperature", num(0.0), bipolar()),
|
||||
p("tint", "Tint", num(0.0), bipolar()),
|
||||
p("exposure", "Exposure", num(0.0), slider(-5.0, 5.0, -5.0, 5.0, 2)),
|
||||
p("contrast", "Contrast", num(0.0), bipolar()),
|
||||
p("highlights", "Highlights", num(0.0), bipolar()),
|
||||
p("shadows", "Shadows", num(0.0), bipolar()),
|
||||
p("whites", "Whites", num(0.0), bipolar()),
|
||||
p("blacks", "Blacks", num(0.0), bipolar()),
|
||||
p("saturation", "Saturation", num(100.0), slider(0.0, 200.0, 0.0, 200.0, 1)),
|
||||
p("look", "Look", Value::Enum(0), popup(&["None"])),
|
||||
p("lookIntensity", "Intensity", num(100.0), slider(0.0, 200.0, 0.0, 200.0, 1)),
|
||||
p("fadedFilm", "Faded Film", num(0.0), pct()),
|
||||
p("sharpen", "Sharpen", num(0.0), bipolar()),
|
||||
p("vibrance", "Vibrance", num(0.0), bipolar()),
|
||||
p("creativeSaturation", "Saturation", num(100.0), slider(0.0, 200.0, 0.0, 200.0, 1)),
|
||||
p("shadowTint", "Shadow Tint", grey(), ParamUi::Color),
|
||||
p("highlightTint", "Highlight Tint", grey(), ParamUi::Color),
|
||||
p("tintBalance", "Tint Balance", num(0.0), bipolar()),
|
||||
p("basicCorrection/inputLut", "Input LUT", Value::Enum(0), popup(&["None"])),
|
||||
p("basicCorrection/whiteBalance/temperature", "Temperature", num(0.0), bipolar()),
|
||||
p("basicCorrection/whiteBalance/tint", "Tint", num(0.0), bipolar()),
|
||||
p("basicCorrection/tone/exposure", "Exposure", num(0.0), slider(-5.0, 5.0, -5.0, 5.0, 2)),
|
||||
p("basicCorrection/tone/contrast", "Contrast", num(0.0), bipolar()),
|
||||
p("basicCorrection/tone/highlights", "Highlights", num(0.0), bipolar()),
|
||||
p("basicCorrection/tone/shadows", "Shadows", num(0.0), bipolar()),
|
||||
p("basicCorrection/tone/whites", "Whites", num(0.0), bipolar()),
|
||||
p("basicCorrection/tone/blacks", "Blacks", num(0.0), bipolar()),
|
||||
p("basicCorrection/saturation", "Saturation", num(100.0), slider(0.0, 200.0, 0.0, 200.0, 1)),
|
||||
p("creative/look", "Look", Value::Enum(0), popup(&["None"])),
|
||||
p("creative/lookIntensity", "Intensity", num(100.0), slider(0.0, 200.0, 0.0, 200.0, 1)),
|
||||
p("creative/adjustments/fadedFilm", "Faded Film", num(0.0), pct()),
|
||||
p("creative/adjustments/sharpen", "Sharpen", num(0.0), bipolar()),
|
||||
p("creative/adjustments/vibrance", "Vibrance", num(0.0), bipolar()),
|
||||
p("creative/adjustments/creativeSaturation", "Saturation", num(100.0), slider(0.0, 200.0, 0.0, 200.0, 1)),
|
||||
p("creative/adjustments/shadowTint", "Shadow Tint", grey(), ParamUi::Color),
|
||||
p("creative/adjustments/highlightTint", "Highlight Tint", grey(), ParamUi::Color),
|
||||
p("creative/adjustments/tintBalance", "Tint Balance", num(0.0), bipolar()),
|
||||
];
|
||||
const CURVES: [(&str, &str); 12] = [
|
||||
("curveMasterShadows", "RGB Curve Shadows"),
|
||||
("curveMasterMidtones", "RGB Curve Midtones"),
|
||||
("curveMasterHighlights", "RGB Curve Highlights"),
|
||||
("curveRedShadows", "Red Curve Shadows"),
|
||||
("curveRedMidtones", "Red Curve Midtones"),
|
||||
("curveRedHighlights", "Red Curve Highlights"),
|
||||
("curveGreenShadows", "Green Curve Shadows"),
|
||||
("curveGreenMidtones", "Green Curve Midtones"),
|
||||
("curveGreenHighlights", "Green Curve Highlights"),
|
||||
("curveBlueShadows", "Blue Curve Shadows"),
|
||||
("curveBlueMidtones", "Blue Curve Midtones"),
|
||||
("curveBlueHighlights", "Blue Curve Highlights"),
|
||||
("curves/curveMasterShadows", "RGB Curve Shadows"),
|
||||
("curves/curveMasterMidtones", "RGB Curve Midtones"),
|
||||
("curves/curveMasterHighlights", "RGB Curve Highlights"),
|
||||
("curves/curveRedShadows", "Red Curve Shadows"),
|
||||
("curves/curveRedMidtones", "Red Curve Midtones"),
|
||||
("curves/curveRedHighlights", "Red Curve Highlights"),
|
||||
("curves/curveGreenShadows", "Green Curve Shadows"),
|
||||
("curves/curveGreenMidtones", "Green Curve Midtones"),
|
||||
("curves/curveGreenHighlights", "Green Curve Highlights"),
|
||||
("curves/curveBlueShadows", "Blue Curve Shadows"),
|
||||
("curves/curveBlueMidtones", "Blue Curve Midtones"),
|
||||
("curves/curveBlueHighlights", "Blue Curve Highlights"),
|
||||
];
|
||||
for (id, name) in CURVES {
|
||||
lum.push(p(id, name, num(0.0), bipolar()));
|
||||
}
|
||||
lum.extend([
|
||||
p("shadowsWheel", "Shadows", grey(), ParamUi::Color),
|
||||
p("midtonesWheel", "Midtones", grey(), ParamUi::Color),
|
||||
p("highlightsWheel", "Highlights", grey(), ParamUi::Color),
|
||||
p("vignetteAmount", "Amount", num(0.0), slider(-5.0, 5.0, -5.0, 5.0, 2)),
|
||||
p("vignetteMidpoint", "Midpoint", num(50.0), pct()),
|
||||
p("vignetteRoundness", "Roundness", num(0.0), bipolar()),
|
||||
p("vignetteFeather", "Feather", num(50.0), pct()),
|
||||
p("colorWheels/shadowsWheel", "Shadows", grey(), ParamUi::Color),
|
||||
p("colorWheels/midtonesWheel", "Midtones", grey(), ParamUi::Color),
|
||||
p("colorWheels/highlightsWheel", "Highlights", grey(), ParamUi::Color),
|
||||
p("vignette/vignetteAmount", "Amount", num(0.0), slider(-5.0, 5.0, -5.0, 5.0, 2)),
|
||||
p("vignette/vignetteMidpoint", "Midpoint", num(50.0), pct()),
|
||||
p("vignette/vignetteRoundness", "Roundness", num(0.0), bipolar()),
|
||||
p("vignette/vignetteFeather", "Feather", num(50.0), pct()),
|
||||
]);
|
||||
vec![
|
||||
spec(
|
||||
|
|
@ -434,7 +442,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("colorToChange", "Color To Change", col(0.8, 0.2, 0.2), ParamUi::Color),
|
||||
p("matchingTolerance", "Matching Tolerance", num(15.0), pct()),
|
||||
p("matchingSoftness", "Matching Softness", num(0.0), pct()),
|
||||
p("matchColors", "Match colors", Value::Enum(1), popup(&["Using RGB", "Using Hue", "Using Chroma"])),
|
||||
p("matchColors", "Match Colors", Value::Enum(1), popup(&["Using RGB", "Using Hue", "Using Chroma"])),
|
||||
p("invertColorCorrectionMask", "Invert Color Correction Mask", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
change_color,
|
||||
|
|
@ -559,17 +567,17 @@ mod tests {
|
|||
let img = ramp();
|
||||
assert!(close(&run("ec.color.lumetri", &[], img.clone()), &img, 1e-5));
|
||||
let g = Image::filled(8, 8, [0.4, 0.4, 0.4, 1.0]);
|
||||
let e = run("ec.color.lumetri", &[("exposure", num(1.0))], g.clone());
|
||||
let e = run("ec.color.lumetri", &[("basicCorrection/tone/exposure", num(1.0))], g.clone());
|
||||
assert!((e.get(0, 0)[0] - 0.8).abs() < 1e-5);
|
||||
let warm = run("ec.color.lumetri", &[("temperature", num(50.0))], g.clone());
|
||||
let warm = run("ec.color.lumetri", &[("basicCorrection/whiteBalance/temperature", num(50.0))], g.clone());
|
||||
assert!(warm.get(0, 0)[0] > warm.get(0, 0)[2]);
|
||||
let bw = run("ec.color.lumetri", &[("saturation", num(0.0))], img.clone());
|
||||
let bw = run("ec.color.lumetri", &[("basicCorrection/saturation", num(0.0))], img.clone());
|
||||
let p = bw.get(5, 3);
|
||||
assert!((p[0] - p[1]).abs() < 1e-5 && (p[1] - p[2]).abs() < 1e-5);
|
||||
let v = run("ec.color.lumetri", &[("vignetteAmount", num(-3.0))], g.clone());
|
||||
let v = run("ec.color.lumetri", &[("vignette/vignetteAmount", num(-3.0))], g.clone());
|
||||
assert!(v.get(0, 0)[0] < v.get(4, 4)[0]);
|
||||
let c = run("ec.color.lumetri", &[("curveMasterMidtones", num(50.0))], g.clone());
|
||||
let c = run("ec.color.lumetri", &[("curves/curveMasterMidtones", num(50.0))], g.clone());
|
||||
assert!(c.get(0, 0)[0] > 0.4);
|
||||
assert_eq!(c.data, run("ec.color.lumetri", &[("curveMasterMidtones", num(50.0))], g).data);
|
||||
assert_eq!(c.data, run("ec.color.lumetri", &[("curves/curveMasterMidtones", num(50.0))], g).data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,30 @@ fn tritone(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Brightness & Contrast without Use Legacy: tone curves that keep black and white fixed (no
|
||||
/// clipping). Brightness bends the curve with a power (`b` in -1.5..1.5), contrast is an S
|
||||
/// curve around mid grey (`c` in -1..1). Values above 1 pass through. The GPU kernel mirrors it.
|
||||
pub fn bc_modern(v: f32, b: f32, c: f32) -> f32 {
|
||||
if v >= 1.0 {
|
||||
return v;
|
||||
}
|
||||
let v = v.max(0.0).powf(2f32.powf(-b));
|
||||
let g = 2f32.powf(2.0 * c);
|
||||
if v < 0.5 { 0.5 * (2.0 * v).powf(g) } else { 1.0 - 0.5 * (2.0 - 2.0 * v).max(0.0).powf(g) }
|
||||
}
|
||||
|
||||
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)));
|
||||
if br == 0.0 && ct == 0.0 {
|
||||
return b;
|
||||
}
|
||||
if ctx.params.b("useLegacy") {
|
||||
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)));
|
||||
} else {
|
||||
b.img.map_straight(|c| c.map(|v| bc_modern(v, br, ct)));
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
|
|
@ -74,18 +93,35 @@ fn hue_saturation(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Options of Levels' Clip To Output Black / White.
|
||||
pub const LEVELS_CLIP: [&str; 3] = ["Off", "On", "Off for 32 bpc Color"];
|
||||
|
||||
/// Whether Levels clips below Input Black / above Input White. "Off for 32 bpc Color" clips
|
||||
/// (effects can't see the project's bit depth; 8/16 bpc behaviour). The hidden `noClip` switch
|
||||
/// of projects saved before the two popups existed turns both off.
|
||||
pub fn levels_clip(ctx: &EffectCtx) -> (bool, bool) {
|
||||
if ctx.params.b("noClip") {
|
||||
return (false, false);
|
||||
}
|
||||
let on = |id: &str| ctx.params.get(id).is_none_or(|v| v.as_enum() != 0);
|
||||
(on("clipToOutputBlack"), on("clipToOutputWhite"))
|
||||
}
|
||||
|
||||
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");
|
||||
let (clip_b, clip_w) = levels_clip(ctx);
|
||||
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);
|
||||
if clip_b {
|
||||
t = t.max(0.0);
|
||||
}
|
||||
if clip_w {
|
||||
t = t.min(1.0);
|
||||
}
|
||||
let t = t.max(0.0).powf(1.0 / g);
|
||||
ob + (ow - ob) * t
|
||||
|
|
@ -94,20 +130,44 @@ fn levels(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Exposure's per-channel (gain, offset, gamma) for R, G, B: the Master group's values, or the
|
||||
/// Red / Green / Blue groups' with Channels = Individual Channels.
|
||||
pub fn exposure_settings(ctx: &EffectCtx) -> [(f32, f32, f32); 3] {
|
||||
let f = |k: &str| ctx.params.f(k) as f32;
|
||||
let one = |e: &str, o: &str, g: &str| (2f32.powf(f(e)), f(o), f(g).max(0.01));
|
||||
if ctx.params.e("channels") == 1 {
|
||||
[
|
||||
one("red/redExposure", "red/redOffset", "red/redGamma"),
|
||||
one("green/greenExposure", "green/greenOffset", "green/greenGamma"),
|
||||
one("blue/blueExposure", "blue/blueOffset", "blue/blueGamma"),
|
||||
]
|
||||
} else {
|
||||
[one("master/exposure", "master/offset", "master/gamma"); 3]
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
let s = exposure_settings(ctx);
|
||||
let bypass = ctx.params.b("bypassLinearLight");
|
||||
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)
|
||||
[0, 1, 2].map(|i| {
|
||||
let (e, off, g) = s[i];
|
||||
if bypass {
|
||||
return mirror_pow(c[i] * e + off, 1.0 / g);
|
||||
}
|
||||
let lin = effectcraft_color::srgb_to_linear(c[i].max(0.0));
|
||||
let o = mirror_pow(lin * e + off, 1.0 / g);
|
||||
effectcraft_color::linear_to_srgb(o.abs()).copysign(o)
|
||||
})
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// `|v|^p` with `v`'s sign: Exposure adjusts negative values as if they were positive.
|
||||
pub fn mirror_pow(v: f32, p: f32) -> f32 {
|
||||
v.abs().powf(p).copysign(v)
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -150,41 +210,122 @@ fn fill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Change to Color: pixels whose hue, lightness and saturation are each within their tolerance
|
||||
/// of From are changed (Softness feathers the match). Change picks the components that change;
|
||||
/// Change By either sets them to To's (Setting To Color) or shifts them by To − From
|
||||
/// (Transforming To Color). View Correction Matte shows the match as grey.
|
||||
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 tol = ["toleranceGroup/hue", "toleranceGroup/lightness", "toleranceGroup/saturation"].map(|k| ctx.params.f(k) as f32 / 100.0);
|
||||
let soft = ctx.params.f("softness") as f32 / 100.0;
|
||||
let change = ctx.params.e("change");
|
||||
let (ch_l, ch_s) = (matches!(change, 1 | 3), matches!(change, 2 | 3));
|
||||
let transform = ctx.params.e("changeBy") == 1;
|
||||
let matte = ctx.params.b("viewCorrectionMatte");
|
||||
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)
|
||||
let d = [((h - fh + 0.5).rem_euclid(1.0) - 0.5).abs() * 2.0, (l - fl).abs(), (s - fs).abs()];
|
||||
let mut k = 1.0f32;
|
||||
for i in 0..3 {
|
||||
let over = d[i] - tol[i];
|
||||
if over > 0.0 {
|
||||
k = k.min(if soft <= 1e-6 { 0.0 } else { (1.0 - over / soft).clamp(0.0, 1.0) });
|
||||
}
|
||||
}
|
||||
if matte {
|
||||
return [k; 3];
|
||||
}
|
||||
if k <= 0.0 {
|
||||
return c;
|
||||
}
|
||||
let (nh, ns, nl) = if transform {
|
||||
let dh = ((th - fh + 0.5).rem_euclid(1.0) - 0.5) * k;
|
||||
((h + dh).rem_euclid(1.0), if ch_s { s + (ts - fs) * k } else { s }, if ch_l { l + (tl - fl) * k } else { l })
|
||||
} else {
|
||||
let dh = ((th - h + 0.5).rem_euclid(1.0) - 0.5) * k;
|
||||
((h + dh).rem_euclid(1.0), if ch_s { s + (ts - s) * k } else { s }, if ch_l { l + (tl - l) * k } else { l })
|
||||
};
|
||||
let (r, g, bl) = hsl_to_rgb(nh, ns.clamp(0.0, 1.0), nl.clamp(0.0, 1.0));
|
||||
[r, g, bl]
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Leave Color: decolours pixels unlike Color To Leave. Tolerance 0 keeps only exact matches,
|
||||
/// 100 keeps everything; Edge Softness feathers the boundary; Match Colors compares RGB
|
||||
/// distance or hue only.
|
||||
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 soft = ctx.params.f("edgeSoftness") as f32 / 100.0;
|
||||
let by_hue = ctx.params.e("matchColors") == 1;
|
||||
if amt <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
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 d = if by_hue {
|
||||
let (h, s, _) = rgb_to_hsl(c[0], c[1], c[2]);
|
||||
// Greys have no hue: they never match a hue.
|
||||
if s < 0.02 { 1.0 } else { ((h - kh + 0.5).rem_euclid(1.0) - 0.5).abs() * 2.0 }
|
||||
} else {
|
||||
((c[0] - keep[0]).powi(2) + (c[1] - keep[1]).powi(2) + (c[2] - keep[2]).powi(2)).sqrt() / 3f32.sqrt()
|
||||
};
|
||||
let keepk = if tol >= 1.0 || d <= tol {
|
||||
1.0
|
||||
} else if soft <= 1e-6 {
|
||||
0.0
|
||||
} else {
|
||||
(1.0 - (d - tol) / soft).clamp(0.0, 1.0)
|
||||
};
|
||||
let g = luminance(c[0], c[1], c[2]);
|
||||
mix(c, [g, g, g], amt * (1.0 - keepk))
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Photo Filter's Filter menu: the conventional camera filters (Wratten-style warming /
|
||||
/// cooling conversion and light-balancing filters) and plain colours, as 8-bit sRGB values
|
||||
/// chosen for EffectCraft; the last entry, Custom, uses the Color parameter.
|
||||
pub const PHOTO_FILTERS: [(&str, [u8; 3]); 21] = [
|
||||
("Warming Filter (85)", [240, 140, 30]),
|
||||
("Warming Filter (LBA)", [245, 155, 25]),
|
||||
("Warming Filter (81)", [240, 180, 40]),
|
||||
("Cooling Filter (80)", [20, 110, 250]),
|
||||
("Cooling Filter (LBB)", [20, 95, 245]),
|
||||
("Cooling Filter (82)", [30, 180, 250]),
|
||||
("Red", [230, 30, 30]),
|
||||
("Orange", [240, 130, 30]),
|
||||
("Yellow", [245, 225, 35]),
|
||||
("Green", [30, 200, 30]),
|
||||
("Cyan", [30, 200, 230]),
|
||||
("Blue", [30, 55, 230]),
|
||||
("Violet", [150, 30, 230]),
|
||||
("Magenta", [225, 30, 225]),
|
||||
("Sepia", [170, 120, 55]),
|
||||
("Deep Red", [250, 0, 0]),
|
||||
("Deep Blue", [0, 40, 200]),
|
||||
("Deep Emerald", [0, 135, 0]),
|
||||
("Deep Yellow", [255, 210, 0]),
|
||||
("Underwater", [0, 190, 175]),
|
||||
("Custom", [0, 0, 0]),
|
||||
];
|
||||
/// Index of Photo Filter's Custom entry.
|
||||
pub const PHOTO_FILTER_CUSTOM: u32 = PHOTO_FILTERS.len() as u32 - 1;
|
||||
|
||||
fn photo_filter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = ctx.params.color("color");
|
||||
let f = ctx.params.e("filter");
|
||||
let c = match PHOTO_FILTERS.get(f as usize) {
|
||||
Some((_, rgb)) if f != PHOTO_FILTER_CUSTOM => rgb.map(|v| v as f32 / 255.0),
|
||||
_ => {
|
||||
let c = ctx.params.color("color");
|
||||
[c[0], c[1], c[2]]
|
||||
}
|
||||
};
|
||||
let d = ctx.params.f("density") as f32 / 100.0;
|
||||
let keep = ctx.params.b("preserveLuminosity");
|
||||
b.img.map_straight(|x| {
|
||||
|
|
@ -217,6 +358,7 @@ fn color_balance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let sh = [g("shadowRed"), g("shadowGreen"), g("shadowBlue")];
|
||||
let md = [g("midRed"), g("midGreen"), g("midBlue")];
|
||||
let hl = [g("hiRed"), g("hiGreen"), g("hiBlue")];
|
||||
let keep_luma = ctx.params.b("preserveLuminosity");
|
||||
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);
|
||||
|
|
@ -226,6 +368,11 @@ fn color_balance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
for i in 0..3 {
|
||||
o[i] = (c[i] + (sh[i] * ws + md[i] * wm + hl[i] * wh) * 0.5).max(0.0);
|
||||
}
|
||||
if keep_luma {
|
||||
// Preserve Luminosity: keep the pixel's luminance, change only its colour.
|
||||
let d = luminance(c[0], c[1], c[2]) - luminance(o[0], o[1], o[2]);
|
||||
o = o.map(|v| (v + d).max(0.0));
|
||||
}
|
||||
o
|
||||
});
|
||||
b
|
||||
|
|
@ -253,28 +400,66 @@ fn gamma_pg(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let r = ch("red");
|
||||
let g = ch("green");
|
||||
let bl = ch("blue");
|
||||
let stretch = ctx.params.f("blackStretch").max(0.0) as f32;
|
||||
b.img.map_straight(|c| {
|
||||
// Black Stretch lifts the low values of every channel (a toe that fades out by mid grey).
|
||||
let toe = |v: f32| if stretch > 0.0 && v > 0.0 && v < 1.0 { v + stretch * 0.25 * v * (1.0 - v).powi(4) } else { v };
|
||||
let c = c.map(toe);
|
||||
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
|
||||
}
|
||||
|
||||
/// Colorama's Get Phase From options.
|
||||
pub const COLORAMA_PHASE: [&str; 10] = ["Intensity", "Red", "Green", "Blue", "Hue", "Lightness", "Saturation", "Value", "Alpha", "Zero"];
|
||||
|
||||
/// The input phase (0..1) of a straight colour + alpha for Get Phase From option `mode`.
|
||||
fn colorama_phase(c: [f32; 3], a: f32, mode: u32) -> f32 {
|
||||
let v = match mode {
|
||||
1 => c[0],
|
||||
2 => c[1],
|
||||
3 => c[2],
|
||||
4 => rgb_to_hsl(c[0], c[1], c[2]).0,
|
||||
5 => rgb_to_hsl(c[0], c[1], c[2]).2,
|
||||
6 => rgb_to_hsl(c[0], c[1], c[2]).1,
|
||||
7 => c[0].max(c[1]).max(c[2]),
|
||||
8 => a,
|
||||
9 => 0.0,
|
||||
_ => luminance(c[0], c[1], c[2]),
|
||||
};
|
||||
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).
|
||||
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 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.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.img.data.iter_mut().for_each(|px| {
|
||||
let a = px[3];
|
||||
if a <= 0.0 {
|
||||
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 (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];
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let pct = || slider(-100.0, 100.0, -100.0, 100.0, 1);
|
||||
let mut gpg = Vec::new();
|
||||
let mut gpg = vec![p("blackStretch", "Black Stretch", num(0.0), slider(0.0, 4.0, 0.0, 4.0, 2))];
|
||||
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)));
|
||||
|
|
@ -306,7 +491,11 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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())],
|
||||
vec![
|
||||
p("brightness", "Brightness", num(0.0), slider(-150.0, 150.0, -150.0, 150.0, 1)),
|
||||
p("contrast", "Contrast", num(0.0), pct()),
|
||||
p("useLegacy", "Use Legacy", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
brightness_contrast,
|
||||
),
|
||||
spec(
|
||||
|
|
@ -332,18 +521,32 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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),
|
||||
p("clipToOutputBlack", "Clip To Output Black", Value::Enum(2), popup(&LEVELS_CLIP)),
|
||||
p("clipToOutputWhite", "Clip To Output White", Value::Enum(2), popup(&LEVELS_CLIP)),
|
||||
// Projects saved before the two popups: "Don't Clip".
|
||||
p("noClip", "Don't Clip", Value::Bool(false), ParamUi::Hidden),
|
||||
],
|
||||
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)),
|
||||
],
|
||||
{
|
||||
let mut v = vec![p("channels", "Channels", Value::Enum(0), popup(&["Master", "Individual Channels"]))];
|
||||
let group = |ids: [&'static str; 3]| {
|
||||
[
|
||||
p(ids[0], "Exposure", num(0.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||||
p(ids[1], "Offset", num(0.0), slider(-2.0, 2.0, -0.5, 0.5, 4)),
|
||||
p(ids[2], "Gamma Correction", num(1.0), slider(0.01, 9.99, 0.1, 3.0, 2)),
|
||||
]
|
||||
};
|
||||
v.extend(group(["master/exposure", "master/offset", "master/gamma"]));
|
||||
v.extend(group(["red/redExposure", "red/redOffset", "red/redGamma"]));
|
||||
v.extend(group(["green/greenExposure", "green/greenOffset", "green/greenGamma"]));
|
||||
v.extend(group(["blue/blueExposure", "blue/blueOffset", "blue/blueGamma"]));
|
||||
v.push(p("bypassLinearLight", "Bypass Linear Light Conversion", Value::Bool(false), ParamUi::Checkbox));
|
||||
v
|
||||
},
|
||||
exposure,
|
||||
),
|
||||
spec(
|
||||
|
|
@ -380,8 +583,13 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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)),
|
||||
p("change", "Change", Value::Enum(0), popup(&["Hue", "Hue & Lightness", "Hue & Saturation", "Hue, Lightness & Saturation"])),
|
||||
p("changeBy", "Change By", Value::Enum(0), popup(&["Setting To Color", "Transforming To Color"])),
|
||||
p("toleranceGroup/hue", "Hue", num(5.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("toleranceGroup/lightness", "Lightness", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("toleranceGroup/saturation", "Saturation", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("softness", "Softness", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("viewCorrectionMatte", "View Correction Matte", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
change_to_color,
|
||||
),
|
||||
|
|
@ -392,6 +600,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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)),
|
||||
p("edgeSoftness", "Edge Softness", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("matchColors", "Match Colors", Value::Enum(0), popup(&["Using RGB", "Using Hue"])),
|
||||
],
|
||||
leave_color,
|
||||
),
|
||||
|
|
@ -399,6 +609,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.color.photofilter",
|
||||
"Photo Filter",
|
||||
vec![
|
||||
p("filter", "Filter", Value::Enum(0), popup(&PHOTO_FILTERS.map(|f| f.0))),
|
||||
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),
|
||||
|
|
@ -423,6 +634,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Highlight Blue Balance",
|
||||
])
|
||||
.map(|(id, n)| p(id, n, num(0.0), pct()))
|
||||
.chain([p("preserveLuminosity", "Preserve Luminosity", Value::Bool(false), ParamUi::Checkbox)])
|
||||
.collect(),
|
||||
color_balance,
|
||||
),
|
||||
|
|
@ -460,12 +672,169 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"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("inputPhase/mode", "Get Phase From", Value::Enum(0), popup(&COLORAMA_PHASE)),
|
||||
p("inputPhase/phaseShift", "Phase Shift", num(0.0), ParamUi::Angle),
|
||||
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("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,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{EffectEnv, run_fx};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
fn px(id: &str, vals: &[(&str, Value)], c: [f32; 4]) -> [f32; 4] {
|
||||
run_fx(id, vals, Image::filled(2, 2, c), 0.0, EffectEnv::default()).img.get(0, 0)
|
||||
}
|
||||
fn close(a: [f32; 4], b: [f32; 4], eps: f32) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() <= eps)
|
||||
}
|
||||
|
||||
/// Projects saved before Use Legacy / the Photo Filter menu existed keep rendering as before.
|
||||
#[test]
|
||||
fn old_saves_get_legacy_values_for_added_params() {
|
||||
use effectcraft_project::build::Ids;
|
||||
let mut next = 1;
|
||||
for (id, param, want) in
|
||||
[("ec.color.brightnesscontrast", "useLegacy", Value::Bool(true)), ("ec.color.photofilter", "filter", Value::Enum(PHOTO_FILTER_CUSTOM))]
|
||||
{
|
||||
let spec = crate::find(id).unwrap();
|
||||
let mut g = crate::instantiate(spec, &mut Ids(&mut next), spec.name, [10.0, 10.0]);
|
||||
g.children.retain(|c| c.match_id() != param);
|
||||
crate::migrate::upgrade_instance(spec, &mut g, &mut Ids(&mut next), [10.0, 10.0]);
|
||||
assert_eq!(g.get(param).unwrap().value, want, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colorama_reads_get_phase_from() {
|
||||
// Same intensity, different red: Intensity gives one colour, Red gives two.
|
||||
let a = [0.6, 0.2, 0.2, 1.0];
|
||||
let b = [0.2, 0.2, 0.6, 1.0];
|
||||
let red = |c| px("ec.color.colorama", &[("inputPhase/mode", Value::Enum(1))], c);
|
||||
assert!(!close(red(a), red(b), 1e-3));
|
||||
// Zero maps everything to the start of the cycle (red).
|
||||
assert!(close(px("ec.color.colorama", &[("inputPhase/mode", Value::Enum(9))], b), [1.0, 0.0, 0.0, 1.0], 1e-5));
|
||||
// Interpolate Palette off posterizes: two nearby inputs land on the same colour.
|
||||
let off = |v: f32| px("ec.color.colorama", &[("outputCycle/interpolatePalette", Value::Bool(false))], [v, v, v, 1.0]);
|
||||
assert_eq!(off(0.20), off(0.22));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brightness_contrast_modern_keeps_black_and_white_and_legacy_is_linear() {
|
||||
let bc = |vals: &[(&str, Value)], v: f32| px("ec.color.brightnesscontrast", vals, [v, v, v, 1.0])[0];
|
||||
let up = [("brightness", num(50.0)), ("contrast", num(40.0))];
|
||||
assert_eq!(bc(&up, 0.0), 0.0);
|
||||
assert!((bc(&up, 1.0) - 1.0).abs() < 1e-6);
|
||||
assert!(bc(&up, 0.4) > 0.4);
|
||||
let legacy = [("brightness", num(50.0)), ("useLegacy", Value::Bool(true))];
|
||||
assert!((bc(&legacy, 0.2) - 0.7).abs() < 1e-5);
|
||||
assert!((bc(&legacy, 0.0) - 0.5).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn levels_clips_black_and_white_independently() {
|
||||
let lv = |vals: &[(&str, Value)], v: f32| px("ec.color.levels", vals, [v, v, v, 1.0])[0];
|
||||
let base = [("inBlack", num(0.2)), ("inWhite", num(0.6)), ("outBlack", num(0.1)), ("outWhite", num(0.9))];
|
||||
assert!((lv(&base, 0.9) - 0.9).abs() < 1e-5);
|
||||
let mut no_white = base.to_vec();
|
||||
no_white.push(("clipToOutputWhite", Value::Enum(0)));
|
||||
assert!(lv(&no_white, 0.9) > 0.9);
|
||||
assert!((lv(&no_white, 0.0) - 0.1).abs() < 1e-5);
|
||||
// The hidden switch of older projects turns both off.
|
||||
let mut legacy = base.to_vec();
|
||||
legacy.push(("noClip", Value::Bool(true)));
|
||||
assert!(lv(&legacy, 0.9) > 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exposure_individual_channels_and_bypass() {
|
||||
let g = [0.5, 0.5, 0.5, 1.0];
|
||||
let one = px("ec.color.exposure", &[("channels", Value::Enum(1)), ("red/redExposure", num(1.0))], g);
|
||||
assert!(one[0] > 0.6 && (one[1] - 0.5).abs() < 1e-5 && (one[2] - 0.5).abs() < 1e-5);
|
||||
// Master values are ignored in Individual Channels mode.
|
||||
assert!(close(px("ec.color.exposure", &[("channels", Value::Enum(1)), ("master/exposure", num(2.0))], g), g, 1e-5));
|
||||
// Bypass Linear Light Conversion: +1 stop doubles the raw value.
|
||||
let raw = px("ec.color.exposure", &[("master/exposure", num(1.0)), ("bypassLinearLight", Value::Bool(true))], [0.25, 0.25, 0.25, 1.0]);
|
||||
assert!((raw[0] - 0.5).abs() < 1e-5);
|
||||
// Negative results are mirrored, not clipped.
|
||||
let neg = px(
|
||||
"ec.color.exposure",
|
||||
&[("master/offset", num(-0.5)), ("bypassLinearLight", Value::Bool(true)), ("master/gamma", num(2.0))],
|
||||
[0.25, 0.25, 0.25, 1.0],
|
||||
);
|
||||
assert!((neg[0] + 0.5).abs() < 1e-4, "{neg:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_to_color_change_modes_tolerances_and_matte() {
|
||||
let red = [0.8, 0.1, 0.1, 1.0];
|
||||
let green = [0.1, 0.8, 0.1, 1.0];
|
||||
let base = [("from", col(0.8, 0.1, 0.1)), ("to", col(0.1, 0.1, 0.8))];
|
||||
// Hue only: red turns blue with its own lightness / saturation; green is untouched.
|
||||
let o = px("ec.color.changetocolor", &base, red);
|
||||
assert!(o[2] > 0.7 && o[0] < 0.2, "{o:?}");
|
||||
assert!(close(px("ec.color.changetocolor", &base, green), green, 1e-5));
|
||||
// The matte is white where matched, black elsewhere.
|
||||
let mut m = base.to_vec();
|
||||
m.push(("viewCorrectionMatte", Value::Bool(true)));
|
||||
assert!(close(px("ec.color.changetocolor", &m, red), [1.0, 1.0, 1.0, 1.0], 1e-5));
|
||||
assert!(close(px("ec.color.changetocolor", &m, green), [0.0, 0.0, 0.0, 1.0], 1e-5));
|
||||
// A lightness tolerance of 0 rejects a lighter red.
|
||||
let mut strict = m.clone();
|
||||
strict.push(("toleranceGroup/lightness", num(0.0)));
|
||||
strict.push(("softness", num(0.0)));
|
||||
assert!(close(px("ec.color.changetocolor", &strict, [1.0, 0.5, 0.5, 1.0]), [0.0, 0.0, 0.0, 1.0], 1e-5));
|
||||
// Hue, Lightness & Saturation, set: a matched pixel becomes the To colour.
|
||||
let mut all = base.to_vec();
|
||||
all.push(("change", Value::Enum(3)));
|
||||
assert!(close(px("ec.color.changetocolor", &all, red), [0.1, 0.1, 0.8, 1.0], 1e-4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leave_color_tolerance_softness_and_match_mode() {
|
||||
let green = [0.1, 0.8, 0.1, 1.0];
|
||||
let lc = |vals: &[(&str, Value)]| {
|
||||
let mut v = vec![("amount", num(100.0))];
|
||||
v.extend_from_slice(vals);
|
||||
px("ec.color.leavecolor", &v, green)
|
||||
};
|
||||
let g = luminance(0.1, 0.8, 0.1);
|
||||
assert!(close(lc(&[]), [g, g, g, 1.0], 1e-5));
|
||||
assert!(close(lc(&[("tolerance", num(100.0))]), green, 1e-6));
|
||||
// Softness keeps part of the colour.
|
||||
let s = lc(&[("tolerance", num(10.0)), ("edgeSoftness", num(100.0))]);
|
||||
assert!(s[1] > g + 0.05 && s[1] < 0.8);
|
||||
// Using Hue: a dark red still matches pure red.
|
||||
let dark = px("ec.color.leavecolor", &[("amount", num(100.0)), ("tolerance", num(5.0)), ("matchColors", Value::Enum(1))], [0.4, 0.0, 0.0, 1.0]);
|
||||
assert!(close(dark, [0.4, 0.0, 0.0, 1.0], 1e-6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn photo_filter_presets_and_custom() {
|
||||
let g = [0.5, 0.5, 0.5, 1.0];
|
||||
let cool = px("ec.color.photofilter", &[("filter", Value::Enum(3)), ("preserveLuminosity", Value::Bool(false))], g);
|
||||
assert!(cool[2] > cool[0], "a cooling filter is blue: {cool:?}");
|
||||
let custom = |c| px("ec.color.photofilter", &[("filter", Value::Enum(PHOTO_FILTER_CUSTOM)), ("color", c)], g);
|
||||
assert!(custom(col(0.0, 1.0, 0.0))[1] > custom(col(1.0, 0.0, 0.0))[1]);
|
||||
// The Color swatch is ignored by the presets.
|
||||
let warm = |c| px("ec.color.photofilter", &[("color", c)], g);
|
||||
assert_eq!(warm(col(0.0, 1.0, 0.0)), warm(col(1.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_balance_preserve_luminosity_and_black_stretch() {
|
||||
let g = [0.5, 0.5, 0.5, 1.0];
|
||||
let o = px("ec.color.colorbalance", &[("midRed", num(60.0)), ("preserveLuminosity", Value::Bool(true))], g);
|
||||
assert!((luminance(o[0], o[1], o[2]) - 0.5).abs() < 1e-4 && o[0] > o[2]);
|
||||
let dark = [0.1, 0.1, 0.1, 1.0];
|
||||
assert!(px("ec.color.gammapedestalgain", &[("blackStretch", num(4.0))], dark)[0] > 0.15);
|
||||
assert!(close(px("ec.color.gammapedestalgain", &[], dark), dark, 1e-6));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ fn remap(src: &Image, repeat: bool, f: impl Fn(f64, f64) -> (f64, f64) + Sync) -
|
|||
|
||||
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);
|
||||
// Twirl Radius is a percentage of the layer's larger dimension: 50 reaches the edges.
|
||||
let r = (ctx.params.f("radius") / 100.0 * b.layer_w(ctx).max(b.layer_h(ctx)) * 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);
|
||||
|
|
@ -63,18 +64,97 @@ fn bulge(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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");
|
||||
// Taper Radius: higher values make the sides of the bulge shallower (a more peaked profile).
|
||||
let taper = 1.0 + ctx.params.f("taperRadius").max(0.0) / 100.0 * 3.0;
|
||||
let pin = ctx.params.b("pinAllEdges");
|
||||
let (lx, ly) = (b.offset[0], b.offset[1]);
|
||||
let (lw, lh) = (b.layer_w(ctx) * b.scale, b.layer_h(ctx) * b.scale);
|
||||
let edge = (lw.min(lh) * 0.1).max(1.0);
|
||||
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;
|
||||
let mut amt = h * (1.0 - d2).powf(taper) * 0.5;
|
||||
if pin {
|
||||
let ex = ((x - lx - 0.5).min(lx + lw - 0.5 - x) / edge).clamp(0.0, 1.0);
|
||||
let ey = ((y - ly - 0.5).min(ly + lh - 0.5 - y) / edge).clamp(0.0, 1.0);
|
||||
amt *= ex.min(ey);
|
||||
}
|
||||
let k = 1.0 - amt;
|
||||
(c.0 + dx * rx * k, c.1 + dy * ry * k)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Wave Warp's wave shapes ([`WAVE_TYPES`] order) at phase `t` (radians), in -1..1. `seed`
|
||||
/// seeds the noise shapes.
|
||||
fn wave_shape(kind: u32, t: f64, seed: u32) -> f64 {
|
||||
let tau = std::f64::consts::TAU;
|
||||
let ph = t.rem_euclid(tau) / tau;
|
||||
// A half circle over u in 0..1 (0 at the ends, 1 in the middle).
|
||||
let half = |u: f64| (1.0 - (2.0 * u - 1.0).powi(2)).max(0.0).sqrt();
|
||||
let noise = |i: f64| crate::util::hash1(i as i64 as u32, 0x5eed, seed) as f64 * 2.0 - 1.0;
|
||||
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,
|
||||
// Circle: alternating upper and lower half circles.
|
||||
4 => {
|
||||
if ph < 0.5 {
|
||||
half(ph * 2.0)
|
||||
} else {
|
||||
-half(ph * 2.0 - 1.0)
|
||||
}
|
||||
}
|
||||
// Semicircle: a row of half circles bulging the same way.
|
||||
5 => 2.0 * half((ph * 2.0).fract()) - 1.0,
|
||||
// Uncircle: the concave gaps between half circles, meeting in cusps.
|
||||
6 => 1.0 - 2.0 * half((ph * 2.0).fract()),
|
||||
// Noise: a random level per half wave; Smooth Noise eases between the levels.
|
||||
7 => noise((t / tau * 2.0).floor()),
|
||||
8 => {
|
||||
let u = t / tau * 2.0;
|
||||
let (i, f) = (u.floor(), u - u.floor());
|
||||
let s = f * f * (3.0 - 2.0 * f);
|
||||
noise(i) * (1.0 - s) + noise(i + 1.0) * s
|
||||
}
|
||||
_ => t.sin(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const WAVE_TYPES: [&str; 9] = ["Sine", "Square", "Triangle", "Sawtooth", "Circle", "Semicircle", "Uncircle", "Noise", "Smooth Noise"];
|
||||
pub(crate) const WAVE_PINNING: [&str; 9] =
|
||||
["None", "All Edges", "Center", "Left Edge", "Top Edge", "Right Edge", "Bottom Edge", "Horizontal Edges", "Vertical Edges"];
|
||||
|
||||
/// Wave Warp pinning weight (0 = pinned, 1 = free) at buffer pixel (x, y) of the layer
|
||||
/// rectangle `(x0, y0, w, h)`.
|
||||
fn wave_pin(pin: u32, x: f64, y: f64, rect: (f64, f64, f64, f64)) -> f64 {
|
||||
let (x0, y0, w, h) = rect;
|
||||
// Measured between the outermost pixel centres, so pinned edges stay exactly in place.
|
||||
let (u, v) = (((x - x0 - 0.5) / (w - 1.0).max(1.0)).clamp(0.0, 1.0), ((y - y0 - 0.5) / (h - 1.0).max(1.0)).clamp(0.0, 1.0));
|
||||
let horiz = (v.min(1.0 - v) * 2.0).clamp(0.0, 1.0);
|
||||
let vert = (u.min(1.0 - u) * 2.0).clamp(0.0, 1.0);
|
||||
match pin {
|
||||
1 => horiz.min(vert),
|
||||
2 => (((u - 0.5).powi(2) + (v - 0.5).powi(2)).sqrt() * 2.0).min(1.0),
|
||||
3 => u,
|
||||
4 => v,
|
||||
5 => 1.0 - u,
|
||||
6 => 1.0 - v,
|
||||
7 => horiz,
|
||||
8 => vert,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -82,36 +162,26 @@ fn wave_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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 pin = ctx.params.e("pinning");
|
||||
let seed = ctx.seed ^ 0x3a7e;
|
||||
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(),
|
||||
}
|
||||
};
|
||||
let rect = (b.offset[0], b.offset[1], (b.layer_w(ctx) * b.scale).max(1.0), (b.layer_h(ctx) * b.scale).max(1.0));
|
||||
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;
|
||||
let disp = wave_shape(kind, along / w * std::f64::consts::TAU + phase, seed) * h * wave_pin(pin, x, y, rect);
|
||||
(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);
|
||||
// Radius is a percentage of the layer: 100 from the layer centre reaches the edge.
|
||||
let r = (ctx.params.f("radius") / 100.0 * b.layer_w(ctx).max(b.layer_h(ctx)) * 0.5 * b.scale).max(1.0);
|
||||
let asymmetric = ctx.params.e("conversion") == 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;
|
||||
|
|
@ -123,8 +193,12 @@ fn ripple(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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)
|
||||
let arg = d / w * std::f64::consts::TAU + phase;
|
||||
let o = arg.sin() * h * fall;
|
||||
// Asymmetric ripples also move sideways (a quarter wave out of step).
|
||||
let t = if asymmetric { arg.cos() * h * fall * 0.5 } else { 0.0 };
|
||||
let (ux, uy) = (dx / d, dy / d);
|
||||
(x + ux * o - uy * t, y + uy * o + ux * t)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
|
@ -193,13 +267,14 @@ fn transform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let skew_axis = ctx.params.f("skewAxis");
|
||||
let rot = ctx.params.f("rotation");
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let sampling = if ctx.params.e("sampling") == 1 { effectcraft_raster::Sampling::Bicubic } else { effectcraft_raster::Sampling::Bilinear };
|
||||
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() });
|
||||
effectcraft_raster::composite_warp(&mut out, &b.img, &m, &effectcraft_raster::WarpOpts { opacity, sampling, ..Default::default() });
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
|
|
@ -255,6 +330,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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)),
|
||||
p("taperRadius", "Taper Radius", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("pinAllEdges", "Pin All Edges", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
bulge,
|
||||
),
|
||||
|
|
@ -262,11 +339,12 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.distort.wavewarp",
|
||||
"Wave Warp",
|
||||
vec![
|
||||
p("waveType", "Wave Type", Value::Enum(0), popup(&["Sine", "Square", "Triangle", "Sawtooth"])),
|
||||
p("waveType", "Wave Type", Value::Enum(0), popup(&WAVE_TYPES)),
|
||||
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("pinning", "Pinning", Value::Enum(0), popup(&WAVE_PINNING)),
|
||||
p("phase", "Phase", num(0.0), ParamUi::Angle),
|
||||
],
|
||||
wave_warp,
|
||||
|
|
@ -277,9 +355,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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("conversion", "Type of Conversion", Value::Enum(0), popup(&["Asymmetric", "Symmetric"])),
|
||||
p("speed", "Wave Speed", num(1.0), slider(-15.0, 15.0, -5.0, 5.0, 1)),
|
||||
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,
|
||||
|
|
@ -318,6 +397,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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)),
|
||||
p("sampling", "Sampling", Value::Enum(0), popup(&["Bilinear", "Bicubic"])),
|
||||
],
|
||||
transform,
|
||||
),
|
||||
|
|
@ -334,3 +414,100 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Params;
|
||||
|
||||
fn img(w: u32, h: u32) -> Image {
|
||||
crate::util::gen_image(w, h, |x, y| [x as f32 / w as f32, y as f32 / h as f32, ((x * 7 + y * 3) % 11) as f32 / 11.0, 1.0])
|
||||
}
|
||||
|
||||
/// Run with Point defaults scaled to the layer (like `instantiate`).
|
||||
fn run(id: &str, set: &[(&str, Value)], im: Image) -> Buf {
|
||||
let s = crate::find(id).unwrap();
|
||||
let ls = [im.width as f64, im.height as f64];
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), crate::default_value(p, ls))).collect() };
|
||||
for (k, v) in set {
|
||||
assert!(params.values.contains_key(*k), "{id}: unknown param {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: ls, seed: 3, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img: im, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
fn differs(a: Px4, b: Px4) -> bool {
|
||||
(0..4).any(|i| (a[i] - b[i]).abs() > 1e-4)
|
||||
}
|
||||
type Px4 = [f32; 4];
|
||||
|
||||
#[test]
|
||||
fn twirl_radius_is_a_percentage_of_the_larger_dimension() {
|
||||
// 60×20 layer, radius 50 → 30 px: a pixel 20 px right of the centre is twirled (the old
|
||||
// smaller-dimension rule gave 5 px), corners (> 30 px away) are not.
|
||||
let im = img(60, 20);
|
||||
let o = run("ec.distort.twirl", &[("angle", num(90.0)), ("radius", num(50.0))], im.clone());
|
||||
assert!(differs(o.img.get(50, 10), im.get(50, 10)));
|
||||
assert_eq!(o.img.get(0, 0), im.get(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ripple_radius_reaches_the_edge_at_100_and_conversion_matters() {
|
||||
let im = img(40, 40);
|
||||
let base = [("radius", num(100.0)), ("waveHeight", num(4.0)), ("waveWidth", num(7.0))];
|
||||
let asym = run("ec.distort.ripple", &base, im.clone());
|
||||
let mut sym_set = base.to_vec();
|
||||
sym_set.push(("conversion", Value::Enum(1)));
|
||||
let sym = run("ec.distort.ripple", &sym_set, im.clone());
|
||||
assert!(asym.img.data.iter().zip(&sym.img.data).any(|(a, b)| differs(*a, *b)));
|
||||
// Corners lie beyond radius 100 % (half the layer) and stay put.
|
||||
assert_eq!(asym.img.get(0, 0), im.get(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulge_taper_and_pin_all_edges() {
|
||||
let im = img(40, 40);
|
||||
let big = [("hradius", num(40.0)), ("vradius", num(40.0)), ("height", num(2.0))];
|
||||
let free = run("ec.distort.bulge", &big, im.clone());
|
||||
let mut pinned_set = big.to_vec();
|
||||
pinned_set.push(("pinAllEdges", Value::Bool(true)));
|
||||
let pinned = run("ec.distort.bulge", &pinned_set, im.clone());
|
||||
assert!(differs(free.img.get(20, 0), im.get(20, 0)));
|
||||
assert!(!differs(pinned.img.get(20, 0), im.get(20, 0)));
|
||||
assert!(!differs(pinned.img.get(0, 20), im.get(0, 20)));
|
||||
let mut taper_set = big.to_vec();
|
||||
taper_set.push(("taperRadius", num(100.0)));
|
||||
let tapered = run("ec.distort.bulge", &taper_set, im.clone());
|
||||
assert!(differs(tapered.img.get(30, 20), free.img.get(30, 20)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wave_warp_shapes_stay_in_range_and_pinning_holds_the_edge() {
|
||||
for k in 0..WAVE_TYPES.len() as u32 {
|
||||
for i in 0..200 {
|
||||
let v = wave_shape(k, i as f64 * 0.137 - 5.0, 9);
|
||||
assert!((-1.0001..=1.0001).contains(&v), "{} {v}", WAVE_TYPES[k as usize]);
|
||||
}
|
||||
}
|
||||
let im = img(40, 30);
|
||||
// Direction 0° → horizontal displacement; Left Edge pinning keeps the left column.
|
||||
let set = [("direction", num(0.0)), ("height", num(5.0)), ("pinning", Value::Enum(3))];
|
||||
let o = run("ec.distort.wavewarp", &set, im.clone());
|
||||
let pad = ((o.img.width - im.width) / 2) as i64;
|
||||
for y in 0..30 {
|
||||
let (a, b) = (o.img.get(pad, y + pad), im.get(0, y));
|
||||
assert!((0..4).all(|i| (a[i] - b[i]).abs() < 0.01), "row {y}: {a:?} vs {b:?}");
|
||||
}
|
||||
let free = run("ec.distort.wavewarp", &[("direction", num(0.0)), ("height", num(5.0))], im);
|
||||
assert!(free.img.data.iter().zip(&o.img.data).any(|(a, b)| differs(*a, *b)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_sampling_bicubic_differs_from_bilinear() {
|
||||
let im = img(32, 32);
|
||||
let a = run("ec.distort.transform", &[("rotation", num(17.0))], im.clone());
|
||||
let b = run("ec.distort.transform", &[("rotation", num(17.0)), ("sampling", Value::Enum(1))], im);
|
||||
assert!(a.img.data.iter().zip(&b.img.data).any(|(p, q)| differs(*p, *q)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@
|
|||
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
|
||||
use effectcraft_color::{BlendMode, blend_pixel};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::generate::value_noise;
|
||||
use crate::util::{SRC_NAMES, layer_rect, lerp4, map_xy, pick, premul, remap, smoothstep, src_at, unpremul};
|
||||
use crate::util::{SRC_NAMES, Src, fit_layer, layer_rect, lerp4, map_xy, pick, premul, remap, smoothstep, src_at, unpremul};
|
||||
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 {
|
||||
|
|
@ -50,46 +51,81 @@ fn fbm(u: f64, v: f64, z: f64, seed: u32, octaves: f64, falloff: f32) -> f32 {
|
|||
sum / norm.max(1e-6)
|
||||
}
|
||||
|
||||
const TURBULENT_KINDS: [&str; 9] = [
|
||||
"Turbulent",
|
||||
"Bulge",
|
||||
"Twist",
|
||||
"Turbulent Smoother",
|
||||
"Bulge Smoother",
|
||||
"Twist Smoother",
|
||||
"Vertical Displacement",
|
||||
"Horizontal Displacement",
|
||||
"Cross Displacement",
|
||||
];
|
||||
const TURBULENT_PINNING: [&str; 8] = ["None", "Pin All", "Pin Horizontal", "Pin Vertical", "Pin Left", "Pin Top", "Pin Right", "Pin Bottom"];
|
||||
|
||||
fn turbulent_displace(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let amount = ctx.params.f("amount") * b.scale;
|
||||
if amount.abs() < 1e-9 {
|
||||
return b;
|
||||
}
|
||||
// TURBULENT_KINDS order: the "Smoother" variants (3, 4, 5) use a gentler octave falloff.
|
||||
let kind = ctx.params.e("displacement");
|
||||
let size = (ctx.params.f("size") * b.scale).max(1.0);
|
||||
if ctx.params.b("resizeLayer") && !ctx.adjustment {
|
||||
b.pad(amount.abs().ceil() as u32 + 1);
|
||||
}
|
||||
let off = b.to_px(ctx.params.v2("offset"));
|
||||
let oct = ctx.params.f("complexity").clamp(1.0, 10.0);
|
||||
let evo = ctx.params.f("evolution") / 360.0;
|
||||
// Evolution Options: Cycle Evolution loops the noise every `cycle` revolutions.
|
||||
let cycle = (ctx.params.b("evolutionOptions/cycleEvolution")).then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0));
|
||||
let pin = ctx.params.e("pinning");
|
||||
let falloff = if kind == 3 { 0.3 } else { 0.5 };
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
let edge = (w.min(h) * 0.1).max(1.0);
|
||||
let seed = ctx.seed ^ 0x7d15;
|
||||
let n = |u: f64, v: f64, s: u32| fbm(u, v, evo, seed.wrapping_add(s), oct, falloff) as f64 - 0.5;
|
||||
let falloff = if (3..=5).contains(&kind) { 0.3 } else { 0.5 };
|
||||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||||
let edge = (lw.min(lh) * 0.1).max(1.0);
|
||||
let seed = (ctx.seed ^ 0x7d15).wrapping_add((ctx.params.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(0x9e37_79b9));
|
||||
let n = |u: f64, v: f64, s: u32| {
|
||||
let s = seed.wrapping_add(s);
|
||||
match cycle {
|
||||
Some(c) => {
|
||||
// Cross-fade the noise at z and z - c so z = 0 and z = c match.
|
||||
let z = evo.rem_euclid(c);
|
||||
let t = z / c;
|
||||
(fbm(u, v, z, s, oct, falloff) as f64 * (1.0 - t) + fbm(u, v, z - c, s, oct, falloff) as f64 * t) - 0.5
|
||||
}
|
||||
None => fbm(u, v, evo, s, oct, falloff) as f64 - 0.5,
|
||||
}
|
||||
};
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let (u, v) = ((x - off.0) / size, (y - off.1) / size);
|
||||
let (dx, dy) = match kind {
|
||||
1 | 2 => {
|
||||
1 | 2 | 4 | 5 => {
|
||||
let e = 0.05;
|
||||
let gx = (n(u + e, v, 0) - n(u - e, v, 0)) / (2.0 * e);
|
||||
let gy = (n(u, v + e, 0) - n(u, v - e, 0)) / (2.0 * e);
|
||||
let k = amount * 0.25;
|
||||
if kind == 1 { (gx * k, gy * k) } else { (-gy * k, gx * k) }
|
||||
if kind == 1 || kind == 4 { (gx * k, gy * k) } else { (-gy * k, gx * k) }
|
||||
}
|
||||
4 => (n(u, v, 0) * amount, 0.0),
|
||||
5 => (0.0, n(u, v, 17) * amount),
|
||||
6 => {
|
||||
6 => (0.0, n(u, v, 17) * amount),
|
||||
7 => (n(u, v, 0) * amount, 0.0),
|
||||
8 => {
|
||||
let a = n(u, v, 0) * amount;
|
||||
(a, a)
|
||||
}
|
||||
_ => (n(u, v, 0) * amount, n(u, v, 17) * amount),
|
||||
};
|
||||
let ex = (x.min(w - x) / edge).clamp(0.0, 1.0);
|
||||
let ey = (y.min(h - y) / edge).clamp(0.0, 1.0);
|
||||
// Measured from the outermost pixel centres, so pinned edges stay exactly in place.
|
||||
let (l, r) = (((x - lx - 0.5) / edge).clamp(0.0, 1.0), ((lx + lw - 0.5 - x) / edge).clamp(0.0, 1.0));
|
||||
let (t, bt) = (((y - ly - 0.5) / edge).clamp(0.0, 1.0), ((ly + lh - 0.5 - y) / edge).clamp(0.0, 1.0));
|
||||
let k = match pin {
|
||||
1 => ex.min(ey),
|
||||
2 => ey,
|
||||
3 => ex,
|
||||
1 => l.min(r).min(t).min(bt),
|
||||
2 => t.min(bt),
|
||||
3 => l.min(r),
|
||||
4 => l,
|
||||
5 => t,
|
||||
6 => r,
|
||||
7 => bt,
|
||||
_ => 1.0,
|
||||
};
|
||||
Some((x + dx * k, y + dy * k))
|
||||
|
|
@ -105,12 +141,54 @@ fn displacement_map(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let mh = ctx.params.f("maxHorizontal") * b.scale;
|
||||
let mv = ctx.params.f("maxVertical") * b.scale;
|
||||
let wrap = ctx.params.b("wrapPixelsAround");
|
||||
if ctx.params.b("expandOutput") && !wrap && !ctx.adjustment {
|
||||
b.pad(mh.abs().max(mv.abs()).ceil() as u32);
|
||||
}
|
||||
// The map: the chosen layer (Center / Stretch / Tile), else the layer itself.
|
||||
let layer = ctx.layer_param("displacementMapLayer", false);
|
||||
let behavior = ctx.params.e("displacementMapBehavior");
|
||||
let map_size = layer.as_ref().map(|o| o.size);
|
||||
let map = match layer {
|
||||
Some(o) => match behavior {
|
||||
1 => fit_layer(ctx, &b, &o, true),
|
||||
2 => {
|
||||
let (os, ls) = (o.size, ctx.layer_size);
|
||||
let (dx, dy) = ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5);
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
crate::util::gen_image(b.img.width, b.img.height, |x, y| {
|
||||
let qx = ((x as f64 + 0.5 - b.offset[0]) * inv + dx).rem_euclid(os[0].max(1.0));
|
||||
let qy = ((y as f64 + 0.5 - b.offset[1]) * inv + dy).rem_euclid(os[1].max(1.0));
|
||||
o.buf.img.sample_bilinear_clamped(qx * o.buf.scale + o.buf.offset[0], qy * o.buf.scale + o.buf.offset[1])
|
||||
})
|
||||
}
|
||||
_ => fit_layer(ctx, &b, &o, false),
|
||||
},
|
||||
None => b.img.clone(),
|
||||
};
|
||||
// Center Map: pixels outside the (smaller) map are not displaced.
|
||||
let (scale, offset) = (b.scale, b.offset);
|
||||
let outside = |x: usize, y: usize| -> bool {
|
||||
match map_size {
|
||||
Some(os) if behavior == 0 => {
|
||||
let inv = 1.0 / scale.max(1e-9);
|
||||
let ls = ctx.layer_size;
|
||||
let qx = (x as f64 + 0.5 - offset[0]) * inv + (os[0] - ls[0]) * 0.5;
|
||||
let qy = (y as f64 + 0.5 - offset[1]) * inv + (os[1] - ls[1]) * 0.5;
|
||||
!(0.0..os[0]).contains(&qx) || !(0.0..os[1]).contains(&qy)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
let src = b.img.clone();
|
||||
let (w, h) = (src.width as f64, src.height as f64);
|
||||
map_xy(&mut b.img, |x, y, px| {
|
||||
let (c, a) = unpremul(px);
|
||||
let dx = (pick(hs, c, a) as f64 - 0.5) * 2.0 * mh;
|
||||
let dy = (pick(vs, c, a) as f64 - 0.5) * 2.0 * mv;
|
||||
if outside(x, y) {
|
||||
return px;
|
||||
}
|
||||
let (c, a) = unpremul(map.get(x as i64, y as i64));
|
||||
// "Off" turns displacement in that direction off.
|
||||
let d = |s: Src, m: f64| if s == Src::Off { 0.0 } else { (pick(s, c, a) as f64 - 0.5) * 2.0 * m };
|
||||
let (dx, dy) = (d(hs, mh), d(vs, mv));
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return px;
|
||||
}
|
||||
|
|
@ -134,16 +212,28 @@ fn optics_compensation(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
return b;
|
||||
}
|
||||
let reverse = ctx.params.b("reverseLensDistortion");
|
||||
let (_, _, lw, lh) = layer_rect(ctx, &b);
|
||||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||||
let r_ref = match ctx.params.e("fovOrientation") {
|
||||
1 => lh * 0.5,
|
||||
2 => (lw * lw + lh * lh).sqrt() * 0.5,
|
||||
_ => lw * 0.5,
|
||||
}
|
||||
.max(1.0);
|
||||
let c = b.to_px(ctx.params.v2("viewCenter"));
|
||||
let half = fov * 0.5;
|
||||
let f = r_ref / half.tan();
|
||||
// Resize (with Reverse Lens Distortion): grow the layer so the stretched corners stay
|
||||
// visible, up to 2× / 4× the layer size (Unlimited is capped at 8×).
|
||||
let resize = ctx.params.e("resize");
|
||||
if reverse && resize > 0 && !ctx.adjustment {
|
||||
let c = b.to_px(ctx.params.v2("viewCenter"));
|
||||
let rc =
|
||||
[(lx, ly), (lx + lw, ly), (lx, ly + lh), (lx + lw, ly + lh)].iter().map(|p| ((p.0 - c.0).powi(2) + (p.1 - c.1).powi(2)).sqrt()).fold(0.0, f64::max);
|
||||
let th = rc / r_ref * half;
|
||||
let cap = lw.max(lh) * [0.0, 0.5, 1.5, 3.5][resize.min(3) as usize];
|
||||
let grow = if th < FRAC_PI_2 - 1e-3 { (f * th.tan() - rc).max(0.0) } else { cap };
|
||||
b.pad(grow.min(cap).min(4096.0).ceil() as u32);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("viewCenter"));
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let (dx, dy) = (x - c.0, y - c.1);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
|
|
@ -181,20 +271,89 @@ fn magnify(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
feather *= mag;
|
||||
}
|
||||
let op = (ctx.params.f("opacity") / 100.0) as f32;
|
||||
let scaling = ctx.params.e("scaling");
|
||||
// Blending Mode: index 0 is "None" (only the magnified area, transparent around it).
|
||||
let mode = ctx.params.e("blendingMode") as usize;
|
||||
let blend = MAGNIFY_MODES.get(mode.wrapping_sub(1)).copied();
|
||||
if ctx.params.b("resizeLayer") && link == 0 && !ctx.adjustment {
|
||||
// Let the magnified area extend past the layer edges.
|
||||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||||
let reach = size.max(0.0);
|
||||
let over = [lx - (c.0 - reach), (c.0 + reach) - (lx + lw), ly - (c.1 - reach), (c.1 + reach) - (ly + lh)].into_iter().fold(0.0, f64::max);
|
||||
b.pad(over.min(4096.0).ceil() as u32);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let src = b.img.clone();
|
||||
let seed = ctx.seed ^ 0x3a61;
|
||||
map_xy(&mut b.img, |x, y, px| {
|
||||
let (dx, dy) = (x as f64 + 0.5 - c.0, y as f64 + 0.5 - c.1);
|
||||
let d = if square { dx.abs().max(dy.abs()) } else { (dx * dx + dy * dy).sqrt() };
|
||||
let w = if feather > 0.01 { 1.0 - smoothstep((size - feather) as f32, size as f32, d as f32) } else { (size - d + 0.5).clamp(0.0, 1.0) as f32 } * op;
|
||||
if w <= 0.0 {
|
||||
return px;
|
||||
return if blend.is_none() { [0.0; 4] } else { px };
|
||||
}
|
||||
let (sx, sy) = (c.0 + dx / mag, c.1 + dy / mag);
|
||||
let m = match scaling {
|
||||
// Soft: smooth (bicubic spline) interpolation.
|
||||
1 => src.sample_bicubic(sx, sy),
|
||||
// Scatter: jitter each sample within the enlarged source pixel.
|
||||
2 => {
|
||||
let j = |k: u32| (crate::util::hash1(x as u32, y as u32 ^ (k << 20), seed) as f64 - 0.5) * (1.0 - 1.0 / mag).max(0.0);
|
||||
src.sample_bilinear(sx.floor() + 0.5 + j(1), sy.floor() + 0.5 + j(2))
|
||||
}
|
||||
// Standard: nearest source pixel (sharp, blocky at high magnification).
|
||||
_ => src.sample_bilinear(sx.floor() + 0.5, sy.floor() + 0.5),
|
||||
};
|
||||
match blend {
|
||||
None => m.map(|v| v * w),
|
||||
Some(BlendMode::Normal) => lerp4(px, m, w),
|
||||
Some(mode) => blend_pixel(mode, px, m.map(|v| v * w), 0.5),
|
||||
}
|
||||
let m = src.sample_bilinear(c.0 + dx / mag, c.1 + dy / mag);
|
||||
lerp4(px, m, w)
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Magnify's Blending Mode choices after "None".
|
||||
const MAGNIFY_MODES: [BlendMode; 17] = [
|
||||
BlendMode::Normal,
|
||||
BlendMode::Add,
|
||||
BlendMode::Multiply,
|
||||
BlendMode::Screen,
|
||||
BlendMode::Overlay,
|
||||
BlendMode::SoftLight,
|
||||
BlendMode::HardLight,
|
||||
BlendMode::ColorDodge,
|
||||
BlendMode::ColorBurn,
|
||||
BlendMode::Darken,
|
||||
BlendMode::Lighten,
|
||||
BlendMode::Difference,
|
||||
BlendMode::Exclusion,
|
||||
BlendMode::Hue,
|
||||
BlendMode::Saturation,
|
||||
BlendMode::Color,
|
||||
BlendMode::Luminosity,
|
||||
];
|
||||
const MAGNIFY_MODE_NAMES: [&str; 18] = [
|
||||
"None",
|
||||
"Normal",
|
||||
"Add",
|
||||
"Multiply",
|
||||
"Screen",
|
||||
"Overlay",
|
||||
"Soft Light",
|
||||
"Hard Light",
|
||||
"Color Dodge",
|
||||
"Color Burn",
|
||||
"Darken",
|
||||
"Lighten",
|
||||
"Difference",
|
||||
"Exclusion",
|
||||
"Hue",
|
||||
"Saturation",
|
||||
"Color",
|
||||
"Luminosity",
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------- grid rasteriser
|
||||
|
||||
type Pt = (f64, f64);
|
||||
|
|
@ -519,18 +678,17 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.distort.turbulentdisplace",
|
||||
"Turbulent Displace",
|
||||
vec![
|
||||
p(
|
||||
"displacement",
|
||||
"Displacement",
|
||||
Value::Enum(0),
|
||||
popup(&["Turbulent", "Bulge", "Twist", "Turbulent Smoother", "Horizontal Displacement", "Vertical Displacement", "Cross Displacement"]),
|
||||
),
|
||||
p("displacement", "Displacement", Value::Enum(0), popup(&TURBULENT_KINDS)),
|
||||
p("amount", "Amount", num(50.0), slider(-1000.0, 1000.0, -200.0, 200.0, 1)),
|
||||
p("size", "Size", num(100.0), slider(2.0, 4000.0, 2.0, 400.0, 1)),
|
||||
p("offset", "Offset (Turbulence)", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("complexity", "Complexity", num(1.0), slider(1.0, 10.0, 1.0, 10.0, 1)),
|
||||
p("evolution", "Evolution", num(0.0), ParamUi::Angle),
|
||||
p("pinning", "Pinning", Value::Enum(0), popup(&["None", "Pin All Edges", "Pin Horizontal Edges", "Pin Vertical Edges"])),
|
||||
p("evolutionOptions/cycleEvolution", "Cycle Evolution", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("evolutionOptions/cycle", "Cycle (in Revolutions)", num(1.0), slider(1.0, 1000.0, 1.0, 20.0, 0)),
|
||||
p("evolutionOptions/randomSeed", "Random Seed", num(0.0), slider(0.0, 10000.0, 0.0, 1000.0, 0)),
|
||||
p("pinning", "Pinning", Value::Enum(1), popup(&TURBULENT_PINNING)),
|
||||
p("resizeLayer", "Resize Layer", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
turbulent_displace,
|
||||
),
|
||||
|
|
@ -538,11 +696,16 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.distort.displacementmap",
|
||||
"Displacement Map",
|
||||
vec![
|
||||
p("displacementMapLayer", "Displacement Map Layer", Value::Layer(None), ParamUi::Layer),
|
||||
// The map layer is read without its masks or effects.
|
||||
p("displacementMapLayerSource", "Displacement Map Layer Source", Value::Enum(0), ParamUi::Hidden),
|
||||
p("useForHorizontal", "Use For Horizontal Displacement", Value::Enum(0), srcs()),
|
||||
p("maxHorizontal", "Max Horizontal Displacement", num(5.0), slider(-32000.0, 32000.0, -100.0, 100.0, 1)),
|
||||
p("useForVertical", "Use For Vertical Displacement", Value::Enum(1), srcs()),
|
||||
p("maxVertical", "Max Vertical Displacement", num(5.0), slider(-32000.0, 32000.0, -100.0, 100.0, 1)),
|
||||
p("displacementMapBehavior", "Displacement Map Behavior", Value::Enum(0), popup(&["Center Map", "Stretch Map to Fit", "Tile Map"])),
|
||||
p("wrapPixelsAround", "Wrap Pixels Around", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("expandOutput", "Expand Output", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
displacement_map,
|
||||
),
|
||||
|
|
@ -554,6 +717,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("reverseLensDistortion", "Reverse Lens Distortion", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("fovOrientation", "FOV Orientation", Value::Enum(0), popup(&["Horizontal", "Vertical", "Diagonal"])),
|
||||
p("viewCenter", "View Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("resize", "Resize", Value::Enum(0), popup(&["Off", "Max 2X", "Max 4X", "Unlimited"])),
|
||||
],
|
||||
optics_compensation,
|
||||
),
|
||||
|
|
@ -568,6 +732,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("size", "Size", num(100.0), slider(0.0, 4000.0, 0.0, 1000.0, 1)),
|
||||
p("feather", "Feather", num(0.0), slider(0.0, 4000.0, 0.0, 200.0, 1)),
|
||||
p("opacity", "Opacity", num(100.0), pct(100.0)),
|
||||
p("scaling", "Scaling", Value::Enum(0), popup(&["Standard", "Soft", "Scatter"])),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(1), popup(&MAGNIFY_MODE_NAMES)),
|
||||
p("resizeLayer", "Resize Layer", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
magnify,
|
||||
),
|
||||
|
|
@ -701,15 +868,20 @@ mod tests {
|
|||
#[test]
|
||||
fn displacement_map_half_is_identity() {
|
||||
let img = test_img(32, 24);
|
||||
let o = run("ec.distort.displacementmap", &[("useForHorizontal", Value::Enum(9)), ("useForVertical", Value::Enum(9))], img.clone());
|
||||
let o = run(
|
||||
"ec.distort.displacementmap",
|
||||
&[("useForHorizontal", Value::Enum(9)), ("useForVertical", Value::Enum(9)), ("expandOutput", Value::Bool(false))],
|
||||
img.clone(),
|
||||
);
|
||||
assert_eq!(o.img, img);
|
||||
let o = run(
|
||||
"ec.distort.displacementmap",
|
||||
&[("useForHorizontal", Value::Enum(8)), ("useForVertical", Value::Enum(10)), ("maxHorizontal", num(2.0))],
|
||||
&[("useForHorizontal", Value::Enum(8)), ("useForVertical", Value::Enum(10)), ("maxHorizontal", num(2.0)), ("expandOutput", Value::Bool(false))],
|
||||
img.clone(),
|
||||
);
|
||||
// Full → +2 px horizontally: pixel x shows source x + 2.
|
||||
assert!((o.img.get(5, 5)[0] - img.get(7, 5)[0]).abs() < 1e-5);
|
||||
// Full → +2 px horizontally: pixel x shows source x + 2; Off → no vertical displacement.
|
||||
let (a, b2) = (o.img.get(5, 5), img.get(7, 5));
|
||||
assert!((0..4).all(|i| (a[i] - b2[i]).abs() < 1e-5), "{a:?} vs {b2:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -802,4 +974,107 @@ mod tests {
|
|||
assert!(o.img.get(0, 0)[3] < 1e-6);
|
||||
assert!(o.img.get(16, 16)[3] > 0.99);
|
||||
}
|
||||
|
||||
/// A host whose every layer is a flat 8×8 image of `px`.
|
||||
struct FlatHost([f32; 4]);
|
||||
impl crate::EffectHost for FlatHost {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
Some(crate::LayerPixels { buf: Buf { img: Image::filled(8, 8, self.0), offset: [0.0; 2], scale: 1.0 }, size: [8.0, 8.0] })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn displacement_map_reads_the_map_layer_and_expands_output() {
|
||||
let img = test_img(32, 24);
|
||||
// A white map layer → Red = 1 → full positive horizontal displacement everywhere.
|
||||
let host = FlatHost([1.0, 1.0, 1.0, 1.0]);
|
||||
let s = crate::find("ec.distort.displacementmap").unwrap();
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
for (k, v) in [
|
||||
("displacementMapLayer", Value::Layer(Some(7))),
|
||||
("useForVertical", Value::Enum(10)),
|
||||
("maxHorizontal", num(3.0)),
|
||||
("expandOutput", Value::Bool(false)),
|
||||
] {
|
||||
params.values.insert(k.into(), v);
|
||||
}
|
||||
for behavior in 0..3 {
|
||||
params.values.insert("displacementMapBehavior".into(), Value::Enum(behavior));
|
||||
let env = crate::EffectEnv { host: Some(&host), ..Default::default() };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [32.0, 24.0], seed: 1, adjustment: false, env };
|
||||
let o = crate::apply(s, &ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 });
|
||||
if behavior == 0 {
|
||||
// Center Map: the 8×8 map covers only the middle; outside it nothing moves.
|
||||
assert_eq!(o.img.get(2, 2), img.get(2, 2));
|
||||
assert!((o.img.get(16, 12)[0] - img.get(19, 12)[0]).abs() < 1e-5);
|
||||
} else {
|
||||
// Stretched or tiled, the map covers the whole layer.
|
||||
assert!(
|
||||
(o.img.get(2, 2)[0] - img.get(5, 2)[0]).abs() < 1e-5,
|
||||
"behavior {behavior}: {:?} {:?} {:?}",
|
||||
o.img.get(2, 2),
|
||||
img.get(5, 2),
|
||||
img.get(2, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
// Expand Output grows the layer by the maximum displacement.
|
||||
let o = run("ec.distort.displacementmap", &[("maxHorizontal", num(6.0))], img.clone());
|
||||
assert_eq!((o.img.width, o.img.height), (32 + 12, 24 + 12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turbulent_displace_pinning_and_cycle_evolution() {
|
||||
let img = test_img(40, 40);
|
||||
let pinned = run("ec.distort.turbulentdisplace", &[("size", num(10.0))], img.clone());
|
||||
let free = run("ec.distort.turbulentdisplace", &[("size", num(10.0)), ("pinning", Value::Enum(0))], img.clone());
|
||||
// Pin All (the default) holds the border.
|
||||
for i in 0..40 {
|
||||
assert_eq!(pinned.img.get(i, 0), img.get(i, 0));
|
||||
assert_eq!(pinned.img.get(0, i), img.get(0, i));
|
||||
}
|
||||
assert!(max_diff(&pinned.img, &free.img) > 1e-3);
|
||||
// Cycle Evolution: evolution 0 and `cycle` revolutions render the same frame.
|
||||
let cyc = |evo: f64| {
|
||||
run(
|
||||
"ec.distort.turbulentdisplace",
|
||||
&[("evolution", num(evo)), ("evolutionOptions/cycleEvolution", Value::Bool(true)), ("evolutionOptions/cycle", num(2.0))],
|
||||
img.clone(),
|
||||
)
|
||||
.img
|
||||
};
|
||||
assert!(max_diff(&cyc(0.0), &cyc(720.0)) < 1e-4);
|
||||
assert!(max_diff(&cyc(0.0), &cyc(200.0)) > 1e-3);
|
||||
// Random Seed changes the pattern.
|
||||
let seeded = run("ec.distort.turbulentdisplace", &[("size", num(10.0)), ("evolutionOptions/randomSeed", num(5.0))], img);
|
||||
assert!(max_diff(&seeded.img, &pinned.img) > 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magnify_blending_none_and_scaling() {
|
||||
let img = test_img(32, 32);
|
||||
let none = run("ec.distort.magnify", &[("size", num(6.0)), ("blendingMode", Value::Enum(0))], img.clone());
|
||||
assert!(none.img.get(0, 0)[3] < 1e-6);
|
||||
assert!(none.img.get(16, 16)[3] > 0.99);
|
||||
let std = run("ec.distort.magnify", &[("magnification", num(400.0))], img.clone());
|
||||
let soft = run("ec.distort.magnify", &[("magnification", num(400.0)), ("scaling", Value::Enum(1))], img.clone());
|
||||
assert!(max_diff(&std.img, &soft.img) > 1e-3);
|
||||
let add = run("ec.distort.magnify", &[("blendingMode", Value::Enum(2))], img);
|
||||
assert!(max_diff(&add.img, &std.img) > 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optics_resize_grows_reversed_layers() {
|
||||
let img = test_img(32, 24);
|
||||
let set = |r: u32| vec![("fieldOfView", num(120.0)), ("reverseLensDistortion", Value::Bool(true)), ("resize", Value::Enum(r))];
|
||||
let off = run("ec.distort.opticscompensation", &set(0), img.clone());
|
||||
assert_eq!(off.img.width, 32);
|
||||
let max2 = run("ec.distort.opticscompensation", &set(1), img.clone());
|
||||
assert!(max2.img.width > 32 && max2.img.width <= 32 + 2 * 16 + 2, "{}", max2.img.width);
|
||||
let max4 = run("ec.distort.opticscompensation", &set(2), img);
|
||||
assert!(max4.img.width >= max2.img.width);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ fn centroid(pts: &[[f64; 2]]) -> [f64; 2] {
|
|||
}
|
||||
|
||||
/// Elasticity popup (Stiff … Super Fluid) → inverse-distance weighting exponent.
|
||||
const ELASTICITY: [&str; 8] = ["Stiff", "Less Stiff", "Below Normal", "Normal", "Above Normal", "Loose", "Liquid", "Super Fluid"];
|
||||
const ELASTICITY: [&str; 8] = ["Stiff", "Less Stiff", "Below Normal", "Absolutely Normal", "Above Average", "Loose", "Liquid", "Super Fluid"];
|
||||
fn elastic_exp(i: u32) -> f64 {
|
||||
[1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 3.5, 4.5][(i as usize).min(7)]
|
||||
}
|
||||
|
|
@ -652,6 +652,17 @@ fn upscale(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let noise = ctx.params.f("reduceNoise") / 100.0;
|
||||
let src = if noise > 0.0 { effectcraft_raster::gaussian_blur(&b.img, noise * 1.5, noise * 1.5, true) } else { b.img.clone() };
|
||||
let lc = [ctx.layer_size[0] * 0.5, ctx.layer_size[1] * 0.5];
|
||||
// Alpha: Bilinear (index 0) resamples the alpha channel bilinearly, Bicubic like the colour.
|
||||
let bilinear_alpha = ctx.params.e("alpha") == 0;
|
||||
let up = |img: &Image, x: f64, y: f64| -> Px {
|
||||
let p = bicubic(img, x, y);
|
||||
if !bilinear_alpha {
|
||||
return p;
|
||||
}
|
||||
let a = img.sample_bilinear_clamped(x, y)[3];
|
||||
let k = if p[3] > 1e-6 { a / p[3] } else { 0.0 };
|
||||
[p[0] * k, p[1] * k, p[2] * k, a]
|
||||
};
|
||||
let mut out = if ctx.adjustment {
|
||||
// Scale in place about the layer centre.
|
||||
let c = b.to_px(lc);
|
||||
|
|
@ -660,7 +671,7 @@ fn upscale(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let sx = c.0 + (x as f64 + 0.5 - c.0) / k;
|
||||
let sy = c.1 + (y as f64 + 0.5 - c.1) / k;
|
||||
*px = if sx < 0.0 || sy < 0.0 || sx > src.width as f64 || sy > src.height as f64 { [0.0; 4] } else { bicubic(&src, sx, sy) };
|
||||
*px = if sx < 0.0 || sy < 0.0 || sx > src.width as f64 || sy > src.height as f64 { [0.0; 4] } else { up(&src, sx, sy) };
|
||||
}
|
||||
});
|
||||
o
|
||||
|
|
@ -671,7 +682,7 @@ fn upscale(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let mut o = Image::new(w, h);
|
||||
o.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
*px = bicubic(&src, (x as f64 + 0.5) / kx, (y as f64 + 0.5) / ky);
|
||||
*px = up(&src, (x as f64 + 0.5) / kx, (y as f64 + 0.5) / ky);
|
||||
}
|
||||
});
|
||||
let c_sc = [lc[0] * b.scale, lc[1] * b.scale];
|
||||
|
|
@ -814,7 +825,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("maskScale", "Mask Scale", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)),
|
||||
p("percent", "Percent", num(100.0), pct()),
|
||||
p("elasticity", "Elasticity", Value::Enum(3), popup(&ELASTICITY)),
|
||||
p("interpolation", "Interpolation", Value::Enum(1), popup(&["Discrete", "Linear", "Smooth"])),
|
||||
p("interpolation", "Interpolation Method", Value::Enum(1), popup(&["Discrete", "Linear", "Smooth"])),
|
||||
],
|
||||
smear,
|
||||
),
|
||||
|
|
@ -827,7 +838,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("boundaryMask", "Boundary Mask", Value::Enum(0), popup(&MASK_CHOICES)),
|
||||
p("percent", "Percent", num(100.0), pct()),
|
||||
p("elasticity", "Elasticity", Value::Enum(0), popup(&ELASTICITY)),
|
||||
p("interpolation", "Interpolation", Value::Enum(1), popup(&["Discrete", "Linear", "Smooth"])),
|
||||
p("interpolation", "Interpolation Method", Value::Enum(1), popup(&["Discrete", "Linear", "Smooth"])),
|
||||
],
|
||||
reshape,
|
||||
),
|
||||
|
|
@ -858,8 +869,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Detail-preserving Upscale",
|
||||
vec![
|
||||
p("scale", "Scale", num(100.0), slider(100.0, 1000.0, 100.0, 400.0, 1)),
|
||||
p("detail", "Detail", num(20.0), pct()),
|
||||
p("reduceNoise", "Reduce Noise", num(0.0), pct()),
|
||||
p("detail", "Detail", num(20.0), pct()),
|
||||
p("alpha", "Alpha", Value::Enum(1), popup(&["Bilinear", "Bicubic"])),
|
||||
],
|
||||
upscale,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -107,6 +107,22 @@ pub const LIQUIFY_TOOLS: [&str; 12] = [
|
|||
"thaw",
|
||||
];
|
||||
|
||||
/// Display labels of [`LIQUIFY_TOOLS`] (the Tool popup), in the same order.
|
||||
pub const LIQUIFY_TOOL_LABELS: [&str; 12] = [
|
||||
"Warp",
|
||||
"Turbulence",
|
||||
"Twirl Clockwise",
|
||||
"Twirl Counterclockwise",
|
||||
"Pucker",
|
||||
"Bloat",
|
||||
"Shift Pixels",
|
||||
"Reflection",
|
||||
"Clone",
|
||||
"Reconstruction",
|
||||
"Freeze",
|
||||
"Thaw",
|
||||
];
|
||||
|
||||
/// One recorded Liquify stroke.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LiquifyStroke {
|
||||
|
|
@ -503,8 +519,8 @@ fn rolling_shutter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if prev.is_none() && next.is_none() {
|
||||
return b;
|
||||
}
|
||||
let per_row = ctx.params.e("method") == 0;
|
||||
let Some(flow) = measure(&b.img, prev.as_ref(), next.as_ref(), 1.0, ctx.params.b("detailedAnalysis"), per_row) else { return b };
|
||||
let per_row = ctx.params.e("advanced/method") == 0;
|
||||
let Some(flow) = measure(&b.img, prev.as_ref(), next.as_ref(), 1.0, ctx.params.b("advanced/detailedAnalysis"), per_row) else { return b };
|
||||
let dir = ctx.params.e("scanDirection");
|
||||
let src = b.img.clone();
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
|
|
@ -545,7 +561,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.distort.liquify",
|
||||
"Liquify",
|
||||
vec![
|
||||
p("tool", "Tool", Value::Enum(0), popup(&LIQUIFY_TOOLS)),
|
||||
p("tool", "Tool", Value::Enum(0), popup(&LIQUIFY_TOOL_LABELS)),
|
||||
p("brushSize", "Brush Size", num(64.0), slider(1.0, 600.0, 1.0, 600.0, 0)),
|
||||
p("brushPressure", "Brush Pressure", num(50.0), slider(1.0, 100.0, 1.0, 100.0, 0)),
|
||||
p("freezeAreaMask", "Freeze Area Mask", num(0.0), ParamUi::Mask),
|
||||
|
|
@ -570,9 +586,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("rollingShutterRate", "Rolling Shutter Rate", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("scanDirection", "Scan Direction", Value::Enum(0), popup(&["Top → Bottom", "Bottom → Top", "Left → Right", "Right → Left"])),
|
||||
p("method", "Method", Value::Enum(0), popup(&["Warp", "Pixel Motion"])),
|
||||
p("detailedAnalysis", "Detailed Analysis", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("pixelMotionDetail", "Pixel Motion Detail", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("advanced/method", "Method", Value::Enum(0), popup(&["Warp", "Pixel Motion"])),
|
||||
p("advanced/detailedAnalysis", "Detailed Analysis", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("advanced/pixelMotionDetail", "Pixel Motion Detail", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
rolling_shutter,
|
||||
),
|
||||
|
|
@ -707,10 +723,21 @@ mod tests {
|
|||
s / n
|
||||
};
|
||||
for method in [0u32, 1] {
|
||||
let out = run_fx("ec.distort.rollingshutterrepair", &[("rollingShutterRate", num(100.0)), ("method", Value::Enum(method))], skewed.clone(), t, env);
|
||||
let out = run_fx(
|
||||
"ec.distort.rollingshutterrepair",
|
||||
&[("rollingShutterRate", num(100.0)), ("advanced/method", Value::Enum(method))],
|
||||
skewed.clone(),
|
||||
t,
|
||||
env,
|
||||
);
|
||||
assert!(err(&out.img) < err(&skewed) * 0.4, "method {method}: {} vs {}", err(&out.img), err(&skewed));
|
||||
let again =
|
||||
run_fx("ec.distort.rollingshutterrepair", &[("rollingShutterRate", num(100.0)), ("method", Value::Enum(method))], skewed.clone(), t, env);
|
||||
let again = run_fx(
|
||||
"ec.distort.rollingshutterrepair",
|
||||
&[("rollingShutterRate", num(100.0)), ("advanced/method", Value::Enum(method))],
|
||||
skewed.clone(),
|
||||
t,
|
||||
env,
|
||||
);
|
||||
assert_eq!(out.img.data, again.img.data);
|
||||
}
|
||||
let none = run_fx("ec.distort.rollingshutterrepair", &[], skewed.clone(), t, EffectEnv::default());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Generate and Noise effects that synthesise images.
|
||||
|
||||
use effectcraft_color::{BlendMode, blend_pixel};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::Px;
|
||||
|
|
@ -7,6 +8,9 @@ use rayon::prelude::*;
|
|||
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
/// Checkerboard / Grid "Size From" options.
|
||||
pub(crate) const SIZE_FROM: [&str; 3] = ["Corner Point", "Width Slider", "Width & Height Sliders"];
|
||||
|
||||
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 }
|
||||
}
|
||||
|
|
@ -46,10 +50,74 @@ fn gradient_ramp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Blending Mode options of the shape generators (Checkerboard, Grid, Circle): "None" shows the
|
||||
/// generated pattern only; the others composite it onto the layer like layer blending modes.
|
||||
pub(crate) const GEN_BLEND_MODES: [&str; 20] = [
|
||||
"None",
|
||||
"Normal",
|
||||
"Add",
|
||||
"Multiply",
|
||||
"Screen",
|
||||
"Overlay",
|
||||
"Soft Light",
|
||||
"Hard Light",
|
||||
"Color Dodge",
|
||||
"Color Burn",
|
||||
"Darken",
|
||||
"Lighten",
|
||||
"Difference",
|
||||
"Exclusion",
|
||||
"Hue",
|
||||
"Saturation",
|
||||
"Color",
|
||||
"Luminosity",
|
||||
"Stencil Alpha",
|
||||
"Silhouette Alpha",
|
||||
];
|
||||
|
||||
fn gen_mode(i: u32) -> Option<BlendMode> {
|
||||
Some(match i {
|
||||
0 => return None,
|
||||
2 => BlendMode::Add,
|
||||
3 => BlendMode::Multiply,
|
||||
4 => BlendMode::Screen,
|
||||
5 => BlendMode::Overlay,
|
||||
6 => BlendMode::SoftLight,
|
||||
7 => BlendMode::HardLight,
|
||||
8 => BlendMode::ColorDodge,
|
||||
9 => BlendMode::ColorBurn,
|
||||
10 => BlendMode::Darken,
|
||||
11 => BlendMode::Lighten,
|
||||
12 => BlendMode::Difference,
|
||||
13 => BlendMode::Exclusion,
|
||||
14 => BlendMode::Hue,
|
||||
15 => BlendMode::Saturation,
|
||||
16 => BlendMode::Color,
|
||||
17 => BlendMode::Luminosity,
|
||||
18 => BlendMode::StencilAlpha,
|
||||
19 => BlendMode::SilhouetteAlpha,
|
||||
_ => BlendMode::Normal,
|
||||
})
|
||||
}
|
||||
|
||||
/// Composite a generated premultiplied pixel `g` onto the original `o` with blending mode
|
||||
/// index `mode` (into [`GEN_BLEND_MODES`]).
|
||||
#[inline]
|
||||
pub(crate) fn gen_blend(o: Px, g: Px, mode: u32) -> Px {
|
||||
match gen_mode(mode) {
|
||||
None => g,
|
||||
Some(m) => blend_pixel(m, o, g, 0.5),
|
||||
}
|
||||
}
|
||||
|
||||
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 pts: Vec<((f64, f64), [f32; 4])> =
|
||||
(1..=4).map(|i| (b.to_px(ctx.params.v2(&format!("positionsColors/point{i}"))), ctx.params.color(&format!("positionsColors/color{i}")))).collect();
|
||||
let blend = ctx.params.f("blend").max(1.0);
|
||||
let jitter = (ctx.params.f("jitter") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let op = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let mode = ctx.params.e("blendingMode");
|
||||
let seed = ctx.seed;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let mut acc = [0.0f64; 3];
|
||||
|
|
@ -62,26 +130,55 @@ fn four_color(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
wsum += w;
|
||||
}
|
||||
let c = acc.map(|v| (v / wsum) as f32);
|
||||
put(px, c, 1.0, 1.0 - op);
|
||||
let mut c = acc.map(|v| (v / wsum) as f32);
|
||||
if jitter > 0.0 {
|
||||
// Dither against banding: a few code values of noise.
|
||||
let n = (effectcraft_raster::hash_noise(x as u32, y as u32, seed) - 0.5) * jitter * (8.0 / 255.0);
|
||||
c = c.map(|v| v + n);
|
||||
}
|
||||
*px = gen_blend(*px, [c[0] * op, c[1] * op, c[2] * op, op], mode);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Cell size of Checkerboard / Grid from Size From (Corner Point, Width Slider, Width &
|
||||
/// Height Sliders), in pixels.
|
||||
fn cell_size(ctx: &EffectCtx, b: &Buf, anchor: (f64, f64)) -> (f64, f64) {
|
||||
let (w, h) = match ctx.params.e("sizeFrom") {
|
||||
0 => {
|
||||
let c = b.to_px(ctx.params.v2("corner"));
|
||||
((c.0 - anchor.0).abs(), (c.1 - anchor.1).abs())
|
||||
}
|
||||
1 => (ctx.params.f("width") * b.scale, ctx.params.f("width") * b.scale),
|
||||
_ => (ctx.params.f("width") * b.scale, ctx.params.f("height") * b.scale),
|
||||
};
|
||||
(w.max(1.0), h.max(1.0))
|
||||
}
|
||||
|
||||
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 (w, h) = cell_size(ctx, &b, anchor);
|
||||
// Feather softens the transitions; 0 keeps a half-pixel antialiased edge.
|
||||
let fw = (ctx.params.f("feather/featherWidth") * b.scale).max(1.0);
|
||||
let fh = (ctx.params.f("feather/featherHeight") * b.scale).max(1.0);
|
||||
let c = ctx.params.color("color");
|
||||
let op = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let mode = ctx.params.e("blendingMode");
|
||||
// Signed distance (in feather units) to the nearest cell edge, positive in even cells.
|
||||
let axis = |p: f64, size: f64, f: f64| -> f64 {
|
||||
let u = p / size;
|
||||
let fr = u - u.floor();
|
||||
let d = fr.min(1.0 - fr) * size;
|
||||
let s = if (u.floor() as i64).rem_euclid(2) == 0 { 1.0 } else { -1.0 };
|
||||
s * (d / (f * 0.5)).min(1.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);
|
||||
}
|
||||
let sx = axis(x as f64 + 0.5 - anchor.0, w, fw);
|
||||
let sy = axis(y as f64 + 0.5 - anchor.1, h, fh);
|
||||
let cov = ((0.5 + 0.5 * sx * sy) as f32).clamp(0.0, 1.0) * op;
|
||||
*px = gen_blend(*px, [c[0] * cov, c[1] * cov, c[2] * cov, cov], mode);
|
||||
}
|
||||
});
|
||||
b
|
||||
|
|
@ -89,50 +186,74 @@ fn checkerboard(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
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 (w, h) = cell_size(ctx, &b, anchor);
|
||||
let border = (ctx.params.f("border") * b.scale).max(0.0);
|
||||
let fw = (ctx.params.f("feather/featherWidth") * b.scale).max(1.0);
|
||||
let fh = (ctx.params.f("feather/featherHeight") * b.scale).max(1.0);
|
||||
let invert = ctx.params.b("invertGrid");
|
||||
let c = ctx.params.color("color");
|
||||
let op = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let mode = ctx.params.e("blendingMode");
|
||||
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;
|
||||
}
|
||||
let gx = (x as f64 + 0.5 - anchor.0).rem_euclid(w);
|
||||
let gy = (y as f64 + 0.5 - anchor.1).rem_euclid(h);
|
||||
let (dx, dy) = (gx.min(w - gx), gy.min(h - gy));
|
||||
let cx = ((border / 2.0 - dx) / fw + 0.5).clamp(0.0, 1.0);
|
||||
let cy = ((border / 2.0 - dy) / fh + 0.5).clamp(0.0, 1.0);
|
||||
let mut cov = if border <= 0.0 { 0.0 } else { cx.max(cy) as f32 };
|
||||
if invert {
|
||||
cov = 1.0 - cov;
|
||||
}
|
||||
let a = cov * op;
|
||||
*px = gen_blend(*px, [c[0] * a, c[1] * a, c[2] * a, a], mode);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Circle Edge options.
|
||||
pub(crate) const CIRCLE_EDGES: [&str; 5] = ["None", "Edge Radius", "Thickness", "Thickness * Radius", "Thickness & Feather * Radius"];
|
||||
|
||||
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 thick = ctx.params.f("thickness").max(0.0);
|
||||
let mut f_out = ctx.params.f("feather/feather").max(0.0) * b.scale;
|
||||
let mut f_in = ctx.params.f("feather/featherInner").max(0.0) * b.scale;
|
||||
// Inner radius of the ring (None = solid disk).
|
||||
let inner = match ctx.params.e("edge") {
|
||||
1 => Some(ctx.params.f("edgeRadius") * b.scale),
|
||||
2 => Some(r - thick * b.scale),
|
||||
3 => Some(r - thick / 100.0 * r),
|
||||
4 => {
|
||||
f_out = ctx.params.f("feather/feather").max(0.0) / 100.0 * r;
|
||||
f_in = ctx.params.f("feather/featherInner").max(0.0) / 100.0 * r;
|
||||
Some(r - thick / 100.0 * r)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (ro, ri) = match inner {
|
||||
Some(i) => (r.max(i), Some(r.min(i).max(0.0))),
|
||||
None => (r, None),
|
||||
};
|
||||
let (f_out, f_in) = (f_out.max(0.5), f_in.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");
|
||||
let mode = ctx.params.e("blendingMode");
|
||||
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;
|
||||
let mut cov = ((ro - d) / f_out + 0.5).clamp(0.0, 1.0) as f32;
|
||||
if let Some(ri) = ri {
|
||||
cov *= ((d - ri) / f_in + 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;
|
||||
}
|
||||
}
|
||||
let a = cov * op;
|
||||
*px = gen_blend(*px, [color[0] * a, color[1] * a, color[2] * a, a], mode);
|
||||
}
|
||||
});
|
||||
b
|
||||
|
|
@ -169,52 +290,7 @@ pub fn value_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 {
|
|||
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
|
||||
}
|
||||
// Fractal Noise lives in `noise3` (with Turbulent Noise).
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let pt = |x, y| Value::Vec2([x, y]);
|
||||
|
|
@ -239,16 +315,18 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"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("positionsColors/point1", "Point 1", pt(0.1, 0.1), ParamUi::Point),
|
||||
p("positionsColors/color1", "Color 1", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("positionsColors/point2", "Point 2", pt(0.9, 0.1), ParamUi::Point),
|
||||
p("positionsColors/color2", "Color 2", col(0.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("positionsColors/point3", "Point 3", pt(0.1, 0.9), ParamUi::Point),
|
||||
p("positionsColors/color3", "Color 3", col(1.0, 0.0, 1.0), ParamUi::Color),
|
||||
p("positionsColors/point4", "Point 4", pt(0.9, 0.9), ParamUi::Point),
|
||||
p("positionsColors/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("jitter", "Jitter", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(0), popup(&GEN_BLEND_MODES[..18])),
|
||||
],
|
||||
four_color,
|
||||
),
|
||||
|
|
@ -258,10 +336,15 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Generate",
|
||||
vec![
|
||||
p("anchor", "Anchor", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("sizeFrom", "Size From", Value::Enum(1), popup(&SIZE_FROM)),
|
||||
p("corner", "Corner", pt(0.1, 0.1), 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("feather/featherWidth", "Width", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 1)),
|
||||
p("feather/featherHeight", "Height", num(0.0), slider(0.0, 1000.0, 0.0, 50.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)),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(0), popup(&GEN_BLEND_MODES)),
|
||||
],
|
||||
checkerboard,
|
||||
),
|
||||
|
|
@ -271,11 +354,17 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Generate",
|
||||
vec![
|
||||
p("anchor", "Anchor", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("sizeFrom", "Size From", Value::Enum(1), popup(&SIZE_FROM)),
|
||||
p("corner", "Corner", pt(0.1, 0.1), 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("feather/featherWidth", "Width", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 1)),
|
||||
p("feather/featherHeight", "Height", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 1)),
|
||||
p("invertGrid", "Invert Grid", 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)),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(0), popup(&GEN_BLEND_MODES)),
|
||||
],
|
||||
grid,
|
||||
),
|
||||
|
|
@ -286,32 +375,103 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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("edge", "Edge", Value::Enum(0), popup(&CIRCLE_EDGES)),
|
||||
p("edgeRadius", "Edge Radius", num(0.0), slider(0.0, 4000.0, 0.0, 1000.0, 1)),
|
||||
p("thickness", "Thickness", num(10.0), slider(0.0, 4000.0, 0.0, 100.0, 1)),
|
||||
p("feather/feather", "Feather Outer Edge", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("feather/featherInner", "Feather Inner 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)),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(0), popup(&GEN_BLEND_MODES)),
|
||||
],
|
||||
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,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod generator_tests {
|
||||
use super::*;
|
||||
use crate::{EffectEnv, run_fx};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
fn red() -> Image {
|
||||
Image::filled(32, 32, [1.0, 0.0, 0.0, 1.0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkerboard_blending_none_shows_pattern_only_and_size_from_width() {
|
||||
let set = [("width", num(8.0)), ("height", num(2.0))];
|
||||
// None (default): the off squares are transparent; Width Slider makes 8×8 squares.
|
||||
let o = run_fx("ec.generate.checkerboard", &set, red(), 0.0, EffectEnv::default());
|
||||
assert!(o.img.get(3, 3)[3] > 0.99 && o.img.get(3, 3)[1] > 0.99, "{:?}", o.img.get(3, 3));
|
||||
assert!(o.img.get(11, 3)[3] < 0.01);
|
||||
assert!(o.img.get(3, 6)[3] > 0.99, "height ignored with Width Slider");
|
||||
// Width & Height Sliders: 8×2 cells; Normal keeps the layer under the off squares.
|
||||
let o = run_fx(
|
||||
"ec.generate.checkerboard",
|
||||
&[set[0].clone(), set[1].clone(), ("sizeFrom", Value::Enum(2)), ("blendingMode", Value::Enum(1))],
|
||||
red(),
|
||||
0.0,
|
||||
EffectEnv::default(),
|
||||
);
|
||||
assert!(o.img.get(3, 2)[0] > 0.99 && o.img.get(3, 2)[1] < 0.01, "second row of cells is off");
|
||||
// Corner Point: cell size from the anchor-corner rectangle.
|
||||
let o = run_fx("ec.generate.checkerboard", &[("sizeFrom", Value::Enum(0)), ("corner", Value::Vec2([4.0, 4.0]))], red(), 0.0, EffectEnv::default());
|
||||
assert!(o.img.get(1, 1)[3] > 0.99 && o.img.get(5, 1)[3] < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_invert_and_stencil() {
|
||||
let set = [("width", num(10.0)), ("border", num(2.0))];
|
||||
let o = run_fx("ec.generate.grid", &set, red(), 0.0, EffectEnv::default());
|
||||
assert!(o.img.get(0, 5)[3] > 0.9, "line at the anchor");
|
||||
assert!(o.img.get(5, 5)[3] < 0.01, "cell interior is empty with None");
|
||||
let o = run_fx("ec.generate.grid", &[set[0].clone(), set[1].clone(), ("invertGrid", Value::Bool(true))], red(), 0.0, EffectEnv::default());
|
||||
assert!(o.img.get(5, 5)[3] > 0.99 && o.img.get(0, 5)[3] < 0.1);
|
||||
// Stencil Alpha: the layer shows only through the grid lines.
|
||||
let o = run_fx("ec.generate.grid", &[set[0].clone(), set[1].clone(), ("blendingMode", Value::Enum(18))], red(), 0.0, EffectEnv::default());
|
||||
assert!(o.img.get(0, 5)[0] > 0.9 && o.img.get(5, 5)[3] < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_edge_makes_a_ring() {
|
||||
let c = ("center", Value::Vec2([16.0, 16.0]));
|
||||
let disk = run_fx("ec.generate.circle", &[c.clone(), ("radius", num(10.0))], red(), 0.0, EffectEnv::default());
|
||||
assert!(disk.img.get(16, 16)[3] > 0.99 && disk.img.get(30, 16)[3] < 0.01);
|
||||
let ring = run_fx(
|
||||
"ec.generate.circle",
|
||||
&[c.clone(), ("radius", num(10.0)), ("edge", Value::Enum(2)), ("thickness", num(3.0))],
|
||||
red(),
|
||||
0.0,
|
||||
EffectEnv::default(),
|
||||
);
|
||||
assert!(ring.img.get(16, 16)[3] < 0.01, "hole in the middle");
|
||||
assert!(ring.img.get(24, 16)[3] > 0.99, "{:?}", ring.img.get(24, 16));
|
||||
let er =
|
||||
run_fx("ec.generate.circle", &[c, ("radius", num(10.0)), ("edge", Value::Enum(1)), ("edgeRadius", num(5.0))], red(), 0.0, EffectEnv::default());
|
||||
assert!(er.img.get(16, 16)[3] < 0.01 && er.img.get(23, 16)[3] > 0.99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_color_blending_mode_and_opacity() {
|
||||
let pts: Vec<(&str, Value)> = (1..=4)
|
||||
.map(|i| match i {
|
||||
1 => ("positionsColors/point1", Value::Vec2([0.0, 0.0])),
|
||||
2 => ("positionsColors/point2", Value::Vec2([32.0, 0.0])),
|
||||
3 => ("positionsColors/point3", Value::Vec2([0.0, 32.0])),
|
||||
_ => ("positionsColors/point4", Value::Vec2([32.0, 32.0])),
|
||||
})
|
||||
.collect();
|
||||
let mut half = pts.clone();
|
||||
half.push(("opacity", num(50.0)));
|
||||
// None: the gradient alone at half opacity.
|
||||
let o = run_fx("ec.generate.fourcolor", &half, red(), 0.0, EffectEnv::default());
|
||||
assert!((o.img.get(16, 16)[3] - 0.5).abs() < 1e-3);
|
||||
// Normal: over the opaque layer.
|
||||
half.push(("blendingMode", Value::Enum(1)));
|
||||
let o = run_fx("ec.generate.fourcolor", &half, red(), 0.0, EffectEnv::default());
|
||||
assert!((o.img.get(16, 16)[3] - 1.0).abs() < 1e-3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,39 @@ fn add_light(px: Px, l: [f32; 3], la: f32) -> Px {
|
|||
|
||||
// ---------------------------------------------------------------- Cell Pattern
|
||||
|
||||
/// Cell Pattern options. The HQ variants share the shaping of their standard counterparts
|
||||
/// (we always render at full precision).
|
||||
const CELL_PATTERNS: [&str; 12] = [
|
||||
"Bubbles",
|
||||
"Crystals",
|
||||
"Plates",
|
||||
"Static Plates",
|
||||
"Crystallize",
|
||||
"Pillow",
|
||||
"Crystals HQ",
|
||||
"Plates HQ",
|
||||
"Static Plates HQ",
|
||||
"Crystallize HQ",
|
||||
"Mixed Crystals HQ",
|
||||
"Tubular",
|
||||
];
|
||||
|
||||
/// Shaping used for Cell Pattern option `i` (see [`cell_value`]).
|
||||
fn pattern_kind(i: u32) -> u32 {
|
||||
match i {
|
||||
0..=5 => i,
|
||||
6 => 1,
|
||||
7 => 2,
|
||||
8 => 3,
|
||||
9 => 4,
|
||||
10 => 6,
|
||||
_ => 8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Worley distances for point (u, v) in cell units: (F1, F2, hash of the nearest cell).
|
||||
fn worley(u: f64, v: f64, disperse: f64, evo: f64, seed: u32) -> (f64, f64, f32) {
|
||||
/// `tile` repeats the cell layout every (columns, rows) cells.
|
||||
fn worley(u: f64, v: f64, disperse: f64, evo: f64, seed: u32, tile: Option<(i64, i64)>) -> (f64, f64, f32) {
|
||||
let (cx, cy) = (u.floor() as i64, v.floor() as i64);
|
||||
let mut f1 = f64::INFINITY;
|
||||
let mut f2 = f64::INFINITY;
|
||||
|
|
@ -52,7 +83,11 @@ fn worley(u: f64, v: f64, disperse: f64, evo: f64, seed: u32) -> (f64, f64, f32)
|
|||
for j in -1..=1 {
|
||||
for i in -1..=1 {
|
||||
let (gx, gy) = (cx + i, cy + j);
|
||||
let (hx, hy) = (gx as i32 as u32, gy as i32 as u32);
|
||||
let (tx, ty) = match tile {
|
||||
Some((nx, ny)) => (gx.rem_euclid(nx), gy.rem_euclid(ny)),
|
||||
None => (gx, gy),
|
||||
};
|
||||
let (hx, hy) = (tx as i32 as u32, ty as i32 as u32);
|
||||
let h1 = hash1(hx, hy, seed) as f64;
|
||||
let h2 = hash1(hx, hy, seed.wrapping_add(1)) as f64;
|
||||
let h3 = hash1(hx, hy, seed.wrapping_add(2)) as f64;
|
||||
|
|
@ -83,7 +118,6 @@ fn cell_value(pattern: u32, f1: f64, f2: f64, id: f32) -> f32 {
|
|||
4 => id, // Crystallize
|
||||
5 => (1.0 - f1 * f1 * 2.0).clamp(0.0, 1.0), // Pillow
|
||||
6 => id * (e * 3.0).clamp(0.0, 1.0), // Mixed Crystals
|
||||
7 => smoothstep(0.35, 0.25, f1), // Dots
|
||||
_ => 1.0 - ((e - 0.15).abs() * 5.0).clamp(0.0, 1.0), // Tubular
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +134,7 @@ fn overflow(v: f32, mode: u32) -> f32 {
|
|||
}
|
||||
|
||||
fn cell_pattern(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pattern = ctx.params.e("cellPattern");
|
||||
let pattern = pattern_kind(ctx.params.e("cellPattern"));
|
||||
let invert = ctx.params.b("invert");
|
||||
let contrast = ctx.params.f("contrast") as f32 / 100.0;
|
||||
let ov = ctx.params.e("overflow");
|
||||
|
|
@ -108,12 +142,20 @@ fn cell_pattern(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let size = (ctx.params.f("size") * b.scale).max(1.0);
|
||||
let off = b.to_px(ctx.params.v2("offset"));
|
||||
// Static Plates keep their shades still while the cells move.
|
||||
let evo = ctx.params.f("evolution") / 360.0;
|
||||
let seed = (ctx.params.f("randomSeed") as i64 as u32).wrapping_mul(7919) ^ 0xce11;
|
||||
let mut evo = ctx.params.f("evolution") / 360.0;
|
||||
// Cycle Evolution loops the evolution every `cycle` revolutions.
|
||||
if ctx.params.b("evolutionOptions/cycleEvolution") {
|
||||
evo = evo.rem_euclid(ctx.params.f("evolutionOptions/cycle").round().max(1.0));
|
||||
}
|
||||
let seed = (ctx.params.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(7919) ^ 0xce11;
|
||||
let tile = ctx
|
||||
.params
|
||||
.b("tilingOptions/enableTiling")
|
||||
.then(|| (ctx.params.f("tilingOptions/cellsHorizontal").round().max(1.0) as i64, ctx.params.f("tilingOptions/cellsVertical").round().max(1.0) as i64));
|
||||
map_xy(&mut b.img, |x, y, _| {
|
||||
let u = (x as f64 + 0.5 - off.0) / size;
|
||||
let v = (y as f64 + 0.5 - off.1) / size;
|
||||
let (f1, f2, id) = worley(u, v, disperse, evo, seed);
|
||||
let (f1, f2, id) = worley(u, v, disperse, evo, seed, tile);
|
||||
let mut val = cell_value(pattern, f1, f2, id);
|
||||
val = overflow((val - 0.5) * contrast + 0.5, ov);
|
||||
if invert {
|
||||
|
|
@ -243,6 +285,10 @@ fn beam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let inside = ctx.params.color("insideColor");
|
||||
let outside = ctx.params.color("outsideColor");
|
||||
let composite = ctx.params.b("compositeOnOriginal");
|
||||
// 3D Perspective: thickness follows the beam's position between the two points (it
|
||||
// grows / shrinks as it travels); off: the beam's own tail and head take the starting and
|
||||
// ending thickness.
|
||||
let persp = ctx.params.b("perspective3d");
|
||||
let t0 = time * (1.0 - len);
|
||||
let t1 = t0 + len;
|
||||
let (dx, dy) = (e.0 - s.0, e.1 - s.1);
|
||||
|
|
@ -252,7 +298,8 @@ fn beam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let tc = ((vx * dx + vy * dy) / l2).clamp(t0, t1);
|
||||
let (qx, qy) = (s.0 + dx * tc, s.1 + dy * tc);
|
||||
let dist = ((x as f64 + 0.5 - qx).powi(2) + (y as f64 + 0.5 - qy).powi(2)).sqrt();
|
||||
let half = ((st + (et - st) * tc) * 0.5).max(0.25);
|
||||
let tk = if persp || t1 - t0 < 1e-9 { tc } else { (tc - t0) / (t1 - t0) };
|
||||
let half = ((st + (et - st) * tk) * 0.5).max(0.25);
|
||||
let tt = (dist / half) as f32;
|
||||
let a = if len <= 0.0 {
|
||||
0.0
|
||||
|
|
@ -270,24 +317,62 @@ fn beam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---------------------------------------------------------------- Radio Waves
|
||||
|
||||
/// Distance from `p` (relative to the wave centre) to a closed outline `pts`, measured along
|
||||
/// the ray from the centre and corrected to the edge normal.
|
||||
fn outline_dist(pts: &[(f64, f64)], dx: f64, dy: f64) -> f64 {
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
if r < 1e-9 {
|
||||
return pts.iter().map(|p| (p.0 * p.0 + p.1 * p.1).sqrt()).fold(f64::INFINITY, f64::min);
|
||||
}
|
||||
let dir = (dx / r, dy / r);
|
||||
let n = pts.len();
|
||||
let mut best = f64::INFINITY;
|
||||
for k in 0..n {
|
||||
let p0 = pts[k];
|
||||
let p1 = pts[(k + 1) % n];
|
||||
let e = (p1.0 - p0.0, p1.1 - p0.1);
|
||||
let den = dir.0 * e.1 - dir.1 * e.0;
|
||||
if den.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
let t = (p0.0 * e.1 - p0.1 * e.0) / den;
|
||||
// The ray must cross this edge between its endpoints.
|
||||
let q = (dir.0 * t - p0.0, dir.1 * t - p0.1);
|
||||
let el2 = (e.0 * e.0 + e.1 * e.1).max(1e-12);
|
||||
let s = (q.0 * e.0 + q.1 * e.1) / el2;
|
||||
if t <= 0.0 || !(-1e-9..=1.0 + 1e-9).contains(&s) {
|
||||
continue;
|
||||
}
|
||||
let d = (r - t).abs() * den.abs() / el2.sqrt();
|
||||
best = best.min(d);
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = b.to_px(ctx.params.v2("producerPoint"));
|
||||
let sides = ctx.params.f("sides").round().clamp(3.0, 64.0);
|
||||
let freq = ctx.params.f("frequency").max(0.01);
|
||||
let vel = ctx.params.f("velocity") * b.scale;
|
||||
let spin = ctx.params.f("spin").to_radians();
|
||||
let life = ctx.params.f("lifespan").max(0.01);
|
||||
let color = ctx.params.color("color");
|
||||
let op = (ctx.params.f("opacity") / 100.0) as f32;
|
||||
let fin = ctx.params.f("fadeInTime").max(0.0);
|
||||
let fout = ctx.params.f("fadeOutTime").max(0.0);
|
||||
let sw = ctx.params.f("startWidth") * b.scale;
|
||||
let ew = ctx.params.f("endWidth") * b.scale;
|
||||
let sides = ctx.params.f("polygon/sides").round().clamp(3.0, 64.0);
|
||||
let star = ctx.params.b("polygon/star");
|
||||
let depth = ctx.params.f("polygon/starDepth").clamp(-1.0, 1.0);
|
||||
let freq = ctx.params.f("waveMotion/frequency").max(0.01);
|
||||
let expansion = ctx.params.f("waveMotion/expansion") * b.scale;
|
||||
let orient = ctx.params.f("waveMotion/orientation").to_radians();
|
||||
// Direction: 0° = up, clockwise.
|
||||
let dirn = ctx.params.f("waveMotion/direction").to_radians();
|
||||
let vel = ctx.params.f("waveMotion/velocity") * b.scale;
|
||||
let spin = ctx.params.f("waveMotion/spin").to_radians();
|
||||
let life = ctx.params.f("waveMotion/lifespan").max(0.01);
|
||||
let color = ctx.params.color("waveStroke/color");
|
||||
let op = (ctx.params.f("waveStroke/opacity") / 100.0) as f32;
|
||||
let fin = ctx.params.f("waveStroke/fadeInTime").max(0.0);
|
||||
let fout = ctx.params.f("waveStroke/fadeOutTime").max(0.0);
|
||||
let sw = ctx.params.f("waveStroke/startWidth") * b.scale;
|
||||
let ew = ctx.params.f("waveStroke/endWidth") * b.scale;
|
||||
let t = ctx.time.max(0.0);
|
||||
let k_hi = (t * freq).floor() as i64;
|
||||
let k_lo = (((t - life) * freq).floor() as i64).max(0).max(k_hi - 256);
|
||||
// (radius, half width, rotation, fade) per live wave.
|
||||
let waves: Vec<(f64, f64, f64, f32)> = (k_lo..=k_hi)
|
||||
// (centre, radius, half width, rotation, fade) per live wave.
|
||||
let waves: Vec<((f64, f64), f64, f64, f64, f32)> = (k_lo..=k_hi)
|
||||
.filter_map(|k| {
|
||||
let age = t - k as f64 / freq;
|
||||
if !(0.0..life).contains(&age) {
|
||||
|
|
@ -295,19 +380,25 @@ fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
let fi = if fin > 0.0 { (age / fin).min(1.0) } else { 1.0 };
|
||||
let fo = if fout > 0.0 { ((life - age) / fout).min(1.0) } else { 1.0 };
|
||||
Some((vel * age, ((sw + (ew - sw) * age / life) * 0.5).max(0.25), spin * age, (fi * fo) as f32))
|
||||
let centre = (c.0 + dirn.sin() * vel * age, c.1 - dirn.cos() * vel * age);
|
||||
Some((centre, expansion * age, ((sw + (ew - sw) * age / life) * 0.5).max(0.25), orient + spin * age, (fi * fo) as f32))
|
||||
})
|
||||
.collect();
|
||||
let seg = 2.0 * PI / sides;
|
||||
let circle = sides >= 64.0;
|
||||
let circle = sides >= 64.0 && !star;
|
||||
// Unit outline (radius 1) of the polygon or star, rotated per wave below.
|
||||
let n = sides as usize;
|
||||
let unit: Vec<(f64, f64)> = if star {
|
||||
(0..2 * n).map(|i| (PI * i as f64 / n as f64 - PI / 2.0, if i % 2 == 0 { 1.0 } else { (1.0 + depth).max(0.0) })).collect()
|
||||
} else {
|
||||
(0..n).map(|i| (2.0 * PI * i as f64 / n as f64 - PI / 2.0, 1.0)).collect()
|
||||
};
|
||||
let outlines: Vec<Vec<(f64, f64)>> =
|
||||
waves.iter().map(|&(_, rad, _, rot, _)| unit.iter().map(|&(a, k)| ((a + rot).cos() * rad * k, (a + rot).sin() * rad * k)).collect()).collect();
|
||||
map_xy(&mut b.img, |x, y, px| {
|
||||
let (dx, dy) = (x as f64 + 0.5 - c.0, y as f64 + 0.5 - c.1);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
let th = dy.atan2(dx);
|
||||
let mut a = 0.0f32;
|
||||
for &(rad, hw, rot, fade) in &waves {
|
||||
let rb = if circle { rad } else { rad * (PI / sides).cos() / (((th - rot).rem_euclid(seg)) - seg * 0.5).cos() };
|
||||
let d = (r - rb).abs();
|
||||
for (wi, &(cc, rad, hw, _, fade)) in waves.iter().enumerate() {
|
||||
let (dx, dy) = (x as f64 + 0.5 - cc.0, y as f64 + 0.5 - cc.1);
|
||||
let d = if circle { ((dx * dx + dy * dy).sqrt() - rad).abs() } else { outline_dist(&outlines[wi], dx, dy) };
|
||||
a = a.max((hw - d + 0.5).clamp(0.0, 1.0) as f32 * fade);
|
||||
}
|
||||
let a = a * op * color[3];
|
||||
|
|
@ -320,6 +411,9 @@ fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
type Seg = ((f64, f64), (f64, f64), f32);
|
||||
|
||||
/// Advanced Lightning's Lightning Type options.
|
||||
const LIGHTNING_TYPES: [&str; 8] = ["Direction", "Strike", "Breaking", "Bouncy", "Omni", "Anywhere", "Vertical", "Two-Way Strike"];
|
||||
|
||||
fn bolt(a: (f64, f64), z: (f64, f64), depth: u32, inten: f32, turb: f64, fork: f64, decay: f32, rng: &mut Rng, out: &mut Vec<Seg>) {
|
||||
if out.len() > 20_000 {
|
||||
return;
|
||||
|
|
@ -346,42 +440,70 @@ fn bolt(a: (f64, f64), z: (f64, f64), depth: u32, inten: f32, turb: f64, fork: f
|
|||
|
||||
fn lightning_segments(ctx: &EffectCtx, b: &Buf) -> Vec<Seg> {
|
||||
let kind = ctx.params.e("lightningType");
|
||||
let decay_main = ctx.params.b("decayMainCore");
|
||||
let o = b.to_px(ctx.params.v2("origin"));
|
||||
let d = b.to_px(ctx.params.v2("direction"));
|
||||
let turb = ctx.params.f("turbulence").max(0.0);
|
||||
let fork = (ctx.params.f("forking") / 100.0).clamp(0.0, 1.0);
|
||||
let decay = ctx.params.f("decay").clamp(0.0, 1.0) as f32;
|
||||
let depth = ctx.params.f("complexity").round().clamp(1.0, 12.0) as u32;
|
||||
let depth = ctx.params.f("expertSettings/complexity").round().clamp(1.0, 12.0) as u32;
|
||||
let state = ctx.params.f("conductivityState").floor() as i64 as u64;
|
||||
let mut rng = Rng::new(state ^ ((ctx.seed as u64) << 32));
|
||||
let mut segs = Vec::new();
|
||||
let (h, w) = (b.img.height as f64, b.img.width as f64);
|
||||
// LIGHTNING_TYPES order.
|
||||
match kind {
|
||||
1 => {
|
||||
// Direction: travels toward the Direction point and beyond.
|
||||
0 => {
|
||||
let z = (o.0 + (d.0 - o.0) * 3.0, o.1 + (d.1 - o.1) * 3.0);
|
||||
bolt(o, z, depth, 1.0, turb, fork, decay, &mut rng, &mut segs);
|
||||
}
|
||||
// Breaking: more branching as the points move apart.
|
||||
2 => {
|
||||
let dist = ((d.0 - o.0).powi(2) + (d.1 - o.1).powi(2)).sqrt();
|
||||
let f = (fork * (0.5 + dist / w.max(h).max(1.0))).min(1.0);
|
||||
bolt(o, d, depth, 1.0, turb, f, decay, &mut rng, &mut segs);
|
||||
}
|
||||
// Bouncy: a strike with a stronger zig-zag.
|
||||
3 => bolt(o, d, depth, 1.0, turb * 1.6, fork, decay, &mut rng, &mut segs),
|
||||
// Omni / Anywhere: bolts in all directions out to the Outer Radius.
|
||||
4 | 5 => {
|
||||
let len = ((d.0 - o.0).powi(2) + (d.1 - o.1).powi(2)).sqrt().max(w.min(h) * 0.25);
|
||||
for _ in 0..4 {
|
||||
let a = rng.f() * 2.0 * PI;
|
||||
let z = (o.0 + a.cos() * len, o.1 + a.sin() * len);
|
||||
let l = if kind == 5 { len * (0.3 + 0.7 * rng.f()) } else { len };
|
||||
let z = (o.0 + a.cos() * l, o.1 + a.sin() * l);
|
||||
bolt(o, z, depth, 1.0, turb, fork, decay, &mut rng, &mut segs);
|
||||
}
|
||||
}
|
||||
3 => bolt(o, (o.0, h), depth, 1.0, turb, fork, decay, &mut rng, &mut segs),
|
||||
6 => bolt(o, (o.0, h), depth, 1.0, turb, fork, decay, &mut rng, &mut segs),
|
||||
// Two-Way Strike: from both ends toward the middle.
|
||||
7 => {
|
||||
let m = ((o.0 + d.0) * 0.5, (o.1 + d.1) * 0.5);
|
||||
bolt(o, m, depth.saturating_sub(1).max(1), 1.0, turb, fork, decay, &mut rng, &mut segs);
|
||||
bolt(d, m, depth.saturating_sub(1).max(1), 1.0, turb, fork, decay, &mut rng, &mut segs);
|
||||
}
|
||||
// Strike.
|
||||
_ => bolt(o, d, depth, 1.0, turb, fork, decay, &mut rng, &mut segs),
|
||||
}
|
||||
// Decay Main Core: the whole bolt fades with distance from the origin.
|
||||
if decay_main {
|
||||
let far = segs.iter().map(|(_, z, _)| ((z.0 - o.0).powi(2) + (z.1 - o.1).powi(2)).sqrt()).fold(1e-9, f64::max);
|
||||
for (a, _, i) in &mut segs {
|
||||
let t = (((a.0 - o.0).powi(2) + (a.1 - o.1).powi(2)).sqrt() / far) as f32;
|
||||
*i *= (1.0 - decay * t).max(0.0);
|
||||
}
|
||||
}
|
||||
segs
|
||||
}
|
||||
|
||||
fn advanced_lightning(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let core_r = (ctx.params.f("coreRadius") * b.scale).max(0.3);
|
||||
let core_op = (ctx.params.f("coreOpacity") / 100.0) as f32;
|
||||
let core_c = ctx.params.color("coreColor");
|
||||
let glow_r = ctx.params.f("glowRadius") * b.scale;
|
||||
let glow_op = (ctx.params.f("glowOpacity") / 100.0) as f32;
|
||||
let glow_c = ctx.params.color("glowColor");
|
||||
let core_r = (ctx.params.f("coreSettings/coreRadius") * b.scale).max(0.3);
|
||||
let core_op = (ctx.params.f("coreSettings/coreOpacity") / 100.0) as f32;
|
||||
let core_c = ctx.params.color("coreSettings/coreColor");
|
||||
let glow_r = ctx.params.f("glowSettings/glowRadius") * b.scale;
|
||||
let glow_op = (ctx.params.f("glowSettings/glowOpacity") / 100.0) as f32;
|
||||
let glow_c = ctx.params.color("glowSettings/glowColor");
|
||||
let composite = ctx.params.b("compositeOnOriginal");
|
||||
let segs = lightning_segments(ctx, &b);
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
|
|
@ -588,20 +710,20 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.generate.cellpattern",
|
||||
"Cell Pattern",
|
||||
vec![
|
||||
p(
|
||||
"cellPattern",
|
||||
"Cell Pattern",
|
||||
Value::Enum(0),
|
||||
popup(&["Bubbles", "Crystals", "Plates", "Static Plates", "Crystallize", "Pillow", "Mixed Crystals", "Dots", "Tubular"]),
|
||||
),
|
||||
p("cellPattern", "Cell Pattern", Value::Enum(0), popup(&CELL_PATTERNS)),
|
||||
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("overflow", "Overflow", Value::Enum(0), popup(&["Clip", "Soft Clamp", "Wrap Back"])),
|
||||
p("disperse", "Disperse", num(1.0), slider(0.0, 1.5, 0.0, 1.5, 2)),
|
||||
p("size", "Size", num(60.0), slider(2.0, 4000.0, 2.0, 400.0, 1)),
|
||||
p("offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("tilingOptions/enableTiling", "Enable Tiling", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("tilingOptions/cellsHorizontal", "Cells Horizontal", num(16.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("tilingOptions/cellsVertical", "Cells Vertical", num(16.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("evolution", "Evolution", num(0.0), ang()),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
p("evolutionOptions/cycleEvolution", "Cycle Evolution", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("evolutionOptions/cycle", "Cycle (in Revolutions)", num(1.0), slider(1.0, 1000.0, 1.0, 20.0, 0)),
|
||||
p("evolutionOptions/randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
cell_pattern,
|
||||
),
|
||||
|
|
@ -644,6 +766,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("softness", "Softness", num(20.0), pct()),
|
||||
p("insideColor", "Inside Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("outsideColor", "Outside Color", col(0.4, 0.4, 1.0), ParamUi::Color),
|
||||
p("perspective3d", "3D Perspective", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("compositeOnOriginal", "Composite On Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
beam,
|
||||
|
|
@ -653,17 +776,22 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Radio Waves",
|
||||
vec![
|
||||
p("producerPoint", "Producer Point", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("sides", "Sides", num(64.0), slider(3.0, 64.0, 3.0, 64.0, 0)),
|
||||
p("frequency", "Frequency", num(1.0), slider(0.01, 100.0, 0.01, 10.0, 2)),
|
||||
p("velocity", "Velocity", num(100.0), slider(0.0, 10000.0, 0.0, 1000.0, 1)),
|
||||
p("spin", "Spin", num(0.0), slider(-3600.0, 3600.0, -360.0, 360.0, 1)),
|
||||
p("lifespan", "Lifespan (sec)", num(2.0), slider(0.01, 100.0, 0.01, 10.0, 2)),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p("fadeInTime", "Fade-in Time", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("fadeOutTime", "Fade-out Time", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("startWidth", "Start Width", num(5.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("endWidth", "End Width", num(5.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("polygon/sides", "Sides", num(64.0), slider(3.0, 64.0, 3.0, 64.0, 0)),
|
||||
p("polygon/star", "Star", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("polygon/starDepth", "Star Depth", num(-0.3), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("waveMotion/frequency", "Frequency", num(1.0), slider(0.01, 100.0, 0.01, 10.0, 2)),
|
||||
p("waveMotion/expansion", "Expansion", num(100.0), slider(0.0, 10000.0, 0.0, 1000.0, 1)),
|
||||
p("waveMotion/orientation", "Orientation", num(0.0), ang()),
|
||||
p("waveMotion/direction", "Direction", num(90.0), ang()),
|
||||
p("waveMotion/velocity", "Velocity", num(0.0), slider(0.0, 10000.0, 0.0, 1000.0, 1)),
|
||||
p("waveMotion/spin", "Spin", num(0.0), slider(-3600.0, 3600.0, -360.0, 360.0, 1)),
|
||||
p("waveMotion/lifespan", "Lifespan (sec)", num(2.0), slider(0.01, 100.0, 0.01, 10.0, 2)),
|
||||
p("waveStroke/color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("waveStroke/opacity", "Opacity", num(100.0), pct()),
|
||||
p("waveStroke/fadeInTime", "Fade-in Time", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("waveStroke/fadeOutTime", "Fade-out Time", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("waveStroke/startWidth", "Start Width", num(5.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("waveStroke/endWidth", "End Width", num(5.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
radio_waves,
|
||||
),
|
||||
|
|
@ -671,21 +799,22 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.generate.advancedlightning",
|
||||
"Advanced Lightning",
|
||||
vec![
|
||||
p("lightningType", "Lightning Type", Value::Enum(0), popup(&["Strike", "Direction", "Omni", "Vertical"])),
|
||||
p("lightningType", "Lightning Type", Value::Enum(1), popup(&LIGHTNING_TYPES)),
|
||||
p("origin", "Origin", pt(0.25, 0.2), ParamUi::Point),
|
||||
p("direction", "Direction", pt(0.75, 0.8), ParamUi::Point),
|
||||
p("conductivityState", "Conductivity State", num(10.0), slider(0.0, 100000.0, 0.0, 100.0, 1)),
|
||||
p("coreRadius", "Core Radius", num(3.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("coreOpacity", "Core Opacity", num(75.0), pct()),
|
||||
p("coreColor", "Core Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("glowRadius", "Glow Radius", num(50.0), slider(0.0, 1000.0, 0.0, 200.0, 1)),
|
||||
p("glowOpacity", "Glow Opacity", num(50.0), pct()),
|
||||
p("glowColor", "Glow Color", col(0.25, 0.35, 1.0), ParamUi::Color),
|
||||
p("coreSettings/coreRadius", "Core Radius", num(3.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("coreSettings/coreOpacity", "Core Opacity", num(75.0), pct()),
|
||||
p("coreSettings/coreColor", "Core Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("glowSettings/glowRadius", "Glow Radius", num(50.0), slider(0.0, 1000.0, 0.0, 200.0, 1)),
|
||||
p("glowSettings/glowOpacity", "Glow Opacity", num(50.0), pct()),
|
||||
p("glowSettings/glowColor", "Glow Color", col(0.25, 0.35, 1.0), ParamUi::Color),
|
||||
p("turbulence", "Turbulence", num(1.0), slider(0.0, 10.0, 0.0, 4.0, 2)),
|
||||
p("forking", "Forking", num(25.0), pct()),
|
||||
p("decay", "Decay", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
p("complexity", "Complexity", num(6.0), slider(1.0, 12.0, 1.0, 12.0, 0)),
|
||||
p("compositeOnOriginal", "Composite on Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("decayMainCore", "Decay Main Core", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("compositeOnOriginal", "Composite On Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("expertSettings/complexity", "Complexity", num(6.0), slider(1.0, 12.0, 1.0, 12.0, 0)),
|
||||
],
|
||||
advanced_lightning,
|
||||
),
|
||||
|
|
@ -763,7 +892,7 @@ mod tests {
|
|||
let a = run("ec.generate.cellpattern", &[("size", num(10.0))], Image::new(40, 30));
|
||||
let b2 = run("ec.generate.cellpattern", &[("size", num(10.0))], Image::new(40, 30));
|
||||
assert_eq!(a.img, b2.img);
|
||||
let c = run("ec.generate.cellpattern", &[("size", num(10.0)), ("randomSeed", num(5.0))], Image::new(40, 30));
|
||||
let c = run("ec.generate.cellpattern", &[("size", num(10.0)), ("evolutionOptions/randomSeed", num(5.0))], Image::new(40, 30));
|
||||
assert_ne!(a.img, c.img);
|
||||
assert!(a.img.data.iter().all(|p| p[3] == 1.0 && (0.0..=1.0).contains(&p[0])));
|
||||
}
|
||||
|
|
@ -795,6 +924,89 @@ mod tests {
|
|||
assert!(o.img.get(2, 16)[3] < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_pattern_tiles() {
|
||||
// 4×4-cell tiles of 8 px: the pattern repeats every 32 px.
|
||||
let set = [
|
||||
("size", num(8.0)),
|
||||
("tilingOptions/enableTiling", Value::Bool(true)),
|
||||
("tilingOptions/cellsHorizontal", num(4.0)),
|
||||
("tilingOptions/cellsVertical", num(4.0)),
|
||||
];
|
||||
let o = run("ec.generate.cellpattern", &set, Image::new(64, 64));
|
||||
for (x, y) in [(3, 5), (10, 20), (17, 2)] {
|
||||
let (a, b2) = (o.img.get(x, y), o.img.get(x + 32, y + 32));
|
||||
assert!((a[0] - b2[0]).abs() < 1e-4, "{x},{y}: {a:?} vs {b2:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radio_waves_expansion_velocity_and_star() {
|
||||
let r = |set: &[(&str, Value)], t: f64| {
|
||||
let s = crate::find("ec.generate.radiowaves").unwrap();
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
params.values.insert("producerPoint".into(), Value::Vec2([32.0, 32.0]));
|
||||
for (k, v) in set {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: t, layer_size: [64.0, 64.0], seed: 3, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img: Image::new(64, 64), offset: [0.0, 0.0], scale: 1.0 })
|
||||
};
|
||||
// One wave born at t = 0, 0.2 s old: radius = Expansion × age = 20 px.
|
||||
let base = [("waveMotion/expansion", num(100.0)), ("waveMotion/frequency", num(0.5))];
|
||||
let o = r(&base, 0.2);
|
||||
assert!(o.img.get(52, 32)[3] > 0.9, "{:?}", o.img.get(52, 32));
|
||||
assert!(o.img.get(32, 32)[3] < 0.01);
|
||||
// Velocity moves the wave toward Direction (90° = right).
|
||||
let mut moving = base.to_vec();
|
||||
moving.push(("waveMotion/velocity", num(50.0)));
|
||||
let o = r(&moving, 0.2);
|
||||
assert!(o.img.get(62, 32)[3] > 0.9 && o.img.get(52, 32)[3] < 0.5, "{:?}", o.img.get(62, 32));
|
||||
// A 4-point star: the outline dips toward the centre between the points.
|
||||
let mut star = base.to_vec();
|
||||
star.extend([("polygon/sides", num(4.0)), ("polygon/star", Value::Bool(true)), ("polygon/starDepth", num(-0.5))]);
|
||||
let o = r(&star, 0.2);
|
||||
assert!(o.img.get(32, 12)[3] > 0.9, "point straight up");
|
||||
// Between points (45°) the outline sits near half the radius.
|
||||
assert!(o.img.get(39, 24)[3] > 0.5, "{:?}", o.img.get(39, 24));
|
||||
assert!(o.img.get(46, 18)[3] < 0.01, "no circle there");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lightning_types_and_decay_main_core() {
|
||||
let a = run("ec.generate.advancedlightning", &[], Image::new(64, 48));
|
||||
let strike = run("ec.generate.advancedlightning", &[("lightningType", Value::Enum(1))], Image::new(64, 48));
|
||||
assert_eq!(a.img, strike.img, "Strike is the default");
|
||||
for t in 0..8 {
|
||||
let o = run("ec.generate.advancedlightning", &[("lightningType", Value::Enum(t))], Image::new(64, 48));
|
||||
assert!(o.img.data.iter().any(|p| p[3] > 0.3), "type {t} draws");
|
||||
}
|
||||
let sum = |i: &Image| i.data.iter().map(|p| p[3]).sum::<f32>();
|
||||
let d = run("ec.generate.advancedlightning", &[("decayMainCore", Value::Bool(true)), ("decay", num(0.9))], Image::new(64, 48));
|
||||
let n = run("ec.generate.advancedlightning", &[("decay", num(0.9))], Image::new(64, 48));
|
||||
assert!(sum(&d.img) < sum(&n.img));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beam_3d_perspective_controls_thickness() {
|
||||
// Short beam near the start: with 3D Perspective its thickness comes from its position
|
||||
// (thin, near the start); without, its head takes the full ending thickness.
|
||||
let set = |p: bool| {
|
||||
vec![
|
||||
("length", num(20.0)),
|
||||
("time", num(0.0)),
|
||||
("softness", num(0.0)),
|
||||
("startThickness", num(2.0)),
|
||||
("endThickness", num(20.0)),
|
||||
("perspective3d", Value::Bool(p)),
|
||||
]
|
||||
};
|
||||
let on = run("ec.generate.beam", &set(true), Image::new(64, 32));
|
||||
let off = run("ec.generate.beam", &set(false), Image::new(64, 32));
|
||||
let sum = |i: &Image| i.data.iter().map(|p| p[3]).sum::<f32>();
|
||||
assert!(sum(&off.img) > sum(&on.img) * 1.5, "{} {}", sum(&off.img), sum(&on.img));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lens_flare_brightest_at_center() {
|
||||
let o = run("ec.generate.lensflare", &[], Image::new(64, 64));
|
||||
|
|
|
|||
|
|
@ -191,6 +191,8 @@ fn chaikin(pts: &[[f64; 2]], iters: usize) -> Vec<[f64; 2]> {
|
|||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FracSet {
|
||||
/// Mandelbrot iterations start at the Julia centre (the "Over Julia" set choices).
|
||||
over_julia: bool,
|
||||
julia: bool,
|
||||
inverse: bool,
|
||||
}
|
||||
|
|
@ -220,38 +222,49 @@ fn escape(z0: (f64, f64), c: (f64, f64), power: u32, limit: f64, max_iter: u32)
|
|||
None
|
||||
}
|
||||
|
||||
/// Fractal Set Choice options.
|
||||
pub(crate) const FRACTAL_SETS: [&str; 6] =
|
||||
["Mandelbrot", "Mandelbrot Inverse", "Mandelbrot Over Julia", "Mandelbrot Inverse Over Julia", "Julia", "Julia Inverse"];
|
||||
/// Fractal Palette options.
|
||||
pub(crate) const FRACTAL_PALETTES: [&str; 4] = ["Lightness Gradient", "Hue Wheel", "Black And White", "Solid Color"];
|
||||
/// Fractal Oversample Method options.
|
||||
pub(crate) const FRACTAL_OVERSAMPLE: [&str; 2] = ["Edge Detect-Fast-May Miss Pixels", "Brute Force-Slow-Every Pixel"];
|
||||
|
||||
fn fractal(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let set_choice = pr.e("setChoice");
|
||||
let power = 2 + pr.e("equation").min(2);
|
||||
let mc = (pr.f("mandelbrotX"), pr.f("mandelbrotY"));
|
||||
let mmag = pr.f("mandelbrotMagnification");
|
||||
let mlim = pr.f("mandelbrotEscapeLimit").max(1.01);
|
||||
let jc = (pr.f("juliaX"), pr.f("juliaY"));
|
||||
let jmag = pr.f("juliaMagnification");
|
||||
let jlim = pr.f("juliaEscapeLimit").max(1.01);
|
||||
let post = (pr.f("postInversionX"), pr.f("postInversionY"));
|
||||
let overlay = pr.b("overlay");
|
||||
let transparent_inside = pr.b("transparency") || overlay;
|
||||
let palette = pr.e("palette");
|
||||
let hue = (pr.f("hue") / 360.0) as f32;
|
||||
let steps = pr.f("cycleSteps").max(1.0);
|
||||
let cyc_off = pr.f("cycleOffset") / 360.0;
|
||||
let edge = pr.b("edgeHighlight");
|
||||
let every = pr.e("samplingMethod") == 1;
|
||||
let factor = pr.f("samplingFactor").round().clamp(1.0, 9.0) as usize;
|
||||
let mc = (pr.f("mandelbrot/mandelbrotX"), pr.f("mandelbrot/mandelbrotY"));
|
||||
let mmag = pr.f("mandelbrot/mandelbrotMagnification");
|
||||
let mlim = pr.f("mandelbrot/mandelbrotEscapeLimit").max(1.01);
|
||||
let jc = (pr.f("julia/juliaX"), pr.f("julia/juliaY"));
|
||||
let jmag = pr.f("julia/juliaMagnification");
|
||||
let jlim = pr.f("julia/juliaEscapeLimit").max(1.01);
|
||||
let post = (pr.f("postInversionOffset/postInversionX"), pr.f("postInversionOffset/postInversionY"));
|
||||
let overlay = pr.b("fractalColor/overlay");
|
||||
let transparency = pr.b("fractalColor/transparency");
|
||||
let transparent_inside = transparency || overlay;
|
||||
let palette = pr.e("fractalColor/palette");
|
||||
let hue = (pr.f("fractalColor/hue") / 360.0) as f32;
|
||||
let steps = pr.f("fractalColor/cycleSteps").max(1.0);
|
||||
let cyc_off = pr.f("fractalColor/cycleOffset") / 360.0;
|
||||
let edge = pr.b("fractalColor/edgeHighlight");
|
||||
let every = pr.e("highQualitySettings/samplingMethod") == 1;
|
||||
let factor = pr.f("highQualitySettings/samplingFactor").round().clamp(1.0, 9.0) as usize;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let lw = ctx.layer_size[0].max(1.0);
|
||||
let lh = ctx.layer_size[1].max(1.0);
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
let (off_x, off_y) = (b.offset[0], b.offset[1]);
|
||||
// FRACTAL_SETS order.
|
||||
let set = |julia, inverse, over_julia| FracSet { over_julia, julia, inverse };
|
||||
let sets: (FracSet, Option<FracSet>) = match set_choice {
|
||||
1 => (FracSet { julia: false, inverse: true }, None),
|
||||
2 => (FracSet { julia: false, inverse: false }, Some(FracSet { julia: false, inverse: true })),
|
||||
3 => (FracSet { julia: false, inverse: true }, Some(FracSet { julia: false, inverse: false })),
|
||||
4 => (FracSet { julia: true, inverse: false }, None),
|
||||
5 => (FracSet { julia: true, inverse: true }, None),
|
||||
_ => (FracSet { julia: false, inverse: false }, None),
|
||||
1 => (set(false, true, false), None),
|
||||
2 => (set(false, false, true), None),
|
||||
3 => (set(false, true, true), None),
|
||||
4 => (set(true, false, false), None),
|
||||
5 => (set(true, true, false), None),
|
||||
_ => (set(false, false, false), None),
|
||||
};
|
||||
let iter_for = |mag: f64| (96.0 + 48.0 * mag.max(0.0)).clamp(64.0, 3000.0) as u32;
|
||||
let eval_set = |s: FracSet, lx: f64, ly: f64| -> Option<f64> {
|
||||
|
|
@ -264,7 +277,12 @@ fn fractal(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
u = u / d + post.0;
|
||||
v = -v / d + post.1;
|
||||
}
|
||||
if s.julia { escape((u, v), mc, power, lim, iter_for(mag)) } else { escape((0.0, 0.0), (u, v), power, lim, iter_for(mag)) }
|
||||
if s.julia {
|
||||
escape((u, v), mc, power, lim, iter_for(mag))
|
||||
} else {
|
||||
let z0 = if s.over_julia { jc } else { (0.0, 0.0) };
|
||||
escape(z0, (u, v), power, lim, iter_for(mag))
|
||||
}
|
||||
};
|
||||
let eval = |lx: f64, ly: f64| -> Option<f64> {
|
||||
let a = eval_set(sets.0, lx, ly);
|
||||
|
|
@ -274,14 +292,26 @@ fn fractal(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
};
|
||||
let shade = |n: Option<f64>| -> Px {
|
||||
// Solid Color: only the inside of the set is drawn (Transparency flips the sides).
|
||||
if palette == 3 {
|
||||
let (r, g, bl) = hsl_to_rgb(hue, 1.0, 0.5);
|
||||
return if n.is_none() != transparency { [r, g, bl, 1.0] } else { [0.0; 4] };
|
||||
}
|
||||
let Some(n) = n else {
|
||||
return if transparent_inside { [0.0; 4] } else { [0.0, 0.0, 0.0, 1.0] };
|
||||
};
|
||||
let band = ((n / steps + cyc_off).rem_euclid(1.0)) as f32;
|
||||
let u = n / steps + cyc_off;
|
||||
let band = (u.rem_euclid(1.0)) as f32;
|
||||
let (r, g, bl) = match palette {
|
||||
// Hue Wheel: the full hue circle at full saturation and brightness.
|
||||
1 => hsl_to_rgb((hue + band).rem_euclid(1.0), 1.0, 0.5),
|
||||
2 => hsl_to_rgb((hue + band).rem_euclid(1.0), 1.0, 0.3 + 0.4 * (band * 4.0).fract()),
|
||||
_ => hsl_to_rgb(hue, 0.85, band.clamp(0.0, 1.0)),
|
||||
// Black And White: alternating bands.
|
||||
2 => {
|
||||
let v = if (u.floor() as i64).rem_euclid(2) == 0 { 0.0 } else { 1.0 };
|
||||
(v, v, v)
|
||||
}
|
||||
// Lightness Gradient: black → hue → white, the hue moving 45° each cycle.
|
||||
_ => hsl_to_rgb((hue + u.floor() as f32 * 0.125).rem_euclid(1.0), 0.85, band.clamp(0.0, 1.0)),
|
||||
};
|
||||
let mut c = [r, g, bl];
|
||||
if edge && n.fract() < 0.12 {
|
||||
|
|
@ -455,17 +485,17 @@ fn scribble(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
let sc = b.scale;
|
||||
let fill = pr.e("fillType");
|
||||
let ew = pr.f("edgeWidth").max(0.1) * sc;
|
||||
let ew = pr.f("edgeOptions/edgeWidth").max(0.1) * sc;
|
||||
let ang = pr.f("angle").to_radians();
|
||||
let d = [ang.cos(), ang.sin()];
|
||||
let n = [-d[1], d[0]];
|
||||
let width = (pr.f("strokeWidth") * sc).max(0.1);
|
||||
let curv = pr.f("curviness") / 100.0;
|
||||
let curv_var = pr.f("curvinessVariation") / 100.0;
|
||||
let spacing = (pr.f("spacing") * sc).max(0.75).max(width * 0.25);
|
||||
let spacing_var = pr.f("spacingVariation") / 100.0;
|
||||
let overlap = pr.f("pathOverlap") / 100.0;
|
||||
let overlap_var = pr.f("pathOverlapVariation") / 100.0;
|
||||
let curv = pr.f("strokeOptions/curviness") / 100.0;
|
||||
let curv_var = pr.f("strokeOptions/curvinessVariation") / 100.0;
|
||||
let spacing = (pr.f("strokeOptions/spacing") * sc).max(0.75).max(width * 0.25);
|
||||
let spacing_var = pr.f("strokeOptions/spacingVariation") / 100.0;
|
||||
let overlap = pr.f("strokeOptions/pathOverlap") / 100.0;
|
||||
let overlap_var = pr.f("strokeOptions/pathOverlapVariation") / 100.0;
|
||||
let start = pr.f("start").clamp(0.0, 100.0) / 100.0;
|
||||
let end = pr.f("end").clamp(0.0, 100.0) / 100.0;
|
||||
let sequential = pr.b("fillPathsSequentially");
|
||||
|
|
@ -757,6 +787,9 @@ fn vegas(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
};
|
||||
let nseg = pr.f("segments").round().clamp(1.0, 1000.0) as usize;
|
||||
// Shorter Contours Have Fewer Segments: segment count scales with contour length.
|
||||
let fewer = pr.e("stroke") == 0 && pr.e("shorterContoursHave") == 1;
|
||||
let longest = paths.iter().map(|(p, c)| poly_length(p, *c)).fold(0.0, f64::max);
|
||||
let seg_len = pr.f("length").clamp(0.0, 1.0);
|
||||
let bunched = pr.e("segmentDistribution") == 0;
|
||||
let rot = pr.f("rotation") / 360.0;
|
||||
|
|
@ -772,6 +805,7 @@ fn vegas(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
continue;
|
||||
}
|
||||
let phase = rot + if random_phase { hash1(ci as u32, 1, seed) as f64 } else { 0.0 };
|
||||
let nseg = if fewer && longest > 0.0 { ((nseg as f64 * len / longest).round() as usize).max(1) } else { nseg };
|
||||
let each = len / nseg as f64 * seg_len;
|
||||
for i in 0..nseg {
|
||||
let base = if bunched { (i as f64 + (hash1(ci as u32, i as u32 + 7, seed) as f64 - 0.5) * 0.8) / nseg as f64 } else { i as f64 / nseg as f64 };
|
||||
|
|
@ -1401,32 +1435,27 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.generate.fractal",
|
||||
"Fractal",
|
||||
vec![
|
||||
p(
|
||||
"setChoice",
|
||||
"Set Choice",
|
||||
Value::Enum(0),
|
||||
popup(&["Mandelbrot", "Mandelbrot Inverse", "Mandelbrot Over Inverse", "Mandelbrot Inverse Over Mandelbrot", "Julia", "Julia Inverse"]),
|
||||
),
|
||||
p("setChoice", "Set Choice", Value::Enum(0), popup(&FRACTAL_SETS)),
|
||||
p("equation", "Equation", Value::Enum(0), popup(&["z = z^2 + c", "z = z^3 + c", "z = z^4 + c"])),
|
||||
p("mandelbrotX", "Mandelbrot: X (Real)", num(-0.74), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("mandelbrotY", "Mandelbrot: Y (Imaginary)", num(0.15), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("mandelbrotMagnification", "Mandelbrot: Magnification", num(0.0), slider(-5.0, 30.0, -5.0, 30.0, 2)),
|
||||
p("mandelbrotEscapeLimit", "Mandelbrot: Escape Limit", num(4.0), slider(1.0, 1000.0, 1.0, 100.0, 1)),
|
||||
p("juliaX", "Julia: X (Real)", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("juliaY", "Julia: Y (Imaginary)", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("juliaMagnification", "Julia: Magnification", num(0.0), slider(-5.0, 30.0, -5.0, 30.0, 2)),
|
||||
p("juliaEscapeLimit", "Julia: Escape Limit", num(4.0), slider(1.0, 1000.0, 1.0, 100.0, 1)),
|
||||
p("postInversionX", "Post-Inversion Offset: X Offset", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("postInversionY", "Post-Inversion Offset: Y Offset", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
check("overlay", "Overlay", false),
|
||||
check("transparency", "Transparency", false),
|
||||
p("palette", "Palette", Value::Enum(0), popup(&["Lightness Bands", "Hue Bands", "Hue/Lightness Bands"])),
|
||||
p("hue", "Hue", num(0.0), ang()),
|
||||
p("cycleSteps", "Cycle Steps", num(32.0), slider(1.0, 1000.0, 1.0, 256.0, 0)),
|
||||
p("cycleOffset", "Cycle Offset", num(0.0), ang()),
|
||||
check("edgeHighlight", "Edge Highlight", false),
|
||||
p("samplingMethod", "Sampling Method", Value::Enum(0), popup(&["Edge Detect", "Every Pixel"])),
|
||||
p("samplingFactor", "Sampling Factor", num(2.0), slider(1.0, 9.0, 1.0, 9.0, 0)),
|
||||
p("mandelbrot/mandelbrotX", "X (Real)", num(-0.74), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("mandelbrot/mandelbrotY", "Y (Imaginary)", num(0.15), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("mandelbrot/mandelbrotMagnification", "Magnification", num(0.0), slider(-5.0, 30.0, -5.0, 30.0, 2)),
|
||||
p("mandelbrot/mandelbrotEscapeLimit", "Escape Limit", num(4.0), slider(1.0, 1000.0, 1.0, 100.0, 1)),
|
||||
p("julia/juliaX", "X (Real)", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("julia/juliaY", "Y (Imaginary)", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("julia/juliaMagnification", "Magnification", num(0.0), slider(-5.0, 30.0, -5.0, 30.0, 2)),
|
||||
p("julia/juliaEscapeLimit", "Escape Limit", num(4.0), slider(1.0, 1000.0, 1.0, 100.0, 1)),
|
||||
p("postInversionOffset/postInversionX", "X Offset", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
p("postInversionOffset/postInversionY", "Y Offset", num(0.0), slider(-2.0, 2.0, -2.0, 2.0, 4)),
|
||||
check("fractalColor/overlay", "Overlay", false),
|
||||
check("fractalColor/transparency", "Transparency", false),
|
||||
p("fractalColor/palette", "Palette", Value::Enum(0), popup(&FRACTAL_PALETTES)),
|
||||
p("fractalColor/hue", "Hue", num(0.0), ang()),
|
||||
p("fractalColor/cycleSteps", "Cycle Steps", num(32.0), slider(1.0, 1000.0, 1.0, 256.0, 0)),
|
||||
p("fractalColor/cycleOffset", "Cycle Offset", num(0.0), ang()),
|
||||
check("fractalColor/edgeHighlight", "Edge Highlight", false),
|
||||
p("highQualitySettings/samplingMethod", "Oversample Method", Value::Enum(0), popup(&FRACTAL_OVERSAMPLE)),
|
||||
p("highQualitySettings/samplingFactor", "Oversample Factor", num(2.0), slider(1.0, 9.0, 1.0, 9.0, 0)),
|
||||
],
|
||||
fractal,
|
||||
),
|
||||
|
|
@ -1437,17 +1466,17 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("scribble", "Scribble", Value::Enum(0), popup(&["Single Mask", "All Masks", "All Masks Using Modes"])),
|
||||
mask_idx("mask", "Mask", 1.0),
|
||||
p("fillType", "Fill Type", Value::Enum(0), popup(&["Inside", "Centered Edge", "Inside Edge", "Outside Edge", "Left Edge", "Right Edge"])),
|
||||
p("edgeWidth", "Edge Options: Edge Width", num(10.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("edgeOptions/edgeWidth", "Edge Width", num(10.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("color", "Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p("angle", "Angle", num(45.0), ang()),
|
||||
p("strokeWidth", "Stroke Width", num(2.0), slider(0.1, 50.0, 0.1, 50.0, 1)),
|
||||
p("curviness", "Stroke Options: Curviness", num(50.0), pct()),
|
||||
p("curvinessVariation", "Stroke Options: Curviness Variation", num(50.0), pct()),
|
||||
p("spacing", "Stroke Options: Spacing", num(5.0), slider(0.1, 100.0, 0.1, 50.0, 1)),
|
||||
p("spacingVariation", "Stroke Options: Spacing Variation", num(50.0), pct()),
|
||||
p("pathOverlap", "Stroke Options: Path Overlap", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("pathOverlapVariation", "Stroke Options: Path Overlap Variation", num(50.0), pct()),
|
||||
p("strokeOptions/curviness", "Curviness", num(50.0), pct()),
|
||||
p("strokeOptions/curvinessVariation", "Curviness Variation", num(50.0), pct()),
|
||||
p("strokeOptions/spacing", "Spacing", num(5.0), slider(0.1, 100.0, 0.1, 50.0, 1)),
|
||||
p("strokeOptions/spacingVariation", "Spacing Variation", num(50.0), pct()),
|
||||
p("strokeOptions/pathOverlap", "Path Overlap", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("strokeOptions/pathOverlapVariation", "Path Overlap Variation", num(50.0), pct()),
|
||||
p("start", "Start", num(0.0), pct()),
|
||||
p("end", "End", num(100.0), pct()),
|
||||
check("fillPathsSequentially", "Fill Paths Sequentially", true),
|
||||
|
|
@ -1488,8 +1517,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("threshold", "Image Contours: Threshold", num(50.0), pct()),
|
||||
p("preBlur", "Image Contours: Pre-Blur", num(1.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("tolerance", "Image Contours: Tolerance", num(0.01), slider(0.0, 1.0, 0.0, 0.1, 3)),
|
||||
p("renderMode", "Image Contours: Render Mode", Value::Enum(0), popup(&["All Contours", "Selected Contour"])),
|
||||
p("renderMode", "Image Contours: Render", Value::Enum(0), popup(&["All Contours", "Selected Contour"])),
|
||||
p("selectedContour", "Image Contours: Selected Contour", num(1.0), slider(1.0, 1000.0, 1.0, 20.0, 0)),
|
||||
p("shorterContoursHave", "Image Contours: Shorter Contours Have", Value::Enum(0), popup(&["Same Number of Segments", "Fewer Segments"])),
|
||||
mask_idx("path", "Mask/Path: Path", 1.0),
|
||||
p("segments", "Segments: Segments", num(32.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("length", "Segments: Length", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
|
|
@ -1696,10 +1726,31 @@ mod tests {
|
|||
// The default view contains the main cardioid (black, opaque) and escaped (coloured) points.
|
||||
assert!(a.img.data.iter().any(|p| p[3] > 0.99 && p[0] + p[1] + p[2] < 0.01));
|
||||
assert!(a.img.data.iter().any(|p| p[0] + p[1] + p[2] > 0.3));
|
||||
let t = run("ec.generate.fractal", &[("transparency", Value::Bool(true))], img);
|
||||
let t = run("ec.generate.fractal", &[("fractalColor/transparency", Value::Bool(true))], img);
|
||||
assert!(t.img.data.iter().any(|p| p[3] == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractal_palettes_and_set_choices() {
|
||||
let img = Image::new(64, 48);
|
||||
// Solid Color: only the inside of the set is drawn; Transparency flips it.
|
||||
let solid = run("ec.generate.fractal", &[("fractalColor/palette", Value::Enum(3))], img.clone());
|
||||
assert!(solid.img.data.iter().any(|p| p[3] == 0.0) && solid.img.data.iter().any(|p| p[3] > 0.99 && p[0] > 0.9));
|
||||
let flipped = run("ec.generate.fractal", &[("fractalColor/palette", Value::Enum(3)), ("fractalColor/transparency", Value::Bool(true))], img.clone());
|
||||
for (a, b) in solid.img.data.iter().zip(&flipped.img.data) {
|
||||
assert!((a[3] > 0.5) != (b[3] > 0.5) || (a[3] - 0.5).abs() < 0.49);
|
||||
}
|
||||
// Black And White: escaped pixels are pure black or white (away from supersampled edges).
|
||||
let bw = run("ec.generate.fractal", &[("fractalColor/palette", Value::Enum(2)), ("highQualitySettings/samplingFactor", num(1.0))], img.clone());
|
||||
assert!(bw.img.data.iter().all(|p| p[0] == p[1] && p[1] == p[2] && (p[0] == 0.0 || p[0] == 1.0)));
|
||||
// Mandelbrot Over Julia depends on the Julia centre.
|
||||
let o1 = run("ec.generate.fractal", &[("setChoice", Value::Enum(2)), ("julia/juliaX", num(0.3))], img.clone());
|
||||
let o2 = run("ec.generate.fractal", &[("setChoice", Value::Enum(2)), ("julia/juliaX", num(-0.2))], img.clone());
|
||||
assert_ne!(o1.img.data, o2.img.data);
|
||||
let plain = run("ec.generate.fractal", &[("julia/juliaX", num(0.3))], img);
|
||||
assert_ne!(o1.img.data, plain.img.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scribble_fills_mask_and_animates() {
|
||||
let masks = square_mask();
|
||||
|
|
@ -1725,7 +1776,7 @@ mod tests {
|
|||
let masks = square_mask();
|
||||
let env = EffectEnv { masks: &masks, ..Default::default() };
|
||||
let img = Image::new(64, 64);
|
||||
let o = run_env("ec.generate.scribble", &[("fillType", Value::Enum(3)), ("edgeWidth", num(6.0))], img, 0.5, env);
|
||||
let o = run_env("ec.generate.scribble", &[("fillType", Value::Enum(3)), ("edgeOptions/edgeWidth", num(6.0))], img, 0.5, env);
|
||||
// Outside edge: paint near the border outside, nothing in the centre.
|
||||
assert!(alpha_sum(&o.img, 26, 26, 38, 38) < 0.5);
|
||||
assert!(alpha_sum(&o.img, 8, 8, 16, 56) > 5.0);
|
||||
|
|
|
|||
|
|
@ -183,20 +183,86 @@ fn luma_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---- Color Difference Key ----
|
||||
|
||||
/// Color Difference Key's View options.
|
||||
pub const COLOR_DIFF_VIEWS: [&str; 9] = [
|
||||
"Source",
|
||||
"Uncorrected Matte Partial A",
|
||||
"Corrected Matte Partial A",
|
||||
"Uncorrected Matte Partial B",
|
||||
"Corrected Matte Partial B",
|
||||
"Uncorrected Matte",
|
||||
"Corrected Matte",
|
||||
"Final Output",
|
||||
"[A, B, Matte] Corrected, Final",
|
||||
];
|
||||
|
||||
/// Levels on a 0..1 matte value: input black / white (0..255), gamma, output black / white.
|
||||
fn matte_levels(v: f32, in_b: f32, in_w: f32, gamma: f32, out_b: f32, out_w: f32) -> f32 {
|
||||
let t = ((v - in_b / 255.0) / ((in_w - in_b) / 255.0).max(1e-4)).clamp(0.0, 1.0);
|
||||
let t = t.powf(1.0 / gamma.max(0.01));
|
||||
(out_b / 255.0 + t * (out_w - out_b) / 255.0).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// The key splits the colour difference between the key's primary channel and each of the two
|
||||
/// other channels into two partial mattes (A: first other channel, B: second); their union is
|
||||
/// the alpha matte. Each partial and the combined matte have their own levels controls.
|
||||
fn color_difference_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let k = ctx.params.color("keyColor");
|
||||
let (pi, o1, o2) = primary(k);
|
||||
let kd = (k[pi] - k[o1].max(k[o2])).max(1e-3);
|
||||
let black = ctx.params.f("blackLevel") as f32 / 255.0;
|
||||
let white = ctx.params.f("whiteLevel") as f32 / 255.0;
|
||||
let gamma = ctx.params.f("gamma").max(0.01) as f32;
|
||||
let m = matte_of(&b.img, |c, _| {
|
||||
let diff = c[pi] - c[o1].max(c[o2]);
|
||||
let a = 1.0 - (diff / kd).clamp(0.0, 1.0);
|
||||
let v = ((a - black) / (white - black).max(1e-4)).clamp(0.0, 1.0);
|
||||
v.powf(1.0 / gamma)
|
||||
let pr = ctx.params;
|
||||
let view = pr.e("view");
|
||||
if view == 0 {
|
||||
return b;
|
||||
}
|
||||
let accurate = pr.e("colorMatchingAccuracy") == 1;
|
||||
let lin = |c: [f32; 3]| if accurate { c.map(|v| srgb_to_linear(v.clamp(0.0, 1.0))) } else { c };
|
||||
let k = pr.color("keyColor");
|
||||
let key = lin([k[0], k[1], k[2]]);
|
||||
let (pi, o1, o2) = primary([key[0], key[1], key[2], 1.0]);
|
||||
let kd1 = (key[pi] - key[o1]).max(1e-3);
|
||||
let kd2 = (key[pi] - key[o2]).max(1e-3);
|
||||
let lv = |pre: &str| {
|
||||
let g = |s: &str| pr.f(&format!("{pre}{s}")) as f32;
|
||||
(g("InBlack"), g("InWhite"), g("Gamma"), g("OutBlack"), g("OutWhite"))
|
||||
};
|
||||
let pa = lv("partialA");
|
||||
let pb = lv("partialB");
|
||||
let (mb, mw, mg) = (pr.f("blackLevel") as f32, pr.f("whiteLevel") as f32, pr.f("gamma") as f32);
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let src = b.img.clone();
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (c, a0) = unpremul(src.data[i]);
|
||||
let c = lin(c);
|
||||
let ua = 1.0 - ((c[pi] - c[o1]) / kd1).clamp(0.0, 1.0);
|
||||
let ub = 1.0 - ((c[pi] - c[o2]) / kd2).clamp(0.0, 1.0);
|
||||
let ca = matte_levels(ua, pa.0, pa.1, pa.2, pa.3, pa.4);
|
||||
let cb = matte_levels(ub, pb.0, pb.1, pb.2, pb.3, pb.4);
|
||||
let um = ua.max(ub);
|
||||
let cm = matte_levels(ca.max(cb), mb, mw, mg, 0.0, 255.0);
|
||||
// The four-up view: Partial A, Partial B, Matte (all corrected) and the final output.
|
||||
let v = if view == 8 {
|
||||
let (x, y) = (i % w.max(1), i / w.max(1));
|
||||
match (x * 2 >= w, y * 2 >= h) {
|
||||
(false, false) => 2,
|
||||
(true, false) => 4,
|
||||
(false, true) => 6,
|
||||
(true, true) => 7,
|
||||
}
|
||||
} else {
|
||||
view
|
||||
};
|
||||
let grey = |m: f32| {
|
||||
let m = m * a0;
|
||||
[m, m, m, 1.0]
|
||||
};
|
||||
*px = match v {
|
||||
1 => grey(ua),
|
||||
2 => grey(ca),
|
||||
3 => grey(ub),
|
||||
4 => grey(cb),
|
||||
5 => grey(um),
|
||||
6 => grey(cm),
|
||||
_ => src.data[i].map(|v| v * cm),
|
||||
};
|
||||
});
|
||||
apply_matte(&mut b.img, &m, ctx.params.e("view"));
|
||||
b
|
||||
}
|
||||
|
||||
|
|
@ -364,10 +430,31 @@ fn screen_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---- Spill suppression ----
|
||||
|
||||
/// Faster: limit the key's primary channel to the larger of the other two. Better: remove the
|
||||
/// part of each pixel's chroma that points along the key colour's chroma (works for any key
|
||||
/// colour, not only primaries).
|
||||
fn spill_suppressor(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let k = ctx.params.color("colorToSuppress");
|
||||
let (pi, o1, o2) = primary(k);
|
||||
let amt = ctx.params.f("suppression") as f32 / 100.0;
|
||||
if ctx.params.e("colorAccuracy") == 1 {
|
||||
let kg = (k[0] + k[1] + k[2]) / 3.0;
|
||||
let kc = [k[0] - kg, k[1] - kg, k[2] - kg];
|
||||
let kn = (kc[0] * kc[0] + kc[1] * kc[1] + kc[2] * kc[2]).sqrt();
|
||||
if kn < 1e-5 {
|
||||
return b;
|
||||
}
|
||||
let dir = kc.map(|v| v / kn);
|
||||
b.img.map_straight(|c| {
|
||||
let g = (c[0] + c[1] + c[2]) / 3.0;
|
||||
let proj = (c[0] - g) * dir[0] + (c[1] - g) * dir[1] + (c[2] - g) * dir[2];
|
||||
if proj <= 0.0 {
|
||||
return c;
|
||||
}
|
||||
[0, 1, 2].map(|i| c[i] - proj * dir[i] * amt)
|
||||
});
|
||||
return b;
|
||||
}
|
||||
let (pi, o1, o2) = primary(k);
|
||||
b.img.map_straight(|mut c| {
|
||||
let spill = (c[pi] - c[o1].max(c[o2])).max(0.0);
|
||||
c[pi] -= spill * amt;
|
||||
|
|
@ -381,7 +468,7 @@ const LUMA_W: [f32; 3] = [0.2126, 0.7152, 0.0722];
|
|||
fn advanced_spill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ultra = ctx.params.e("method") == 1;
|
||||
let pi = if ultra {
|
||||
primary(ctx.params.color("keyColor")).0
|
||||
primary(ctx.params.color("ultraSettings/keyColor")).0
|
||||
} else {
|
||||
// Dominant key primary: the channel with the largest total excess over the other two.
|
||||
let ex = b
|
||||
|
|
@ -407,8 +494,8 @@ fn advanced_spill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
_ => (0, 1),
|
||||
};
|
||||
let amt = ctx.params.f("suppression") as f32 / 100.0;
|
||||
let range = ctx.params.f("spillRange") as f32 / 100.0;
|
||||
let luma = ctx.params.f("lumaCorrection") as f32 / 100.0;
|
||||
let range = ctx.params.f("ultraSettings/spillRange") as f32 / 100.0;
|
||||
let luma = ctx.params.f("ultraSettings/lumaCorrection") as f32 / 100.0;
|
||||
b.img.map_straight(|mut c| {
|
||||
let mx = c[o1].max(c[o2]);
|
||||
let avg = 0.5 * (c[o1] + c[o2]);
|
||||
|
|
@ -455,18 +542,19 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
let thin = || slider(-5.0, 5.0, -5.0, 5.0, 1);
|
||||
let feather = || slider(0.0, 100.0, 0.0, 20.0, 1);
|
||||
let b255 = || slider(0.0, 255.0, 0.0, 255.0, 0);
|
||||
let gamma = || slider(0.01, 10.0, 0.1, 3.0, 2);
|
||||
vec![
|
||||
spec(
|
||||
"ec.key.linearcolor",
|
||||
"Linear Color Key",
|
||||
"Keying",
|
||||
vec![
|
||||
p("view", "View", Value::Enum(0), view_popup()),
|
||||
p("keyColor", "Key Color", col(0.0, 0.0, 1.0), ParamUi::Color),
|
||||
p("matchColors", "Match colors", Value::Enum(0), popup(&["Using RGB", "Using Hue", "Using Chroma"])),
|
||||
p("tolerance", "Matching Tolerance", num(10.0), pct()),
|
||||
p("softness", "Matching Softness", num(10.0), pct()),
|
||||
p("keyOperation", "Key Operation", Value::Enum(0), popup(&["Key Colors", "Keep Colors"])),
|
||||
p("view", "View", Value::Enum(0), view_popup()),
|
||||
],
|
||||
linear_color_key,
|
||||
),
|
||||
|
|
@ -500,11 +588,22 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Color Difference Key",
|
||||
"Keying",
|
||||
vec![
|
||||
p("view", "View", Value::Enum(7), popup(&COLOR_DIFF_VIEWS)),
|
||||
p("keyColor", "Key Color", col(0.0, 0.0, 1.0), ParamUi::Color),
|
||||
p("blackLevel", "Matte Black", num(0.0), b255()),
|
||||
p("whiteLevel", "Matte White", num(255.0), b255()),
|
||||
p("gamma", "Matte Gamma", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 2)),
|
||||
p("view", "View", Value::Enum(0), view_popup()),
|
||||
p("colorMatchingAccuracy", "Color Matching Accuracy", Value::Enum(0), popup(&["Faster", "More Accurate"])),
|
||||
p("partialAInBlack", "Partial A In Black", num(0.0), b255()),
|
||||
p("partialAInWhite", "Partial A In White", num(255.0), b255()),
|
||||
p("partialAGamma", "Partial A Gamma", num(1.0), gamma()),
|
||||
p("partialAOutBlack", "Partial A Out Black", num(0.0), b255()),
|
||||
p("partialAOutWhite", "Partial A Out White", num(255.0), b255()),
|
||||
p("partialBInBlack", "Partial B In Black", num(0.0), b255()),
|
||||
p("partialBInWhite", "Partial B In White", num(255.0), b255()),
|
||||
p("partialBGamma", "Partial B Gamma", num(1.0), gamma()),
|
||||
p("partialBOutBlack", "Partial B Out Black", num(0.0), b255()),
|
||||
p("partialBOutWhite", "Partial B Out White", num(255.0), b255()),
|
||||
p("blackLevel", "Matte In Black", num(0.0), b255()),
|
||||
p("whiteLevel", "Matte In White", num(255.0), b255()),
|
||||
p("gamma", "Matte Gamma", num(1.0), gamma()),
|
||||
],
|
||||
color_difference_key,
|
||||
),
|
||||
|
|
@ -513,8 +612,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Color Range",
|
||||
"Keying",
|
||||
vec![
|
||||
p("colorSpace", "Color Space", Value::Enum(0), popup(&["Lab", "YUV", "RGB"])),
|
||||
p("fuzziness", "Fuzziness", num(0.0), b255()),
|
||||
p("colorSpace", "Color Space", Value::Enum(0), popup(&["Lab", "YUV", "RGB"])),
|
||||
p("minL", "Min (L, Y, R)", num(0.0), b255()),
|
||||
p("maxL", "Max (L, Y, R)", num(255.0), b255()),
|
||||
p("minA", "Min (a, U, G)", num(0.0), b255()),
|
||||
|
|
@ -563,6 +662,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Obsolete",
|
||||
vec![
|
||||
p("colorToSuppress", "Color To Suppress", col(0.0, 0.0, 1.0), ParamUi::Color),
|
||||
p("colorAccuracy", "Color Accuracy", Value::Enum(0), popup(&["Faster", "Better"])),
|
||||
p("suppression", "Suppression", num(100.0), slider(0.0, 200.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
spill_suppressor,
|
||||
|
|
@ -574,9 +674,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("method", "Method", Value::Enum(0), popup(&["Standard", "Ultra"])),
|
||||
p("suppression", "Suppression", num(100.0), slider(0.0, 200.0, 0.0, 100.0, 0)),
|
||||
p("keyColor", "Key Color", col(0.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("spillRange", "Spill Range", num(50.0), pct()),
|
||||
p("lumaCorrection", "Luma Correction", num(0.0), pct()),
|
||||
p("ultraSettings/keyColor", "Key Color", col(0.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("ultraSettings/spillRange", "Spill Range", num(50.0), pct()),
|
||||
p("ultraSettings/lumaCorrection", "Luma Correction", num(0.0), pct()),
|
||||
],
|
||||
advanced_spill,
|
||||
),
|
||||
|
|
@ -704,6 +804,42 @@ mod tests {
|
|||
assert!((out.get(0, 0)[1] - 0.3).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_difference_key_partials_views_and_levels() {
|
||||
// Source view passes through.
|
||||
let src = run("ec.key.colordifference", &[("keyColor", green()), ("view", Value::Enum(0))], green_screen());
|
||||
assert_eq!(src.get(2, 2), green_screen().get(2, 2));
|
||||
// Matte views are opaque grey; the corrected matte keys the screen.
|
||||
let m = run("ec.key.colordifference", &[("keyColor", green()), ("view", Value::Enum(6))], green_screen());
|
||||
assert!(m.get(2, 2)[0] < 0.01 && m.get(12, 2)[0] > 0.99 && m.get(2, 2)[3] == 1.0);
|
||||
// Partial A's output white caps that partial (and so the final matte where only A keeps).
|
||||
let pa = run("ec.key.colordifference", &[("keyColor", green()), ("view", Value::Enum(2)), ("partialAOutWhite", num(128.0))], green_screen());
|
||||
assert!((pa.get(12, 2)[0] - 128.0 / 255.0).abs() < 1e-3, "{:?}", pa.get(12, 2));
|
||||
// The four-up view shows a different panel per quadrant.
|
||||
let four = run("ec.key.colordifference", &[("keyColor", green()), ("view", Value::Enum(8))], green_screen());
|
||||
assert_eq!(four.get(2, 2)[3], 1.0, "matte panel is opaque");
|
||||
assert!(four.get(12, 6)[3] > 0.99, "final output keeps the foreground");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spill_suppressor_better_handles_secondary_key_colours() {
|
||||
// A yellow cast on a grey pixel: Faster only limits the primary channel, Better removes
|
||||
// the yellow chroma along the key direction.
|
||||
let img = Image::filled(1, 1, [0.7, 0.7, 0.3, 1.0]);
|
||||
let yellow = Value::Color([1.0, 1.0, 0.0, 1.0]);
|
||||
let better = run("ec.key.spill", &[("colorToSuppress", yellow), ("colorAccuracy", Value::Enum(1))], img);
|
||||
let px = better.get(0, 0);
|
||||
assert!((px[0] - px[2]).abs() < 1e-4 && (px[1] - px[2]).abs() < 1e-4, "{px:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_spill_ultra_reads_its_twirl_down() {
|
||||
// Ultra with a blue key leaves a green pixel alone.
|
||||
let img = Image::filled(1, 1, [0.3, 0.8, 0.3, 1.0]);
|
||||
let out = run("ec.key.advancedspill", &[("method", Value::Enum(1)), ("ultraSettings/keyColor", Value::Color([0.0, 0.0, 1.0, 1.0]))], img);
|
||||
assert!((out.get(0, 0)[1] - 0.8).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_cleaner_defaults_keep_opaque_pixels() {
|
||||
let img = green_screen();
|
||||
|
|
|
|||
|
|
@ -212,17 +212,42 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---------------------------------------------------------------- Unmult
|
||||
|
||||
/// Turns black into transparency: alpha becomes the brightest straight channel and colour is
|
||||
/// divided by it (premultiplied colour is unchanged).
|
||||
fn unmult(_: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Turns a black (or white) background into transparency. Alpha ramps up from the level (Black
|
||||
/// Level: on the brightest channel; White Level: on the darkest channel, mirrored) over a
|
||||
/// transition whose width is Softness (0 % = hard cutoff at the level, 100 % = the whole
|
||||
/// remaining range, which with the default levels is the classic unmultiply: alpha = brightest
|
||||
/// channel). Remove Color Matting takes the background colour back out of semi-transparent
|
||||
/// pixels; Clip HDR Results keeps the recovered colours in 0..1.
|
||||
fn unmult(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let white = pr.e("backgroundColor") == 1;
|
||||
let level = if white { pr.f("whiteLevel") } else { pr.f("blackLevel") }.clamp(0.0, 100.0) as f32 / 100.0;
|
||||
let soft = pr.f("softness").clamp(0.0, 100.0) as f32 / 100.0;
|
||||
let decon = pr.b("removeColorMatting");
|
||||
let clip = pr.b("clipHdrResults");
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let (c, a) = unpremul(*px);
|
||||
let m = c[0].max(c[1]).max(c[2]).clamp(0.0, 1.0);
|
||||
let (c, a0) = unpremul(*px);
|
||||
// Distance from the background colour, and where the ramp starts on that scale.
|
||||
let (d, start) = if white { (1.0 - c[0].min(c[1]).min(c[2]), 1.0 - level) } else { (c[0].max(c[1]).max(c[2]), level) };
|
||||
let d = d.clamp(0.0, 1.0);
|
||||
let width = soft * (1.0 - start);
|
||||
let m = if width > 1e-6 {
|
||||
((d - start) / width).clamp(0.0, 1.0)
|
||||
} else if d > start {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if m <= 1e-6 {
|
||||
*px = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
*px = premul([c[0] / m, c[1] / m, c[2] / m], a * m);
|
||||
let bg = if white { 1.0 } else { 0.0 };
|
||||
let mut out = if decon { c.map(|v| (v - bg * (1.0 - m)) / m) } else { c };
|
||||
if clip {
|
||||
out = out.map(|v| v.clamp(0.0, 1.0));
|
||||
}
|
||||
*px = premul(out, a0 * m);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
|
@ -321,7 +346,19 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
],
|
||||
inner_outer_key,
|
||||
),
|
||||
spec("ec.key.unmult", "Unmult", vec![], unmult),
|
||||
spec(
|
||||
"ec.key.unmult",
|
||||
"Unmult",
|
||||
vec![
|
||||
p("backgroundColor", "Background Color", Value::Enum(0), popup(&["Black", "White"])),
|
||||
p("blackLevel", "Black Level", num(0.0), pct()),
|
||||
p("whiteLevel", "White Level", num(100.0), pct()),
|
||||
p("softness", "Softness", num(100.0), pct()),
|
||||
p("removeColorMatting", "Remove Color Matting", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("clipHdrResults", "Clip HDR Results", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
unmult,
|
||||
),
|
||||
spec(
|
||||
"ec.key.ccsimplewireremoval",
|
||||
"CC Simple Wire Removal",
|
||||
|
|
@ -398,6 +435,31 @@ mod tests {
|
|||
assert!((p[3] - 0.5).abs() < 1e-6 && (p[0] - 0.5).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmult_white_background_levels_and_softness() {
|
||||
// White background: white goes transparent, a half-grey pixel half transparent with its
|
||||
// colour recovered as black.
|
||||
let mut img = Image::filled(3, 1, [1.0, 1.0, 1.0, 1.0]);
|
||||
img.set(1, 0, [0.5, 0.5, 0.5, 1.0]);
|
||||
img.set(2, 0, [0.0, 0.0, 0.0, 1.0]);
|
||||
let out = run_fx("ec.key.unmult", &[("backgroundColor", Value::Enum(1))], img.clone(), 0.0, EffectEnv::default());
|
||||
assert_eq!(out.img.get(0, 0), [0.0; 4]);
|
||||
let p = out.img.get(1, 0);
|
||||
assert!((p[3] - 0.5).abs() < 1e-5 && p[0].abs() < 1e-5, "{p:?}");
|
||||
assert_eq!(out.img.get(2, 0), [0.0, 0.0, 0.0, 1.0]);
|
||||
// Hard cutoff: Softness 0 with Black Level 40 % keeps a 50 % grey fully opaque, and
|
||||
// without Remove Color Matting the colour is untouched.
|
||||
let hard = run_fx(
|
||||
"ec.key.unmult",
|
||||
&[("blackLevel", num(40.0)), ("softness", num(0.0)), ("removeColorMatting", Value::Bool(false))],
|
||||
img,
|
||||
0.0,
|
||||
EffectEnv::default(),
|
||||
);
|
||||
assert_eq!(hard.img.get(1, 0), [0.5, 0.5, 0.5, 1.0]);
|
||||
assert_eq!(hard.img.get(2, 0), [0.0; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_removal_hides_a_wire() {
|
||||
let mut img = Image::filled(20, 20, [0.5, 0.5, 0.5, 1.0]);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,16 @@ pub const VIEWS: [&str; 11] = [
|
|||
"Final Result",
|
||||
];
|
||||
|
||||
/// Display names of the effect's twirl-down groups (by match id).
|
||||
pub const GROUPS: &[(&str, &str)] = &[
|
||||
("screenMatte", "Screen Matte"),
|
||||
("insideMask", "Inside Mask"),
|
||||
("outsideMask", "Outside Mask"),
|
||||
("foregroundColourCorrection", "Foreground Colour Correction"),
|
||||
("edgeColourCorrection", "Edge Colour Correction"),
|
||||
("sourceCrops", "Source Crops"),
|
||||
];
|
||||
|
||||
const REPLACE: [&str; 4] = ["None", "Source", "Hard Colour", "Soft Colour"];
|
||||
|
||||
fn pct(d: f64) -> (Value, ParamUi) {
|
||||
|
|
@ -132,14 +142,14 @@ fn key_light(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (dbias, abias) = ([dbias[0], dbias[1], dbias[2]], [abias[0], abias[1], abias[2]]);
|
||||
let s_alpha = neutralise(screen, abias);
|
||||
let sd = screen_diff(s_alpha, pi, o1, o2, bal).max(1e-3);
|
||||
let cb = pr.f("clipBlack") as f32 / 100.0;
|
||||
let cw = pr.f("clipWhite") as f32 / 100.0;
|
||||
let rollback = pr.f("clipRollback") * b.scale;
|
||||
let cb = pr.f("screenMatte/clipBlack") as f32 / 100.0;
|
||||
let cw = pr.f("screenMatte/clipWhite") as f32 / 100.0;
|
||||
let rollback = pr.f("screenMatte/clipRollback") * b.scale;
|
||||
let preblur = pr.f("screenPreblur") * b.scale;
|
||||
let sg = pr.f("screenShrinkGrow") * b.scale;
|
||||
let soft = pr.f("screenSoftness") * b.scale;
|
||||
let despot_b = pr.f("screenDespotBlack") * b.scale;
|
||||
let despot_w = pr.f("screenDespotWhite") * b.scale;
|
||||
let sg = pr.f("screenMatte/screenShrinkGrow") * b.scale;
|
||||
let soft = pr.f("screenMatte/screenSoftness") * b.scale;
|
||||
let despot_b = pr.f("screenMatte/screenDespotBlack") * b.scale;
|
||||
let despot_w = pr.f("screenMatte/screenDespotWhite") * b.scale;
|
||||
|
||||
let src = b.img.clone();
|
||||
let blurred;
|
||||
|
|
@ -187,9 +197,9 @@ fn key_light(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
m = gauss_plane(&m, soft * 0.5, soft * 0.5);
|
||||
}
|
||||
let screen_matte = m.clone();
|
||||
let inside = mask_coverage(ctx, &b, pr.e("insideMask"), pr.f("insideMaskSoftness"), pr.b("invertInsideMask"));
|
||||
let outside = mask_coverage(ctx, &b, pr.e("outsideMask"), pr.f("outsideMaskSoftness"), pr.b("invertOutsideMask"));
|
||||
let src_alpha_mode = pr.e("sourceAlpha");
|
||||
let inside = mask_coverage(ctx, &b, pr.e("insideMask/insideMask"), pr.f("insideMask/insideMaskSoftness"), pr.b("insideMask/invertInsideMask"));
|
||||
let outside = mask_coverage(ctx, &b, pr.e("outsideMask/outsideMask"), pr.f("outsideMask/outsideMaskSoftness"), pr.b("outsideMask/invertOutsideMask"));
|
||||
let src_alpha_mode = pr.e("insideMask/sourceAlpha");
|
||||
let n = src.data.len();
|
||||
let combined: Vec<f32> = (0..n)
|
||||
.into_par_iter()
|
||||
|
|
@ -208,10 +218,10 @@ fn key_light(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
a.clamp(0.0, 1.0)
|
||||
})
|
||||
.collect();
|
||||
let edges: Option<Vec<f32>> = pr.b("enableEdgeColourCorrection").then(|| {
|
||||
let grow = pr.f("edgeGrow") * b.scale;
|
||||
let hard = (pr.f("edgeHardness") / 100.0) as f32;
|
||||
let esoft = pr.f("edgeSoftness") * b.scale;
|
||||
let edges: Option<Vec<f32>> = pr.b("edgeColourCorrection/enableEdgeColourCorrection").then(|| {
|
||||
let grow = pr.f("edgeColourCorrection/edgeGrow") * b.scale;
|
||||
let hard = (pr.f("edgeColourCorrection/edgeHardness") / 100.0) as f32;
|
||||
let esoft = pr.f("edgeColourCorrection/edgeSoftness") * b.scale;
|
||||
// Edge band: where the (grown) combined matte is partial.
|
||||
let cm = Plane { w: m.w, h: m.h, data: combined.clone() };
|
||||
let band = cm.map(|a| if a > 1e-3 && a < 0.999 { 1.0 } else { 0.0 });
|
||||
|
|
@ -221,15 +231,23 @@ fn key_light(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
band.data.iter().map(|v| (v * (1.0 + hard * 4.0)).min(1.0)).collect()
|
||||
});
|
||||
let replace = pr.e("replaceMethod");
|
||||
let rc = pr.color("replaceColour");
|
||||
let in_replace = pr.e("insideReplaceMethod");
|
||||
let in_rc = pr.color("insideReplaceColour");
|
||||
let fg_cc = pr.b("enableColourCorrection");
|
||||
let (fs, fc, fbr) = (pr.f("saturation") as f32 / 100.0, pr.f("contrast") as f32 / 100.0 + 1.0, pr.f("brightness") as f32 / 100.0 + 1.0);
|
||||
let (es, ec, ebr) = (pr.f("edgeSaturation") as f32 / 100.0, pr.f("edgeContrast") as f32 / 100.0 + 1.0, pr.f("edgeBrightness") as f32 / 100.0 + 1.0);
|
||||
let replace = pr.e("screenMatte/replaceMethod");
|
||||
let rc = pr.color("screenMatte/replaceColour");
|
||||
let in_replace = pr.e("insideMask/insideReplaceMethod");
|
||||
let in_rc = pr.color("insideMask/insideReplaceColour");
|
||||
let fg_cc = pr.b("foregroundColourCorrection/enableColourCorrection");
|
||||
let (fs, fc, fbr) = (
|
||||
pr.f("foregroundColourCorrection/saturation") as f32 / 100.0,
|
||||
pr.f("foregroundColourCorrection/contrast") as f32 / 100.0 + 1.0,
|
||||
pr.f("foregroundColourCorrection/brightness") as f32 / 100.0 + 1.0,
|
||||
);
|
||||
let (es, ec, ebr) = (
|
||||
pr.f("edgeColourCorrection/edgeSaturation") as f32 / 100.0,
|
||||
pr.f("edgeColourCorrection/edgeContrast") as f32 / 100.0 + 1.0,
|
||||
pr.f("edgeColourCorrection/edgeBrightness") as f32 / 100.0 + 1.0,
|
||||
);
|
||||
let unpremultiply = pr.b("unpremultiplyResult");
|
||||
let crops = [pr.f("cropLeft"), pr.f("cropRight"), pr.f("cropTop"), pr.f("cropBottom")].map(|v| v / 100.0);
|
||||
let crops = [pr.f("sourceCrops/cropLeft"), pr.f("sourceCrops/cropRight"), pr.f("sourceCrops/cropTop"), pr.f("sourceCrops/cropBottom")].map(|v| v / 100.0);
|
||||
let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]);
|
||||
let w = b.img.width as usize;
|
||||
let (scale, off) = (b.scale, b.offset);
|
||||
|
|
@ -337,44 +355,44 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("lockBiasesTogether", "Lock Biases Together", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("screenPreblur", "Screen Pre-blur", num(0.0), slider(0.0, 100.0, 0.0, 10.0, 1)),
|
||||
// Screen Matte
|
||||
pp("clipBlack", "Clip Black", pct(0.0)),
|
||||
pp("clipWhite", "Clip White", pct(100.0)),
|
||||
p("clipRollback", "Clip Rollback", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("screenShrinkGrow", "Screen Shrink/Grow", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 1)),
|
||||
p("screenSoftness", "Screen Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("screenDespotBlack", "Screen Despot Black", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("screenDespotWhite", "Screen Despot White", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("replaceMethod", "Replace Method", Value::Enum(3), popup(&REPLACE)),
|
||||
p("replaceColour", "Replace Colour", col(0.5, 0.5, 0.5), ParamUi::Color),
|
||||
pp("screenMatte/clipBlack", "Clip Black", pct(0.0)),
|
||||
pp("screenMatte/clipWhite", "Clip White", pct(100.0)),
|
||||
p("screenMatte/clipRollback", "Clip Rollback", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("screenMatte/screenShrinkGrow", "Screen Shrink/Grow", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 1)),
|
||||
p("screenMatte/screenSoftness", "Screen Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("screenMatte/screenDespotBlack", "Screen Despot Black", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("screenMatte/screenDespotWhite", "Screen Despot White", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("screenMatte/replaceMethod", "Replace Method", Value::Enum(3), popup(&REPLACE)),
|
||||
p("screenMatte/replaceColour", "Replace Colour", col(0.5, 0.5, 0.5), ParamUi::Color),
|
||||
// Inside Mask
|
||||
p("insideMask", "Inside Mask", Value::Enum(0), mask_popup()),
|
||||
p("insideMaskSoftness", "Inside Mask Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("invertInsideMask", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("insideReplaceMethod", "Inside Replace Method", Value::Enum(1), popup(&REPLACE)),
|
||||
p("insideReplaceColour", "Inside Replace Colour", col(0.5, 0.5, 0.5), ParamUi::Color),
|
||||
p("sourceAlpha", "Source Alpha", Value::Enum(2), popup(&["Ignore", "Add to Inside Mask", "Normal"])),
|
||||
p("insideMask/insideMask", "Inside Mask", Value::Enum(0), mask_popup()),
|
||||
p("insideMask/insideMaskSoftness", "Inside Mask Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("insideMask/invertInsideMask", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("insideMask/insideReplaceMethod", "Replace Method", Value::Enum(1), popup(&REPLACE)),
|
||||
p("insideMask/insideReplaceColour", "Replace Colour", col(0.5, 0.5, 0.5), ParamUi::Color),
|
||||
p("insideMask/sourceAlpha", "Source Alpha", Value::Enum(2), popup(&["Ignore", "Add to Inside Mask", "Normal"])),
|
||||
// Outside Mask
|
||||
p("outsideMask", "Outside Mask", Value::Enum(0), mask_popup()),
|
||||
p("outsideMaskSoftness", "Outside Mask Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("invertOutsideMask", "Invert Outside Mask", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("outsideMask/outsideMask", "Outside Mask", Value::Enum(0), mask_popup()),
|
||||
p("outsideMask/outsideMaskSoftness", "Outside Mask Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("outsideMask/invertOutsideMask", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
// Foreground Colour Correction
|
||||
p("enableColourCorrection", "Enable Colour Correction", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("saturation", "Saturation", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
cc("contrast", "Contrast"),
|
||||
cc("brightness", "Brightness"),
|
||||
p("foregroundColourCorrection/enableColourCorrection", "Enable Colour Correction", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("foregroundColourCorrection/saturation", "Saturation", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
cc("foregroundColourCorrection/contrast", "Contrast"),
|
||||
cc("foregroundColourCorrection/brightness", "Brightness"),
|
||||
// Edge Colour Correction
|
||||
p("enableEdgeColourCorrection", "Enable Edge Colour Correction", Value::Bool(false), ParamUi::Checkbox),
|
||||
pp("edgeHardness", "Edge Hardness", pct(0.0)),
|
||||
p("edgeSoftness", "Edge Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("edgeGrow", "Edge Grow", num(1.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("edgeSaturation", "Edge Saturation", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
cc("edgeContrast", "Edge Contrast"),
|
||||
cc("edgeBrightness", "Edge Brightness"),
|
||||
p("edgeColourCorrection/enableEdgeColourCorrection", "Enable Edge Colour Correction", Value::Bool(false), ParamUi::Checkbox),
|
||||
pp("edgeColourCorrection/edgeHardness", "Edge Hardness", pct(0.0)),
|
||||
p("edgeColourCorrection/edgeSoftness", "Edge Softness", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("edgeColourCorrection/edgeGrow", "Edge Grow", num(1.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("edgeColourCorrection/edgeSaturation", "Saturation", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
cc("edgeColourCorrection/edgeContrast", "Contrast"),
|
||||
cc("edgeColourCorrection/edgeBrightness", "Brightness"),
|
||||
// Source Crops
|
||||
pp("cropLeft", "Left", pct(0.0)),
|
||||
pp("cropRight", "Right", pct(0.0)),
|
||||
pp("cropTop", "Top", pct(0.0)),
|
||||
pp("cropBottom", "Bottom", pct(0.0)),
|
||||
pp("sourceCrops/cropLeft", "Left", pct(0.0)),
|
||||
pp("sourceCrops/cropRight", "Right", pct(0.0)),
|
||||
pp("sourceCrops/cropTop", "Top", pct(0.0)),
|
||||
pp("sourceCrops/cropBottom", "Bottom", pct(0.0)),
|
||||
],
|
||||
render: key_light,
|
||||
gpu: false,
|
||||
|
|
@ -425,7 +443,7 @@ mod tests {
|
|||
#[test]
|
||||
fn clip_gain_views_and_masks() {
|
||||
// Clip White 60 %: the half-transparent strip becomes solid.
|
||||
let out = run_fx("ec.keying.keylight", &[("screenColour", green()), ("clipWhite", num(40.0))], plate(), 0.0, EffectEnv::default());
|
||||
let out = run_fx("ec.keying.keylight", &[("screenColour", green()), ("screenMatte/clipWhite", num(40.0))], plate(), 0.0, EffectEnv::default());
|
||||
assert!(out.img.get(27, 5)[3] > 0.99);
|
||||
// Screen Matte and Status views are grey-scale and opaque.
|
||||
let sm = run_fx("ec.keying.keylight", &[("screenColour", green()), ("view", Value::Enum(4))], plate(), 0.0, EffectEnv::default());
|
||||
|
|
@ -444,7 +462,13 @@ mod tests {
|
|||
};
|
||||
let masks = [sq(0.0, 0.0, 6.0, 6.0), sq(12.0, 12.0, 18.0, 18.0)];
|
||||
let env = EffectEnv { masks: &masks, ..Default::default() };
|
||||
let m = run_fx("ec.keying.keylight", &[("screenColour", green()), ("insideMask", Value::Enum(1)), ("outsideMask", Value::Enum(2))], plate(), 0.0, env);
|
||||
let m = run_fx(
|
||||
"ec.keying.keylight",
|
||||
&[("screenColour", green()), ("insideMask/insideMask", Value::Enum(1)), ("outsideMask/outsideMask", Value::Enum(2))],
|
||||
plate(),
|
||||
0.0,
|
||||
env,
|
||||
);
|
||||
assert!(m.img.get(2, 2)[3] > 0.99, "{:?}", m.img.get(2, 2));
|
||||
assert!(m.img.get(15, 15)[3] < 0.01);
|
||||
// Screen gain above 100 % keys more of the strip.
|
||||
|
|
@ -452,4 +476,28 @@ mod tests {
|
|||
assert!(g.img.get(27, 5)[3] < out.img.get(27, 5)[3]);
|
||||
assert_eq!(crate::lookup("Keylight (1.2)").map(|s| s.id), Some("ec.keying.keylight"));
|
||||
}
|
||||
|
||||
/// Instances saved with the flat layout get their parameters moved into the twirl-downs.
|
||||
#[test]
|
||||
fn flat_instances_move_into_twirl_downs() {
|
||||
use effectcraft_project::build::Ids;
|
||||
let spec = crate::find("ec.keying.keylight").unwrap();
|
||||
let mut next = 1;
|
||||
let mut ids = Ids(&mut next);
|
||||
let mut g = crate::instantiate(spec, &mut ids, "Key Light", [10.0, 10.0]);
|
||||
// Flatten it the way older versions saved it.
|
||||
let mut flat = vec![];
|
||||
g.walk_mut(&mut |p| flat.push(p.clone()));
|
||||
g.children = flat.into_iter().map(Into::into).collect();
|
||||
let clip = g.children.iter_mut().find_map(|c| match c {
|
||||
effectcraft_project::Node::Prop(p) if p.match_id == "clipWhite" => Some(p),
|
||||
_ => None,
|
||||
});
|
||||
clip.unwrap().value = num(42.0);
|
||||
assert!(crate::migrate::upgrade_instance(spec, &mut g, &mut ids, [10.0, 10.0]));
|
||||
assert_eq!(g.sub("screenMatte").map(|s| s.name.as_str()), Some("Screen Matte"));
|
||||
assert_eq!(g.sub("screenMatte").and_then(|s| s.get("clipWhite")).map(|p| p.value.clone()), Some(num(42.0)));
|
||||
assert_eq!(g.sub("insideMask").and_then(|s| s.get("insideMask")).map(|p| p.name.as_str()), Some("Inside Mask"));
|
||||
assert!(g.get("clipWhite").is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub mod audio_fx;
|
|||
mod blur2;
|
||||
mod blur3;
|
||||
pub mod camera_tracker;
|
||||
pub mod catalog;
|
||||
mod channel;
|
||||
mod channel2;
|
||||
mod channel3d;
|
||||
|
|
@ -28,10 +29,12 @@ mod keying;
|
|||
mod keying2;
|
||||
pub mod keylight;
|
||||
mod matte;
|
||||
pub mod migrate;
|
||||
mod misc;
|
||||
pub mod mocha_shape;
|
||||
mod noise;
|
||||
mod noise2;
|
||||
mod noise3;
|
||||
mod obsolete;
|
||||
mod ocio;
|
||||
pub mod paint;
|
||||
|
|
@ -57,11 +60,13 @@ pub mod warp_stab;
|
|||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub use color_fx::{exposure_settings, levels_clip};
|
||||
pub use color2::Curve;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
|
||||
pub use effectcraft_raster::{AuxChannels, Image};
|
||||
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS};
|
||||
|
||||
/// Effect categories in Effects & Presets order.
|
||||
pub const CATEGORIES: &[&str] = &[
|
||||
|
|
@ -151,14 +156,20 @@ impl Params {
|
|||
/// meshes and pins). Direct properties keep their match id as the key. The `i`-th child group
|
||||
/// (1-based, counting groups only) of a group with key prefix `pre` gets the prefix
|
||||
/// `{pre}#{i}/`, its properties `{pre}#{i}/{match}`, and metadata keys `@match`, `@name`
|
||||
/// (`Value::Str`), `@enabled` (`Value::Bool`) and `@uid` (`Value::Scalar`).
|
||||
/// (`Value::Str`), `@enabled` (`Value::Bool`) and `@uid` (`Value::Scalar`). Nested properties
|
||||
/// are also keyed by their match path (`group/sub/param`, the first group with each match id),
|
||||
/// which is how effects with twirl-down groups declare and read them.
|
||||
pub fn flatten_params(g: &PropGroup, eval: &mut dyn FnMut(&Property) -> Value) -> Params {
|
||||
fn walk(g: &PropGroup, pre: &str, eval: &mut dyn FnMut(&Property) -> Value, out: &mut HashMap<String, Value>) {
|
||||
fn walk(g: &PropGroup, pre: &str, mpre: &str, eval: &mut dyn FnMut(&Property) -> Value, out: &mut HashMap<String, Value>) {
|
||||
let mut gi = 0;
|
||||
for c in &g.children {
|
||||
match c {
|
||||
effectcraft_project::Node::Prop(p) => {
|
||||
out.insert(format!("{pre}{}", p.match_id), eval(p));
|
||||
let v = eval(p);
|
||||
if !mpre.is_empty() {
|
||||
out.entry(format!("{mpre}{}", p.match_id)).or_insert_with(|| v.clone());
|
||||
}
|
||||
out.insert(format!("{pre}{}", p.match_id), v);
|
||||
}
|
||||
effectcraft_project::Node::Group(sg) => {
|
||||
gi += 1;
|
||||
|
|
@ -167,13 +178,13 @@ pub fn flatten_params(g: &PropGroup, eval: &mut dyn FnMut(&Property) -> Value) -
|
|||
out.insert(format!("{sp}@name"), Value::Str(sg.name.clone()));
|
||||
out.insert(format!("{sp}@enabled"), Value::Bool(sg.enabled));
|
||||
out.insert(format!("{sp}@uid"), Value::Scalar(sg.uid as f64));
|
||||
walk(sg, &sp, eval, out);
|
||||
walk(sg, &sp, &format!("{mpre}{}/", sg.match_id), eval, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut values = HashMap::new();
|
||||
walk(g, "", eval, &mut values);
|
||||
walk(g, "", "", eval, &mut values);
|
||||
Params { values }
|
||||
}
|
||||
|
||||
|
|
@ -368,6 +379,7 @@ pub fn registry() -> &'static [EffectSpec] {
|
|||
v.extend(channel2::specs());
|
||||
v.extend(keying2::specs());
|
||||
v.extend(noise2::specs());
|
||||
v.extend(noise3::specs());
|
||||
v.extend(color3::specs());
|
||||
v.extend(obsolete::specs());
|
||||
v.extend(ocio::specs());
|
||||
|
|
@ -401,10 +413,16 @@ pub fn find(id: &str) -> Option<&'static EffectSpec> {
|
|||
|
||||
/// 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))).or_else(|| {
|
||||
// After Effects' third-party display names we register under a generic name.
|
||||
keylight::KEYLIGHT_ALIASES.iter().any(|a| a.eq_ignore_ascii_case(name_or_id)).then(|| find("ec.keying.keylight")).flatten()
|
||||
})
|
||||
find(name_or_id)
|
||||
.or_else(|| registry().iter().find(|s| s.name.eq_ignore_ascii_case(name_or_id)))
|
||||
.or_else(|| {
|
||||
// After Effects' third-party display names we register under a generic name.
|
||||
keylight::KEYLIGHT_ALIASES.iter().any(|a| a.eq_ignore_ascii_case(name_or_id)).then(|| find("ec.keying.keylight")).flatten()
|
||||
})
|
||||
.or_else(|| {
|
||||
// Display names of earlier versions.
|
||||
migrate::EFFECT_NAME_ALIASES.iter().find(|(old, _)| old.eq_ignore_ascii_case(name_or_id)).and_then(|(_, id)| find(id))
|
||||
})
|
||||
}
|
||||
|
||||
/// A parameter's default value on a layer of `layer_size` (point defaults are fractions of the
|
||||
|
|
@ -448,19 +466,143 @@ pub fn instantiate(spec: &EffectSpec, ids: &mut Ids, instance_name: &str, layer_
|
|||
}
|
||||
// Layer parameters carry a source choice (Source / Masks / Effects & Masks) unless the
|
||||
// effect declares its own (with its own default).
|
||||
// A grouped layer parameter's companion sits next to it in its group, so it flattens
|
||||
// to `group/paramSource` = `layer_source_id("group/param")`.
|
||||
let sid = layer_source_id(ps.id);
|
||||
if matches!(ps.ui, ParamUi::Layer) && !spec.params.iter().any(|q| q.id == sid) {
|
||||
let mut src = Property::new(ids.alloc(), &sid, &format!("{} Source", ps.name), Value::Enum(2)).with_ui(ParamUi::Hidden);
|
||||
let mut src = Property::new(ids.alloc(), &layer_source_id(leaf), &format!("{} Source", ps.name), Value::Enum(2)).with_ui(ParamUi::Hidden);
|
||||
src.hold_only = true;
|
||||
g.children.push(src.into());
|
||||
match path {
|
||||
Some(path) => nested_group(&mut g, ids, path).children.push(src.into()),
|
||||
None => g.children.push(src.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Display names of twirl-down parameter groups (`group/param` spec ids) by match id, for
|
||||
/// effects without their own table.
|
||||
pub const PARAM_GROUPS: &[(&str, &str)] = &[
|
||||
// Color Correction: Exposure, Levels (Individual Controls), Change to Color, Colorama,
|
||||
// Shadow/Highlight, Lumetri Color.
|
||||
("master", "Master"),
|
||||
("rgb", "RGB"),
|
||||
("red", "Red"),
|
||||
("green", "Green"),
|
||||
("blue", "Blue"),
|
||||
("alpha", "Alpha"),
|
||||
("toleranceGroup", "Tolerance"),
|
||||
("inputPhase", "Input Phase"),
|
||||
("outputCycle", "Output Cycle"),
|
||||
("moreOptions", "More Options"),
|
||||
("basicCorrection", "Basic Correction"),
|
||||
("whiteBalance", "White Balance"),
|
||||
("tone", "Tone"),
|
||||
("creative", "Creative"),
|
||||
("adjustments", "Adjustments"),
|
||||
("curves", "Curves"),
|
||||
("colorWheels", "Color Wheels"),
|
||||
("vignette", "Vignette"),
|
||||
// Camera Lens Blur.
|
||||
("irisProperties", "Iris Properties"),
|
||||
("highlight", "Highlight"),
|
||||
// Numbers.
|
||||
("format", "Format"),
|
||||
("fillAndStroke", "Fill and Stroke"),
|
||||
// Stylize: Cartoon, Roughen Edges.
|
||||
("fill", "Fill"),
|
||||
("edge", "Edge"),
|
||||
("evolutionOptions", "Evolution Options"),
|
||||
// Noise & Grain: Fractal / Turbulent Noise, Add / Match Grain, Remove Grain, Noise Alpha.
|
||||
("transform", "Transform"),
|
||||
("subSettings", "Sub Settings"),
|
||||
("tweaking", "Tweaking"),
|
||||
("channelIntensities", "Channel Intensities"),
|
||||
("channelSize", "Channel Size"),
|
||||
("color", "Color"),
|
||||
("application", "Application"),
|
||||
("animation", "Animation"),
|
||||
("noiseReductionSettings", "Noise Reduction Settings"),
|
||||
("fineTuning", "Fine Tuning"),
|
||||
("unsharpMask", "Unsharp Mask"),
|
||||
("noiseOptions", "Noise Options (Animation)"),
|
||||
// Time: Timewarp.
|
||||
("tuning", "Tuning"),
|
||||
("motionBlur", "Motion Blur"),
|
||||
// Simulation.
|
||||
("extras", "Extras"),
|
||||
("wiggle", "Wiggle"),
|
||||
("light", "Light"),
|
||||
("shading", "Shading"),
|
||||
("producer", "Producer"),
|
||||
("physics", "Physics"),
|
||||
("directionAxis", "Direction Axis"),
|
||||
("gravityVector", "Gravity Vector"),
|
||||
("particle", "Particle"),
|
||||
("hairfallMap", "Hairfall Map"),
|
||||
("hairColor", "Hair Color"),
|
||||
("bottom", "Bottom"),
|
||||
("water", "Water"),
|
||||
("lighting", "Lighting"),
|
||||
("material", "Material"),
|
||||
("shape", "Shape"),
|
||||
("force1", "Force 1"),
|
||||
("force2", "Force 2"),
|
||||
("bubbles", "Bubbles"),
|
||||
("rendering", "Rendering"),
|
||||
("heightMapControls", "Height Map Controls"),
|
||||
("simulation", "Simulation"),
|
||||
("producer1", "Producer 1"),
|
||||
("producer2", "Producer 2"),
|
||||
("xPosition", "X Position"),
|
||||
("yPosition", "Y Position"),
|
||||
("zPosition", "Z Position"),
|
||||
("xRotation", "X Rotation"),
|
||||
("yRotation", "Y Rotation"),
|
||||
("zRotation", "Z Rotation"),
|
||||
("xScale", "X Scale"),
|
||||
("yScale", "Y Scale"),
|
||||
("cameraPosition", "Camera Position"),
|
||||
("cannon", "Cannon"),
|
||||
("grid", "Grid"),
|
||||
("layerExploder", "Layer Exploder"),
|
||||
("layerMap", "Layer Map"),
|
||||
("gravity", "Gravity"),
|
||||
("repel", "Repel"),
|
||||
("wall", "Wall"),
|
||||
("persistentPropertyMapper", "Persistent Property Mapper"),
|
||||
// Keying, Matte, Obsolete (Key Light's groups are in `keylight::GROUPS`).
|
||||
("ultraSettings", "Ultra Settings"),
|
||||
("fillAndStroke", "Fill and Stroke"),
|
||||
("pathOptions", "Path Options"),
|
||||
("controlPoints", "Control Points"),
|
||||
("character", "Character"),
|
||||
("orientation", "Orientation"),
|
||||
("paragraph", "Paragraph"),
|
||||
("preview", "Preview"),
|
||||
// Generate.
|
||||
("positionsColors", "Positions & Colors"),
|
||||
("feather", "Feather"),
|
||||
("tilingOptions", "Tiling Options"),
|
||||
("polygon", "Polygon"),
|
||||
("waveMotion", "Wave Motion"),
|
||||
("waveStroke", "Stroke"),
|
||||
("coreSettings", "Core Settings"),
|
||||
("glowSettings", "Glow Settings"),
|
||||
("expertSettings", "Expert Settings"),
|
||||
("mandelbrot", "Mandelbrot"),
|
||||
("julia", "Julia"),
|
||||
("postInversionOffset", "Post-Inversion Offset"),
|
||||
("fractalColor", "Color"),
|
||||
("highQualitySettings", "High Quality Settings"),
|
||||
("edgeOptions", "Edge Options"),
|
||||
("strokeOptions", "Stroke Options"),
|
||||
];
|
||||
|
||||
/// Display names of nested parameter groups by match id.
|
||||
fn group_name(m: &str) -> &str {
|
||||
warp_stab::GROUPS.iter().chain(roto::GROUPS).find(|(id, _)| *id == m).map(|(_, n)| *n).unwrap_or(m)
|
||||
warp_stab::GROUPS.iter().chain(roto::GROUPS).chain(keylight::GROUPS).chain(PARAM_GROUPS).find(|(id, _)| *id == m).map(|(_, n)| *n).unwrap_or(m)
|
||||
}
|
||||
|
||||
/// The nested group at `path` (`borders/autoScale`) under `g`, created on first use.
|
||||
|
|
@ -526,6 +668,11 @@ pub const TIME_DEPENDENT: &[&str] = &[
|
|||
"ec.time.ccforcemotionblur",
|
||||
"ec.time.ccwidetime",
|
||||
"ec.time.pixelmotionblur",
|
||||
// Temporal Smoothing reads neighbouring frames.
|
||||
"ec.color.autolevels",
|
||||
"ec.color.autocontrast",
|
||||
"ec.color.autocolor",
|
||||
"ec.color.shadowhighlight",
|
||||
"ec.vr.digitalglitch",
|
||||
"ec.blur.camerashakedeblur",
|
||||
"ec.distort.rollingshutterrepair",
|
||||
|
|
@ -598,6 +745,73 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Registry-wide parameter sanity: unique ids and names, non-empty display names, defaults
|
||||
/// of the right kind and inside their ranges, popup defaults inside their options.
|
||||
#[test]
|
||||
fn every_param_is_well_formed() {
|
||||
let mut problems = vec![];
|
||||
let mut names = std::collections::HashSet::new();
|
||||
for s in registry() {
|
||||
if s.name.trim().is_empty() {
|
||||
problems.push(format!("{}: empty display name", s.id));
|
||||
}
|
||||
if !names.insert(s.name) {
|
||||
problems.push(format!("{}: duplicate display name {}", s.id, s.name));
|
||||
}
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
for p in &s.params {
|
||||
let at = format!("{} / {}", s.id, p.id);
|
||||
if !ids.insert(p.id) {
|
||||
problems.push(format!("{at}: duplicate id"));
|
||||
}
|
||||
if p.id.is_empty() || p.id.contains(char::is_whitespace) {
|
||||
problems.push(format!("{at}: bad id"));
|
||||
}
|
||||
if p.name.trim().is_empty() {
|
||||
problems.push(format!("{at}: empty display name"));
|
||||
}
|
||||
match (&p.ui, &p.default) {
|
||||
(ParamUi::Slider { min, max, slider_min, slider_max, .. }, d) => {
|
||||
if !(min <= max && slider_min <= slider_max) {
|
||||
problems.push(format!("{at}: inverted range"));
|
||||
}
|
||||
if !(slider_min >= min && slider_max <= max) {
|
||||
problems.push(format!("{at}: slider range {slider_min}..{slider_max} outside valid range {min}..{max}"));
|
||||
}
|
||||
match d {
|
||||
Value::Scalar(v) if v < min || v > max => problems.push(format!("{at}: default {v} outside {min}..{max}")),
|
||||
Value::Scalar(_) => {}
|
||||
d => problems.push(format!("{at}: slider default {d:?} is not a number")),
|
||||
}
|
||||
}
|
||||
(ParamUi::Popup { options }, d) => {
|
||||
if options.is_empty() || options.iter().any(|o| o.trim().is_empty()) {
|
||||
problems.push(format!("{at}: empty popup option"));
|
||||
}
|
||||
let uniq: std::collections::HashSet<_> = options.iter().collect();
|
||||
if uniq.len() != options.len() {
|
||||
problems.push(format!("{at}: duplicate popup option"));
|
||||
}
|
||||
match d {
|
||||
Value::Enum(i) if (*i as usize) < options.len() => {}
|
||||
d => problems.push(format!("{at}: popup default {d:?} not among {} options", options.len())),
|
||||
}
|
||||
}
|
||||
(ParamUi::Checkbox, d) if !matches!(d, Value::Bool(_)) => problems.push(format!("{at}: checkbox default {d:?}")),
|
||||
(ParamUi::Color, d) if !matches!(d, Value::Color(_)) => problems.push(format!("{at}: color default {d:?}")),
|
||||
(ParamUi::Point, d) if !matches!(d, Value::Vec2(_)) => problems.push(format!("{at}: point default {d:?}")),
|
||||
(ParamUi::Point3, d) if !matches!(d, Value::Vec3(_)) => problems.push(format!("{at}: 3D point default {d:?}")),
|
||||
(ParamUi::Layer, d) if !matches!(d, Value::Layer(_)) => problems.push(format!("{at}: layer default {d:?}")),
|
||||
(ParamUi::Percent | ParamUi::Angle | ParamUi::Pixels | ParamUi::Number, d) if !matches!(d, Value::Scalar(_) | Value::Vec2(_)) => {
|
||||
problems.push(format!("{at}: numeric default {d:?}"))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(problems.is_empty(), "{} problems:\n{}", problems.len(), problems.join("\n"));
|
||||
}
|
||||
|
||||
/// Every effect runs on a small image with its default parameters without panicking and
|
||||
/// keeps alpha in range.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -67,27 +67,47 @@ fn matte_choker(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Refine Soft / Hard Matte. Refine Soft Matte nests its decontamination settings in a
|
||||
/// "Decontamination" twirl-down; Refine Hard Matte lists them flat.
|
||||
fn refine(ctx: &EffectCtx, mut b: Buf, hard: bool) -> Buf {
|
||||
let r = (ctx.params.f("radius") * b.scale).round().max(1.0) as usize;
|
||||
let smooth = ctx.params.f("smooth") as f32 / 100.0;
|
||||
let feather = ctx.params.f("feather") / 100.0 * r as f64 * 0.5;
|
||||
let choke = ctx.params.f("choke") as f32 / 100.0 * 0.5;
|
||||
let decon = ctx.params.b("decontaminate");
|
||||
let decon_amt = ctx.params.f("decontaminationAmount") as f32 / 100.0;
|
||||
let invert = ctx.params.b("invert");
|
||||
let contrast = if hard { ctx.params.f("alphaContrast") as f32 / 100.0 } else { 1.0 };
|
||||
let pr = ctx.params;
|
||||
let dk = |id: &str| if hard { id.to_string() } else { format!("decontamination/{id}") };
|
||||
let r = (pr.f("radius") * b.scale).round().max(1.0) as usize;
|
||||
let smooth = pr.f("smooth") as f32 / 100.0;
|
||||
let feather = pr.f("feather") / 100.0 * r as f64 * 0.5;
|
||||
// Choke (positive shrinks; Refine Hard Matte) and Shift Edge (positive grows; Refine Soft
|
||||
// Matte; older soft instances may still carry a hidden Choke).
|
||||
let choke = (pr.f("choke") - if hard { 0.0 } else { pr.f("shiftEdge") }).clamp(-100.0, 100.0) as f32 / 100.0 * 0.5;
|
||||
let decon = pr.b("decontaminate");
|
||||
let decon_amt = pr.f(&dk("decontaminationAmount")) as f32 / 100.0;
|
||||
let extend = pr.b(&dk("extendWhereSmoothed"));
|
||||
let decon_r = r as f64 + pr.f(&dk("increaseDecontaminationRadius")).max(0.0) * b.scale;
|
||||
let view_map = pr.b(&dk("viewDecontaminationMap"));
|
||||
let invert = pr.b("invert");
|
||||
let contrast = if hard { pr.f("alphaContrast") as f32 / 100.0 } else { 1.0 + pr.f("contrast").max(0.0) as f32 / 100.0 * 3.0 };
|
||||
let edge_details = hard || pr.b("calculateEdgeDetails");
|
||||
let view_edges = !hard && pr.b("viewEdgeRegion");
|
||||
|
||||
let a = Plane::alpha(&b.img);
|
||||
let guide = Plane::from_image(&b.img, |px| effectcraft_color::luminance(px[0], px[1], px[2]));
|
||||
let eps = 1e-4 + smooth * smooth * 0.05;
|
||||
let filtered = guided_filter(&guide, &a, r, eps);
|
||||
// Only the edge band (where the matte varies within the radius) is refined.
|
||||
let hi = morph_plane(&a, r, r, true);
|
||||
let lo = morph_plane(&a, r, r, false);
|
||||
let band = |i: usize| hi.data[i] - lo.data[i] > 1e-3;
|
||||
let mut na = Plane::new(a.w, a.h);
|
||||
na.data.par_iter_mut().enumerate().for_each(|(i, v)| {
|
||||
*v = if hi.data[i] - lo.data[i] > 1e-3 { filtered.data[i].clamp(0.0, 1.0) } else { a.data[i] };
|
||||
});
|
||||
if edge_details {
|
||||
let guide = Plane::from_image(&b.img, |px| effectcraft_color::luminance(px[0], px[1], px[2]));
|
||||
let eps = 1e-4 + smooth * smooth * 0.05;
|
||||
let filtered = guided_filter(&guide, &a, r, eps);
|
||||
na.data.par_iter_mut().enumerate().for_each(|(i, v)| {
|
||||
*v = if band(i) { filtered.data[i].clamp(0.0, 1.0) } else { a.data[i] };
|
||||
});
|
||||
} else {
|
||||
na = a.clone();
|
||||
if smooth > 0.0 {
|
||||
let s = smooth as f64 * r as f64 * 0.5;
|
||||
na = gauss_plane(&na, s, s);
|
||||
}
|
||||
}
|
||||
if feather > 0.0 {
|
||||
na = gauss_plane(&na, feather, feather);
|
||||
}
|
||||
|
|
@ -105,14 +125,43 @@ fn refine(ctx: &EffectCtx, mut b: Buf, hard: bool) -> Buf {
|
|||
let v = v.clamp(0.0, 1.0);
|
||||
if invert { 1.0 - v } else { v }
|
||||
});
|
||||
if view_edges {
|
||||
// The refined matte in grey with the analysed edge band tinted.
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let v = na.data[i];
|
||||
*px = if band(i) { [0.5 + 0.5 * v, 0.5 * v, 0.5 * v, 1.0] } else { [v, v, v, 1.0] };
|
||||
});
|
||||
return b;
|
||||
}
|
||||
// Decontamination strength per pixel: partial input alpha, and (Extend Where Smoothed) where
|
||||
// the refinement made the matte partial.
|
||||
let strength = |i: usize, a0: f32| -> f32 {
|
||||
let partial = |a: f32| a > 1e-4 && a < 0.999;
|
||||
let n = na.data[i];
|
||||
let t = if partial(a0) {
|
||||
1.0 - a0
|
||||
} else if extend && partial(n) {
|
||||
1.0 - n
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
decon_amt * t
|
||||
};
|
||||
if view_map {
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let v = if decon { strength(i, px[3]) } else { 0.0 };
|
||||
*px = [v, v, v, 1.0];
|
||||
});
|
||||
return b;
|
||||
}
|
||||
if decon && decon_amt > 0.0 {
|
||||
let est = gaussian_blur(&b.img, r as f64 * 0.5, r as f64 * 0.5, true);
|
||||
let est = gaussian_blur(&b.img, decon_r * 0.5, decon_r * 0.5, true);
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (c, a0) = unpremul(*px);
|
||||
if a0 > 1e-4 && a0 < 0.999 {
|
||||
let t = strength(i, a0);
|
||||
if t > 0.0 && a0 > 1e-4 {
|
||||
let (ec, ea) = unpremul(est.data[i]);
|
||||
if ea > 1e-4 {
|
||||
let t = decon_amt * (1.0 - a0);
|
||||
*px = premul([0, 1, 2].map(|k| c[k] + (ec[k] - c[k]) * t), a0);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,21 +180,47 @@ fn refine_hard(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let refine_params = |hard: bool| {
|
||||
let mut v = vec![
|
||||
p("radius", "Additional Edge Radius", num(if hard { 3.0 } else { 10.0 }), slider(1.0, 200.0, 1.0, 50.0, 1)),
|
||||
p("smooth", "Smooth", num(if hard { 20.0 } else { 0.0 }), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("feather", "Feather", num(if hard { 0.0 } else { 10.0 }), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("choke", "Choke", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
];
|
||||
if hard {
|
||||
v.push(p("alphaContrast", "Alpha Contrast", num(300.0), slider(100.0, 1000.0, 100.0, 500.0, 0)));
|
||||
}
|
||||
v.push(p("decontaminate", "Decontaminate Edge Colors", Value::Bool(true), ParamUi::Checkbox));
|
||||
v.push(p("decontaminationAmount", "Decontamination Amount", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 0)));
|
||||
v.push(p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox));
|
||||
v
|
||||
let pct0 = || slider(0.0, 100.0, 0.0, 100.0, 0);
|
||||
let check = |id: &'static str, name: &'static str, on: bool| p(id, name, Value::Bool(on), ParamUi::Checkbox);
|
||||
let decon = |pre: &'static [&'static str; 4]| {
|
||||
vec![
|
||||
p(pre[0], "Decontamination Amount", num(100.0), pct0()),
|
||||
check(pre[1], "Extend Where Smoothed", true),
|
||||
p(pre[2], "Increase Decontamination Radius", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
check(pre[3], "View Decontamination Map", false),
|
||||
]
|
||||
};
|
||||
let radius = |d: f64| p("radius", "Additional Edge Radius", num(d), slider(1.0, 200.0, 1.0, 50.0, 1));
|
||||
let mut soft_params = vec![
|
||||
check("calculateEdgeDetails", "Calculate Edge Details", true),
|
||||
radius(10.0),
|
||||
check("viewEdgeRegion", "View Edge Region", false),
|
||||
p("smooth", "Smooth", num(0.0), pct0()),
|
||||
p("feather", "Feather", num(10.0), pct0()),
|
||||
p("contrast", "Contrast", num(0.0), pct0()),
|
||||
p("shiftEdge", "Shift Edge", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
check("decontaminate", "Decontaminate Edge Colors", true),
|
||||
];
|
||||
soft_params.extend(decon(&[
|
||||
"decontamination/decontaminationAmount",
|
||||
"decontamination/extendWhereSmoothed",
|
||||
"decontamination/increaseDecontaminationRadius",
|
||||
"decontamination/viewDecontaminationMap",
|
||||
]));
|
||||
soft_params.push(check("invert", "Invert", false));
|
||||
// Superseded by Shift Edge (opposite sign); kept so older projects render as before.
|
||||
soft_params.push(p("choke", "Choke", num(0.0), ParamUi::Hidden));
|
||||
let mut hard_params = vec![
|
||||
p("smooth", "Smooth", num(20.0), pct0()),
|
||||
p("feather", "Feather", num(0.0), pct0()),
|
||||
p("choke", "Choke", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
check("decontaminate", "Decontaminate Edge Colors", true),
|
||||
];
|
||||
hard_params.extend(decon(&["decontaminationAmount", "extendWhereSmoothed", "increaseDecontaminationRadius", "viewDecontaminationMap"]));
|
||||
// Our extras: the analysed edge band's radius, alpha contrast and inversion.
|
||||
hard_params.push(radius(3.0));
|
||||
hard_params.push(p("alphaContrast", "Alpha Contrast", num(300.0), slider(100.0, 1000.0, 100.0, 500.0, 0)));
|
||||
hard_params.push(check("invert", "Invert", false));
|
||||
let soft = || slider(0.0, 1000.0, 0.0, 100.0, 1);
|
||||
let ch = || slider(-127.0, 127.0, -127.0, 127.0, 0);
|
||||
let gl = || slider(0.0, 100.0, 0.0, 100.0, 1);
|
||||
|
|
@ -173,8 +248,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
],
|
||||
matte_choker,
|
||||
),
|
||||
spec("ec.matte.refinesoft", "Refine Soft Matte", refine_params(false), refine_soft),
|
||||
spec("ec.matte.refinehard", "Refine Hard Matte", refine_params(true), refine_hard),
|
||||
spec("ec.matte.refinesoft", "Refine Soft Matte", soft_params, refine_soft),
|
||||
spec("ec.matte.refinehard", "Refine Hard Matte", hard_params, refine_hard),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +305,38 @@ mod tests {
|
|||
assert!(out.get(12, 12)[3] > 0.99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refine_soft_matte_shift_edge_contrast_and_views() {
|
||||
let mut img = square();
|
||||
// A soft ramp on the left edge of the square.
|
||||
for y in 6..18 {
|
||||
img.set(5, y, [0.4, 0.2, 0.1, 0.5]);
|
||||
}
|
||||
let base = run("ec.matte.refinesoft", &[("feather", num(0.0))], img.clone());
|
||||
let grown = run("ec.matte.refinesoft", &[("feather", num(0.0)), ("shiftEdge", num(60.0))], img.clone());
|
||||
assert!(alpha_sum(&grown) > alpha_sum(&base) + 0.5, "{} vs {}", alpha_sum(&grown), alpha_sum(&base));
|
||||
let legacy = run("ec.matte.refinesoft", &[("feather", num(0.0)), ("choke", num(-60.0))], img.clone());
|
||||
assert_eq!(legacy.data, grown.data, "the hidden legacy Choke is Shift Edge negated");
|
||||
// Without edge detail calculation (and no smoothing) the matte only goes through the
|
||||
// level adjustments: identity at defaults.
|
||||
let plain = run(
|
||||
"ec.matte.refinesoft",
|
||||
&[("feather", num(0.0)), ("calculateEdgeDetails", Value::Bool(false)), ("decontaminate", Value::Bool(false))],
|
||||
img.clone(),
|
||||
);
|
||||
assert!(plain.data.iter().zip(&img.data).all(|(a, b)| (0..4).all(|k| (a[k] - b[k]).abs() < 1e-5)));
|
||||
// View Edge Region and View Decontamination Map are opaque diagnostic views.
|
||||
let edges = run("ec.matte.refinesoft", &[("viewEdgeRegion", Value::Bool(true))], img.clone());
|
||||
assert!(edges.data.iter().all(|p| p[3] == 1.0));
|
||||
assert!(edges.get(5, 10)[0] > edges.get(5, 10)[1], "band is tinted");
|
||||
let map = run(
|
||||
"ec.matte.refinesoft",
|
||||
&[("decontamination/viewDecontaminationMap", Value::Bool(true)), ("calculateEdgeDetails", Value::Bool(false)), ("feather", num(0.0))],
|
||||
img,
|
||||
);
|
||||
assert!(map.get(5, 10)[0] > 0.4 && map.get(12, 12)[0] == 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refine_soft_matte_keeps_interior_and_range() {
|
||||
let img = square();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,371 @@
|
|||
//! Bringing effect instances saved by earlier versions up to date with the registry.
|
||||
//!
|
||||
//! Effect instances are ordinary property groups saved with their parameters' display names
|
||||
//! and UI hints, so a project written before a parameter was renamed, regrouped, added or had
|
||||
//! its popup reordered still carries the old shape. [`upgrade_instance`] (run when a project is
|
||||
//! opened) refreshes each instance from its [`EffectSpec`]:
|
||||
//!
|
||||
//! - parameters whose id changed are found through [`PARAM_ID_ALIASES`] and renamed;
|
||||
//! - parameters moved into a twirl-down group (`group/param` ids) are moved there;
|
||||
//! - display names and UI hints (ranges, popup options) are refreshed, and popup values are
|
||||
//! remapped **by label** when the option list changed order ([`POPUP_LABEL_ALIASES`] covers
|
||||
//! renamed labels);
|
||||
//! - parameters added since are created with their defaults.
|
||||
//!
|
||||
//! Animation, expressions and values are kept.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
use effectcraft_project::{Node, ParamUi, PropGroup, Property};
|
||||
|
||||
use crate::{EffectSpec, ParamSpec, default_value, group_name, layer_source_id};
|
||||
|
||||
/// Parameter ids renamed since they were first saved: `(effect id, old id, new id)`. The new id
|
||||
/// is a spec id (it may include twirl-down group segments, `group/param`).
|
||||
pub const PARAM_ID_ALIASES: &[(&str, &str, &str)] = &[
|
||||
// Color Correction: Change to Color's single Tolerance became the Tolerance group's Hue.
|
||||
("ec.color.changetocolor", "tolerance", "toleranceGroup/hue"),
|
||||
// Roto Brush & Refine Edge: propagation controls got their own twirl-down; motion blur and
|
||||
// decontamination settings are nested under Refine Edge Matte.
|
||||
("ec.matte.rotobrush", "rotoBrushMatte/searchRadius", "rotoBrushPropagation/searchRadius"),
|
||||
("ec.matte.rotobrush", "rotoBrushMatte/motionThreshold", "rotoBrushPropagation/motionThreshold"),
|
||||
("ec.matte.rotobrush", "rotoBrushMatte/motionDamping", "rotoBrushPropagation/motionDamping"),
|
||||
("ec.matte.rotobrush", "rotoBrushMatte/viewSearchRegion", "rotoBrushPropagation/viewSearchRegion"),
|
||||
("ec.matte.rotobrush", "refineEdgeMatte/motionBlurSamples", "refineEdgeMatte/motionBlur/motionBlurSamples"),
|
||||
("ec.matte.rotobrush", "refineEdgeMatte/shutterAngle", "refineEdgeMatte/motionBlur/shutterAngle"),
|
||||
("ec.matte.rotobrush", "refineEdgeMatte/higherQuality", "refineEdgeMatte/motionBlur/higherQuality"),
|
||||
("ec.matte.rotobrush", "refineEdgeMatte/decontaminationAmount", "refineEdgeMatte/decontamination/decontaminationAmount"),
|
||||
("ec.matte.rotobrush", "refineEdgeMatte/extendWhereSmoothed", "refineEdgeMatte/decontamination/extendWhereSmoothed"),
|
||||
("ec.matte.rotobrush", "refineEdgeMatte/increaseDecontaminationRadius", "refineEdgeMatte/decontamination/increaseDecontaminationRadius"),
|
||||
("ec.matte.rotobrush", "refineEdgeMatte/viewDecontaminationMap", "refineEdgeMatte/decontamination/viewDecontaminationMap"),
|
||||
// Generate: Radio Waves' growth speed was called Velocity; Velocity now moves the wave.
|
||||
("ec.generate.radiowaves", "velocity", "waveMotion/expansion"),
|
||||
];
|
||||
|
||||
/// Popup option labels renamed since they were first saved:
|
||||
/// `(effect id, param id, old label, new label)`.
|
||||
pub const POPUP_LABEL_ALIASES: &[(&str, &str, &str, &str)] = &[
|
||||
// Blur & Sharpen, Channel.
|
||||
("ec.blur.smart", "mode", "Edge Overlay", "Overlay Edge"),
|
||||
("ec.channel.minimax", "direction", "Horizontal Only", "Just Horizontal"),
|
||||
("ec.channel.minimax", "direction", "Vertical Only", "Just Vertical"),
|
||||
("ec.channel.minimax", "operation", "Minimum then Maximum", "Minimum Then Maximum"),
|
||||
("ec.channel.minimax", "operation", "Maximum then Minimum", "Maximum Then Minimum"),
|
||||
("ec.channel.combiner", "to", "Lightness", "Lightness Only"),
|
||||
("ec.channel.combiner", "to", "Hue", "Hue Only"),
|
||||
("ec.channel.combiner", "to", "Saturation", "Saturation Only"),
|
||||
// Distort.
|
||||
("ec.distort.turbulentdisplace", "pinning", "Pin All Edges", "Pin All"),
|
||||
("ec.distort.turbulentdisplace", "pinning", "Pin Horizontal Edges", "Pin Horizontal"),
|
||||
("ec.distort.turbulentdisplace", "pinning", "Pin Vertical Edges", "Pin Vertical"),
|
||||
("ec.distort.reshape", "elasticity", "Normal", "Absolutely Normal"),
|
||||
("ec.distort.reshape", "elasticity", "Above Normal", "Above Average"),
|
||||
("ec.distort.smear", "elasticity", "Normal", "Absolutely Normal"),
|
||||
("ec.distort.smear", "elasticity", "Above Normal", "Above Average"),
|
||||
("ec.distort.liquify", "tool", "warp", "Warp"),
|
||||
("ec.distort.liquify", "tool", "turbulence", "Turbulence"),
|
||||
("ec.distort.liquify", "tool", "twirlClockwise", "Twirl Clockwise"),
|
||||
("ec.distort.liquify", "tool", "twirlCounterclockwise", "Twirl Counterclockwise"),
|
||||
("ec.distort.liquify", "tool", "pucker", "Pucker"),
|
||||
("ec.distort.liquify", "tool", "bloat", "Bloat"),
|
||||
("ec.distort.liquify", "tool", "shiftPixels", "Shift Pixels"),
|
||||
("ec.distort.liquify", "tool", "reflection", "Reflection"),
|
||||
("ec.distort.liquify", "tool", "clone", "Clone"),
|
||||
("ec.distort.liquify", "tool", "reconstruction", "Reconstruction"),
|
||||
("ec.distort.liquify", "tool", "freeze", "Freeze"),
|
||||
("ec.distort.liquify", "tool", "thaw", "Thaw"),
|
||||
// Keying, Matte, Obsolete.
|
||||
("ec.key.colordifference", "view", "Source Only", "Source"),
|
||||
("ec.key.colordifference", "view", "Matte Only", "Corrected Matte"),
|
||||
// Simulation.
|
||||
("ec.sim.carddance", "rowsColumns", "Columns Follow Rows", "Columns Follows Rows"),
|
||||
("ec.sim.particleplayground", "persistentPropertyMapper/mapRedTo", "X Velocity", "X Speed"),
|
||||
("ec.sim.particleplayground", "persistentPropertyMapper/mapRedTo", "Y Velocity", "Y Speed"),
|
||||
("ec.sim.particleplayground", "persistentPropertyMapper/mapGreenTo", "X Velocity", "X Speed"),
|
||||
("ec.sim.particleplayground", "persistentPropertyMapper/mapGreenTo", "Y Velocity", "Y Speed"),
|
||||
("ec.sim.particleplayground", "persistentPropertyMapper/mapBlueTo", "X Velocity", "X Speed"),
|
||||
("ec.sim.particleplayground", "persistentPropertyMapper/mapBlueTo", "Y Velocity", "Y Speed"),
|
||||
// Generate.
|
||||
("ec.generate.fractal", "setChoice", "Mandelbrot Over Inverse", "Mandelbrot Over Julia"),
|
||||
("ec.generate.fractal", "setChoice", "Mandelbrot Inverse Over Mandelbrot", "Mandelbrot Inverse Over Julia"),
|
||||
("ec.generate.fractal", "fractalColor/palette", "Lightness Bands", "Lightness Gradient"),
|
||||
("ec.generate.fractal", "fractalColor/palette", "Hue Bands", "Hue Wheel"),
|
||||
("ec.generate.fractal", "fractalColor/palette", "Hue/Lightness Bands", "Hue Wheel"),
|
||||
("ec.generate.fractal", "highQualitySettings/samplingMethod", "Edge Detect", "Edge Detect-Fast-May Miss Pixels"),
|
||||
("ec.generate.fractal", "highQualitySettings/samplingMethod", "Every Pixel", "Brute Force-Slow-Every Pixel"),
|
||||
("ec.generate.cellpattern", "cellPattern", "Mixed Crystals", "Mixed Crystals HQ"),
|
||||
("ec.generate.cellpattern", "cellPattern", "Dots", "Bubbles"),
|
||||
// Noise & Grain, Time.
|
||||
("ec.noise.matchgrain", "application/blendingMode", "Normal", "Add"),
|
||||
("ec.time.timedifference", "alphaChannel", "Lightness of Result", "Lightness Of Result"),
|
||||
("ec.time.timedifference", "alphaChannel", "Max of Result", "Max Of Result"),
|
||||
// Immersive Video: VR Converter's layouts.
|
||||
("ec.vr.converter", "sourceProjection", "Equirectangular", "Equirectangular 2:1"),
|
||||
("ec.vr.converter", "sourceProjection", "Cube-map 3:2", "Cube-map Pano2VR 3:2"),
|
||||
("ec.vr.converter", "sourceProjection", "Cube-map 6:1", "Cube-map GearVR 6:1"),
|
||||
("ec.vr.converter", "sourceProjection", "Sphere Map", "Sphere-map"),
|
||||
("ec.vr.converter", "sourceProjection", "2D Rectilinear", "2D Source"),
|
||||
("ec.vr.converter", "targetProjection", "Equirectangular", "Equirectangular 2:1"),
|
||||
("ec.vr.converter", "targetProjection", "Cube-map 3:2", "Cube-map Pano2VR 3:2"),
|
||||
("ec.vr.converter", "targetProjection", "Cube-map 6:1", "Cube-map GearVR 6:1"),
|
||||
("ec.vr.converter", "targetProjection", "Sphere Map", "Sphere-map"),
|
||||
("ec.vr.converter", "targetProjection", "2D Rectilinear", "2D Source"),
|
||||
];
|
||||
|
||||
/// Effect display names used by earlier versions: `(old name, effect id)`. [`crate::lookup`]
|
||||
/// resolves them so scripts and saved presets naming the old effect still work.
|
||||
pub const EFFECT_NAME_ALIASES: &[(&str, &str)] = &[
|
||||
// Keying, Matte, Obsolete.
|
||||
("mocha shape", "ec.obsolete.mochashape"),
|
||||
// Immersive Video.
|
||||
("VR Sphere To Plane", "ec.vr.spheretoplane"),
|
||||
];
|
||||
|
||||
/// The value a parameter added since a project was saved takes in that project when it must
|
||||
/// differ from the parameter's default to reproduce how the effect rendered before the parameter
|
||||
/// existed (Brightness & Contrast's Use Legacy, Photo Filter's Custom filter, …).
|
||||
pub fn legacy_default(effect: &str, param: &str) -> Option<Value> {
|
||||
Some(match (effect, param) {
|
||||
("ec.color.brightnesscontrast", "useLegacy") => Value::Bool(true),
|
||||
// Distort: earlier Ripples were symmetric, Displacement Map kept the layer bounds and
|
||||
// Magnify sampled smoothly.
|
||||
("ec.distort.ripple", "conversion") => Value::Enum(1),
|
||||
("ec.distort.displacementmap", "expandOutput") => Value::Bool(false),
|
||||
("ec.distort.magnify", "scaling") => Value::Enum(1),
|
||||
("ec.color.photofilter", "filter") => Value::Enum(crate::color_fx::PHOTO_FILTER_CUSTOM),
|
||||
("ec.color.colorbalance", "preserveLuminosity") => Value::Bool(false),
|
||||
("ec.color.leavecolor", "matchColors") => Value::Enum(1),
|
||||
("ec.color.changetocolor", "change") => Value::Enum(3),
|
||||
("ec.color.changetocolor", "changeBy") => Value::Enum(1),
|
||||
("ec.color.changetocolor", "toleranceGroup/lightness" | "toleranceGroup/saturation") => Value::Scalar(100.0),
|
||||
("ec.color.shadowhighlight", "moreOptions/blackClip" | "moreOptions/whiteClip") => Value::Scalar(0.0),
|
||||
// Generate: the shape generators used to composite Normal over the layer, with
|
||||
// independent width and height.
|
||||
("ec.generate.fourcolor" | "ec.generate.checkerboard" | "ec.generate.grid" | "ec.generate.circle", "blendingMode") => Value::Enum(1),
|
||||
("ec.generate.checkerboard" | "ec.generate.grid", "sizeFrom") => Value::Enum(2),
|
||||
// Noise & Grain: Fractal / Turbulent Noise used to clip, halve each octave's weight and
|
||||
// size, and draw the noise alone; grain was added; Remove Grain filtered channels apart.
|
||||
("ec.noise.fractal", "overflow") => Value::Enum(0),
|
||||
("ec.noise.fractal", "subSettings/subInfluence" | "subSettings/subScaling") => Value::Scalar(50.0),
|
||||
("ec.noise.fractal" | "ec.noise.turbulent", "blendingMode") => Value::Enum(crate::noise3::BLEND_NONE),
|
||||
("ec.noise.turbulent", "evolutionOptions/turbulenceFactor") => Value::Scalar(0.0),
|
||||
("ec.noise.addgrain", "application/blendingMode") => Value::Enum(2),
|
||||
("ec.noise.removegrain", "noiseReductionSettings/mode") => Value::Enum(1),
|
||||
// Audio: the Compressor's release was always manual.
|
||||
("ec.audio.compressor", "autoRelease") => Value::Bool(false),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Categories whose parameters users edit per instance (Dropdown Menu Control's items, slider
|
||||
/// ranges): their stored names and UI hints are kept.
|
||||
const USER_EDITED_CATEGORIES: &[&str] = &["Expression Controls"];
|
||||
|
||||
fn take_prop(g: &mut PropGroup, match_id: &str) -> Option<Property> {
|
||||
let i = g.children.iter().position(|c| matches!(c, Node::Prop(p) if p.match_id == match_id))?;
|
||||
match g.children.remove(i) {
|
||||
Node::Prop(p) => Some(p),
|
||||
Node::Group(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the property at spec id `id` (`a/b/param`) from `g`.
|
||||
fn take_at(g: &mut PropGroup, id: &str) -> Option<Property> {
|
||||
match id.split_once('/') {
|
||||
None => take_prop(g, id),
|
||||
Some((first, rest)) => take_at(g.sub_mut(first)?, rest),
|
||||
}
|
||||
}
|
||||
|
||||
fn prop_at<'a>(g: &'a mut PropGroup, id: &str) -> Option<&'a mut Property> {
|
||||
match id.split_once('/') {
|
||||
None => g.get_mut(id),
|
||||
Some((first, rest)) => prop_at(g.sub_mut(first)?, rest),
|
||||
}
|
||||
}
|
||||
|
||||
fn group_at<'a>(g: &'a mut PropGroup, ids: &mut Ids, path: &str) -> &'a mut PropGroup {
|
||||
let (first, rest) = match path.split_once('/') {
|
||||
Some((a, b)) => (a, Some(b)),
|
||||
None => (path, None),
|
||||
};
|
||||
if g.sub(first).is_none() {
|
||||
g.children.push(ids.group(first, group_name(first)).into());
|
||||
}
|
||||
let sub = g.sub_mut(first).expect("just added");
|
||||
match rest {
|
||||
Some(r) => group_at(sub, ids, r),
|
||||
None => sub,
|
||||
}
|
||||
}
|
||||
|
||||
/// Remap one stored popup value from `old` option labels to `new` ones.
|
||||
fn remap_enum(v: &mut Value, old: &[String], new: &[String], fx: &str, pid: &str) {
|
||||
let Value::Enum(i) = v else { return };
|
||||
let Some(label) = old.get(*i as usize) else { return };
|
||||
let label = POPUP_LABEL_ALIASES.iter().find(|(e, p, o, _)| *e == fx && *p == pid && o == label).map(|(_, _, _, n)| *n).unwrap_or(label);
|
||||
if let Some(j) = new.iter().position(|n| n == label) {
|
||||
*i = j as u32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh a stored property from its spec: name, UI hints, popup values by label.
|
||||
fn refresh(pr: &mut Property, ps: &ParamSpec, spec: &EffectSpec) {
|
||||
if USER_EDITED_CATEGORIES.contains(&spec.category) {
|
||||
return;
|
||||
}
|
||||
pr.name = ps.name.to_string();
|
||||
match (&pr.ui, &ps.ui) {
|
||||
(ParamUi::Popup { options: old }, ParamUi::Popup { options: new }) => {
|
||||
if old != new {
|
||||
let old = old.clone();
|
||||
remap_enum(&mut pr.value, &old, new, spec.id, ps.id);
|
||||
for k in &mut pr.keys {
|
||||
remap_enum(&mut k.value, &old, new, spec.id, ps.id);
|
||||
}
|
||||
}
|
||||
pr.ui = ps.ui.clone();
|
||||
}
|
||||
// A parameter that became internal (superseded by a new one) is hidden.
|
||||
(_, ParamUi::Hidden) => pr.ui = ParamUi::Hidden,
|
||||
// Same kind of value: take the spec's hints (units, ranges).
|
||||
(a, b) if std::mem::discriminant(a) == std::mem::discriminant(b) || is_numeric_ui(a) && is_numeric_ui(b) => pr.ui = ps.ui.clone(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_numeric_ui(u: &ParamUi) -> bool {
|
||||
matches!(u, ParamUi::Number | ParamUi::Slider { .. } | ParamUi::Percent | ParamUi::Angle | ParamUi::Pixels)
|
||||
}
|
||||
|
||||
/// Bring the saved instance `g` of `spec` up to date (see the module docs). `layer_size` sizes
|
||||
/// point defaults of added parameters. Returns whether anything changed.
|
||||
pub fn upgrade_instance(spec: &EffectSpec, g: &mut PropGroup, ids: &mut Ids, layer_size: [f64; 2]) -> bool {
|
||||
let before = g.clone();
|
||||
for ps in &spec.params {
|
||||
// Locate the stored property: at its spec path, under an old id, or at top level
|
||||
// before it moved into a twirl-down group.
|
||||
let found = prop_at(g, ps.id).is_some();
|
||||
if !found {
|
||||
let moved = PARAM_ID_ALIASES.iter().filter(|(e, _, new)| *e == spec.id && *new == ps.id).find_map(|(_, old, _)| take_at(g, old)).or_else(|| {
|
||||
let leaf = ps.id.rsplit('/').next().unwrap_or(ps.id);
|
||||
// Not when the top-level leaf is an old id of another parameter (Radio
|
||||
// Waves' old `velocity` is the new `waveMotion/expansion`).
|
||||
let aliased = PARAM_ID_ALIASES.iter().any(|(e, old, _)| *e == spec.id && *old == leaf);
|
||||
(leaf != ps.id && !aliased && !spec.params.iter().any(|q| q.id == leaf)).then(|| take_prop(g, leaf)).flatten()
|
||||
});
|
||||
let (path, leaf) = ps.id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, ps.id));
|
||||
let pr = match moved {
|
||||
Some(mut pr) => {
|
||||
pr.match_id = leaf.to_string();
|
||||
pr
|
||||
}
|
||||
None => {
|
||||
let v = legacy_default(spec.id, ps.id).unwrap_or_else(|| default_value(ps, layer_size));
|
||||
let mut pr = Property::new(ids.alloc(), leaf, ps.name, v).with_ui(ps.ui.clone());
|
||||
pr.spatial = matches!(ps.ui, ParamUi::Point);
|
||||
pr.hold_only |= matches!(ps.ui, ParamUi::Checkbox | ParamUi::Popup { .. } | ParamUi::Layer);
|
||||
pr
|
||||
}
|
||||
};
|
||||
match path {
|
||||
Some(path) => group_at(g, ids, path).children.push(pr.into()),
|
||||
None => g.children.push(pr.into()),
|
||||
}
|
||||
}
|
||||
if let Some(pr) = prop_at(g, ps.id) {
|
||||
refresh(pr, ps, spec);
|
||||
}
|
||||
// Layer parameters' source companions.
|
||||
let sid = layer_source_id(ps.id);
|
||||
if matches!(ps.ui, ParamUi::Layer) && !spec.params.iter().any(|q| q.id == sid) && prop_at(g, &sid).is_none() {
|
||||
let (path, leaf) = ps.id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, ps.id));
|
||||
// Companions of grouped layer parameters were once stored at top level.
|
||||
let src = take_prop(g, &sid).map(|mut p| {
|
||||
p.match_id = layer_source_id(leaf);
|
||||
p
|
||||
});
|
||||
let src = src.unwrap_or_else(|| {
|
||||
let mut src = Property::new(ids.alloc(), &layer_source_id(leaf), &format!("{} Source", ps.name), Value::Enum(2)).with_ui(ParamUi::Hidden);
|
||||
src.hold_only = true;
|
||||
src
|
||||
});
|
||||
match path {
|
||||
Some(path) => group_at(g, ids, path).children.push(src.into()),
|
||||
None => g.children.push(src.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Refresh twirl-down group names.
|
||||
fn names(g: &mut PropGroup) {
|
||||
for c in &mut g.children {
|
||||
if let Node::Group(sg) = c {
|
||||
let n = group_name(&sg.match_id);
|
||||
if n != sg.match_id {
|
||||
sg.name = n.to_string();
|
||||
}
|
||||
names(sg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !USER_EDITED_CATEGORIES.contains(&spec.category) && spec.params.iter().any(|p| p.id.contains('/')) {
|
||||
names(g);
|
||||
}
|
||||
*g != before
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{find, instantiate};
|
||||
|
||||
#[test]
|
||||
fn upgrade_is_a_no_op_on_fresh_instances() {
|
||||
let mut next = 1;
|
||||
for spec in crate::registry() {
|
||||
let mut g = instantiate(spec, &mut Ids(&mut next), spec.name, [100.0, 50.0]);
|
||||
assert!(!upgrade_instance(spec, &mut g, &mut Ids(&mut next), [100.0, 50.0]), "{} changed on upgrade", spec.id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_adds_missing_params_refreshes_names_and_remaps_popups() {
|
||||
let spec = find("ec.blur.gaussian").unwrap();
|
||||
let mut next = 1;
|
||||
let mut g = instantiate(spec, &mut Ids(&mut next), "Gaussian Blur", [100.0, 50.0]);
|
||||
// An old save: a missing parameter, a stale name, a popup stored with options in
|
||||
// another order.
|
||||
let removed = spec.params.last().unwrap().id;
|
||||
g.children.retain(|c| c.match_id() != removed);
|
||||
let popup = spec.params.iter().find(|p| matches!(p.ui, ParamUi::Popup { .. })).expect("Gaussian Blur has a popup");
|
||||
let ParamUi::Popup { options } = &popup.ui else { unreachable!() };
|
||||
{
|
||||
let pr = g.get_mut(popup.id).unwrap();
|
||||
pr.name = "Old Name".into();
|
||||
let mut rev = options.clone();
|
||||
rev.reverse();
|
||||
pr.ui = ParamUi::Popup { options: rev };
|
||||
pr.value = Value::Enum(0); // = the last option in the current order
|
||||
}
|
||||
assert!(upgrade_instance(spec, &mut g, &mut Ids(&mut next), [100.0, 50.0]));
|
||||
assert!(g.get(removed).is_some());
|
||||
let pr = g.get(popup.id).unwrap();
|
||||
assert_eq!(pr.name, popup.name);
|
||||
assert_eq!(pr.value, Value::Enum(options.len() as u32 - 1));
|
||||
assert_eq!(pr.ui, popup.ui);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_moves_params_into_twirl_down_groups() {
|
||||
let spec = find(crate::warp_stab::ID).unwrap();
|
||||
let mut next = 1;
|
||||
let mut g = instantiate(spec, &mut Ids(&mut next), "Warp Stabilizer", [100.0, 50.0]);
|
||||
// Pretend Smoothness was saved at top level with a custom value.
|
||||
let mut pr = take_at(&mut g, "stabilization/smoothness").unwrap();
|
||||
pr.value = Value::Scalar(12.0);
|
||||
g.children.push(pr.into());
|
||||
assert!(upgrade_instance(spec, &mut g, &mut Ids(&mut next), [100.0, 50.0]));
|
||||
assert!(g.get("smoothness").is_none());
|
||||
assert_eq!(prop_at(&mut g, "stabilization/smoothness").unwrap().value, Value::Scalar(12.0));
|
||||
}
|
||||
}
|
||||
|
|
@ -100,13 +100,39 @@ fn unsharp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Position (0 = Color A, 1 = Color B) in Glow's A & B gradient for brightness `l`: `loops`
|
||||
/// cycles starting at `phase` (revolutions), shaped by Color Looping (0 Sawtooth A>B,
|
||||
/// 1 Sawtooth B>A, 2 Triangle A>B>A, 3 Triangle B>A>B) and biased so `mid` maps to the middle.
|
||||
pub(crate) fn glow_ab_t(l: f32, looping: u32, loops: f32, phase: f32, mid: f32) -> f32 {
|
||||
let u = l.clamp(0.0, 1.0) * loops.max(0.0) + phase;
|
||||
let mut t = u - u.floor();
|
||||
if u > 0.0 && t <= 0.0 {
|
||||
t = 1.0;
|
||||
}
|
||||
let tri = 1.0 - (2.0 * t - 1.0).abs();
|
||||
let t = match looping {
|
||||
0 => t,
|
||||
1 => 1.0 - t,
|
||||
3 => 1.0 - tri,
|
||||
_ => tri,
|
||||
};
|
||||
let m = mid.clamp(0.01, 0.99);
|
||||
t.max(0.0).powf(0.5f32.ln() / m.ln())
|
||||
}
|
||||
|
||||
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 alpha_based = ctx.params.e("based") == 0;
|
||||
let use_colors = ctx.params.e("colors") == 1;
|
||||
let looping = ctx.params.e("colorLooping");
|
||||
let loops = ctx.params.f("colorLoops") as f32;
|
||||
let phase = (ctx.params.f("colorPhase") / 360.0) as f32;
|
||||
let mid = ctx.params.f("abMidpoint") as f32 / 100.0;
|
||||
let ca = ctx.params.color("colorA");
|
||||
let cb = ctx.params.color("colorB");
|
||||
let (kx, ky) = dims_xy(ctx.params.e("glowDimensions"));
|
||||
if !ctx.adjustment {
|
||||
b.pad((radius * 1.5).ceil() as u32 + 2);
|
||||
}
|
||||
|
|
@ -118,10 +144,11 @@ fn glow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
*px = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
let l = luminance(px[0] / a, px[1] / a, px[2] / a);
|
||||
// Glow Based On: brightness from the colours, or from the alpha channel.
|
||||
let l = if alpha_based { a } else { 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 t = glow_ab_t(l, looping, loops, phase, mid);
|
||||
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 {
|
||||
|
|
@ -129,21 +156,27 @@ fn glow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
});
|
||||
let s = (radius / 2.0).max(0.5);
|
||||
let blurred = gaussian_blur(&bright, s, s, false);
|
||||
let composite_behind = ctx.params.e("operation") == 1;
|
||||
let blurred = gaussian_blur(&bright, s * kx, s * ky, false);
|
||||
let mode = ctx.params.e("operation");
|
||||
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;
|
||||
match mode {
|
||||
// Behind: the glow shows only where the original is transparent.
|
||||
1 => {
|
||||
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];
|
||||
// None: the glow alone.
|
||||
2 => *o = [g[0], g[1], g[2], g[3]],
|
||||
// On Top: 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] + g[3] * (1.0 - o[3])).min(1.0);
|
||||
}
|
||||
o[3] = o[3].clamp(0.0, 1.0);
|
||||
});
|
||||
|
|
@ -185,12 +218,66 @@ fn drop_shadow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Invert's Channel options (After Effects order): RGB / HLS / YIQ groups, each followed by its
|
||||
/// single channels, then Alpha.
|
||||
pub const INVERT_CHANNELS: [&str; 13] =
|
||||
["RGB", "Red", "Green", "Blue", "HLS", "Hue", "Lightness", "Saturation", "YIQ", "Luminance", "In Phase Chrominance", "Quadrature Chrominance", "Alpha"];
|
||||
/// Index of "Alpha" in [`INVERT_CHANNELS`].
|
||||
pub const INVERT_ALPHA: u32 = 12;
|
||||
|
||||
fn rgb_to_yiq(c: [f32; 3]) -> [f32; 3] {
|
||||
[0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2], 0.596 * c[0] - 0.274 * c[1] - 0.322 * c[2], 0.211 * c[0] - 0.523 * c[1] + 0.312 * c[2]]
|
||||
}
|
||||
|
||||
fn yiq_to_rgb(c: [f32; 3]) -> [f32; 3] {
|
||||
[c[0] + 0.956 * c[1] + 0.621 * c[2], c[0] - 0.272 * c[1] - 0.647 * c[2], c[0] - 1.106 * c[1] + 1.703 * c[2]]
|
||||
}
|
||||
|
||||
/// Invert straight colour `s` in the colour space / channel `ch` (an [`INVERT_CHANNELS`] index
|
||||
/// other than Alpha).
|
||||
fn invert_color(ch: u32, s: [f32; 3]) -> [f32; 3] {
|
||||
match ch {
|
||||
1 => [1.0 - s[0], s[1], s[2]],
|
||||
2 => [s[0], 1.0 - s[1], s[2]],
|
||||
3 => [s[0], s[1], 1.0 - s[2]],
|
||||
4..=7 => {
|
||||
let (mut h, mut sat, mut l) = effectcraft_color::rgb_to_hsl(s[0], s[1], s[2]);
|
||||
if matches!(ch, 4 | 5) {
|
||||
h = 1.0 - h;
|
||||
}
|
||||
if matches!(ch, 4 | 6) {
|
||||
l = 1.0 - l;
|
||||
}
|
||||
if matches!(ch, 4 | 7) {
|
||||
sat = 1.0 - sat;
|
||||
}
|
||||
let (r, g, b) = effectcraft_color::hsl_to_rgb(h, sat, l);
|
||||
[r, g, b]
|
||||
}
|
||||
8..=11 => {
|
||||
// Y inverts around 1, the (signed) chrominance axes around 0.
|
||||
let mut q = rgb_to_yiq(s);
|
||||
if matches!(ch, 8 | 9) {
|
||||
q[0] = 1.0 - q[0];
|
||||
}
|
||||
if matches!(ch, 8 | 10) {
|
||||
q[1] = -q[1];
|
||||
}
|
||||
if matches!(ch, 8 | 11) {
|
||||
q[2] = -q[2];
|
||||
}
|
||||
yiq_to_rgb(q).map(|v| v.max(0.0))
|
||||
}
|
||||
_ => [1.0 - s[0], 1.0 - s[1], 1.0 - s[2]],
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if ch == INVERT_ALPHA {
|
||||
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];
|
||||
|
|
@ -203,13 +290,7 @@ fn invert(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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]],
|
||||
}
|
||||
let o = invert_color(ch, s);
|
||||
for c in 0..3 {
|
||||
px[c] = (s[c] + (o[c] - s[c]) * blend) * a;
|
||||
}
|
||||
|
|
@ -224,11 +305,38 @@ fn mosaic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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();
|
||||
if ctx.params.b("sharpColors") {
|
||||
// Each tile takes the colour of the pixel at its centre.
|
||||
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);
|
||||
}
|
||||
});
|
||||
return b;
|
||||
}
|
||||
// Each tile takes the average colour of its region.
|
||||
let (nx, ny) = ((w as f64 / cw).ceil() as usize, (h as f64 / ch).ceil() as usize);
|
||||
let mut sums = vec![[0.0f64; 5]; nx * ny];
|
||||
for y in 0..h {
|
||||
let ty = ((y as f64 / ch) as usize).min(ny - 1);
|
||||
for x in 0..w {
|
||||
let tx = ((x as f64 / cw) as usize).min(nx - 1);
|
||||
let p = src.get(x as i64, y as i64);
|
||||
let s = &mut sums[ty * nx + tx];
|
||||
for c in 0..4 {
|
||||
s[c] += p[c] as f64;
|
||||
}
|
||||
s[4] += 1.0;
|
||||
}
|
||||
}
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
let cy = ((y as f64 / ch).floor() + 0.5) * ch;
|
||||
let ty = ((y as f64 / ch) as usize).min(ny - 1);
|
||||
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);
|
||||
let tx = ((x as f64 / cw) as usize).min(nx - 1);
|
||||
let s = sums[ty * nx + tx];
|
||||
*px = [0, 1, 2, 3].map(|c| (s[c] / s[4].max(1.0)) as f32);
|
||||
}
|
||||
});
|
||||
b
|
||||
|
|
@ -251,6 +359,7 @@ fn threshold(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
fn find_edges(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let inv = ctx.params.b("invert");
|
||||
let blend = ctx.params.f("blend") as f32 / 100.0;
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
|
|
@ -263,16 +372,22 @@ fn find_edges(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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];
|
||||
*px = blend_with(*px, [v * a, v * a, v * a, a], blend);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Blend With Original: `orig` over the effect result `fx` by `k` (0 = effect only).
|
||||
fn blend_with(orig: [f32; 4], fx: [f32; 4], k: f32) -> [f32; 4] {
|
||||
[0, 1, 2, 3].map(|c| fx[c] + (orig[c] - fx[c]) * k)
|
||||
}
|
||||
|
||||
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 blend = ctx.params.f("blend") 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)| {
|
||||
|
|
@ -282,7 +397,7 @@ fn emboss(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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];
|
||||
*px = blend_with(*px, [v * al, v * al, v * al, al], blend);
|
||||
}
|
||||
});
|
||||
b
|
||||
|
|
@ -291,6 +406,8 @@ fn emboss(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
fn noise(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let amt = ctx.params.f("amount") as f32 / 100.0;
|
||||
let color = ctx.params.b("color");
|
||||
// Old saves lack the switch: clipping was always on.
|
||||
let clip = ctx.params.get("clip").is_none_or(|v| v.as_bool());
|
||||
let frame_seed = (ctx.time * 1000.0) as u32 ^ ctx.seed;
|
||||
let w = b.img.width;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
|
|
@ -306,9 +423,16 @@ fn noise(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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);
|
||||
if clip {
|
||||
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);
|
||||
} else {
|
||||
// Unclipped: values past black / white wrap around (integer overflow look).
|
||||
for (c, n) in [r, g, bl].into_iter().enumerate() {
|
||||
px[c] = ((px[c] / a + n).rem_euclid(1.0)) * a;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
|
|
@ -346,8 +470,12 @@ fn radial_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
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;
|
||||
// Both: the wipe opens in both directions from the start angle at once.
|
||||
let (a, edge) = match dir {
|
||||
1 => (360.0 - a, done * 360.0),
|
||||
2 => (a.min(360.0 - a), done * 180.0),
|
||||
_ => (a, 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;
|
||||
|
|
@ -416,6 +544,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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"])),
|
||||
// Our blur is always fully anti-aliased; the choice is kept for compatibility.
|
||||
p("antialiasing", "Antialiasing (Best Quality)", Value::Enum(0), popup(&["Low", "High"])),
|
||||
],
|
||||
radial,
|
||||
),
|
||||
|
|
@ -442,8 +572,13 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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("colorLooping", "Color Looping", Value::Enum(2), popup(&["Sawtooth A>B", "Sawtooth B>A", "Triangle A>B>A", "Triangle B>A>B"])),
|
||||
p("colorLoops", "Color Loops", num(1.0), slider(1.0, 127.0, 1.0, 10.0, 1)),
|
||||
p("colorPhase", "Color Phase", num(0.0), ParamUi::Angle),
|
||||
p("abMidpoint", "A & B Midpoint", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
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),
|
||||
p("glowDimensions", "Glow Dimensions", Value::Enum(0), dims()),
|
||||
],
|
||||
glow,
|
||||
),
|
||||
|
|
@ -454,12 +589,19 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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)),
|
||||
p("sharpColors", "Sharp Colors", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
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.findedges",
|
||||
"Find Edges",
|
||||
"Stylize",
|
||||
vec![p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox), p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0))],
|
||||
find_edges,
|
||||
),
|
||||
spec(
|
||||
"ec.stylize.emboss",
|
||||
"Emboss",
|
||||
|
|
@ -468,6 +610,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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)),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
emboss,
|
||||
),
|
||||
|
|
@ -490,7 +633,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Invert",
|
||||
"Channel",
|
||||
vec![
|
||||
p("channel", "Channel", Value::Enum(0), popup(&["RGB", "Red", "Green", "Blue", "Alpha"])),
|
||||
p("channel", "Channel", Value::Enum(0), popup(&INVERT_CHANNELS)),
|
||||
p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
invert,
|
||||
|
|
@ -502,6 +645,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
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),
|
||||
p("clip", "Clip Result Values", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
noise,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Obsolete ▸ mocha shape: renders planar-tracked roto shapes as a matte.
|
||||
//! Matte ▸ Mocha shape: renders planar-tracked roto shapes as a matte.
|
||||
//!
|
||||
//! Mocha's own clipboard / export formats are not publicly specified, so the effect reads shape
|
||||
//! data in a small documented JSON format of ours (in the hidden `shapeData` parameter, either the
|
||||
|
|
@ -184,8 +184,8 @@ fn mocha_shape(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
pub fn specs() -> Vec<EffectSpec> {
|
||||
vec![EffectSpec {
|
||||
id: "ec.obsolete.mochashape",
|
||||
name: "mocha shape",
|
||||
category: "Obsolete",
|
||||
name: "Mocha shape",
|
||||
category: "Matte",
|
||||
params: vec![
|
||||
p("blendMode", "Blend mode", Value::Enum(0), popup(&["Add", "Subtract", "Intersect", "Difference"])),
|
||||
p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
|
|
|
|||
|
|
@ -34,52 +34,178 @@ pub(crate) fn fbm(u: f32, v: f32, z: f32, seed: u32, octaves: f32) -> f32 {
|
|||
sum / norm.max(1e-6)
|
||||
}
|
||||
|
||||
// ---- Add Grain / Match Grain shared look ----
|
||||
|
||||
/// Blending Mode options of Add Grain and Match Grain.
|
||||
pub(crate) const GRAIN_BLEND_MODES: [&str; 5] = ["Film", "Multiply", "Add", "Screen", "Overlay"];
|
||||
|
||||
/// The Tweaking / Color / Application / Animation controls Add Grain and Match Grain share
|
||||
/// (twirl-down groups with the same ids in both effects).
|
||||
pub(crate) struct GrainLook {
|
||||
pub intensity: f32,
|
||||
pub channel_intensity: [f32; 3],
|
||||
pub size: f32,
|
||||
pub channel_size: [f32; 3],
|
||||
pub softness: f64,
|
||||
pub aspect: f32,
|
||||
pub mono: bool,
|
||||
pub saturation: f32,
|
||||
pub tint_amount: f32,
|
||||
pub tint: [f32; 3],
|
||||
pub mode: u32,
|
||||
/// Shadows, Midtones, Highlights weights and the tonal Midpoint.
|
||||
pub tones: [f32; 3],
|
||||
pub midpoint: f32,
|
||||
/// Grain time (frames of grain at the animation speed) and whether it interpolates.
|
||||
pub frame: f32,
|
||||
pub seed: u32,
|
||||
}
|
||||
|
||||
impl GrainLook {
|
||||
pub(crate) fn from_params(ctx: &EffectCtx) -> GrainLook {
|
||||
let pr = ctx.params;
|
||||
let ch = |pre: &str, a: &str, b: &str, c: &str| [a, b, c].map(|k| pr.get(&format!("{pre}{k}")).map(Value::as_f64).unwrap_or(1.0) as f32);
|
||||
let ft = ctx.time * 24.0 * pr.f("animation/animationSpeed");
|
||||
let smooth = pr.get("animation/animateSmoothly").is_none_or(Value::as_bool);
|
||||
let tint = pr.color("color/tintColor");
|
||||
GrainLook {
|
||||
intensity: pr.f("tweaking/intensity") as f32,
|
||||
channel_intensity: ch("tweaking/channelIntensities/", "redIntensity", "greenIntensity", "blueIntensity"),
|
||||
size: pr.f("tweaking/size").max(0.05) as f32,
|
||||
channel_size: ch("tweaking/channelSize/", "redSize", "greenSize", "blueSize"),
|
||||
softness: pr.f("tweaking/softness"),
|
||||
aspect: pr.f("tweaking/aspectRatio").max(0.05) as f32,
|
||||
mono: pr.b("color/monochromatic"),
|
||||
saturation: pr.f("color/saturation") as f32,
|
||||
tint_amount: (pr.f("color/tintAmount") as f32).clamp(0.0, 1.0),
|
||||
tint: [tint[0], tint[1], tint[2]],
|
||||
mode: pr.e("application/blendingMode"),
|
||||
tones: [pr.f("application/shadows") as f32, pr.f("application/midtones") as f32, pr.f("application/highlights") as f32],
|
||||
midpoint: pr.get("application/midpoint").map(Value::as_f64).unwrap_or(0.5).clamp(0.01, 0.99) as f32,
|
||||
frame: if smooth && ft.fract() != 0.0 { ft as f32 } else { ft.floor() as f32 },
|
||||
seed: (pr.f("animation/randomSeed") as i64 as u32) ^ ctx.seed.wrapping_mul(0x9e37),
|
||||
}
|
||||
}
|
||||
|
||||
/// Zero-mean noise planes (one, or one per channel) of grain `size` pixels (times each
|
||||
/// channel's size), softened by Softness, stretched by Aspect Ratio.
|
||||
pub(crate) fn planes(&self, w: usize, h: usize, size: f32, scale: f64, salt: u32) -> Vec<Plane> {
|
||||
let nch = if self.mono { 1 } else { 3 };
|
||||
let mut planes: Vec<Plane> = (0..nch)
|
||||
.map(|k| {
|
||||
let s = (size * if self.mono { 1.0 } else { self.channel_size[k].max(0.05) }).max(0.05);
|
||||
let mut pl = Plane::new(w, h);
|
||||
pl.data.par_chunks_mut(w.max(1)).enumerate().for_each(|(y, row)| {
|
||||
for (x, v) in row.iter_mut().enumerate() {
|
||||
*v = value_noise(x as f32 / (s * self.aspect), y as f32 / s, self.frame, self.seed.wrapping_add(k as u32 * salt)) - 0.5;
|
||||
}
|
||||
});
|
||||
pl
|
||||
})
|
||||
.collect();
|
||||
let soft = self.softness * scale;
|
||||
if soft > 0.05 {
|
||||
planes = planes.iter().map(|pl| gauss_plane(pl, soft * self.aspect as f64, soft)).collect();
|
||||
}
|
||||
planes
|
||||
}
|
||||
|
||||
/// Tonal weight of the grain at straight colour `c` (Shadows / Midtones / Highlights around
|
||||
/// the Midpoint).
|
||||
pub(crate) fn weight(&self, c: [f32; 3]) -> f32 {
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let s = 1.0 - smoothstep(0.0, self.midpoint, l);
|
||||
let hi = smoothstep(self.midpoint, 1.0, l);
|
||||
self.tones[0] * s + self.tones[1] * (1.0 - s - hi) + self.tones[2] * hi
|
||||
}
|
||||
|
||||
/// Per-channel grain from the raw plane values `g` (Saturation, Tint).
|
||||
pub(crate) fn color(&self, mut g: [f32; 3]) -> [f32; 3] {
|
||||
if !self.mono {
|
||||
let gm = (g[0] + g[1] + g[2]) / 3.0;
|
||||
g = g.map(|v| gm + (v - gm) * self.saturation);
|
||||
}
|
||||
if self.tint_amount > 0.0 {
|
||||
let l = luminance(self.tint[0], self.tint[1], self.tint[2]).max(1e-3);
|
||||
g = [0, 1, 2].map(|k| g[k] * ((1.0 - self.tint_amount) + self.tint_amount * self.tint[k] / l));
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Combine source channel `c` with signed grain `n` by the Blending Mode.
|
||||
pub(crate) fn blend(&self, c: f32, n: f32) -> f32 {
|
||||
match self.mode {
|
||||
// Multiply: scales the source (the grain is signed, so it can darken or lighten).
|
||||
1 => c * (1.0 + n),
|
||||
// Add.
|
||||
2 => c + n,
|
||||
// Screen: always lighter.
|
||||
3 => 1.0 - (1.0 - c) * (1.0 - n.abs()),
|
||||
// Overlay: full grain in the midtones, less in shadows and highlights.
|
||||
4 => c + n * 4.0 * c * (1.0 - c),
|
||||
// Film: embedded in the image, stronger in the darker tones.
|
||||
_ => c + n * (1.0 - 0.5 * c),
|
||||
}
|
||||
.max(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Grain control parameters shared by Add Grain and Match Grain, in Effect Controls order:
|
||||
/// (Tweaking, Color, Application, Animation).
|
||||
pub(crate) fn grain_params() -> (Vec<crate::ParamSpec>, Vec<crate::ParamSpec>, Vec<crate::ParamSpec>, Vec<crate::ParamSpec>) {
|
||||
let mult = || slider(0.0, 10.0, 0.0, 2.0, 2);
|
||||
let tweaking = vec![
|
||||
p("tweaking/intensity", "Intensity", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("tweaking/channelIntensities/redIntensity", "Red Intensity", num(1.0), mult()),
|
||||
p("tweaking/channelIntensities/greenIntensity", "Green Intensity", num(1.0), mult()),
|
||||
p("tweaking/channelIntensities/blueIntensity", "Blue Intensity", num(1.0), mult()),
|
||||
p("tweaking/size", "Size", num(1.0), slider(0.05, 100.0, 0.1, 10.0, 2)),
|
||||
p("tweaking/channelSize/redSize", "Red Size", num(1.0), mult()),
|
||||
p("tweaking/channelSize/greenSize", "Green Size", num(1.0), mult()),
|
||||
p("tweaking/channelSize/blueSize", "Blue Size", num(1.0), mult()),
|
||||
p("tweaking/softness", "Softness", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("tweaking/aspectRatio", "Aspect Ratio", num(1.0), slider(0.05, 20.0, 0.25, 4.0, 2)),
|
||||
];
|
||||
let color = vec![
|
||||
p("color/monochromatic", "Monochromatic", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("color/saturation", "Saturation", num(1.0), mult()),
|
||||
p("color/tintAmount", "Tint Amount", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
p("color/tintColor", "Tint Color", crate::col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
];
|
||||
let application = vec![
|
||||
p("application/blendingMode", "Blending Mode", Value::Enum(0), popup(&GRAIN_BLEND_MODES)),
|
||||
p("application/shadows", "Shadows", num(1.0), mult()),
|
||||
p("application/midtones", "Midtones", num(1.0), mult()),
|
||||
p("application/highlights", "Highlights", num(1.0), mult()),
|
||||
p("application/midpoint", "Midpoint", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
];
|
||||
let animation = vec![
|
||||
p("animation/animationSpeed", "Animation Speed", num(1.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("animation/animateSmoothly", "Animate Smoothly", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("animation/randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
];
|
||||
(tweaking, color, application, animation)
|
||||
}
|
||||
|
||||
// ---- Add Grain ----
|
||||
|
||||
fn add_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let intensity = ctx.params.f("intensity") as f32;
|
||||
if intensity <= 0.0 {
|
||||
let look = GrainLook::from_params(ctx);
|
||||
if look.intensity <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let size = (ctx.params.f("size") * b.scale).max(0.05) as f32;
|
||||
let aspect = ctx.params.f("aspectRatio").max(0.05) as f32;
|
||||
let soft = ctx.params.f("softness") * b.scale;
|
||||
let mono = ctx.params.b("monochromatic");
|
||||
let sat = ctx.params.f("saturation") as f32;
|
||||
let (ws, wm, wh) = (ctx.params.f("shadows") as f32, ctx.params.f("midtones") as f32, ctx.params.f("highlights") as f32);
|
||||
let seed = (ctx.params.f("randomSeed") as i64 as u32) ^ ctx.seed.wrapping_mul(0x9e37);
|
||||
let frame = (ctx.time * 24.0 * ctx.params.f("animationSpeed")).floor() as i64 as f32;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let nch = if mono { 1 } else { 3 };
|
||||
let mut planes: Vec<Plane> = (0..nch)
|
||||
.map(|k| {
|
||||
let mut pl = Plane::new(w, h);
|
||||
pl.data.par_chunks_mut(w.max(1)).enumerate().for_each(|(y, row)| {
|
||||
for (x, v) in row.iter_mut().enumerate() {
|
||||
let n = value_noise(x as f32 / size, y as f32 / (size * aspect), frame, seed.wrapping_add(k as u32 * 101));
|
||||
*v = (n - 0.5) * 2.5;
|
||||
}
|
||||
});
|
||||
pl
|
||||
})
|
||||
.collect();
|
||||
if soft > 0.05 {
|
||||
planes = planes.iter().map(|pl| gauss_plane(pl, soft, soft * aspect as f64)).collect();
|
||||
}
|
||||
let amp = intensity * 0.1;
|
||||
let planes = look.planes(w, h, look.size * b.scale as f32, b.scale, 101);
|
||||
let amp = look.intensity * 0.25;
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (c, a) = unpremul(*px);
|
||||
if a <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let mut g = if mono { [planes[0].data[i]; 3] } else { [planes[0].data[i], planes[1].data[i], planes[2].data[i]] };
|
||||
let gm = (g[0] + g[1] + g[2]) / 3.0;
|
||||
g = g.map(|v| gm + (v - gm) * sat);
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let s = 1.0 - smoothstep(0.0, 0.5, l);
|
||||
let hi = smoothstep(0.5, 1.0, l);
|
||||
let weight = ws * s + wm * (1.0 - s - hi) + wh * hi;
|
||||
let o = [0, 1, 2].map(|k| (c[k] + g[k] * amp * weight).max(0.0));
|
||||
let raw = if look.mono { [planes[0].data[i]; 3] } else { [planes[0].data[i], planes[1].data[i], planes[2].data[i]] };
|
||||
let g = look.color(raw);
|
||||
let weight = look.weight(c);
|
||||
let o = [0, 1, 2].map(|k| look.blend(c[k], g[k] * amp * look.channel_intensity[k] * weight));
|
||||
*px = premul(o, a);
|
||||
});
|
||||
b
|
||||
|
|
@ -208,24 +334,55 @@ fn dust_scratches(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
|
||||
fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let amt = ctx.params.f("noiseReduction") as f32;
|
||||
if amt <= 0.0 {
|
||||
let pr = ctx.params;
|
||||
let amt = pr.f("noiseReductionSettings/noiseReduction") as f32;
|
||||
let um_amount = pr.f("unsharpMask/amount") as f32 / 100.0;
|
||||
if amt <= 0.0 && um_amount <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let passes = ctx.params.f("passes").round().clamp(1.0, 4.0) as usize;
|
||||
let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize;
|
||||
let multichannel = pr.e("noiseReductionSettings/mode") == 0;
|
||||
let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0);
|
||||
let r = (2.0 * b.scale).round().max(1.0) as usize;
|
||||
let eps = (0.02 * amt).powi(2);
|
||||
let mut ch = split(&b.img);
|
||||
for c in ch.iter_mut().take(3) {
|
||||
let orig = split(&b.img);
|
||||
let mut ch = orig.clone();
|
||||
if amt > 0.0 {
|
||||
// Multichannel: all channels are guided by the luminance (correlated noise model);
|
||||
// Single Channel: each channel guides itself.
|
||||
for _ in 0..passes {
|
||||
*c = guided_filter(c, c, r, eps);
|
||||
let guide = multichannel.then(|| {
|
||||
let (w, h) = (ch[0].w, ch[0].h);
|
||||
Plane { w, h, data: (0..w * h).map(|i| luminance(ch[0].data[i], ch[1].data[i], ch[2].data[i])).collect() }
|
||||
});
|
||||
for c in ch.iter_mut().take(3) {
|
||||
*c = match &guide {
|
||||
Some(g) => guided_filter(g, c, r, eps),
|
||||
None => guided_filter(c, c, r, eps),
|
||||
};
|
||||
}
|
||||
}
|
||||
// Texture: let some of the removed fine detail back through.
|
||||
if texture > 0.0 {
|
||||
for k in 0..3 {
|
||||
ch[k] = ch[k].zip_map(&orig[k], |f, o| f + (o - f) * texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
let alpha = ch[3].clone();
|
||||
// Unsharp Mask: restore edge contrast lost to the degraining.
|
||||
if um_amount > 0.0 {
|
||||
let radius = (pr.f("unsharpMask/radius") * b.scale).max(0.1);
|
||||
let thr = pr.f("unsharpMask/threshold") as f32 / 255.0;
|
||||
for c in ch.iter_mut().take(3) {
|
||||
let low = gauss_plane(c, radius, radius);
|
||||
*c = c.zip_map(&low, |v, l| if (v - l).abs() > thr { v + (v - l) * um_amount } else { v });
|
||||
}
|
||||
}
|
||||
let alpha = orig[3].clone();
|
||||
let mut out = join(&ch);
|
||||
out.data.par_iter_mut().zip(alpha.data.par_iter()).for_each(|(p, &a)| {
|
||||
for c in 0..3 {
|
||||
p[c] = p[c].max(0.0);
|
||||
p[c] = p[c].clamp(0.0, a.max(0.0));
|
||||
}
|
||||
p[3] = a;
|
||||
});
|
||||
|
|
@ -233,85 +390,23 @@ fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
// ---- Turbulent Noise ----
|
||||
|
||||
fn overflow(v: f32, mode: u32) -> f32 {
|
||||
match mode {
|
||||
1 => (0.5 + 0.5 * (2.0 * (v - 0.5)).tanh() / 1f32.tanh()).clamp(0.0, 1.0),
|
||||
2 => {
|
||||
let t = v.rem_euclid(2.0);
|
||||
if t > 1.0 { 2.0 - t } else { t }
|
||||
}
|
||||
_ => v.clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn turbulent_noise(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let kind = ctx.params.e("fractalType");
|
||||
let invert = ctx.params.b("invert");
|
||||
let contrast = ctx.params.f("contrast") as f32 / 100.0;
|
||||
let brightness = ctx.params.f("brightness") as f32 / 100.0;
|
||||
let ov = ctx.params.e("overflow");
|
||||
let scale = (ctx.params.f("scale") * b.scale).max(1.0) as f32;
|
||||
let rot = (ctx.params.f("rotation") as f32).to_radians();
|
||||
let octaves = ctx.params.f("complexity").clamp(1.0, 20.0) as f32;
|
||||
let infl = (ctx.params.f("subInfluence") as f32 / 100.0).clamp(0.0, 1.0);
|
||||
let sub = (ctx.params.f("subScaling") as f32 / 100.0).clamp(0.1, 1.0);
|
||||
let evo = ctx.params.f("evolution") as f32 / 360.0;
|
||||
let seed = (ctx.params.f("randomSeed") as i64 as u32) ^ 0x7a3d;
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let blend = ctx.params.f("blend") as f32 / 100.0;
|
||||
let (sr, cr) = rot.sin_cos();
|
||||
let (ox, oy) = b.to_px(ctx.params.v2("offset"));
|
||||
let n_oct = octaves.ceil() as usize;
|
||||
let frac = octaves - octaves.floor();
|
||||
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 (mut u, mut v) = ((dx * cr + dy * sr) / scale, (-dx * sr + dy * cr) / scale);
|
||||
if kind == 3 {
|
||||
// Dynamic: domain-warped by a low-frequency field.
|
||||
u += (value_noise(u * 0.5, v * 0.5, evo, seed ^ 0x55) - 0.5) * 1.5;
|
||||
v += (value_noise(u * 0.5, v * 0.5, evo, seed ^ 0xaa) - 0.5) * 1.5;
|
||||
}
|
||||
let (mut sum, mut norm, mut amp, mut f) = (0.0, 0.0, 1.0, 1.0);
|
||||
for o in 0..n_oct {
|
||||
let w = if o + 1 == n_oct && frac > 0.0 { frac } else { 1.0 };
|
||||
let n = value_noise(u * f, v * f, evo + o as f32 * 5.17, seed.wrapping_add(o as u32 * 31));
|
||||
let n = match kind {
|
||||
1 => 1.0 - (n * 2.0 - 1.0).powi(2),
|
||||
2 => 1.0 - (n * 2.0 - 1.0).abs(),
|
||||
_ => n,
|
||||
};
|
||||
sum += n * amp * w;
|
||||
norm += amp * w;
|
||||
amp *= infl;
|
||||
f /= sub;
|
||||
}
|
||||
let mut val = sum / norm.max(1e-6);
|
||||
val = overflow((val - 0.5) * contrast + 0.5 + brightness, ov);
|
||||
if invert {
|
||||
val = 1.0 - val;
|
||||
}
|
||||
let g = [val * opacity, val * opacity, val * opacity, opacity];
|
||||
for c in 0..4 {
|
||||
px[c] = px[c] * blend + g[c] * (1.0 - blend);
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ---- Noise Alpha / Noise HLS ----
|
||||
|
||||
/// Per-pixel noise in 0..1 that changes smoothly with `phase` (whole units = new pattern).
|
||||
#[inline]
|
||||
fn phased(x: u32, y: u32, seed: u32, phase: f32) -> f32 {
|
||||
phased_cycle(x, y, seed, phase, 0)
|
||||
}
|
||||
|
||||
/// [`phased`] whose patterns repeat every `cycle` whole units (0 = never): Cycle Noise.
|
||||
#[inline]
|
||||
fn phased_cycle(x: u32, y: u32, seed: u32, phase: f32, cycle: i64) -> f32 {
|
||||
let k = phase.floor();
|
||||
let t = phase - k;
|
||||
let k = k as i64 as u32;
|
||||
let a = hash1(x, y, seed.wrapping_add(k.wrapping_mul(0x632b)));
|
||||
let b = hash1(x, y, seed.wrapping_add(k.wrapping_add(1).wrapping_mul(0x632b)));
|
||||
let k = k as i64;
|
||||
let wrap = |k: i64| if cycle > 0 { k.rem_euclid(cycle) } else { k } as u32;
|
||||
let a = hash1(x, y, seed.wrapping_add(wrap(k).wrapping_mul(0x632b)));
|
||||
let b = hash1(x, y, seed.wrapping_add(wrap(k + 1).wrapping_mul(0x632b)));
|
||||
lerp(a, b, t * t * (3.0 - 2.0 * t))
|
||||
}
|
||||
|
||||
|
|
@ -325,9 +420,11 @@ fn noise_alpha(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let ov = ctx.params.e("overflow");
|
||||
let seed = (ctx.params.f("randomSeed") as i64 as u32) ^ ctx.seed.wrapping_mul(0x2f1);
|
||||
let phase = ctx.params.f("noisePhase") as f32 / 360.0 + if kind >= 2 { ctx.time as f32 } else { 0.0 };
|
||||
let cycle = if ctx.params.b("noiseOptions/cycleNoise") { ctx.params.f("noiseOptions/cycle").round().max(1.0) as i64 } else { 0 };
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let mut n = if kind >= 2 { phased(x as u32, y as u32, seed, phase) } else { phased(x as u32, y as u32, seed, phase.floor()) };
|
||||
let ph = if kind >= 2 { phase } else { phase.floor() };
|
||||
let mut n = phased_cycle(x as u32, y as u32, seed, ph, cycle);
|
||||
if kind % 2 == 1 {
|
||||
n *= n;
|
||||
}
|
||||
|
|
@ -434,30 +531,16 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
let mut hls_auto = hls_common();
|
||||
hls_auto.push(p("noiseAnimationSpeed", "Noise Animation Speed", num(10.0), slider(0.0, 1000.0, 0.0, 30.0, 1)));
|
||||
vec![
|
||||
spec(
|
||||
"ec.noise.addgrain",
|
||||
"Add Grain",
|
||||
vec![
|
||||
p("intensity", "Intensity", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("size", "Size", num(1.0), slider(0.05, 100.0, 0.1, 10.0, 2)),
|
||||
p("softness", "Softness", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("aspectRatio", "Aspect Ratio", num(1.0), slider(0.05, 20.0, 0.25, 4.0, 2)),
|
||||
p("monochromatic", "Monochromatic", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("saturation", "Saturation", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("shadows", "Shadows", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("midtones", "Midtones", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("highlights", "Highlights", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
p("animationSpeed", "Animation Speed", num(1.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
],
|
||||
add_grain,
|
||||
),
|
||||
{
|
||||
let (tweaking, color, application, animation) = grain_params();
|
||||
spec("ec.noise.addgrain", "Add Grain", [tweaking, color, application, animation].into_iter().flatten().collect(), add_grain)
|
||||
},
|
||||
spec(
|
||||
"ec.noise.median",
|
||||
"Median",
|
||||
vec![
|
||||
p("radius", "Radius", num(0.0), slider(0.0, 255.0, 0.0, 50.0, 0)),
|
||||
p("operateOnAlpha", "Operate on Alpha Channel", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("operateOnAlpha", "Operate On Alpha Channel", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
median,
|
||||
),
|
||||
|
|
@ -467,7 +550,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("radius", "Radius", num(1.0), slider(0.0, 255.0, 0.0, 50.0, 0)),
|
||||
p("threshold", "Threshold", num(0.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||||
p("operateOnAlpha", "Operate on Alpha Channel", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("operateOnAlpha", "Operate On Alpha Channel", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
dust_scratches,
|
||||
),
|
||||
|
|
@ -475,33 +558,16 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.noise.removegrain",
|
||||
"Remove Grain",
|
||||
vec![
|
||||
p("noiseReduction", "Noise Reduction", num(1.0), slider(0.0, 5.0, 0.0, 5.0, 2)),
|
||||
p("passes", "Passes", num(1.0), slider(1.0, 4.0, 1.0, 4.0, 0)),
|
||||
p("noiseReductionSettings/noiseReduction", "Noise Reduction", num(1.0), slider(0.0, 5.0, 0.0, 5.0, 2)),
|
||||
p("noiseReductionSettings/passes", "Passes", num(1.0), slider(1.0, 8.0, 1.0, 4.0, 0)),
|
||||
p("noiseReductionSettings/mode", "Mode", Value::Enum(0), popup(&["Multichannel", "Single Channel"])),
|
||||
p("fineTuning/texture", "Texture", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
p("unsharpMask/amount", "Amount", num(0.0), slider(0.0, 500.0, 0.0, 200.0, 1)),
|
||||
p("unsharpMask/radius", "Radius", num(1.0), slider(0.1, 100.0, 0.1, 10.0, 1)),
|
||||
p("unsharpMask/threshold", "Threshold", num(0.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||||
],
|
||||
remove_grain,
|
||||
),
|
||||
spec(
|
||||
"ec.noise.turbulent",
|
||||
"Turbulent Noise",
|
||||
vec![
|
||||
p("fractalType", "Fractal Type", Value::Enum(1), popup(&["Basic", "Turbulent Smooth", "Turbulent Sharp", "Dynamic"])),
|
||||
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("overflow", "Overflow", Value::Enum(0), popup(&["Clip", "Soft Clamp", "Wrap Back"])),
|
||||
p("scale", "Scale", num(100.0), slider(1.0, 10000.0, 20.0, 600.0, 1)),
|
||||
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||||
p("offset", "Offset Turbulence", Value::Vec2([0.5, 0.5]), ParamUi::Point),
|
||||
p("complexity", "Complexity", num(6.0), slider(1.0, 20.0, 1.0, 10.0, 1)),
|
||||
p("subInfluence", "Sub Influence (%)", num(70.0), slider(0.0, 100.0, 25.0, 100.0, 1)),
|
||||
p("subScaling", "Sub Scaling", num(56.0), slider(10.0, 100.0, 25.0, 100.0, 1)),
|
||||
p("evolution", "Evolution", num(0.0), ParamUi::Angle),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p("blend", "Blend With Original", num(0.0), pct()),
|
||||
],
|
||||
turbulent_noise,
|
||||
),
|
||||
spec(
|
||||
"ec.noise.noisealpha",
|
||||
"Noise Alpha",
|
||||
|
|
@ -512,6 +578,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("overflow", "Overflow", Value::Enum(0), popup(&["Clip", "Wrap Back", "Wrap"])),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
p("noisePhase", "Noise Phase", num(0.0), ParamUi::Angle),
|
||||
p("noiseOptions/cycleNoise", "Cycle Noise", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("noiseOptions/cycle", "Cycle", num(1.0), slider(1.0, 100.0, 1.0, 30.0, 0)),
|
||||
],
|
||||
noise_alpha,
|
||||
),
|
||||
|
|
@ -594,9 +662,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn turbulent_noise_seeded() {
|
||||
let a = run("ec.noise.turbulent", &[("randomSeed", num(4.0))], gray(24, 16, 0.0), 0.0);
|
||||
let b = run("ec.noise.turbulent", &[("randomSeed", num(4.0))], gray(24, 16, 0.0), 0.0);
|
||||
let c = run("ec.noise.turbulent", &[("randomSeed", num(5.0))], gray(24, 16, 0.0), 0.0);
|
||||
let a = run("ec.noise.turbulent", &[("evolutionOptions/randomSeed", num(4.0))], gray(24, 16, 0.0), 0.0);
|
||||
let b = run("ec.noise.turbulent", &[("evolutionOptions/randomSeed", num(4.0))], gray(24, 16, 0.0), 0.0);
|
||||
let c = run("ec.noise.turbulent", &[("evolutionOptions/randomSeed", num(5.0))], gray(24, 16, 0.0), 0.0);
|
||||
assert_eq!(a.img, b.img);
|
||||
assert_ne!(a.img, c.img);
|
||||
assert!(a.img.data.iter().all(|p| (0.0..=1.0).contains(&p[0])));
|
||||
|
|
@ -631,7 +699,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn add_grain_keeps_flat_mean() {
|
||||
let out = run("ec.noise.addgrain", &[("intensity", num(2.0))], gray(64, 64, 0.5), 0.0);
|
||||
let out = run("ec.noise.addgrain", &[("tweaking/intensity", num(2.0)), ("application/blendingMode", Value::Enum(2))], gray(64, 64, 0.5), 0.0);
|
||||
let mean: f32 = out.img.data.iter().map(|p| p[0]).sum::<f32>() / out.img.data.len() as f32;
|
||||
assert!((mean - 0.5).abs() < 0.05, "{mean}");
|
||||
assert!(out.img.data.iter().any(|p| (p[0] - 0.5).abs() > 0.01));
|
||||
|
|
@ -645,7 +713,79 @@ mod tests {
|
|||
*p = [0.5 + n, 0.5 + n, 0.5 + n, 1.0];
|
||||
}
|
||||
let var = |im: &Image| im.data.iter().map(|p| (p[0] - 0.5).powi(2)).sum::<f32>();
|
||||
let out = run("ec.noise.removegrain", &[("noiseReduction", num(3.0))], img.clone(), 0.0);
|
||||
let out = run("ec.noise.removegrain", &[("noiseReductionSettings/noiseReduction", num(3.0))], img.clone(), 0.0);
|
||||
assert!(var(&out.img) < var(&img) * 0.5);
|
||||
// Single Channel also smooths; Texture lets detail back; Unsharp Mask restores edges.
|
||||
let single = run(
|
||||
"ec.noise.removegrain",
|
||||
&[("noiseReductionSettings/noiseReduction", num(3.0)), ("noiseReductionSettings/mode", Value::Enum(1))],
|
||||
img.clone(),
|
||||
0.0,
|
||||
);
|
||||
assert!(var(&single.img) < var(&img) * 0.5);
|
||||
let tex = run("ec.noise.removegrain", &[("noiseReductionSettings/noiseReduction", num(3.0)), ("fineTuning/texture", num(1.0))], img.clone(), 0.0);
|
||||
assert!((var(&tex.img) - var(&img)).abs() < 1e-3);
|
||||
let mut edge = gray(16, 8, 0.2);
|
||||
for y in 0..8 {
|
||||
for x in 8..16 {
|
||||
edge.set(x, y, [0.8, 0.8, 0.8, 1.0]);
|
||||
}
|
||||
}
|
||||
let sharp = run(
|
||||
"ec.noise.removegrain",
|
||||
&[("noiseReductionSettings/noiseReduction", num(0.0)), ("unsharpMask/amount", num(100.0)), ("unsharpMask/radius", num(2.0))],
|
||||
edge.clone(),
|
||||
0.0,
|
||||
);
|
||||
assert!(sharp.img.get(8, 4)[0] > 0.8 && sharp.img.get(7, 4)[0] < 0.2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_grain_blending_modes_tint_and_channel_controls() {
|
||||
let base = gray(48, 48, 0.4);
|
||||
let a = run("ec.noise.addgrain", &[("tweaking/intensity", num(3.0))], base.clone(), 0.0);
|
||||
let screen = run("ec.noise.addgrain", &[("tweaking/intensity", num(3.0)), ("application/blendingMode", Value::Enum(3))], base.clone(), 0.0);
|
||||
// Screen only ever lightens.
|
||||
assert!(screen.img.data.iter().all(|p| p[0] >= 0.4 - 1e-6));
|
||||
assert_ne!(a.img, screen.img);
|
||||
// Zero red intensity leaves red untouched.
|
||||
let nored = run("ec.noise.addgrain", &[("tweaking/intensity", num(3.0)), ("tweaking/channelIntensities/redIntensity", num(0.0))], base.clone(), 0.0);
|
||||
assert!(nored.img.data.iter().all(|p| (p[0] - 0.4).abs() < 1e-6));
|
||||
assert!(nored.img.data.iter().any(|p| (p[1] - 0.4).abs() > 1e-3));
|
||||
// A full red tint on monochromatic grain puts the grain in red only... mostly.
|
||||
let tinted = run(
|
||||
"ec.noise.addgrain",
|
||||
&[
|
||||
("tweaking/intensity", num(3.0)),
|
||||
("color/monochromatic", Value::Bool(true)),
|
||||
("color/tintAmount", num(1.0)),
|
||||
("color/tintColor", crate::col(1.0, 0.0, 0.0)),
|
||||
],
|
||||
base,
|
||||
0.0,
|
||||
);
|
||||
assert!(tinted.img.data.iter().all(|p| (p[1] - 0.4).abs() < 1e-6));
|
||||
assert!(tinted.img.data.iter().any(|p| (p[0] - 0.4).abs() > 1e-3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_alpha_cycle_repeats() {
|
||||
let at = |phase: f64| {
|
||||
run(
|
||||
"ec.noise.noisealpha",
|
||||
&[
|
||||
("amount", num(60.0)),
|
||||
("noise", Value::Enum(2)),
|
||||
("noisePhase", num(phase)),
|
||||
("noiseOptions/cycleNoise", Value::Bool(true)),
|
||||
("noiseOptions/cycle", num(3.0)),
|
||||
],
|
||||
Image::filled(12, 12, [0.25, 0.25, 0.25, 0.5]),
|
||||
0.0,
|
||||
)
|
||||
.img
|
||||
};
|
||||
assert_eq!(at(90.0), at(90.0 + 3.0 * 360.0));
|
||||
assert_ne!(at(90.0), at(90.0 + 360.0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@
|
|||
//! and synthesised onto this one), Median (Legacy) and Curl Noise (divergence-free flow
|
||||
//! distortion from the curl of a fractal noise potential).
|
||||
|
||||
use effectcraft_color::luminance;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::Image;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::generate::value_noise;
|
||||
use crate::noise::{fbm, median_image};
|
||||
use crate::util::{Plane, gauss_plane, layer_or_self, premul, smoothstep, unpremul};
|
||||
use crate::noise::{GrainLook, fbm, grain_params, median_image};
|
||||
use crate::util::{Plane, gauss_plane, layer_or_self, premul, unpremul};
|
||||
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 {
|
||||
|
|
@ -79,39 +77,15 @@ fn match_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let src_stats = grain_stats(&source);
|
||||
let comp = (ctx.params.f("compensateForExistingNoise") / 100.0) as f32;
|
||||
let own = if comp > 0.0 { grain_stats(&b.img) } else { GrainStats::default() };
|
||||
let intensity = ctx.params.f("intensity") as f32;
|
||||
let chan = [ctx.params.f("redIntensity") as f32, ctx.params.f("greenIntensity") as f32, ctx.params.f("blueIntensity") as f32];
|
||||
let look = GrainLook::from_params(ctx);
|
||||
let amp: [f32; 3] = [0, 1, 2].map(|k| {
|
||||
let s = src_stats.std[k];
|
||||
let e = own.std[k] * comp;
|
||||
(s * s - e * e).max(0.0).sqrt() * intensity * chan[k]
|
||||
(s * s - e * e).max(0.0).sqrt() * look.intensity * look.channel_intensity[k]
|
||||
});
|
||||
let size = (src_stats.size.max(0.5) * ctx.params.f("size") as f32 * b.scale as f32).max(0.05);
|
||||
let aspect = ctx.params.f("aspectRatio").max(0.05) as f32;
|
||||
let soft = ctx.params.f("softness") * b.scale;
|
||||
let mono = ctx.params.b("monochromatic");
|
||||
let sat = ctx.params.f("saturation") as f32;
|
||||
let blend_mode = ctx.params.e("blendingMode");
|
||||
let (ws, wm, wh) = (ctx.params.f("shadows") as f32, ctx.params.f("midtones") as f32, ctx.params.f("highlights") as f32);
|
||||
let seed = (ctx.params.f("randomSeed") as i64 as u32) ^ ctx.seed.wrapping_mul(0x9e37);
|
||||
let ft = ctx.time * 24.0 * ctx.params.f("animationSpeed");
|
||||
let frame = if ctx.params.b("animateSmoothly") { ft as f32 } else { ft.floor() as f32 };
|
||||
let size = (src_stats.size.max(0.5) * look.size * b.scale as f32).max(0.05);
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let nch = if mono { 1 } else { 3 };
|
||||
let mut planes: Vec<Plane> = (0..nch)
|
||||
.map(|k| {
|
||||
let mut pl = Plane::new(w, h);
|
||||
pl.data.par_chunks_mut(w.max(1)).enumerate().for_each(|(y, row)| {
|
||||
for (x, v) in row.iter_mut().enumerate() {
|
||||
*v = value_noise(x as f32 / size, y as f32 / (size * aspect), frame, seed.wrapping_add(k as u32 * 131)) - 0.5;
|
||||
}
|
||||
});
|
||||
pl
|
||||
})
|
||||
.collect();
|
||||
if soft > 0.05 {
|
||||
planes = planes.iter().map(|pl| gauss_plane(pl, soft, soft * aspect as f64)).collect();
|
||||
}
|
||||
let mut planes = look.planes(w, h, size, b.scale, 131);
|
||||
// Normalise each synthetic plane to unit standard deviation so the measured stats transfer.
|
||||
for pl in planes.iter_mut() {
|
||||
let n = pl.data.len().max(1) as f64;
|
||||
|
|
@ -122,13 +96,9 @@ fn match_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (c, a) = unpremul(*px);
|
||||
let mut g = if mono { [planes[0].data[i]; 3] } else { [planes[0].data[i], planes[1].data[i], planes[2].data[i]] };
|
||||
let gm = (g[0] + g[1] + g[2]) / 3.0;
|
||||
g = g.map(|v| gm + (v - gm) * sat);
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let s = 1.0 - smoothstep(0.0, 0.5, l);
|
||||
let hi = smoothstep(0.5, 1.0, l);
|
||||
let weight = ws * s + wm * (1.0 - s - hi) + wh * hi;
|
||||
let raw = if look.mono { [planes[0].data[i]; 3] } else { [planes[0].data[i], planes[1].data[i], planes[2].data[i]] };
|
||||
let g = look.color(raw);
|
||||
let weight = look.weight(c);
|
||||
match view {
|
||||
1 => {
|
||||
let o = [0, 1, 2].map(|k| (0.5 + g[k] * amp[k]).clamp(0.0, 1.0));
|
||||
|
|
@ -139,15 +109,7 @@ fn match_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if a <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let o = [0, 1, 2].map(|k| {
|
||||
let n = g[k] * amp[k] * weight;
|
||||
let v = match blend_mode {
|
||||
1 => c[k] * (1.0 + n * 2.0),
|
||||
2 => 1.0 - (1.0 - c[k]) * (1.0 - n * 2.0),
|
||||
_ => c[k] + n,
|
||||
};
|
||||
v.max(0.0)
|
||||
});
|
||||
let o = [0, 1, 2].map(|k| look.blend(c[k], g[k] * amp[k] * weight));
|
||||
*px = premul(o, a);
|
||||
}
|
||||
}
|
||||
|
|
@ -246,7 +208,7 @@ fn curl_noise(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let mult = || slider(0.0, 10.0, 0.0, 2.0, 2);
|
||||
let (tweaking, color, application, animation) = grain_params();
|
||||
vec![
|
||||
spec(
|
||||
"ec.noise.matchgrain",
|
||||
|
|
@ -256,23 +218,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("noiseSourceLayer", "Noise Source Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("noiseSourceLayerSource", "Noise Source Layer Source", Value::Enum(0), ParamUi::Hidden),
|
||||
p("compensateForExistingNoise", "Compensate for Existing Noise", num(0.0), pct()),
|
||||
p("intensity", "Intensity", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("size", "Size", num(1.0), slider(0.05, 100.0, 0.1, 10.0, 2)),
|
||||
p("softness", "Softness", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("aspectRatio", "Aspect Ratio", num(1.0), slider(0.05, 20.0, 0.25, 4.0, 2)),
|
||||
p("redIntensity", "Red Intensity", num(1.0), mult()),
|
||||
p("greenIntensity", "Green Intensity", num(1.0), mult()),
|
||||
p("blueIntensity", "Blue Intensity", num(1.0), mult()),
|
||||
p("monochromatic", "Monochromatic", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("saturation", "Saturation", num(1.0), mult()),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(0), popup(&["Normal", "Multiply", "Screen"])),
|
||||
p("shadows", "Shadows", num(1.0), mult()),
|
||||
p("midtones", "Midtones", num(1.0), mult()),
|
||||
p("highlights", "Highlights", num(1.0), mult()),
|
||||
p("animationSpeed", "Animation Speed", num(1.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("animateSmoothly", "Animate Smoothly", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.chain([tweaking, color, application, animation].into_iter().flatten())
|
||||
.collect(),
|
||||
match_grain,
|
||||
),
|
||||
spec(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,507 @@
|
|||
//! Fractal Noise and Turbulent Noise: one fractal value-noise engine with the parameter tree of
|
||||
//! the two effects (Fractal Type, Noise Type, Overflow, the Transform / Sub Settings / Evolution
|
||||
//! Options twirl-downs, Opacity and Blending Mode).
|
||||
//!
|
||||
//! Each "noise layer" (octave) is 3D value noise (x, y, evolution) on an integer lattice. The
|
||||
//! Noise Type picks the interpolation between lattice values (Block = nearest, Linear,
|
||||
//! Soft Linear = quintic fade, Spline = Catmull-Rom). Successive layers are scaled by Sub
|
||||
//! Scaling, weighted by Sub Influence, rotated by Sub Rotation and shifted by Sub Offset, and the
|
||||
//! Fractal Type shapes each layer (turbulent types fold the signed noise; dynamic types warp the
|
||||
//! domain; Max keeps the strongest layer; Strings keeps thin lines). Contrast / Brightness /
|
||||
//! Overflow then map the sum to the output range. Cycle Evolution loops the evolution by
|
||||
//! cross-fading two evolution offsets one cycle apart; Turbulent Noise's Turbulence Factor makes
|
||||
//! finer layers evolve and drift faster than coarse ones.
|
||||
|
||||
use effectcraft_color::{BlendMode, blend_pixel};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::generate::value_noise;
|
||||
use crate::{Buf, EffectCtx, EffectSpec, ParamSpec, num, p, popup, slider};
|
||||
|
||||
/// Fractal Type options (shared by both effects).
|
||||
pub const FRACTAL_TYPES: [&str; 9] =
|
||||
["Basic", "Turbulent Smooth", "Turbulent Basic", "Turbulent Sharp", "Dynamic", "Dynamic Twist", "Dynamic Progressive", "Max", "Strings"];
|
||||
/// Noise Type options: interpolation between lattice values.
|
||||
pub const NOISE_TYPES: [&str; 4] = ["Block", "Linear", "Soft Linear", "Spline"];
|
||||
/// Overflow options.
|
||||
pub const OVERFLOWS: [&str; 4] = ["Clip", "Soft Clamp", "Wrap Back", "Allow HDR Results"];
|
||||
/// Blending Mode options: None renders the noise alone, the rest composite it over the layer.
|
||||
pub const BLEND_MODES: [&str; 24] = [
|
||||
"None",
|
||||
"Normal",
|
||||
"Add",
|
||||
"Multiply",
|
||||
"Screen",
|
||||
"Overlay",
|
||||
"Soft Light",
|
||||
"Hard Light",
|
||||
"Color Dodge",
|
||||
"Color Burn",
|
||||
"Darken",
|
||||
"Lighten",
|
||||
"Difference",
|
||||
"Exclusion",
|
||||
"Hue",
|
||||
"Saturation",
|
||||
"Color",
|
||||
"Luminosity",
|
||||
"Stencil Alpha",
|
||||
"Stencil Luma",
|
||||
"Silhouette Alpha",
|
||||
"Silhouette Luma",
|
||||
"Alpha Add",
|
||||
"Luminescent Premul",
|
||||
];
|
||||
/// Index of "None" / "Normal" in [`BLEND_MODES`].
|
||||
pub const BLEND_NONE: u32 = 0;
|
||||
pub const BLEND_NORMAL: u32 = 1;
|
||||
/// Index of "Soft Linear" in [`NOISE_TYPES`] (the default, and the GPU path's interpolation).
|
||||
pub const NOISE_SOFT_LINEAR: u32 = 2;
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Noise & Grain", params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
fn params(turbulent: bool) -> Vec<ParamSpec> {
|
||||
let pct = || slider(0.0, 100.0, 0.0, 100.0, 1);
|
||||
let scale = || slider(1.0, 10000.0, 20.0, 600.0, 1);
|
||||
let mut v = vec![
|
||||
p("fractalType", "Fractal Type", Value::Enum(if turbulent { 1 } else { 0 }), popup(&FRACTAL_TYPES)),
|
||||
p("noiseType", "Noise Type", Value::Enum(NOISE_SOFT_LINEAR), popup(&NOISE_TYPES)),
|
||||
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("overflow", "Overflow", Value::Enum(if turbulent { 0 } else { 3 }), popup(&OVERFLOWS)),
|
||||
p("transform/rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||||
p("transform/uniformScaling", "Uniform Scaling", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("transform/scale", "Scale", num(100.0), scale()),
|
||||
p("transform/scaleWidth", "Scale Width", num(100.0), scale()),
|
||||
p("transform/scaleHeight", "Scale Height", num(100.0), scale()),
|
||||
p("transform/offset", "Offset Turbulence", Value::Vec2([0.5, 0.5]), ParamUi::Point),
|
||||
p("transform/perspectiveOffset", "Perspective Offset", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("complexity", "Complexity", num(6.0), slider(1.0, 20.0, 1.0, 10.0, 1)),
|
||||
p("subSettings/subInfluence", "Sub Influence (%)", num(70.0), slider(0.0, 100.0, 25.0, 100.0, 1)),
|
||||
p("subSettings/subScaling", "Sub Scaling", num(56.0), slider(10.0, 100.0, 25.0, 100.0, 1)),
|
||||
];
|
||||
if !turbulent {
|
||||
v.extend([
|
||||
p("subSettings/subRotation", "Sub Rotation", num(0.0), ParamUi::Angle),
|
||||
p("subSettings/subOffset", "Sub Offset", Value::Vec2([0.0, 0.0]), ParamUi::Point),
|
||||
p("subSettings/centerSubscale", "Center Subscale", Value::Bool(false), ParamUi::Checkbox),
|
||||
]);
|
||||
}
|
||||
v.push(p("evolution", "Evolution", num(0.0), ParamUi::Angle));
|
||||
if turbulent {
|
||||
v.extend([
|
||||
p("evolutionOptions/turbulenceFactor", "Turbulence Factor", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("evolutionOptions/randomSeed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
]);
|
||||
} else {
|
||||
v.extend([
|
||||
p("evolutionOptions/cycleEvolution", "Cycle Evolution", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("evolutionOptions/cycle", "Cycle (in Revolutions)", num(1.0), slider(1.0, 1000.0, 1.0, 30.0, 0)),
|
||||
p("evolutionOptions/seed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)),
|
||||
]);
|
||||
}
|
||||
v.extend([
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(BLEND_NORMAL), popup(&BLEND_MODES)),
|
||||
// Not in After Effects: a mix with the untouched layer (kept for older projects).
|
||||
p("blend", "Blend With Original", num(0.0), pct()),
|
||||
]);
|
||||
v
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
vec![spec("ec.noise.fractal", "Fractal Noise", params(false), fractal_noise), spec("ec.noise.turbulent", "Turbulent Noise", params(true), turbulent_noise)]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- noise kernels
|
||||
|
||||
fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 {
|
||||
// The same hash as `generate::value_noise`, so Soft Linear matches it bit for bit.
|
||||
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)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
|
||||
0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t * t + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t * t * t)
|
||||
}
|
||||
|
||||
/// 3D value noise in about 0..1 with the given [`NOISE_TYPES`] interpolation in x and y (the
|
||||
/// evolution axis z always fades smoothly).
|
||||
pub fn typed_noise(x: f32, y: f32, z: f32, seed: u32, noise_type: u32) -> f32 {
|
||||
if noise_type == NOISE_SOFT_LINEAR {
|
||||
return value_noise(x, y, z, seed);
|
||||
}
|
||||
let (x0, y0, z0) = (x.floor(), y.floor(), z.floor());
|
||||
let (tx, ty, fz) = (x - x0, 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 slice = |dz: i32| -> f32 {
|
||||
let c = |dx: i32, dy: i32| lattice(ix + dx, iy + dy, iz + dz, seed);
|
||||
match noise_type {
|
||||
0 => c(0, 0),
|
||||
1 => l(l(c(0, 0), c(1, 0), tx), l(c(0, 1), c(1, 1), tx), ty),
|
||||
_ => {
|
||||
let row = |dy: i32| catmull(c(-1, dy), c(0, dy), c(1, dy), c(2, dy), tx);
|
||||
catmull(row(-1), row(0), row(1), row(2), ty)
|
||||
}
|
||||
}
|
||||
};
|
||||
l(slice(0), slice(1), fz)
|
||||
}
|
||||
|
||||
/// Everything the per-pixel evaluation needs, resolved from the parameters.
|
||||
pub(crate) struct Fractal {
|
||||
pub kind: u32,
|
||||
pub noise_type: u32,
|
||||
pub contrast: f32,
|
||||
pub brightness: f32,
|
||||
pub overflow: u32,
|
||||
pub invert: bool,
|
||||
/// Rotation (sin, cos).
|
||||
pub rot: (f32, f32),
|
||||
/// Noise cell size in pixels along the rotated x / y axes.
|
||||
pub size: [f32; 2],
|
||||
/// Offset Turbulence in pixels, and the layer centre (for Perspective Offset).
|
||||
pub origin: [f32; 2],
|
||||
pub center: [f32; 2],
|
||||
pub perspective: bool,
|
||||
pub octaves: f32,
|
||||
pub influence: f32,
|
||||
/// Sub Scaling as a fraction (each layer is this much smaller).
|
||||
pub sub_scaling: f32,
|
||||
pub sub_rotation: f32,
|
||||
/// Sub Offset in pixels.
|
||||
pub sub_offset: [f32; 2],
|
||||
pub center_subscale: bool,
|
||||
/// Evolution in revolutions.
|
||||
pub evolution: f32,
|
||||
/// Cycle length in revolutions when Cycle Evolution is on.
|
||||
pub cycle: Option<f32>,
|
||||
pub turbulence: f32,
|
||||
pub seed: u32,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn shape(kind: u32, n: f32) -> f32 {
|
||||
let s = n * 2.0 - 1.0;
|
||||
match kind {
|
||||
1 => 1.0 - s * s,
|
||||
2 => 1.0 - s.abs(),
|
||||
3 => (1.0 - s.abs()).powi(2),
|
||||
8 => 1.0 - (s.abs() / 0.12).clamp(0.0, 1.0).powi(2),
|
||||
_ => n,
|
||||
}
|
||||
}
|
||||
|
||||
impl Fractal {
|
||||
pub(crate) fn from_params(ctx: &EffectCtx, b: &Buf, turbulent: bool) -> Fractal {
|
||||
let pr = ctx.params;
|
||||
let uniform = pr.get("transform/uniformScaling").is_none_or(Value::as_bool);
|
||||
let s = pr.f("transform/scale");
|
||||
let (sw, sh) = if uniform { (s, s) } else { (pr.f("transform/scaleWidth"), pr.f("transform/scaleHeight")) };
|
||||
let rot = (pr.f("transform/rotation") as f32).to_radians();
|
||||
let (ox, oy) = b.to_px(pr.v2("transform/offset"));
|
||||
let (cx, cy) = b.to_px([ctx.layer_size[0] / 2.0, ctx.layer_size[1] / 2.0]);
|
||||
let so = pr.v2("subSettings/subOffset");
|
||||
let cycle = (!turbulent && pr.b("evolutionOptions/cycleEvolution")).then(|| pr.f("evolutionOptions/cycle").round().max(1.0) as f32);
|
||||
let seed = if turbulent { (pr.f("evolutionOptions/randomSeed") as i64 as u32) ^ 0x7a3d } else { pr.f("evolutionOptions/seed") as u32 ^ 0x51ed };
|
||||
Fractal {
|
||||
kind: pr.e("fractalType"),
|
||||
noise_type: pr.get("noiseType").map(Value::as_enum).unwrap_or(NOISE_SOFT_LINEAR),
|
||||
contrast: pr.f("contrast") as f32 / 100.0,
|
||||
brightness: pr.f("brightness") as f32 / 100.0,
|
||||
overflow: pr.e("overflow"),
|
||||
invert: pr.b("invert"),
|
||||
rot: rot.sin_cos(),
|
||||
size: [(sw * b.scale).max(1.0) as f32, (sh * b.scale).max(1.0) as f32],
|
||||
origin: [ox as f32, oy as f32],
|
||||
center: [cx as f32, cy as f32],
|
||||
perspective: pr.b("transform/perspectiveOffset"),
|
||||
octaves: pr.f("complexity").clamp(1.0, 20.0) as f32,
|
||||
influence: (pr.f("subSettings/subInfluence") as f32 / 100.0).clamp(0.0, 1.0),
|
||||
sub_scaling: (pr.f("subSettings/subScaling") as f32 / 100.0).clamp(0.1, 1.0),
|
||||
sub_rotation: (pr.f("subSettings/subRotation") as f32).to_radians(),
|
||||
sub_offset: [(so[0] * b.scale) as f32, (so[1] * b.scale) as f32],
|
||||
center_subscale: pr.b("subSettings/centerSubscale"),
|
||||
evolution: pr.f("evolution") as f32 / 360.0,
|
||||
cycle,
|
||||
turbulence: if turbulent { pr.f("evolutionOptions/turbulenceFactor").max(0.0) as f32 } else { 0.0 },
|
||||
seed,
|
||||
}
|
||||
}
|
||||
|
||||
/// One noise layer at lattice coordinates (u, v) for octave `o` (evolution offset `z`), with
|
||||
/// Cycle Evolution's loop. With turbulence, finer layers evolve faster.
|
||||
#[inline]
|
||||
fn layer(&self, u: f32, v: f32, o: f32, z: f32, seed: u32) -> f32 {
|
||||
match self.cycle {
|
||||
Some(c) => {
|
||||
let t = self.evolution.rem_euclid(c);
|
||||
let w = t / c;
|
||||
let a = typed_noise(u, v, t + z, seed, self.noise_type);
|
||||
let b = typed_noise(u, v, t - c + z, seed, self.noise_type);
|
||||
a * (1.0 - w) + b * w
|
||||
}
|
||||
None => typed_noise(u, v, self.evolution * (1.0 + self.turbulence * o) + z, seed, self.noise_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw fractal value (before contrast / overflow) at pixel centre (x, y).
|
||||
pub(crate) fn raw(&self, x: f32, y: f32) -> f32 {
|
||||
let n_oct = self.octaves.ceil() as usize;
|
||||
let frac = self.octaves - self.octaves.floor();
|
||||
let (sr, cr) = self.rot;
|
||||
let (mut sum, mut norm, mut amp, mut f, mut best) = (0.0f32, 0.0f32, 1.0f32, 1.0f32, 0.0f32);
|
||||
let mut depth = 1.0f32;
|
||||
for o in 0..n_oct {
|
||||
let w = if o + 1 == n_oct && frac > 0.0 { frac } else { 1.0 };
|
||||
let of = o as f32;
|
||||
// This layer's origin: Perspective Offset moves finer (farther) layers less;
|
||||
// turbulence makes them drift faster.
|
||||
let k = if self.perspective { depth } else { 1.0 } * (1.0 + self.turbulence * of);
|
||||
let mut org = self.origin;
|
||||
if k != 1.0 {
|
||||
org = [self.center[0] + (self.origin[0] - self.center[0]) * k, self.center[1] + (self.origin[1] - self.center[1]) * k];
|
||||
}
|
||||
if !self.center_subscale {
|
||||
org = [org[0] + self.sub_offset[0] * of, org[1] + self.sub_offset[1] * of];
|
||||
}
|
||||
let (dx, dy) = (x - org[0], y - org[1]);
|
||||
let (mut u, mut v) = ((dx * cr + dy * sr) / self.size[0], (-dx * sr + dy * cr) / self.size[1]);
|
||||
if self.sub_rotation != 0.0 {
|
||||
let (s, c) = (self.sub_rotation * of).sin_cos();
|
||||
(u, v) = (u * c + v * s, -u * s + v * c);
|
||||
}
|
||||
let (mut u, mut v) = (u * f, v * f);
|
||||
let z = of * 7.31;
|
||||
let seed = self.seed.wrapping_add(o as u32);
|
||||
if (4..=6).contains(&self.kind) {
|
||||
// Dynamic: domain warp by a coarse field (Twist rotates, Progressive grows with depth).
|
||||
let k = if self.kind == 6 { 0.5 + of * 0.35 } else { 1.0 };
|
||||
let a = self.layer(u * 0.5, v * 0.5, of, z + 3.1, seed ^ 0x55) - 0.5;
|
||||
let bb = self.layer(u * 0.5, v * 0.5, of, z + 5.7, seed ^ 0xaa) - 0.5;
|
||||
if self.kind == 5 {
|
||||
let (s, c) = (a * 3.0).sin_cos();
|
||||
(u, v) = (u * c - v * s, u * s + v * c);
|
||||
} else {
|
||||
u += a * 1.5 * k;
|
||||
v += bb * 1.5 * k;
|
||||
}
|
||||
}
|
||||
let n = shape(self.kind, self.layer(u, v, of, z, seed));
|
||||
if self.kind == 7 {
|
||||
best = best.max(n * amp);
|
||||
}
|
||||
sum += n * amp * w;
|
||||
norm += amp * w;
|
||||
amp *= self.influence;
|
||||
f /= self.sub_scaling;
|
||||
depth *= self.sub_scaling;
|
||||
}
|
||||
if self.kind == 7 { best } else { sum / norm.max(1e-6) }
|
||||
}
|
||||
|
||||
/// Final grey value: contrast, brightness, overflow and invert applied.
|
||||
pub(crate) fn value(&self, x: f32, y: f32) -> f32 {
|
||||
let v = (self.raw(x, y) - 0.5) * self.contrast + 0.5 + self.brightness;
|
||||
let v = overflow(v, self.overflow);
|
||||
if self.invert { 1.0 - v } else { v }
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a value by an [`OVERFLOWS`] option.
|
||||
pub fn overflow(v: f32, mode: u32) -> f32 {
|
||||
match mode {
|
||||
1 => (0.5 + 0.5 * (2.0 * (v - 0.5)).tanh() / 1f32.tanh()).clamp(0.0, 1.0),
|
||||
2 => {
|
||||
let t = v.rem_euclid(2.0);
|
||||
if t > 1.0 { 2.0 - t } else { t }
|
||||
}
|
||||
3 => v,
|
||||
_ => v.clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Composite grey noise `val` over the original pixel by Blending Mode, Opacity and (ours) Blend
|
||||
/// With Original.
|
||||
pub(crate) fn composite(orig: [f32; 4], val: f32, opacity: f32, mode: u32, blend_orig: f32) -> [f32; 4] {
|
||||
let src = [val * opacity, val * opacity, val * opacity, opacity];
|
||||
let out = match mode {
|
||||
BLEND_NONE => src,
|
||||
m => {
|
||||
let bm = BLEND_MODES.get(m as usize).and_then(|l| BlendMode::from_name(l)).unwrap_or(BlendMode::Normal);
|
||||
blend_pixel(bm, orig, src, 0.5)
|
||||
}
|
||||
};
|
||||
let k = 1.0 - blend_orig;
|
||||
[0, 1, 2, 3].map(|c| orig[c] * blend_orig + out[c] * k)
|
||||
}
|
||||
|
||||
fn render(ctx: &EffectCtx, mut b: Buf, turbulent: bool) -> Buf {
|
||||
let fr = Fractal::from_params(ctx, &b, turbulent);
|
||||
let opacity = (ctx.params.f("opacity") as f32 / 100.0).clamp(0.0, 1.0);
|
||||
let mode = ctx.params.get("blendingMode").map(Value::as_enum).unwrap_or(BLEND_NONE);
|
||||
let blend = ctx.params.f("blend") as f32 / 100.0;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let v = fr.value(x as f32 + 0.5, y as f32 + 0.5);
|
||||
*px = composite(*px, v, opacity, mode, blend);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn fractal_noise(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
render(ctx, b, false)
|
||||
}
|
||||
|
||||
fn turbulent_noise(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
render(ctx, b, true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{EffectEnv, run_fx};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
fn run(id: &str, vals: &[(&str, Value)]) -> Image {
|
||||
run_fx(id, vals, Image::filled(40, 24, [0.2, 0.4, 0.6, 1.0]), 0.0, EffectEnv::default()).img
|
||||
}
|
||||
|
||||
/// A Fractal Noise saved before the twirl-downs existed: flat ids move into their groups and
|
||||
/// the new controls take values that keep the old look.
|
||||
#[test]
|
||||
fn old_fractal_noise_instances_upgrade() {
|
||||
use effectcraft_project::build::Ids;
|
||||
use effectcraft_project::{GroupKind, Property};
|
||||
let spec = crate::find("ec.noise.fractal").unwrap();
|
||||
let mut next = 1;
|
||||
let mut ids = Ids(&mut next);
|
||||
let mut g = ids.group(spec.id, "Fractal Noise");
|
||||
g.kind = GroupKind::Effect { effect: spec.id.into() };
|
||||
for (m, v) in [
|
||||
("fractalType", Value::Enum(1)),
|
||||
("invert", Value::Bool(false)),
|
||||
("contrast", num(150.0)),
|
||||
("brightness", num(0.0)),
|
||||
("scale", num(40.0)),
|
||||
("offset", Value::Vec2([5.0, 6.0])),
|
||||
("rotation", num(30.0)),
|
||||
("complexity", num(3.0)),
|
||||
("evolution", num(0.0)),
|
||||
("seed", num(9.0)),
|
||||
("opacity", num(100.0)),
|
||||
("blend", num(0.0)),
|
||||
] {
|
||||
let id = ids.alloc();
|
||||
g.children.push(Property::new(id, m, m, v).into());
|
||||
}
|
||||
assert!(crate::migrate::upgrade_instance(spec, &mut g, &mut ids, [40.0, 24.0]));
|
||||
assert_eq!(g.prop("transform/scale").unwrap().value, num(40.0));
|
||||
assert_eq!(g.prop("transform/offset").unwrap().value, Value::Vec2([5.0, 6.0]));
|
||||
assert_eq!(g.prop("evolutionOptions/seed").unwrap().value, num(9.0));
|
||||
assert!(g.get("scale").is_none() && g.get("seed").is_none());
|
||||
// Clip, half-weight half-size octaves and the noise drawn alone, as before.
|
||||
assert_eq!(g.get("overflow").unwrap().value, Value::Enum(0));
|
||||
assert_eq!(g.prop("subSettings/subInfluence").unwrap().value, num(50.0));
|
||||
assert_eq!(g.prop("subSettings/subScaling").unwrap().value, num(50.0));
|
||||
assert_eq!(g.get("blendingMode").unwrap().value, Value::Enum(BLEND_NONE));
|
||||
assert_eq!(g.sub("transform").unwrap().name, "Transform");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_types_differ_and_block_is_piecewise_constant() {
|
||||
let a = run("ec.noise.fractal", &[("noiseType", Value::Enum(0)), ("complexity", num(1.0)), ("transform/scale", num(10.0))]);
|
||||
// Block noise: neighbours inside one 10 px cell share a value.
|
||||
assert_eq!(a.get(1, 1), a.get(2, 2));
|
||||
let others: Vec<Image> = (1..4).map(|t| run("ec.noise.fractal", &[("noiseType", Value::Enum(t)), ("transform/scale", num(10.0))])).collect();
|
||||
assert_ne!(others[0], others[1]);
|
||||
assert_ne!(others[1], others[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_width_height_apply_without_uniform_scaling() {
|
||||
let base = &[("complexity", num(1.0)), ("transform/scale", num(8.0))];
|
||||
let uni = run("ec.noise.fractal", &[base[0].clone(), base[1].clone(), ("transform/scaleWidth", num(50.0))]);
|
||||
let plain = run("ec.noise.fractal", base);
|
||||
assert_eq!(uni, plain, "Scale Width is ignored while Uniform Scaling is on");
|
||||
let wide = run(
|
||||
"ec.noise.fractal",
|
||||
&[base[0].clone(), ("transform/uniformScaling", Value::Bool(false)), ("transform/scaleWidth", num(400.0)), ("transform/scaleHeight", num(8.0))],
|
||||
);
|
||||
// Stretched horizontally: horizontal neighbours are more alike than vertical ones.
|
||||
let (mut dh, mut dv) = (0.0, 0.0);
|
||||
for y in 0..23 {
|
||||
for x in 0..39 {
|
||||
dh += (wide.get(x + 1, y)[0] - wide.get(x, y)[0]).abs();
|
||||
dv += (wide.get(x, y + 1)[0] - wide.get(x, y)[0]).abs();
|
||||
}
|
||||
}
|
||||
assert!(dh < dv * 0.5, "{dh} {dv}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_hdr_keeps_out_of_range_values() {
|
||||
let hi = run("ec.noise.fractal", &[("contrast", num(400.0)), ("blendingMode", Value::Enum(BLEND_NONE))]);
|
||||
assert!(hi.data.iter().any(|p| p[0] > 1.0 || p[0] < 0.0));
|
||||
let clip = run("ec.noise.fractal", &[("contrast", num(400.0)), ("overflow", Value::Enum(0)), ("blendingMode", Value::Enum(BLEND_NONE))]);
|
||||
assert!(clip.data.iter().all(|p| (0.0..=1.0).contains(&p[0])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_evolution_loops() {
|
||||
let at = |evo: f64| {
|
||||
run("ec.noise.fractal", &[("evolution", num(evo)), ("evolutionOptions/cycleEvolution", Value::Bool(true)), ("evolutionOptions/cycle", num(2.0))])
|
||||
};
|
||||
assert_eq!(at(0.0), at(720.0));
|
||||
assert_ne!(at(0.0), at(360.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blending_modes_composite_over_the_layer() {
|
||||
let none = run("ec.noise.fractal", &[("blendingMode", Value::Enum(BLEND_NONE))]);
|
||||
let mul = run("ec.noise.fractal", &[("blendingMode", Value::Enum(3))]);
|
||||
// Multiply keeps the layer's colour cast (blue > red), None is grey.
|
||||
assert!(none.data.iter().all(|p| (p[0] - p[2]).abs() < 1e-6));
|
||||
assert!(mul.data.iter().all(|p| p[2] >= p[0]));
|
||||
assert!(mul.data.iter().any(|p| p[2] > p[0] + 0.01));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_settings_and_fractal_types_change_the_result() {
|
||||
let base = run("ec.noise.fractal", &[]);
|
||||
for (k, v) in [
|
||||
("subSettings/subInfluence", num(20.0)),
|
||||
("subSettings/subScaling", num(80.0)),
|
||||
("subSettings/subRotation", num(45.0)),
|
||||
("subSettings/subOffset", Value::Vec2([7.0, 3.0])),
|
||||
] {
|
||||
assert_ne!(run("ec.noise.fractal", &[(k, v)]), base, "{k}");
|
||||
}
|
||||
for t in 1..FRACTAL_TYPES.len() as u32 {
|
||||
assert_ne!(run("ec.noise.fractal", &[("fractalType", Value::Enum(t))]), base, "type {t}");
|
||||
}
|
||||
// Turbulence factor only matters while evolving.
|
||||
let t0 = run("ec.noise.turbulent", &[("evolution", num(200.0)), ("evolutionOptions/turbulenceFactor", num(0.0))]);
|
||||
let t1 = run("ec.noise.turbulent", &[("evolution", num(200.0)), ("evolutionOptions/turbulenceFactor", num(2.0))]);
|
||||
assert_ne!(t0, t1);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ fn basic_3d(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let tilt = ctx.params.f("tilt").to_radians();
|
||||
let dist = ctx.params.f("distanceToImage") * b.scale;
|
||||
let spec_on = ctx.params.b("specularHighlight");
|
||||
let wire = ctx.params.b("drawPreviewWireframe");
|
||||
let wire = ctx.params.b("preview/drawPreviewWireframe");
|
||||
if swivel == 0.0 && tilt == 0.0 && dist == 0.0 && !spec_on && !wire {
|
||||
return b;
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("tilt", "Tilt", num(0.0), ParamUi::Angle),
|
||||
p("distanceToImage", "Distance to Image", num(0.0), slider(-1000.0, 30000.0, -100.0, 100.0, 1)),
|
||||
p("specularHighlight", "Specular Highlight", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("drawPreviewWireframe", "Draw Preview Wireframe", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("preview/drawPreviewWireframe", "Draw Preview Wireframe", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
basic_3d,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -932,9 +932,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
cc,
|
||||
vec![
|
||||
p("config", "Configuration", Value::Enum(0), popup(&["Built-in (EffectCraft minimal)"])),
|
||||
space_param("source", "Source", lin709),
|
||||
p("display", "Display", Value::Enum(0), popup(&DISPLAYS.iter().map(|d| d.0).collect::<Vec<_>>())),
|
||||
p("view", "View", Value::Enum(0), popup(VIEWS)),
|
||||
space_param("source", "Input Color Space", lin709),
|
||||
p("display", "Display Device", Value::Enum(0), popup(&DISPLAYS.iter().map(|d| d.0).collect::<Vec<_>>())),
|
||||
p("view", "View Transform", Value::Enum(0), popup(VIEWS)),
|
||||
direction(),
|
||||
],
|
||||
ocio_display,
|
||||
|
|
@ -971,8 +971,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Color Stabilizer",
|
||||
cc,
|
||||
vec![
|
||||
p("stabilize", "Stabilize", Value::Enum(0), popup(&["Brightness", "Levels", "Curves"])),
|
||||
// The pivot frame (After Effects' Set Frame button stores it).
|
||||
p("referenceFrame", "Reference Frame (s)", num(0.0), slider(-100000.0, 100000.0, 0.0, 60.0, 2)),
|
||||
p("stabilize", "Stabilize", Value::Enum(0), popup(&["Brightness", "Levels", "Curves"])),
|
||||
p("blackPoint", "Black Point", Value::Vec2([0.25, 0.75]), ParamUi::Point),
|
||||
p("midPoint", "Mid Point", Value::Vec2([0.5, 0.5]), ParamUi::Point),
|
||||
p("whitePoint", "White Point", Value::Vec2([0.75, 0.25]), ParamUi::Point),
|
||||
|
|
@ -990,6 +991,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("outputProfile", "Output Profile", Value::Enum(1), popup(PROFILES)),
|
||||
p("linearizeOutputProfile", "Linearize Output Profile", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("intent", "Intent", Value::Enum(1), popup(&["Perceptual", "Relative Colorimetric", "Saturation", "Absolute Colorimetric"])),
|
||||
// Our built-in profiles share a zero black point and none is scene-referred, so
|
||||
// these two are kept for compatibility and do not change the result.
|
||||
p("useBlackPointCompensation", "Use Black Point Compensation", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("sceneRefCompensation", "Scene-ref. Profile Compensation", Value::Enum(2), popup(&["On", "Off", "Use Project Setting"])),
|
||||
],
|
||||
profile_converter,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("sceneConvergence", "Scene Convergence", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)),
|
||||
p("verticalAlignment", "Vertical Alignment", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)),
|
||||
p("units", "Units", Value::Enum(0), popup(&["Pixels", "% of Source"])),
|
||||
p("leftRightSwap", "Left Right Swap", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("leftRightSwap", "Swap Left-Right", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("view3d", "3D View", Value::Enum(5), popup(&VIEWS)),
|
||||
p("balance", "Balance", num(7.0), slider(0.0, 20.0, 0.0, 20.0, 1)),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -46,7 +46,13 @@ pub const FROZEN: &str = "frozen";
|
|||
pub const DEFAULT_SPAN: i64 = 20;
|
||||
|
||||
/// Display names of the instance's parameter groups.
|
||||
pub const GROUPS: &[(&str, &str)] = &[("rotoBrushMatte", "Roto Brush Matte"), ("refineEdgeMatte", "Refine Edge Matte")];
|
||||
pub const GROUPS: &[(&str, &str)] = &[
|
||||
("rotoBrushPropagation", "Roto Brush Propagation"),
|
||||
("rotoBrushMatte", "Roto Brush Matte"),
|
||||
("refineEdgeMatte", "Refine Edge Matte"),
|
||||
("motionBlur", "Motion Blur"),
|
||||
("decontamination", "Decontamination"),
|
||||
];
|
||||
|
||||
pub const VERSIONS: [&str; 3] = ["1.0", "2.0", "3.0"];
|
||||
pub const QUALITIES: [&str; 2] = ["Standard", "Best"];
|
||||
|
|
@ -59,10 +65,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
params: vec![
|
||||
p("version", "Version", Value::Enum(2), popup(&VERSIONS)),
|
||||
p("quality", "Quality", Value::Enum(0), popup(&QUALITIES)),
|
||||
p("rotoBrushMatte/searchRadius", "Search Radius", num(15.0), slider(0.0, 100.0, 0.0, 50.0, 1)),
|
||||
p("rotoBrushMatte/motionThreshold", "Motion Threshold", num(10.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("rotoBrushMatte/motionDamping", "Motion Damping", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("rotoBrushMatte/viewSearchRegion", "View Search Region", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("rotoBrushPropagation/searchRadius", "Search Radius", num(15.0), slider(0.0, 100.0, 0.0, 50.0, 1)),
|
||||
p("rotoBrushPropagation/motionThreshold", "Motion Threshold", num(10.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("rotoBrushPropagation/motionDamping", "Motion Damping", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("rotoBrushPropagation/viewSearchRegion", "View Search Region", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("rotoBrushMatte/feather", "Feather", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("rotoBrushMatte/contrast", "Contrast", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("rotoBrushMatte/shiftEdge", "Shift Edge", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
|
|
@ -73,14 +79,14 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("refineEdgeMatte/shiftEdge", "Shift Edge", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
p("refineEdgeMatte/reduceChatter", "Reduce Chatter", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("refineEdgeMatte/useMotionBlur", "Use Motion Blur", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("refineEdgeMatte/motionBlurSamples", "Motion Blur Samples", num(11.0), slider(1.0, 64.0, 1.0, 32.0, 0)),
|
||||
p("refineEdgeMatte/shutterAngle", "Shutter Angle", num(180.0), slider(0.0, 720.0, 0.0, 360.0, 0)),
|
||||
p("refineEdgeMatte/higherQuality", "Higher Quality", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("refineEdgeMatte/motionBlur/motionBlurSamples", "Samples", num(11.0), slider(1.0, 64.0, 1.0, 32.0, 0)),
|
||||
p("refineEdgeMatte/motionBlur/shutterAngle", "Shutter Angle", num(180.0), slider(0.0, 720.0, 0.0, 360.0, 0)),
|
||||
p("refineEdgeMatte/motionBlur/higherQuality", "Higher Quality", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("refineEdgeMatte/decontaminateEdgeColors", "Decontaminate Edge Colors", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("refineEdgeMatte/decontaminationAmount", "Decontamination Amount", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("refineEdgeMatte/extendWhereSmoothed", "Extend Where Smoothed", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("refineEdgeMatte/increaseDecontaminationRadius", "Increase Decontamination Radius", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("refineEdgeMatte/viewDecontaminationMap", "View Decontamination Map", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("refineEdgeMatte/decontamination/decontaminationAmount", "Decontamination Amount", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("refineEdgeMatte/decontamination/extendWhereSmoothed", "Extend Where Smoothed", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("refineEdgeMatte/decontamination/increaseDecontaminationRadius", "Increase Decontamination Radius", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("refineEdgeMatte/decontamination/viewDecontaminationMap", "View Decontamination Map", Value::Bool(false), ParamUi::Checkbox),
|
||||
p(STROKES, "Strokes", Value::Str(String::new()), ParamUi::Hidden),
|
||||
p(INPUT_KEY, "Input Key", Value::Str(String::new()), ParamUi::Hidden),
|
||||
p(FROZEN, "Frozen", Value::Str(String::new()), ParamUi::Hidden),
|
||||
|
|
@ -137,9 +143,9 @@ pub fn data(params: &Params) -> Arc<RotoData> {
|
|||
pub fn seg_opts(params: &Params, scale: f64) -> SegOpts {
|
||||
SegOpts {
|
||||
scale,
|
||||
search_radius: f(params, "rotoBrushMatte/searchRadius").max(0.0),
|
||||
motion_threshold: f(params, "rotoBrushMatte/motionThreshold"),
|
||||
motion_damping: f(params, "rotoBrushMatte/motionDamping"),
|
||||
search_radius: f(params, "rotoBrushPropagation/searchRadius").max(0.0),
|
||||
motion_threshold: f(params, "rotoBrushPropagation/motionThreshold"),
|
||||
motion_damping: f(params, "rotoBrushPropagation/motionDamping"),
|
||||
best: e(params, "quality") == 1,
|
||||
}
|
||||
}
|
||||
|
|
@ -157,13 +163,13 @@ pub fn matte_params(params: &Params) -> MatteParams {
|
|||
shift: f(params, "refineEdgeMatte/shiftEdge"),
|
||||
chatter: f(params, "refineEdgeMatte/reduceChatter"),
|
||||
motion_blur: b(params, "refineEdgeMatte/useMotionBlur"),
|
||||
mb_samples: f(params, "refineEdgeMatte/motionBlurSamples").round().clamp(1.0, 64.0) as usize,
|
||||
shutter_angle: f(params, "refineEdgeMatte/shutterAngle"),
|
||||
higher_quality: b(params, "refineEdgeMatte/higherQuality"),
|
||||
mb_samples: f(params, "refineEdgeMatte/motionBlur/motionBlurSamples").round().clamp(1.0, 64.0) as usize,
|
||||
shutter_angle: f(params, "refineEdgeMatte/motionBlur/shutterAngle"),
|
||||
higher_quality: b(params, "refineEdgeMatte/motionBlur/higherQuality"),
|
||||
decontaminate: b(params, "refineEdgeMatte/decontaminateEdgeColors"),
|
||||
decontamination: f(params, "refineEdgeMatte/decontaminationAmount"),
|
||||
extend_where_smoothed: b(params, "refineEdgeMatte/extendWhereSmoothed"),
|
||||
increase_radius: f(params, "refineEdgeMatte/increaseDecontaminationRadius").max(0.0),
|
||||
decontamination: f(params, "refineEdgeMatte/decontamination/decontaminationAmount"),
|
||||
extend_where_smoothed: b(params, "refineEdgeMatte/decontamination/extendWhereSmoothed"),
|
||||
increase_radius: f(params, "refineEdgeMatte/decontamination/increaseDecontaminationRadius").max(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,9 +182,9 @@ pub fn seed(params: &Params, layer_size: [f64; 2], fps: f64) -> u64 {
|
|||
layer_size[0],
|
||||
layer_size[1],
|
||||
fps,
|
||||
f(params, "rotoBrushMatte/searchRadius"),
|
||||
f(params, "rotoBrushMatte/motionThreshold"),
|
||||
f(params, "rotoBrushMatte/motionDamping"),
|
||||
f(params, "rotoBrushPropagation/searchRadius"),
|
||||
f(params, "rotoBrushPropagation/motionThreshold"),
|
||||
f(params, "rotoBrushPropagation/motionDamping"),
|
||||
] {
|
||||
fnv(&mut h, &v.to_bits().to_le_bytes());
|
||||
}
|
||||
|
|
@ -555,7 +561,7 @@ fn render(ctx: &EffectCtx, buf: Buf) -> Buf {
|
|||
let mp = matte_params(ctx.params);
|
||||
let mut rgb = rb::rgb_of(&img);
|
||||
let map = refine::decontaminate(&mut rgb, &alpha, &band, w, h, &mp, rr * sc, sc);
|
||||
let view_map = b(ctx.params, "refineEdgeMatte/viewDecontaminationMap");
|
||||
let view_map = b(ctx.params, "refineEdgeMatte/decontamination/viewDecontaminationMap");
|
||||
let (ox, oy) = (buf.offset[0].round() as i64, buf.offset[1].round() as i64);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
|
|
@ -695,4 +701,35 @@ mod tests {
|
|||
let o = crate::apply(&specs()[0], &ctx, Buf { img: frame(0.3), offset: [0.0; 2], scale: 1.0 }).img;
|
||||
assert!(o.data.iter().all(|p| p[3] == 0.0));
|
||||
}
|
||||
|
||||
/// Instances saved before the propagation controls got their own twirl-down (and motion
|
||||
/// blur / decontamination their nested ones) load with their values in the new places.
|
||||
#[test]
|
||||
fn old_group_layout_is_migrated_on_load() {
|
||||
use effectcraft_project::build::Ids;
|
||||
let spec = crate::find(ID).unwrap();
|
||||
let mut next = 1;
|
||||
let mut ids = Ids(&mut next);
|
||||
// The old layout: everything under Roto Brush Matte / Refine Edge Matte.
|
||||
let mut g = ids.group(ID, "Roto Brush & Refine Edge");
|
||||
g.kind = effectcraft_project::GroupKind::Effect { effect: ID.into() };
|
||||
let mut rb = ids.group("rotoBrushMatte", "Roto Brush Matte");
|
||||
rb.children.push(ids.prop("searchRadius", "Search Radius", Value::Scalar(33.0)).into());
|
||||
rb.children.push(ids.prop("feather", "Feather", Value::Scalar(2.0)).into());
|
||||
let mut re = ids.group("refineEdgeMatte", "Refine Edge Matte");
|
||||
re.children.push(ids.prop("shutterAngle", "Shutter Angle", Value::Scalar(90.0)).into());
|
||||
re.children.push(ids.prop("decontaminationAmount", "Decontamination Amount", Value::Scalar(40.0)).into());
|
||||
g.children.push(rb.into());
|
||||
g.children.push(re.into());
|
||||
assert!(crate::migrate::upgrade_instance(spec, &mut g, &mut ids, [100.0, 80.0]));
|
||||
let params = crate::flatten_params(&g, &mut |p| p.value.clone());
|
||||
assert_eq!(f(¶ms, "rotoBrushPropagation/searchRadius"), 33.0);
|
||||
assert_eq!(f(¶ms, "rotoBrushMatte/feather"), 2.0);
|
||||
assert_eq!(f(¶ms, "refineEdgeMatte/motionBlur/shutterAngle"), 90.0);
|
||||
assert_eq!(f(¶ms, "refineEdgeMatte/decontamination/decontaminationAmount"), 40.0);
|
||||
assert!(g.sub("rotoBrushMatte").unwrap().get("searchRadius").is_none());
|
||||
assert_eq!(g.sub("rotoBrushPropagation").unwrap().name, "Roto Brush Propagation");
|
||||
// Parameters the old save lacked get their defaults.
|
||||
assert_eq!(f(¶ms, "rotoBrushPropagation/motionDamping"), 20.0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,9 +328,9 @@ fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
let opacity = pr.f("opacity") as f32 / 100.0;
|
||||
let mode = pr.e("transferMode");
|
||||
let with_orig = pr.b("compositeWithOriginal");
|
||||
let refract = pr.e("appearance") == 0;
|
||||
let off = pr.v2("offset");
|
||||
let seed = pr.f("randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x9e37);
|
||||
let refract = pr.e("extras/appearance") == 0;
|
||||
let off = pr.v2("extras/offset");
|
||||
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x9e37);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
|
|
@ -382,16 +382,16 @@ fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
let wind = pr.f("wind") as f32;
|
||||
let wvar = pr.f("windVariation") as f32 / 100.0;
|
||||
let spread = pr.f("spread") as f32;
|
||||
let wig = pr.f("wiggleAmount") as f32;
|
||||
let wigvar = pr.f("wiggleAmountVariation") as f32 / 100.0;
|
||||
let wfreq = pr.f("wiggleFrequency") as f32;
|
||||
let wfvar = pr.f("wiggleFrequencyVariation") as f32 / 100.0;
|
||||
let wig = pr.f("wiggle/wiggleAmount") as f32;
|
||||
let wigvar = pr.f("wiggle/wiggleAmountVariation") as f32 / 100.0;
|
||||
let wfreq = pr.f("wiggle/wiggleFrequency") as f32;
|
||||
let wfvar = pr.f("wiggle/wiggleFrequencyVariation") as f32 / 100.0;
|
||||
let color = pr.color("color");
|
||||
let opacity = pr.f("opacity") as f32 / 100.0;
|
||||
let mode = pr.e("transferMode");
|
||||
let with_orig = pr.b("compositeWithOriginal");
|
||||
let off = pr.v2("offset");
|
||||
let seed = pr.f("randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x85eb);
|
||||
let off = pr.v2("extras/offset");
|
||||
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x85eb);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
|
|
@ -503,14 +503,14 @@ fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let disp = pr.f("displacement") as f32;
|
||||
let height = pr.f("rippleHeight") as f32 / 100.0;
|
||||
let spreading = pr.f("spreading") as f32;
|
||||
let li = pr.f("lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("lightColor");
|
||||
let lheight = pr.f("lightHeight") as f32 / 100.0;
|
||||
let ldir = (pr.f("lightDirection") as f32).to_radians();
|
||||
let ambient = pr.f("ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("specular") as f32 / 100.0;
|
||||
let rough = pr.f("roughness").max(0.001) as f32;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||||
let seed = ctx.seed.wrapping_mul(0x165667b1);
|
||||
let t = ctx.time;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
|
|
@ -865,20 +865,21 @@ fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let phys = Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 120.0,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pr.f("producerX") as f32, pr.f("producerY") as f32, pr.f("producerZ") as f32],
|
||||
radius: [pr.f("radiusX").max(0.0) as f32, pr.f("radiusY").max(0.0) as f32, pr.f("radiusZ").max(0.0) as f32],
|
||||
anim: pr.e("animation"),
|
||||
speed: pr.f("velocity") as f32 * 0.5,
|
||||
producer: [pr.f("producer/producerX") as f32, pr.f("producer/producerY") as f32, pr.f("producer/producerZ") as f32],
|
||||
radius: [pr.f("producer/radiusX").max(0.0) as f32, pr.f("producer/radiusY").max(0.0) as f32, pr.f("producer/radiusZ").max(0.0) as f32],
|
||||
anim: pr.e("physics/animation"),
|
||||
speed: pr.f("physics/velocity") as f32 * 0.5,
|
||||
gravity: {
|
||||
let g = pr.f("gravity") as f32;
|
||||
let gv = [pr.f("gravityX") as f32, pr.f("gravityY") as f32, pr.f("gravityZ") as f32];
|
||||
let g = pr.f("physics/gravity") as f32;
|
||||
let gv =
|
||||
[pr.f("physics/gravityVector/gravityX") as f32, pr.f("physics/gravityVector/gravityY") as f32, pr.f("physics/gravityVector/gravityZ") as f32];
|
||||
[gv[0] * g, gv[1] * g, gv[2] * g]
|
||||
},
|
||||
resistance: pr.f("resistance").max(0.0) as f32,
|
||||
axis: [pr.f("axisX") as f32, pr.f("axisY") as f32, pr.f("axisZ") as f32],
|
||||
extra: pr.f("extra") as f32,
|
||||
extra_angle: pr.f("extraAngle") as f32,
|
||||
seed: (pr.f("randomSeed") as u32).wrapping_mul(0x9e3779b9) ^ ctx.seed,
|
||||
resistance: pr.f("physics/resistance").max(0.0) as f32,
|
||||
axis: [pr.f("physics/directionAxis/axisX") as f32, pr.f("physics/directionAxis/axisY") as f32, pr.f("physics/directionAxis/axisZ") as f32],
|
||||
extra: pr.f("physics/extra") as f32,
|
||||
extra_angle: pr.f("physics/extraAngle") as f32,
|
||||
seed: (pr.f("extras/randomSeed") as u32).wrapping_mul(0x9e3779b9) ^ ctx.seed,
|
||||
max: 40_000,
|
||||
flat: false,
|
||||
};
|
||||
|
|
@ -886,12 +887,12 @@ fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
return b;
|
||||
}
|
||||
let st = simulate(&PW_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 1), ctx.time, &phys);
|
||||
let kind = pr.e("particleType");
|
||||
let birth = pr.color("birthColor");
|
||||
let death = pr.color("deathColor");
|
||||
let (bs, ds) = (pr.f("birthSize") as f32, pr.f("deathSize") as f32);
|
||||
let svar = pr.f("sizeVariation") as f32 / 100.0;
|
||||
let max_op = pr.f("maxOpacity") as f32 / 100.0;
|
||||
let kind = pr.e("particle/particleType");
|
||||
let birth = pr.color("particle/birthColor");
|
||||
let death = pr.color("particle/deathColor");
|
||||
let (bs, ds) = (pr.f("particle/birthSize") as f32, pr.f("particle/deathSize") as f32);
|
||||
let svar = pr.f("particle/sizeVariation") as f32 / 100.0;
|
||||
let max_op = pr.f("particle/maxOpacity") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
// Camera at distance 2 (world units = layer width) looking at the origin (layer centre).
|
||||
let cam = 2.0f32;
|
||||
|
|
@ -915,7 +916,7 @@ fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
.collect();
|
||||
list.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
let sprites: Vec<Sprite> = list.into_iter().map(|(_, s)| s).collect();
|
||||
let mode = pr.e("transferMode");
|
||||
let mode = pr.e("particle/transferMode");
|
||||
let fx = splat(b.img.width, b.img.height, &sprites, if mode == 2 { Acc::Add } else { Acc::Over });
|
||||
b.img = combine(
|
||||
&b.img,
|
||||
|
|
@ -939,32 +940,32 @@ fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let pos = pr.v2("position");
|
||||
let dir = (pr.f("direction") as f32).to_radians();
|
||||
let pos = pr.v2("producer/position");
|
||||
let dir = (pr.f("physics/direction") as f32).to_radians();
|
||||
let phys = Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 60.0,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pos[0] as f32 / unit, pos[1] as f32 / unit, 0.0],
|
||||
radius: [pr.f("radiusX").max(0.0) as f32 / unit, pr.f("radiusY").max(0.0) as f32 / unit, 0.0],
|
||||
anim: [0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9][pr.e("animation").min(9) as usize],
|
||||
speed: pr.f("velocity") as f32 * 0.25,
|
||||
gravity: [0.0, pr.f("gravity") as f32 * 0.5, 0.0],
|
||||
resistance: pr.f("resistance").max(0.0) as f32,
|
||||
radius: [pr.f("producer/radiusX").max(0.0) as f32 / unit, pr.f("producer/radiusY").max(0.0) as f32 / unit, 0.0],
|
||||
anim: [0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9][pr.e("physics/animation").min(9) as usize],
|
||||
speed: pr.f("physics/velocity") as f32 * 0.25,
|
||||
gravity: [0.0, pr.f("physics/gravity") as f32 * 0.5, 0.0],
|
||||
resistance: pr.f("physics/resistance").max(0.0) as f32,
|
||||
// AE angles: 0° = up, clockwise.
|
||||
axis: [dir.sin(), -dir.cos(), 0.0],
|
||||
extra: pr.f("extra") as f32,
|
||||
extra_angle: 45.0 * pr.f("extra") as f32,
|
||||
extra: pr.f("physics/extra") as f32,
|
||||
extra_angle: 45.0 * pr.f("physics/extra") as f32,
|
||||
seed: (pr.f("randomSeed") as u32).wrapping_mul(0x85ebca6b) ^ ctx.seed,
|
||||
max: 40_000,
|
||||
flat: true,
|
||||
};
|
||||
let st = simulate(&PS2_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 2), ctx.time, &phys);
|
||||
let kind = pr.e("particleType");
|
||||
let birth = pr.color("birthColor");
|
||||
let death = pr.color("deathColor");
|
||||
let (bs, ds) = (pr.f("birthSize") as f32, pr.f("deathSize") as f32);
|
||||
let svar = pr.f("sizeVariation") as f32 / 100.0;
|
||||
let max_op = pr.f("maxOpacity") as f32 / 100.0;
|
||||
let kind = pr.e("particle/particleType");
|
||||
let birth = pr.color("particle/birthColor");
|
||||
let death = pr.color("particle/deathColor");
|
||||
let (bs, ds) = (pr.f("particle/birthSize") as f32, pr.f("particle/deathSize") as f32);
|
||||
let svar = pr.f("particle/sizeVariation") as f32 / 100.0;
|
||||
let max_op = pr.f("particle/maxOpacity") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let sprites: Vec<Sprite> = st
|
||||
.parts
|
||||
|
|
@ -976,7 +977,7 @@ fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
type_sprite(kind, bx as f32, by as f32, r, c, q.v[0] * unit * s, q.v[1] * unit * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0)
|
||||
})
|
||||
.collect();
|
||||
let mode = pr.e("transferMode");
|
||||
let mode = pr.e("particle/transferMode");
|
||||
let fx = splat(b.img.width, b.img.height, &sprites, if mode == 2 { Acc::Add } else { Acc::Over });
|
||||
b.img = combine(
|
||||
&b.img,
|
||||
|
|
@ -1065,15 +1066,15 @@ fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
Acc::Add,
|
||||
);
|
||||
let thr = 0.35f32;
|
||||
let ambient = pr.f("ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("specular") as f32 / 100.0;
|
||||
let rough = pr.f("roughness").max(0.001) as f32;
|
||||
let metal = pr.f("metal") as f32 / 100.0;
|
||||
let li = pr.f("lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("lightColor");
|
||||
let ldir = (pr.f("lightDirection") as f32).to_radians();
|
||||
let lheight = pr.f("lightHeight") as f32 / 100.0;
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||||
let metal = pr.f("shading/metal") as f32 / 100.0;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let l = norm3([ldir.sin(), -ldir.cos(), lheight.max(0.05)]);
|
||||
let src = b.img.clone();
|
||||
let mut out = Image::new(w, hh);
|
||||
|
|
@ -1122,9 +1123,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("opacity", "Opacity", num(25.0), pct()),
|
||||
p("transferMode", "Transfer Mode", Value::Enum(1), transfer()),
|
||||
p("compositeWithOriginal", "Composite With Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("appearance", "Appearance", Value::Enum(0), popup(&["Refracting", "Soft Solid"])),
|
||||
p("offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
p("extras/appearance", "Appearance", Value::Enum(0), popup(&["Refracting", "Soft Solid"])),
|
||||
p("extras/offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
rainfall,
|
||||
),
|
||||
|
|
@ -1141,16 +1142,16 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("wind", "Wind", num(25.0), slider(-5000.0, 5000.0, -500.0, 500.0, 1)),
|
||||
p("windVariation", "Variation % (Wind)", num(50.0), pct()),
|
||||
p("spread", "Spread", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("wiggleAmount", "Amount (Wiggle)", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("wiggleAmountVariation", "Variation % (Amount)", num(50.0), pct()),
|
||||
p("wiggleFrequency", "Frequency (Wiggle)", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("wiggleFrequencyVariation", "Variation % (Frequency)", num(50.0), pct()),
|
||||
p("wiggle/wiggleAmount", "Amount", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("wiggle/wiggleAmountVariation", "Variation % (Amount)", num(50.0), pct()),
|
||||
p("wiggle/wiggleFrequency", "Frequency", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("wiggle/wiggleFrequencyVariation", "Variation % (Frequency)", num(50.0), pct()),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p("transferMode", "Transfer Mode", Value::Enum(0), transfer()),
|
||||
p("compositeWithOriginal", "Composite With Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
p("extras/offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
snowfall,
|
||||
),
|
||||
|
|
@ -1178,14 +1179,14 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("displacement", "Displacement", num(5.0), slider(0.0, 100.0, 0.0, 30.0, 2)),
|
||||
p("rippleHeight", "Ripple Height", num(30.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("spreading", "Spreading", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||||
p("lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("lightHeight", "Light Height", num(25.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
p("ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||||
p("diffuse", "Diffuse", num(0.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("specular", "Specular", num(20.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||||
p("light/lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("light/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("light/lightHeight", "Light Height", num(25.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("light/lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
p("shading/ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||||
p("shading/diffuse", "Diffuse", num(0.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/specular", "Specular", num(20.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||||
],
|
||||
drizzle,
|
||||
),
|
||||
|
|
@ -1208,34 +1209,34 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("birthRate", "Birth Rate", num(0.5), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("producerX", "Position X", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producerY", "Position Y", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producerZ", "Position Z", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("radiusX", "Radius X", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("radiusY", "Radius Y", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("radiusZ", "Radius Z", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||||
p("velocity", "Velocity", num(1.0), slider(-10.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("gravity", "Gravity", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 3)),
|
||||
p("resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("extraAngle", "Extra Angle", num(360.0), ParamUi::Angle),
|
||||
p("axisX", "Direction Axis X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("axisY", "Direction Axis Y", num(-1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("axisZ", "Direction Axis Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("gravityX", "Gravity Vector X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("gravityY", "Gravity Vector Y", num(1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("gravityZ", "Gravity Vector Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||||
p("birthSize", "Birth Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("deathSize", "Death Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("sizeVariation", "Size Variation", num(0.0), pct()),
|
||||
p("maxOpacity", "Max Opacity", num(75.0), pct()),
|
||||
p("birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||||
p("transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
p("producer/producerX", "Position X", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producer/producerY", "Position Y", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producer/producerZ", "Position Z", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producer/radiusX", "Radius X", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("producer/radiusY", "Radius Y", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("producer/radiusZ", "Radius Z", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("physics/animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||||
p("physics/velocity", "Velocity", num(1.0), slider(-10.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("physics/inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("physics/gravity", "Gravity", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 3)),
|
||||
p("physics/resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("physics/extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("physics/extraAngle", "Extra Angle", num(360.0), ParamUi::Angle),
|
||||
p("physics/directionAxis/axisX", "Axis X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/directionAxis/axisY", "Axis Y", num(-1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/directionAxis/axisZ", "Axis Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/gravityVector/gravityX", "Gravity X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/gravityVector/gravityY", "Gravity Y", num(1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/gravityVector/gravityZ", "Gravity Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("particle/particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||||
p("particle/birthSize", "Birth Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("particle/deathSize", "Death Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("particle/sizeVariation", "Size Variation", num(0.0), pct()),
|
||||
p("particle/maxOpacity", "Max Opacity", num(75.0), pct()),
|
||||
p("particle/birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("particle/deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||||
p("particle/transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
particle_world,
|
||||
),
|
||||
|
|
@ -1245,24 +1246,24 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("birthRate", "Birth Rate", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("radiusX", "Radius X", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||||
p("radiusY", "Radius Y", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||||
p("animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||||
p("velocity", "Velocity", num(2.0), slider(-20.0, 20.0, 0.0, 10.0, 2)),
|
||||
p("inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||||
p("resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||||
p("birthSize", "Birth Size", num(0.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("deathSize", "Death Size", num(1.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("sizeVariation", "Size Variation", num(0.0), pct()),
|
||||
p("maxOpacity", "Max Opacity", num(75.0), pct()),
|
||||
p("birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||||
p("transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||||
p("producer/position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("producer/radiusX", "Radius X", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||||
p("producer/radiusY", "Radius Y", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||||
p("physics/animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||||
p("physics/velocity", "Velocity", num(2.0), slider(-20.0, 20.0, 0.0, 10.0, 2)),
|
||||
p("physics/inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("physics/gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||||
p("physics/resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("physics/direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("physics/extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("particle/particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||||
p("particle/birthSize", "Birth Size", num(0.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("particle/deathSize", "Death Size", num(1.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("particle/sizeVariation", "Size Variation", num(0.0), pct()),
|
||||
p("particle/maxOpacity", "Max Opacity", num(75.0), pct()),
|
||||
p("particle/birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("particle/deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||||
p("particle/transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
particle_systems2,
|
||||
|
|
@ -1285,15 +1286,15 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("blobInfluence", "Blob Influence", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("blobBirthSize", "Blob Birth Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("blobDeathSize", "Blob Death Size", num(0.25), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("lightHeight", "Light Height", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
p("ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||||
p("diffuse", "Diffuse", num(75.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("specular", "Specular", num(50.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||||
p("metal", "Metal", num(100.0), pct()),
|
||||
p("light/lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("light/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("light/lightHeight", "Light Height", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("light/lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
p("shading/ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||||
p("shading/diffuse", "Diffuse", num(75.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/specular", "Specular", num(50.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||||
p("shading/metal", "Metal", num(100.0), pct()),
|
||||
],
|
||||
mr_mercury,
|
||||
),
|
||||
|
|
@ -1357,7 +1358,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn rainfall_deterministic_and_falls() {
|
||||
let vals = [("drops", num(200.0)), ("opacity", num(100.0)), ("appearance", Value::Enum(1)), ("color", col(1.0, 1.0, 1.0))];
|
||||
let vals = [("drops", num(200.0)), ("opacity", num(100.0)), ("extras/appearance", Value::Enum(1)), ("color", col(1.0, 1.0, 1.0))];
|
||||
let a = run("ec.sim.ccrainfall", &vals, grey(40, 40), 0.5);
|
||||
let b = run("ec.sim.ccrainfall", &vals, grey(40, 40), 0.5);
|
||||
assert_eq!(a, b);
|
||||
|
|
@ -1416,7 +1417,13 @@ mod tests {
|
|||
}
|
||||
|
||||
fn pw_vals(seed: f64) -> Vec<(&'static str, Value)> {
|
||||
vec![("birthRate", num(1.0)), ("randomSeed", num(seed)), ("particleType", Value::Enum(2)), ("birthSize", num(0.3)), ("deathSize", num(0.3))]
|
||||
vec![
|
||||
("birthRate", num(1.0)),
|
||||
("extras/randomSeed", num(seed)),
|
||||
("particle/particleType", Value::Enum(2)),
|
||||
("particle/birthSize", num(0.3)),
|
||||
("particle/deathSize", num(0.3)),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1470,11 +1477,11 @@ mod tests {
|
|||
let base = |g: f64| {
|
||||
vec![
|
||||
("birthRate", num(2.0)),
|
||||
("velocity", num(0.2)),
|
||||
("gravity", num(g)),
|
||||
("particleType", Value::Enum(2)),
|
||||
("birthSize", num(0.2)),
|
||||
("deathSize", num(0.2)),
|
||||
("physics/velocity", num(0.2)),
|
||||
("physics/gravity", num(g)),
|
||||
("particle/particleType", Value::Enum(2)),
|
||||
("particle/birthSize", num(0.2)),
|
||||
("particle/deathSize", num(0.2)),
|
||||
("longevity", num(3.0)),
|
||||
]
|
||||
};
|
||||
|
|
@ -1486,7 +1493,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn particle_systems2_seek_and_direction() {
|
||||
let v = vec![("position", pt(24.0, 24.0)), ("birthRate", num(3.0)), ("gravity", num(0.0)), ("velocity", num(1.0)), ("animation", Value::Enum(1))];
|
||||
let v = vec![
|
||||
("producer/position", pt(24.0, 24.0)),
|
||||
("birthRate", num(3.0)),
|
||||
("physics/gravity", num(0.0)),
|
||||
("physics/velocity", num(1.0)),
|
||||
("physics/animation", Value::Enum(1)),
|
||||
];
|
||||
let _ = run("ec.sim.ccparticlesystems2", &v, Image::new(48, 48), 0.5);
|
||||
let a = run("ec.sim.ccparticlesystems2", &v, Image::new(48, 48), 1.0);
|
||||
let mut v2 = v.clone();
|
||||
|
|
@ -1506,7 +1519,7 @@ mod tests {
|
|||
let _ = run("ec.sim.ccmrmercury", &v, ramp(48, 48), 0.7);
|
||||
let a = run("ec.sim.ccmrmercury", &v, ramp(48, 48), 1.4);
|
||||
let mut v2 = v.clone();
|
||||
v2.push(("metal", num(100.0)));
|
||||
v2.push(("shading/metal", num(100.0)));
|
||||
let b = run("ec.sim.ccmrmercury", &v2, ramp(48, 48), 1.4);
|
||||
assert_eq!(a, b);
|
||||
assert!(a.data.iter().any(|p| p[3] > 0.5), "blobs visible");
|
||||
|
|
|
|||
|
|
@ -389,15 +389,16 @@ fn scatterize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
// ------------------------------------------------------------------ Card Dance
|
||||
|
||||
const CD_SOURCES: &[&str] = &["None", "Intensity 1", "Red 1", "Green 1", "Blue 1", "Alpha 1", "Intensity 2", "Red 2", "Green 2", "Blue 2", "Alpha 2"];
|
||||
/// (param id prefix, twirl-down group match id) per transformed card property.
|
||||
const CD_PROPS: [(&str, &str); 8] = [
|
||||
("xPos", "X Position"),
|
||||
("yPos", "Y Position"),
|
||||
("zPos", "Z Position"),
|
||||
("xRot", "X Rotation"),
|
||||
("yRot", "Y Rotation"),
|
||||
("zRot", "Z Rotation"),
|
||||
("xScale", "X Scale"),
|
||||
("yScale", "Y Scale"),
|
||||
("xPos", "xPosition"),
|
||||
("yPos", "yPosition"),
|
||||
("zPos", "zPosition"),
|
||||
("xRot", "xRotation"),
|
||||
("yRot", "yRotation"),
|
||||
("zRot", "zRotation"),
|
||||
("xScale", "xScale"),
|
||||
("yScale", "yScale"),
|
||||
];
|
||||
|
||||
fn card_source(src: u32, g1: [f32; 4], g2: [f32; 4]) -> f32 {
|
||||
|
|
@ -429,14 +430,14 @@ fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
let (cw, ch) = (lw / cols as f64, lh / rows as f64);
|
||||
let zpos = pr.f("cameraZ").max(0.05);
|
||||
let focal = pr.f("focalLength").max(1.0);
|
||||
let zpos = pr.f("cameraPosition/cameraZ").max(0.05);
|
||||
let focal = pr.f("cameraPosition/focalLength").max(1.0);
|
||||
let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: lw.max(lh) * s * zpos * focal / 140.0 };
|
||||
let ambient = pr.f("ambientLight") as f32;
|
||||
let diffuse = pr.f("diffuse") as f32;
|
||||
let intensity = pr.f("lightIntensity") as f32;
|
||||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||||
let diffuse = pr.f("material/diffuse") as f32;
|
||||
let intensity = pr.f("lighting/lightIntensity") as f32;
|
||||
let props: Vec<(u32, f64, f64)> =
|
||||
CD_PROPS.iter().map(|(id, _)| (pr.e(&format!("{id}Source")), pr.f(&format!("{id}Multiplier")), pr.f(&format!("{id}Offset")))).collect();
|
||||
CD_PROPS.iter().map(|(id, g)| (pr.e(&format!("{g}/{id}Source")), pr.f(&format!("{g}/{id}Multiplier")), pr.f(&format!("{g}/{id}Offset")))).collect();
|
||||
let src = b.img.clone();
|
||||
let mut pieces: Vec<Piece> = (0..rows * cols)
|
||||
.into_par_iter()
|
||||
|
|
@ -574,24 +575,24 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let pr = ctx.params;
|
||||
let view = pr.e("view");
|
||||
let render = pr.e("render");
|
||||
let kind = pr.e("pattern");
|
||||
let reps = pr.f("repetitions").clamp(1.0, 500.0);
|
||||
let dir = pr.f("direction").to_radians();
|
||||
let origin = pr.v2("origin");
|
||||
let kind = pr.e("shape/pattern");
|
||||
let reps = pr.f("shape/repetitions").clamp(1.0, 500.0);
|
||||
let dir = pr.f("shape/direction").to_radians();
|
||||
let origin = pr.v2("shape/origin");
|
||||
let forces = [
|
||||
(pr.v2("force1Position"), pr.f("force1Depth"), pr.f("force1Radius"), pr.f("force1Strength")),
|
||||
(pr.v2("force2Position"), pr.f("force2Depth"), pr.f("force2Radius"), pr.f("force2Strength")),
|
||||
(pr.v2("force1/force1Position"), pr.f("force1/force1Depth"), pr.f("force1/force1Radius"), pr.f("force1/force1Strength")),
|
||||
(pr.v2("force2/force2Position"), pr.f("force2/force2Depth"), pr.f("force2/force2Radius"), pr.f("force2/force2Strength")),
|
||||
];
|
||||
let rot_speed = pr.f("rotationSpeed");
|
||||
let tumble = pr.e("tumbleAxis");
|
||||
let randomness = pr.f("randomness");
|
||||
let k = pr.f("viscosity").max(0.0) * 5.0;
|
||||
let mass_var = pr.f("massVariance") / 100.0;
|
||||
let gravity = pr.f("gravity");
|
||||
let gdir = pr.f("gravityDirection").to_radians();
|
||||
let ginc = pr.f("gravityInclination").to_radians();
|
||||
let ambient = pr.f("ambientLight") as f32;
|
||||
let intensity = pr.f("lightIntensity") as f32;
|
||||
let rot_speed = pr.f("physics/rotationSpeed");
|
||||
let tumble = pr.e("physics/tumbleAxis");
|
||||
let randomness = pr.f("physics/randomness");
|
||||
let k = pr.f("physics/viscosity").max(0.0) * 5.0;
|
||||
let mass_var = pr.f("physics/massVariance") / 100.0;
|
||||
let gravity = pr.f("physics/gravity");
|
||||
let gdir = pr.f("physics/gravityDirection").to_radians();
|
||||
let ginc = pr.f("physics/gravityInclination").to_radians();
|
||||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||||
let intensity = pr.f("lighting/lightIntensity") as f32;
|
||||
let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x9e3779b1) ^ ctx.seed;
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
|
|
@ -747,31 +748,31 @@ fn sample_repeat(img: &Image, x: f64, y: f64, mode: u32) -> Px {
|
|||
fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let s = b.scale;
|
||||
let mut bottom = layer_or_self(ctx, &b, "bottom", true, pr.e("bottomSizeDiffers") == 1);
|
||||
let blur = pr.f("blur") * s;
|
||||
let mut bottom = layer_or_self(ctx, &b, "bottom/bottom", true, pr.e("bottom/bottomSizeDiffers") == 1);
|
||||
let blur = pr.f("bottom/blur") * s;
|
||||
if blur > 0.05 {
|
||||
bottom = effectcraft_raster::gaussian_blur(&bottom, blur * 0.5, blur * 0.5, false);
|
||||
}
|
||||
let scaling = pr.f("scaling").max(0.01);
|
||||
let repeat = pr.e("repeatMode");
|
||||
let wave_h = pr.f("waveHeight");
|
||||
let smoothing = pr.f("smoothing") * s;
|
||||
let depth = pr.f("waterDepth");
|
||||
let ior = pr.f("refractiveIndex").max(1.0);
|
||||
let surf = pr.color("surfaceColor");
|
||||
let surf_op = pr.f("surfaceOpacity") as f32;
|
||||
let cstr = pr.f("causticsStrength") as f32;
|
||||
let li = pr.f("lightIntensity") as f32;
|
||||
let lc = pr.color("lightColor");
|
||||
let lpos = pr.v2("lightPosition");
|
||||
let lheight = pr.f("lightHeight").max(0.01);
|
||||
let ambient = pr.f("ambientLight") as f32;
|
||||
let diffuse = pr.f("diffuse") as f32;
|
||||
let specular = pr.f("specular") as f32;
|
||||
let sharp = pr.f("highlightSharpness").max(1.0) as f32;
|
||||
let scaling = pr.f("bottom/scaling").max(0.01);
|
||||
let repeat = pr.e("bottom/repeatMode");
|
||||
let wave_h = pr.f("water/waveHeight");
|
||||
let smoothing = pr.f("water/smoothing") * s;
|
||||
let depth = pr.f("water/waterDepth");
|
||||
let ior = pr.f("water/refractiveIndex").max(1.0);
|
||||
let surf = pr.color("water/surfaceColor");
|
||||
let surf_op = pr.f("water/surfaceOpacity") as f32;
|
||||
let cstr = pr.f("water/causticsStrength") as f32;
|
||||
let li = pr.f("lighting/lightIntensity") as f32;
|
||||
let lc = pr.color("lighting/lightColor");
|
||||
let lpos = pr.v2("lighting/lightPosition");
|
||||
let lheight = pr.f("lighting/lightHeight").max(0.01);
|
||||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||||
let diffuse = pr.f("material/diffuse") as f32;
|
||||
let specular = pr.f("material/specular") as f32;
|
||||
let sharp = pr.f("material/highlightSharpness").max(1.0) as f32;
|
||||
let (w, hh) = (b.img.width as usize, b.img.height as usize);
|
||||
// Height field of the water surface (luminance of the chosen layer; none = flat).
|
||||
let height = ctx.layer_param("waterSurface", true).map(|o| {
|
||||
let height = ctx.layer_param("water/waterSurface", true).map(|o| {
|
||||
let img = crate::util::fit_layer(ctx, &b, &o, true);
|
||||
let pl = Plane::luma(&img);
|
||||
if smoothing > 0.05 { gauss_plane(&pl, smoothing * 0.5, smoothing * 0.5) } else { pl }
|
||||
|
|
@ -887,27 +888,27 @@ static WAVE_CACHE: SimCache<Waves> = SimCache::new(4);
|
|||
fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let res = pr.f("gridResolution").clamp(1.0, 400.0) as usize;
|
||||
let res = if pr.b("gridResDownsamples") { ((res as f64) * b.scale).round().max(1.0) as usize } else { res };
|
||||
let res = pr.f("simulation/gridResolution").clamp(1.0, 400.0) as usize;
|
||||
let res = if pr.b("simulation/gridResDownsamples") { ((res as f64) * b.scale).round().max(1.0) as usize } else { res };
|
||||
let nx = res.max(2) + 1;
|
||||
let ny = ((res as f64 * lh / lw.max(1.0)).round() as usize).max(2) + 1;
|
||||
let speed = pr.f("waveSpeed").max(0.0) as f32;
|
||||
let damping = pr.f("damping").max(0.0) as f32;
|
||||
let reflect = pr.e("reflectEdges");
|
||||
let preroll = pr.f("preRoll").max(0.0);
|
||||
let speed = pr.f("simulation/waveSpeed").max(0.0) as f32;
|
||||
let damping = pr.f("simulation/damping").max(0.0) as f32;
|
||||
let reflect = pr.e("simulation/reflectEdges");
|
||||
let preroll = pr.f("simulation/preRoll").max(0.0);
|
||||
let prods: Vec<Producer> = [1, 2]
|
||||
.iter()
|
||||
.map(|k| {
|
||||
let pos = pr.v2(&format!("producer{k}Position"));
|
||||
let pos = pr.v2(&format!("producer{k}/producer{k}Position"));
|
||||
Producer {
|
||||
ring: pr.e(&format!("producer{k}Type")) == 0,
|
||||
ring: pr.e(&format!("producer{k}/producer{k}Type")) == 0,
|
||||
pos: [(pos[0] / lw.max(1.0)) as f32, (pos[1] / lw.max(1.0)) as f32],
|
||||
len: pr.f(&format!("producer{k}Length")) as f32,
|
||||
width: pr.f(&format!("producer{k}Width")) as f32,
|
||||
angle: (pr.f(&format!("producer{k}Angle")) as f32).to_radians(),
|
||||
amp: pr.f(&format!("producer{k}Amplitude")) as f32,
|
||||
freq: pr.f(&format!("producer{k}Frequency")) as f32,
|
||||
phase: (pr.f(&format!("producer{k}Phase")) as f32).to_radians(),
|
||||
len: pr.f(&format!("producer{k}/producer{k}Length")) as f32,
|
||||
width: pr.f(&format!("producer{k}/producer{k}Width")) as f32,
|
||||
angle: (pr.f(&format!("producer{k}/producer{k}Angle")) as f32).to_radians(),
|
||||
amp: pr.f(&format!("producer{k}/producer{k}Amplitude")) as f32,
|
||||
freq: pr.f(&format!("producer{k}/producer{k}Frequency")) as f32,
|
||||
phase: (pr.f(&format!("producer{k}/producer{k}Phase")) as f32).to_radians(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -918,7 +919,7 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let dt = 1.0 / (sps * sub as f32);
|
||||
let cell = 1.0 / (nx - 1) as f32;
|
||||
let weights: Vec<Vec<f32>> = prods.iter().map(|p| (0..nx * ny).map(|i| p.weight((i % nx) as f32 * cell, (i / nx) as f32 * cell)).collect()).collect();
|
||||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: if pr.b("gridResDownsamples") { b.scale } else { 1.0 } }, 4);
|
||||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: if pr.b("simulation/gridResDownsamples") { b.scale } else { 1.0 } }, 4);
|
||||
let steps = crate::sim::steps_at(ctx.time + preroll);
|
||||
let st = WAVE_CACHE.run(
|
||||
key,
|
||||
|
|
@ -997,10 +998,10 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
a + (c - a) * ty
|
||||
};
|
||||
if pr.e("view") == 1 {
|
||||
let bright = pr.f("brightness") as f32;
|
||||
let contrast = pr.f("contrast") as f32;
|
||||
let gamma = pr.f("gamma").max(0.01) as f32;
|
||||
let alpha = 1.0 - pr.f("transparency") as f32;
|
||||
let bright = pr.f("heightMapControls/brightness") as f32;
|
||||
let contrast = pr.f("heightMapControls/contrast") as f32;
|
||||
let gamma = pr.f("heightMapControls/gamma").max(0.01) as f32;
|
||||
let alpha = 1.0 - pr.f("heightMapControls/transparency") as f32;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let lx = (x as f64 + 0.5 - b.offset[0]) / s;
|
||||
|
|
@ -1061,26 +1062,35 @@ static FOAM_CACHE: SimCache<FoamState> = SimCache::new(4);
|
|||
/// Foam steps once per frame at 30 frames per second.
|
||||
const FOAM_FPS: f64 = 30.0;
|
||||
|
||||
/// Age (in simulation steps) at which bubble `id` pops: its lifespan, shortened at random for
|
||||
/// frail bubbles (`frailty` 0 = every bubble reaches `lifespan`, 1 = uniformly early).
|
||||
fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 {
|
||||
lifespan * (1.0 - frailty * (hs(id, 11, seed) * 0.5 + 0.5))
|
||||
}
|
||||
|
||||
fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let unit = lw * 0.01;
|
||||
let prod = pr.v2("producerPoint");
|
||||
let (psx, psy) = (pr.f("producerXSize") as f32 * lw, pr.f("producerYSize") as f32 * lh);
|
||||
let porient = (pr.f("producerOrientation") as f32).to_radians();
|
||||
let rate = pr.f("productionRate").max(0.0) as f32;
|
||||
let size = pr.f("size").max(0.0) as f32;
|
||||
let size_var = pr.f("sizeVariance") as f32;
|
||||
let lifespan = pr.f("lifespan").max(1.0) as f32;
|
||||
let growth = pr.f("growthSpeed").max(0.0001) as f32;
|
||||
let init_speed = pr.f("initialSpeed") as f32;
|
||||
let init_dir = (pr.f("initialDirection") as f32).to_radians();
|
||||
let wind = pr.f("windSpeed") as f32;
|
||||
let wind_dir = (pr.f("windDirection") as f32).to_radians();
|
||||
let turb = pr.f("turbulence") as f32;
|
||||
let repulsion = pr.f("repulsion") as f32;
|
||||
let viscosity = pr.f("viscosity").clamp(0.0, 1.0) as f32;
|
||||
let sticky = pr.f("stickiness").clamp(0.0, 1.0) as f32;
|
||||
let prod = pr.v2("producer/producerPoint");
|
||||
let (psx, psy) = (pr.f("producer/producerXSize") as f32 * lw, pr.f("producer/producerYSize") as f32 * lh);
|
||||
let porient = (pr.f("producer/producerOrientation") as f32).to_radians();
|
||||
let rate = pr.f("producer/productionRate").max(0.0) as f32;
|
||||
let size = pr.f("bubbles/size").max(0.0) as f32;
|
||||
let size_var = pr.f("bubbles/sizeVariance") as f32;
|
||||
let lifespan = pr.f("bubbles/lifespan").max(1.0) as f32;
|
||||
let growth = pr.f("bubbles/growthSpeed").max(0.0001) as f32;
|
||||
// Strength: weaker bubbles tend to pop before their lifespan (at 20 or more every bubble
|
||||
// lives the full lifespan); each bubble gets its own pop age.
|
||||
let frailty = 1.0 - (pr.f("bubbles/strength") as f32 / 20.0).clamp(0.0, 1.0);
|
||||
let init_speed = pr.f("physics/initialSpeed") as f32;
|
||||
let init_dir = (pr.f("physics/initialDirection") as f32).to_radians();
|
||||
let wind = pr.f("physics/windSpeed") as f32;
|
||||
let wind_dir = (pr.f("physics/windDirection") as f32).to_radians();
|
||||
let turb = pr.f("physics/turbulence") as f32;
|
||||
let repulsion = pr.f("physics/repulsion") as f32;
|
||||
let viscosity = pr.f("physics/viscosity").clamp(0.0, 1.0) as f32;
|
||||
let sticky = pr.f("physics/stickiness").clamp(0.0, 1.0) as f32;
|
||||
let universe = pr.f("universeSize").max(0.01) as f32;
|
||||
let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x68e31da4) ^ ctx.seed;
|
||||
let wvec = [wind_dir.sin() * wind * unit, -wind_dir.cos() * wind * unit];
|
||||
|
|
@ -1162,12 +1172,12 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
q.p[1] += q.v[1];
|
||||
q.age += 1.0;
|
||||
}
|
||||
st.b.retain(|q| q.age < lifespan && q.p[0] > ux0 && q.p[0] < ux1 && q.p[1] > uy0 && q.p[1] < uy1);
|
||||
st.b.retain(|q| q.age < foam_pop_age(q.id, lifespan, frailty, seed) && q.p[0] > ux0 && q.p[0] < ux1 && q.p[1] > uy0 && q.p[1] < uy1);
|
||||
});
|
||||
let view = pr.e("view");
|
||||
let zoom = pr.f("zoom").max(0.01) as f32;
|
||||
let texture = pr.e("bubbleTexture");
|
||||
let blend = pr.e("blendMode");
|
||||
let texture = pr.e("rendering/bubbleTexture");
|
||||
let blend = pr.e("rendering/blendMode");
|
||||
let s = b.scale as f32;
|
||||
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
||||
let mut order: Vec<&FoamBubble> = st.b.iter().collect();
|
||||
|
|
@ -1182,8 +1192,9 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
.map(|q| {
|
||||
let r = q.size * unit * 3.0 * (q.age * growth).min(1.0) * zoom * s;
|
||||
let (bx, by) = b.to_px([(cx + (q.p[0] - cx) * zoom) as f64, (cy + (q.p[1] - cy) * zoom) as f64]);
|
||||
let fade = ((lifespan - q.age) / 5.0).clamp(0.0, 1.0);
|
||||
if view == 0 {
|
||||
let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0);
|
||||
// Draft and Draft + Flow Map show the wireframe-style bubbles.
|
||||
if view != 2 {
|
||||
Sprite::new(bx as f32, by as f32, r, [0.55, 0.75, 1.0, fade], Shape::Bubble)
|
||||
} else {
|
||||
let (c, shape) = match texture {
|
||||
|
|
@ -1197,7 +1208,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
})
|
||||
.collect();
|
||||
b.img = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view != 0 { Acc::Add } else { Acc::Over });
|
||||
b.img = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view == 2 { Acc::Add } else { Acc::Over });
|
||||
b
|
||||
}
|
||||
|
||||
|
|
@ -1206,24 +1217,24 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
fn producer_params(k: u32, amp: f64, pos: (f64, f64)) -> Vec<crate::ParamSpec> {
|
||||
let ids: [&'static str; 8] = match k {
|
||||
1 => [
|
||||
"producer1Type",
|
||||
"producer1Position",
|
||||
"producer1Length",
|
||||
"producer1Width",
|
||||
"producer1Angle",
|
||||
"producer1Amplitude",
|
||||
"producer1Frequency",
|
||||
"producer1Phase",
|
||||
"producer1/producer1Type",
|
||||
"producer1/producer1Position",
|
||||
"producer1/producer1Length",
|
||||
"producer1/producer1Width",
|
||||
"producer1/producer1Angle",
|
||||
"producer1/producer1Amplitude",
|
||||
"producer1/producer1Frequency",
|
||||
"producer1/producer1Phase",
|
||||
],
|
||||
_ => [
|
||||
"producer2Type",
|
||||
"producer2Position",
|
||||
"producer2Length",
|
||||
"producer2Width",
|
||||
"producer2Angle",
|
||||
"producer2Amplitude",
|
||||
"producer2Frequency",
|
||||
"producer2Phase",
|
||||
"producer2/producer2Type",
|
||||
"producer2/producer2Position",
|
||||
"producer2/producer2Length",
|
||||
"producer2/producer2Width",
|
||||
"producer2/producer2Angle",
|
||||
"producer2/producer2Amplitude",
|
||||
"producer2/producer2Frequency",
|
||||
"producer2/producer2Phase",
|
||||
],
|
||||
};
|
||||
vec![
|
||||
|
|
@ -1240,7 +1251,7 @@ fn producer_params(k: u32, amp: f64, pos: (f64, f64)) -> Vec<crate::ParamSpec> {
|
|||
|
||||
fn card_dance_params() -> Vec<crate::ParamSpec> {
|
||||
let mut v = vec![
|
||||
p("rowsColumns", "Rows & Columns", Value::Enum(0), popup(&["Independent", "Columns Follow Rows"])),
|
||||
p("rowsColumns", "Rows & Columns", Value::Enum(0), popup(&["Independent", "Columns Follows Rows"])),
|
||||
p("rows", "Rows", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("columns", "Columns", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer),
|
||||
|
|
@ -1248,14 +1259,14 @@ fn card_dance_params() -> Vec<crate::ParamSpec> {
|
|||
p("gradientLayer2", "Gradient Layer 2", Value::Layer(None), ParamUi::Layer),
|
||||
];
|
||||
let names: [[&'static str; 4]; 8] = [
|
||||
["xPosSource", "xPosMultiplier", "xPosOffset", "X Position"],
|
||||
["yPosSource", "yPosMultiplier", "yPosOffset", "Y Position"],
|
||||
["zPosSource", "zPosMultiplier", "zPosOffset", "Z Position"],
|
||||
["xRotSource", "xRotMultiplier", "xRotOffset", "X Rotation"],
|
||||
["yRotSource", "yRotMultiplier", "yRotOffset", "Y Rotation"],
|
||||
["zRotSource", "zRotMultiplier", "zRotOffset", "Z Rotation"],
|
||||
["xScaleSource", "xScaleMultiplier", "xScaleOffset", "X Scale"],
|
||||
["yScaleSource", "yScaleMultiplier", "yScaleOffset", "Y Scale"],
|
||||
["xPosition/xPosSource", "xPosition/xPosMultiplier", "xPosition/xPosOffset", "X Position"],
|
||||
["yPosition/yPosSource", "yPosition/yPosMultiplier", "yPosition/yPosOffset", "Y Position"],
|
||||
["zPosition/zPosSource", "zPosition/zPosMultiplier", "zPosition/zPosOffset", "Z Position"],
|
||||
["xRotation/xRotSource", "xRotation/xRotMultiplier", "xRotation/xRotOffset", "X Rotation"],
|
||||
["yRotation/yRotSource", "yRotation/yRotMultiplier", "yRotation/yRotOffset", "Y Rotation"],
|
||||
["zRotation/zRotSource", "zRotation/zRotMultiplier", "zRotation/zRotOffset", "Z Rotation"],
|
||||
["xScale/xScaleSource", "xScale/xScaleMultiplier", "xScale/xScaleOffset", "X Scale"],
|
||||
["yScale/yScaleSource", "yScale/yScaleMultiplier", "yScale/yScaleOffset", "Y Scale"],
|
||||
];
|
||||
debug_assert_eq!(names.len(), CD_PROPS.len());
|
||||
for (i, n) in names.iter().enumerate() {
|
||||
|
|
@ -1267,11 +1278,11 @@ fn card_dance_params() -> Vec<crate::ParamSpec> {
|
|||
v.push(p(n[2], "Offset", num(if scale { 1.0 } else { 0.0 }), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2)));
|
||||
}
|
||||
v.extend([
|
||||
p("cameraZ", "Z Position", num(2.0), slider(0.05, 10.0, 0.5, 5.0, 2)),
|
||||
p("focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||||
p("lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("cameraPosition/cameraZ", "Z Position", num(2.0), slider(0.05, 10.0, 0.5, 5.0, 2)),
|
||||
p("cameraPosition/focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("lighting/ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("material/diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
]);
|
||||
v
|
||||
}
|
||||
|
|
@ -1326,27 +1337,27 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.sim.caustics",
|
||||
"Caustics",
|
||||
vec![
|
||||
p("bottom", "Bottom", Value::Layer(None), ParamUi::Layer),
|
||||
p("scaling", "Scaling", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 3)),
|
||||
p("repeatMode", "Repeat Mode", Value::Enum(2), popup(&["Once", "Tiled", "Reflected"])),
|
||||
p("bottomSizeDiffers", "If Layer Size Differs", Value::Enum(1), popup(&["Center", "Stretch to Fit"])),
|
||||
p("blur", "Blur", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("waterSurface", "Water Surface", Value::Layer(None), ParamUi::Layer),
|
||||
p("waveHeight", "Wave Height", num(0.2), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||||
p("smoothing", "Smoothing", num(5.0), slider(0.0, 100.0, 0.0, 50.0, 1)),
|
||||
p("waterDepth", "Water Depth", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("refractiveIndex", "Refractive Index", num(1.2), slider(1.0, 3.0, 1.0, 2.0, 3)),
|
||||
p("surfaceColor", "Surface Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("surfaceOpacity", "Surface Opacity", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("causticsStrength", "Caustics Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("lightPosition", "Light Position", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("lightHeight", "Light Height", num(1.0), slider(0.0, 10.0, 0.0, 4.0, 2)),
|
||||
p("ambientLight", "Ambient Light", num(0.35), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("specular", "Specular Reflection", num(0.2), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("highlightSharpness", "Highlight Sharpness", num(15.0), slider(1.0, 100.0, 1.0, 100.0, 1)),
|
||||
p("bottom/bottom", "Bottom", Value::Layer(None), ParamUi::Layer),
|
||||
p("bottom/scaling", "Scaling", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 3)),
|
||||
p("bottom/repeatMode", "Repeat Mode", Value::Enum(2), popup(&["Once", "Tiled", "Reflected"])),
|
||||
p("bottom/bottomSizeDiffers", "If Layer Size Differs", Value::Enum(1), popup(&["Center", "Stretch to Fit"])),
|
||||
p("bottom/blur", "Blur", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("water/waterSurface", "Water Surface", Value::Layer(None), ParamUi::Layer),
|
||||
p("water/waveHeight", "Wave Height", num(0.2), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||||
p("water/smoothing", "Smoothing", num(5.0), slider(0.0, 100.0, 0.0, 50.0, 1)),
|
||||
p("water/waterDepth", "Water Depth", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("water/refractiveIndex", "Refractive Index", num(1.2), slider(1.0, 3.0, 1.0, 2.0, 3)),
|
||||
p("water/surfaceColor", "Surface Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("water/surfaceOpacity", "Surface Opacity", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("water/causticsStrength", "Caustics Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("lighting/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("lighting/lightPosition", "Light Position", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("lighting/lightHeight", "Light Height", num(1.0), slider(0.0, 10.0, 0.0, 4.0, 2)),
|
||||
p("lighting/ambientLight", "Ambient Light", num(0.35), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("material/diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("material/specular", "Specular Reflection", num(0.2), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("material/highlightSharpness", "Highlight Sharpness", num(15.0), slider(1.0, 100.0, 1.0, 100.0, 1)),
|
||||
],
|
||||
caustics,
|
||||
),
|
||||
|
|
@ -1356,16 +1367,16 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
{
|
||||
let mut v = vec![
|
||||
p("view", "View", Value::Enum(0), popup(&["Wireframe Preview", "Height Map"])),
|
||||
p("brightness", "Brightness", num(0.5), slider(-1.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("contrast", "Contrast", num(0.75), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||||
p("gamma", "Gamma Adjustment", num(1.0), slider(0.01, 10.0, 0.2, 5.0, 3)),
|
||||
p("transparency", "Transparency", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("gridResolution", "Grid Resolution", num(60.0), slider(1.0, 400.0, 1.0, 200.0, 0)),
|
||||
p("gridResDownsamples", "Grid Res Downsamples", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("waveSpeed", "Wave Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||||
p("damping", "Damping", num(0.05), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||||
p("reflectEdges", "Reflect Edges", Value::Enum(0), popup(&["None", "Left", "Top", "Right", "Bottom", "All"])),
|
||||
p("preRoll", "Pre-roll (seconds)", num(0.0), slider(0.0, 30.0, 0.0, 10.0, 2)),
|
||||
p("heightMapControls/brightness", "Brightness", num(0.5), slider(-1.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("heightMapControls/contrast", "Contrast", num(0.75), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||||
p("heightMapControls/gamma", "Gamma Adjustment", num(1.0), slider(0.01, 10.0, 0.2, 5.0, 3)),
|
||||
p("heightMapControls/transparency", "Transparency", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("simulation/gridResolution", "Grid Resolution", num(60.0), slider(1.0, 400.0, 1.0, 200.0, 0)),
|
||||
p("simulation/gridResDownsamples", "Grid Res Downsamples", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("simulation/waveSpeed", "Wave Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||||
p("simulation/damping", "Damping", num(0.05), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||||
p("simulation/reflectEdges", "Reflect Edges", Value::Enum(0), popup(&["None", "Left", "Top", "Right", "Bottom", "All"])),
|
||||
p("simulation/preRoll", "Pre-roll (seconds)", num(0.0), slider(0.0, 30.0, 0.0, 10.0, 2)),
|
||||
];
|
||||
v.extend(producer_params(1, 0.5, (0.5, 0.5)));
|
||||
v.extend(producer_params(2, 0.0, (0.25, 0.25)));
|
||||
|
|
@ -1384,29 +1395,29 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
popup(&["Rendered", "Wireframe Front View", "Wireframe", "Wireframe Front View + Forces", "Wireframe + Forces"]),
|
||||
),
|
||||
p("render", "Render", Value::Enum(0), popup(&["All", "Layer", "Pieces"])),
|
||||
p("pattern", "Pattern", Value::Enum(0), popup(SHATTER_PATTERNS)),
|
||||
p("repetitions", "Repetitions", num(10.0), slider(1.0, 500.0, 1.0, 100.0, 2)),
|
||||
p("direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("origin", "Origin", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("extrusionDepth", "Extrusion Depth", num(0.05), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("force1Position", "Force 1 Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("force1Depth", "Force 1 Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||||
p("force1Radius", "Force 1 Radius", num(0.4), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||||
p("force1Strength", "Force 1 Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)),
|
||||
p("force2Position", "Force 2 Position", pt(0.25, 0.25), ParamUi::Point),
|
||||
p("force2Depth", "Force 2 Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||||
p("force2Radius", "Force 2 Radius", num(0.0), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||||
p("force2Strength", "Force 2 Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)),
|
||||
p("rotationSpeed", "Rotation Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||||
p("tumbleAxis", "Tumble Axis", Value::Enum(0), popup(&["Free", "None", "X", "Y", "Z", "XY", "XZ", "YZ"])),
|
||||
p("randomness", "Randomness", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("massVariance", "Mass Variance", num(30.0), pct()),
|
||||
p("gravity", "Gravity", num(3.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("gravityDirection", "Gravity Direction", num(180.0), ParamUi::Angle),
|
||||
p("gravityInclination", "Gravity Inclination", num(0.0), slider(-90.0, 90.0, -90.0, 90.0, 1)),
|
||||
p("lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("shape/pattern", "Pattern", Value::Enum(0), popup(SHATTER_PATTERNS)),
|
||||
p("shape/repetitions", "Repetitions", num(10.0), slider(1.0, 500.0, 1.0, 100.0, 2)),
|
||||
p("shape/direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("shape/origin", "Origin", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("shape/extrusionDepth", "Extrusion Depth", num(0.05), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("force1/force1Position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("force1/force1Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||||
p("force1/force1Radius", "Radius", num(0.4), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||||
p("force1/force1Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)),
|
||||
p("force2/force2Position", "Position", pt(0.25, 0.25), ParamUi::Point),
|
||||
p("force2/force2Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||||
p("force2/force2Radius", "Radius", num(0.0), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||||
p("force2/force2Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)),
|
||||
p("physics/rotationSpeed", "Rotation Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||||
p("physics/tumbleAxis", "Tumble Axis", Value::Enum(0), popup(&["Free", "None", "X", "Y", "Z", "XY", "XZ", "YZ"])),
|
||||
p("physics/randomness", "Randomness", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("physics/massVariance", "Mass Variance", num(30.0), pct()),
|
||||
p("physics/gravity", "Gravity", num(3.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("physics/gravityDirection", "Gravity Direction", num(180.0), ParamUi::Angle),
|
||||
p("physics/gravityInclination", "Gravity Inclination", num(0.0), slider(-90.0, 90.0, -90.0, 90.0, 1)),
|
||||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("lighting/ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
shatter,
|
||||
|
|
@ -1415,29 +1426,29 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.sim.foam",
|
||||
"Foam",
|
||||
vec![
|
||||
p("view", "View", Value::Enum(0), popup(&["Draft", "Rendered"])),
|
||||
p("producerPoint", "Producer Point", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("producerXSize", "Producer X Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("producerYSize", "Producer Y Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("producerOrientation", "Producer Orientation", num(0.0), ParamUi::Angle),
|
||||
p("productionRate", "Production Rate", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 3)),
|
||||
p("size", "Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("sizeVariance", "Size Variance", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("lifespan", "Lifespan", num(300.0), slider(1.0, 30_000.0, 1.0, 1000.0, 1)),
|
||||
p("growthSpeed", "Bubble Growth Speed", num(0.1), slider(0.0001, 10.0, 0.0, 1.0, 3)),
|
||||
p("strength", "Strength", num(10.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("initialSpeed", "Initial Speed", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||||
p("initialDirection", "Initial Direction", num(0.0), ParamUi::Angle),
|
||||
p("windSpeed", "Wind Speed", num(0.5), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||||
p("windDirection", "Wind Direction", num(90.0), ParamUi::Angle),
|
||||
p("turbulence", "Turbulence", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("repulsion", "Repulsion", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("stickiness", "Stickiness", num(0.75), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("view", "View", Value::Enum(0), popup(&["Draft", "Draft + Flow Map", "Rendered"])),
|
||||
p("producer/producerPoint", "Producer Point", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("producer/producerXSize", "Producer X Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("producer/producerYSize", "Producer Y Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("producer/producerOrientation", "Producer Orientation", num(0.0), ParamUi::Angle),
|
||||
p("producer/productionRate", "Production Rate", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 3)),
|
||||
p("bubbles/size", "Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("bubbles/sizeVariance", "Size Variance", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("bubbles/lifespan", "Lifespan", num(300.0), slider(1.0, 30_000.0, 1.0, 1000.0, 1)),
|
||||
p("bubbles/growthSpeed", "Bubble Growth Speed", num(0.1), slider(0.0001, 10.0, 0.0001, 1.0, 3)),
|
||||
p("bubbles/strength", "Strength", num(10.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("physics/initialSpeed", "Initial Speed", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||||
p("physics/initialDirection", "Initial Direction", num(0.0), ParamUi::Angle),
|
||||
p("physics/windSpeed", "Wind Speed", num(0.5), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||||
p("physics/windDirection", "Wind Direction", num(90.0), ParamUi::Angle),
|
||||
p("physics/turbulence", "Turbulence", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("physics/repulsion", "Repulsion", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("physics/stickiness", "Stickiness", num(0.75), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("zoom", "Zoom", num(1.0), slider(0.01, 50.0, 0.1, 5.0, 3)),
|
||||
p("universeSize", "Universe Size", num(1.0), slider(0.01, 10.0, 0.5, 3.0, 3)),
|
||||
p("blendMode", "Blend Mode", Value::Enum(0), popup(&["Transparent", "Solid Old on Top", "Solid New on Top"])),
|
||||
p("bubbleTexture", "Bubble Texture", Value::Enum(0), popup(&["Default Bubble", "Spit", "Bubblegum", "Dishwater", "Milky"])),
|
||||
p("rendering/blendMode", "Blend Mode", Value::Enum(0), popup(&["Transparent", "Solid Old on Top", "Solid New on Top"])),
|
||||
p("rendering/bubbleTexture", "Bubble Texture", Value::Enum(0), popup(&["Default Bubble", "Spit", "Bubblegum", "Dishwater", "Milky"])),
|
||||
p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
foam,
|
||||
|
|
@ -1522,16 +1533,16 @@ mod tests {
|
|||
let src = ramp(30, 20);
|
||||
let a = run("ec.sim.carddance", &[], src.clone(), 0.0);
|
||||
assert!(max_diff(&a, &src) < 0.02, "flat cards reproduce the layer: {}", max_diff(&a, &src));
|
||||
let r = run("ec.sim.carddance", &[("yRotSource", Value::Enum(1)), ("yRotMultiplier", num(90.0))], src.clone(), 0.0);
|
||||
assert_eq!(r, run("ec.sim.carddance", &[("yRotSource", Value::Enum(1)), ("yRotMultiplier", num(90.0))], src.clone(), 0.0));
|
||||
let r = run("ec.sim.carddance", &[("yRotation/yRotSource", Value::Enum(1)), ("yRotation/yRotMultiplier", num(90.0))], src.clone(), 0.0);
|
||||
assert_eq!(r, run("ec.sim.carddance", &[("yRotation/yRotSource", Value::Enum(1)), ("yRotation/yRotMultiplier", num(90.0))], src.clone(), 0.0));
|
||||
assert!(sum_diff(&r, &src) > 5.0);
|
||||
let z = run("ec.sim.carddance", &[("zPosOffset", num(-1.0))], src.clone(), 0.0);
|
||||
let z = run("ec.sim.carddance", &[("zPosition/zPosOffset", num(-1.0))], src.clone(), 0.0);
|
||||
assert!(sum_diff(&z, &src) > 5.0, "cards pulled towards the camera grow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shatter_at_rest_then_breaks() {
|
||||
let v = [("view", Value::Enum(0)), ("force1Position", pt(16.0, 16.0)), ("origin", pt(16.0, 16.0))];
|
||||
let v = [("view", Value::Enum(0)), ("force1/force1Position", pt(16.0, 16.0)), ("shape/origin", pt(16.0, 16.0))];
|
||||
let src = ramp(32, 32);
|
||||
let t0 = run("ec.sim.shatter", &v, src.clone(), 0.0);
|
||||
assert!(max_diff(&t0, &src) < 0.03, "{}", max_diff(&t0, &src));
|
||||
|
|
@ -1539,13 +1550,15 @@ mod tests {
|
|||
assert_eq!(t1, run("ec.sim.shatter", &v, src.clone(), 0.5));
|
||||
assert!(sum_diff(&t1, &src) > 10.0);
|
||||
for pat in 0..SHATTER_PATTERNS.len() as u32 {
|
||||
let vv = [("view", Value::Enum(0)), ("pattern", Value::Enum(pat)), ("force1Position", pt(16.0, 16.0)), ("origin", pt(16.0, 16.0))];
|
||||
let vv =
|
||||
[("view", Value::Enum(0)), ("shape/pattern", Value::Enum(pat)), ("force1/force1Position", pt(16.0, 16.0)), ("shape/origin", pt(16.0, 16.0))];
|
||||
let r = run("ec.sim.shatter", &vv, src.clone(), 0.0);
|
||||
assert!(max_diff(&r, &src) < 0.05, "pattern {pat} tiles the layer: {}", max_diff(&r, &src));
|
||||
}
|
||||
let wire = run("ec.sim.shatter", &[("repetitions", num(3.0))], src.clone(), 0.0);
|
||||
let wire = run("ec.sim.shatter", &[("shape/repetitions", num(3.0))], src.clone(), 0.0);
|
||||
assert!(wire.data.iter().any(|p| p[3] > 0.5) && wire.data.iter().any(|p| p[3] == 0.0));
|
||||
let layer_only = run("ec.sim.shatter", &[("view", Value::Enum(0)), ("render", Value::Enum(1)), ("force1Position", pt(16.0, 16.0))], src.clone(), 0.5);
|
||||
let layer_only =
|
||||
run("ec.sim.shatter", &[("view", Value::Enum(0)), ("render", Value::Enum(1)), ("force1/force1Position", pt(16.0, 16.0))], src.clone(), 0.5);
|
||||
assert!(layer_only.get(16, 16)[3] == 0.0, "broken centre removed");
|
||||
}
|
||||
|
||||
|
|
@ -1554,7 +1567,13 @@ mod tests {
|
|||
let src = ramp(24, 24);
|
||||
let a = run(
|
||||
"ec.sim.caustics",
|
||||
&[("surfaceOpacity", num(0.0)), ("ambientLight", num(0.0)), ("diffuse", num(1.0)), ("lightHeight", num(1000.0)), ("specular", num(0.0))],
|
||||
&[
|
||||
("water/surfaceOpacity", num(0.0)),
|
||||
("lighting/ambientLight", num(0.0)),
|
||||
("material/diffuse", num(1.0)),
|
||||
("lighting/lightHeight", num(1000.0)),
|
||||
("material/specular", num(0.0)),
|
||||
],
|
||||
src.clone(),
|
||||
0.0,
|
||||
);
|
||||
|
|
@ -1566,27 +1585,33 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn wave_world_seek_and_waves() {
|
||||
let v = vec![("view", Value::Enum(1)), ("producer1Position", pt(16.0, 16.0)), ("gridResolution", num(24.0))];
|
||||
let v = vec![("view", Value::Enum(1)), ("producer1/producer1Position", pt(16.0, 16.0)), ("simulation/gridResolution", num(24.0))];
|
||||
let _ = run("ec.sim.waveworld", &v, Image::new(32, 32), 0.5);
|
||||
let a = run("ec.sim.waveworld", &v, Image::new(32, 32), 1.0);
|
||||
let mut v2 = v.clone();
|
||||
v2.push(("transparency", num(0.0)));
|
||||
v2.push(("heightMapControls/transparency", num(0.0)));
|
||||
assert_eq!(a, run("ec.sim.waveworld", &v2, Image::new(32, 32), 1.0));
|
||||
// Waves spread: some pixels differ from the flat-water grey.
|
||||
assert!(a.data.iter().any(|p| (p[0] - 0.5).abs() > 0.05), "waves");
|
||||
let flat = run("ec.sim.waveworld", &[("view", Value::Enum(1)), ("producer1Amplitude", num(0.0))], Image::new(32, 32), 1.0);
|
||||
let flat = run("ec.sim.waveworld", &[("view", Value::Enum(1)), ("producer1/producer1Amplitude", num(0.0))], Image::new(32, 32), 1.0);
|
||||
assert!(flat.data.iter().all(|p| (p[0] - 0.5).abs() < 1e-5));
|
||||
let wire = run("ec.sim.waveworld", &[("producer1Position", pt(16.0, 16.0)), ("gridResolution", num(10.0))], Image::new(32, 32), 0.3);
|
||||
let wire =
|
||||
run("ec.sim.waveworld", &[("producer1/producer1Position", pt(16.0, 16.0)), ("simulation/gridResolution", num(10.0))], Image::new(32, 32), 0.3);
|
||||
assert!(wire.data.iter().any(|p| p[1] > 0.3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wave_world_steps_match_direct() {
|
||||
// Fresh caches: resumed simulation equals simulation from scratch.
|
||||
let v = vec![("view", Value::Enum(1)), ("producer1Position", pt(16.0, 16.0)), ("gridResolution", num(20.0)), ("damping", num(0.07))];
|
||||
let v = vec![
|
||||
("view", Value::Enum(1)),
|
||||
("producer1/producer1Position", pt(16.0, 16.0)),
|
||||
("simulation/gridResolution", num(20.0)),
|
||||
("simulation/damping", num(0.07)),
|
||||
];
|
||||
let a = run("ec.sim.waveworld", &v, Image::new(32, 32), 1.3);
|
||||
let mut v2 = v.clone();
|
||||
v2.push(("damping", num(0.07000001)));
|
||||
v2.push(("simulation/damping", num(0.07000001)));
|
||||
// Different key: computed from scratch at 1.3 directly vs via 0.4 (both fresh keys).
|
||||
let _ = run("ec.sim.waveworld", &v2, Image::new(32, 32), 0.4);
|
||||
let b = run("ec.sim.waveworld", &v2, Image::new(32, 32), 1.3);
|
||||
|
|
@ -1595,7 +1620,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn foam_bubbles_drift_and_seek() {
|
||||
let v = vec![("producerPoint", pt(16.0, 16.0)), ("view", Value::Enum(1)), ("productionRate", num(2.0))];
|
||||
let v = vec![("producer/producerPoint", pt(16.0, 16.0)), ("view", Value::Enum(2)), ("producer/productionRate", num(2.0))];
|
||||
let none = run("ec.sim.foam", &v, Image::new(32, 32), 0.0);
|
||||
assert!(none.data.iter().all(|p| p[3] == 0.0));
|
||||
let _ = run("ec.sim.foam", &v, Image::new(32, 32), 0.6);
|
||||
|
|
@ -1604,7 +1629,26 @@ mod tests {
|
|||
v2.push(("randomSeed", num(1.0)));
|
||||
assert_eq!(a, run("ec.sim.foam", &v2, Image::new(32, 32), 1.2));
|
||||
assert!(a.data.iter().any(|p| p[3] > 0.05), "bubbles");
|
||||
let draft = run("ec.sim.foam", &[("producerPoint", pt(16.0, 16.0)), ("productionRate", num(2.0))], Image::new(32, 32), 1.0);
|
||||
let draft = run("ec.sim.foam", &[("producer/producerPoint", pt(16.0, 16.0)), ("producer/productionRate", num(2.0))], Image::new(32, 32), 1.0);
|
||||
assert!(draft.data.iter().any(|p| p[3] > 0.05));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foam_strength_controls_early_popping() {
|
||||
// Strong bubbles all live their full lifespan; weak ones pop at random earlier.
|
||||
assert_eq!(foam_pop_age(7, 300.0, 0.0, 1), 300.0);
|
||||
let early = (0..200).filter(|&id| foam_pop_age(id, 300.0, 1.0, 1) < 150.0).count();
|
||||
assert!(early > 60 && early < 140, "{early}");
|
||||
let count = |strength: f64| {
|
||||
let v = vec![
|
||||
("producer/producerPoint", pt(16.0, 16.0)),
|
||||
("producer/productionRate", num(3.0)),
|
||||
("bubbles/lifespan", num(40.0)),
|
||||
("bubbles/strength", num(strength)),
|
||||
("physics/windSpeed", num(0.0)),
|
||||
];
|
||||
run("ec.sim.foam", &v, Image::new(32, 32), 1.5).data.iter().map(|p| p[3]).sum::<f32>()
|
||||
};
|
||||
assert!(count(0.0) < count(20.0) * 0.9, "{} vs {}", count(0.0), count(20.0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,8 +76,10 @@ impl MapImg {
|
|||
}
|
||||
}
|
||||
|
||||
pub const MAP_TARGETS: [&str; 12] =
|
||||
["None", "Red", "Green", "Blue", "Scale", "Opacity", "X Velocity", "Y Velocity", "X Force", "Y Force", "Kinetic Friction", "Mass"];
|
||||
/// Particle properties a Property Mapper channel can drive (After Effects' order; the
|
||||
/// properties we don't model yet are left out).
|
||||
pub const MAP_TARGETS: [&str; 14] =
|
||||
["None", "Red", "Green", "Blue", "Kinetic Friction", "Scale", "X", "Y", "X Speed", "Y Speed", "X Force", "Y Force", "Opacity", "Mass"];
|
||||
|
||||
struct Pg {
|
||||
// Cannon
|
||||
|
|
@ -113,10 +115,16 @@ impl Pg {
|
|||
// AE angles: 0° = up, clockwise.
|
||||
let a = (self.barrel_angle + h(1) * self.dir_spread * 0.5).to_radians();
|
||||
let d = [a.sin(), -a.cos()];
|
||||
let perp = [-d[1], d[0]];
|
||||
let off = h(2) * self.barrel_radius;
|
||||
let speed = (self.vel + h(3) * self.vel_spread).max(0.0);
|
||||
let p = [self.pos[0] + perp[0] * off, self.pos[1] + perp[1] * off];
|
||||
// Barrel: positive radii are a square around the cannon position, negative ones a disc.
|
||||
let r = self.barrel_radius;
|
||||
let off = if r >= 0.0 {
|
||||
[h(2) * r, h(4) * r]
|
||||
} else {
|
||||
let (a, rr) = (hash1(id, 5, s) * std::f32::consts::TAU, hash1(id, 6, s).sqrt() * -r);
|
||||
[a.cos() * rr, a.sin() * rr]
|
||||
};
|
||||
let p = [self.pos[0] + off[0], self.pos[1] + off[1]];
|
||||
PgParticle { p, v: [d[0] * speed, d[1] * speed], origin: p, c: self.color, r: self.radius, mass: 1.0, friction: 0.0, force: [0.0; 2], id }
|
||||
}
|
||||
|
||||
|
|
@ -133,14 +141,16 @@ impl Pg {
|
|||
1 => q.c[0] = v,
|
||||
2 => q.c[1] = v,
|
||||
3 => q.c[2] = v,
|
||||
4 => q.r = self.radius * v.max(0.0),
|
||||
5 => q.c[3] = v.clamp(0.0, 1.0),
|
||||
6 => q.v[0] = v,
|
||||
7 => q.v[1] = v,
|
||||
8 => q.force[0] = v,
|
||||
9 => q.force[1] = v,
|
||||
10 => q.friction = v.clamp(0.0, 1.0),
|
||||
11 => q.mass = v.max(0.01),
|
||||
4 => q.friction = v.clamp(0.0, 1.0),
|
||||
5 => q.r = self.radius * v.max(0.0),
|
||||
6 => q.p[0] = v,
|
||||
7 => q.p[1] = v,
|
||||
8 => q.v[0] = v,
|
||||
9 => q.v[1] = v,
|
||||
10 => q.force[0] = v,
|
||||
11 => q.force[1] = v,
|
||||
12 => q.c[3] = v.clamp(0.0, 1.0),
|
||||
13 => q.mass = v.max(0.01),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -237,46 +247,46 @@ fn image_key(img: &Image) -> u64 {
|
|||
fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let grav_dir = (pr.f("gravityDirection") as f32).to_radians();
|
||||
let gf = pr.f("gravityForce") as f32;
|
||||
let wall = (pr.f("wallBoundary").round() as usize).checked_sub(1).and_then(|i| ctx.env.masks.get(i)).map(|m| m.points.clone());
|
||||
let mapper = if pr.b("mapperEnabled") { ctx.layer_param("useLayerAsMap", true).map(|lp| MapImg::from(ctx, lp)) } else { None };
|
||||
let grav_dir = (pr.f("gravity/gravityDirection") as f32).to_radians();
|
||||
let gf = pr.f("gravity/gravityForce") as f32;
|
||||
let wall = (pr.f("wall/wallBoundary").round() as usize).checked_sub(1).and_then(|i| ctx.env.masks.get(i)).map(|m| m.points.clone());
|
||||
let mapper = if pr.b("mapperEnabled") { ctx.layer_param("persistentPropertyMapper/useLayerAsMap", true).map(|lp| MapImg::from(ctx, lp)) } else { None };
|
||||
let pg = Pg {
|
||||
cannon: pr.b("cannonEnabled"),
|
||||
pos: { pr.v2("cannonPosition").map(|v| v as f32) },
|
||||
barrel_angle: pr.f("barrelAngle") as f32,
|
||||
barrel_radius: pr.f("barrelRadius") as f32,
|
||||
rate: pr.f("particlesPerSecond").max(0.0),
|
||||
dir_spread: pr.f("directionRandomSpread") as f32,
|
||||
vel: pr.f("velocity") as f32,
|
||||
vel_spread: pr.f("velocityRandomSpread") as f32,
|
||||
color: pr.color("cannonColor"),
|
||||
radius: pr.f("cannonParticleRadius").max(0.0) as f32,
|
||||
pos: { pr.v2("cannon/cannonPosition").map(|v| v as f32) },
|
||||
barrel_angle: pr.f("cannon/barrelAngle") as f32,
|
||||
barrel_radius: pr.f("cannon/barrelRadius") as f32,
|
||||
rate: pr.f("cannon/particlesPerSecond").max(0.0),
|
||||
dir_spread: pr.f("cannon/directionRandomSpread") as f32,
|
||||
vel: pr.f("cannon/velocity") as f32,
|
||||
vel_spread: pr.f("cannon/velocityRandomSpread") as f32,
|
||||
color: pr.color("cannon/cannonColor"),
|
||||
radius: pr.f("cannon/cannonParticleRadius").max(0.0) as f32,
|
||||
gravity: [grav_dir.sin() * gf, -grav_dir.cos() * gf],
|
||||
grav_spread: (pr.f("gravityForceRandomSpread") / 100.0).max(0.0) as f32,
|
||||
repel: pr.f("repelForce") as f32 * 100.0,
|
||||
repel_radius: pr.f("repelForceRadius").max(0.0) as f32,
|
||||
grav_spread: (pr.f("gravity/gravityForceRandomSpread") / 100.0).max(0.0) as f32,
|
||||
repel: pr.f("repel/repelForce") as f32 * 100.0,
|
||||
repel_radius: pr.f("repel/repelForceRadius").max(0.0) as f32,
|
||||
wall,
|
||||
mapper,
|
||||
map_to: [
|
||||
(pr.e("mapRedTo"), pr.f("redMin") as f32, pr.f("redMax") as f32),
|
||||
(pr.e("mapGreenTo"), pr.f("greenMin") as f32, pr.f("greenMax") as f32),
|
||||
(pr.e("mapBlueTo"), pr.f("blueMin") as f32, pr.f("blueMax") as f32),
|
||||
(pr.e("persistentPropertyMapper/mapRedTo"), pr.f("persistentPropertyMapper/redMin") as f32, pr.f("persistentPropertyMapper/redMax") as f32),
|
||||
(pr.e("persistentPropertyMapper/mapGreenTo"), pr.f("persistentPropertyMapper/greenMin") as f32, pr.f("persistentPropertyMapper/greenMax") as f32),
|
||||
(pr.e("persistentPropertyMapper/mapBlueTo"), pr.f("persistentPropertyMapper/blueMin") as f32, pr.f("persistentPropertyMapper/blueMax") as f32),
|
||||
],
|
||||
seed: (pr.f("randomSeed") as u32).wrapping_mul(0x9e37_79b9) ^ ctx.seed,
|
||||
bounds: [-lw * 2.0, -lh * 2.0, lw * 3.0, lh * 3.0],
|
||||
};
|
||||
// Initial particles: Grid and Layer Exploder.
|
||||
let grid_on = pr.b("gridEnabled");
|
||||
let across = pr.f("particlesAcross").max(0.0).round() as u32;
|
||||
let down = pr.f("particlesDown").max(0.0).round() as u32;
|
||||
let gpos = pr.v2("gridPosition");
|
||||
let (gw, gh) = (pr.f("gridWidth"), pr.f("gridHeight"));
|
||||
let gcol = pr.color("gridColor");
|
||||
let grad = pr.f("gridParticleRadius").max(0.0) as f32;
|
||||
let across = pr.f("grid/particlesAcross").max(0.0).round() as u32;
|
||||
let down = pr.f("grid/particlesDown").max(0.0).round() as u32;
|
||||
let gpos = pr.v2("grid/gridPosition");
|
||||
let (gw, gh) = (pr.f("grid/gridWidth"), pr.f("grid/gridHeight"));
|
||||
let gcol = pr.color("grid/gridColor");
|
||||
let grad = pr.f("grid/gridParticleRadius").max(0.0) as f32;
|
||||
let exploder = if pr.b("exploderEnabled") {
|
||||
let host = ctx.env.host;
|
||||
let lid = pr.get("explodeLayer").and_then(Value::as_layer);
|
||||
let lid = pr.get("layerExploder/explodeLayer").and_then(Value::as_layer);
|
||||
match (host, lid) {
|
||||
(Some(h), Some(id)) => h.layer_at(id, ctx.env.comp_time - ctx.time, true).or_else(|| h.layer(id, true)),
|
||||
_ => None,
|
||||
|
|
@ -284,9 +294,9 @@ fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let ex_r = pr.f("radiusOfNewParticles").max(0.5) as f32;
|
||||
let ex_disp = pr.f("velocityDispersion") as f32;
|
||||
let layer_map = pr.b("layerMapEnabled").then(|| ctx.layer_param("layerMapLayer", true).map(|lp| MapImg::from(ctx, lp))).flatten();
|
||||
let ex_r = pr.f("layerExploder/radiusOfNewParticles").max(0.5) as f32;
|
||||
let ex_disp = pr.f("layerExploder/velocityDispersion") as f32;
|
||||
let layer_map = pr.b("layerMapEnabled").then(|| ctx.layer_param("layerMap/layerMapLayer", true).map(|lp| MapImg::from(ctx, lp))).flatten();
|
||||
|
||||
let mut key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 0x7067);
|
||||
if let Some(m) = &pg.mapper {
|
||||
|
|
@ -372,19 +382,19 @@ fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let thick = pr.f("thickness").max(0.05) as f32;
|
||||
let weight = pr.f("weight") as f32;
|
||||
let const_mass = pr.b("constantMass");
|
||||
let map = ctx.layer_param("mapLayer", true).map(|lp| MapImg::from(ctx, lp));
|
||||
let map_strength = (pr.f("mapStrength") / 100.0) as f32;
|
||||
let map_soft = pr.f("mapSoftness").max(0.0) as f32;
|
||||
let noise = (pr.f("addNoise") / 100.0) as f32;
|
||||
let hc = pr.color("hairColor");
|
||||
let bright = (pr.f("brightness") / 100.0) as f32;
|
||||
let opacity = (pr.f("opacity") / 100.0) as f32;
|
||||
let inherit = (pr.f("colorInheritance") / 100.0) as f32;
|
||||
let light_i = (pr.f("lightIntensity") / 100.0) as f32;
|
||||
let ld = (pr.f("lightDirection") as f32).to_radians();
|
||||
let map = ctx.layer_param("hairfallMap/mapLayer", true).map(|lp| MapImg::from(ctx, lp));
|
||||
let map_strength = (pr.f("hairfallMap/mapStrength") / 100.0) as f32;
|
||||
let map_soft = pr.f("hairfallMap/mapSoftness").max(0.0) as f32;
|
||||
let noise = (pr.f("hairfallMap/addNoise") / 100.0) as f32;
|
||||
let hc = pr.color("hairColor/hairColor");
|
||||
let bright = (pr.f("hairColor/brightness") / 100.0) as f32;
|
||||
let opacity = (pr.f("hairColor/opacity") / 100.0) as f32;
|
||||
let inherit = (pr.f("hairColor/colorInheritance") / 100.0) as f32;
|
||||
let light_i = (pr.f("light/lightIntensity") / 100.0) as f32;
|
||||
let ld = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let light = [ld.sin(), -ld.cos()];
|
||||
let (amb, dif, spe) = ((pr.f("ambient") / 100.0) as f32, (pr.f("diffuse") / 100.0) as f32, (pr.f("specular") / 100.0) as f32);
|
||||
let rough = (pr.f("roughness") / 100.0).clamp(0.01, 1.0) as f32;
|
||||
let (amb, dif, spe) = ((pr.f("shading/ambient") / 100.0) as f32, (pr.f("shading/diffuse") / 100.0) as f32, (pr.f("shading/specular") / 100.0) as f32);
|
||||
let rough = (pr.f("shading/roughness") / 100.0).clamp(0.01, 1.0) as f32;
|
||||
let seed = ctx.seed ^ (pr.f("randomSeed") as u32).wrapping_mul(0x85eb_ca6b);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
// Roots: a jittered grid over the layer, density per 1 000 px².
|
||||
|
|
@ -464,55 +474,57 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
let px = |d: f64, max: f64| (num(d), slider(0.0, max * 10.0, 0.0, max, 1));
|
||||
let sp = |id: &'static str, name: &'static str, v: (Value, ParamUi)| p(id, name, v.0, v.1);
|
||||
let mut pg = vec![
|
||||
// Generator switches (kept for saved projects; After Effects turns a generator off with
|
||||
// its rate / counts instead).
|
||||
p("cannonEnabled", "Cannon Enabled", Value::Bool(true), ParamUi::Hidden),
|
||||
p("gridEnabled", "Grid Enabled", Value::Bool(true), ParamUi::Hidden),
|
||||
p("exploderEnabled", "Layer Exploder Enabled", Value::Bool(true), ParamUi::Hidden),
|
||||
p("layerMapEnabled", "Layer Map Enabled", Value::Bool(true), ParamUi::Hidden),
|
||||
p("mapperEnabled", "Persistent Property Mapper Enabled", Value::Bool(true), ParamUi::Hidden),
|
||||
// Cannon
|
||||
p("cannonEnabled", "Cannon", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("cannonPosition", "Position", pt(0.5, 0.9), ParamUi::Point),
|
||||
p("barrelAngle", "Barrel Angle", num(0.0), ParamUi::Angle),
|
||||
sp("barrelRadius", "Barrel Radius", px(0.0, 100.0)),
|
||||
sp("particlesPerSecond", "Particles Per Second", px(60.0, 500.0)),
|
||||
sp("directionRandomSpread", "Direction Random Spread", px(20.0, 360.0)),
|
||||
sp("velocity", "Velocity", px(130.0, 1000.0)),
|
||||
sp("velocityRandomSpread", "Velocity Random Spread", px(20.0, 500.0)),
|
||||
p("cannonColor", "Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
sp("cannonParticleRadius", "Particle Radius", px(2.0, 50.0)),
|
||||
p("cannon/cannonPosition", "Position", pt(0.5, 0.9), ParamUi::Point),
|
||||
p("cannon/barrelRadius", "Barrel Radius", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)),
|
||||
sp("cannon/particlesPerSecond", "Particles Per Second", px(60.0, 500.0)),
|
||||
p("cannon/barrelAngle", "Direction", num(0.0), ParamUi::Angle),
|
||||
sp("cannon/directionRandomSpread", "Direction Random Spread", px(20.0, 360.0)),
|
||||
sp("cannon/velocity", "Velocity", px(130.0, 1000.0)),
|
||||
sp("cannon/velocityRandomSpread", "Velocity Random Spread", px(20.0, 500.0)),
|
||||
p("cannon/cannonColor", "Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
sp("cannon/cannonParticleRadius", "Particle Radius", px(2.0, 50.0)),
|
||||
// Grid
|
||||
p("gridEnabled", "Grid", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("gridPosition", "Grid Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
sp("gridWidth", "Width", px(100.0, 2000.0)),
|
||||
sp("gridHeight", "Height", px(100.0, 2000.0)),
|
||||
sp("particlesAcross", "Particles Across", px(0.0, 100.0)),
|
||||
sp("particlesDown", "Particles Down", px(0.0, 100.0)),
|
||||
p("gridColor", "Grid Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
sp("gridParticleRadius", "Grid Particle Radius", px(2.0, 50.0)),
|
||||
p("grid/gridPosition", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
sp("grid/gridWidth", "Width", px(100.0, 2000.0)),
|
||||
sp("grid/gridHeight", "Height", px(100.0, 2000.0)),
|
||||
sp("grid/particlesAcross", "Particles Across", px(0.0, 100.0)),
|
||||
sp("grid/particlesDown", "Particles Down", px(0.0, 100.0)),
|
||||
p("grid/gridColor", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
sp("grid/gridParticleRadius", "Particle Radius", px(2.0, 50.0)),
|
||||
// Layer Exploder
|
||||
p("exploderEnabled", "Layer Exploder", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("explodeLayer", "Explode Layer", Value::Layer(None), ParamUi::Layer),
|
||||
sp("radiusOfNewParticles", "Radius of New Particles", px(2.0, 50.0)),
|
||||
sp("velocityDispersion", "Velocity Dispersion", px(20.0, 500.0)),
|
||||
p("layerExploder/explodeLayer", "Explode Layer", Value::Layer(None), ParamUi::Layer),
|
||||
sp("layerExploder/radiusOfNewParticles", "Radius of New Particles", px(2.0, 50.0)),
|
||||
sp("layerExploder/velocityDispersion", "Velocity Dispersion", px(20.0, 500.0)),
|
||||
// Layer Map
|
||||
p("layerMapEnabled", "Layer Map", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("layerMapLayer", "Use Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("layerMap/layerMapLayer", "Use Layer", Value::Layer(None), ParamUi::Layer),
|
||||
// Gravity
|
||||
sp("gravityForce", "Force", px(108.0, 1000.0)),
|
||||
sp("gravityForceRandomSpread", "Force Random Spread", px(0.0, 100.0)),
|
||||
p("gravityDirection", "Direction", num(180.0), ParamUi::Angle),
|
||||
sp("gravity/gravityForce", "Force", px(108.0, 1000.0)),
|
||||
sp("gravity/gravityForceRandomSpread", "Force Random Spread", px(0.0, 100.0)),
|
||||
p("gravity/gravityDirection", "Direction", num(180.0), ParamUi::Angle),
|
||||
// Repel
|
||||
p("repelForce", "Repel Force", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 2)),
|
||||
sp("repelForceRadius", "Force Radius", px(0.0, 100.0)),
|
||||
p("repel/repelForce", "Force", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 2)),
|
||||
sp("repel/repelForceRadius", "Force Radius", px(0.0, 100.0)),
|
||||
// Wall
|
||||
p("wallBoundary", "Boundary", num(0.0), ParamUi::Mask),
|
||||
p("wall/wallBoundary", "Boundary", num(0.0), ParamUi::Mask),
|
||||
// Persistent Property Mapper
|
||||
p("mapperEnabled", "Persistent Property Mapper", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("useLayerAsMap", "Use Layer As Map", Value::Layer(None), ParamUi::Layer),
|
||||
p("persistentPropertyMapper/useLayerAsMap", "Use Layer As Map", Value::Layer(None), ParamUi::Layer),
|
||||
];
|
||||
for (to, mn, mx, n_to, n_min, n_max) in [
|
||||
("mapRedTo", "redMin", "redMax", "Map Red To", "Red Min", "Red Max"),
|
||||
("mapGreenTo", "greenMin", "greenMax", "Map Green To", "Green Min", "Green Max"),
|
||||
("mapBlueTo", "blueMin", "blueMax", "Map Blue To", "Blue Min", "Blue Max"),
|
||||
for (to, mn, mx, n_to) in [
|
||||
("persistentPropertyMapper/mapRedTo", "persistentPropertyMapper/redMin", "persistentPropertyMapper/redMax", "Map Red To"),
|
||||
("persistentPropertyMapper/mapGreenTo", "persistentPropertyMapper/greenMin", "persistentPropertyMapper/greenMax", "Map Green To"),
|
||||
("persistentPropertyMapper/mapBlueTo", "persistentPropertyMapper/blueMin", "persistentPropertyMapper/blueMax", "Map Blue To"),
|
||||
] {
|
||||
pg.push(p(to, n_to, Value::Enum(0), popup(&MAP_TARGETS)));
|
||||
pg.push(p(mn, n_min, num(0.0), slider(-10000.0, 10000.0, -100.0, 100.0, 2)));
|
||||
pg.push(p(mx, n_max, num(1.0), slider(-10000.0, 10000.0, -100.0, 100.0, 2)));
|
||||
pg.push(p(mn, "Min", num(0.0), slider(-10000.0, 10000.0, -100.0, 100.0, 2)));
|
||||
pg.push(p(mx, "Max", num(1.0), slider(-10000.0, 10000.0, -100.0, 100.0, 2)));
|
||||
}
|
||||
pg.push(p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10000.0, 0.0, 1000.0, 0)));
|
||||
let pc = |d: f64| (num(d), slider(0.0, 100.0, 0.0, 100.0, 1));
|
||||
|
|
@ -528,23 +540,23 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("constantMass", "Constant Mass", Value::Bool(false), ParamUi::Checkbox),
|
||||
sp("density", "Density", px(100.0, 1000.0)),
|
||||
// Hairfall Map
|
||||
sp("mapStrength", "Map Strength", pc(0.0)),
|
||||
p("mapLayer", "Map Layer", Value::Layer(None), ParamUi::Layer),
|
||||
sp("mapSoftness", "Map Softness", px(0.0, 100.0)),
|
||||
sp("addNoise", "Add Noise", pc(0.0)),
|
||||
sp("hairfallMap/mapStrength", "Map Strength", pc(0.0)),
|
||||
p("hairfallMap/mapLayer", "Map Layer", Value::Layer(None), ParamUi::Layer),
|
||||
sp("hairfallMap/mapSoftness", "Map Softness", px(0.0, 100.0)),
|
||||
sp("hairfallMap/addNoise", "Add Noise", pc(0.0)),
|
||||
// Hair Color
|
||||
p("hairColor", "Color", col(0.35, 0.25, 0.15), ParamUi::Color),
|
||||
sp("brightness", "Brightness", px(100.0, 400.0)),
|
||||
sp("opacity", "Opacity", pc(100.0)),
|
||||
sp("colorInheritance", "Color Inheritance", pc(0.0)),
|
||||
p("hairColor/hairColor", "Color", col(0.35, 0.25, 0.15), ParamUi::Color),
|
||||
sp("hairColor/brightness", "Brightness", px(100.0, 400.0)),
|
||||
sp("hairColor/opacity", "Opacity", pc(100.0)),
|
||||
sp("hairColor/colorInheritance", "Color Inheritance", pc(0.0)),
|
||||
// Light
|
||||
sp("lightIntensity", "Light Intensity", px(100.0, 400.0)),
|
||||
p("lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
sp("light/lightIntensity", "Light Intensity", px(100.0, 400.0)),
|
||||
p("light/lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
// Shading
|
||||
sp("ambient", "Ambient", pc(40.0)),
|
||||
sp("diffuse", "Diffuse", pc(60.0)),
|
||||
sp("specular", "Specular", pc(30.0)),
|
||||
sp("roughness", "Roughness", pc(20.0)),
|
||||
sp("shading/ambient", "Ambient", pc(40.0)),
|
||||
sp("shading/diffuse", "Diffuse", pc(60.0)),
|
||||
sp("shading/specular", "Specular", pc(30.0)),
|
||||
sp("shading/roughness", "Roughness", pc(20.0)),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
cc_hair,
|
||||
|
|
@ -562,7 +574,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn cannon(t: f64) -> Image {
|
||||
run(&[("cannonPosition", Value::Vec2([40.0, 50.0])), ("velocity", num(60.0)), ("gravityForce", num(30.0))], t)
|
||||
run(&[("cannon/cannonPosition", Value::Vec2([40.0, 50.0])), ("cannon/velocity", num(60.0)), ("gravity/gravityForce", num(30.0))], t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -590,13 +602,13 @@ mod tests {
|
|||
"ec.sim.particleplayground",
|
||||
&[
|
||||
("cannonEnabled", Value::Bool(false)),
|
||||
("gridPosition", Value::Vec2([40.0, 10.0])),
|
||||
("gridWidth", num(40.0)),
|
||||
("gridHeight", num(0.0)),
|
||||
("particlesAcross", num(5.0)),
|
||||
("particlesDown", num(1.0)),
|
||||
("gravityForce", num(100.0)),
|
||||
("wallBoundary", num(1.0)),
|
||||
("grid/gridPosition", Value::Vec2([40.0, 10.0])),
|
||||
("grid/gridWidth", num(40.0)),
|
||||
("grid/gridHeight", num(0.0)),
|
||||
("grid/particlesAcross", num(5.0)),
|
||||
("grid/particlesDown", num(1.0)),
|
||||
("gravity/gravityForce", num(100.0)),
|
||||
("wall/wallBoundary", num(1.0)),
|
||||
],
|
||||
Image::new(80, 60),
|
||||
t,
|
||||
|
|
@ -634,14 +646,14 @@ mod tests {
|
|||
let img = run(
|
||||
&[
|
||||
("cannonEnabled", Value::Bool(false)),
|
||||
("gridPosition", Value::Vec2([40.0, 30.0])),
|
||||
("gridWidth", num(6.0)),
|
||||
("gridHeight", num(6.0)),
|
||||
("particlesAcross", num(3.0)),
|
||||
("particlesDown", num(3.0)),
|
||||
("gravityForce", num(0.0)),
|
||||
("repelForce", num(force)),
|
||||
("repelForceRadius", num(20.0)),
|
||||
("grid/gridPosition", Value::Vec2([40.0, 30.0])),
|
||||
("grid/gridWidth", num(6.0)),
|
||||
("grid/gridHeight", num(6.0)),
|
||||
("grid/particlesAcross", num(3.0)),
|
||||
("grid/particlesDown", num(3.0)),
|
||||
("gravity/gravityForce", num(0.0)),
|
||||
("repel/repelForce", num(force)),
|
||||
("repel/repelForceRadius", num(20.0)),
|
||||
],
|
||||
0.5,
|
||||
);
|
||||
|
|
@ -662,8 +674,22 @@ mod tests {
|
|||
#[test]
|
||||
fn property_mapper_and_layer_map_read_layers() {
|
||||
// Without a host, the layer-based features are inert (deterministic output).
|
||||
let a = run(&[("mapRedTo", Value::Enum(6)), ("redMin", num(-50.0)), ("redMax", num(50.0))], 0.7);
|
||||
let b = run(&[("mapRedTo", Value::Enum(6)), ("redMin", num(-50.0)), ("redMax", num(50.0))], 0.7);
|
||||
let a = run(
|
||||
&[
|
||||
("persistentPropertyMapper/mapRedTo", Value::Enum(8)),
|
||||
("persistentPropertyMapper/redMin", num(-50.0)),
|
||||
("persistentPropertyMapper/redMax", num(50.0)),
|
||||
],
|
||||
0.7,
|
||||
);
|
||||
let b = run(
|
||||
&[
|
||||
("persistentPropertyMapper/mapRedTo", Value::Enum(8)),
|
||||
("persistentPropertyMapper/redMin", num(-50.0)),
|
||||
("persistentPropertyMapper/redMax", num(50.0)),
|
||||
],
|
||||
0.7,
|
||||
);
|
||||
assert_eq!(a.data, b.data);
|
||||
let st = Pg {
|
||||
cannon: false,
|
||||
|
|
@ -682,7 +708,7 @@ mod tests {
|
|||
repel_radius: 0.0,
|
||||
wall: None,
|
||||
mapper: Some(MapImg { img: Image::filled(4, 4, [1.0, 0.0, 0.5, 1.0]), k: [1.0; 2], o: [0.0; 2] }),
|
||||
map_to: [(6, 0.0, 120.0), (0, 0.0, 1.0), (4, 0.0, 2.0)],
|
||||
map_to: [(8, 0.0, 120.0), (0, 0.0, 1.0), (5, 0.0, 2.0)],
|
||||
seed: 1,
|
||||
bounds: [-1e6, -1e6, 1e6, 1e6],
|
||||
};
|
||||
|
|
@ -694,6 +720,77 @@ mod tests {
|
|||
assert!((s.parts[0].r - 2.0).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn barrel_radius_square_or_disc() {
|
||||
let pg = |r: f32| Pg {
|
||||
cannon: true,
|
||||
pos: [0.0; 2],
|
||||
barrel_angle: 0.0,
|
||||
barrel_radius: r,
|
||||
rate: 0.0,
|
||||
dir_spread: 0.0,
|
||||
vel: 0.0,
|
||||
vel_spread: 0.0,
|
||||
color: [1.0; 4],
|
||||
radius: 2.0,
|
||||
gravity: [0.0; 2],
|
||||
grav_spread: 0.0,
|
||||
repel: 0.0,
|
||||
repel_radius: 0.0,
|
||||
wall: None,
|
||||
mapper: None,
|
||||
map_to: [(0, 0.0, 1.0); 3],
|
||||
seed: 3,
|
||||
bounds: [-1e6, -1e6, 1e6, 1e6],
|
||||
};
|
||||
let (sq, disc) = (pg(10.0), pg(-10.0));
|
||||
let mut corner = false;
|
||||
for id in 0..400 {
|
||||
let a = sq.spawn(id).p;
|
||||
assert!(a[0].abs() <= 10.0 && a[1].abs() <= 10.0);
|
||||
// A square barrel fills its corners (both offsets in use, not a line).
|
||||
corner |= a[0].abs() > 7.5 && a[1].abs() > 7.5;
|
||||
let b = disc.spawn(id).p;
|
||||
assert!(b[0].hypot(b[1]) <= 10.0 + 1e-4);
|
||||
}
|
||||
assert!(corner);
|
||||
// Radius 0: every particle starts at the cannon position.
|
||||
assert_eq!(pg(0.0).spawn(5).p, [0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper_drives_position_and_opacity_in_ae_order() {
|
||||
let st = Pg {
|
||||
cannon: false,
|
||||
pos: [0.0; 2],
|
||||
barrel_angle: 0.0,
|
||||
barrel_radius: 0.0,
|
||||
rate: 0.0,
|
||||
dir_spread: 0.0,
|
||||
vel: 0.0,
|
||||
vel_spread: 0.0,
|
||||
color: [1.0; 4],
|
||||
radius: 2.0,
|
||||
gravity: [0.0; 2],
|
||||
grav_spread: 0.0,
|
||||
repel: 0.0,
|
||||
repel_radius: 0.0,
|
||||
wall: None,
|
||||
mapper: Some(MapImg { img: Image::filled(4, 4, [1.0, 0.5, 0.25, 1.0]), k: [1.0; 2], o: [0.0; 2] }),
|
||||
map_to: [(6, 0.0, 30.0), (12, 0.0, 1.0), (4, 0.0, 1.0)],
|
||||
seed: 1,
|
||||
bounds: [-1e6, -1e6, 1e6, 1e6],
|
||||
};
|
||||
assert_eq!(MAP_TARGETS[6], "X");
|
||||
assert_eq!(MAP_TARGETS[12], "Opacity");
|
||||
let mut s = PgState::default();
|
||||
s.parts.push(PgParticle { p: [1.0, 1.0], v: [0.0; 2], origin: [1.0, 1.0], c: [1.0; 4], r: 2.0, mass: 1.0, friction: 0.0, force: [0.0; 2], id: 0 });
|
||||
st.step(&mut s);
|
||||
assert!((s.parts[0].p[0] - 30.0).abs() < 1e-3, "{:?}", s.parts[0].p);
|
||||
assert!((s.parts[0].c[3] - 0.5).abs() < 1e-3);
|
||||
assert!((s.parts[0].friction - 0.25).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cc_hair_grows_from_opaque_pixels() {
|
||||
let mut img = Image::new(60, 60);
|
||||
|
|
|
|||
|
|
@ -38,12 +38,17 @@ fn cartoon(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let render = ctx.params.e("render");
|
||||
let r = (ctx.params.f("detailRadius") * b.scale).round().max(0.0) as usize;
|
||||
let thr = ctx.params.f("detailThreshold") as f32 / 100.0;
|
||||
let steps = ctx.params.f("shadingSteps").max(1.0) as f32;
|
||||
let smooth = ctx.params.f("shadingSmoothness") as f32 / 100.0;
|
||||
let et = ctx.params.f("edgeThreshold") as f32;
|
||||
let ew = ctx.params.f("edgeWidth") * b.scale;
|
||||
let es = ctx.params.f("edgeSoftness") as f32 / 100.0;
|
||||
let eo = ctx.params.f("edgeOpacity") as f32 / 100.0;
|
||||
let steps = ctx.params.f("fill/shadingSteps").max(1.0) as f32;
|
||||
let smooth = ctx.params.f("fill/shadingSmoothness") as f32 / 100.0;
|
||||
let et = ctx.params.f("edge/edgeThreshold") as f32;
|
||||
let ew = ctx.params.f("edge/edgeWidth") * b.scale;
|
||||
let es = ctx.params.f("edge/edgeSoftness") as f32 / 100.0;
|
||||
let eo = ctx.params.f("edge/edgeOpacity") as f32 / 100.0;
|
||||
// Advanced: Edge Black Level shifts the grey representation of the edges towards white
|
||||
// strokes on black; Edge Contrast (0.5 = neutral) steepens or flattens it.
|
||||
let black = (ctx.params.f("advanced/edgeBlackLevel") as f32 / 100.0).clamp(0.0, 1.0);
|
||||
let contrast = ((ctx.params.f("advanced/edgeContrast").clamp(0.0, 0.999) * std::f64::consts::FRAC_PI_2).tan() as f32).min(100.0);
|
||||
let grey = move |e: f32| (((1.0 - e) - black).abs() - 0.5) * contrast + 0.5;
|
||||
// Edge-preserving smoothing: guided filter on each premultiplied channel, guided by luma.
|
||||
let smoothed = if r > 0 {
|
||||
let guide = Plane::luma(&b.img);
|
||||
|
|
@ -87,10 +92,11 @@ fn cartoon(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if l > 1e-4 { c.map(|v| v * lq / l) } else { [lq; 3] }
|
||||
};
|
||||
let e = edges.as_ref().map(|e| e.data[i] * eo).unwrap_or(0.0);
|
||||
let g = grey(e).clamp(0.0, 1.0);
|
||||
let o = match render {
|
||||
0 => fill,
|
||||
1 => [1.0 - e; 3],
|
||||
_ => fill.map(|v| v * (1.0 - e)),
|
||||
1 => [g; 3],
|
||||
_ => fill.map(|v| v * g),
|
||||
};
|
||||
*px = premul(o, a);
|
||||
});
|
||||
|
|
@ -109,6 +115,18 @@ fn roughen_edges(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (ox, oy) = b.to_px(ctx.params.v2("offset"));
|
||||
let oct = ctx.params.f("complexity") as f32;
|
||||
let evo = ctx.params.f("evolution") as f32 / 360.0;
|
||||
// Stretch Width or Height: positive values widen the fractal, negative ones heighten it.
|
||||
let st = ctx.params.f("stretch") as f32;
|
||||
let (sx, sy) = (1.0 + st.max(0.0), 1.0 + (-st).max(0.0));
|
||||
// Cycle Evolution: cross-fade one cycle into the next so the roughness loops seamlessly.
|
||||
let cycle = ctx.params.b("evolutionOptions/cycleEvolution").then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0) as f32);
|
||||
let (evo, evo_w) = match cycle {
|
||||
Some(c) => {
|
||||
let e = evo.rem_euclid(c);
|
||||
(e, e / c)
|
||||
}
|
||||
None => (evo, 0.0),
|
||||
};
|
||||
// (frequency, influence, sharpness, coloured) per edge type.
|
||||
let (fm, im, sm, colored) = match kind {
|
||||
1 => (1.0, 1.0, 1.0, true),
|
||||
|
|
@ -123,14 +141,18 @@ fn roughen_edges(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let alpha = Plane::alpha(&b.img);
|
||||
let d = if border > 0.05 { gauss_plane(&alpha, border * 0.5, border * 0.5) } else { alpha };
|
||||
let extra = if kind == 4 || kind == 5 { 1.0 } else { 0.0 };
|
||||
let seed = ctx.seed ^ 0x40f1;
|
||||
let seed = ctx.seed ^ 0x40f1 ^ (ctx.params.f("evolutionOptions/randomSeed").round() as i64 as u32).wrapping_mul(0x9e37_79b9);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
if px[3] <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let dv = d.get(x, y);
|
||||
let n = fbm((x as f32 + 0.5 - ox as f32) / scale * fm, (y as f32 + 0.5 - oy as f32) / scale * fm, evo, seed, oct + extra);
|
||||
let (nx, ny) = ((x as f32 + 0.5 - ox as f32) / (scale * sx) * fm, (y as f32 + 0.5 - oy as f32) / (scale * sy) * fm);
|
||||
let mut n = fbm(nx, ny, evo, seed, oct + extra);
|
||||
if let Some(c) = cycle {
|
||||
n += (fbm(nx, ny, evo - c, seed, oct + extra) - n) * evo_w;
|
||||
}
|
||||
let s = dv - 0.5 + (n - 0.5) * infl * im * 0.5;
|
||||
let m = (s * sharp * sm * 4.0 + 0.5).clamp(0.0, 1.0);
|
||||
let (mut c, a) = unpremul(*px);
|
||||
|
|
@ -206,7 +228,7 @@ fn strobe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
// ---- Texturize (self relief) ----
|
||||
// ---- Texturize ----
|
||||
|
||||
fn texturize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ang = ctx.params.f("lightDirection").to_radians();
|
||||
|
|
@ -214,8 +236,11 @@ fn texturize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if k == 0.0 {
|
||||
return b;
|
||||
}
|
||||
// Relief from the texture layer's luminance; without a texture there is nothing to do.
|
||||
let Some(tex) = ctx.layer_param("textureLayer", true) else { return b };
|
||||
let tex = crate::transition::place_layer(ctx, &b, &tex, ctx.params.e("texturePlacement"));
|
||||
let (lx, ly) = (ang.cos() as f32, -ang.sin() as f32);
|
||||
let h = gauss_plane(&Plane::luma(&b.img), 0.7 * b.scale, 0.7 * b.scale);
|
||||
let h = gauss_plane(&Plane::luma(&tex), 0.7 * b.scale, 0.7 * b.scale);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let gx = (h.get_clamped(x as i64 + 1, y as i64) - h.get_clamped(x as i64 - 1, y as i64)) * 0.5;
|
||||
|
|
@ -512,12 +537,14 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("render", "Render", Value::Enum(2), popup(&["Fill", "Edges", "Fill & Edges"])),
|
||||
p("detailRadius", "Detail Radius", num(8.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("detailThreshold", "Detail Threshold", num(10.0), pct()),
|
||||
p("shadingSteps", "Shading Steps", num(8.0), slider(1.0, 64.0, 1.0, 64.0, 1)),
|
||||
p("shadingSmoothness", "Shading Smoothness", num(70.0), pct()),
|
||||
p("edgeThreshold", "Edge Threshold", num(1.5), slider(0.0, 5.0, 0.0, 5.0, 2)),
|
||||
p("edgeWidth", "Edge Width", num(1.5), slider(0.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("edgeSoftness", "Edge Softness", num(60.0), pct()),
|
||||
p("edgeOpacity", "Edge Opacity", num(100.0), pct()),
|
||||
p("fill/shadingSteps", "Shading Steps", num(8.0), slider(1.0, 64.0, 1.0, 64.0, 1)),
|
||||
p("fill/shadingSmoothness", "Shading Smoothness", num(70.0), pct()),
|
||||
p("edge/edgeThreshold", "Threshold", num(1.5), slider(0.0, 5.0, 0.0, 5.0, 2)),
|
||||
p("edge/edgeWidth", "Width", num(1.5), slider(0.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("edge/edgeSoftness", "Softness", num(60.0), pct()),
|
||||
p("edge/edgeOpacity", "Opacity", num(100.0), pct()),
|
||||
p("advanced/edgeBlackLevel", "Edge Black Level", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("advanced/edgeContrast", "Edge Contrast", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
],
|
||||
cartoon,
|
||||
),
|
||||
|
|
@ -536,9 +563,13 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("edgeSharpness", "Edge Sharpness", num(1.0), slider(0.0, 10.0, 0.0, 10.0, 2)),
|
||||
p("fractalInfluence", "Fractal Influence", num(1.0), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
p("scale", "Scale", num(100.0), slider(10.0, 1000.0, 10.0, 300.0, 1)),
|
||||
p("stretch", "Stretch Width or Height", num(0.0), slider(-10.0, 10.0, -5.0, 5.0, 2)),
|
||||
p("offset", "Offset (Turbulence)", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("complexity", "Complexity", num(2.0), slider(1.0, 10.0, 1.0, 10.0, 0)),
|
||||
p("evolution", "Evolution", num(0.0), ParamUi::Angle),
|
||||
p("evolutionOptions/cycleEvolution", "Cycle Evolution", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("evolutionOptions/cycle", "Cycle (in Revolutions)", num(1.0), slider(1.0, 1000.0, 1.0, 20.0, 0)),
|
||||
p("evolutionOptions/randomSeed", "Random Seed", num(0.0), slider(0.0, 10000.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
roughen_edges,
|
||||
),
|
||||
|
|
@ -548,7 +579,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
p("amount", "Scatter Amount", num(0.0), slider(0.0, 127.0, 0.0, 127.0, 1)),
|
||||
p("grain", "Grain", Value::Enum(0), popup(&["Both", "Horizontal", "Vertical"])),
|
||||
p("randomizeEveryFrame", "Randomize Every Frame", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("randomizeEveryFrame", "Scatter Randomness", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
scatter,
|
||||
),
|
||||
|
|
@ -575,8 +606,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.stylize.texturize",
|
||||
"Texturize",
|
||||
vec![
|
||||
p("textureLayer", "Texture Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("lightDirection", "Light Direction", num(135.0), ParamUi::Angle),
|
||||
p("textureContrast", "Texture Contrast", num(1.0), slider(0.0, 2.0, 0.0, 2.0, 2)),
|
||||
p("texturePlacement", "Texture Placement", Value::Enum(0), popup(&["Tile Texture", "Center Texture", "Stretch Texture to Fit"])),
|
||||
],
|
||||
texturize,
|
||||
),
|
||||
|
|
@ -800,7 +833,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cartoon_quantises_flat_regions() {
|
||||
let out = run_at("ec.stylize.cartoon", &[("render", Value::Enum(0)), ("shadingSmoothness", num(0.0)), ("shadingSteps", num(4.0))], ramp(24, 8), 0.0);
|
||||
let out = run_at(
|
||||
"ec.stylize.cartoon",
|
||||
&[("render", Value::Enum(0)), ("fill/shadingSmoothness", num(0.0)), ("fill/shadingSteps", num(4.0))],
|
||||
ramp(24, 8),
|
||||
0.0,
|
||||
);
|
||||
let mut lv: Vec<i32> = out
|
||||
.img
|
||||
.data
|
||||
|
|
@ -814,4 +852,145 @@ mod tests {
|
|||
lv.dedup();
|
||||
assert!(lv.len() <= 6, "{lv:?}");
|
||||
}
|
||||
|
||||
/// A host whose every layer is `img`.
|
||||
struct ImgHost(Image);
|
||||
impl crate::EffectHost for ImgHost {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
let s = [self.0.width as f64, self.0.height as f64];
|
||||
Some(crate::LayerPixels { buf: Buf { img: self.0.clone(), offset: [0.0; 2], scale: 1.0 }, size: s })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn run_env(id: &str, vals: &[(&str, Value)], img: Image, env: crate::EffectEnv) -> Buf {
|
||||
let s = crate::find(id).unwrap();
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env };
|
||||
crate::apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn texturize_needs_a_texture_layer_and_places_it() {
|
||||
let img = Image::filled(16, 16, [0.5, 0.5, 0.5, 1.0]);
|
||||
// No texture layer: nothing to emboss.
|
||||
assert_eq!(run_at("ec.stylize.texturize", &[], img.clone(), 0.0).img.data, img.data);
|
||||
// A 4-px stripe texture, tiled, shades the flat layer along the stripes.
|
||||
let mut tex = Image::new(4, 4);
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
let v = if x < 2 { 1.0 } else { 0.0 };
|
||||
tex.set(x, y, [v, v, v, 1.0]);
|
||||
}
|
||||
}
|
||||
let host = ImgHost(tex);
|
||||
let env = crate::EffectEnv { host: Some(&host), ..Default::default() };
|
||||
let out = run_env("ec.stylize.texturize", &[("textureLayer", Value::Layer(Some(1))), ("lightDirection", num(0.0))], img.clone(), env);
|
||||
let row: Vec<f32> = (0..16).map(|x| out.img.get(x, 8)[0]).collect();
|
||||
assert!(row.iter().any(|v| *v > 0.55) && row.iter().any(|v| *v < 0.45), "{row:?}");
|
||||
// Repeats every 4 px (tiled).
|
||||
assert!((row[5] - row[9]).abs() < 1e-4 && (row[6] - row[10]).abs() < 1e-4, "{row:?}");
|
||||
// Stretched, the single stripe edge sits in the middle of the layer only.
|
||||
let out = run_env(
|
||||
"ec.stylize.texturize",
|
||||
&[("textureLayer", Value::Layer(Some(1))), ("lightDirection", num(0.0)), ("texturePlacement", Value::Enum(2))],
|
||||
img,
|
||||
env,
|
||||
);
|
||||
let (a, b) = (out.img.get(4, 8)[0], out.img.get(8, 8)[0]);
|
||||
assert!((a - b).abs() > 0.02, "stretched is not periodic every 4 px: {a} {b}");
|
||||
assert!((out.img.get(8, 8)[0] - 0.5).abs() > 0.05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mosaic_averages_unless_sharp_colors() {
|
||||
let img = ramp(20, 10);
|
||||
let avg = run_at("ec.stylize.mosaic", &[("horizontal", num(2.0)), ("vertical", num(1.0))], img.clone(), 0.0);
|
||||
// Left tile: the mean of x / 20 over x = 0..9 is 0.225.
|
||||
assert!((avg.img.get(3, 4)[0] - 0.225).abs() < 1e-4, "{:?}", avg.img.get(3, 4));
|
||||
let sharp = run_at("ec.stylize.mosaic", &[("horizontal", num(2.0)), ("vertical", num(1.0)), ("sharpColors", Value::Bool(true))], img.clone(), 0.0);
|
||||
assert_eq!(sharp.img.get(3, 4), img.get(5, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_edges_and_emboss_blend_with_original() {
|
||||
let img = ramp(12, 12);
|
||||
for id in ["ec.stylize.findedges", "ec.stylize.emboss"] {
|
||||
let full = run_at(id, &[("blend", num(100.0))], img.clone(), 0.0);
|
||||
assert!(full.img.data.iter().zip(&img.data).all(|(a, b)| (0..4).all(|c| (a[c] - b[c]).abs() < 1e-6)), "{id}");
|
||||
let fx = run_at(id, &[], img.clone(), 0.0);
|
||||
let half = run_at(id, &[("blend", num(50.0))], img.clone(), 0.0);
|
||||
let (a, b, h) = (fx.img.get(6, 6)[1], img.get(6, 6)[1], half.img.get(6, 6)[1]);
|
||||
assert!((h - (a + b) * 0.5).abs() < 1e-5, "{id}: {a} {b} {h}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glow_composite_none_dimensions_and_ab_looping() {
|
||||
let mut img = Image::new(30, 30);
|
||||
for y in 12..18 {
|
||||
for x in 12..18 {
|
||||
img.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
let vals = |extra: &[(&'static str, Value)]| {
|
||||
let mut v = vec![("radius", num(8.0)), ("threshold", num(20.0))];
|
||||
v.extend_from_slice(extra);
|
||||
v
|
||||
};
|
||||
// None: the glow alone (the core is the glow's own brightness, not the original's 1.0 + glow).
|
||||
let on_top = run_at("ec.stylize.glow", &vals(&[]), img.clone(), 0.0);
|
||||
let none = run_at("ec.stylize.glow", &vals(&[("operation", Value::Enum(2))]), img.clone(), 0.0);
|
||||
let (ox, oy) = (on_top.offset[0] as i64, on_top.offset[1] as i64);
|
||||
assert!(none.img.get(ox + 15, oy + 15)[0] < on_top.img.get(ox + 15, oy + 15)[0]);
|
||||
// Horizontal only: glow spreads sideways but not up.
|
||||
let h = run_at("ec.stylize.glow", &vals(&[("glowDimensions", Value::Enum(1))]), img.clone(), 0.0);
|
||||
assert!(h.img.get(ox + 21, oy + 15)[3] > 0.05);
|
||||
assert!(h.img.get(ox + 15, oy + 9)[3] < 1e-4);
|
||||
// A & B colour looping: triangle returns to A at full brightness, sawtooth ends at B.
|
||||
assert!((crate::misc::glow_ab_t(1.0, 2, 1.0, 0.0, 0.5)).abs() < 1e-5);
|
||||
assert!((crate::misc::glow_ab_t(1.0, 0, 1.0, 0.0, 0.5) - 1.0).abs() < 1e-5);
|
||||
assert!((crate::misc::glow_ab_t(0.5, 2, 1.0, 0.0, 0.5) - 1.0).abs() < 1e-5);
|
||||
// The midpoint moves where the gradient is half-way.
|
||||
assert!((crate::misc::glow_ab_t(0.2, 0, 1.0, 0.0, 0.2) - 0.5).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cartoon_edge_black_level_inverts_edges() {
|
||||
let mut img = Image::filled(24, 24, [0.0, 0.0, 0.0, 1.0]);
|
||||
for y in 0..24 {
|
||||
for x in 12..24 {
|
||||
img.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
let edges = |bl: f64| run_at("ec.stylize.cartoon", &[("render", Value::Enum(1)), ("advanced/edgeBlackLevel", num(bl))], img.clone(), 0.0);
|
||||
let plain = edges(0.0);
|
||||
let inv = edges(100.0);
|
||||
// Flat areas: white without black level, black at full black level; the edge flips too.
|
||||
assert!(plain.img.get(3, 12)[0] > 0.99 && inv.img.get(3, 12)[0] < 0.01);
|
||||
assert!(plain.img.get(12, 12)[0] < inv.img.get(12, 12)[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roughen_edges_seed_stretch_and_cycle() {
|
||||
let mut img = Image::new(40, 40);
|
||||
for y in 8..32 {
|
||||
for x in 8..32 {
|
||||
img.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
let run = |vals: &[(&str, Value)]| run_at("ec.stylize.roughenedges", vals, img.clone(), 0.0).img.data;
|
||||
let base = run(&[]);
|
||||
assert_ne!(base, run(&[("evolutionOptions/randomSeed", num(7.0))]));
|
||||
assert_ne!(base, run(&[("stretch", num(3.0))]));
|
||||
// Cycling over 1 revolution: evolution 0° and 360° give the same roughness.
|
||||
let cyc = |e: f64| run(&[("evolutionOptions/cycleEvolution", Value::Bool(true)), ("evolution", num(e))]);
|
||||
assert_eq!(cyc(0.0), cyc(360.0));
|
||||
assert_ne!(cyc(0.0), cyc(180.0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,13 +227,19 @@ fn draw_text(b: &mut Buf, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) {
|
|||
});
|
||||
}
|
||||
|
||||
/// Parameter `id`, read from its `group` twirl-down when the effect nests it there.
|
||||
fn grouped<'a>(pr: &'a crate::Params, group: &str, id: &str) -> Option<&'a Value> {
|
||||
pr.get(&format!("{group}/{id}")).or_else(|| pr.get(id))
|
||||
}
|
||||
|
||||
fn look_from(ctx: &EffectCtx, b: &Buf, on_original_id: &str) -> TextLook {
|
||||
let pr = ctx.params;
|
||||
let fs = |id: &str| grouped(pr, "fillAndStroke", id);
|
||||
TextLook {
|
||||
display: pr.e("displayOptions"),
|
||||
fill: pr.color("fillColor"),
|
||||
stroke: pr.color("strokeColor"),
|
||||
stroke_w: pr.f("strokeWidth") * b.scale,
|
||||
display: fs("displayOptions").map(Value::as_enum).unwrap_or(0),
|
||||
fill: fs("fillColor").map(Value::as_color).unwrap_or([1.0; 4]),
|
||||
stroke: fs("strokeColor").map(Value::as_color).unwrap_or([1.0; 4]),
|
||||
stroke_w: fs("strokeWidth").map(Value::as_f64).unwrap_or(0.0) * b.scale,
|
||||
on_original: pr.b(on_original_id),
|
||||
opacity: 1.0,
|
||||
}
|
||||
|
|
@ -268,8 +274,24 @@ fn timecode(frames: i64, fps: f64) -> String {
|
|||
if neg { format!("-{s}") } else { s }
|
||||
}
|
||||
|
||||
/// Numbers' twirl-down groups (Format, Fill and Stroke).
|
||||
const NUMBERS_GROUPS: [&str; 2] = ["format/", "fillAndStroke/"];
|
||||
|
||||
/// `params` with Numbers' group prefixes stripped (`format/value` → `value`), so the drawing
|
||||
/// helpers shared with Basic Text read plain ids. Plain keys already present win.
|
||||
fn ungroup(params: &crate::Params) -> crate::Params {
|
||||
let mut out = params.clone();
|
||||
for (k, v) in ¶ms.values {
|
||||
if let Some(leaf) = NUMBERS_GROUPS.iter().find_map(|g| k.strip_prefix(g)) {
|
||||
out.values.entry(leaf.to_string()).or_insert_with(|| v.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn numbers_text(ctx: &EffectCtx) -> String {
|
||||
let pr = ctx.params;
|
||||
let flat = ungroup(ctx.params);
|
||||
let pr = ♭
|
||||
let kind = pr.e("type");
|
||||
let dp = pr.f("decimalPlaces").round().clamp(0.0, 10.0) as usize;
|
||||
let mut v = pr.f("value");
|
||||
|
|
@ -311,6 +333,8 @@ fn numbers_text(ctx: &EffectCtx) -> String {
|
|||
}
|
||||
|
||||
fn numbers(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let flat = ungroup(ctx.params);
|
||||
let ctx = &EffectCtx { params: &flat, time: ctx.time, layer_size: ctx.layer_size, seed: ctx.seed, adjustment: ctx.adjustment, env: ctx.env };
|
||||
let pr = ctx.params;
|
||||
let text = numbers_text(ctx);
|
||||
let size = (pr.f("size") * b.scale).max(0.5);
|
||||
|
|
@ -431,13 +455,14 @@ fn basic_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
fn path_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let g = |group: &str, id: &str| grouped(pr, group, id).cloned().unwrap_or(Value::Scalar(0.0));
|
||||
let px = |id: &str| {
|
||||
let q = b.to_px(pr.v2(id));
|
||||
let q = b.to_px(g("pathOptions/controlPoints", id).as_vec2());
|
||||
[q.0, q.1]
|
||||
};
|
||||
let (mut path, mut closed) = match mask_px(ctx, &b, pr.f("customPath").round() as usize) {
|
||||
let (mut path, mut closed) = match mask_px(ctx, &b, g("pathOptions", "customPath").as_f64().round() as usize) {
|
||||
Some((p, c, _)) => (p, c),
|
||||
None => match pr.e("shapeType") {
|
||||
None => match g("pathOptions", "shapeType").as_enum() {
|
||||
1 | 2 => {
|
||||
let c = px("vertex1");
|
||||
let s = px("tangent1");
|
||||
|
|
@ -471,26 +496,26 @@ fn path_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
},
|
||||
};
|
||||
if pr.b("reversePath") {
|
||||
if g("pathOptions", "reversePath").as_bool() {
|
||||
path.reverse();
|
||||
}
|
||||
if path.len() < 2 {
|
||||
closed = false;
|
||||
}
|
||||
let len = poly_length(&path, closed);
|
||||
let text: String = pr.s("text").chars().take(pr.f("visibleCharacters").max(0.0) as usize).collect();
|
||||
let size = (pr.f("size") * b.scale).max(0.5);
|
||||
let text: String = pr.s("text").chars().take(g("advanced", "visibleCharacters").as_f64().max(0.0) as usize).collect();
|
||||
let size = (g("character", "size").as_f64() * b.scale).max(0.5);
|
||||
let u = unit(size);
|
||||
let tracking = pr.f("tracking") * b.scale;
|
||||
let lm = pr.f("leftMargin") * b.scale;
|
||||
let rm = pr.f("rightMargin") * b.scale;
|
||||
let shift = pr.f("baselineShift") * b.scale;
|
||||
let rot = pr.f("characterRotation").to_radians();
|
||||
let tracking = g("character", "tracking").as_f64() * b.scale;
|
||||
let lm = g("paragraph", "leftMargin").as_f64() * b.scale;
|
||||
let rm = g("paragraph", "rightMargin").as_f64() * b.scale;
|
||||
let shift = g("paragraph", "baselineShift").as_f64() * b.scale;
|
||||
let rot = g("character/orientation", "characterRotation").as_f64().to_radians();
|
||||
let n = text.chars().count();
|
||||
let advs: Vec<f64> = text.chars().map(|c| advance(c, true) * u).collect();
|
||||
let natural: f64 = advs.iter().sum::<f64>() + tracking * (n.max(1) as f64 - 1.0);
|
||||
let avail = (len - lm - rm).max(0.0);
|
||||
let (mut cursor, extra) = match pr.e("alignment") {
|
||||
let (mut cursor, extra) = match g("paragraph", "alignment").as_enum() {
|
||||
1 => (len - rm - natural, 0.0),
|
||||
2 => (lm + (avail - natural) * 0.5, 0.0),
|
||||
3 => (lm, if n > 1 { (avail - natural) / (n as f64 - 1.0) } else { 0.0 }),
|
||||
|
|
@ -691,8 +716,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Text",
|
||||
vec![
|
||||
p(
|
||||
"type",
|
||||
"Format: Type",
|
||||
"format/type",
|
||||
"Type",
|
||||
Value::Enum(0),
|
||||
popup(&[
|
||||
"Number",
|
||||
|
|
@ -707,15 +732,15 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Hexadecimal",
|
||||
]),
|
||||
),
|
||||
check("randomValues", "Format: Random Values", false),
|
||||
p("value", "Format: Value/Offset/Random Max", num(0.0), slider(-30000.0, 30000.0, -1000.0, 1000.0, 3)),
|
||||
p("decimalPlaces", "Format: Decimal Places", num(2.0), slider(0.0, 10.0, 0.0, 10.0, 0)),
|
||||
check("currentTimeDate", "Format: Current Time/Date", false),
|
||||
p("position", "Fill and Stroke: Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("displayOptions", "Fill and Stroke: Display Options", Value::Enum(0), display()),
|
||||
p("fillColor", "Fill and Stroke: Fill Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("strokeColor", "Fill and Stroke: Stroke Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("strokeWidth", "Fill and Stroke: Stroke Width", num(2.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
check("format/randomValues", "Random Values", false),
|
||||
p("format/value", "Value/Offset/Random Max", num(0.0), slider(-30000.0, 30000.0, -1000.0, 1000.0, 3)),
|
||||
p("format/decimalPlaces", "Decimal Places", num(2.0), slider(0.0, 10.0, 0.0, 10.0, 0)),
|
||||
check("format/currentTimeDate", "Current Time/Date", false),
|
||||
p("fillAndStroke/position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("fillAndStroke/displayOptions", "Display Options", Value::Enum(0), display()),
|
||||
p("fillAndStroke/fillColor", "Fill Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("fillAndStroke/strokeColor", "Stroke Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("fillAndStroke/strokeWidth", "Stroke Width", num(2.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("size", "Size", num(36.0), size()),
|
||||
p("tracking", "Tracking", num(0.0), slider(-100.0, 100.0, -20.0, 50.0, 1)),
|
||||
check("proportionalSpacing", "Proportional Spacing", true),
|
||||
|
|
@ -739,7 +764,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
check("showBox", "Show Box", true),
|
||||
p("boxColor", "Box Color", col(0.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
check("renderOnOriginal", "Render On Original", true),
|
||||
check("renderOnOriginal", "Composite on Original", true),
|
||||
],
|
||||
timecode_fx,
|
||||
),
|
||||
|
|
@ -751,10 +776,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("text", "Text", Value::Str("Text".into()), ParamUi::Text),
|
||||
p("alignment", "Alignment", Value::Enum(1), popup(&["Left", "Center", "Right"])),
|
||||
p("position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("displayOptions", "Fill and Stroke: Display Options", Value::Enum(0), display()),
|
||||
p("fillColor", "Fill and Stroke: Fill Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("strokeColor", "Fill and Stroke: Stroke Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("strokeWidth", "Fill and Stroke: Stroke Width", num(2.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("fillAndStroke/displayOptions", "Display Options", Value::Enum(0), display()),
|
||||
p("fillAndStroke/fillColor", "Fill Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("fillAndStroke/strokeColor", "Stroke Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("fillAndStroke/strokeWidth", "Stroke Width", num(2.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("size", "Size", num(36.0), size()),
|
||||
p("tracking", "Tracking", num(0.0), slider(-100.0, 100.0, -20.0, 50.0, 1)),
|
||||
p("lineSpacing", "Line Spacing", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||||
|
|
@ -768,25 +793,25 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Obsolete",
|
||||
vec![
|
||||
p("text", "Text", Value::Str("Text".into()), ParamUi::Text),
|
||||
p("shapeType", "Path Options: Shape Type", Value::Enum(0), popup(&["Bezier", "Circle", "Loop", "Line"])),
|
||||
p("tangent1", "Control Points: Tangent 1/Circle Point", pt(0.3, 0.3), ParamUi::Point),
|
||||
p("vertex1", "Control Points: Vertex 1/Circle Center", pt(0.1, 0.6), ParamUi::Point),
|
||||
p("tangent2", "Control Points: Tangent 2", pt(0.7, 0.3), ParamUi::Point),
|
||||
p("vertex2", "Control Points: Vertex 2/Line Right", pt(0.9, 0.6), ParamUi::Point),
|
||||
p("customPath", "Path Options: Custom Path", num(0.0), slider(0.0, 32.0, 0.0, 8.0, 0)),
|
||||
check("reversePath", "Path Options: Reverse Path", false),
|
||||
p("displayOptions", "Fill and Stroke: Options", Value::Enum(0), display()),
|
||||
p("fillColor", "Fill and Stroke: Fill Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("strokeColor", "Fill and Stroke: Stroke Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("strokeWidth", "Fill and Stroke: Stroke Width", num(2.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("size", "Character: Size", num(36.0), size()),
|
||||
p("tracking", "Character: Tracking", num(0.0), slider(-100.0, 100.0, -20.0, 50.0, 1)),
|
||||
p("characterRotation", "Character: Orientation: Character Rotation", num(0.0), ang()),
|
||||
p("alignment", "Paragraph: Alignment", Value::Enum(0), popup(&["Left", "Right", "Center", "Force"])),
|
||||
p("leftMargin", "Paragraph: Left Margin", num(0.0), slider(-10000.0, 10000.0, -500.0, 500.0, 1)),
|
||||
p("rightMargin", "Paragraph: Right Margin", num(0.0), slider(-10000.0, 10000.0, -500.0, 500.0, 1)),
|
||||
p("baselineShift", "Paragraph: Baseline Shift", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)),
|
||||
p("visibleCharacters", "Advanced: Visible Characters", num(1024.0), slider(0.0, 1024.0, 0.0, 1024.0, 0)),
|
||||
p("pathOptions/shapeType", "Shape Type", Value::Enum(0), popup(&["Bezier", "Circle", "Loop", "Line"])),
|
||||
p("pathOptions/controlPoints/tangent1", "Tangent 1/Circle Point", pt(0.3, 0.3), ParamUi::Point),
|
||||
p("pathOptions/controlPoints/vertex1", "Vertex 1/Circle Center", pt(0.1, 0.6), ParamUi::Point),
|
||||
p("pathOptions/controlPoints/tangent2", "Tangent 2", pt(0.7, 0.3), ParamUi::Point),
|
||||
p("pathOptions/controlPoints/vertex2", "Vertex 2/Line Right", pt(0.9, 0.6), ParamUi::Point),
|
||||
p("pathOptions/customPath", "Custom Path", num(0.0), slider(0.0, 32.0, 0.0, 8.0, 0)),
|
||||
check("pathOptions/reversePath", "Reverse Path", false),
|
||||
p("fillAndStroke/displayOptions", "Options", Value::Enum(0), display()),
|
||||
p("fillAndStroke/fillColor", "Fill Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("fillAndStroke/strokeColor", "Stroke Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("fillAndStroke/strokeWidth", "Stroke Width", num(2.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("character/size", "Size", num(36.0), size()),
|
||||
p("character/tracking", "Tracking", num(0.0), slider(-100.0, 100.0, -20.0, 50.0, 1)),
|
||||
p("character/orientation/characterRotation", "Character Rotation", num(0.0), ang()),
|
||||
p("paragraph/alignment", "Alignment", Value::Enum(0), popup(&["Left", "Right", "Center", "Force"])),
|
||||
p("paragraph/leftMargin", "Left Margin", num(0.0), slider(-10000.0, 10000.0, -500.0, 500.0, 1)),
|
||||
p("paragraph/rightMargin", "Right Margin", num(0.0), slider(-10000.0, 10000.0, -500.0, 500.0, 1)),
|
||||
p("paragraph/baselineShift", "Baseline Shift", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)),
|
||||
p("advanced/visibleCharacters", "Visible Characters", num(1024.0), slider(0.0, 1024.0, 0.0, 1024.0, 0)),
|
||||
check("compositeOnOriginal", "Composite On Original", true),
|
||||
],
|
||||
path_text,
|
||||
|
|
@ -900,8 +925,8 @@ mod tests {
|
|||
#[test]
|
||||
fn numbers_draws_glyph_at_position() {
|
||||
let img = Image::new(80, 60);
|
||||
let a = run("ec.text.numbers", &[("value", num(8.0)), ("decimalPlaces", num(0.0))], img.clone(), 0.0);
|
||||
assert_eq!(a.img.data, run("ec.text.numbers", &[("value", num(8.0)), ("decimalPlaces", num(0.0))], img.clone(), 0.0).img.data);
|
||||
let a = run("ec.text.numbers", &[("format/value", num(8.0)), ("format/decimalPlaces", num(0.0))], img.clone(), 0.0);
|
||||
assert_eq!(a.img.data, run("ec.text.numbers", &[("format/value", num(8.0)), ("format/decimalPlaces", num(0.0))], img.clone(), 0.0).img.data);
|
||||
// Ink only around the centre.
|
||||
let mut inside = 0.0;
|
||||
let mut outside = 0.0;
|
||||
|
|
@ -916,7 +941,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
assert!(inside > 20.0 && outside < 0.5, "{inside} {outside}");
|
||||
let stroke = run("ec.text.numbers", &[("value", num(8.0)), ("displayOptions", Value::Enum(2))], img, 0.0);
|
||||
let stroke = run("ec.text.numbers", &[("format/value", num(8.0)), ("fillAndStroke/displayOptions", Value::Enum(2))], img, 0.0);
|
||||
assert!(stroke.img.data.iter().any(|p| p[3] > 0.5 && p[1] > 0.5 * p[3]), "white stroke visible");
|
||||
}
|
||||
|
||||
|
|
@ -948,7 +973,12 @@ mod tests {
|
|||
let img = Image::new(120, 60);
|
||||
let line = run(
|
||||
"ec.obsolete.pathtext",
|
||||
&[("shapeType", Value::Enum(3)), ("vertex1", Value::Vec2([5.0, 50.0])), ("vertex2", Value::Vec2([115.0, 50.0])), ("size", num(20.0))],
|
||||
&[
|
||||
("pathOptions/shapeType", Value::Enum(3)),
|
||||
("pathOptions/controlPoints/vertex1", Value::Vec2([5.0, 50.0])),
|
||||
("pathOptions/controlPoints/vertex2", Value::Vec2([115.0, 50.0])),
|
||||
("character/size", num(20.0)),
|
||||
],
|
||||
img.clone(),
|
||||
0.0,
|
||||
);
|
||||
|
|
@ -958,10 +988,10 @@ mod tests {
|
|||
assert!(below < 1.0);
|
||||
let masks = vec![MaskShape { name: "M".into(), points: vec![[10.0, 20.0], [110.0, 20.0]], closed: false, inverted: false }];
|
||||
let env = EffectEnv { masks: &masks, ..Default::default() };
|
||||
let m = run_env("ec.obsolete.pathtext", &[("customPath", num(1.0)), ("size", num(20.0))], img.clone(), 0.0, env);
|
||||
let m = run_env("ec.obsolete.pathtext", &[("pathOptions/customPath", num(1.0)), ("character/size", num(20.0))], img.clone(), 0.0, env);
|
||||
let near: f32 = (0..24).flat_map(|y| (0..120).map(move |x| (x, y))).map(|(x, y)| m.img.get(x, y)[3]).sum();
|
||||
assert!(near > 10.0 && (ink(&m.img) - near).abs() < 1.0);
|
||||
let circle = run("ec.obsolete.pathtext", &[("shapeType", Value::Enum(1)), ("text", Value::Str("CIRCLE TEXT".into()))], img, 0.0);
|
||||
let circle = run("ec.obsolete.pathtext", &[("pathOptions/shapeType", Value::Enum(1)), ("text", Value::Str("CIRCLE TEXT".into()))], img, 0.0);
|
||||
assert!(ink(&circle.img) > 10.0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Blend",
|
||||
"Max",
|
||||
"Full On",
|
||||
"Lightness of Result",
|
||||
"Max of Result",
|
||||
"Lightness Of Result",
|
||||
"Max Of Result",
|
||||
"Alpha Difference",
|
||||
"Alpha Difference Only",
|
||||
]),
|
||||
|
|
@ -75,9 +75,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Time Displacement",
|
||||
vec![
|
||||
p("displacementMapLayer", "Time Displacement Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("maxDisplacementTime", "Max Displacement Time [sec]", num(1.0), slider(-30.0, 30.0, -5.0, 5.0, 2)),
|
||||
p("timeResolution", "Time Resolution [fps]", num(60.0), slider(1.0, 999.0, 1.0, 120.0, 1)),
|
||||
p("stretchMap", "If Layer Sizes Differ: Stretch Map to Fit", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("maxDisplacementTime", "Max Displacement Time (sec)", num(1.0), slider(-30.0, 30.0, -5.0, 5.0, 2)),
|
||||
p("timeResolution", "Time Resolution (fps)", num(60.0), slider(1.0, 999.0, 1.0, 120.0, 1)),
|
||||
p("stretchMap", "Stretch Map To Fit", Value::Bool(true), ParamUi::Checkbox),
|
||||
],
|
||||
time_displacement,
|
||||
),
|
||||
|
|
@ -89,11 +89,12 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("adjustTimeBy", "Adjust Time By", Value::Enum(0), popup(&["Speed", "Source Frame"])),
|
||||
p("speed", "Speed", num(50.0), slider(-10000.0, 10000.0, 0.0, 200.0, 1)),
|
||||
p("sourceFrame", "Source Frame", num(0.0), slider(-100000.0, 100000.0, 0.0, 300.0, 1)),
|
||||
p("vectorDetail", "Vector Detail", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("enableMotionBlur", "Enable Motion Blur", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("shutterControl", "Shutter Control", Value::Enum(0), popup(&["Automatic", "Manual"])),
|
||||
p("shutterAngle", "Shutter Angle", num(180.0), slider(0.0, 720.0, 0.0, 360.0, 1)),
|
||||
p("shutterSamples", "Shutter Samples", num(5.0), slider(1.0, 64.0, 1.0, 32.0, 0)),
|
||||
p("tuning/vectorDetail", "Vector Detail", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("tuning/buildFromOneImage", "Build From One Image", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("motionBlur/enableMotionBlur", "Enable Motion Blur", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("motionBlur/shutterControl", "Shutter Control", Value::Enum(0), popup(&["Automatic", "Manual"])),
|
||||
p("motionBlur/shutterAngle", "Shutter Angle", num(180.0), slider(0.0, 720.0, 0.0, 360.0, 1)),
|
||||
p("motionBlur/shutterSamples", "Shutter Samples", num(5.0), slider(1.0, 64.0, 1.0, 32.0, 0)),
|
||||
],
|
||||
timewarp,
|
||||
),
|
||||
|
|
@ -436,17 +437,22 @@ fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
let speed = ctx.params.f("speed");
|
||||
let src = timewarp_source_time(ctx.time, by_frame, speed, ctx.params.f("sourceFrame"), fps);
|
||||
let method = ctx.params.e("method");
|
||||
// Build From One Image: interpolated frames come from the nearest source frame alone.
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
// Sample times (with weights) for one instant of source time.
|
||||
let instant = |s: f64| -> Vec<(f64, f32)> {
|
||||
let f = s * fps;
|
||||
let f0 = (f + 1e-6).floor();
|
||||
let frac = (f - f0) as f32;
|
||||
if one {
|
||||
return vec![((f + 1e-6).round() / fps, 1.0)];
|
||||
}
|
||||
if method == 0 || frac < 1e-4 { vec![(f0 / fps, 1.0)] } else { vec![(f0 / fps, 1.0 - frac), ((f0 + 1.0) / fps, frac)] }
|
||||
};
|
||||
let mut samples: Vec<(f64, f32)> = Vec::new();
|
||||
if ctx.params.b("enableMotionBlur") {
|
||||
let angle = if ctx.params.e("shutterControl") == 1 { ctx.params.f("shutterAngle") } else { 180.0 };
|
||||
let n = ctx.params.f("shutterSamples").round().clamp(1.0, 64.0) as usize;
|
||||
if ctx.params.b("motionBlur/enableMotionBlur") {
|
||||
let angle = if ctx.params.e("motionBlur/shutterControl") == 1 { ctx.params.f("motionBlur/shutterAngle") } else { 180.0 };
|
||||
let n = ctx.params.f("motionBlur/shutterSamples").round().clamp(1.0, 64.0) as usize;
|
||||
let rate = if by_frame { 1.0 } else { speed / 100.0 };
|
||||
let span = angle / 360.0 * rate / fps;
|
||||
for i in 0..n {
|
||||
|
|
@ -566,6 +572,12 @@ mod tests {
|
|||
// Frame Mix between 0.5 and 0.6 at source 0.55.
|
||||
let out = run("ec.time.timewarp", &[("method", Value::Enum(1))], 1.1, &h);
|
||||
assert!((out.img.data[0][0] - 0.55).abs() < 1e-4, "{}", out.img.data[0][0]);
|
||||
// Build From One Image: the nearest source frame alone (0.57 → 0.6).
|
||||
let out = run("ec.time.timewarp", &[("method", Value::Enum(1)), ("tuning/buildFromOneImage", Value::Bool(true))], 1.14, &h);
|
||||
assert!((out.img.data[0][0] - 0.6).abs() < 1e-4, "{}", out.img.data[0][0]);
|
||||
// Motion blur (in its twirl-down) averages over the shutter.
|
||||
let out = run("ec.time.timewarp", &[("method", Value::Enum(1)), ("motionBlur/enableMotionBlur", Value::Bool(true))], 1.0, &h);
|
||||
assert!(out.img.data[0][0] > 0.5 + 1e-3, "{}", out.img.data[0][0]);
|
||||
// Source Frame mode: frame 3 at 10 fps.
|
||||
let out = run("ec.time.timewarp", &[("method", Value::Enum(0)), ("adjustTimeBy", Value::Enum(1)), ("sourceFrame", num(3.0))], 2.0, &h);
|
||||
assert!((out.img.data[0][0] - 0.3).abs() < 1e-5);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Transition effects (batch 2): Block Dissolve, Gradient Wipe (self gradient), Iris Wipe, Card
|
||||
//! Transition effects (batch 2): Block Dissolve, Gradient Wipe (gradient layer or self), Iris Wipe, Card
|
||||
//! Wipe (2D cards), CC Grid Wipe, CC Radial ScaleWipe, CC Scale Wipe and CC Light Wipe.
|
||||
//!
|
||||
//! Every transition is the identity at 0 % completion. Angles are clockwise from "up".
|
||||
|
|
@ -27,6 +27,24 @@ fn scale_px(px: &mut Px, k: f32) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Resample `other` into `b`'s pixel grid with placement `mode` (Gradient Wipe's Gradient
|
||||
/// Placement, Texturize's Texture Placement): 0 tile from the top-left, 1 centre once,
|
||||
/// 2 stretch to fit.
|
||||
pub(crate) fn place_layer(ctx: &EffectCtx, b: &Buf, other: &crate::LayerPixels, mode: u32) -> effectcraft_raster::Image {
|
||||
let (ls, os) = (ctx.layer_size, other.size);
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
crate::util::gen_image(b.img.width, b.img.height, |x, y| {
|
||||
let (px, py) = ((x as f64 + 0.5 - b.offset[0]) * inv, (y as f64 + 0.5 - b.offset[1]) * inv);
|
||||
let (qx, qy) = match mode {
|
||||
0 if os[0] >= 1.0 && os[1] >= 1.0 => (px.rem_euclid(os[0]), py.rem_euclid(os[1])),
|
||||
1 => (px + (os[0] - ls[0]) * 0.5, py + (os[1] - ls[1]) * 0.5),
|
||||
_ if ls[0] > 0.0 && ls[1] > 0.0 => (px * os[0] / ls[0], py * os[1] / ls[1]),
|
||||
_ => (px, py),
|
||||
};
|
||||
other.buf.img.sample_bilinear(qx * other.buf.scale + other.buf.offset[0], qy * other.buf.scale + other.buf.offset[1])
|
||||
})
|
||||
}
|
||||
|
||||
/// Largest distance from `c` to a corner of the buffer.
|
||||
fn max_corner_dist(b: &Buf, c: (f64, f64), metric: impl Fn(f64, f64) -> f64) -> f64 {
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
|
|
@ -67,8 +85,13 @@ fn gradient_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
let soft = (ctx.params.f("softness") / 100.0) as f32;
|
||||
let invert = ctx.params.b("invert");
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let (c, _) = unpremul(*px);
|
||||
// The gradient layer (the layer itself when none is chosen), placed over this one.
|
||||
let grad = match ctx.layer_param("gradientLayer", false) {
|
||||
Some(o) => place_layer(ctx, &b, &o, ctx.params.e("gradientPlacement")),
|
||||
None => b.img.clone(),
|
||||
};
|
||||
b.img.data.par_iter_mut().zip(grad.data.par_iter()).for_each(|(px, g)| {
|
||||
let (c, _) = unpremul(*g);
|
||||
let mut t = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
if invert {
|
||||
t = 1.0 - t;
|
||||
|
|
@ -147,15 +170,23 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
let tw = (ctx.params.f("transitionWidth") / 100.0).clamp(0.01, 1.0);
|
||||
let rows = ctx.params.f("rows").round().clamp(1.0, 1000.0) as i64;
|
||||
let cols = ctx.params.f("columns").round().clamp(1.0, 1000.0) as i64;
|
||||
// Rows & Columns: Independent, or Columns Follows Rows.
|
||||
let cols = if ctx.params.e("rowsAndColumns") == 1 { rows } else { ctx.params.f("columns").round().clamp(1.0, 1000.0) as i64 };
|
||||
let card_scale = ctx.params.f("cardScale").max(0.01);
|
||||
let axis = ctx.params.e("flipAxis");
|
||||
let order = ctx.params.e("flipOrder");
|
||||
let back_self = ctx.params.e("backLayer") == 1;
|
||||
let jitter = ctx.params.f("timingRandomness").clamp(0.0, 1.0);
|
||||
let seed = ctx.params.f("randomSeed") as u32 ^ 0xca7d;
|
||||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||||
let (cw, ch) = (w / cols as f64, h / rows as f64);
|
||||
let nx = |i: i64| if cols > 1 { i as f64 / (cols - 1) as f64 } else { 0.0 };
|
||||
let ny = |j: i64| if rows > 1 { j as f64 / (rows - 1) as f64 } else { 0.0 };
|
||||
// Flip Order "Gradient": cards flip first where the gradient layer (default: this layer) is dark.
|
||||
let grad = (order == 8).then(|| match ctx.layer_param("gradientLayer", false) {
|
||||
Some(o) => place_layer(ctx, &b, &o, 2),
|
||||
None => b.img.clone(),
|
||||
});
|
||||
let src = b.img.clone();
|
||||
b.img = remap(&src, false, |x, y| {
|
||||
let (lx, ly) = (x - x0, y - y0);
|
||||
|
|
@ -164,18 +195,33 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
let i = ((lx / cw).floor() as i64).min(cols - 1);
|
||||
let j = ((ly / ch).floor() as i64).min(rows - 1);
|
||||
let o = match order {
|
||||
let (ccx, ccy) = (x0 + (i as f64 + 0.5) * cw, y0 + (j as f64 + 0.5) * ch);
|
||||
let mut o = match order {
|
||||
1 => 1.0 - nx(i),
|
||||
2 => ny(j),
|
||||
3 => 1.0 - ny(j),
|
||||
4 => (nx(i) + ny(j)) * 0.5,
|
||||
5 => 1.0 - (nx(i) + ny(j)) * 0.5,
|
||||
6 => hash1(i as u32, j as u32, seed) as f64,
|
||||
5 => (1.0 - nx(i) + ny(j)) * 0.5,
|
||||
6 => (nx(i) + 1.0 - ny(j)) * 0.5,
|
||||
7 => 1.0 - (nx(i) + ny(j)) * 0.5,
|
||||
8 => {
|
||||
let g = grad.as_ref().map(|g| g.get_clamped(ccx as i64, ccy as i64)).unwrap_or([0.0; 4]);
|
||||
let (c, _) = unpremul(g);
|
||||
luminance(c[0], c[1], c[2]).clamp(0.0, 1.0) as f64
|
||||
}
|
||||
_ => nx(i),
|
||||
};
|
||||
if jitter > 0.0 {
|
||||
o += (hash1(i as u32, j as u32, seed ^ 0x7177) as f64 - o) * jitter;
|
||||
}
|
||||
let p = ((done - o * (1.0 - tw)) / tw).clamp(0.0, 1.0);
|
||||
// Card Scale: each card is scaled about its centre (gaps below 1, cropped above 1).
|
||||
let (mut dx, mut dy) = ((x - ccx) / card_scale, (y - ccy) / card_scale);
|
||||
if dx.abs() > cw * 0.5 + 1e-9 || dy.abs() > ch * 0.5 + 1e-9 {
|
||||
return None;
|
||||
}
|
||||
if p <= 0.0 {
|
||||
return Some((x, y));
|
||||
return Some((ccx + dx, ccy + dy));
|
||||
}
|
||||
let cos = (p * std::f64::consts::PI).cos();
|
||||
if cos.abs() < 1e-4 || (cos < 0.0 && !back_self) {
|
||||
|
|
@ -186,8 +232,6 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
2 => hash1(i as u32, j as u32, seed ^ 0x55) < 0.5,
|
||||
_ => true,
|
||||
};
|
||||
let (ccx, ccy) = (x0 + (i as f64 + 0.5) * cw, y0 + (j as f64 + 0.5) * ch);
|
||||
let (mut dx, mut dy) = (x - ccx, y - ccy);
|
||||
if around_x {
|
||||
dy /= cos;
|
||||
if dy.abs() > ch * 0.5 {
|
||||
|
|
@ -357,6 +401,8 @@ fn light_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let pt = |x, y| Value::Vec2([x, y]);
|
||||
let done = || p("completion", "Transition Completion", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0));
|
||||
// The CC transitions call it plain "Completion".
|
||||
let cc_done = || p("completion", "Completion", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1));
|
||||
vec![
|
||||
spec(
|
||||
"ec.transition.blockdissolve",
|
||||
|
|
@ -375,6 +421,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
vec![
|
||||
done(),
|
||||
p("softness", "Transition Softness", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("gradientLayer", "Gradient Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("gradientPlacement", "Gradient Placement", Value::Enum(2), popup(&["Tile Gradient", "Center Gradient", "Stretch Gradient to Fit"])),
|
||||
p("invert", "Invert Gradient", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
gradient_wipe,
|
||||
|
|
@ -400,8 +448,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
done(),
|
||||
p("transitionWidth", "Transition Width", num(50.0), slider(1.0, 100.0, 1.0, 100.0, 0)),
|
||||
p("backLayer", "Back Layer", Value::Enum(0), popup(&["None", "Self"])),
|
||||
p("rowsAndColumns", "Rows & Columns", Value::Enum(0), popup(&["Independent", "Columns Follows Rows"])),
|
||||
p("rows", "Rows", num(8.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("columns", "Columns", num(8.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("cardScale", "Card Scale", num(1.0), slider(0.01, 10.0, 0.01, 2.0, 2)),
|
||||
p("flipAxis", "Flip Axis", Value::Enum(0), popup(&["X", "Y", "Random"])),
|
||||
p(
|
||||
"flipOrder",
|
||||
|
|
@ -413,10 +463,14 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"Top to Bottom",
|
||||
"Bottom to Top",
|
||||
"Top Left to Bottom Right",
|
||||
"Top Right to Bottom Left",
|
||||
"Bottom Left to Top Right",
|
||||
"Bottom Right to Top Left",
|
||||
"Random",
|
||||
"Gradient",
|
||||
]),
|
||||
),
|
||||
p("gradientLayer", "Gradient Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("timingRandomness", "Timing Randomness", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10000.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
card_wipe,
|
||||
|
|
@ -425,7 +479,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.transition.ccgridwipe",
|
||||
"CC Grid Wipe",
|
||||
vec![
|
||||
done(),
|
||||
cc_done(),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||||
p("border", "Border", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
|
|
@ -438,7 +492,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
spec(
|
||||
"ec.transition.ccradialscalewipe",
|
||||
"CC Radial ScaleWipe",
|
||||
vec![done(), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("reverse", "Reverse Transition", Value::Bool(false), ParamUi::Checkbox)],
|
||||
vec![cc_done(), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("reverse", "Reverse Transition", Value::Bool(false), ParamUi::Checkbox)],
|
||||
radial_scale_wipe,
|
||||
),
|
||||
spec(
|
||||
|
|
@ -455,7 +509,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"ec.transition.cclightwipe",
|
||||
"CC Light Wipe",
|
||||
vec![
|
||||
done(),
|
||||
cc_done(),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("intensity", "Intensity", num(100.0), slider(0.0, 1000.0, 0.0, 200.0, 1)),
|
||||
p("shape", "Shape", Value::Enum(1), popup(&["Doors", "Round", "Square"])),
|
||||
|
|
@ -608,6 +662,83 @@ mod tests {
|
|||
assert!((out.get(38, 1)[0] - centre).abs() < 0.03, "{}", out.get(38, 1)[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_wipe_both_opens_symmetrically() {
|
||||
let img = Image::filled(41, 41, [1.0, 1.0, 1.0, 1.0]);
|
||||
let both = run("ec.transition.radialwipe", &img, &[("completion", num(50.0)), ("wipe", Value::Enum(2))]);
|
||||
// Start angle 0 = up: half done in both directions clears the whole top half…
|
||||
assert_eq!(both.get(5, 5)[3], 0.0);
|
||||
assert_eq!(both.get(35, 5)[3], 0.0);
|
||||
// …and leaves the bottom half.
|
||||
assert_eq!(both.get(5, 35)[3], 1.0);
|
||||
assert_eq!(both.get(35, 35)[3], 1.0);
|
||||
let cw = run("ec.transition.radialwipe", &img, &[("completion", num(50.0))]);
|
||||
assert_eq!(cw.get(35, 35)[3], 0.0, "clockwise covers the right half");
|
||||
assert_eq!(cw.get(5, 5)[3], 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_wipe_columns_follow_rows_scale_and_gradient_order() {
|
||||
let img = Image::filled(80, 40, [1.0, 0.0, 0.0, 1.0]);
|
||||
// Card Scale 0.5 leaves gaps between untouched cards.
|
||||
let out = run("ec.transition.cardwipe", &img, &[("completion", num(1.0)), ("rows", num(1.0)), ("columns", num(4.0)), ("cardScale", num(0.5))]);
|
||||
assert_eq!(out.get(1, 20)[3], 0.0);
|
||||
assert_eq!(out.get(70, 20)[3], 1.0);
|
||||
// Columns Follows Rows: 2 rows → 2 columns, so at 50 % with a narrow transition the left
|
||||
// half (first column) has flipped away while the right half remains.
|
||||
let out = run(
|
||||
"ec.transition.cardwipe",
|
||||
&img,
|
||||
&[("completion", num(50.0)), ("rows", num(2.0)), ("columns", num(40.0)), ("rowsAndColumns", Value::Enum(1)), ("transitionWidth", num(10.0))],
|
||||
);
|
||||
assert_eq!(out.get(30, 10)[3], 0.0);
|
||||
assert_eq!(out.get(50, 10)[3], 1.0);
|
||||
// Gradient order (self as gradient): dark cards flip first.
|
||||
let ramp = ramp(80, 4);
|
||||
let out = run(
|
||||
"ec.transition.cardwipe",
|
||||
&ramp,
|
||||
&[("completion", num(50.0)), ("rows", num(1.0)), ("columns", num(8.0)), ("flipOrder", Value::Enum(8)), ("transitionWidth", num(10.0))],
|
||||
);
|
||||
assert_eq!(out.get(2, 2)[3], 0.0);
|
||||
assert_eq!(out.get(77, 2)[3], 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_wipe_uses_the_gradient_layer_with_placement() {
|
||||
struct Host(Image);
|
||||
impl crate::EffectHost for Host {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
Some(crate::LayerPixels { buf: Buf { img: self.0.clone(), offset: [0.0; 2], scale: 1.0 }, size: [self.0.width as f64, self.0.height as f64] })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
// A flat layer wiped by a 10-px left-to-right ramp layer.
|
||||
let img = Image::filled(40, 4, [1.0, 1.0, 1.0, 1.0]);
|
||||
let host = Host(ramp(10, 4));
|
||||
let s = find("ec.transition.gradientwipe").unwrap();
|
||||
let wipe = |placement: u32| {
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
params.values.insert("completion".into(), num(50.0));
|
||||
params.values.insert("gradientLayer".into(), Value::Layer(Some(1)));
|
||||
params.values.insert("gradientPlacement".into(), Value::Enum(placement));
|
||||
let env = crate::EffectEnv { host: Some(&host), ..Default::default() };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [40.0, 4.0], seed: 7, adjustment: false, env };
|
||||
(s.render)(&ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
};
|
||||
// Stretched: one ramp across the layer.
|
||||
let st = wipe(2);
|
||||
assert_eq!(st.get(5, 1)[3], 0.0);
|
||||
assert_eq!(st.get(35, 1)[3], 1.0);
|
||||
// Tiled: the ramp repeats every 10 px.
|
||||
let tl = wipe(0);
|
||||
assert_eq!(tl.get(11, 1)[3], 0.0);
|
||||
assert_eq!(tl.get(18, 1)[3], 1.0);
|
||||
assert_eq!(tl.get(31, 1)[3], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_wipe_shrinks_tiles_from_center() {
|
||||
let img = Image::filled(100, 100, [1.0, 1.0, 1.0, 1.0]);
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ fn mtv(m: &[[f64; 3]; 3], v: V3) -> V3 {
|
|||
|
||||
// ---------------------------------------------------------------- projections (VR Converter)
|
||||
|
||||
pub const PROJECTIONS: [&str; 6] = ["Equirectangular", "Cube-map 4:3", "Cube-map 3:2", "Cube-map 6:1", "Sphere Map", "2D Rectilinear"];
|
||||
pub const PROJECTIONS: [&str; 6] = ["Equirectangular 2:1", "Cube-map 4:3", "Cube-map Pano2VR 3:2", "Cube-map GearVR 6:1", "Sphere-map", "2D Source"];
|
||||
|
||||
/// Cube faces: (forward, right, up).
|
||||
const FACES: [(V3, V3, V3); 6] = [
|
||||
|
|
@ -465,6 +465,12 @@ fn vr_denoise(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
return b;
|
||||
}
|
||||
let r = ((1.0 + level * 4.0) * b.scale).max(1.0) as usize;
|
||||
if ctx.params.e("noiseType") == 1 {
|
||||
// Salt-and-pepper speckles: a median removes isolated outliers outright.
|
||||
let mr = ((level * 3.0 * b.scale).round() as usize).max(1);
|
||||
b.img = per_eye(&b.img, layout(ctx), |e, _| crate::noise::median_image(e, mr));
|
||||
return b;
|
||||
}
|
||||
let eps = (level * level * 0.02) as f32;
|
||||
let detail = (ctx.params.f("detail") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
b.img = per_eye(&b.img, layout(ctx), |e, _| {
|
||||
|
|
@ -558,7 +564,11 @@ fn vr_rotate(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if t == 0.0 && pn == 0.0 && r == 0.0 {
|
||||
return b;
|
||||
}
|
||||
let m = rot_params(ctx);
|
||||
let mut m = rot_params(ctx);
|
||||
if ctx.params.b("invertRotation") {
|
||||
// The inverse of a rotation is its transpose.
|
||||
m = [[m[0][0], m[1][0], m[2][0]], [m[0][1], m[1][1], m[2][1]], [m[0][2], m[1][2], m[2][2]]];
|
||||
}
|
||||
let eq = Proj { kind: 0, fov: 360.0 };
|
||||
b.img = per_eye(&b.img, layout(ctx), |e, _| convert(e, eq, eq, e.width, e.height, Some(&m)));
|
||||
b
|
||||
|
|
@ -586,8 +596,8 @@ fn vr_sphere_to_plane(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
|
||||
fn vr_plane_to_sphere(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
// The layer is a flat picture placed on the sphere: horizontal FOV = 90° × scale.
|
||||
let fov = (90.0 * ctx.params.f("scale") / 100.0).clamp(1.0, 179.0);
|
||||
// The layer is a flat picture placed on the sphere, Scale degrees wide.
|
||||
let fov = ctx.params.f("scale").clamp(1.0, 179.0);
|
||||
let m = rot_params(ctx);
|
||||
let feather = (ctx.params.f("feather") / 100.0).clamp(0.0, 1.0);
|
||||
let flat = Proj { kind: 5, fov };
|
||||
|
|
@ -630,6 +640,8 @@ fn vr_chromatic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
let center = mv(&rotation(ctx.params.f("centerTilt"), ctx.params.f("centerPan"), 0.0), [0.0, 0.0, 1.0]);
|
||||
let falloff = (ctx.params.f("falloff") / 100.0).clamp(0.0, 1.0);
|
||||
// Falloff Invert keeps the aberration near the point of interest instead of away from it.
|
||||
let invert = ctx.params.b("falloffInvert");
|
||||
b.img = per_eye(&b.img, layout(ctx), |e, _| {
|
||||
let (w, h) = (e.width as f64, e.height as f64);
|
||||
let mut o = Image::new(e.width, e.height);
|
||||
|
|
@ -641,6 +653,7 @@ fn vr_chromatic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let ax = [center[1] * d[2] - center[2] * d[1], center[2] * d[0] - center[0] * d[2], center[0] * d[1] - center[1] * d[0]];
|
||||
let al = dot(ax, ax).sqrt();
|
||||
let weight = (th / PI).powf(1.0 + falloff * 3.0);
|
||||
let weight = if invert { 1.0 - weight } else { weight };
|
||||
let mut out = [0.0f32; 4];
|
||||
for c in 0..3 {
|
||||
let s = if al < 1e-9 {
|
||||
|
|
@ -759,7 +772,10 @@ fn vr_fractal(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let invert = pr.b("invert");
|
||||
let contrast = pr.f("contrast") as f32 / 100.0;
|
||||
let brightness = pr.f("brightness") as f32 / 100.0;
|
||||
let scale = (pr.f("scale") / 100.0).max(0.01) as f32;
|
||||
let scale = (pr.f("transform/scale") / 100.0).max(0.01) as f32;
|
||||
let rot = rotation(pr.f("transform/tilt"), pr.f("transform/pan"), pr.f("transform/roll"));
|
||||
let infl = (pr.get("subSettings/subInfluence").map(Value::as_f64).unwrap_or(50.0) / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let sub = (pr.get("subSettings/subScaling").map(Value::as_f64).unwrap_or(50.0) / 100.0).clamp(0.1, 1.0) as f32;
|
||||
let octaves = pr.f("complexity").clamp(1.0, 20.0);
|
||||
let evo = (pr.f("evolution") / 360.0) as f32;
|
||||
let opacity = (pr.f("opacity") / 100.0) as f32;
|
||||
|
|
@ -771,8 +787,9 @@ fn vr_fractal(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let mut g = Image::new(e.width, e.height);
|
||||
g.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let d = equi_dir(x as f64 + 0.5, y as f64 + 0.5, w, h);
|
||||
let d = mv(&rot, equi_dir(x as f64 + 0.5, y as f64 + 0.5, w, h));
|
||||
let mut v = 0.0f32;
|
||||
let mut best = 0.0f32;
|
||||
let mut amp = 1.0f32;
|
||||
let mut freq = base;
|
||||
let mut norm = 0.0f32;
|
||||
|
|
@ -782,16 +799,19 @@ fn vr_fractal(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let q = [d[0] as f32 * freq + evo * 0.37, d[1] as f32 * freq + evo * 0.71, d[2] as f32 * freq + evo * 0.59];
|
||||
let n = value_noise(q[0] + o as f32 * 17.3, q[1], q[2], seed.wrapping_add(o as u32));
|
||||
let n = match kind {
|
||||
1 => (n * 2.0 - 1.0).abs(),
|
||||
2 => 1.0 - (n * 2.0 - 1.0).abs(),
|
||||
1 => 1.0 - (n * 2.0 - 1.0).abs(),
|
||||
// Strings: thin bright lines along the noise's zero crossings.
|
||||
3 => 1.0 - ((n * 2.0 - 1.0).abs() / 0.12).clamp(0.0, 1.0).powi(2),
|
||||
_ => n,
|
||||
};
|
||||
best = best.max(n * amp);
|
||||
v += n * amp * wgt;
|
||||
norm += amp * wgt;
|
||||
amp *= 0.5;
|
||||
freq *= 2.0;
|
||||
amp *= infl;
|
||||
freq /= sub;
|
||||
}
|
||||
let mut v = v / norm.max(1e-6);
|
||||
// Max keeps the strongest layer rather than the weighted sum.
|
||||
let mut v = if kind == 2 { best } else { v / norm.max(1e-6) };
|
||||
v = (v - 0.5) * contrast + 0.5 + brightness;
|
||||
if invert {
|
||||
v = 1.0 - v;
|
||||
|
|
@ -871,7 +891,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
grad.push(p(ids[2], ids[3], Value::Vec2(pt), ParamUi::Point));
|
||||
grad.push(p(ids[4], ids[5], col(c[0], c[1], c[2]), ParamUi::Color));
|
||||
}
|
||||
grad.push(p("blend", "Blend", num(2.0), slider(0.1, 10.0, 0.5, 6.0, 2)));
|
||||
grad.push(p("blend", "Gradient Power", num(2.0), slider(0.1, 10.0, 0.5, 6.0, 2)));
|
||||
let (v, u) = pct(100.0);
|
||||
grad.push(p("opacity", "Opacity", v, u));
|
||||
grad.push(p("blendingMode", "Blending Mode", Value::Enum(0), popup(&BLEND_OPTS)));
|
||||
|
|
@ -887,7 +907,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("aberrationRed", "Aberration (Red)", num(10.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("aberrationGreen", "Aberration (Green)", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("aberrationBlue", "Aberration (Blue)", num(-10.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("falloff", "Falloff", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("falloff", "Falloff Distance", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("falloffInvert", "Falloff Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
vr_chromatic,
|
||||
),
|
||||
|
|
@ -912,6 +933,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"VR De-Noise",
|
||||
vec![
|
||||
layout_param(),
|
||||
p("noiseType", "Noise Type", Value::Enum(0), popup(&["Random Valued", "Salt-and-Pepper"])),
|
||||
p("noiseLevel", "Noise Level", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("detail", "Detail", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
|
|
@ -937,13 +959,18 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"VR Fractal Noise",
|
||||
vec![
|
||||
layout_param(),
|
||||
p("fractalType", "Fractal Type", Value::Enum(0), popup(&["Basic", "Turbulent Smooth", "Turbulent Sharp"])),
|
||||
p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("fractalType", "Fractal Type", Value::Enum(0), popup(&["Basic", "Turbulent Sharp", "Max", "Strings"])),
|
||||
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, 10.0, 600.0, 1)),
|
||||
p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("complexity", "Complexity", num(6.0), slider(1.0, 20.0, 1.0, 20.0, 1)),
|
||||
ang("evolution", "Evolution"),
|
||||
p("transform/scale", "Scale", num(100.0), slider(1.0, 10000.0, 10.0, 600.0, 1)),
|
||||
ang("transform/tilt", "Tilt (X Axis)"),
|
||||
ang("transform/pan", "Pan (Y Axis)"),
|
||||
ang("transform/roll", "Roll (Z Axis)"),
|
||||
p("subSettings/subInfluence", "Sub Influence", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("subSettings/subScaling", "Sub Scaling", num(50.0), slider(10.0, 100.0, 10.0, 100.0, 1)),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10000.0, 0.0, 1000.0, 0)),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("blendingMode", "Blending Mode", Value::Enum(0), popup(&BLEND_OPTS)),
|
||||
|
|
@ -955,7 +982,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"VR Glow",
|
||||
vec![
|
||||
layout_param(),
|
||||
p("luminanceThreshold", "Luminance Threshold", num(60.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("luminanceThreshold", "Luma Threshold", num(60.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("glowRadius", "Glow Radius", num(20.0), slider(0.0, 1000.0, 0.0, 200.0, 1)),
|
||||
p("glowBrightness", "Glow Brightness", num(100.0), slider(0.0, 1000.0, 0.0, 400.0, 1)),
|
||||
p("glowSaturation", "Glow Saturation", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
|
|
@ -969,29 +996,35 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
"VR Plane to Sphere",
|
||||
vec![
|
||||
layout_param(),
|
||||
p("scale", "Scale", num(100.0), slider(1.0, 198.0, 1.0, 198.0, 1)),
|
||||
p("scale", "Scale (Degrees)", num(90.0), slider(1.0, 179.0, 1.0, 179.0, 1)),
|
||||
p("feather", "Feather", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
ang("tilt", "Tilt (X Axis)"),
|
||||
ang("pan", "Pan (Y Axis)"),
|
||||
ang("roll", "Roll (Z Axis)"),
|
||||
p("feather", "Feather", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
vr_plane_to_sphere,
|
||||
),
|
||||
spec(
|
||||
"ec.vr.rotatesphere",
|
||||
"VR Rotate Sphere",
|
||||
vec![layout_param(), ang("tilt", "Tilt (X Axis)"), ang("pan", "Pan (Y Axis)"), ang("roll", "Roll (Z Axis)")],
|
||||
vec![
|
||||
layout_param(),
|
||||
p("invertRotation", "Invert Rotation", Value::Bool(false), ParamUi::Checkbox),
|
||||
ang("tilt", "Tilt (X Axis)"),
|
||||
ang("pan", "Pan (Y Axis)"),
|
||||
ang("roll", "Roll (Z Axis)"),
|
||||
],
|
||||
vr_rotate,
|
||||
),
|
||||
spec(
|
||||
"ec.vr.sharpen",
|
||||
"VR Sharpen",
|
||||
vec![layout_param(), p("sharpenAmount", "Sharpen Amount", num(20.0), slider(0.0, 500.0, 0.0, 100.0, 1))],
|
||||
vec![layout_param(), p("sharpenAmount", "Sharpen Amount", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 1))],
|
||||
vr_sharpen,
|
||||
),
|
||||
spec(
|
||||
"ec.vr.spheretoplane",
|
||||
"VR Sphere To Plane",
|
||||
"VR Sphere to Plane",
|
||||
vec![
|
||||
layout_param(),
|
||||
p("horizontalFov", "Horizontal FOV", num(90.0), slider(1.0, 179.0, 10.0, 170.0, 1)),
|
||||
|
|
@ -1146,6 +1179,29 @@ mod tests {
|
|||
assert!(s.img.get(32, 16)[3] > 0.99 && s.img.get(1, 16)[3] < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_invert_undoes_rotation_and_options_take_effect() {
|
||||
let img = sphere_img(64, 32);
|
||||
let rot = [("pan", num(40.0)), ("tilt", num(15.0))];
|
||||
let a = run_fx("ec.vr.rotatesphere", &rot, img.clone(), 0.0, EffectEnv::default());
|
||||
let mut inv = rot.to_vec();
|
||||
inv.push(("invertRotation", Value::Bool(true)));
|
||||
let back = run_fx("ec.vr.rotatesphere", &inv, a.img.clone(), 0.0, EffectEnv::default());
|
||||
let c = back.img.get(32, 16);
|
||||
let o = img.get(32, 16);
|
||||
assert!((0..3).all(|k| (c[k] - o[k]).abs() < 0.08), "{c:?} {o:?}");
|
||||
// Falloff Invert, salt-and-pepper De-Noise and the VR fractal types all change the output.
|
||||
let f0 = run_fx("ec.vr.chromaticaberrations", &[], img.clone(), 0.0, EffectEnv::default());
|
||||
let f1 = run_fx("ec.vr.chromaticaberrations", &[("falloffInvert", Value::Bool(true))], img.clone(), 0.0, EffectEnv::default());
|
||||
assert_ne!(f0.img.data, f1.img.data);
|
||||
let d0 = run_fx("ec.vr.denoise", &[], img.clone(), 0.0, EffectEnv::default());
|
||||
let d1 = run_fx("ec.vr.denoise", &[("noiseType", Value::Enum(1))], img.clone(), 0.0, EffectEnv::default());
|
||||
assert_ne!(d0.img.data, d1.img.data);
|
||||
let n: Vec<_> =
|
||||
(0..4).map(|t| run_fx("ec.vr.fractalnoise", &[("fractalType", Value::Enum(t))], img.clone(), 0.0, EffectEnv::default()).img.data).collect();
|
||||
assert!(n[0] != n[1] && n[1] != n[2] && n[2] != n[3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generators_and_filters_are_deterministic_and_seamless() {
|
||||
let img = sphere_img(64, 32);
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@
|
|||
//! valid on every frame, **Stabilize, Crop, Auto-scale** scales that region back up to the
|
||||
//! frame (up to Maximum Scale, minus the Action-safe Margin), Additional Scale scales on top;
|
||||
//! - **Stabilize, Synthesize Edges** fills the uncovered borders from neighbouring frames
|
||||
//! (within Synthesize Input Range), mapped through the analysed frame-to-frame motion and read
|
||||
//! with [`crate::EffectHost::self_at`]; Synthesize Edge Feather blends the seam and Synthesize
|
||||
//! (within Synthesis Input Range), mapped through the analysed frame-to-frame motion and read
|
||||
//! with [`crate::EffectHost::self_at`]; Synthesis Edge Feather blends the seam and Synthesis
|
||||
//! Edge Cropping ignores bad edge pixels of the input frames;
|
||||
//! - **Show Track Points** draws the analysed background features.
|
||||
//!
|
||||
|
|
@ -48,7 +48,7 @@ pub const GROUPS: &[(&str, &str)] = &[
|
|||
("borders", "Borders"),
|
||||
("autoScale", "Auto-scale"),
|
||||
("advanced", "Advanced"),
|
||||
("edgeCropping", "Synthesize Edge Cropping"),
|
||||
("edgeCropping", "Synthesis Edge Cropping"),
|
||||
];
|
||||
|
||||
pub const RESULTS: [&str; 2] = ["Smooth Motion", "No Motion"];
|
||||
|
|
@ -72,8 +72,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("advanced/detailedAnalysis", "Detailed Analysis", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("advanced/rollingShutterRipple", "Rolling Shutter Ripple", Value::Enum(0), popup(&["Automatic Reduction", "Enhanced Reduction"])),
|
||||
p("advanced/cropLessSmoothMore", "Crop Less <-> Smooth More", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("advanced/synthesizeInputRange", "Synthesize Input Range (seconds)", num(1.0), slider(0.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("advanced/synthesizeEdgeFeather", "Synthesize Edge Feather", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("advanced/synthesizeInputRange", "Synthesis Input Range (seconds)", num(1.0), slider(0.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("advanced/synthesizeEdgeFeather", "Synthesis Edge Feather", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
|
||||
p("advanced/edgeCropping/left", "Left", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 0)),
|
||||
p("advanced/edgeCropping/top", "Top", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 0)),
|
||||
p("advanced/edgeCropping/right", "Right", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 0)),
|
||||
|
|
|
|||
|
|
@ -146,7 +146,15 @@ fn card(p: &mut Project, comp: &Comp, folder: ItemId, name: &str, a: &str, b: &s
|
|||
&mut l,
|
||||
"ec.generate.grid",
|
||||
[w, h],
|
||||
&[("width", Value::Scalar(w)), ("height", Value::Scalar(h)), ("border", Value::Scalar(10.0)), ("opacity", Value::Scalar(35.0))],
|
||||
&[
|
||||
("sizeFrom", Value::Enum(2)),
|
||||
("width", Value::Scalar(w)),
|
||||
("height", Value::Scalar(h)),
|
||||
("border", Value::Scalar(10.0)),
|
||||
("opacity", Value::Scalar(35.0)),
|
||||
// Normal: the grid over the gradient.
|
||||
("blendingMode", Value::Enum(1)),
|
||||
],
|
||||
);
|
||||
l.switches.three_d = true;
|
||||
set(&mut l, "transform/position", Value::Vec3(pos));
|
||||
|
|
@ -174,6 +182,7 @@ fn showcase_3d(p: &mut Project, solids: ItemId) -> Comp {
|
|||
("border", Value::Scalar(3.0)),
|
||||
("color", Value::Color(hex("#6F7BB8"))),
|
||||
("opacity", Value::Scalar(45.0)),
|
||||
("blendingMode", Value::Enum(1)),
|
||||
],
|
||||
);
|
||||
floor.switches.three_d = true;
|
||||
|
|
|
|||
|
|
@ -635,6 +635,7 @@ impl Session {
|
|||
self.roto_job = None;
|
||||
self.roto_pending.clear();
|
||||
p.fix_next_id();
|
||||
upgrade_effects(&mut p);
|
||||
self.project = Arc::new(p);
|
||||
self.history = History::default();
|
||||
self.state = EditorState { snapping: true, view_layout: 1, ..Default::default() };
|
||||
|
|
@ -648,6 +649,35 @@ impl Session {
|
|||
}
|
||||
}
|
||||
|
||||
/// Bring effect instances saved by earlier versions up to date with the effect registry
|
||||
/// (renamed / regrouped / added parameters, reordered popups; see
|
||||
/// [`effectcraft_effects::migrate`]).
|
||||
pub fn upgrade_effects(p: &mut Project) {
|
||||
let mut sizes = std::collections::HashMap::new();
|
||||
for (cid, c) in p.comps() {
|
||||
for l in &c.layers {
|
||||
let (w, h) = effectcraft_render::source_size(p, l);
|
||||
sizes.insert((*cid, l.id), if w == 0 { [c.width as f64, c.height as f64] } else { [w as f64, h as f64] });
|
||||
}
|
||||
}
|
||||
let mut next = p.next_id;
|
||||
for (cid, it) in p.items.iter_mut() {
|
||||
let effectcraft_project::ItemKind::Comp(c) = &mut it.kind else { continue };
|
||||
for l in &mut std::sync::Arc::make_mut(c).layers {
|
||||
let size = sizes.get(&(*cid, l.id)).copied().unwrap_or([100.0, 100.0]);
|
||||
let Some(fx) = l.props.sub_mut("effects") else { continue };
|
||||
for n in &mut fx.children {
|
||||
let effectcraft_project::Node::Group(g) = n else { continue };
|
||||
let effectcraft_project::GroupKind::Effect { effect } = &g.kind else { continue };
|
||||
if let Some(spec) = effectcraft_effects::find(effect) {
|
||||
effectcraft_effects::migrate::upgrade_instance(spec, g, &mut effectcraft_project::build::Ids(&mut next), size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p.next_id = next;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod rq_tests;
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -352,7 +352,8 @@ fn add_effect(proj: &mut Project, l: &mut Layer, effect: &str, comp: &Comp, para
|
|||
let mut ids = Ids(&mut proj.next_id);
|
||||
let mut g = effectcraft_effects::instantiate(spec, &mut ids, spec.name, [comp.width as f64, comp.height as f64]);
|
||||
for (k, v) in params {
|
||||
if let Some(p) = g.get_mut(k) {
|
||||
// Paths reach parameters in twirl-down groups (`master/exposure`).
|
||||
if let Some(p) = g.prop_mut(k) {
|
||||
p.value = v.clone();
|
||||
}
|
||||
}
|
||||
|
|
@ -364,9 +365,13 @@ fn add_effect(proj: &mut Project, l: &mut Layer, effect: &str, comp: &Comp, para
|
|||
fn add_adjustment_effect(proj: &mut Project, l: &mut Layer, adj: &Adjustment, comp: &Comp, warnings: &mut Vec<String>, name: &str) {
|
||||
match adj {
|
||||
Adjustment::Invert => add_effect(proj, l, "Invert", comp, &[]),
|
||||
Adjustment::BrightnessContrast { brightness, contrast, .. } => {
|
||||
add_effect(proj, l, "Brightness & Contrast", comp, &[("brightness", Value::Scalar(*brightness)), ("contrast", Value::Scalar(*contrast))])
|
||||
}
|
||||
Adjustment::BrightnessContrast { brightness, contrast, legacy } => add_effect(
|
||||
proj,
|
||||
l,
|
||||
"Brightness & Contrast",
|
||||
comp,
|
||||
&[("brightness", Value::Scalar(*brightness)), ("contrast", Value::Scalar(*contrast)), ("useLegacy", Value::Bool(*legacy))],
|
||||
),
|
||||
Adjustment::HueSaturation { hue, saturation, lightness, colorize } => {
|
||||
let mut p = vec![("hue", Value::Scalar(*hue)), ("saturation", Value::Scalar(*saturation)), ("lightness", Value::Scalar(*lightness))];
|
||||
if *colorize {
|
||||
|
|
@ -397,7 +402,7 @@ fn add_adjustment_effect(proj: &mut Project, l: &mut Layer, adj: &Adjustment, co
|
|||
l,
|
||||
"Exposure",
|
||||
comp,
|
||||
&[("exposure", Value::Scalar(*exposure)), ("offset", Value::Scalar(*offset)), ("gamma", Value::Scalar(*gamma))],
|
||||
&[("master/exposure", Value::Scalar(*exposure)), ("master/offset", Value::Scalar(*offset)), ("master/gamma", Value::Scalar(*gamma))],
|
||||
),
|
||||
Adjustment::Other(k) => warnings.push(format!("{name}: adjustment `{k}` is not converted (layer kept as an empty adjustment layer)")),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,3 +152,108 @@ fn layer_params_get_a_source_companion() {
|
|||
assert!(matches!(src.ui, effectcraft_project::ParamUi::Hidden));
|
||||
}
|
||||
}
|
||||
|
||||
/// Projects saved before parameters were renamed, regrouped, added or had their popups
|
||||
/// reordered open with their instances brought up to date (`effectcraft_effects::migrate`).
|
||||
#[test]
|
||||
fn opening_an_old_project_upgrades_effect_instances() {
|
||||
use effectcraft_effects::migrate::PARAM_ID_ALIASES;
|
||||
use effectcraft_project::{Node, ParamUi, PropGroup};
|
||||
let (mut s, a, _) = comp_with_two_solids();
|
||||
apply(&mut s, a, "ec.blur.gaussian");
|
||||
// One instance of every effect with renamed parameter ids.
|
||||
let mut aliased: Vec<&str> = PARAM_ID_ALIASES.iter().map(|(e, _, _)| *e).collect();
|
||||
aliased.dedup();
|
||||
for e in &aliased {
|
||||
apply(&mut s, a, e);
|
||||
}
|
||||
// Rewrite the saved instances the way an older version wrote them.
|
||||
fn take(g: &mut PropGroup, path: &str) -> Option<effectcraft_project::Property> {
|
||||
match path.split_once('/') {
|
||||
None => {
|
||||
let i = g.children.iter().position(|c| matches!(c, Node::Prop(p) if p.match_id == path))?;
|
||||
match g.children.remove(i) {
|
||||
Node::Prop(p) => Some(p),
|
||||
Node::Group(_) => None,
|
||||
}
|
||||
}
|
||||
Some((first, rest)) => take(g.sub_mut(first)?, rest),
|
||||
}
|
||||
}
|
||||
let mut old = (*s.project).clone();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
{
|
||||
let comp = std::sync::Arc::make_mut(match &mut old.items.get_mut(&cid).unwrap().kind {
|
||||
effectcraft_project::ItemKind::Comp(c) => c,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
let l = comp.layers.iter_mut().find(|l| l.id == LayerId(a)).unwrap();
|
||||
for n in &mut l.props.sub_mut("effects").unwrap().children {
|
||||
let Node::Group(g) = n else { continue };
|
||||
if g.match_id == "ec.blur.gaussian" {
|
||||
// Repeat Edge Pixels did not exist yet, Blurriness had another name, the popup
|
||||
// listed its options in reverse.
|
||||
take(g, "repeatEdge").unwrap();
|
||||
g.get_mut("blurriness").unwrap().name = "Old Blurriness".into();
|
||||
let d = g.get_mut("dimensions").unwrap();
|
||||
let ParamUi::Popup { options } = &d.ui else { panic!("dimensions is a popup") };
|
||||
d.ui = ParamUi::Popup { options: options.iter().rev().cloned().collect() };
|
||||
d.value = KV::Enum(0);
|
||||
}
|
||||
for (e, old_id, new_id) in PARAM_ID_ALIASES {
|
||||
if g.match_id == *e {
|
||||
let mut p = take(g, new_id).unwrap_or_else(|| panic!("{e}: no {new_id}"));
|
||||
// Put it back at its old path (which may be inside a group).
|
||||
let (path, leaf) = old_id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, old_id));
|
||||
p.match_id = leaf.to_string();
|
||||
let mut at: &mut PropGroup = g;
|
||||
for seg in path.into_iter().flat_map(|p| p.split('/')) {
|
||||
if at.sub(seg).is_none() {
|
||||
at.children.push(PropGroup::new(1_000_000 + at.children.len() as u64, seg, seg).into());
|
||||
}
|
||||
at = at.sub_mut(seg).unwrap();
|
||||
}
|
||||
at.children.push(p.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("ec-upgrade-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("old.ecproj").to_string_lossy().to_string();
|
||||
std::fs::write(&path, old.to_json()).unwrap();
|
||||
let mut s2 = Session::default();
|
||||
s2.execute("file.open", json!({"path": path})).unwrap();
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
let l = s2.project.comp(cid).unwrap().layer(LayerId(a)).unwrap().clone();
|
||||
let fx = l.effects().unwrap();
|
||||
let spec = effectcraft_effects::find("ec.blur.gaussian").unwrap();
|
||||
let sp = |id: &str| spec.params.iter().find(|p| p.id == id).unwrap();
|
||||
let blur = fx.groups().find(|g| g.match_id == "ec.blur.gaussian").unwrap();
|
||||
assert_eq!(blur.get("blurriness").unwrap().name, sp("blurriness").name);
|
||||
assert!(blur.get("repeatEdge").is_some(), "missing parameter added");
|
||||
let dims = blur.get("dimensions").unwrap();
|
||||
let ParamUi::Popup { options } = &sp("dimensions").ui else { panic!() };
|
||||
assert_eq!(dims.value, KV::Enum(options.len() as u32 - 1), "popup value remapped by label");
|
||||
assert_eq!(dims.ui, sp("dimensions").ui);
|
||||
for (e, old_id, new_id) in PARAM_ID_ALIASES {
|
||||
let g = fx.groups().find(|g| g.match_id == *e).unwrap();
|
||||
let at = |id: &str| {
|
||||
let (path, leaf) = id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, id));
|
||||
let mut node = Some(g);
|
||||
for seg in path.into_iter().flat_map(|p| p.split('/')) {
|
||||
node = node.and_then(|n| n.sub(seg));
|
||||
}
|
||||
node.and_then(|n| n.get(leaf)).is_some()
|
||||
};
|
||||
assert!(!at(old_id), "{e}: {old_id} still present");
|
||||
let (path, leaf) = new_id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, new_id));
|
||||
let mut node = Some(g);
|
||||
for seg in path.into_iter().flat_map(|p| p.split('/')) {
|
||||
node = node.and_then(|n| n.sub(seg));
|
||||
}
|
||||
assert!(node.and_then(|n| n.get(leaf)).is_some(), "{e}: {old_id} not moved to {new_id}");
|
||||
}
|
||||
// The upgraded project renders.
|
||||
let _ = s2.render(cid, s2.time(), Default::default());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,20 +179,21 @@ fn glow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let use_colors = ctx.params.e("colors") == 1;
|
||||
let ca = ctx.params.color("colorA");
|
||||
let cb = ctx.params.color("colorB");
|
||||
let (kx, ky) = dims_xy(ctx.params.e("glowDimensions"));
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, (radius * 1.5).ceil() as u32 + 2)?;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = use_colors as u32;
|
||||
p.f[0][0] = thr;
|
||||
p.u[0] = [use_colors as u32, (ctx.params.e("based") == 0) as u32, ctx.params.e("colorLooping"), 0];
|
||||
p.f[0] = [thr, ctx.params.f("colorLoops") as f32, (ctx.params.f("colorPhase") / 360.0) as f32, ctx.params.f("abMidpoint") as f32 / 100.0];
|
||||
p.f[1] = ca;
|
||||
p.f[2] = cb;
|
||||
let bright = e.image(b.img.width, b.img.height);
|
||||
e.pixels("glow_bright", &p, &b.img, None, &bright, None);
|
||||
let s = (radius / 2.0).max(0.5);
|
||||
let blurred = gaussian_blur(e, &bright, s, s, false);
|
||||
let blurred = gaussian_blur(e, &bright, s * kx, s * ky, false);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = (ctx.params.e("operation") == 1) as u32;
|
||||
p.u[0][0] = ctx.params.e("operation").min(2);
|
||||
p.f[0][0] = intensity;
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels("glow_combine", &p, &b.img, Some(&blurred), &out, None);
|
||||
|
|
@ -240,8 +241,9 @@ fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
* Mat3::skew_deg(ctx.params.f("skew"), ctx.params.f("skewAxis"))
|
||||
* Mat3::scale(vec2(sw / 100.0, sh / 100.0))
|
||||
* Mat3::translate(vec2(-anchor.0, -anchor.1));
|
||||
let sampling = if ctx.params.e("sampling") == 1 { Sampling::Bicubic } else { Sampling::Bilinear };
|
||||
let empty = e.image(b.img.width, b.img.height);
|
||||
b.img = ops::warp(e, &empty, &b.img, &m, Sampling::Bilinear, effectcraft_color::BlendMode::Normal, opacity, 0, None);
|
||||
b.img = ops::warp(e, &empty, &b.img, &m, sampling, effectcraft_color::BlendMode::Normal, opacity, 0, None);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
|
|
@ -292,8 +294,17 @@ fn pointwise(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
let br = f("brightness") as f32 / 100.0;
|
||||
let ct = f("contrast") as f32 / 100.0;
|
||||
let k = if ct >= 0.0 { 1.0 / (1.0 - ct * 0.99) } else { 1.0 + ct };
|
||||
p.u[0][0] = 2;
|
||||
p.f[0] = [br, k, 0.0, 0.0];
|
||||
let legacy = ctx.params.b("useLegacy");
|
||||
// Mode: 0 = identity, 1 = legacy linear, 2 = tone curves (see `bc_modern`).
|
||||
let mode = if br == 0.0 && ct == 0.0 {
|
||||
0
|
||||
} else if legacy {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
p.u[0] = [2, mode, 0, 0];
|
||||
p.f[0] = [br, k, ct, 0.0];
|
||||
}
|
||||
"ec.color.huesaturation" => {
|
||||
p.u[0] = [3, ctx.params.b("colorize") as u32, 0, 0];
|
||||
|
|
@ -301,16 +312,28 @@ fn pointwise(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.f[1] = [f("colorizeHue") as f32 / 360.0, f("colorizeSaturation") as f32 / 100.0, f("colorizeLightness") as f32 / 100.0, 0.0];
|
||||
}
|
||||
"ec.color.levels" => {
|
||||
p.u[0] = [4, !ctx.params.b("noClip") as u32, 0, 0];
|
||||
let (clip_b, clip_w) = effectcraft_effects::levels_clip(ctx);
|
||||
p.u[0] = [4, clip_b as u32, clip_w as u32, 0];
|
||||
p.f[0] = [f("inBlack") as f32, f("inWhite") as f32, f("gamma").max(0.01) as f32, f("outBlack") as f32];
|
||||
p.f[1][0] = f("outWhite") as f32;
|
||||
}
|
||||
"ec.color.exposure" => {
|
||||
p.u[0][0] = 5;
|
||||
p.f[0] = [2f32.powf(f("exposure") as f32), f("offset") as f32, f("gamma").max(0.01) as f32, 0.0];
|
||||
let s = effectcraft_effects::exposure_settings(ctx);
|
||||
p.u[0] = [5, ctx.params.b("bypassLinearLight") as u32, 0, 0];
|
||||
for i in 0..3 {
|
||||
p.f[0][i] = s[i].0;
|
||||
p.f[1][i] = s[i].1;
|
||||
p.f[2][i] = s[i].2;
|
||||
}
|
||||
}
|
||||
"ec.channel.invert" => {
|
||||
p.u[0] = [6, ctx.params.e("channel"), 0, 0];
|
||||
// The shader does the RGB channels and Alpha; HLS / YIQ inversions run on the CPU.
|
||||
let ch = match ctx.params.e("channel") {
|
||||
c @ 0..=3 => c,
|
||||
effectcraft_effects::INVERT_ALPHA => 4,
|
||||
_ => return None,
|
||||
};
|
||||
p.u[0] = [6, ch, 0, 0];
|
||||
p.f[0][0] = 1.0 - f("blend") as f32 / 100.0;
|
||||
}
|
||||
"ec.generate.fill" => {
|
||||
|
|
@ -330,14 +353,35 @@ fn pointwise(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.f[3] = [len2 as f32, f("scatter") as f32 / 512.0, f("blend") as f32 / 100.0, 0.0];
|
||||
}
|
||||
"ec.noise.fractal" => {
|
||||
// The shader covers Soft Linear noise, the Basic / Turbulent Smooth / Turbulent Basic
|
||||
// types, all Overflow modes, independent width / height scaling, Sub Influence and Sub
|
||||
// Scaling, and the None / Normal blending modes; anything else renders on the CPU.
|
||||
let pr = ctx.params;
|
||||
let kind = pr.e("fractalType");
|
||||
let mode = pr.e("blendingMode");
|
||||
let sub_offset = pr.v2("subSettings/subOffset");
|
||||
if pr.e("noiseType") != 2
|
||||
|| kind > 2
|
||||
|| mode > 1
|
||||
|| pr.b("evolutionOptions/cycleEvolution")
|
||||
|| pr.b("transform/perspectiveOffset")
|
||||
|| f("subSettings/subRotation") != 0.0
|
||||
|| (!pr.b("subSettings/centerSubscale") && sub_offset != [0.0, 0.0])
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let octaves = f("complexity").clamp(1.0, 20.0);
|
||||
let (sr, cr) = (f("rotation") as f32).to_radians().sin_cos();
|
||||
let (ox, oy) = b.to_px(ctx.params.v2("offset"));
|
||||
p.u[0] = [10, ctx.params.e("fractalType"), octaves.ceil() as u32, f("seed") as u32 ^ 0x51ed];
|
||||
p.u[1][0] = ctx.params.b("invert") as u32;
|
||||
p.f[0] = [f("contrast") as f32 / 100.0, f("brightness") as f32 / 100.0, (f("scale") * b.scale).max(1.0) as f32, (octaves - octaves.floor()) as f32];
|
||||
let (sr, cr) = (f("transform/rotation") as f32).to_radians().sin_cos();
|
||||
let (ox, oy) = b.to_px(pr.v2("transform/offset"));
|
||||
let s = f("transform/scale");
|
||||
let (sw, sh) = if pr.b("transform/uniformScaling") { (s, s) } else { (f("transform/scaleWidth"), f("transform/scaleHeight")) };
|
||||
let sub = (f("subSettings/subScaling") / 100.0).clamp(0.1, 1.0);
|
||||
p.u[0] = [10, kind, octaves.ceil() as u32, f("evolutionOptions/seed") as u32 ^ 0x51ed];
|
||||
p.u[1] = [pr.b("invert") as u32, pr.e("overflow"), mode, 0];
|
||||
p.f[0] = [f("contrast") as f32 / 100.0, f("brightness") as f32 / 100.0, (sw * b.scale).max(1.0) as f32, (octaves - octaves.floor()) as f32];
|
||||
p.f[1] = [ox as f32, oy as f32, sr, cr];
|
||||
p.f[2] = [f("evolution") as f32 / 360.0, f("blend") as f32 / 100.0, f("opacity") as f32 / 100.0, 0.0];
|
||||
p.f[2] = [f("evolution") as f32 / 360.0, f("blend") as f32 / 100.0, (f("opacity") as f32 / 100.0).clamp(0.0, 1.0), (sh * b.scale).max(1.0) as f32];
|
||||
p.f[3] = [(f("subSettings/subInfluence") as f32 / 100.0).clamp(0.0, 1.0), 1.0 / sub as f32, 0.0, 0.0];
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,7 +326,29 @@ fn directional(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
|
||||
// ---------------------------------------------------------------- Glow / Drop Shadow
|
||||
|
||||
// Glow bright pass. u[0].x = A & B colours; f[0].x = threshold; f[1] = colour A; f[2] = colour B.
|
||||
// Glow's A & B gradient position (effects::misc::glow_ab_t). f[0].yzw = loops, phase, midpoint;
|
||||
// u[0].z = colour looping.
|
||||
fn glow_ab_t(l: f32) -> f32 {
|
||||
let u = clamp(l, 0.0, 1.0) * max(P.f[0].y, 0.0) + P.f[0].z;
|
||||
var t = u - floor(u);
|
||||
if (u > 0.0 && t <= 0.0) {
|
||||
t = 1.0;
|
||||
}
|
||||
let tri = 1.0 - abs(2.0 * t - 1.0);
|
||||
let lp = P.u[0].z;
|
||||
if (lp == 1u) {
|
||||
t = 1.0 - t;
|
||||
} else if (lp == 3u) {
|
||||
t = 1.0 - tri;
|
||||
} else if (lp != 0u) {
|
||||
t = tri;
|
||||
}
|
||||
let m = clamp(P.f[0].w, 0.01, 0.99);
|
||||
return pow(max(t, 0.0), log(0.5) / log(m));
|
||||
}
|
||||
|
||||
// Glow bright pass. u[0].x = A & B colours; u[0].y = based on alpha; f[0].x = threshold;
|
||||
// f[1] = colour A; f[2] = colour B.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn glow_bright(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
|
|
@ -341,10 +363,13 @@ fn glow_bright(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
return;
|
||||
}
|
||||
let thr = P.f[0].x;
|
||||
let l = luminance(px.xyz / a);
|
||||
var l = luminance(px.xyz / a);
|
||||
if (P.u[0].y != 0u) {
|
||||
l = a;
|
||||
}
|
||||
let k = clamp((l - thr) / max(1.0 - thr, 1e-3), 0.0, 1.0);
|
||||
if (P.u[0].x != 0u) {
|
||||
let t = clamp(l, 0.0, 1.0);
|
||||
let t = glow_ab_t(l);
|
||||
let ca = P.f[1].xyz;
|
||||
let cb = P.f[2].xyz;
|
||||
let c = ca + (cb - ca) * t;
|
||||
|
|
@ -354,7 +379,8 @@ fn glow_bright(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
}
|
||||
}
|
||||
|
||||
// Glow composite: original `src` with the blurred glow `aux`. u[0].x = behind; f[0].x = intensity.
|
||||
// Glow composite: original `src` with the blurred glow `aux`. u[0].x = 0 on top, 1 behind,
|
||||
// 2 none (glow only); f[0].x = intensity.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn glow_combine(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
|
|
@ -364,7 +390,9 @@ fn glow_combine(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
}
|
||||
var o = textureLoad(src, p, 0);
|
||||
let g = textureLoad(aux, p, 0) * P.f[0].x;
|
||||
if (P.u[0].x != 0u) {
|
||||
if (P.u[0].x == 2u) {
|
||||
o = g;
|
||||
} else if (P.u[0].x == 1u) {
|
||||
let k = 1.0 - o.w;
|
||||
o = o + g * k;
|
||||
} else {
|
||||
|
|
@ -518,8 +546,9 @@ fn straight(px: vec4<f32>) -> vec3<f32> {
|
|||
// 8 Curves f[0] = data offsets (rgb, red, green, blue), f[1].x = alpha (−1 = none)
|
||||
// 9 Gradient Ramp f[0] = (sx, sy, ex, ey); f[1] = start colour; f[2] = end colour;
|
||||
// f[3] = (len2, scatter, blend); u[0] = (_, radial, seed)
|
||||
// 10 Fractal Noise f[0] = (contrast, brightness, scale, frac); f[1] = (ox, oy, sr, cr);
|
||||
// f[2] = (evo, blend, opacity); u[0] = (_, kind, n_oct, seed); u[1].x = invert
|
||||
// 10 Fractal Noise f[0] = (contrast, brightness, scale x, frac); f[1] = (ox, oy, sr, cr);
|
||||
// f[2] = (evo, blend, opacity, scale y); f[3] = (sub influence, 1 / sub scaling);
|
||||
// u[0] = (_, kind, n_oct, seed); u[1] = (invert, overflow, normal blend)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
|
|
@ -546,7 +575,26 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
let c = straight(px);
|
||||
let br = P.f[0].x;
|
||||
let k = P.f[0].y;
|
||||
px = vec4<f32>(max((c + br - 0.5) * k + 0.5, vec3<f32>(0.0)) * a, a);
|
||||
let ct = P.f[0].z;
|
||||
if (P.u[0].y == 1u) {
|
||||
px = vec4<f32>(max((c + br - 0.5) * k + 0.5, vec3<f32>(0.0)) * a, a);
|
||||
} else if (P.u[0].y == 2u) {
|
||||
// Brightness & Contrast tone curves (effects crate bc_modern).
|
||||
var o = c;
|
||||
let pb = exp2(-br);
|
||||
let g = exp2(2.0 * ct);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (c[i] < 1.0) {
|
||||
let v = powz(max(c[i], 0.0), pb);
|
||||
if (v < 0.5) {
|
||||
o[i] = 0.5 * powz(2.0 * v, g);
|
||||
} else {
|
||||
o[i] = 1.0 - 0.5 * powz(max(2.0 - 2.0 * v, 0.0), g);
|
||||
}
|
||||
}
|
||||
}
|
||||
px = vec4<f32>(o * a, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 3u: {
|
||||
|
|
@ -600,7 +648,10 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
for (var i = 0; i < 3; i++) {
|
||||
var t = (c[i] - ib) / max(iw - ib, 1e-6);
|
||||
if (P.u[0].y != 0u) {
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
t = max(t, 0.0);
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
t = min(t, 1.0);
|
||||
}
|
||||
t = powz(max(t, 0.0), 1.0 / g);
|
||||
o[i] = ob + (ow - ob) * t;
|
||||
|
|
@ -613,9 +664,15 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
let c = straight(px);
|
||||
var o = vec3<f32>(0.0);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
let lin = srgb_to_linear(max(c[i], 0.0));
|
||||
let v = powz(max(lin * P.f[0].x + P.f[0].y, 0.0), 1.0 / P.f[0].z);
|
||||
o[i] = linear_to_srgb(v);
|
||||
// Per-channel gain, offset, gamma; negative values are mirrored.
|
||||
if (P.u[0].y != 0u) {
|
||||
let x = c[i] * P.f[0][i] + P.f[1][i];
|
||||
o[i] = sign(x) * powz(abs(x), 1.0 / P.f[2][i]);
|
||||
} else {
|
||||
let lin = srgb_to_linear(max(c[i], 0.0));
|
||||
let x = lin * P.f[0][i] + P.f[1][i];
|
||||
o[i] = sign(x) * linear_to_srgb(powz(abs(x), 1.0 / P.f[2][i]));
|
||||
}
|
||||
}
|
||||
px = vec4<f32>(o * a, a);
|
||||
}
|
||||
|
|
@ -706,6 +763,9 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
let contrast = P.f[0].x;
|
||||
let brightness = P.f[0].y;
|
||||
let scale = P.f[0].z;
|
||||
let scale_y = P.f[2].w;
|
||||
let infl = P.f[3].x;
|
||||
let fmul = P.f[3].y;
|
||||
let frac = P.f[0].w;
|
||||
let ox = P.f[1].x;
|
||||
let oy = P.f[1].y;
|
||||
|
|
@ -718,7 +778,7 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
let dx = f32(p.x) + 0.5 - ox;
|
||||
let dy = f32(p.y) + 0.5 - oy;
|
||||
let u = (dx * cr + dy * sr) / scale;
|
||||
let v = (-dx * sr + dy * cr) / scale;
|
||||
let v = (-dx * sr + dy * cr) / scale_y;
|
||||
var sum = 0.0;
|
||||
var amp = 1.0;
|
||||
var norm = 0.0;
|
||||
|
|
@ -729,20 +789,40 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
w = frac;
|
||||
}
|
||||
var n = value_noise(u * f, v * f, evo + f32(o) * 7.31, seed + o);
|
||||
let s = n * 2.0 - 1.0;
|
||||
if (kind == 1u) {
|
||||
n = 1.0 - abs(n * 2.0 - 1.0);
|
||||
n = 1.0 - s * s;
|
||||
} else if (kind == 2u) {
|
||||
n = 1.0 - abs(s);
|
||||
}
|
||||
sum += n * amp * w;
|
||||
norm += amp * w;
|
||||
amp *= 0.5;
|
||||
f *= 2.0;
|
||||
amp *= infl;
|
||||
f *= fmul;
|
||||
}
|
||||
var val = sum / max(norm, 1e-6);
|
||||
val = clamp((val - 0.5) * contrast + 0.5 + brightness, 0.0, 1.0);
|
||||
val = (val - 0.5) * contrast + 0.5 + brightness;
|
||||
// Overflow: Clip, Soft Clamp, Wrap Back, Allow HDR Results.
|
||||
let ov = P.u[1].y;
|
||||
if (ov == 0u) {
|
||||
val = clamp(val, 0.0, 1.0);
|
||||
} else if (ov == 1u) {
|
||||
val = clamp(0.5 + 0.5 * tanh(2.0 * (val - 0.5)) / tanh(1.0), 0.0, 1.0);
|
||||
} else if (ov == 2u) {
|
||||
let t = val - 2.0 * floor(val / 2.0);
|
||||
val = select(t, 2.0 - t, t > 1.0);
|
||||
}
|
||||
if (P.u[1].x != 0u) {
|
||||
val = 1.0 - val;
|
||||
}
|
||||
px = put(px, vec3<f32>(val), P.f[2].z, P.f[2].y);
|
||||
let op = P.f[2].z;
|
||||
if (P.u[1].z == 1u) {
|
||||
// Normal: the noise over the layer at Opacity, then Blend With Original.
|
||||
let over = vec4<f32>(vec3<f32>(val) * op, op) + px * (1.0 - op);
|
||||
px = px * P.f[2].y + over * (1.0 - P.f[2].y);
|
||||
} else {
|
||||
px = put(px, vec3<f32>(val), op, P.f[2].y);
|
||||
}
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -434,8 +434,22 @@ fn gpu_effects_match_cpu() {
|
|||
effect_case("ec.blur.directional", &[("direction", n(30.0)), ("length", n(12.0))]);
|
||||
effect_case("ec.stylize.glow", &[("threshold", n(40.0)), ("radius", n(14.0)), ("intensity", n(1.5))]);
|
||||
effect_case("ec.stylize.glow", &[("colors", Value::Enum(1)), ("operation", Value::Enum(1)), ("colorA", c(1.0, 0.8, 0.1))]);
|
||||
effect_case(
|
||||
"ec.stylize.glow",
|
||||
&[
|
||||
("colors", Value::Enum(1)),
|
||||
("colorLooping", Value::Enum(3)),
|
||||
("colorLoops", n(2.5)),
|
||||
("colorPhase", n(40.0)),
|
||||
("abMidpoint", n(30.0)),
|
||||
("glowDimensions", Value::Enum(1)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.stylize.glow", &[("based", Value::Enum(0)), ("operation", Value::Enum(2)), ("threshold", n(20.0))]);
|
||||
effect_case("ec.color.levels", &[("inBlack", n(0.1)), ("inWhite", n(0.8)), ("gamma", n(1.6)), ("outBlack", n(0.05))]);
|
||||
effect_case("ec.color.levels", &[("inWhite", n(0.5)), ("noClip", Value::Bool(true))]);
|
||||
effect_case("ec.color.levels", &[("inBlack", n(0.2)), ("inWhite", n(0.6)), ("clipToOutputBlack", Value::Enum(0))]);
|
||||
effect_case("ec.color.levels", &[("inBlack", n(0.2)), ("inWhite", n(0.6)), ("clipToOutputWhite", Value::Enum(0))]);
|
||||
effect_case(
|
||||
"ec.color.curves",
|
||||
&[("rgb", Value::Str("0,0 0.3,0.45 1,1".into())), ("red", Value::Str("0,0.1 1,0.9".into())), ("alpha", Value::Str("0,0 0.5,0.7 1,1".into()))],
|
||||
|
|
@ -450,15 +464,40 @@ fn gpu_effects_match_cpu() {
|
|||
effect_case("ec.noise.fractal", &[]);
|
||||
effect_case(
|
||||
"ec.noise.fractal",
|
||||
&[("fractalType", Value::Enum(1)), ("complexity", n(4.5)), ("rotation", n(30.0)), ("evolution", n(90.0)), ("scale", n(40.0)), ("blend", n(30.0))],
|
||||
&[
|
||||
("fractalType", Value::Enum(1)),
|
||||
("complexity", n(4.5)),
|
||||
("transform/rotation", n(30.0)),
|
||||
("evolution", n(90.0)),
|
||||
("transform/scale", n(40.0)),
|
||||
("blend", n(30.0)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.noise.fractal",
|
||||
&[
|
||||
("fractalType", Value::Enum(2)),
|
||||
("overflow", Value::Enum(1)),
|
||||
("contrast", n(250.0)),
|
||||
("transform/uniformScaling", Value::Bool(false)),
|
||||
("transform/scaleWidth", n(60.0)),
|
||||
("transform/scaleHeight", n(25.0)),
|
||||
("subSettings/subInfluence", n(40.0)),
|
||||
("subSettings/subScaling", n(70.0)),
|
||||
("opacity", n(60.0)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.noise.fractal", &[("overflow", Value::Enum(2)), ("contrast", n(300.0)), ("blendingMode", Value::Enum(0)), ("opacity", n(50.0))]);
|
||||
effect_case("ec.perspective.dropshadow", &[("distance", n(6.0)), ("softness", n(8.0)), ("opacity", n(80.0))]);
|
||||
effect_case("ec.perspective.dropshadow", &[("shadowOnly", Value::Bool(true)), ("color", c(0.0, 0.2, 0.6))]);
|
||||
effect_case("ec.color.brightnesscontrast", &[("brightness", n(20.0)), ("contrast", n(35.0))]);
|
||||
effect_case("ec.color.brightnesscontrast", &[("brightness", n(-10.0)), ("contrast", n(-40.0))]);
|
||||
effect_case("ec.color.exposure", &[("exposure", n(1.3)), ("offset", n(0.02)), ("gamma", n(1.2))]);
|
||||
effect_case("ec.color.brightnesscontrast", &[("brightness", n(20.0)), ("contrast", n(35.0)), ("useLegacy", Value::Bool(true))]);
|
||||
effect_case("ec.color.exposure", &[("master/exposure", n(1.3)), ("master/offset", n(0.02)), ("master/gamma", n(1.2))]);
|
||||
effect_case("ec.color.exposure", &[("channels", Value::Enum(1)), ("red/redExposure", n(0.8)), ("green/greenOffset", n(-0.05)), ("blue/blueGamma", n(1.6))]);
|
||||
effect_case("ec.color.exposure", &[("master/exposure", n(0.5)), ("master/offset", n(-0.03)), ("bypassLinearLight", Value::Bool(true))]);
|
||||
effect_case("ec.channel.invert", &[]);
|
||||
effect_case("ec.channel.invert", &[("channel", Value::Enum(4)), ("blend", n(30.0))]);
|
||||
effect_case("ec.channel.invert", &[("channel", Value::Enum(effectcraft_effects::INVERT_ALPHA)), ("blend", n(30.0))]);
|
||||
effect_case("ec.channel.invert", &[("channel", Value::Enum(2))]);
|
||||
effect_case("ec.distort.transform", &[("rotation", n(20.0)), ("scaleHeight", n(80.0)), ("skew", n(10.0)), ("opacity", n(70.0))]);
|
||||
}
|
||||
|
|
@ -470,7 +509,7 @@ fn effect_chains_and_mixed_stacks() {
|
|||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let mut l = s.footage(80, 50);
|
||||
s.effect(&mut l, "ec.color.exposure", &[("exposure", n(0.7))]);
|
||||
s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]);
|
||||
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]);
|
||||
s.effect(&mut l, "ec.stylize.posterize", &[]);
|
||||
s.effect(&mut l, "ec.stylize.glow", &[]);
|
||||
|
|
|
|||
|
|
@ -698,8 +698,9 @@ fn levels_ids(effect: &str, ch: usize) -> [String; 5] {
|
|||
if effect == "ec.color.levels" {
|
||||
return ["inBlack", "inWhite", "gamma", "outBlack", "outWhite"].map(String::from);
|
||||
}
|
||||
// Levels (Individual Controls) nests each channel's controls in its twirl-down group.
|
||||
let pre = fw::LEVELS_CHANNELS[ch.min(4)].0;
|
||||
["InBlack", "InWhite", "Gamma", "OutBlack", "OutWhite"].map(|s| format!("{pre}{s}"))
|
||||
["InBlack", "InWhite", "Gamma", "OutBlack", "OutWhite"].map(|s| format!("{pre}/{pre}{s}"))
|
||||
}
|
||||
|
||||
/// Levels: (channel popup,) histogram, input black/gamma/white triangles, output bar with
|
||||
|
|
@ -741,10 +742,10 @@ fn levels_editor(
|
|||
}
|
||||
app.auto.add(&format!("effectControls.effect.{euid}.levels.histogram"), hr, "Histogram");
|
||||
let ids = levels_ids(effect, ch);
|
||||
let val = |id: &str| g.get(id).map(|pr| ectx.value(layer, pr).as_f64()).unwrap_or(0.0);
|
||||
let val = |id: &str| g.prop(id).map(|pr| ectx.value(layer, pr).as_f64()).unwrap_or(0.0);
|
||||
let (ib, iw, gm, ob, ow) = (val(&ids[0]), val(&ids[1]), val(&ids[2]), val(&ids[3]), val(&ids[4]));
|
||||
let mut set = |ui: &egui::Ui, id: &str, v: f64, started: bool| {
|
||||
if let Some(pr) = g.get(id) {
|
||||
if let Some(pr) = g.prop(id) {
|
||||
let key = gesture_key(ui, egui::Id::new(("ec-lv", euid, id)), started);
|
||||
actions.push(("prop.set".into(), json!({"layer": layer.id.0, "prop": pr.uid, "value": v, "merge": key})));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ pub fn draw_overlay(
|
|||
let bc = c32(o.boundary_color, 255);
|
||||
let oc = c32(o.overlay_color, (o.overlay_opacity / 100.0 * 255.0).clamp(0.0, 255.0) as u8);
|
||||
// View Search Region: the band around the boundary (yellow).
|
||||
let search = roto_group(layer).filter(|g| g.prop("rotoBrushMatte/viewSearchRegion").is_some_and(|p| p.value.as_bool())).map(|g| {
|
||||
let r = g.prop("rotoBrushMatte/searchRadius").map(|p| p.value.as_f64()).unwrap_or(15.0) * scale;
|
||||
let search = roto_group(layer).filter(|g| g.prop("rotoBrushPropagation/viewSearchRegion").is_some_and(|p| p.value.as_bool())).map(|g| {
|
||||
let r = g.prop("rotoBrushPropagation/searchRadius").map(|p| p.value.as_f64()).unwrap_or(15.0) * scale;
|
||||
let bin: Vec<u8> = alpha.iter().map(|a| (*a >= 0.5) as u8).collect();
|
||||
let sd = effectcraft_engine::track::roto::matting::signed_distance(&bin, w, h);
|
||||
(sd, r as f32)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,421 @@
|
|||
# Effects
|
||||
|
||||
<!-- 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 2434 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**: 261 implemented in full, 42 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`).
|
||||
|
||||
## 3D Channel
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Channel Extract | `ec.3d.channelextract` | 5 | | 32 | Partial: no anti-alias option |
|
||||
| Cryptomatte | `ec.3d.cryptomatte` | 4 | | 32 | Implemented |
|
||||
| Depth Matte | `ec.3d.depthmatte` | 3 | | 32 | Implemented |
|
||||
| Depth of Field | `ec.3d.depthoffield` | 4 | | 32 | Implemented |
|
||||
| EXtractoR | `ec.3d.extractor` | 8 | | 32 | Implemented |
|
||||
| Fog 3D | `ec.3d.fog3d` | 8 | | 32 | Implemented |
|
||||
| ID Matte | `ec.3d.idmatte` | 5 | | 32 | Implemented |
|
||||
| IDentifier | `ec.3d.identifier` | 3 | | 32 | Implemented |
|
||||
|
||||
## Audio
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Backwards | `ec.audio.backwards` | 1 | | 32 | Implemented |
|
||||
| Bass & Treble | `ec.audio.basstreble` | 2 | | 32 | Implemented |
|
||||
| Compressor | `ec.audio.compressor` | 8 | | 32 | Implemented |
|
||||
| Delay | `ec.audio.delay` | 5 | | 32 | Implemented |
|
||||
| Distortion | `ec.audio.distortion` | 7 | | 32 | Implemented |
|
||||
| Flange & Chorus | `ec.audio.flangechorus` | 9 | | 32 | Implemented |
|
||||
| Gate | `ec.audio.gate` | 4 | | 32 | Implemented |
|
||||
| High-Low Pass | `ec.audio.highlowpass` | 4 | | 32 | Implemented |
|
||||
| Modulator | `ec.audio.modulator` | 4 | | 32 | Implemented |
|
||||
| Parametric EQ | `ec.audio.parametriceq` | 12 | | 32 | Implemented |
|
||||
| Reverb | `ec.audio.reverb` | 6 | | 32 | Implemented |
|
||||
| Stereo Mixer | `ec.audio.stereomixer` | 5 | | 32 | Implemented |
|
||||
| Tone | `ec.audio.tone` | 7 | | 32 | Implemented |
|
||||
|
||||
## Blur & Sharpen
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Bilateral Blur | `ec.blur.bilateral` | 3 | | 32 | Implemented |
|
||||
| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented |
|
||||
| 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-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
|
||||
| Channel Blur | `ec.blur.channel` | 6 | | 32 | Implemented |
|
||||
| Compound Blur | `ec.blur.compound` | 4 | | 32 | Implemented |
|
||||
| Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented |
|
||||
| Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented |
|
||||
| Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented |
|
||||
| Radial Blur | `ec.blur.radial` | 4 | | 32 | Implemented |
|
||||
| Sharpen | `ec.blur.sharpen` | 1 | | 32 | Implemented |
|
||||
| Smart Blur | `ec.blur.smart` | 3 | | 32 | Implemented |
|
||||
| Unsharp Mask | `ec.blur.unsharp` | 3 | | 32 | Implemented |
|
||||
|
||||
## Channel
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Arithmetic | `ec.channel.arithmetic` | 5 | | 32 | Implemented |
|
||||
| Blend | `ec.channel.blend` | 4 | | 32 | Implemented |
|
||||
| CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented |
|
||||
| Calculations | `ec.channel.calculations` | 9 | | 32 | Implemented |
|
||||
| Channel Combiner | `ec.channel.combiner` | 6 | | 32 | Partial: no Saturation Multiplied target |
|
||||
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | | 32 | Implemented |
|
||||
| Invert | `ec.channel.invert` | 2 | GPU | 32 | Implemented |
|
||||
| Minimax | `ec.channel.minimax` | 4 | | 32 | Partial: no Don't Shrink Edges option |
|
||||
| Remove Color Matting | `ec.channel.removecolormatting` | 2 | | 32 | Implemented |
|
||||
| Set Channels | `ec.channel.setchannels` | 9 | | 32 | Implemented |
|
||||
| Set Matte | `ec.channel.setmatte` | 6 | | 32 | Implemented |
|
||||
| Shift Channels | `ec.channel.shiftchannels` | 4 | | 32 | Implemented |
|
||||
| Solid Composite | `ec.channel.solidcomposite` | 4 | | 32 | Implemented |
|
||||
|
||||
## Color Correction
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Auto Color | `ec.color.autocolor` | 6 | | 32 | Implemented |
|
||||
| Auto Contrast | `ec.color.autocontrast` | 5 | | 32 | Implemented |
|
||||
| Auto Levels | `ec.color.autolevels` | 5 | | 32 | Implemented |
|
||||
| Black & White | `ec.color.blackwhite` | 8 | | 32 | Implemented |
|
||||
| Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented |
|
||||
| Broadcast Colors | `ec.color.broadcast` | 3 | | 32 | Implemented |
|
||||
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | | 32 | Implemented |
|
||||
| CC Color Offset | `ec.color.cccoloroffset` | 4 | | 32 | Implemented |
|
||||
| CC Kernel | `ec.color.cckernel` | 12 | | 32 | Implemented |
|
||||
| CC Toner | `ec.color.cctoner` | 7 | | 32 | Implemented |
|
||||
| Change Color | `ec.color.changecolor` | 9 | | 32 | Implemented |
|
||||
| Change to Color | `ec.color.changetocolor` | 9 | | 32 | Implemented |
|
||||
| Channel Mixer | `ec.color.channelmixer` | 13 | | 32 | Implemented |
|
||||
| Color Balance | `ec.color.colorbalance` | 10 | | 32 | Implemented |
|
||||
| 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 |
|
||||
| Curves | `ec.color.curves` | 0 | GPU | 32 | Partial: no Channel popup |
|
||||
| Equalize | `ec.color.equalize` | 2 | | 32 | Implemented |
|
||||
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
|
||||
| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | | 32 | Implemented |
|
||||
| Hue/Saturation | `ec.color.huesaturation` | 7 | GPU | 32 | Partial: no per-range Channel Control |
|
||||
| Leave Color | `ec.color.leavecolor` | 5 | | 32 | Implemented |
|
||||
| Levels | `ec.color.levels` | 7 | GPU | 32 | Partial: master channel only (no Channel popup) |
|
||||
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | | 32 | Implemented |
|
||||
| Lumetri Color | `ec.color.lumetri` | 38 | | 32 | Partial: no HSL Secondary, hue/saturation curves, HDR mode or look files |
|
||||
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented |
|
||||
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 4 | | 32 | Implemented |
|
||||
| OCIO Display Transform | `ec.color.ociodisplay` | 5 | | 32 | Implemented |
|
||||
| OCIO File Transform | `ec.color.ociofile` | 4 | | 32 | Implemented |
|
||||
| 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 |
|
||||
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | | 32 | Implemented |
|
||||
| Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented |
|
||||
| Tritone | `ec.color.tritone` | 4 | | 32 | Implemented |
|
||||
| Vibrance | `ec.color.vibrance` | 2 | | 32 | Implemented |
|
||||
| Video Limiter | `ec.color.videolimiter` | 5 | | 32 | Implemented |
|
||||
|
||||
## Distort
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Bezier Warp | `ec.distort.bezierwarp` | 13 | | 32 | Implemented |
|
||||
| Bulge | `ec.distort.bulge` | 6 | | 32 | Implemented |
|
||||
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
|
||||
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
|
||||
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
|
||||
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 32 | Implemented |
|
||||
| CC Griddler | `ec.distort.ccgriddler` | 5 | | 32 | Implemented |
|
||||
| CC Lens | `ec.distort.cclens` | 3 | | 32 | Implemented |
|
||||
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
|
||||
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented |
|
||||
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented |
|
||||
| CC Slant | `ec.distort.ccslant` | 4 | | 32 | Implemented |
|
||||
| CC Smear | `ec.distort.ccsmear` | 4 | | 32 | Implemented |
|
||||
| CC Split | `ec.distort.ccsplit` | 3 | | 32 | Implemented |
|
||||
| CC Split 2 | `ec.distort.ccsplit2` | 4 | | 32 | Implemented |
|
||||
| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented |
|
||||
| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented |
|
||||
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
|
||||
| Displacement Map | `ec.distort.displacementmap` | 8 | | 32 | Implemented |
|
||||
| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented |
|
||||
| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented |
|
||||
| Mesh Warp | `ec.distort.meshwarp` | 3 | | 32 | Implemented |
|
||||
| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented |
|
||||
| Offset | `ec.distort.offset` | 2 | | 32 | Implemented |
|
||||
| Optics Compensation | `ec.distort.opticscompensation` | 5 | | 32 | Partial: no Optimal Pixels |
|
||||
| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented |
|
||||
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
|
||||
| Reshape | `ec.distort.reshape` | 6 | | 32 | Partial: no correspondence points |
|
||||
| Ripple | `ec.distort.ripple` | 7 | | 32 | Implemented |
|
||||
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Partial: Pixel Motion Detail has no effect |
|
||||
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
|
||||
| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented |
|
||||
| Transform | `ec.distort.transform` | 10 | GPU | 32 | Partial: no shutter-angle motion blur |
|
||||
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | | 32 | Partial: no locked pinning variants |
|
||||
| Twirl | `ec.distort.twirl` | 3 | | 32 | Implemented |
|
||||
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented |
|
||||
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
|
||||
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
|
||||
| Wave Warp | `ec.distort.wavewarp` | 7 | | 32 | Implemented |
|
||||
|
||||
## Expression Controls
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Point Control | `ec.control.point3d` | 1 | GPU | 32 | Implemented |
|
||||
| Angle Control | `ec.control.angle` | 1 | GPU | 32 | Implemented |
|
||||
| Checkbox Control | `ec.control.checkbox` | 1 | GPU | 32 | Implemented |
|
||||
| Color Control | `ec.control.color` | 1 | GPU | 32 | Implemented |
|
||||
| Dropdown Menu Control | `ec.control.dropdown` | 1 | GPU | 32 | Implemented |
|
||||
| Layer Control | `ec.control.layer` | 1 | GPU | 32 | Implemented |
|
||||
| Point Control | `ec.control.point` | 1 | GPU | 32 | Implemented |
|
||||
| Slider Control | `ec.control.slider` | 1 | GPU | 32 | Implemented |
|
||||
|
||||
## Generate
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 4-Color Gradient | `ec.generate.fourcolor` | 12 | | 32 | Implemented |
|
||||
| Advanced Lightning | `ec.generate.advancedlightning` | 16 | | 32 | Partial: no Alpha Obstacle; most Expert Settings missing |
|
||||
| Audio Spectrum | `ec.generate.audiospectrum` | 23 | | 32 | Implemented |
|
||||
| Audio Waveform | `ec.generate.audiowaveform` | 17 | | 32 | Implemented |
|
||||
| Beam | `ec.generate.beam` | 11 | | 32 | Implemented |
|
||||
| CC Glue Gun | `ec.generate.ccgluegun` | 7 | | 32 | Implemented |
|
||||
| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | | 32 | Implemented |
|
||||
| CC Light Rays | `ec.generate.cclightrays` | 8 | | 32 | Implemented |
|
||||
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | | 32 | Implemented |
|
||||
| CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented |
|
||||
| Cell Pattern | `ec.generate.cellpattern` | 14 | | 32 | Partial: HQ variants render like the standard ones |
|
||||
| Checkerboard | `ec.generate.checkerboard` | 10 | | 32 | Implemented |
|
||||
| Circle | `ec.generate.circle` | 11 | | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented |
|
||||
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
|
||||
| Fill | `ec.generate.fill` | 3 | GPU | 32 | Partial: no Fill Mask / All Masks / feather |
|
||||
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
|
||||
| Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented |
|
||||
| Grid | `ec.generate.grid` | 12 | | 32 | Implemented |
|
||||
| Lens Flare | `ec.generate.lensflare` | 4 | | 32 | Implemented |
|
||||
| Paint Bucket | `ec.generate.paintbucket` | 11 | | 32 | Implemented |
|
||||
| Radio Waves | `ec.generate.radiowaves` | 17 | | 32 | Partial: no Image Contours or Mask wave types, Parameters Are Set At, reflection or stroke profile |
|
||||
| Scribble | `ec.generate.scribble` | 21 | | 32 | Partial: no caps, joins or Start/End Apply To |
|
||||
| Stroke | `ec.generate.stroke` | 11 | | 32 | Implemented |
|
||||
| Vegas | `ec.generate.vegas` | 26 | | 32 | Implemented |
|
||||
| Write-on | `ec.generate.writeon` | 10 | | 32 | Implemented |
|
||||
|
||||
## Immersive Video
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| VR Blur | `ec.vr.blur` | 2 | | 32 | Implemented |
|
||||
| VR Chromatic Aberrations | `ec.vr.chromaticaberrations` | 8 | | 32 | Implemented |
|
||||
| VR Color Gradients | `ec.vr.colorgradients` | 19 | | 32 | Implemented |
|
||||
| VR Converter | `ec.vr.converter` | 8 | | 32 | Partial: common layouts only |
|
||||
| VR De-Noise | `ec.vr.denoise` | 4 | | 32 | Implemented |
|
||||
| VR Digital Glitch | `ec.vr.digitalglitch` | 8 | | 32 | Partial: reduced control set |
|
||||
| VR Fractal Noise | `ec.vr.fractalnoise` | 16 | | 32 | Implemented |
|
||||
| VR Glow | `ec.vr.glow` | 7 | | 32 | Implemented |
|
||||
| VR Plane to Sphere | `ec.vr.planetosphere` | 6 | | 32 | Implemented |
|
||||
| VR Rotate Sphere | `ec.vr.rotatesphere` | 5 | | 32 | Implemented |
|
||||
| VR Sharpen | `ec.vr.sharpen` | 2 | | 32 | Implemented |
|
||||
| VR Sphere to Plane | `ec.vr.spheretoplane` | 5 | | 32 | Implemented |
|
||||
|
||||
## Keying
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Advanced Spill Suppressor | `ec.key.advancedspill` | 5 | | 32 | Partial: no Tolerance, Desaturate or Spill Color Correction |
|
||||
| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | | 32 | Implemented |
|
||||
| Color Difference Key | `ec.key.colordifference` | 16 | | 32 | Implemented |
|
||||
| Color Range | `ec.key.colorrange` | 8 | | 32 | Implemented |
|
||||
| Difference Matte | `ec.key.differencematte` | 6 | | 32 | Implemented |
|
||||
| Extract | `ec.key.extract` | 6 | | 32 | Implemented |
|
||||
| Inner/Outer Key | `ec.key.innerouter` | 10 | | 32 | Partial: one additional mask per side; no cleanup strokes |
|
||||
| Key Cleaner | `ec.key.keycleaner` | 4 | | 32 | Implemented |
|
||||
| Key Light | `ec.keying.keylight` | 42 | | 32 | Partial: no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing |
|
||||
| Linear Color Key | `ec.key.linearcolor` | 6 | | 32 | Implemented |
|
||||
| Screen Key | `ec.key.screen` | 11 | | 32 | Implemented |
|
||||
| Unmult | `ec.key.unmult` | 6 | | 32 | Implemented |
|
||||
|
||||
## Matte
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Matte Choker | `ec.matte.mattechoker` | 7 | | 32 | Implemented |
|
||||
| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
|
||||
| Refine Hard Matte | `ec.matte.refinehard` | 11 | | 32 | Partial: no Reduce Chatter or motion blur |
|
||||
| Refine Soft Matte | `ec.matte.refinesoft` | 13 | | 32 | Partial: no Reduce Chatter or motion blur |
|
||||
| Roto Brush & Refine Edge | `ec.matte.rotobrush` | 24 | | 32 | Implemented |
|
||||
| Simple Choker | `ec.matte.simplechoker` | 2 | | 32 | Implemented |
|
||||
|
||||
## Noise & Grain
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Add Grain | `ec.noise.addgrain` | 22 | | 32 | Partial: no preview region, presets, channel balance or temporal controls |
|
||||
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
|
||||
| Dust & Scratches | `ec.noise.dustscratches` | 3 | | 32 | Implemented |
|
||||
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
|
||||
| Match Grain | `ec.noise.matchgrain` | 25 | | 32 | Partial: no preview region, presets, sampling or temporal controls |
|
||||
| Median | `ec.noise.median` | 2 | | 32 | Implemented |
|
||||
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | | 32 | Implemented |
|
||||
| Noise | `ec.noise.noise` | 3 | | 32 | Implemented |
|
||||
| Noise Alpha | `ec.noise.noisealpha` | 8 | | 32 | Implemented |
|
||||
| Noise HLS | `ec.noise.noisehls` | 6 | | 32 | Implemented |
|
||||
| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | | 32 | Implemented |
|
||||
| Remove Grain | `ec.noise.removegrain` | 7 | | 32 | Partial: no preview region, sampling or temporal filtering |
|
||||
| Turbulent Noise | `ec.noise.turbulent` | 22 | | 32 | Implemented |
|
||||
|
||||
## Obsolete
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
|
||||
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
|
||||
| Color Key | `ec.key.colorkey` | 4 | | 32 | Implemented |
|
||||
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented |
|
||||
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
|
||||
| Luma Key | `ec.key.luma` | 5 | | 32 | Implemented |
|
||||
| Path Text | `ec.obsolete.pathtext` | 21 | | 32 | Partial: no kerning, shear/scale, line spacing, fade time or jitter |
|
||||
| Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | | 32 | Implemented |
|
||||
| Spill Suppressor | `ec.key.spill` | 3 | | 32 | Implemented |
|
||||
|
||||
## Paint
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Paint | `ec.paint.paint` | 1 | | 32 | Implemented |
|
||||
|
||||
## Perspective
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Camera Tracker | `ec.perspective.cameratracker` | 10 | | 32 | Implemented |
|
||||
| 3D Glasses | `ec.perspective.3dglasses` | 8 | | 32 | Implemented |
|
||||
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | | 32 | Implemented |
|
||||
| Bevel Edges | `ec.perspective.beveledges` | 4 | | 32 | Implemented |
|
||||
| CC Cylinder | `ec.perspective.cccylinder` | 12 | | 32 | Implemented |
|
||||
| CC Environment | `ec.perspective.ccenvironment` | 4 | | 32 | Implemented |
|
||||
| CC Sphere | `ec.perspective.ccsphere` | 14 | | 32 | Implemented |
|
||||
| CC Spotlight | `ec.perspective.ccspotlight` | 8 | | 32 | Implemented |
|
||||
| Drop Shadow | `ec.perspective.dropshadow` | 6 | GPU | 32 | Implemented |
|
||||
| Radial Shadow | `ec.perspective.radialshadow` | 9 | | 32 | Implemented |
|
||||
|
||||
## Simulation
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Ball Action | `ec.sim.ccballaction` | 8 | | 32 | Implemented |
|
||||
| CC Bubbles | `ec.sim.ccbubbles` | 7 | | 32 | Implemented |
|
||||
| CC Drizzle | `ec.sim.ccdrizzle` | 14 | | 32 | Implemented |
|
||||
| CC Hair | `ec.sim.cchair` | 20 | | 32 | Implemented |
|
||||
| CC Mr. Mercury | `ec.sim.ccmrmercury` | 23 | | 32 | Implemented |
|
||||
| CC Particle Systems II | `ec.sim.ccparticlesystems2` | 21 | | 32 | Implemented |
|
||||
| CC Particle World | `ec.sim.ccparticleworld` | 30 | | 32 | Implemented |
|
||||
| CC Pixel Polly | `ec.sim.ccpixelpolly` | 10 | | 32 | Implemented |
|
||||
| CC Rainfall | `ec.sim.ccrainfall` | 14 | | 32 | Implemented |
|
||||
| CC Scatterize | `ec.sim.ccscatterize` | 4 | | 32 | Implemented |
|
||||
| CC Snowfall | `ec.sim.ccsnowfall` | 19 | | 32 | Implemented |
|
||||
| CC Star Burst | `ec.sim.ccstarburst` | 6 | | 32 | Implemented |
|
||||
| Card Dance | `ec.sim.carddance` | 35 | | 32 | Partial: no camera system, corner pins or lighting/material controls |
|
||||
| Caustics | `ec.sim.caustics` | 21 | | 32 | Partial: no Sky group or light type |
|
||||
| Foam | `ec.sim.foam` | 24 | | 32 | Partial: no wobble, pop velocity, custom bubble texture, environment map or flow map |
|
||||
| Particle Playground | `ec.sim.particleplayground` | 37 | | 32 | Partial: no Particle Exploder, Ephemeral Property Mapper, Affects groups or text particles |
|
||||
| Shatter | `ec.sim.shatter` | 26 | | 32 | Partial: no custom shatter map, gradient, textures or extrusion rendering |
|
||||
| Wave World | `ec.sim.waveworld` | 27 | | 32 | Partial: no wireframe controls, dry-area rendering or ground group |
|
||||
|
||||
## Stylize
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | | 32 | Implemented |
|
||||
| CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented |
|
||||
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented |
|
||||
| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented |
|
||||
| CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented |
|
||||
| CC Kaleida | `ec.stylize.cckaleida` | 4 | | 32 | Implemented |
|
||||
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented |
|
||||
| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented |
|
||||
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | | 32 | Implemented |
|
||||
| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented |
|
||||
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented |
|
||||
| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented |
|
||||
| Cartoon | `ec.stylize.cartoon` | 11 | | 32 | Partial: no Edge Enhancement |
|
||||
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
|
||||
| Emboss | `ec.stylize.emboss` | 4 | | 32 | Implemented |
|
||||
| Find Edges | `ec.stylize.findedges` | 2 | | 32 | Implemented |
|
||||
| Glow | `ec.stylize.glow` | 13 | GPU | 32 | Partial: no Glow Operation or arbitrary colour map |
|
||||
| Mosaic | `ec.stylize.mosaic` | 3 | | 32 | Implemented |
|
||||
| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented |
|
||||
| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented |
|
||||
| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented |
|
||||
| Scatter | `ec.stylize.scatter` | 3 | | 32 | Implemented |
|
||||
| Strobe Light | `ec.stylize.strobe` | 7 | | 32 | Implemented |
|
||||
| Texturize | `ec.stylize.texturize` | 4 | | 32 | Implemented |
|
||||
| Threshold | `ec.stylize.threshold` | 1 | | 32 | Implemented |
|
||||
|
||||
## Text
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Numbers | `ec.text.numbers` | 14 | | 32 | Implemented |
|
||||
| Timecode | `ec.text.timecode` | 12 | | 32 | Implemented |
|
||||
|
||||
## Time
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented |
|
||||
| CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented |
|
||||
| Echo | `ec.time.echo` | 5 | | 32 | Implemented |
|
||||
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented |
|
||||
| Posterize Time | `ec.time.posterizetime` | 1 | | 32 | Implemented |
|
||||
| Time Difference | `ec.time.timedifference` | 5 | | 32 | Implemented |
|
||||
| Time Displacement | `ec.time.timedisplacement` | 4 | | 32 | Implemented |
|
||||
| Timewarp | `ec.time.timewarp` | 10 | | 32 | Partial: Pixel Motion blends frames (no optical flow); no matte, warp or crop controls |
|
||||
|
||||
## Transition
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Block Dissolve | `ec.transition.blockdissolve` | 4 | | 32 | Partial: no Soft Edges option |
|
||||
| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | | 32 | Implemented |
|
||||
| CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | | 32 | Implemented |
|
||||
| CC Image Wipe | `ec.transition.ccimagewipe` | 7 | | 32 | Implemented |
|
||||
| CC Jaws | `ec.transition.ccjaws` | 6 | | 32 | Implemented |
|
||||
| CC Light Wipe | `ec.transition.cclightwipe` | 8 | | 32 | Implemented |
|
||||
| CC Line Sweep | `ec.transition.cclinesweep` | 5 | | 32 | Implemented |
|
||||
| CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | | 32 | Implemented |
|
||||
| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | | 32 | Implemented |
|
||||
| CC Twister | `ec.transition.cctwister` | 5 | | 32 | Implemented |
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 12 | | 32 | Partial: 2D flip only; no back layer picker, camera, lighting or jitter |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | | 32 | Implemented |
|
||||
| Radial Wipe | `ec.transition.radialwipe` | 5 | | 32 | Implemented |
|
||||
| Venetian Blinds | `ec.transition.venetian` | 4 | | 32 | Implemented |
|
||||
|
||||
## Utility
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Apply Color LUT | `ec.utility.applylut` | 0 | | 32 | Implemented |
|
||||
| CC Overbrights | `ec.utility.ccoverbrights` | 2 | | 32 | Implemented |
|
||||
| Cineon Converter | `ec.utility.cineon` | 7 | | 32 | Implemented |
|
||||
| Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | | 32 | Implemented |
|
||||
| Grow Bounds | `ec.utility.growbounds` | 1 | | 32 | Implemented |
|
||||
| HDR Compander | `ec.utility.hdrcompander` | 3 | | 32 | Implemented |
|
||||
| HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | | 32 | Implemented |
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Text | ≈ 91% | 0.8 | no OpenType feature panel, no extruded strokes, variable-axis animation changes outlines but not advances (vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 85% | 1.0 | viewer frames and Roto Brush propagation still on the page's thread; GPU effects in the browser (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2) |
|
||||
| 3D | 78% | 9.3 | multi-view layouts, Advanced 3D motion blur and blend modes (and iris shapes in Advanced 3D's depth of field), cameras/lights from models; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6 |
|
||||
| Effects | ≈ 75% | 5.0 | GPU versions of more effects (every After Effects effect exists since M9.11, M12.5 and M12.6) |
|
||||
| Effects | ≈ 80% | 4.5 | GPU versions of more effects and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 74% | 5.0 | a richer Learn area (Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 60% | 6.5 | 8/16/32-bit pipeline and colour management, auto-save, folder moves (proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
|
|
@ -131,6 +131,9 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
|
||||
## Effects still missing
|
||||
|
||||
The full catalogue with GPU / 32-bpc badges and per-effect status is [effects.md](effects.md)
|
||||
(generated from the registry).
|
||||
|
||||
None: the 3D Camera Tracker landed in M12.6. Boris FX
|
||||
Mocha and Cineware are third-party and not counted.
|
||||
|
||||
|
|
@ -139,7 +142,7 @@ Mocha and Cineware are third-party and not counted.
|
|||
| Category | Effects | Notes |
|
||||
|---|---|---|
|
||||
| 3D Channel | 3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR, Fog 3D, ID Matte, IDentifier | Read auxiliary channels: multi-layer OpenEXR (any `layer.channel`, Cryptomatte manifests from the header) or the compositor's own depth / layer-ID / normal / UV / Cryptomatte pass for precomps of 3D comps. |
|
||||
| Immersive Video | VR Blur, VR Chromatic Aberrations, VR Color Gradients, VR Converter, VR De-Noise, VR Digital Glitch, VR Fractal Noise, VR Glow, VR Plane to Sphere, VR Rotate Sphere, VR Sharpen, VR Sphere To Plane | Seam-aware equirectangular processing (wrap-around, pole continuation, latitude-scaled filters), mono / over-under / side-by-side layouts; VR Converter between equirectangular, cube maps (4:3, 3:2, 6:1), sphere map and 2D. |
|
||||
| Immersive Video | VR Blur, VR Chromatic Aberrations, VR Color Gradients, VR Converter, VR De-Noise, VR Digital Glitch, VR Fractal Noise, VR Glow, VR Plane to Sphere, VR Rotate Sphere, VR Sharpen, VR Sphere to Plane | Seam-aware equirectangular processing (wrap-around, pole continuation, latitude-scaled filters), mono / over-under / side-by-side layouts; VR Converter between equirectangular, cube maps (4:3, 3:2, 6:1), sphere map and 2D. |
|
||||
| Color Correction | OCIO CDL / Color Space / Display / File / Look Transform, Color Stabilizer | Built-in minimal OCIO-style config (ACES2065-1, ACEScg, ACEScct, sRGB, Rec.709, Rec.2020, Display P3, linear variants, XYZ) from published matrices and transfer functions; `.cube` / `.3dl` / `.csp` LUTs and ASC `.cc` / `.ccc` / `.cdl`. Custom `.ocio` config files are not read yet. |
|
||||
| Distort | CC Flo Motion, Liquify, Rolling Shutter Repair | Liquify strokes are effect data replayed into a displacement mesh (`liquify.stroke` / `liquify.clear` commands; no interactive viewer brush yet). Rolling Shutter Repair uses the KLT tracker. |
|
||||
| Blur & Sharpen | Camera-Shake Deblur, CC Radial Blur | Deblur substitutes aligned patches from sharper neighbouring frames. |
|
||||
|
|
@ -147,7 +150,7 @@ Mocha and Cineware are third-party and not counted.
|
|||
| Simulation | CC Hair, Particle Playground | Particle Playground: cannon, grid, layer exploder, layer map, gravity, repel, wall, persistent property mapper (no Particle Exploder, text particles or ephemeral mapper yet). |
|
||||
| Keying | Key Light | The full Keylight 1.2 control set under a generic name ("Keylight" is a vendor trademark); `lookup("Keylight (1.2)")` finds it. |
|
||||
| Utility | Color Profile Converter | Our colour spaces and ACES; rendering intents (perceptual gamut compression, relative / absolute colorimetric, saturation). |
|
||||
| Obsolete | mocha shape | Mocha's export format is not public: reads a documented JSON shape format instead. |
|
||||
| Matte | Mocha shape | Mocha's export format is not public: reads a documented JSON shape format instead. |
|
||||
|
||||
## Update: M13.5 long-tail polish
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue