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