//! Generate and Noise effects that synthesise images. use rayon::prelude::*; use effectcraft_keyframe::Value; use effectcraft_project::ParamUi; use effectcraft_raster::Px; use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider}; fn spec(id: &'static str, name: &'static str, category: &'static str, params: Vec, render: crate::RenderFn) -> EffectSpec { EffectSpec { id, name, category, params, render, gpu: false, float: true } } /// Blend a generated straight colour `g` (with alpha `ga`) over the original pixel. fn put(px: &mut Px, g: [f32; 3], ga: f32, blend_orig: f32) { let g2 = [g[0] * ga, g[1] * ga, g[2] * ga, ga]; let k = 1.0 - blend_orig; for c in 0..4 { px[c] = px[c] * blend_orig + g2[c] * k; } } fn gradient_ramp(ctx: &EffectCtx, mut b: Buf) -> Buf { let s = b.to_px(ctx.params.v2("start")); let e = b.to_px(ctx.params.v2("end")); let c0 = ctx.params.color("startColor"); let c1 = ctx.params.color("endColor"); let radial = ctx.params.e("shape") == 1; let blend = ctx.params.f("blend") as f32 / 100.0; let scatter = ctx.params.f("scatter") as f32 / 512.0; let (dx, dy) = (e.0 - s.0, e.1 - s.1); let len2 = (dx * dx + dy * dy).max(1e-9); let seed = ctx.seed; b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { let (vx, vy) = (x as f64 + 0.5 - s.0, y as f64 + 0.5 - s.1); let mut t = if radial { ((vx * vx + vy * vy) / len2).sqrt() } else { (vx * dx + vy * dy) / len2 } as f32; if scatter > 0.0 { t += (effectcraft_raster::hash_noise(x as u32, y as u32, seed) - 0.5) * scatter; } let t = t.clamp(0.0, 1.0); let c = [c0[0] + (c1[0] - c0[0]) * t, c0[1] + (c1[1] - c0[1]) * t, c0[2] + (c1[2] - c0[2]) * t]; put(px, c, 1.0, blend); } }); b } fn four_color(ctx: &EffectCtx, mut b: Buf) -> Buf { let pts: Vec<((f64, f64), [f32; 4])> = (1..=4).map(|i| (b.to_px(ctx.params.v2(&format!("point{i}"))), ctx.params.color(&format!("color{i}")))).collect(); let blend = ctx.params.f("blend").max(1.0); let op = ctx.params.f("opacity") as f32 / 100.0; b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { let mut acc = [0.0f64; 3]; let mut wsum = 0.0; for ((qx, qy), c) in &pts { let d2 = (x as f64 + 0.5 - qx).powi(2) + (y as f64 + 0.5 - qy).powi(2); let w = 1.0 / (d2 / (blend * 100.0) + 1e-6).powf(1.5); for i in 0..3 { acc[i] += c[i] as f64 * w; } wsum += w; } let c = acc.map(|v| (v / wsum) as f32); put(px, c, 1.0, 1.0 - op); } }); b } fn checkerboard(ctx: &EffectCtx, mut b: Buf) -> Buf { let anchor = b.to_px(ctx.params.v2("anchor")); let w = (ctx.params.f("width") * b.scale).max(1.0); let h = (ctx.params.f("height") * b.scale).max(1.0); let c = ctx.params.color("color"); let op = ctx.params.f("opacity") as f32 / 100.0; b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { let cx = ((x as f64 + 0.5 - anchor.0) / w).floor() as i64; let cy = ((y as f64 + 0.5 - anchor.1) / h).floor() as i64; if (cx + cy).rem_euclid(2) == 0 { put(px, [c[0], c[1], c[2]], op, 0.0); } } }); b } fn grid(ctx: &EffectCtx, mut b: Buf) -> Buf { let anchor = b.to_px(ctx.params.v2("anchor")); let w = (ctx.params.f("width") * b.scale).max(1.0); let h = (ctx.params.f("height") * b.scale).max(1.0); let border = (ctx.params.f("border") * b.scale).max(0.0); let c = ctx.params.color("color"); let op = ctx.params.f("opacity") as f32 / 100.0; b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { let fx = (x as f64 + 0.5 - anchor.0).rem_euclid(w); let fy = (y as f64 + 0.5 - anchor.1).rem_euclid(h); let d = fx.min(w - fx).min(fy.min(h - fy)); let cov = ((border / 2.0 - d) + 0.5).clamp(0.0, 1.0) as f32; if cov > 0.0 { let mut g = *px; put(&mut g, [c[0], c[1], c[2]], op, 0.0); for i in 0..4 { px[i] += (g[i] - px[i]) * cov; } } } }); b } fn circle(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = b.to_px(ctx.params.v2("center")); let r = ctx.params.f("radius") * b.scale; let feather = (ctx.params.f("feather") * b.scale).max(0.5); let color = ctx.params.color("color"); let op = ctx.params.f("opacity") as f32 / 100.0; let invert = ctx.params.b("invert"); b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { let d = ((x as f64 + 0.5 - c.0).powi(2) + (y as f64 + 0.5 - c.1).powi(2)).sqrt(); let mut cov = ((r - d) / feather + 0.5).clamp(0.0, 1.0) as f32; if invert { cov = 1.0 - cov; } if cov > 0.0 { let mut g = *px; put(&mut g, [color[0], color[1], color[2]], op, 0.0); for i in 0..4 { px[i] += (g[i] - px[i]) * cov; } } } }); b } // ---- value noise / fBm (our own lattice noise with quintic fade) ---- #[inline] fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 { let mut h = (ix as u32).wrapping_mul(0x27d4_eb2d) ^ (iy as u32).wrapping_mul(0x1656_67b1) ^ (iz as u32).wrapping_mul(0x9e37_79b9) ^ seed.wrapping_mul(0x85eb_ca6b); h ^= h >> 15; h = h.wrapping_mul(0x2c1b_3c6d); h ^= h >> 12; h = h.wrapping_mul(0x297a_2d39); h ^= h >> 15; (h & 0xffff) as f32 / 65535.0 } #[inline] fn fade(t: f32) -> f32 { t * t * t * (t * (t * 6.0 - 15.0) + 10.0) } /// 3D value noise in 0..1 (z = evolution). pub fn value_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 { let (x0, y0, z0) = (x.floor(), y.floor(), z.floor()); let (fx, fy, fz) = (fade(x - x0), fade(y - y0), fade(z - z0)); let (ix, iy, iz) = (x0 as i32, y0 as i32, z0 as i32); let l = |a: f32, b: f32, t: f32| a + (b - a) * t; let c = |dx: i32, dy: i32, dz: i32| lattice(ix + dx, iy + dy, iz + dz, seed); let a = l(l(c(0, 0, 0), c(1, 0, 0), fx), l(c(0, 1, 0), c(1, 1, 0), fx), fy); let b = l(l(c(0, 0, 1), c(1, 0, 1), fx), l(c(0, 1, 1), c(1, 1, 1), fx), fy); l(a, b, fz) } fn fractal_noise(ctx: &EffectCtx, mut b: Buf) -> Buf { let kind = ctx.params.e("fractalType"); let contrast = ctx.params.f("contrast") as f32 / 100.0; let brightness = ctx.params.f("brightness") as f32 / 100.0; let invert = ctx.params.b("invert"); let scale = (ctx.params.f("scale") * b.scale).max(1.0) as f32; let off = ctx.params.v2("offset"); let rot = (ctx.params.f("rotation") as f32).to_radians(); let octaves = ctx.params.f("complexity").clamp(1.0, 20.0); let evo = ctx.params.f("evolution") as f32 / 360.0; let seed = ctx.params.f("seed") as u32 ^ 0x51ed; let blend = ctx.params.f("blend") as f32 / 100.0; let opacity = ctx.params.f("opacity") as f32 / 100.0; let (sr, cr) = rot.sin_cos(); let n_oct = octaves.ceil() as usize; let frac = (octaves - octaves.floor()) as f32; let (ox, oy) = b.to_px(off); b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { let (dx, dy) = (x as f32 + 0.5 - ox as f32, y as f32 + 0.5 - oy as f32); let (u, v) = ((dx * cr + dy * sr) / scale, (-dx * sr + dy * cr) / scale); let mut sum = 0.0; let mut amp = 1.0; let mut norm = 0.0; let mut f = 1.0; for o in 0..n_oct { let w = if o + 1 == n_oct && frac > 0.0 { frac } else { 1.0 }; let mut n = value_noise(u * f, v * f, evo + o as f32 * 7.31, seed + o as u32); if kind == 1 { n = 1.0 - (n * 2.0 - 1.0).abs(); // turbulent / ridged } sum += n * amp * w; norm += amp * w; amp *= 0.5; f *= 2.0; } let mut val = sum / norm.max(1e-6); val = ((val - 0.5) * contrast + 0.5 + brightness).clamp(0.0, 1.0); if invert { val = 1.0 - val; } put(px, [val, val, val], opacity, blend); } }); b } pub fn specs() -> Vec { let pt = |x, y| Value::Vec2([x, y]); vec![ spec( "ec.generate.gradientramp", "Gradient Ramp", "Generate", vec![ p("start", "Start of Ramp", pt(0.5, 0.0), ParamUi::Point), p("startColor", "Start Color", col(0.0, 0.0, 0.0), ParamUi::Color), p("end", "End of Ramp", pt(0.5, 1.0), ParamUi::Point), p("endColor", "End Color", col(1.0, 1.0, 1.0), ParamUi::Color), p("shape", "Ramp Shape", Value::Enum(0), popup(&["Linear Ramp", "Radial Ramp"])), p("scatter", "Ramp Scatter", num(0.0), slider(0.0, 512.0, 0.0, 512.0, 1)), p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), ], gradient_ramp, ), spec( "ec.generate.fourcolor", "4-Color Gradient", "Generate", vec![ p("point1", "Point 1", pt(0.1, 0.1), ParamUi::Point), p("color1", "Color 1", col(1.0, 1.0, 0.0), ParamUi::Color), p("point2", "Point 2", pt(0.9, 0.1), ParamUi::Point), p("color2", "Color 2", col(0.0, 1.0, 0.0), ParamUi::Color), p("point3", "Point 3", pt(0.1, 0.9), ParamUi::Point), p("color3", "Color 3", col(1.0, 0.0, 1.0), ParamUi::Color), p("point4", "Point 4", pt(0.9, 0.9), ParamUi::Point), p("color4", "Color 4", col(0.0, 0.0, 1.0), ParamUi::Color), p("blend", "Blend", num(100.0), slider(1.0, 1000.0, 1.0, 1000.0, 1)), p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)), ], four_color, ), spec( "ec.generate.checkerboard", "Checkerboard", "Generate", vec![ p("anchor", "Anchor", pt(0.0, 0.0), ParamUi::Point), p("width", "Width", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)), p("height", "Height", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)), p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color), p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)), ], checkerboard, ), spec( "ec.generate.grid", "Grid", "Generate", vec![ p("anchor", "Anchor", pt(0.0, 0.0), ParamUi::Point), p("width", "Width", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)), p("height", "Height", num(64.0), slider(1.0, 4000.0, 1.0, 400.0, 1)), p("border", "Border", num(2.0), slider(0.0, 400.0, 0.0, 40.0, 1)), p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color), p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)), ], grid, ), spec( "ec.generate.circle", "Circle", "Generate", vec![ p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("radius", "Radius", num(75.0), slider(0.0, 4000.0, 0.0, 1000.0, 1)), p("feather", "Feather Outer Edge", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)), p("invert", "Invert Circle", Value::Bool(false), ParamUi::Checkbox), p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color), p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)), ], circle, ), spec( "ec.noise.fractal", "Fractal Noise", "Noise & Grain", vec![ p("fractalType", "Fractal Type", Value::Enum(0), popup(&["Basic", "Turbulent Smooth"])), p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox), p("contrast", "Contrast", num(100.0), slider(0.0, 10000.0, 0.0, 400.0, 1)), p("brightness", "Brightness", num(0.0), slider(-10000.0, 10000.0, -200.0, 200.0, 1)), p("scale", "Scale", num(100.0), slider(1.0, 10000.0, 20.0, 600.0, 1)), p("offset", "Offset Turbulence", pt(0.5, 0.5), ParamUi::Point), p("rotation", "Rotation", num(0.0), ParamUi::Angle), p("complexity", "Complexity", num(6.0), slider(1.0, 20.0, 1.0, 10.0, 1)), p("evolution", "Evolution", num(0.0), ParamUi::Angle), p("seed", "Random Seed", num(0.0), slider(0.0, 100000.0, 0.0, 1000.0, 0)), p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)), p("blend", "Blend With Original", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), ], fractal_noise, ), ] }