M9.12: effect parameter fidelity – all categories
Align every effect's display name, category, parameter names, order, twirl-down groups, popup options, units, defaults and ranges with the public After Effects effect reference, and implement controls that were ignored or missing where cheap. One commit because the categories share the registry, migration tables and GPU kernels (only green states). Infrastructure - effects::migrate: saved instances are upgraded on project open (Session::replace_project → upgrade_effects): renamed ids (PARAM_ID_ALIASES), params moved into twirl-down groups, popup values remapped by label (POPUP_LABEL_ALIASES), refreshed names/UI hints, added params created with legacy_default values that keep old renders. - flatten_params also keys nested props by match path (group/param), so effects declare grouped params with `group/param` ids; layer-source companions sit next to grouped layer params. - lookup() resolves old effect names (EFFECT_NAME_ALIASES). - Registry-wide test: unique ids/names, non-empty names, defaults in range, popup defaults within options, value kinds match UI. - docs/effects.md generated from the registry (catalog.rs) with GPU / 32-bpc badges and per-effect status; a test keeps it current. Batches - Blur & Sharpen, Channel, 3D Channel, Perspective, Text, Utility: Camera Lens Blur iris/highlight groups, Compound Blur layer, Camera- Shake Deblur method/strength, Invert full channel list (CPU HLS/YIQ), Set Channels/Set Matte layers, Channel Combiner targets, 3D Channel Extract clamp/invert. - Color Correction: temporal smoothing + scene detect for Auto Color/ Contrast/Levels and Shadow/Highlight, Brightness & Contrast legacy switch, Change to Color tolerance group, Leave Color, Photo Filter presets, Exposure channels, Levels clip popups, Colorama phase source, Color Link sample list, Lumetri groups (CPU + GPU kernels). - Distort: Bulge, Displacement Map layer/behaviour, Magnify, Optics Compensation resize, Ripple conversion, Transform sampling, Turbulent Displace types/pinning/evolution, Wave Warp types/pinning, Twirl and Ripple radius fixes. - Generate: 4-Color Gradient, Checkerboard, Grid, Circle size/feather/ blending, Advanced Lightning types, Cell Pattern, Radio Waves, Fractal palettes, Scribble/Vegas names. - Keying, Matte, Obsolete: Color Difference Key partials/views, Unmult controls, Key Light groups, Refine Soft/Hard Matte, Roto Brush groups, Mocha shape moved to Matte, Spill Suppressor accuracy, Basic/Path Text groups. - Noise & Grain, Immersive Video, Audio, Time: Fractal/Turbulent Noise full control set (noise3.rs, GPU), grain look groups, Remove Grain, VR effects, Compressor/Gate/Distortion controls, Timewarp groups, VR Sphere to Plane name. - Simulation: AE twirl-down groups for all simulations, Foam strength, Particle Playground barrel radius and mapper targets. - Stylize, Transition: Glow looping/dimensions/alpha (CPU + GPU), Mosaic averaging, Cartoon groups, Roughen Edges evolution, Texturize layer, Gradient Wipe layer, Radial Wipe both, Card Wipe options. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0631ef29dc
commit
355a6255a2
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
pub mod audio_fx;
|
||||
mod blur2;
|
||||
mod blur3;
|
||||
pub mod catalog;
|
||||
mod channel;
|
||||
mod channel2;
|
||||
mod channel3d;
|
||||
|
|
@ -27,10 +28,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;
|
||||
|
|
@ -56,11 +59,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] = &[
|
||||
|
|
@ -150,14 +155,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;
|
||||
|
|
@ -166,13 +177,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 }
|
||||
}
|
||||
|
||||
|
|
@ -367,6 +378,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());
|
||||
|
|
@ -399,10 +411,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
|
||||
|
|
@ -446,19 +464,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.
|
||||
|
|
@ -524,6 +666,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",
|
||||
|
|
@ -594,6 +741,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;
|
||||
|
|
|
|||
|
|
@ -598,6 +598,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() };
|
||||
|
|
@ -611,6 +612,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)]
|
||||
|
|
|
|||
|
|
@ -351,7 +351,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();
|
||||
}
|
||||
}
|
||||
|
|
@ -363,9 +364,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 {
|
||||
|
|
@ -396,7 +401,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", &[]);
|
||||
|
|
|
|||
|
|
@ -697,8 +697,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
|
||||
|
|
@ -740,10 +741,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,420 @@
|
|||
# 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 302 effects with 2416 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**: 302 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 260 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 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 |
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ the disk cache in the formats wave.)
|
|||
| Text | ≈ 86% | 1.3 | vertical type is basic (upright characters, no tate-chu-yoko), no OpenType feature panel, no extruded strokes (extruded, bevelled text landed in M7.6); per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API landed (M9.9–M9.10) |
|
||||
| Web | 60% | 2.5 | threads, browser storage, audio |
|
||||
| 3D | 72% | 12.0 | 3D camera tracker, multi-view layouts, Advanced 3D motion blur and blend modes, cameras/lights from models; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) landed in M7.4–M7.6 |
|
||||
| Effects | ≈ 75% | 5.0 | GPU versions of more effects and the 3D Camera Tracker (every other After Effects effect exists since M9.11 and M12.5) |
|
||||
| Effects | ≈ 80% | 4.5 | GPU versions of more effects, the 3D Camera Tracker, and the missing controls listed as partial in [effects.md](effects.md) (every other After Effects effect exists since M9.11 and M12.5; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 70% | 6.0 | Timeline horizontal scrolling of many columns, a richer Learn area (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 | 53% | 8.1 | 8/16/32-bit pipeline and colour management, auto-save, proxies, folder moves |
|
||||
| Masks & roto | 68% | 6.0 | variable-width mask feather points, Roto Brush's learned (3.0) segmentation model (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) |
|
||||
|
|
@ -106,6 +106,9 @@ the disk cache in the formats wave.)
|
|||
|
||||
## Effects still missing
|
||||
|
||||
The full catalogue with GPU / 32-bpc badges and per-effect status is [effects.md](effects.md)
|
||||
(generated from the registry).
|
||||
|
||||
The 3D Camera Tracker (built on the tracking library by other work). Boris FX
|
||||
Mocha and Cineware are third-party and not counted.
|
||||
|
||||
|
|
@ -114,7 +117,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. |
|
||||
|
|
@ -122,7 +125,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. |
|
||||
|
||||
## Highest-value gaps, in order
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue