Files
effectcraft/crates/effects/src/noise3.rs
T

591 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 }
}
}
/// Fractal / Turbulent Noise resolved for the GPU kernel (effectcraft-gpu `fx_noise`): the
/// per-pixel constants and, per octave, the terms [`Fractal::raw`] computes before touching the
/// pixel, in the same f32 order.
pub struct FractalGpu {
pub kind: u32,
pub noise_type: u32,
pub seed: u32,
pub invert: bool,
pub overflow: u32,
pub contrast: f32,
pub brightness: f32,
/// Rotation (sin, cos).
pub rot: (f32, f32),
pub size: [f32; 2],
/// Cycle Evolution: (t, cycle, t / cycle).
pub cycle: Option<[f32; 3]>,
pub sub_rotation: bool,
/// Σ amplitude × weight (the normaliser).
pub norm: f32,
/// Per octave: origin x, y, sub-rotation sin, cos, frequency, amplitude, weight, evolution
/// (turbulence applied), z offset, Dynamic Progressive warp factor.
pub octaves: Vec<[f32; 10]>,
/// Blending Mode (`None` = the noise alone), Opacity and Blend With Original.
pub mode: Option<BlendMode>,
pub opacity: f32,
pub blend: f32,
}
/// [`FractalGpu`] for a buffer with `offset` / `scale`.
pub fn fractal_gpu(ctx: &EffectCtx, offset: [f64; 2], scale: f64, turbulent: bool) -> FractalGpu {
let b = Buf { img: effectcraft_raster::Image::new(0, 0), offset, scale };
let fr = Fractal::from_params(ctx, &b, turbulent);
let n_oct = fr.octaves.ceil() as usize;
let frac = fr.octaves - fr.octaves.floor();
let (mut norm, mut amp, mut f, mut depth) = (0.0f32, 1.0f32, 1.0f32, 1.0f32);
let mut octaves = Vec::with_capacity(n_oct);
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;
let k = if fr.perspective { depth } else { 1.0 } * (1.0 + fr.turbulence * of);
let mut org = fr.origin;
if k != 1.0 {
org = [fr.center[0] + (fr.origin[0] - fr.center[0]) * k, fr.center[1] + (fr.origin[1] - fr.center[1]) * k];
}
if !fr.center_subscale {
org = [org[0] + fr.sub_offset[0] * of, org[1] + fr.sub_offset[1] * of];
}
let (s, c) = (fr.sub_rotation * of).sin_cos();
let evo = fr.evolution * (1.0 + fr.turbulence * of);
let dk = if fr.kind == 6 { 0.5 + of * 0.35 } else { 1.0 };
octaves.push([org[0], org[1], s, c, f, amp, w, evo, of * 7.31, dk]);
norm += amp * w;
amp *= fr.influence;
f /= fr.sub_scaling;
depth *= fr.sub_scaling;
}
let cycle = fr.cycle.map(|c| {
let t = fr.evolution.rem_euclid(c);
[t, c, t / c]
});
FractalGpu {
kind: fr.kind,
noise_type: fr.noise_type,
seed: fr.seed,
invert: fr.invert,
overflow: fr.overflow,
contrast: fr.contrast,
brightness: fr.brightness,
rot: fr.rot,
size: fr.size,
cycle,
sub_rotation: fr.sub_rotation != 0.0,
norm,
octaves,
mode: match ctx.params.get("blendingMode").map(Value::as_enum).unwrap_or(BLEND_NONE) {
BLEND_NONE => None,
m => Some(BLEND_MODES.get(m as usize).and_then(|l| BlendMode::from_name(l)).unwrap_or(BlendMode::Normal)),
},
opacity: (ctx.params.f("opacity") as f32 / 100.0).clamp(0.0, 1.0),
blend: ctx.params.f("blend") as f32 / 100.0,
}
}
/// 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(4, 4), a.get(5, 5));
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);
}
}