mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 03:57:44 -06:00
refactor(effects): split generate3.rs into generate3/*.rs
This commit is contained in:
+20
-2020
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,314 @@
|
||||
use super::raster::{Seg, mask_px, over, poly_segs, raster_segs};
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------- Audio Spectrum / Waveform
|
||||
|
||||
/// In-place iterative radix-2 FFT (`re`, `im` lengths a power of two).
|
||||
pub(crate) fn fft(re: &mut [f64], im: &mut [f64]) {
|
||||
let n = re.len();
|
||||
if n < 2 {
|
||||
return;
|
||||
}
|
||||
let mut j = 0;
|
||||
for i in 1..n {
|
||||
let mut bit = n >> 1;
|
||||
while j & bit != 0 {
|
||||
j ^= bit;
|
||||
bit >>= 1;
|
||||
}
|
||||
j |= bit;
|
||||
if i < j {
|
||||
re.swap(i, j);
|
||||
im.swap(i, j);
|
||||
}
|
||||
}
|
||||
let mut len = 2;
|
||||
while len <= n {
|
||||
let ang = -2.0 * PI / len as f64;
|
||||
let (ws, wc) = ang.sin_cos();
|
||||
for start in (0..n).step_by(len) {
|
||||
let (mut cr, mut ci) = (1.0, 0.0);
|
||||
for k in 0..len / 2 {
|
||||
let (a, bb) = (start + k, start + k + len / 2);
|
||||
let tr = re[bb] * cr - im[bb] * ci;
|
||||
let ti = re[bb] * ci + im[bb] * cr;
|
||||
re[bb] = re[a] - tr;
|
||||
im[bb] = im[a] - ti;
|
||||
re[a] += tr;
|
||||
im[a] += ti;
|
||||
let ncr = cr * wc - ci * ws;
|
||||
ci = cr * ws + ci * wc;
|
||||
cr = ncr;
|
||||
}
|
||||
}
|
||||
len <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
const AUDIO_RATE: u32 = 48_000;
|
||||
|
||||
/// Fetch mono (or one channel) samples of the audio layer around the current time.
|
||||
fn audio_window(ctx: &EffectCtx, dur_ms: f64, offset_ms: f64, channel: u32) -> Option<Vec<f64>> {
|
||||
let id = ctx.params.get("audioLayer")?.as_layer()?;
|
||||
let host = ctx.env.host?;
|
||||
let dur = (dur_ms / 1000.0).max(0.001);
|
||||
let frames = ((dur * AUDIO_RATE as f64) as usize).clamp(16, 1 << 16);
|
||||
let start = ctx.env.comp_time + offset_ms / 1000.0 - dur * 0.5;
|
||||
let s = host.audio(id, start, frames, AUDIO_RATE)?;
|
||||
Some(
|
||||
(0..frames)
|
||||
.map(|i| {
|
||||
let (l, r) = (*s.get(2 * i).unwrap_or(&0.0) as f64, *s.get(2 * i + 1).unwrap_or(&0.0) as f64);
|
||||
match channel {
|
||||
1 => l,
|
||||
2 => r,
|
||||
_ => (l + r) * 0.5,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The drawing baseline: a polyline in buffer px (mask path or start→end line), sampled at
|
||||
/// parameter u∈[0,1] → (point, unit normal).
|
||||
struct Baseline {
|
||||
pts: Vec<[f64; 2]>,
|
||||
closed: bool,
|
||||
len: f64,
|
||||
polar: bool,
|
||||
center: [f64; 2],
|
||||
radius: f64,
|
||||
}
|
||||
|
||||
impl Baseline {
|
||||
fn new(ctx: &EffectCtx, b: &Buf) -> Baseline {
|
||||
let pr = ctx.params;
|
||||
let polar = pr.b("usePolarPath");
|
||||
let (pts, closed) = match mask_px(ctx, b, pr.f("path").round() as usize) {
|
||||
Some((p, c, _)) if !polar => (p, c),
|
||||
_ => {
|
||||
let s = b.to_px(pr.v2("startPoint"));
|
||||
let e = b.to_px(pr.v2("endPoint"));
|
||||
(vec![[s.0, s.1], [e.0, e.1]], false)
|
||||
}
|
||||
};
|
||||
let len = poly_length(&pts, closed);
|
||||
let center = pts[0];
|
||||
let radius = if pts.len() > 1 { ((pts[1][0] - pts[0][0]).powi(2) + (pts[1][1] - pts[0][1]).powi(2)).sqrt() } else { 1.0 };
|
||||
Baseline { pts, closed, len, polar, center, radius }
|
||||
}
|
||||
fn at(&self, u: f64) -> ([f64; 2], [f64; 2]) {
|
||||
if self.polar {
|
||||
let a = u * 2.0 * PI - PI * 0.5;
|
||||
let (s, c) = a.sin_cos();
|
||||
return ([self.center[0] + c * self.radius * 0.5, self.center[1] + s * self.radius * 0.5], [c, s]);
|
||||
}
|
||||
let (q, d) = crate::util::poly_point_at(&self.pts, self.closed, u * self.len);
|
||||
(q, [d[1], -d[0]])
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw line/dot marks with core (inside colour) and soft halo (outside colour).
|
||||
fn draw_marks(b: &mut Buf, segs_core: &[Seg], thickness: f64, softness: f64, inside: [f32; 4], outside: [f32; 4], composite: bool) {
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let core = raster_segs(w, h, segs_core, 1.0);
|
||||
let halo_segs: Vec<Seg> = segs_core.iter().map(|s| Seg { r: s.r + thickness * softness.max(0.0) + 0.5, ..*s }).collect();
|
||||
let halo = if softness > 0.0 { raster_segs(w, h, &halo_segs, 0.0) } else { core.clone() };
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (c, hl) = (core.data[i], halo.data[i]);
|
||||
let a = c.max(hl);
|
||||
let base = if composite { *px } else { [0.0; 4] };
|
||||
if a <= 0.0 {
|
||||
*px = base;
|
||||
return;
|
||||
}
|
||||
let t = if a > 0.0 { c / a } else { 0.0 };
|
||||
let col = crate::util::lerp3([outside[0], outside[1], outside[2]], [inside[0], inside[1], inside[2]], t);
|
||||
*px = over(base, premul(col, a));
|
||||
});
|
||||
}
|
||||
|
||||
/// Audio Spectrum / Audio Waveform, shared with the GPU compositor (effectcraft-gpu `fx_gen2`):
|
||||
/// the marks (the audio analysis runs on the CPU) and how `draw_marks` colours them.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MarksPlan {
|
||||
pub segs: Vec<Seg>,
|
||||
pub thickness: f64,
|
||||
pub softness: f64,
|
||||
pub inside: [f32; 4],
|
||||
pub outside: [f32; 4],
|
||||
pub composite: bool,
|
||||
}
|
||||
|
||||
/// [`MarksPlan`] of Audio Spectrum or Audio Waveform on `b`'s geometry (`None` = no audio:
|
||||
/// the layer passes through).
|
||||
pub fn marks_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<MarksPlan> {
|
||||
match id {
|
||||
"ec.generate.audiospectrum" => spectrum_plan(ctx, b),
|
||||
"ec.generate.audiowaveform" => waveform_plan(ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn marks(ctx: &EffectCtx, segs: Vec<Seg>, thick: f64, soft: f64) -> MarksPlan {
|
||||
let pr = ctx.params;
|
||||
MarksPlan {
|
||||
segs,
|
||||
thickness: thick,
|
||||
softness: soft * 2.0,
|
||||
inside: pr.color("insideColor"),
|
||||
outside: pr.color("outsideColor"),
|
||||
composite: pr.b("compositeOnOriginal"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn audio_spectrum(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
if let Some(m) = spectrum_plan(ctx, &b) {
|
||||
draw_marks(&mut b, &m.segs, m.thickness, m.softness, m.inside, m.outside, m.composite);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
pub(super) fn audio_waveform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
if let Some(m) = waveform_plan(ctx, &b) {
|
||||
draw_marks(&mut b, &m.segs, m.thickness, m.softness, m.inside, m.outside, m.composite);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn compute_fft_mags(samples: &[f64]) -> (usize, f64, Vec<f64>) {
|
||||
let m = samples.len();
|
||||
let n = m.next_power_of_two().max(64);
|
||||
let mut re = vec![0.0; n];
|
||||
let mut im = vec![0.0; n];
|
||||
let denom = (m.max(2) - 1) as f64;
|
||||
for (i, s) in samples.iter().enumerate() {
|
||||
let w = 0.5 - 0.5 * (2.0 * PI * i as f64 / denom).cos();
|
||||
if let Some(r) = re.get_mut(i) {
|
||||
*r = s * w;
|
||||
}
|
||||
}
|
||||
fft(&mut re, &mut im);
|
||||
let hz_per_bin = AUDIO_RATE as f64 / n as f64;
|
||||
let scale = 4.0 / m.max(1) as f64;
|
||||
let num_bins = n / 2 + 1;
|
||||
let mut mags = Vec::with_capacity(num_bins);
|
||||
for k in 0..num_bins {
|
||||
let r = re.get(k).copied().unwrap_or(0.0);
|
||||
let im_val = im.get(k).copied().unwrap_or(0.0);
|
||||
let mag = (r * r + im_val * im_val).sqrt() * scale;
|
||||
mags.push(mag);
|
||||
}
|
||||
(n, hz_per_bin, mags)
|
||||
}
|
||||
|
||||
fn spectrum_plan(ctx: &EffectCtx, b: &Buf) -> Option<MarksPlan> {
|
||||
let pr = ctx.params;
|
||||
let samples = audio_window(ctx, pr.f("audioDuration"), pr.f("audioOffset"), 0)?;
|
||||
let (_n, hz_per_bin, mags) = compute_fft_mags(&samples);
|
||||
|
||||
let bands = pr.f("frequencyBands").round().clamp(1.0, 4096.0) as usize;
|
||||
let f0 = pr.f("startFrequency").max(0.0);
|
||||
let f1 = pr.f("endFrequency").max(f0 + 1.0);
|
||||
let max_bin = mags.len().saturating_sub(1);
|
||||
|
||||
let get_band_mag = |fc: f64| -> f64 {
|
||||
if mags.is_empty() || hz_per_bin <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let k_float = (fc / hz_per_bin).clamp(0.0, max_bin as f64);
|
||||
let k0 = (k_float.floor() as usize).min(max_bin.saturating_sub(1));
|
||||
let k1 = (k0 + 1).min(max_bin);
|
||||
let frac = (k_float - k0 as f64).clamp(0.0, 1.0);
|
||||
let m0 = mags.get(k0).copied().unwrap_or(0.0);
|
||||
let m1 = mags.get(k1).copied().unwrap_or(0.0);
|
||||
m0 * (1.0 - frac) + m1 * frac
|
||||
};
|
||||
|
||||
let max_h = pr.f("maximumHeight") * b.scale;
|
||||
let thick = (pr.f("thickness") * b.scale).max(0.1);
|
||||
let soft = pr.f("softness") / 100.0;
|
||||
let disp = pr.e("displayOptions");
|
||||
let side = pr.e("sideOptions");
|
||||
let base = Baseline::new(ctx, b);
|
||||
let mut tops = Vec::with_capacity(bands);
|
||||
let mut segs = Vec::new();
|
||||
for i in 0..bands {
|
||||
let u = if bands > 1 { i as f64 / (bands - 1) as f64 } else { 0.5 };
|
||||
let fc = f0 + (f1 - f0) * u;
|
||||
let v = get_band_mag(fc);
|
||||
let hgt = if (v * max_h).is_finite() { (v * max_h).clamp(0.0, max_h.max(0.0)) } else { 0.0 };
|
||||
let (q, nrm) = base.at(u);
|
||||
let (ua, ub) = match side {
|
||||
0 => (0.0, hgt),
|
||||
1 => (-hgt, 0.0),
|
||||
_ => (-hgt, hgt),
|
||||
};
|
||||
let pa = [q[0] + nrm[0] * ua, q[1] + nrm[1] * ua];
|
||||
let pb = [q[0] + nrm[0] * ub, q[1] + nrm[1] * ub];
|
||||
match disp {
|
||||
0 => segs.push(Seg { a: pa, b: pb, r: thick * 0.5, v: 1.0 }),
|
||||
_ => tops.push((pa, pb)),
|
||||
}
|
||||
}
|
||||
if disp == 1 {
|
||||
for w2 in tops.windows(2) {
|
||||
segs.push(Seg { a: w2[0].1, b: w2[1].1, r: thick * 0.5, v: 1.0 });
|
||||
if side == 2 {
|
||||
segs.push(Seg { a: w2[0].0, b: w2[1].0, r: thick * 0.5, v: 1.0 });
|
||||
}
|
||||
}
|
||||
} else if disp == 2 {
|
||||
for &(pa, pb) in &tops {
|
||||
segs.push(Seg { a: pb, b: pb, r: thick * 0.5, v: 1.0 });
|
||||
if side == 2 {
|
||||
segs.push(Seg { a: pa, b: pa, r: thick * 0.5, v: 1.0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(marks(ctx, segs, thick, soft))
|
||||
}
|
||||
|
||||
fn waveform_plan(ctx: &EffectCtx, b: &Buf) -> Option<MarksPlan> {
|
||||
let pr = ctx.params;
|
||||
let samples = audio_window(ctx, pr.f("audioDuration"), pr.f("audioOffset"), pr.e("waveformOptions"))?;
|
||||
let count = pr.f("displayedSamples").round().clamp(2.0, 4096.0) as usize;
|
||||
let max_h = pr.f("maximumHeight") * b.scale;
|
||||
let thick = (pr.f("thickness") * b.scale).max(0.1);
|
||||
let soft = pr.f("softness") / 100.0;
|
||||
let disp = pr.e("displayOptions");
|
||||
let base = Baseline::new(ctx, b);
|
||||
let m = samples.len();
|
||||
let mut pts = Vec::with_capacity(count);
|
||||
let mut segs = Vec::new();
|
||||
for i in 0..count {
|
||||
let u = i as f64 / (count - 1) as f64;
|
||||
let a = ((u * (m - 1) as f64) as usize).min(m - 1);
|
||||
let bnd = (((i + 1) as f64 / count as f64 * (m - 1) as f64) as usize).clamp(a, m - 1);
|
||||
// Peak with sign of the window for a stable digital display.
|
||||
let (mut lo, mut hi) = (0.0f64, 0.0f64);
|
||||
for s in &samples[a..=bnd] {
|
||||
lo = lo.min(*s);
|
||||
hi = hi.max(*s);
|
||||
}
|
||||
let (q, nrm) = base.at(u);
|
||||
match disp {
|
||||
0 => segs.push(Seg {
|
||||
a: [q[0] + nrm[0] * lo * max_h, q[1] + nrm[1] * lo * max_h],
|
||||
b: [q[0] + nrm[0] * hi * max_h, q[1] + nrm[1] * hi * max_h],
|
||||
r: thick * 0.5,
|
||||
v: 1.0,
|
||||
}),
|
||||
_ => {
|
||||
let v = samples[a] * max_h;
|
||||
pts.push([q[0] + nrm[0] * v, q[1] + nrm[1] * v]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if disp == 1 {
|
||||
poly_segs(&pts, false, thick * 0.5, 1.0, &mut segs);
|
||||
} else if disp == 2 {
|
||||
segs.extend(pts.iter().map(|&q| Seg { a: q, b: q, r: thick * 0.5, v: 1.0 }));
|
||||
}
|
||||
Some(marks(ctx, segs, thick, soft))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Generate", params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
pub(super) fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
pub(super) fn pct() -> ParamUi {
|
||||
slider(0.0, 100.0, 0.0, 100.0, 1)
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
use super::raster::over;
|
||||
use super::*;
|
||||
|
||||
pub(super) fn glue_gun(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let q = b.to_px(pr.v2("brushPosition"));
|
||||
let rad = (pr.f("strokeWidth") * b.scale * 0.5).max(0.5);
|
||||
let density = pr.f("density").max(0.0);
|
||||
let refl = pr.f("reflection").to_radians();
|
||||
let strength = (pr.f("strength") / 100.0) as f32;
|
||||
let glue_only = pr.e("style") == 1;
|
||||
let src = b.img.clone();
|
||||
let light = [refl.cos() * 0.6, -refl.sin() * 0.6, 0.53];
|
||||
let beads = (density.round() as usize).clamp(1, 32);
|
||||
let seed = ctx.seed;
|
||||
let centers: Vec<([f64; 2], f64)> = (0..beads)
|
||||
.map(|i| {
|
||||
let a = hash1(i as u32, 1, seed) as f64 * 2.0 * PI;
|
||||
let d = if i == 0 { 0.0 } else { hash1(i as u32, 2, seed) as f64 * rad * 0.35 };
|
||||
([q.0 + a.cos() * d, q.1 + a.sin() * d], rad * (0.75 + 0.25 * hash1(i as u32, 3, seed) as f64))
|
||||
})
|
||||
.collect();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (fx, fy) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
// Height field: union of hemispheres.
|
||||
let mut best = (0.0f64, [0.0f64; 2], 1.0f64);
|
||||
for &(c, r) in ¢ers {
|
||||
let (dx, dy) = ((fx - c[0]) / r, (fy - c[1]) / r);
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if d2 < 1.0 {
|
||||
let hgt = (1.0 - d2).sqrt();
|
||||
if hgt > best.0 {
|
||||
best = (hgt, [dx, dy], r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if best.0 <= 0.0 {
|
||||
if glue_only {
|
||||
*px = [0.0; 4];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let nrm = [best.1[0], best.1[1], best.0];
|
||||
let ndl = (nrm[0] * light[0] + nrm[1] * light[1] + nrm[2] * light[2]).max(0.0);
|
||||
let spec = ndl.powi(24) as f32;
|
||||
// Refraction: look through the bead.
|
||||
let s = src.sample_bilinear_clamped(fx - nrm[0] * best.2 * 0.5 * strength as f64, fy - nrm[1] * best.2 * 0.5 * strength as f64);
|
||||
let edge = smoothstep(0.0, 0.15, best.0 as f32);
|
||||
let shade = 0.65 + 0.35 * ndl as f32;
|
||||
let g = [s[0] * shade + spec, s[1] * shade + spec, s[2] * shade + spec, s[3].max(0.6)];
|
||||
let gp = [g[0].min(g[3] + spec), g[1].min(g[3] + spec), g[2].min(g[3] + spec), g[3].min(1.0)];
|
||||
let base = if glue_only { [0.0; 4] } else { *px };
|
||||
*px = crate::util::lerp4(base, gp, edge * strength.clamp(0.0, 1.0).max(0.3));
|
||||
px[3] = px[3].clamp(0.0, 1.0);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Paint Bucket
|
||||
|
||||
/// Paint Bucket's filled region (1 inside) on `b`: the flood fill from the Fill Point over the
|
||||
/// layer's pixels (shared with the GPU compositor, which reads the layer back for it).
|
||||
pub fn paint_bucket_region(ctx: &EffectCtx, b: &Buf) -> Plane {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let fp = b.to_px(pr.v2("fillPoint"));
|
||||
let (sx, sy) = (fp.0.floor() as i64, fp.1.floor() as i64);
|
||||
let sel = pr.e("fillSelector");
|
||||
let tol = (pr.f("tolerance") / 255.0) as f32;
|
||||
let key = |p: Px| -> [f32; 4] {
|
||||
let (c, a) = unpremul(p);
|
||||
match sel {
|
||||
1 => [c[0], c[1], c[2], 0.0],
|
||||
2 => [0.0, 0.0, 0.0, if a <= 1e-3 { 0.0 } else { 1.0 }],
|
||||
3 | 4 => [0.0, 0.0, 0.0, a],
|
||||
_ => p,
|
||||
}
|
||||
};
|
||||
let mut region = Plane::new(w, h);
|
||||
if sx >= 0 && sy >= 0 && (sx as usize) < w && (sy as usize) < h {
|
||||
let seed = key(b.img.data[sy as usize * w + sx as usize]);
|
||||
let within = |i: usize| {
|
||||
let k = key(b.img.data[i]);
|
||||
if sel == 2 {
|
||||
return k[3] == seed[3];
|
||||
}
|
||||
(0..4).all(|j| (k[j] - seed[j]).abs() <= tol + 1e-6)
|
||||
};
|
||||
let mut seen = vec![false; w * h];
|
||||
let mut stack = vec![(sx as usize, sy as usize)];
|
||||
while let Some((x, y)) = stack.pop() {
|
||||
let row = y * w;
|
||||
if seen[row + x] || !within(row + x) {
|
||||
continue;
|
||||
}
|
||||
let mut l = x;
|
||||
while l > 0 && !seen[row + l - 1] && within(row + l - 1) {
|
||||
l -= 1;
|
||||
}
|
||||
let mut r = x;
|
||||
while r + 1 < w && !seen[row + r + 1] && within(row + r + 1) {
|
||||
r += 1;
|
||||
}
|
||||
for i in l..=r {
|
||||
seen[row + i] = true;
|
||||
region.data[row + i] = 1.0;
|
||||
if y > 0 && !seen[row - w + i] {
|
||||
stack.push((i, y - 1));
|
||||
}
|
||||
if y + 1 < h && !seen[row + w + i] {
|
||||
stack.push((i, y + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
region
|
||||
}
|
||||
|
||||
pub(super) fn paint_bucket(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
if w == 0 || h == 0 {
|
||||
return b;
|
||||
}
|
||||
let region = paint_bucket_region(ctx, &b);
|
||||
if pr.b("viewThreshold") {
|
||||
b.img.data.par_iter_mut().zip(region.data.par_iter()).for_each(|(px, &v)| *px = [v, v, v, 1.0]);
|
||||
return b;
|
||||
}
|
||||
let sc = b.scale;
|
||||
let mut cov = match pr.e("stroke") {
|
||||
1 => {
|
||||
let s = pr.f("featherSoftness") * sc * 0.5;
|
||||
if s > 0.05 { gauss_plane(®ion, s, s) } else { region.clone() }
|
||||
}
|
||||
2 => morph_frac(®ion, pr.f("spreadRadius") * sc, true),
|
||||
3 => morph_frac(®ion, pr.f("spreadRadius") * sc, false),
|
||||
4 => {
|
||||
let r = pr.f("strokeWidth") * sc;
|
||||
let out = morph_frac(®ion, r * 0.5, true);
|
||||
let inn = morph_frac(®ion, r * 0.5, false);
|
||||
out.zip_map(&inn, |a, b| (a - b).max(0.0))
|
||||
}
|
||||
_ => gauss_plane(®ion, 0.5, 0.5),
|
||||
};
|
||||
if pr.e("stroke") == 0 {
|
||||
// Antialias: keep the hard interior, soften only the boundary.
|
||||
cov = cov.zip_map(®ion, |a, r| if r > 0.0 { a.max(0.5) } else { a });
|
||||
}
|
||||
let color = pr.color("color");
|
||||
let op = (pr.f("opacity") / 100.0) as f32;
|
||||
let mode = pr.e("blendingMode");
|
||||
b.img.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(px, &c)| {
|
||||
let k = (c * op * color[3]).clamp(0.0, 1.0);
|
||||
if k <= 0.0 {
|
||||
if mode == 9 {
|
||||
*px = [0.0; 4];
|
||||
}
|
||||
return;
|
||||
}
|
||||
let (dc, da) = unpremul(*px);
|
||||
let fc = [color[0], color[1], color[2]];
|
||||
let mixed = match mode {
|
||||
1 => [dc[0] + fc[0], dc[1] + fc[1], dc[2] + fc[2]],
|
||||
2 => [dc[0] * fc[0], dc[1] * fc[1], dc[2] * fc[2]],
|
||||
3 => [1.0 - (1.0 - dc[0]) * (1.0 - fc[0]), 1.0 - (1.0 - dc[1]) * (1.0 - fc[1]), 1.0 - (1.0 - dc[2]) * (1.0 - fc[2])],
|
||||
4..=7 => {
|
||||
let (dh, ds, dl) = rgb_to_hsl(dc[0], dc[1], dc[2]);
|
||||
let (fh, fs, fl) = rgb_to_hsl(fc[0], fc[1], fc[2]);
|
||||
let (r, g, bl) = match mode {
|
||||
4 => hsl_to_rgb(fh, ds, dl),
|
||||
5 => hsl_to_rgb(dh, fs, dl),
|
||||
6 => hsl_to_rgb(fh, fs, dl),
|
||||
_ => hsl_to_rgb(dh, ds, fl),
|
||||
};
|
||||
[r, g, bl]
|
||||
}
|
||||
_ => fc,
|
||||
};
|
||||
*px = match mode {
|
||||
8 => over(premul(fc, k), *px),
|
||||
9 => premul(fc, k),
|
||||
0 => over(*px, premul(fc, k)),
|
||||
_ => {
|
||||
if da <= 1e-6 {
|
||||
return;
|
||||
}
|
||||
let c3 = crate::util::lerp3(dc, mixed, k);
|
||||
premul(c3, da)
|
||||
}
|
||||
};
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Eyedropper Fill
|
||||
|
||||
pub(super) fn eyedropper_fill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let fill = eyedropper_color(ctx, &b);
|
||||
let keep_alpha = pr.b("maintainOriginalAlpha");
|
||||
let blend = (pr.f("blendWithOriginal") / 100.0) as f32;
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let a = if keep_alpha { px[3] } else { fill[3] };
|
||||
let f = premul([fill[0], fill[1], fill[2]], a);
|
||||
*px = crate::util::lerp4(f, *px, blend);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Eyedropper Fill's sampled colour (straight RGB, alpha) from the layer's pixels (shared with
|
||||
/// the GPU compositor, which reads the layer back for it).
|
||||
pub fn eyedropper_color(ctx: &EffectCtx, b: &Buf) -> Px {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as i64, b.img.height as i64);
|
||||
let c = b.to_px(pr.v2("samplePoint"));
|
||||
let r = (pr.f("sampleRadius") * b.scale).max(0.0);
|
||||
let mode = pr.e("averagePixelColors");
|
||||
let (cx, cy) = (c.0.floor() as i64, c.1.floor() as i64);
|
||||
let ri = r.ceil() as i64;
|
||||
let mut acc = [0.0f64; 4];
|
||||
let mut n = 0.0f64;
|
||||
for y in (cy - ri).max(0)..=(cy + ri).min(h - 1) {
|
||||
for x in (cx - ri).max(0)..=(cx + ri).min(w - 1) {
|
||||
if ((x - cx).pow(2) + (y - cy).pow(2)) as f64 > r * r + 0.5 {
|
||||
continue;
|
||||
}
|
||||
let px = b.img.data[(y * w + x) as usize];
|
||||
match mode {
|
||||
0 => {
|
||||
if px[3] > 1e-4 {
|
||||
let (s, _) = unpremul(px);
|
||||
for k in 0..3 {
|
||||
acc[k] += s[k] as f64;
|
||||
}
|
||||
acc[3] += 1.0;
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let (s, _) = unpremul(px);
|
||||
for k in 0..3 {
|
||||
acc[k] += s[k] as f64;
|
||||
}
|
||||
acc[3] += 1.0;
|
||||
n += 1.0;
|
||||
}
|
||||
2 => {
|
||||
for k in 0..3 {
|
||||
acc[k] += px[k] as f64;
|
||||
}
|
||||
acc[3] += 1.0;
|
||||
n += 1.0;
|
||||
}
|
||||
_ => {
|
||||
for k in 0..4 {
|
||||
acc[k] += px[k] as f64;
|
||||
}
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if n > 0.0 {
|
||||
if mode == 3 {
|
||||
let a = (acc[3] / n) as f32;
|
||||
let s = if a > 1e-6 { [(acc[0] / n) as f32 / a, (acc[1] / n) as f32 / a, (acc[2] / n) as f32 / a] } else { [0.0; 3] };
|
||||
[s[0], s[1], s[2], a]
|
||||
} else {
|
||||
[(acc[0] / n) as f32, (acc[1] / n) as f32, (acc[2] / n) as f32, 1.0]
|
||||
}
|
||||
} else {
|
||||
[0.0, 0.0, 0.0, 0.0]
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Threads
|
||||
|
||||
pub(super) fn threads(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let sc = b.scale;
|
||||
let tw = (pr.f("width") * sc).max(1.0);
|
||||
let th = (pr.f("height") * sc).max(1.0);
|
||||
let overlaps = pr.f("overlaps").round().clamp(1.0, 20.0) as i64;
|
||||
let ang = pr.f("direction").to_radians();
|
||||
let (sn, cs) = ang.sin_cos();
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let coverage = (pr.f("coverage") / 100.0).clamp(0.0, 1.0);
|
||||
let shadow = (pr.f("shadowing") / 100.0) as f32;
|
||||
let texture = (pr.f("texture") / 100.0) as f32;
|
||||
let src = b.img.clone();
|
||||
let to_px = |u: f64, v: f64| (c.0 + u * cs - v * sn, c.1 + u * sn + v * cs);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (dx, dy) = (x as f64 + 0.5 - c.0, y as f64 + 0.5 - c.1);
|
||||
let u = dx * cs + dy * sn;
|
||||
let v = -dx * sn + dy * cs;
|
||||
let (ci, cj) = ((u / tw).floor() as i64, (v / th).floor() as i64);
|
||||
let (fu, fv) = (u / tw - ci as f64, v / th - cj as f64);
|
||||
// Horizontal thread j occupies the band |fv-0.5| < coverage/2; vertical thread i the
|
||||
// band |fu-0.5| < coverage/2.
|
||||
let in_h = (fv - 0.5).abs() < coverage * 0.5;
|
||||
let in_v = (fu - 0.5).abs() < coverage * 0.5;
|
||||
// Twill: horizontal is on top when (i + j) mod (2·overlaps) < overlaps.
|
||||
let h_top = (ci + cj).rem_euclid(2 * overlaps) < overlaps;
|
||||
let which = match (in_h, in_v) {
|
||||
(true, true) => Some(h_top),
|
||||
(true, false) => Some(true),
|
||||
(false, true) => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
let Some(horizontal) = which else {
|
||||
*px = [0.0; 4];
|
||||
continue;
|
||||
};
|
||||
// Thread colour: the source sampled along the thread centre line.
|
||||
let (sx, sy) = if horizontal { to_px(u, (cj as f64 + 0.5) * th) } else { to_px((ci as f64 + 0.5) * tw, v) };
|
||||
let s = src.sample_bilinear_clamped(sx, sy);
|
||||
// Round profile across the thread + darkening where it dives under its neighbour.
|
||||
let across = if horizontal { (fv - 0.5) / (coverage * 0.5).max(1e-6) } else { (fu - 0.5) / (coverage * 0.5).max(1e-6) };
|
||||
let along = if horizontal { fu } else { fv };
|
||||
let profile = (1.0 - across * across).max(0.0).sqrt() as f32;
|
||||
let dive = 1.0 - (along - 0.5).abs() as f32 * 2.0;
|
||||
let shade = 1.0 - shadow * (1.0 - profile) * 0.8 - shadow * 0.25 * (1.0 - dive) * if horizontal == h_top { 0.0 } else { 1.0 };
|
||||
let fib =
|
||||
if texture > 0.0 { 1.0 - texture * 0.3 * hash1((along * 64.0) as u32, (across * 8.0 + 8.0) as u32, ci as u32 ^ (cj as u32) << 8) } else { 1.0 };
|
||||
let k = (shade * fib).max(0.0);
|
||||
*px = [s[0] * k, s[1] * k, s[2] * k, s[3]];
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
use super::raster::over;
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------- Fractal
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FracSet {
|
||||
/// Mandelbrot iterations start at the Julia centre (the "Over Julia" set choices).
|
||||
over_julia: bool,
|
||||
julia: bool,
|
||||
inverse: bool,
|
||||
}
|
||||
|
||||
/// Smooth escape count, or `None` inside the set.
|
||||
fn escape(z0: (f64, f64), c: (f64, f64), power: u32, limit: f64, max_iter: u32) -> Option<f64> {
|
||||
let (mut x, mut y) = z0;
|
||||
let lim2 = limit * limit;
|
||||
for i in 0..max_iter {
|
||||
let (nx, ny) = match power {
|
||||
3 => (x * x * x - 3.0 * x * y * y, 3.0 * x * x * y - y * y * y),
|
||||
4 => {
|
||||
let (a, b) = (x * x - y * y, 2.0 * x * y);
|
||||
(a * a - b * b, 2.0 * a * b)
|
||||
}
|
||||
_ => (x * x - y * y, 2.0 * x * y),
|
||||
};
|
||||
x = nx + c.0;
|
||||
y = ny + c.1;
|
||||
let m2 = x * x + y * y;
|
||||
if m2 > lim2 {
|
||||
let lz = m2.ln() * 0.5;
|
||||
let nu = (lz / limit.max(1.0001).ln()).max(1e-9).ln() / (power as f64).ln();
|
||||
return Some((i as f64 + 1.0 - nu).max(0.0));
|
||||
}
|
||||
}
|
||||
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"];
|
||||
|
||||
pub(super) 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("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 => (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> {
|
||||
let (center, mag, lim) = if s.julia { (jc, jmag, jlim) } else { (mc, mmag, mlim) };
|
||||
let span = 3.5 / 2f64.powf(mag);
|
||||
let mut u = (lx - lw * 0.5) / lw * span + center.0;
|
||||
let mut v = (ly - lh * 0.5) / lw * span + center.1;
|
||||
if s.inverse {
|
||||
let d = (u * u + v * v).max(1e-12);
|
||||
u = u / d + post.0;
|
||||
v = -v / d + post.1;
|
||||
}
|
||||
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);
|
||||
match (a, sets.1) {
|
||||
(None, Some(s2)) => eval_set(s2, lx, ly),
|
||||
_ => a,
|
||||
}
|
||||
};
|
||||
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 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),
|
||||
// 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 {
|
||||
c = [c[0] * 0.4 + 0.6, c[1] * 0.4 + 0.6, c[2] * 0.4 + 0.6];
|
||||
}
|
||||
[c[0], c[1], c[2], 1.0]
|
||||
};
|
||||
// First pass: one sample per pixel.
|
||||
let mut first: Vec<Option<f64>> = vec![None; w * h];
|
||||
first.par_chunks_mut(w.max(1)).enumerate().for_each(|(y, row)| {
|
||||
for (x, v) in row.iter_mut().enumerate() {
|
||||
*v = eval((x as f64 + 0.5 - off_x) * inv, (y as f64 + 0.5 - off_y) * inv);
|
||||
}
|
||||
});
|
||||
let src = if overlay { Some(b.img.clone()) } else { None };
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let here = first[y * w + x];
|
||||
let needs_ss = factor > 1
|
||||
&& (every || {
|
||||
let mut diff = false;
|
||||
for (dx, dy) in [(-1i64, 0i64), (1, 0), (0, -1), (0, 1)] {
|
||||
let (nx, ny) = (x as i64 + dx, y as i64 + dy);
|
||||
if nx < 0 || ny < 0 || nx >= w as i64 || ny >= h as i64 {
|
||||
continue;
|
||||
}
|
||||
let o = first[ny as usize * w + nx as usize];
|
||||
diff |= match (here, o) {
|
||||
(Some(a), Some(c)) => (a - c).abs() > steps * 0.25,
|
||||
(None, None) => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
diff
|
||||
});
|
||||
let c = if needs_ss {
|
||||
let mut acc = [0.0f32; 4];
|
||||
for sy in 0..factor {
|
||||
for sx in 0..factor {
|
||||
let fx = x as f64 + (sx as f64 + 0.5) / factor as f64;
|
||||
let fy = y as f64 + (sy as f64 + 0.5) / factor as f64;
|
||||
let s = shade(eval((fx - off_x) * inv, (fy - off_y) * inv));
|
||||
let a = s[3];
|
||||
for k in 0..3 {
|
||||
acc[k] += s[k] * a;
|
||||
}
|
||||
acc[3] += a;
|
||||
}
|
||||
}
|
||||
let n = (factor * factor) as f32;
|
||||
[acc[0] / n, acc[1] / n, acc[2] / n, acc[3] / n]
|
||||
} else {
|
||||
let s = shade(here);
|
||||
[s[0] * s[3], s[1] * s[3], s[2] * s[3], s[3]]
|
||||
};
|
||||
*px = match &src {
|
||||
Some(o) => over(o.data[y * w + x], c),
|
||||
None => c,
|
||||
};
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
use super::raster::{Seg, all_masks_px, chaikin, mask_px, over, poly_segs, raster_convex, raster_segs, scan_runs, stroke_outline, trim_poly};
|
||||
use super::*;
|
||||
|
||||
/// How painted coverage meets the layer: 0 On Original Image, 1 On Transparent, 2 Reveal
|
||||
/// Original Image.
|
||||
pub(crate) fn paint(img: &mut Image, cov: &Plane, color: [f32; 4], opacity: f32, style: u32) {
|
||||
img.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(px, &c)| {
|
||||
let a = (c * opacity * color[3]).clamp(0.0, 1.0);
|
||||
let s = [color[0] * a, color[1] * a, color[2] * a, a];
|
||||
*px = match style {
|
||||
1 => s,
|
||||
2 => [px[0] * a, px[1] * a, px[2] * a, px[3] * a],
|
||||
_ => over(*px, s),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// Coverage of a plan-drawn generator: round-capped segments rasterised by `raster_segs`
|
||||
/// with `hardness`, or the union of convex polygons (`raster_convex`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Coverage {
|
||||
Segs { segs: Vec<Seg>, hardness: f64 },
|
||||
Convex(Vec<Vec<[f64; 2]>>),
|
||||
}
|
||||
|
||||
impl Coverage {
|
||||
fn raster(&self, w: usize, h: usize) -> Plane {
|
||||
match self {
|
||||
Coverage::Segs { segs, hardness } => raster_segs(w, h, segs, *hardness),
|
||||
Coverage::Convex(polys) => raster_convex(w, h, polys),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke, Scribble, Vegas and Write-on, shared with the GPU compositor (effectcraft-gpu
|
||||
/// `fx_gen2`): the coverage and how it is painted. `style` 0–2 as `paint` (On Original
|
||||
/// Image, On Transparent, Reveal Original Image), 3 = under the layer (Vegas' Composite Under).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PaintPlan {
|
||||
pub cov: Coverage,
|
||||
pub color: [f32; 4],
|
||||
pub opacity: f32,
|
||||
pub style: u32,
|
||||
}
|
||||
|
||||
/// Draw a [`PaintPlan`].
|
||||
fn draw_paint(b: &mut Buf, plan: &PaintPlan) {
|
||||
let cov = plan.cov.raster(b.img.width as usize, b.img.height as usize);
|
||||
if plan.style == 3 {
|
||||
let color = plan.color;
|
||||
b.img.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(px, &c)| {
|
||||
let a = c * color[3];
|
||||
*px = over([color[0] * a, color[1] * a, color[2] * a, a], *px);
|
||||
});
|
||||
} else {
|
||||
paint(&mut b.img, &cov, plan.color, plan.opacity, plan.style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generators drawn from a plan that need the layer's own pixels (`b.img`) to build it.
|
||||
pub fn plan_reads_pixels(id: &str, ctx: &EffectCtx) -> bool {
|
||||
match id {
|
||||
// Image Contours read the layer itself when no (resolvable) Input Layer is chosen.
|
||||
"ec.generate.vegas" => ctx.params.e("stroke") == 0,
|
||||
"ec.generate.paintbucket" | "ec.generate.eyedropperfill" => true,
|
||||
"ec.generate.advancedlightning" => ctx.params.f("alphaObstacle").clamp(-10.0, 10.0) != 0.0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`PaintPlan`] of Stroke, Scribble, Vegas or Write-on on `b`'s geometry (`b.img` is read
|
||||
/// only when [`plan_reads_pixels`]). `None`: Scribble without a usable mask (the layer is
|
||||
/// cleared when it composites On Transparent, else kept).
|
||||
pub fn paint_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<PaintPlan> {
|
||||
match id {
|
||||
"ec.generate.stroke" => Some(stroke_plan(ctx, b)),
|
||||
"ec.generate.scribble" => scribble_plan(ctx, b),
|
||||
"ec.generate.vegas" => Some(vegas_plan(ctx, b)),
|
||||
"ec.generate.writeon" => Some(write_on_plan(ctx, b)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scribble's Composite as a `paint` style.
|
||||
pub fn scribble_style(ctx: &EffectCtx) -> u32 {
|
||||
match ctx.params.e("composite") {
|
||||
0 => 0,
|
||||
2 => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn scribble(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
match scribble_plan(ctx, &b) {
|
||||
Some(plan) => draw_paint(&mut b, &plan),
|
||||
None if scribble_style(ctx) == 1 => b.img = Image::new(b.img.width, b.img.height),
|
||||
None => {}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn scribble_plan(ctx: &EffectCtx, b: &Buf) -> Option<PaintPlan> {
|
||||
let pr = ctx.params;
|
||||
let which = pr.e("scribble");
|
||||
let polys: Vec<_> = if which == 0 { mask_px(ctx, b, pr.f("mask").round().max(1.0) as usize).into_iter().collect() } else { all_masks_px(ctx, b) };
|
||||
let polys: Vec<_> = polys.into_iter().filter(|p| p.0.len() >= 3).map(|(p, c, i)| (p, c, i && which == 2)).collect();
|
||||
let style = scribble_style(ctx);
|
||||
if polys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let sc = b.scale;
|
||||
let fill = pr.e("fillType");
|
||||
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("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");
|
||||
let wiggle = pr.e("wiggleType");
|
||||
let wps = pr.f("wigglesPerSecond").max(0.0);
|
||||
let seed = pr.f("randomSeed") as i64 as u32 ^ ctx.seed;
|
||||
let (step, frac) = match wiggle {
|
||||
0 => (0u32, 0.0f32),
|
||||
1 => ((ctx.time * wps).floor().max(0.0) as u32, 0.0),
|
||||
_ => {
|
||||
let u = (ctx.time * wps).max(0.0);
|
||||
(u.floor() as u32, smoothstep(0.0, 1.0, u.fract() as f32))
|
||||
}
|
||||
};
|
||||
let rnd = |i: u32, k: u32| {
|
||||
let a = hash1(i, k.wrapping_mul(31).wrapping_add(step.wrapping_mul(7919)), seed) * 2.0 - 1.0;
|
||||
if frac > 0.0 {
|
||||
let c = hash1(i, k.wrapping_mul(31).wrapping_add((step + 1).wrapping_mul(7919)), seed) * 2.0 - 1.0;
|
||||
a + (c - a) * frac
|
||||
} else {
|
||||
a
|
||||
}
|
||||
};
|
||||
let groups: Vec<Vec<(Vec<[f64; 2]>, bool, bool)>> =
|
||||
if which == 0 || sequential { vec![polys.clone()] } else { polys.iter().map(|p| vec![p.clone()]).collect() };
|
||||
// Per group a zig-zag polyline (in buffer px).
|
||||
let mut lines: Vec<Vec<Vec<[f64; 2]>>> = Vec::new();
|
||||
let mut runs = Vec::new();
|
||||
for (gi, g) in groups.iter().enumerate() {
|
||||
let mut nmin = f64::INFINITY;
|
||||
let mut nmax = f64::NEG_INFINITY;
|
||||
for (pts, _, _) in g {
|
||||
for q in pts {
|
||||
let v = q[0] * n[0] + q[1] * n[1];
|
||||
nmin = nmin.min(v);
|
||||
nmax = nmax.max(v);
|
||||
}
|
||||
}
|
||||
nmin -= ew;
|
||||
nmax += ew;
|
||||
// Chains of zig-zags: each run joins the nearest chain that ended on the previous
|
||||
// scan line (so separate regions get separate strokes).
|
||||
let mut chains: Vec<(Vec<[f64; 2]>, u32)> = Vec::new();
|
||||
let mut s = nmin + spacing * 0.5;
|
||||
let mut li = 0u32;
|
||||
let reach = spacing * 3.0 + ew;
|
||||
while s < nmax && li < 20_000 {
|
||||
scan_runs(g, fill, ew, d, n, s, &mut runs);
|
||||
for (ri, &(a, bb)) in runs.iter().enumerate() {
|
||||
let k = (gi as u32) << 20 | ri as u32;
|
||||
let ov = spacing * (overlap + overlap_var * rnd(li, k * 4) as f64 * 0.5).max(-0.45);
|
||||
let ov2 = spacing * (overlap + overlap_var * rnd(li, k * 4 + 1) as f64 * 0.5).max(-0.45);
|
||||
let (a, bb) = (a - ov, bb + ov2);
|
||||
if bb <= a {
|
||||
continue;
|
||||
}
|
||||
let at = |t: f64| [d[0] * t + n[0] * s, d[1] * t + n[1] * s];
|
||||
let (pa, pb) = (at(a), at(bb));
|
||||
let dist = |p: [f64; 2], q: [f64; 2]| ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2)).sqrt();
|
||||
let best = chains
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| li > 0 && c.1 == li - 1)
|
||||
.map(|(i, c)| {
|
||||
let last = *c.0.last().unwrap_or(&pa);
|
||||
(i, dist(last, pa).min(dist(last, pb)))
|
||||
})
|
||||
.filter(|&(_, dd)| dd < reach)
|
||||
.min_by(|x, y| x.1.total_cmp(&y.1));
|
||||
match best {
|
||||
Some((ci, _)) => {
|
||||
let last = *chains[ci].0.last().unwrap_or(&pa);
|
||||
let (p0, p1, dir) = if dist(last, pa) <= dist(last, pb) { (pa, pb, -1.0) } else { (pb, pa, 1.0) };
|
||||
// Curved turn: an extra point pushed past the edge.
|
||||
let c = (curv + curv_var * rnd(li, k * 4 + 2) as f64 * 0.5).max(0.0);
|
||||
let mid = [(last[0] + p0[0]) * 0.5 + d[0] * dir * c * spacing, (last[1] + p0[1]) * 0.5 + d[1] * dir * c * spacing];
|
||||
let ch = &mut chains[ci];
|
||||
ch.0.extend([mid, p0, p1]);
|
||||
ch.1 = li;
|
||||
}
|
||||
None => chains.push((vec![pa, pb], li)),
|
||||
}
|
||||
}
|
||||
s += spacing * (1.0 + spacing_var * rnd(li, 3) as f64 * 0.5).max(0.25);
|
||||
li += 1;
|
||||
}
|
||||
lines.push(chains.into_iter().map(|(z, _)| if curv > 0.0 { chaikin(&z, 2) } else { z }).collect());
|
||||
}
|
||||
let r = width * 0.5;
|
||||
// Start / End Apply To: all of a mask's strokes as one path, or each stroke on its own.
|
||||
let each = pr.e("startEndApplyTo") == 1;
|
||||
let mut parts: Vec<Vec<[f64; 2]>> = Vec::new();
|
||||
for group in &lines {
|
||||
let lens: Vec<f64> = group.iter().map(|l| poly_length(l, false)).collect();
|
||||
let total: f64 = lens.iter().sum();
|
||||
let (g0, g1) = (total * start, total * end);
|
||||
let mut acc = 0.0;
|
||||
for (l, len) in group.iter().zip(lens) {
|
||||
let part =
|
||||
if each { trim_poly(l, false, len * start, len * end) } else { trim_poly(l, false, (g0 - acc).clamp(0.0, len), (g1 - acc).clamp(0.0, len)) };
|
||||
acc += len;
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
let cap = pr.e("edgeOptions/endCap");
|
||||
let join = pr.e("edgeOptions/join");
|
||||
let cov = if cap == 0 && join == 0 {
|
||||
let mut segs = Vec::new();
|
||||
for part in &parts {
|
||||
poly_segs(part, false, r, 1.0, &mut segs);
|
||||
}
|
||||
Coverage::Segs { segs, hardness: 0.9 }
|
||||
} else {
|
||||
Coverage::Convex(stroke_outline(&parts, r, cap, join, pr.f("edgeOptions/miterLimit").max(1.0)))
|
||||
};
|
||||
Some(PaintPlan { cov, color: pr.color("color"), opacity: (pr.f("opacity") / 100.0) as f32, style })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Stroke
|
||||
|
||||
pub(super) fn stroke(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let plan = stroke_plan(ctx, &b);
|
||||
draw_paint(&mut b, &plan);
|
||||
b
|
||||
}
|
||||
|
||||
fn stroke_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan {
|
||||
let pr = ctx.params;
|
||||
let all = pr.b("allMasks");
|
||||
let polys: Vec<_> = if all { all_masks_px(ctx, b) } else { mask_px(ctx, b, pr.f("path").round() as usize).into_iter().collect() };
|
||||
let style = pr.e("paintStyle");
|
||||
let size = (pr.f("brushSize") * b.scale).max(0.1);
|
||||
let r = size * 0.5;
|
||||
let hard = pr.f("brushHardness") / 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 spacing = pr.f("spacing") / 100.0;
|
||||
let sequential = pr.b("strokeSequentially");
|
||||
let lens: Vec<f64> = polys.iter().map(|(p, c, _)| poly_length(p, *c)).collect();
|
||||
let total: f64 = lens.iter().sum();
|
||||
let mut segs = Vec::new();
|
||||
let mut acc = 0.0;
|
||||
for ((pts, closed, _), &len) in polys.iter().zip(&lens) {
|
||||
let (s0, s1) = if all && sequential {
|
||||
let (g0, g1) = (total * start, total * end);
|
||||
((g0 - acc).clamp(0.0, len), (g1 - acc).clamp(0.0, len))
|
||||
} else {
|
||||
(len * start, len * end)
|
||||
};
|
||||
acc += len;
|
||||
if s1 <= s0 {
|
||||
continue;
|
||||
}
|
||||
if spacing <= 0.5 {
|
||||
let part = trim_poly(pts, *closed, s0, s1);
|
||||
poly_segs(&part, false, r, 1.0, &mut segs);
|
||||
} else {
|
||||
let step = (size * spacing).max(0.5);
|
||||
let mut s = s0;
|
||||
while s <= s1 + 1e-9 && segs.len() < 200_000 {
|
||||
let (q, _) = crate::util::poly_point_at(pts, *closed, s);
|
||||
segs.push(Seg { a: q, b: q, r, v: 1.0 });
|
||||
s += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
PaintPlan { cov: Coverage::Segs { segs, hardness: hard }, color: pr.color("color"), opacity: (pr.f("opacity") / 100.0) as f32, style }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Vegas
|
||||
|
||||
/// Marching-squares iso-contours of `pl` at `thr`, chained into polylines (closed flag).
|
||||
pub(super) fn contours(pl: &Plane, thr: f32) -> Vec<(Vec<[f64; 2]>, bool)> {
|
||||
use std::collections::HashMap;
|
||||
let (w, h) = (pl.w, pl.h);
|
||||
if w < 2 || h < 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
// Edge key: horizontal edge (x,y)-(x+1,y) = 2*(y*w+x), vertical (x,y)-(x,y+1) = 2*(y*w+x)+1.
|
||||
let hk = |x: usize, y: usize| 2 * (y * w + x) as u64;
|
||||
let vk = |x: usize, y: usize| 2 * (y * w + x) as u64 + 1;
|
||||
let mut pos: HashMap<u64, [f64; 2]> = HashMap::new();
|
||||
let mut adj: HashMap<u64, Vec<u64>> = HashMap::new();
|
||||
let v = |x: usize, y: usize| pl.data[y * w + x];
|
||||
let interp = |a: f32, b: f32| if (b - a).abs() > 1e-9 { ((thr - a) / (b - a)).clamp(0.0, 1.0) as f64 } else { 0.5 };
|
||||
for y in 0..h - 1 {
|
||||
for x in 0..w - 1 {
|
||||
let (a, bb, c, d) = (v(x, y), v(x + 1, y), v(x + 1, y + 1), v(x, y + 1));
|
||||
let idx = (a > thr) as u8 | ((bb > thr) as u8) << 1 | ((c > thr) as u8) << 2 | ((d > thr) as u8) << 3;
|
||||
if idx == 0 || idx == 15 {
|
||||
continue;
|
||||
}
|
||||
// Edge points (pixel centres at +0.5).
|
||||
let top = (hk(x, y), [x as f64 + 0.5 + interp(a, bb), y as f64 + 0.5]);
|
||||
let right = (vk(x + 1, y), [x as f64 + 1.5, y as f64 + 0.5 + interp(bb, c)]);
|
||||
let bottom = (hk(x, y + 1), [x as f64 + 0.5 + interp(d, c), y as f64 + 1.5]);
|
||||
let left = (vk(x, y), [x as f64 + 0.5, y as f64 + 0.5 + interp(a, d)]);
|
||||
let pairs: &[(usize, usize)] = match idx {
|
||||
1 | 14 => &[(3, 0)],
|
||||
2 | 13 => &[(0, 1)],
|
||||
3 | 12 => &[(3, 1)],
|
||||
4 | 11 => &[(1, 2)],
|
||||
5 => &[(3, 0), (1, 2)],
|
||||
6 | 9 => &[(0, 2)],
|
||||
7 | 8 => &[(3, 2)],
|
||||
10 => &[(0, 1), (2, 3)],
|
||||
_ => &[],
|
||||
};
|
||||
let e = [top, right, bottom, left];
|
||||
for &(i, j) in pairs {
|
||||
pos.insert(e[i].0, e[i].1);
|
||||
pos.insert(e[j].0, e[j].1);
|
||||
adj.entry(e[i].0).or_default().push(e[j].0);
|
||||
adj.entry(e[j].0).or_default().push(e[i].0);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut keys: Vec<u64> = adj.keys().copied().collect();
|
||||
keys.sort_unstable();
|
||||
// Start open chains at endpoints (degree 1) first, then closed loops.
|
||||
keys.sort_by_key(|k| adj[k].len() != 1);
|
||||
let mut used: std::collections::HashSet<(u64, u64)> = std::collections::HashSet::new();
|
||||
let mut out = Vec::new();
|
||||
for &k0 in &keys {
|
||||
for &first in &adj[&k0] {
|
||||
if used.contains(&(k0.min(first), k0.max(first))) {
|
||||
continue;
|
||||
}
|
||||
let mut chain = vec![pos[&k0]];
|
||||
let (mut prev, mut cur) = (k0, first);
|
||||
used.insert((prev.min(cur), prev.max(cur)));
|
||||
let mut closed = false;
|
||||
loop {
|
||||
chain.push(pos[&cur]);
|
||||
if cur == k0 {
|
||||
closed = true;
|
||||
chain.pop();
|
||||
break;
|
||||
}
|
||||
let next = adj[&cur].iter().copied().find(|&n| n != prev && !used.contains(&(cur.min(n), cur.max(n))));
|
||||
match next {
|
||||
Some(n) => {
|
||||
used.insert((cur.min(n), cur.max(n)));
|
||||
prev = cur;
|
||||
cur = n;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
if chain.len() >= 2 {
|
||||
out.push((chain, closed));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(super) fn vegas(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let plan = vegas_plan(ctx, &b);
|
||||
draw_paint(&mut b, &plan);
|
||||
b
|
||||
}
|
||||
|
||||
fn vegas_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let sc = b.scale;
|
||||
let paths: Vec<(Vec<[f64; 2]>, bool)> = if pr.e("stroke") == 1 {
|
||||
mask_px(ctx, b, pr.f("path").round() as usize).map(|(p, c, _)| vec![(p, c)]).unwrap_or_default()
|
||||
} else {
|
||||
let src = layer_or_self(ctx, b, "inputLayer", true, pr.e("ifLayerSizesDiffer") == 1);
|
||||
let ch = pr.e("channel");
|
||||
let inv = pr.b("invertInput");
|
||||
let mut pl = Plane::from_image(&src, |px| {
|
||||
let (c, a) = unpremul(px);
|
||||
let v = match ch {
|
||||
1 => c[0],
|
||||
2 => c[1],
|
||||
3 => c[2],
|
||||
4 => a,
|
||||
_ => luminance(c[0], c[1], c[2]) * a,
|
||||
};
|
||||
if inv { 1.0 - v } else { v }
|
||||
});
|
||||
let blur = pr.f("preBlur") * sc;
|
||||
if blur > 0.05 {
|
||||
pl = gauss_plane(&pl, blur, blur);
|
||||
}
|
||||
let mut cs = contours(&pl, (pr.f("threshold") / 100.0) as f32);
|
||||
// Simplify by tolerance (drop points closer than tol × size).
|
||||
let tol = pr.f("tolerance").max(0.0) * (w.max(h) as f64) * 0.1;
|
||||
if tol > 0.0 {
|
||||
for (c, _) in cs.iter_mut() {
|
||||
let mut keep: Vec<[f64; 2]> = Vec::with_capacity(c.len());
|
||||
for &q in c.iter() {
|
||||
if keep.last().is_none_or(|l: &[f64; 2]| ((l[0] - q[0]).powi(2) + (l[1] - q[1]).powi(2)).sqrt() >= tol) {
|
||||
keep.push(q);
|
||||
}
|
||||
}
|
||||
*c = keep;
|
||||
}
|
||||
}
|
||||
cs.sort_by(|a, b| poly_length(&b.0, b.1).total_cmp(&poly_length(&a.0, a.1)));
|
||||
if pr.e("renderMode") == 1 {
|
||||
let sel = (pr.f("selectedContour").round().max(1.0) as usize) - 1;
|
||||
cs.into_iter().skip(sel).take(1).collect()
|
||||
} else {
|
||||
cs
|
||||
}
|
||||
};
|
||||
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;
|
||||
let random_phase = pr.b("randomPhase");
|
||||
let seed = pr.f("randomSeed") as i64 as u32 ^ ctx.seed;
|
||||
let width = (pr.f("width") * sc).max(0.1);
|
||||
let hard = pr.f("hardness").clamp(0.0, 1.0);
|
||||
let (o0, om, o1, mp) = (pr.f("startOpacity") as f32, pr.f("midpointOpacity") as f32, pr.f("endOpacity") as f32, pr.f("midpointPosition").clamp(0.01, 0.99));
|
||||
let mut segs = Vec::new();
|
||||
for (ci, (pts, closed)) in paths.iter().enumerate() {
|
||||
let len = poly_length(pts, *closed);
|
||||
if len <= 0.0 {
|
||||
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 };
|
||||
let s0 = (base + phase).rem_euclid(1.0) * len;
|
||||
// Sub-sample the segment so opacity can ramp along it.
|
||||
let sub = 12usize;
|
||||
let mut prev: Option<[f64; 2]> = None;
|
||||
for k in 0..=sub {
|
||||
let f = k as f64 / sub as f64;
|
||||
let mut s = s0 + each * f;
|
||||
if *closed {
|
||||
s = s.rem_euclid(len);
|
||||
} else if s > len {
|
||||
break;
|
||||
}
|
||||
let (q, _) = crate::util::poly_point_at(pts, *closed, s);
|
||||
let op = if f < mp { o0 + (om - o0) * (f / mp) as f32 } else { om + (o1 - om) * ((f - mp) / (1.0 - mp)) as f32 };
|
||||
if let Some(pv) = prev
|
||||
&& ((pv[0] - q[0]).powi(2) + (pv[1] - q[1]).powi(2)) < (each * 0.5).powi(2) + 4.0
|
||||
{
|
||||
segs.push(Seg { a: pv, b: q, r: width * 0.5, v: op.clamp(0.0, 1.0) });
|
||||
}
|
||||
prev = Some(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
let style = match pr.e("blendMode") {
|
||||
1 => 0,
|
||||
2 => 3,
|
||||
3 => 2,
|
||||
_ => 1,
|
||||
};
|
||||
PaintPlan { cov: Coverage::Segs { segs, hardness: hard }, color: pr.color("color"), opacity: 1.0, style }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Write-on / Glue Gun
|
||||
|
||||
/// Write-on draws a stroke along the animated Brush Position, as in After Effects: a dab every
|
||||
/// Brush Spacing seconds from the layer's start up to now, each one lasting Stroke Length
|
||||
/// seconds (0: for ever). The dabs' positions come from the host's parameters at their times;
|
||||
/// with Paint Time Properties ▸ Opacity or Brush Time Properties ▸ Size each dab keeps its own
|
||||
/// opacity or size, else the current ones apply to every dab. (Colour and hardness are one per
|
||||
/// stroke: the current ones.) Without a host (or dabs) the current dab draws. Deterministic.
|
||||
pub(super) fn write_on(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let plan = write_on_plan(ctx, &b);
|
||||
draw_paint(&mut b, &plan);
|
||||
b
|
||||
}
|
||||
|
||||
/// The most dabs one Write-on frame draws; longer strokes lay them further apart, joined.
|
||||
const WRITE_ON_MAX_DABS: i64 = 4096;
|
||||
|
||||
fn write_on_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan {
|
||||
let pr = ctx.params;
|
||||
let own_opacity = pr.e("paintTimeProperties") == 1;
|
||||
let own_size = matches!(pr.e("brushTimeProperties"), 1 | 3);
|
||||
let opacity = |p: &Params| (p.f("brushOpacity") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let radius = |p: &Params| (p.f("brushSize") * b.scale * 0.5).max(0.05);
|
||||
let dab = |p: &Params| {
|
||||
let q = b.to_px(p.v2("brushPosition"));
|
||||
([q.0, q.1], radius(if own_size { p } else { pr }), if own_opacity { opacity(p) } else { 1.0 })
|
||||
};
|
||||
let mut segs = vec![];
|
||||
let t = ctx.time;
|
||||
let spacing = pr.f("brushSpacing").max(0.001);
|
||||
let life = pr.f("strokeLength").max(0.0);
|
||||
if let Some(host) = ctx.env.host
|
||||
&& t.is_finite()
|
||||
&& t >= 0.0
|
||||
{
|
||||
let start = if life > 0.0 { (t - life).max(0.0) } else { 0.0 };
|
||||
let k0 = (start / spacing - 1e-9).ceil().max(0.0) as i64;
|
||||
let k1 = (t / spacing + 1e-9).floor().max(0.0) as i64;
|
||||
let n = k1.saturating_sub(k0).saturating_add(1);
|
||||
let stride = (n / WRITE_ON_MAX_DABS).saturating_add(1).max(1);
|
||||
let mut prev: Option<[f64; 2]> = None;
|
||||
let mut k = k0;
|
||||
while k <= k1 {
|
||||
let tk = k as f64 * spacing;
|
||||
let at = if (tk - t).abs() < 1e-9 { Some(dab(pr)) } else { host.params_at(tk).map(|p| dab(&p)) };
|
||||
if let Some((q, r, v)) = at {
|
||||
// Dabs laid further apart than the brush spacing are joined into a line.
|
||||
let a = if stride > 1 { prev.unwrap_or(q) } else { q };
|
||||
segs.push(Seg { a, b: q, r, v });
|
||||
prev = Some(q);
|
||||
}
|
||||
k = k.saturating_add(stride);
|
||||
}
|
||||
}
|
||||
if segs.is_empty() {
|
||||
let (q, r, v) = dab(pr);
|
||||
segs.push(Seg { a: q, b: q, r, v });
|
||||
}
|
||||
PaintPlan {
|
||||
cov: Coverage::Segs { segs, hardness: pr.f("brushHardness") / 100.0 },
|
||||
color: pr.color("color"),
|
||||
opacity: if own_opacity { 1.0 } else { opacity(pr) },
|
||||
style: pr.e("paintStyle"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------- shared rasterisation
|
||||
|
||||
/// A round-capped line segment in buffer pixels with radius `r` and intensity `v`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Seg {
|
||||
pub a: [f64; 2],
|
||||
pub b: [f64; 2],
|
||||
pub r: f64,
|
||||
pub v: f32,
|
||||
}
|
||||
|
||||
/// Rasterise segments into a coverage plane (max-combined). `hardness` (0..1) is the fraction
|
||||
/// of the radius that is fully opaque; the rest falls off smoothly.
|
||||
pub(crate) fn raster_segs(w: usize, h: usize, segs: &[Seg], hardness: f64) -> Plane {
|
||||
let mut out = Plane::new(w, h);
|
||||
if w == 0 || h == 0 || segs.is_empty() {
|
||||
return out;
|
||||
}
|
||||
let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); h];
|
||||
for (i, s) in segs.iter().enumerate() {
|
||||
let pad = s.r + 1.0;
|
||||
let y0 = (s.a[1].min(s.b[1]) - pad).floor().max(0.0) as usize;
|
||||
let y1 = (s.a[1].max(s.b[1]) + pad).ceil().min(h as f64);
|
||||
if y1 <= 0.0 || y0 >= h {
|
||||
continue;
|
||||
}
|
||||
for row in buckets.iter_mut().take(y1 as usize).skip(y0) {
|
||||
row.push(i as u32);
|
||||
}
|
||||
}
|
||||
let hard = hardness.clamp(0.0, 1.0);
|
||||
out.data.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
|
||||
let py = y as f64 + 0.5;
|
||||
for &i in &buckets[y] {
|
||||
let s = segs[i as usize];
|
||||
let pad = s.r + 1.0;
|
||||
let x0 = (s.a[0].min(s.b[0]) - pad).floor().max(0.0) as usize;
|
||||
let x1 = ((s.a[0].max(s.b[0]) + pad).ceil().max(0.0) as usize).min(w);
|
||||
let (dx, dy) = (s.b[0] - s.a[0], s.b[1] - s.a[1]);
|
||||
let l2 = dx * dx + dy * dy;
|
||||
let inner = s.r * hard - 0.5;
|
||||
let outer = s.r + 0.5;
|
||||
for (x, o) in row.iter_mut().enumerate().take(x1).skip(x0) {
|
||||
let px = x as f64 + 0.5;
|
||||
let t = if l2 > 1e-12 { (((px - s.a[0]) * dx + (py - s.a[1]) * dy) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let d = ((px - s.a[0] - dx * t).powi(2) + (py - s.a[1] - dy * t).powi(2)).sqrt();
|
||||
if d >= outer {
|
||||
continue;
|
||||
}
|
||||
let c = if d <= inner { 1.0 } else { 1.0 - smoothstep(inner as f32, outer as f32, d as f32) } * s.v;
|
||||
if c > *o {
|
||||
*o = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Segments along a polyline (buffer px).
|
||||
pub fn poly_segs(pts: &[[f64; 2]], closed: bool, r: f64, v: f32, out: &mut Vec<Seg>) {
|
||||
let n = pts.len();
|
||||
if n == 1 {
|
||||
out.push(Seg { a: pts[0], b: pts[0], r, v });
|
||||
}
|
||||
if n < 2 {
|
||||
return;
|
||||
}
|
||||
let segs = if closed { n } else { n - 1 };
|
||||
for i in 0..segs {
|
||||
out.push(Seg { a: pts[i], b: pts[(i + 1) % n], r, v });
|
||||
}
|
||||
}
|
||||
|
||||
/// Portion `[s0, s1]` (arc length) of a polyline.
|
||||
pub(crate) fn trim_poly(pts: &[[f64; 2]], closed: bool, s0: f64, s1: f64) -> Vec<[f64; 2]> {
|
||||
let n = pts.len();
|
||||
let mut out = Vec::new();
|
||||
if n < 2 || s1 <= s0 {
|
||||
return out;
|
||||
}
|
||||
let segs = if closed { n } else { n - 1 };
|
||||
let mut acc = 0.0;
|
||||
for i in 0..segs {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||||
let l = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
|
||||
let (e0, e1) = (acc, acc + l);
|
||||
acc = e1;
|
||||
if e1 < s0 || e0 > s1 || l <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let at = |s: f64| {
|
||||
let t = ((s - e0) / l).clamp(0.0, 1.0);
|
||||
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
|
||||
};
|
||||
let p0 = at(s0.max(e0));
|
||||
if out.last() != Some(&p0) {
|
||||
out.push(p0);
|
||||
}
|
||||
out.push(at(s1.min(e1)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn over(dst: Px, src: Px) -> Px {
|
||||
let k = 1.0 - src[3];
|
||||
[src[0] + dst[0] * k, src[1] + dst[1] * k, src[2] + dst[2] * k, src[3] + dst[3] * k]
|
||||
}
|
||||
|
||||
/// Mask `idx` (1-based; 0 = none) mapped to buffer pixels.
|
||||
pub(crate) fn mask_px(ctx: &EffectCtx, b: &Buf, idx: usize) -> Option<(Vec<[f64; 2]>, bool, bool)> {
|
||||
let m = ctx.env.masks.get(idx.checked_sub(1)?)?;
|
||||
if m.points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((m.points.iter().map(|&q| [q[0] * b.scale + b.offset[0], q[1] * b.scale + b.offset[1]]).collect(), m.closed, m.inverted))
|
||||
}
|
||||
|
||||
/// All masks mapped to buffer pixels.
|
||||
pub(super) fn all_masks_px(ctx: &EffectCtx, b: &Buf) -> Vec<(Vec<[f64; 2]>, bool, bool)> {
|
||||
(1..=ctx.env.masks.len()).filter_map(|i| mask_px(ctx, b, i)).collect()
|
||||
}
|
||||
|
||||
/// Chaikin corner cutting (open polyline, endpoints kept).
|
||||
pub(super) fn chaikin(pts: &[[f64; 2]], iters: usize) -> Vec<[f64; 2]> {
|
||||
let mut cur = pts.to_vec();
|
||||
for _ in 0..iters {
|
||||
if cur.len() < 3 {
|
||||
return cur;
|
||||
}
|
||||
let mut nx = Vec::with_capacity(cur.len() * 2);
|
||||
nx.push(cur[0]);
|
||||
for w in cur.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
nx.push([a[0] * 0.75 + b[0] * 0.25, a[1] * 0.75 + b[1] * 0.25]);
|
||||
nx.push([a[0] * 0.25 + b[0] * 0.75, a[1] * 0.25 + b[1] * 0.75]);
|
||||
}
|
||||
nx.push(*cur.last().unwrap_or(&[0.0; 2]));
|
||||
cur = nx;
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Scribble
|
||||
|
||||
/// Scan runs (start, end along direction `d`) of the fill region on the line `n·p = s`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn scan_runs(polys: &[(Vec<[f64; 2]>, bool, bool)], fill: u32, ew: f64, d: [f64; 2], n: [f64; 2], s: f64, out: &mut Vec<(f64, f64)>) {
|
||||
out.clear();
|
||||
for (pts, _closed, inverted) in polys {
|
||||
let m = pts.len();
|
||||
if m < 2 {
|
||||
continue;
|
||||
}
|
||||
// Crossings: (t along d, |sin| of crossing angle).
|
||||
let mut xs: Vec<(f64, f64)> = Vec::new();
|
||||
for i in 0..m {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % m]);
|
||||
let (na, nb) = (a[0] * n[0] + a[1] * n[1] - s, b[0] * n[0] + b[1] * n[1] - s);
|
||||
if (na > 0.0) == (nb > 0.0) {
|
||||
continue;
|
||||
}
|
||||
let t = na / (na - nb);
|
||||
let q = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
||||
let (ex, ey) = (b[0] - a[0], b[1] - a[1]);
|
||||
let el = (ex * ex + ey * ey).sqrt().max(1e-9);
|
||||
let sin = ((ex * n[0] + ey * n[1]) / el).abs().max(0.2);
|
||||
xs.push((q[0] * d[0] + q[1] * d[1], sin));
|
||||
}
|
||||
xs.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
// Signed area decides which side is "left".
|
||||
let area: f64 = (0..m).map(|i| pts[i][0] * pts[(i + 1) % m][1] - pts[(i + 1) % m][0] * pts[i][1]).sum();
|
||||
let left_is_inside = area < 0.0;
|
||||
let mode = match fill {
|
||||
4 => {
|
||||
if left_is_inside {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
5 => {
|
||||
if left_is_inside {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
f => f,
|
||||
};
|
||||
let mode = if *inverted && mode == 0 { 6 } else { mode };
|
||||
for (k, &(t, sin)) in xs.iter().enumerate() {
|
||||
let entering = k % 2 == 0;
|
||||
let l = ew / sin;
|
||||
match mode {
|
||||
0 if entering && k + 1 < xs.len() => {
|
||||
out.push((t, xs[k + 1].0));
|
||||
}
|
||||
1 => out.push((t - l * 0.5, t + l * 0.5)),
|
||||
2 => out.push(if entering { (t, t + l) } else { (t - l, t) }),
|
||||
3 => out.push(if entering { (t - l, t) } else { (t, t + l) }),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if mode == 6 {
|
||||
// Inverted Inside: outside of the polygon within the bounds.
|
||||
let lo = pts.iter().map(|q| q[0] * d[0] + q[1] * d[1]).fold(f64::INFINITY, f64::min) - ew;
|
||||
let hi = pts.iter().map(|q| q[0] * d[0] + q[1] * d[1]).fold(f64::NEG_INFINITY, f64::max) + ew;
|
||||
let mut cur = lo;
|
||||
for pair in xs.chunks(2) {
|
||||
out.push((cur, pair[0].0));
|
||||
cur = pair.get(1).map(|v| v.0).unwrap_or(hi);
|
||||
}
|
||||
out.push((cur, hi));
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
// Merge overlaps.
|
||||
let mut merged: Vec<(f64, f64)> = Vec::with_capacity(out.len());
|
||||
for &(a, b) in out.iter() {
|
||||
if let Some(last) = merged.last_mut()
|
||||
&& a <= last.1
|
||||
{
|
||||
last.1 = last.1.max(b);
|
||||
continue;
|
||||
}
|
||||
merged.push((a, b));
|
||||
}
|
||||
*out = merged;
|
||||
}
|
||||
|
||||
/// End Cap and Join options (Scribble's Edge Options).
|
||||
pub(super) const END_CAPS: [&str; 3] = ["Round", "Butt", "Projecting"];
|
||||
|
||||
pub(super) const JOINS: [&str; 3] = ["Round", "Miter", "Bevel"];
|
||||
|
||||
/// Convex polygons whose union is the stroke of the polylines `lines` with half width `r`:
|
||||
/// one quad per segment, plus the joins (Round discs, Miter wedges up to `miter_limit` × the
|
||||
/// half width, Bevel triangles) and the caps (Round discs, Butt none, Projecting square ends).
|
||||
pub(crate) fn stroke_outline(lines: &[Vec<[f64; 2]>], r: f64, cap: u32, join: u32, miter_limit: f64) -> Vec<Vec<[f64; 2]>> {
|
||||
let disc = |c: [f64; 2]| -> Vec<[f64; 2]> {
|
||||
(0..16).map(|k| (k as f64 / 16.0 * std::f64::consts::TAU).sin_cos()).map(|(s, co)| [c[0] + co * r, c[1] + s * r]).collect()
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for l in lines {
|
||||
let pts: Vec<[f64; 2]> = l.iter().copied().fold(Vec::new(), |mut v, p| {
|
||||
if v.last().is_none_or(|q: &[f64; 2]| (q[0] - p[0]).hypot(q[1] - p[1]) > 1e-9) {
|
||||
v.push(p);
|
||||
}
|
||||
v
|
||||
});
|
||||
if pts.len() < 2 {
|
||||
if let (Some(p), 0) = (pts.first(), cap) {
|
||||
out.push(disc(*p));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let dir = |a: [f64; 2], b: [f64; 2]| {
|
||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||
let l = dx.hypot(dy).max(1e-12);
|
||||
[dx / l, dy / l]
|
||||
};
|
||||
let n = pts.len();
|
||||
for k in 0..n - 1 {
|
||||
let (mut a, mut b) = (pts[k], pts[k + 1]);
|
||||
let d = dir(a, b);
|
||||
// Projecting caps extend the end segments by the half width.
|
||||
if cap == 2 && k == 0 {
|
||||
a = [a[0] - d[0] * r, a[1] - d[1] * r];
|
||||
}
|
||||
if cap == 2 && k == n - 2 {
|
||||
b = [b[0] + d[0] * r, b[1] + d[1] * r];
|
||||
}
|
||||
let nn = [-d[1] * r, d[0] * r];
|
||||
out.push(vec![[a[0] + nn[0], a[1] + nn[1]], [b[0] + nn[0], b[1] + nn[1]], [b[0] - nn[0], b[1] - nn[1]], [a[0] - nn[0], a[1] - nn[1]]]);
|
||||
}
|
||||
// Joins.
|
||||
for k in 1..n - 1 {
|
||||
let (p, d0, d1) = (pts[k], dir(pts[k - 1], pts[k]), dir(pts[k], pts[k + 1]));
|
||||
let cross = d0[0] * d1[1] - d0[1] * d1[0];
|
||||
if cross.abs() < 1e-9 && d0[0] * d1[0] + d0[1] * d1[1] > 0.0 {
|
||||
continue;
|
||||
}
|
||||
if join == 0 {
|
||||
out.push(disc(p));
|
||||
continue;
|
||||
}
|
||||
// The outer side of the turn.
|
||||
let s = if cross > 0.0 { -1.0 } else { 1.0 };
|
||||
let o0 = [p[0] - d0[1] * r * s, p[1] + d0[0] * r * s];
|
||||
let o1 = [p[0] - d1[1] * r * s, p[1] + d1[0] * r * s];
|
||||
let bevel = vec![p, o0, o1];
|
||||
if join == 1 {
|
||||
// Miter point: where the two offset edges meet.
|
||||
let bis = [d0[0] - d1[0], d0[1] - d1[1]];
|
||||
let bl = bis[0].hypot(bis[1]);
|
||||
let cos_half = ((1.0 + d0[0] * d1[0] + d0[1] * d1[1]) * 0.5).max(0.0).sqrt();
|
||||
let len = if cos_half > 1e-6 { r / cos_half } else { f64::INFINITY };
|
||||
if bl > 1e-9 && len <= miter_limit * r {
|
||||
let m = [p[0] + (o0[0] + o1[0] - 2.0 * p[0]) * 0.5, p[1] + (o0[1] + o1[1] - 2.0 * p[1]) * 0.5];
|
||||
let ml = (m[0] - p[0]).hypot(m[1] - p[1]).max(1e-12);
|
||||
let tip = [p[0] + (m[0] - p[0]) / ml * len, p[1] + (m[1] - p[1]) / ml * len];
|
||||
out.push(vec![p, o0, tip, o1]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(bevel);
|
||||
}
|
||||
if cap == 0 {
|
||||
out.push(disc(pts[0]));
|
||||
out.push(disc(pts[n - 1]));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Coverage (union, 4×4 samples per pixel) of convex polygons on a `w`×`h` grid.
|
||||
pub(crate) fn raster_convex(w: usize, h: usize, polys: &[Vec<[f64; 2]>]) -> Plane {
|
||||
let mut out = Plane::new(w, h);
|
||||
let inside = |poly: &[[f64; 2]], x: f64, y: f64| {
|
||||
let n = poly.len();
|
||||
let mut sign = 0.0;
|
||||
for i in 0..n {
|
||||
let (a, b) = (poly[i], poly[(i + 1) % n]);
|
||||
let c = (b[0] - a[0]) * (y - a[1]) - (b[1] - a[1]) * (x - a[0]);
|
||||
if c.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
if sign == 0.0 {
|
||||
sign = c.signum();
|
||||
} else if c.signum() != sign {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
};
|
||||
for poly in polys {
|
||||
if poly.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
||||
for p in poly {
|
||||
x0 = x0.min(p[0]);
|
||||
y0 = y0.min(p[1]);
|
||||
x1 = x1.max(p[0]);
|
||||
y1 = y1.max(p[1]);
|
||||
}
|
||||
let (xa, xb) = (x0.floor().max(0.0) as usize, (x1.ceil().max(0.0) as usize).min(w));
|
||||
let (ya, yb) = (y0.floor().max(0.0) as usize, (y1.ceil().max(0.0) as usize).min(h));
|
||||
for y in ya..yb {
|
||||
for x in xa..xb {
|
||||
let mut hit = 0;
|
||||
for sy in 0..4 {
|
||||
for sx in 0..4 {
|
||||
if inside(poly, x as f64 + (sx as f64 + 0.5) / 4.0, y as f64 + (sy as f64 + 0.5) / 4.0) {
|
||||
hit += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let v = &mut out.data[y * w + x];
|
||||
*v = v.max(hit as f32 / 16.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
use super::audio::{audio_spectrum, audio_waveform};
|
||||
use super::common::{pct, pt, spec};
|
||||
use super::fill::{eyedropper_fill, glue_gun, paint_bucket, threads};
|
||||
use super::fractal::{FRACTAL_OVERSAMPLE, FRACTAL_PALETTES, FRACTAL_SETS, fractal};
|
||||
use super::paths::{scribble, stroke, vegas, write_on};
|
||||
use super::raster::{END_CAPS, JOINS};
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------- registry
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let ang = || ParamUi::Angle;
|
||||
let check = |id: &'static str, name: &'static str, v: bool| p(id, name, Value::Bool(v), ParamUi::Checkbox);
|
||||
let paint_style = || popup(&["On Original Image", "On Transparent", "Reveal Original Image"]);
|
||||
let mask_idx = |id: &'static str, name: &'static str, d: f64| p(id, name, num(d), ParamUi::Mask);
|
||||
let display = || popup(&["Digital", "Analog lines", "Analog dots"]);
|
||||
vec![
|
||||
spec(
|
||||
"ec.generate.fractal",
|
||||
"Fractal",
|
||||
vec![
|
||||
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("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,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.scribble",
|
||||
"Scribble",
|
||||
vec![
|
||||
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("edgeOptions/edgeWidth", "Edge Width", num(10.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("edgeOptions/endCap", "End Cap", Value::Enum(0), popup(&END_CAPS)),
|
||||
p("edgeOptions/join", "Join", Value::Enum(0), popup(&JOINS)),
|
||||
p("edgeOptions/miterLimit", "Miter Limit", num(4.0), slider(1.0, 100.0, 1.0, 20.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("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()),
|
||||
p("startEndApplyTo", "Start/End Apply To", Value::Enum(0), popup(&["All Strokes", "Each Stroke"])),
|
||||
check("fillPathsSequentially", "Fill Paths Sequentially", true),
|
||||
p("wiggleType", "Wiggle Type", Value::Enum(1), popup(&["Static", "Jumpy", "Smooth"])),
|
||||
p("wigglesPerSecond", "Wiggles/Second", num(5.0), slider(0.0, 100.0, 0.0, 30.0, 2)),
|
||||
p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10000.0, 0.0, 100.0, 0)),
|
||||
p("composite", "Composite", Value::Enum(1), popup(&["On Original Image", "On Transparent", "Reveal Original Image"])),
|
||||
],
|
||||
scribble,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.stroke",
|
||||
"Stroke",
|
||||
vec![
|
||||
mask_idx("path", "Path", 1.0),
|
||||
check("allMasks", "All Masks", false),
|
||||
check("strokeSequentially", "Stroke Sequentially", true),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("brushSize", "Brush Size", num(2.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("brushHardness", "Brush Hardness", num(75.0), pct()),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p("start", "Start", num(0.0), pct()),
|
||||
p("end", "End", num(100.0), pct()),
|
||||
p("spacing", "Spacing", num(15.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("paintStyle", "Paint Style", Value::Enum(0), paint_style()),
|
||||
],
|
||||
stroke,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.vegas",
|
||||
"Vegas",
|
||||
vec![
|
||||
p("stroke", "Stroke", Value::Enum(0), popup(&["Image Contours", "Mask/Path"])),
|
||||
p("inputLayer", "Image Contours: Input Layer", Value::Layer(None), ParamUi::Layer),
|
||||
check("invertInput", "Image Contours: Invert Input", false),
|
||||
p("ifLayerSizesDiffer", "Image Contours: If Layer Sizes Differ", Value::Enum(0), popup(&["Center", "Stretch to Fit"])),
|
||||
p("channel", "Image Contours: Channel", Value::Enum(0), popup(&["Intensity", "Red", "Green", "Blue", "Alpha"])),
|
||||
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", 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)),
|
||||
p("segmentDistribution", "Segments: Segment Distribution", Value::Enum(0), popup(&["Bunched", "Even"])),
|
||||
p("rotation", "Segments: Rotation", num(0.0), ang()),
|
||||
check("randomPhase", "Segments: Random Phase", false),
|
||||
p("randomSeed", "Segments: Random Seed", num(1.0), slider(1.0, 10000.0, 1.0, 100.0, 0)),
|
||||
p("blendMode", "Rendering: Blend Mode", Value::Enum(1), popup(&["Transparent", "Over", "Under", "Stencil"])),
|
||||
p("color", "Rendering: Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("width", "Rendering: Width", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("hardness", "Rendering: Hardness", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("startOpacity", "Rendering: Start Opacity", num(1.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("midpointOpacity", "Rendering: Mid-point Opacity", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("midpointPosition", "Rendering: Mid-point Position", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("endOpacity", "Rendering: End Opacity", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
],
|
||||
vegas,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.writeon",
|
||||
"Write-on",
|
||||
vec![
|
||||
p("brushPosition", "Brush Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("brushSize", "Brush Size", num(6.0), slider(0.0, 50.0, 0.0, 50.0, 1)),
|
||||
p("brushHardness", "Brush Hardness", num(75.0), pct()),
|
||||
p("brushOpacity", "Brush Opacity", num(100.0), pct()),
|
||||
p("strokeLength", "Stroke Length (secs)", num(0.0), slider(0.0, 30.0, 0.0, 30.0, 1)),
|
||||
p("brushSpacing", "Brush Spacing (secs)", num(0.001), slider(0.001, 5.0, 0.001, 5.0, 3)),
|
||||
p("paintTimeProperties", "Paint Time Properties", Value::Enum(0), popup(&["None", "Opacity", "Color"])),
|
||||
p("brushTimeProperties", "Brush Time Properties", Value::Enum(0), popup(&["None", "Size", "Hardness", "Size & Hardness"])),
|
||||
p("paintStyle", "Paint Style", Value::Enum(0), paint_style()),
|
||||
],
|
||||
write_on,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.paintbucket",
|
||||
"Paint Bucket",
|
||||
vec![
|
||||
p("fillPoint", "Fill Point", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("fillSelector", "Fill Selector", Value::Enum(0), popup(&["Color & Alpha", "Straight Color", "Transparency", "Opacity", "Alpha Channel"])),
|
||||
p("tolerance", "Tolerance", num(10.0), slider(0.0, 255.0, 0.0, 100.0, 1)),
|
||||
check("viewThreshold", "View Threshold", false),
|
||||
p("stroke", "Stroke", Value::Enum(0), popup(&["Antialias", "Feather", "Spread", "Choke", "Stroke"])),
|
||||
p("featherSoftness", "Feather Softness", num(10.0), slider(0.0, 100.0, 0.0, 50.0, 1)),
|
||||
p("spreadRadius", "Spread Radius", num(3.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("strokeWidth", "Stroke Width", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||||
p("color", "Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p(
|
||||
"blendingMode",
|
||||
"Blending Mode",
|
||||
Value::Enum(0),
|
||||
popup(&["Normal", "Add", "Multiply", "Screen", "Hue", "Saturation", "Color", "Luminosity", "Behind", "Fill Only"]),
|
||||
),
|
||||
],
|
||||
paint_bucket,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.eyedropperfill",
|
||||
"Eyedropper Fill",
|
||||
vec![
|
||||
p("samplePoint", "Sample Point", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("sampleRadius", "Sample Radius", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("averagePixelColors", "Average Pixel Colors", Value::Enum(0), popup(&["Skip Empty", "All", "All Premultiplied", "Including Alpha"])),
|
||||
check("maintainOriginalAlpha", "Maintain Original Alpha", false),
|
||||
p("blendWithOriginal", "Blend With Original", num(0.0), pct()),
|
||||
],
|
||||
eyedropper_fill,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.ccgluegun",
|
||||
"CC Glue Gun",
|
||||
vec![
|
||||
p("brushPosition", "Brush Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("strokeWidth", "Stroke Width", num(25.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||||
p("density", "Density", num(5.0), slider(0.0, 32.0, 0.0, 32.0, 1)),
|
||||
p("timeSpan", "Time Span (sec)", num(1.0), slider(0.0, 60.0, 0.0, 10.0, 2)),
|
||||
p("reflection", "Reflection", num(45.0), ang()),
|
||||
p("strength", "Strength", num(100.0), pct()),
|
||||
p("style", "Style", Value::Enum(0), popup(&["Over Layer", "Glue Only"])),
|
||||
],
|
||||
glue_gun,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.ccthreads",
|
||||
"CC Threads",
|
||||
vec![
|
||||
p("width", "Width", num(50.0), slider(1.0, 1000.0, 1.0, 200.0, 1)),
|
||||
p("height", "Height", num(50.0), slider(1.0, 1000.0, 1.0, 200.0, 1)),
|
||||
p("overlaps", "Overlaps", num(1.0), slider(1.0, 20.0, 1.0, 10.0, 0)),
|
||||
p("direction", "Direction", num(0.0), ang()),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("coverage", "Coverage", num(80.0), pct()),
|
||||
p("shadowing", "Shadowing", num(50.0), pct()),
|
||||
p("texture", "Texture", num(10.0), pct()),
|
||||
],
|
||||
threads,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.audiospectrum",
|
||||
"Audio Spectrum",
|
||||
vec![
|
||||
p("audioLayer", "Audio Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("startPoint", "Start Point", pt(0.1, 0.5), ParamUi::Point),
|
||||
p("endPoint", "End Point", pt(0.9, 0.5), ParamUi::Point),
|
||||
mask_idx("path", "Path", 0.0),
|
||||
check("usePolarPath", "Use Polar Path", false),
|
||||
p("startFrequency", "Start Frequency", num(20.0), slider(1.0, 20000.0, 1.0, 2000.0, 0)),
|
||||
p("endFrequency", "End Frequency", num(2000.0), slider(1.0, 20000.0, 1.0, 10000.0, 0)),
|
||||
p("frequencyBands", "Frequency bands", num(64.0), slider(1.0, 4096.0, 1.0, 256.0, 0)),
|
||||
p("maximumHeight", "Maximum Height", num(50.0), slider(0.0, 10000.0, 0.0, 500.0, 1)),
|
||||
p("audioDuration", "Audio Duration (milliseconds)", num(90.0), slider(1.0, 30000.0, 1.0, 1000.0, 1)),
|
||||
p("audioOffset", "Audio Offset (milliseconds)", num(0.0), slider(-30000.0, 30000.0, -1000.0, 1000.0, 1)),
|
||||
p("thickness", "Thickness", num(3.0), slider(0.0, 1000.0, 0.0, 50.0, 2)),
|
||||
p("softness", "Softness", num(50.0), pct()),
|
||||
p("insideColor", "Inside Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("outsideColor", "Outside Color", col(0.2, 0.4, 1.0), ParamUi::Color),
|
||||
check("blendOverlappingColors", "Blend Overlapping Colors", true),
|
||||
p("hueInterpolation", "Hue Interpolation", num(0.0), ang()),
|
||||
check("dynamicHuePhase", "Dynamic Hue Phase", false),
|
||||
check("colorSymmetry", "Color Symmetry", false),
|
||||
p("displayOptions", "Display Options", Value::Enum(0), display()),
|
||||
p("sideOptions", "Side Options", Value::Enum(0), popup(&["Side A", "Side B", "Side A & B"])),
|
||||
check("durationAveraging", "Duration Averaging", false),
|
||||
check("compositeOnOriginal", "Composite On Original", false),
|
||||
],
|
||||
audio_spectrum,
|
||||
),
|
||||
spec(
|
||||
"ec.generate.audiowaveform",
|
||||
"Audio Waveform",
|
||||
vec![
|
||||
p("audioLayer", "Audio Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("startPoint", "Start Point", pt(0.1, 0.5), ParamUi::Point),
|
||||
p("endPoint", "End Point", pt(0.9, 0.5), ParamUi::Point),
|
||||
mask_idx("path", "Path", 0.0),
|
||||
check("usePolarPath", "Use Polar Path", false),
|
||||
p("displayedSamples", "Displayed Samples", num(200.0), slider(2.0, 4096.0, 2.0, 1000.0, 0)),
|
||||
p("maximumHeight", "Maximum Height", num(100.0), slider(0.0, 10000.0, 0.0, 500.0, 1)),
|
||||
p("audioDuration", "Audio Duration (milliseconds)", num(200.0), slider(1.0, 30000.0, 1.0, 1000.0, 1)),
|
||||
p("audioOffset", "Audio Offset (milliseconds)", num(0.0), slider(-30000.0, 30000.0, -1000.0, 1000.0, 1)),
|
||||
p("thickness", "Thickness", num(2.0), slider(0.0, 1000.0, 0.0, 50.0, 2)),
|
||||
p("softness", "Softness", num(10.0), pct()),
|
||||
p("randomSeed", "Random Seed (Analog)", num(1.0), slider(1.0, 10000.0, 1.0, 100.0, 0)),
|
||||
p("insideColor", "Inside Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("outsideColor", "Outside Color", col(0.2, 0.4, 1.0), ParamUi::Color),
|
||||
p("waveformOptions", "Waveform Options", Value::Enum(0), popup(&["Mono", "Left", "Right"])),
|
||||
p("displayOptions", "Display Options", Value::Enum(1), display()),
|
||||
check("compositeOnOriginal", "Composite On Original", false),
|
||||
],
|
||||
audio_waveform,
|
||||
),
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user