mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 18:47:07 -06:00
Align every effect's display name, category, parameter names, order, twirl-down groups, popup options, units, defaults and ranges with the public After Effects effect reference, and implement controls that were ignored or missing where cheap. One commit because the categories share the registry, migration tables and GPU kernels (only green states). Infrastructure - effects::migrate: saved instances are upgraded on project open (Session::replace_project → upgrade_effects): renamed ids (PARAM_ID_ALIASES), params moved into twirl-down groups, popup values remapped by label (POPUP_LABEL_ALIASES), refreshed names/UI hints, added params created with legacy_default values that keep old renders. - flatten_params also keys nested props by match path (group/param), so effects declare grouped params with `group/param` ids; layer-source companions sit next to grouped layer params. - lookup() resolves old effect names (EFFECT_NAME_ALIASES). - Registry-wide test: unique ids/names, non-empty names, defaults in range, popup defaults within options, value kinds match UI. - docs/effects.md generated from the registry (catalog.rs) with GPU / 32-bpc badges and per-effect status; a test keeps it current. Batches - Blur & Sharpen, Channel, 3D Channel, Perspective, Text, Utility: Camera Lens Blur iris/highlight groups, Compound Blur layer, Camera- Shake Deblur method/strength, Invert full channel list (CPU HLS/YIQ), Set Channels/Set Matte layers, Channel Combiner targets, 3D Channel Extract clamp/invert. - Color Correction: temporal smoothing + scene detect for Auto Color/ Contrast/Levels and Shadow/Highlight, Brightness & Contrast legacy switch, Change to Color tolerance group, Leave Color, Photo Filter presets, Exposure channels, Levels clip popups, Colorama phase source, Color Link sample list, Lumetri groups (CPU + GPU kernels). - Distort: Bulge, Displacement Map layer/behaviour, Magnify, Optics Compensation resize, Ripple conversion, Transform sampling, Turbulent Displace types/pinning/evolution, Wave Warp types/pinning, Twirl and Ripple radius fixes. - Generate: 4-Color Gradient, Checkerboard, Grid, Circle size/feather/ blending, Advanced Lightning types, Cell Pattern, Radio Waves, Fractal palettes, Scribble/Vegas names. - Keying, Matte, Obsolete: Color Difference Key partials/views, Unmult controls, Key Light groups, Refine Soft/Hard Matte, Roto Brush groups, Mocha shape moved to Matte, Spill Suppressor accuracy, Basic/Path Text groups. - Noise & Grain, Immersive Video, Audio, Time: Fractal/Turbulent Noise full control set (noise3.rs, GPU), grain look groups, Remove Grain, VR effects, Compressor/Gate/Distortion controls, Timewarp groups, VR Sphere to Plane name. - Simulation: AE twirl-down groups for all simulations, Foam strength, Particle Playground barrel radius and mapper targets. - Stylize, Transition: Glow looping/dimensions/alpha (CPU + GPU), Mosaic averaging, Cartoon groups, Roughen Edges evolution, Texturize layer, Gradient Wipe layer, Radial Wipe both, Card Wipe options. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1655 lines
78 KiB
Rust
1655 lines
78 KiB
Rust
//! Simulation effects, part 2: layer-shattering and surface simulations — CC Ball Action,
|
||
//! CC Pixel Polly, CC Scatterize, Card Dance, Shatter (planar pieces in perspective), Caustics,
|
||
//! Wave World (stepped 2D wave equation) and Foam (stepped bubble sim).
|
||
//!
|
||
//! Written from the public descriptions of the effects' behaviour. Pieces are textured convex
|
||
//! polygons placed in 3D and drawn through a per-piece inverse homography, far to near.
|
||
|
||
use effectcraft_keyframe::Value;
|
||
use effectcraft_project::ParamUi;
|
||
use effectcraft_raster::{Image, Px};
|
||
use rayon::prelude::*;
|
||
|
||
use crate::generate::value_noise;
|
||
use crate::sim::{Acc, Shape, Sprite, h, hs, pct, pt, raster, spec, splat};
|
||
use crate::util::{Plane, SimCache, gauss_plane, layer_or_self, params_key, unpremul};
|
||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||
|
||
// ------------------------------------------------------------------ 3D pieces
|
||
|
||
type M3 = [[f64; 3]; 3];
|
||
|
||
fn mat_mul(a: &M3, b: &M3) -> M3 {
|
||
std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| a[i][k] * b[k][j]).sum()))
|
||
}
|
||
|
||
/// Rotation from Euler angles (radians), applied X then Y then Z.
|
||
fn rot_xyz(x: f64, y: f64, z: f64) -> M3 {
|
||
let (sx, cx) = x.sin_cos();
|
||
let (sy, cy) = y.sin_cos();
|
||
let (sz, cz) = z.sin_cos();
|
||
let rx = [[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]];
|
||
let ry = [[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]];
|
||
let rz = [[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]];
|
||
mat_mul(&rz, &mat_mul(&ry, &rx))
|
||
}
|
||
|
||
/// Rotation by `angle` about unit `axis` (Rodrigues).
|
||
fn rot_axis(axis: [f64; 3], angle: f64) -> M3 {
|
||
let l = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
|
||
if l < 1e-12 || angle == 0.0 {
|
||
return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||
}
|
||
let (x, y, z) = (axis[0] / l, axis[1] / l, axis[2] / l);
|
||
let (s, c) = angle.sin_cos();
|
||
let t = 1.0 - c;
|
||
[
|
||
[t * x * x + c, t * x * y - s * z, t * x * z + s * y],
|
||
[t * x * y + s * z, t * y * y + c, t * y * z - s * x],
|
||
[t * x * z - s * y, t * y * z + s * x, t * z * z + c],
|
||
]
|
||
}
|
||
|
||
fn inv3(m: &M3) -> Option<M3> {
|
||
let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
|
||
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
|
||
if det.abs() < 1e-12 {
|
||
return None;
|
||
}
|
||
let d = 1.0 / det;
|
||
Some([
|
||
[(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * d, (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * d, (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * d],
|
||
[(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * d, (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * d, (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * d],
|
||
[(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * d, (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * d, (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * d],
|
||
])
|
||
}
|
||
|
||
/// Perspective camera looking down +z at the layer plane z = 0, `d` pixels away, centred on
|
||
/// `c` (buffer pixels): screen = c + (P - c) · d / (d + z).
|
||
#[derive(Clone, Copy)]
|
||
struct Cam {
|
||
c: [f64; 2],
|
||
d: f64,
|
||
}
|
||
|
||
/// A planar textured piece ready to draw.
|
||
#[derive(Clone, Copy)]
|
||
struct Piece {
|
||
/// Screen (buffer px, homogeneous) → piece-local (a, b, w).
|
||
inv: M3,
|
||
/// Rest centre of the piece in texture pixels.
|
||
c0: [f32; 2],
|
||
/// Convex polygon (texture pixels), counter-clockwise in y-down space.
|
||
poly: [[f32; 2]; 6],
|
||
n: u8,
|
||
bbox: [f32; 4],
|
||
shade: f32,
|
||
alpha: f32,
|
||
/// Flat straight colour instead of the texture.
|
||
flat: Option<[f32; 3]>,
|
||
/// Facing away from the camera (draw the back texture).
|
||
back: bool,
|
||
depth: f64,
|
||
}
|
||
|
||
impl Piece {
|
||
/// Place polygon `poly` (texture px, around rest centre `c0`) with rotation `r` and its
|
||
/// centre at 3D `pos` (buffer px; z away from the camera).
|
||
fn new(poly: &[[f32; 2]], c0: [f32; 2], r: &M3, pos: [f64; 3], cam: Cam) -> Option<Piece> {
|
||
let n = poly.len().min(6);
|
||
if n < 3 {
|
||
return None;
|
||
}
|
||
let (cx, cy, d) = (cam.c[0], cam.c[1], cam.d);
|
||
let zc = pos[2];
|
||
let hm: M3 = [
|
||
[cx * r[2][0] + d * r[0][0], cx * r[2][1] + d * r[0][1], cx * (d + zc) + d * (pos[0] - cx)],
|
||
[cy * r[2][0] + d * r[1][0], cy * r[2][1] + d * r[1][1], cy * (d + zc) + d * (pos[1] - cy)],
|
||
[r[2][0], r[2][1], d + zc],
|
||
];
|
||
let mut bb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
|
||
for v in &poly[..n] {
|
||
let (a, b) = ((v[0] - c0[0]) as f64, (v[1] - c0[1]) as f64);
|
||
let w = hm[2][0] * a + hm[2][1] * b + hm[2][2];
|
||
if w < d * 0.05 {
|
||
return None;
|
||
}
|
||
let sx = ((hm[0][0] * a + hm[0][1] * b + hm[0][2]) / w) as f32;
|
||
let sy = ((hm[1][0] * a + hm[1][1] * b + hm[1][2]) / w) as f32;
|
||
bb = [bb[0].min(sx), bb[1].min(sy), bb[2].max(sx), bb[3].max(sy)];
|
||
}
|
||
let inv = inv3(&hm)?;
|
||
let mut pp = [[0.0f32; 2]; 6];
|
||
pp[..n].copy_from_slice(&poly[..n]);
|
||
// Normalise winding so "inside" is cross >= 0 for every edge.
|
||
let area: f32 = (0..n).map(|i| pp[i][0] * pp[(i + 1) % n][1] - pp[(i + 1) % n][0] * pp[i][1]).sum();
|
||
if area < 0.0 {
|
||
pp[..n].reverse();
|
||
}
|
||
// Front normal (0, 0, -1) rotated; facing the camera when its z is negative.
|
||
let nz = -r[2][2];
|
||
Some(Piece {
|
||
inv,
|
||
c0,
|
||
poly: pp,
|
||
n: n as u8,
|
||
bbox: [bb[0] - 1.0, bb[1] - 1.0, bb[2] + 1.0, bb[3] + 1.0],
|
||
shade: 1.0,
|
||
alpha: 1.0,
|
||
flat: None,
|
||
back: nz > 0.0,
|
||
depth: zc,
|
||
})
|
||
}
|
||
|
||
/// Lambert factor of the piece normal against a light shining along +z.
|
||
fn facing(r: &M3) -> f32 {
|
||
r[2][2].abs() as f32
|
||
}
|
||
}
|
||
|
||
/// Draw pieces (already sorted far → near) over `out`.
|
||
fn draw_pieces(out: &mut Image, pieces: &[Piece], front: &Image, back: Option<&Image>) {
|
||
raster(
|
||
out,
|
||
pieces,
|
||
|q| Some(q.bbox),
|
||
|q, x, y| {
|
||
let (xf, yf) = (x as f64, y as f64);
|
||
let m = &q.inv;
|
||
let w = m[2][0] * xf + m[2][1] * yf + m[2][2];
|
||
if w.abs() < 1e-12 {
|
||
return None;
|
||
}
|
||
let u = ((m[0][0] * xf + m[0][1] * yf + m[0][2]) / w) as f32 + q.c0[0];
|
||
let v = ((m[1][0] * xf + m[1][1] * yf + m[1][2]) / w) as f32 + q.c0[1];
|
||
let n = q.n as usize;
|
||
let mut md = f32::INFINITY;
|
||
for i in 0..n {
|
||
let a = q.poly[i];
|
||
let b = q.poly[(i + 1) % n];
|
||
let (ex, ey) = (b[0] - a[0], b[1] - a[1]);
|
||
let l = (ex * ex + ey * ey).sqrt().max(1e-6);
|
||
md = md.min((ex * (v - a[1]) - ey * (u - a[0])) / l);
|
||
}
|
||
// Coverage extends half a pixel past the edge so abutting pieces leave no seams.
|
||
let cov = (md + 1.0).clamp(0.0, 1.0);
|
||
if cov <= 0.0 {
|
||
return None;
|
||
}
|
||
let (c, a) = match q.flat {
|
||
Some(c) => (c, 1.0),
|
||
None => {
|
||
let tex = if q.back { back.unwrap_or(front) } else { front };
|
||
unpremul(tex.sample_bilinear(u as f64, v as f64))
|
||
}
|
||
};
|
||
let a = a * cov * q.alpha;
|
||
if a <= 1e-6 {
|
||
return None;
|
||
}
|
||
let k = q.shade;
|
||
Some([c[0] * k * a, c[1] * k * a, c[2] * k * a, a])
|
||
},
|
||
Acc::Over,
|
||
);
|
||
}
|
||
|
||
fn sort_far_first(pieces: &mut [Piece]) {
|
||
pieces.sort_by(|a, b| b.depth.total_cmp(&a.depth));
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Ball Action
|
||
|
||
fn ball_action(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let scatter = pr.f("scatter") as f32;
|
||
let axis = pr.e("rotationAxis");
|
||
let rot = pr.f("rotation").to_radians();
|
||
let twist_prop = pr.e("twistProperty");
|
||
let twist = pr.f("twistAngle").to_radians();
|
||
let spacing = pr.f("gridSpacing").max(1.0) as f32;
|
||
let size = pr.f("ballSize") as f32 / 100.0;
|
||
let instab = pr.f("instabilityState").to_radians() as f32;
|
||
let seed = ctx.seed.wrapping_mul(0x2545f491);
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let s = b.scale as f32;
|
||
let nx = (lw / spacing).ceil().max(1.0) as u32;
|
||
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
||
let d = 2.0 * lw.max(lh) as f64;
|
||
let (cx, cy) = (lw as f64 * 0.5, lh as f64 * 0.5);
|
||
let src = b.img.clone();
|
||
let ax: [f64; 3] = match axis {
|
||
0 => [1.0, 0.0, 0.0],
|
||
1 => [0.0, 1.0, 0.0],
|
||
2 => [0.0, 0.0, 1.0],
|
||
3 => [1.0, 1.0, 0.0],
|
||
4 => [1.0, 0.0, 1.0],
|
||
5 => [0.0, 1.0, 1.0],
|
||
_ => [1.0, 1.0, 1.0],
|
||
};
|
||
let maxr = ((lw * lw + lh * lh).sqrt() * 0.5).max(1.0);
|
||
let mut balls: Vec<(f64, Sprite)> = (0..nx * ny)
|
||
.into_par_iter()
|
||
.filter_map(|i| {
|
||
let gx = (i % nx) as f32 * spacing + spacing * 0.5;
|
||
let gy = (i / nx) as f32 * spacing + spacing * 0.5;
|
||
let (sx, sy) = b.to_px([gx as f64, gy as f64]);
|
||
let (c, a) = unpremul(src.sample_bilinear(sx, sy));
|
||
if a <= 0.0 || size <= 0.0 {
|
||
return None;
|
||
}
|
||
let tp = match twist_prop {
|
||
0 => gx / lw.max(1.0),
|
||
1 => gy / lh.max(1.0),
|
||
2 => ((gx - cx as f32).hypot(gy - cy as f32)) / maxr,
|
||
_ => crate::util::rgb3([c[0], c[1], c[2], 0.0]).iter().sum::<f32>() / 3.0,
|
||
} as f64;
|
||
let ang = rot + twist * tp;
|
||
// Scatter: random 3D offsets whose direction drifts with the instability state.
|
||
let ph = h(i, 1, seed) * std::f32::consts::TAU + instab;
|
||
let sc = scatter * h(i, 2, seed);
|
||
let off = [(sc * ph.cos()) as f64, (sc * ph.sin()) as f64, (scatter * hs(i, 3, seed)) as f64];
|
||
let p0 = [gx as f64 - cx + off[0], gy as f64 - cy + off[1], off[2]];
|
||
let m = rot_axis(ax, ang);
|
||
let q: [f64; 3] = std::array::from_fn(|k| m[k][0] * p0[0] + m[k][1] * p0[1] + m[k][2] * p0[2]);
|
||
if d + q[2] <= d * 0.05 {
|
||
return None;
|
||
}
|
||
let persp = d / (d + q[2]);
|
||
let (bx, by) = b.to_px([cx + q[0] * persp, cy + q[1] * persp]);
|
||
let r = spacing * 0.5 * size * persp as f32 * s;
|
||
Some((q[2], Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], a], Shape::Sphere)))
|
||
})
|
||
.collect();
|
||
balls.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||
let sprites: Vec<Sprite> = balls.into_iter().map(|(_, s)| s).collect();
|
||
b.img = splat(src.width, src.height, &sprites, Acc::Over);
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Pixel Polly
|
||
|
||
fn pixel_polly(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let t = ctx.time - pr.f("startTime");
|
||
if t <= 0.0 {
|
||
return b;
|
||
}
|
||
let force = pr.f("force");
|
||
let gravity = pr.f("gravity");
|
||
let spin = pr.f("spinning").to_radians();
|
||
let fc = pr.v2("forceCenter");
|
||
let dir_rand = pr.f("directionRandomness") / 100.0;
|
||
let spd_rand = pr.f("speedRandomness") / 100.0;
|
||
let g = pr.f("gridSpacing").max(1.0);
|
||
let object = pr.e("object");
|
||
let depth_sort = pr.b("enableDepthSort");
|
||
let seed = ctx.seed.wrapping_mul(0x632be5ab);
|
||
let [lw, lh] = ctx.layer_size;
|
||
let s = b.scale;
|
||
let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: 2.0 * lw.max(lh) * s };
|
||
let nx = (lw / g).ceil().max(1.0) as u32;
|
||
let ny = (lh / g).ceil().max(1.0) as u32;
|
||
let tri = object <= 1;
|
||
let textured = object == 1 || object == 3;
|
||
let maxd = lw.max(lh);
|
||
let src = b.img.clone();
|
||
let per = if tri { 2 } else { 1 };
|
||
let mut pieces: Vec<Piece> = (0..nx * ny * per)
|
||
.into_par_iter()
|
||
.filter_map(|i| {
|
||
let cell = i / per;
|
||
let (x0, y0) = ((cell % nx) as f64 * g, (cell / nx) as f64 * g);
|
||
let poly: Vec<[f64; 2]> = if !tri {
|
||
vec![[x0, y0], [x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]]
|
||
} else if i % 2 == 0 {
|
||
vec![[x0, y0], [x0 + g, y0], [x0, y0 + g]]
|
||
} else {
|
||
vec![[x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]]
|
||
};
|
||
let c = [poly.iter().map(|v| v[0]).sum::<f64>() / poly.len() as f64, poly.iter().map(|v| v[1]).sum::<f64>() / poly.len() as f64];
|
||
let (dx, dy) = (c[0] - fc[0], c[1] - fc[1]);
|
||
let dist = (dx * dx + dy * dy).sqrt().max(1e-6);
|
||
let ang = dy.atan2(dx) + dir_rand * std::f64::consts::PI * hs(i, 1, seed) as f64;
|
||
let speed = force / 100.0 * 0.6 * maxd / (1.0 + dist / (0.5 * maxd)) * (1.0 + spd_rand * hs(i, 2, seed) as f64);
|
||
let vz = -speed * 0.5 * h(i, 3, seed) as f64;
|
||
let pos = [c[0] + ang.cos() * speed * t, c[1] + ang.sin() * speed * t + 0.5 * gravity * 0.5 * lh * t * t, vz * t];
|
||
let axis = [hs(i, 4, seed) as f64, hs(i, 5, seed) as f64, hs(i, 6, seed) as f64];
|
||
let r = rot_axis(axis, t * (std::f64::consts::PI * h(i, 7, seed) as f64 + spin));
|
||
let tex: Vec<[f32; 2]> = poly.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect();
|
||
let c0 = [(c[0] * s + b.offset[0]) as f32, (c[1] * s + b.offset[1]) as f32];
|
||
let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s];
|
||
let mut pc = Piece::new(&tex, c0, &r, posb, cam)?;
|
||
pc.shade = 0.35 + 0.65 * Piece::facing(&r);
|
||
if !textured {
|
||
let (cc, a) = unpremul(src.sample_bilinear(c0[0] as f64, c0[1] as f64));
|
||
if a <= 0.0 {
|
||
return None;
|
||
}
|
||
pc.flat = Some(cc);
|
||
pc.alpha = a;
|
||
}
|
||
Some(pc)
|
||
})
|
||
.collect();
|
||
if depth_sort {
|
||
sort_far_first(&mut pieces);
|
||
}
|
||
let mut out = Image::new(src.width, src.height);
|
||
draw_pieces(&mut out, &pieces, &src, None);
|
||
b.img = out;
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Scatterize
|
||
|
||
fn scatterize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let scatter = pr.f("scatter") as f32;
|
||
let right = pr.f("rightTwist").to_radians();
|
||
let left = pr.f("leftTwist").to_radians();
|
||
if scatter == 0.0 && right == 0.0 && left == 0.0 {
|
||
return b;
|
||
}
|
||
let add = pr.e("transferMode") == 1;
|
||
let seed = ctx.seed.wrapping_mul(0x7feb352d);
|
||
let s = b.scale;
|
||
let (w, hh) = (b.img.width, b.img.height);
|
||
let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s);
|
||
let half = (ctx.layer_size[0] * 0.5 * s).max(1.0);
|
||
let d = 2.0 * ctx.layer_size[0].max(ctx.layer_size[1]) * s;
|
||
let src = &b.img;
|
||
let sprites: Vec<Sprite> = (0..w * hh)
|
||
.into_par_iter()
|
||
.filter_map(|i| {
|
||
let px = src.data[i as usize];
|
||
if px[3] <= 0.0 {
|
||
return None;
|
||
}
|
||
let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
|
||
// Twist about the horizontal axis through the centre, growing towards each side.
|
||
let k = ((x - cx) / half).abs().min(1.0);
|
||
let ang = if x < cx { left } else { right } * k;
|
||
let (sa, ca) = ang.sin_cos();
|
||
let (yy, zz) = ((y - cy) * ca, (y - cy) * sa);
|
||
let persp = d / (d + zz).max(d * 0.05);
|
||
let jx = hs(i, 1, seed) * scatter * s as f32;
|
||
let jy = hs(i, 2, seed) * scatter * s as f32;
|
||
let sx = cx + (x - cx) * persp + jx as f64;
|
||
let sy = cy + yy * persp + jy as f64;
|
||
let (c, a) = unpremul(px);
|
||
Some(Sprite::new(sx as f32, sy as f32, 0.6 * persp as f32, [c[0], c[1], c[2], a], Shape::Disc))
|
||
})
|
||
.collect();
|
||
b.img = splat(w, hh, &sprites, if add { Acc::Add } else { Acc::Over });
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ Card Dance
|
||
|
||
const CD_SOURCES: &[&str] = &["None", "Intensity 1", "Red 1", "Green 1", "Blue 1", "Alpha 1", "Intensity 2", "Red 2", "Green 2", "Blue 2", "Alpha 2"];
|
||
/// (param id prefix, twirl-down group match id) per transformed card property.
|
||
const CD_PROPS: [(&str, &str); 8] = [
|
||
("xPos", "xPosition"),
|
||
("yPos", "yPosition"),
|
||
("zPos", "zPosition"),
|
||
("xRot", "xRotation"),
|
||
("yRot", "yRotation"),
|
||
("zRot", "zRotation"),
|
||
("xScale", "xScale"),
|
||
("yScale", "yScale"),
|
||
];
|
||
|
||
fn card_source(src: u32, g1: [f32; 4], g2: [f32; 4]) -> f32 {
|
||
let (c1, a1) = unpremul(g1);
|
||
let (c2, a2) = unpremul(g2);
|
||
let lum = |c: [f32; 3]| effectcraft_color::luminance(c[0], c[1], c[2]);
|
||
match src {
|
||
1 => lum(c1),
|
||
2 => c1[0],
|
||
3 => c1[1],
|
||
4 => c1[2],
|
||
5 => a1,
|
||
6 => lum(c2),
|
||
7 => c2[0],
|
||
8 => c2[1],
|
||
9 => c2[2],
|
||
10 => a2,
|
||
_ => 0.0,
|
||
}
|
||
}
|
||
|
||
fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let rows = pr.f("rows").clamp(1.0, 1000.0) as u32;
|
||
let cols = if pr.e("rowsColumns") == 1 { rows } else { pr.f("columns").clamp(1.0, 1000.0) as u32 };
|
||
let g1 = layer_or_self(ctx, &b, "gradientLayer1", true, true);
|
||
let g2 = layer_or_self(ctx, &b, "gradientLayer2", true, true);
|
||
let back = ctx.layer_param("backLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true));
|
||
let [lw, lh] = ctx.layer_size;
|
||
let s = b.scale;
|
||
let (cw, ch) = (lw / cols as f64, lh / rows as f64);
|
||
let zpos = pr.f("cameraPosition/cameraZ").max(0.05);
|
||
let focal = pr.f("cameraPosition/focalLength").max(1.0);
|
||
let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: lw.max(lh) * s * zpos * focal / 140.0 };
|
||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||
let diffuse = pr.f("material/diffuse") as f32;
|
||
let intensity = pr.f("lighting/lightIntensity") as f32;
|
||
let props: Vec<(u32, f64, f64)> =
|
||
CD_PROPS.iter().map(|(id, g)| (pr.e(&format!("{g}/{id}Source")), pr.f(&format!("{g}/{id}Multiplier")), pr.f(&format!("{g}/{id}Offset")))).collect();
|
||
let src = b.img.clone();
|
||
let mut pieces: Vec<Piece> = (0..rows * cols)
|
||
.into_par_iter()
|
||
.filter_map(|i| {
|
||
let (cx0, cy0) = ((i % cols) as f64 * cw, (i / cols) as f64 * ch);
|
||
let c = [cx0 + cw * 0.5, cy0 + ch * 0.5];
|
||
let (bx, by) = (c[0] * s + b.offset[0], c[1] * s + b.offset[1]);
|
||
let (gp1, gp2) = (g1.sample_bilinear(bx, by), g2.sample_bilinear(bx, by));
|
||
let val = |k: usize| {
|
||
let (sr, m, o) = props[k];
|
||
o + m * card_source(sr, gp1, gp2) as f64
|
||
};
|
||
let (sx, sy) = (val(6), val(7));
|
||
let r = rot_xyz(val(3).to_radians(), val(4).to_radians(), val(5).to_radians());
|
||
let rs: M3 = std::array::from_fn(|k| [r[k][0] * sx, r[k][1] * sy, r[k][2]]);
|
||
let pos = [bx + val(0) * cw * s, by + val(1) * ch * s, val(2) * cw.max(ch) * s];
|
||
let (tx0, ty0) = ((cx0 * s + b.offset[0]) as f32, (cy0 * s + b.offset[1]) as f32);
|
||
let (tw, th) = ((cw * s) as f32, (ch * s) as f32);
|
||
let poly = [[tx0, ty0], [tx0 + tw, ty0], [tx0 + tw, ty0 + th], [tx0, ty0 + th]];
|
||
let mut pc = Piece::new(&poly, [bx as f32, by as f32], &rs, pos, cam)?;
|
||
pc.shade = ambient + diffuse * intensity * Piece::facing(&r);
|
||
Some(pc)
|
||
})
|
||
.collect();
|
||
sort_far_first(&mut pieces);
|
||
let mut out = Image::new(src.width, src.height);
|
||
draw_pieces(&mut out, &pieces, &src, back.as_ref());
|
||
b.img = out;
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ Shatter
|
||
|
||
const SHATTER_PATTERNS: &[&str] = &["Bricks", "Glass", "Hexagons", "Squares", "Triangles"];
|
||
|
||
/// Pattern cells (polygons in pattern space, before rotation) covering `[x0, x1]×[y0, y1]`.
|
||
fn pattern_cells(kind: u32, cell: f64, x0: f64, y0: f64, x1: f64, y1: f64, seed: u32) -> Vec<Vec<[f64; 2]>> {
|
||
let mut out = Vec::new();
|
||
let i0 = (x0 / cell).floor() as i64 - 1;
|
||
let i1 = (x1 / cell).ceil() as i64 + 1;
|
||
let j0 = (y0 / cell).floor() as i64 - 1;
|
||
let j1 = (y1 / cell).ceil() as i64 + 1;
|
||
match kind {
|
||
0 => {
|
||
let bh = cell * 0.5;
|
||
let r0 = (y0 / bh).floor() as i64 - 1;
|
||
let r1 = (y1 / bh).ceil() as i64 + 1;
|
||
for r in r0..=r1 {
|
||
let sh = if r.rem_euclid(2) == 1 { cell * 0.5 } else { 0.0 };
|
||
for i in i0..=i1 {
|
||
let (x, y) = (i as f64 * cell + sh, r as f64 * bh);
|
||
out.push(vec![[x, y], [x + cell, y], [x + cell, y + bh], [x, y + bh]]);
|
||
}
|
||
}
|
||
}
|
||
1 => {
|
||
// Jittered lattice split into triangles along a random diagonal.
|
||
let jit = |i: i64, j: i64| -> [f64; 2] {
|
||
let (a, b) = (i as u32, j as u32);
|
||
[i as f64 * cell + hs(a, b.wrapping_add(17), seed) as f64 * cell * 0.35, j as f64 * cell + hs(a, b.wrapping_add(91), seed) as f64 * cell * 0.35]
|
||
};
|
||
for j in j0..=j1 {
|
||
for i in i0..=i1 {
|
||
let (p00, p10, p11, p01) = (jit(i, j), jit(i + 1, j), jit(i + 1, j + 1), jit(i, j + 1));
|
||
if h(i as u32, j as u32, seed ^ 0x55) < 0.5 {
|
||
out.push(vec![p00, p10, p11]);
|
||
out.push(vec![p00, p11, p01]);
|
||
} else {
|
||
out.push(vec![p00, p10, p01]);
|
||
out.push(vec![p10, p11, p01]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
2 => {
|
||
let r = cell / 3f64.sqrt();
|
||
let dx = 1.5 * r;
|
||
let dy = 3f64.sqrt() * r;
|
||
let c0 = (x0 / dx).floor() as i64 - 1;
|
||
let c1 = (x1 / dx).ceil() as i64 + 1;
|
||
let rr0 = (y0 / dy).floor() as i64 - 1;
|
||
let rr1 = (y1 / dy).ceil() as i64 + 1;
|
||
for ci in c0..=c1 {
|
||
for rj in rr0..=rr1 {
|
||
let cx = ci as f64 * dx;
|
||
let cy = (rj as f64 + if ci.rem_euclid(2) == 1 { 0.5 } else { 0.0 }) * dy;
|
||
out.push(
|
||
(0..6)
|
||
.map(|k| {
|
||
let a = k as f64 * std::f64::consts::PI / 3.0;
|
||
[cx + r * a.cos(), cy + r * a.sin()]
|
||
})
|
||
.collect(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
3 => {
|
||
for j in j0..=j1 {
|
||
for i in i0..=i1 {
|
||
let (x, y) = (i as f64 * cell, j as f64 * cell);
|
||
out.push(vec![[x, y], [x + cell, y], [x + cell, y + cell], [x, y + cell]]);
|
||
}
|
||
}
|
||
}
|
||
_ => {
|
||
for j in j0..=j1 {
|
||
for i in i0..=i1 {
|
||
let (x, y) = (i as f64 * cell, j as f64 * cell);
|
||
if (i + j).rem_euclid(2) == 0 {
|
||
out.push(vec![[x, y], [x + cell, y], [x, y + cell]]);
|
||
out.push(vec![[x + cell, y], [x + cell, y + cell], [x, y + cell]]);
|
||
} else {
|
||
out.push(vec![[x, y], [x + cell, y], [x + cell, y + cell]]);
|
||
out.push(vec![[x, y], [x + cell, y + cell], [x, y + cell]]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `∫0^t e^{-ks} ds` and `∫0^t ∫0^u e^{-ks} ds du` (drag-damped ballistic factors).
|
||
fn drag_factors(k: f64, t: f64) -> (f64, f64) {
|
||
if k < 1e-6 {
|
||
(t, 0.5 * t * t)
|
||
} else {
|
||
let e = (-k * t).exp();
|
||
((1.0 - e) / k, t / k - (1.0 - e) / (k * k))
|
||
}
|
||
}
|
||
|
||
fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let view = pr.e("view");
|
||
let render = pr.e("render");
|
||
let kind = pr.e("shape/pattern");
|
||
let reps = pr.f("shape/repetitions").clamp(1.0, 500.0);
|
||
let dir = pr.f("shape/direction").to_radians();
|
||
let origin = pr.v2("shape/origin");
|
||
let forces = [
|
||
(pr.v2("force1/force1Position"), pr.f("force1/force1Depth"), pr.f("force1/force1Radius"), pr.f("force1/force1Strength")),
|
||
(pr.v2("force2/force2Position"), pr.f("force2/force2Depth"), pr.f("force2/force2Radius"), pr.f("force2/force2Strength")),
|
||
];
|
||
let rot_speed = pr.f("physics/rotationSpeed");
|
||
let tumble = pr.e("physics/tumbleAxis");
|
||
let randomness = pr.f("physics/randomness");
|
||
let k = pr.f("physics/viscosity").max(0.0) * 5.0;
|
||
let mass_var = pr.f("physics/massVariance") / 100.0;
|
||
let gravity = pr.f("physics/gravity");
|
||
let gdir = pr.f("physics/gravityDirection").to_radians();
|
||
let ginc = pr.f("physics/gravityInclination").to_radians();
|
||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||
let intensity = pr.f("lighting/lightIntensity") as f32;
|
||
let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x9e3779b1) ^ ctx.seed;
|
||
let [lw, lh] = ctx.layer_size;
|
||
let s = b.scale;
|
||
let t = ctx.time.max(0.0);
|
||
let unit = lw * 0.1;
|
||
let cell = lw / reps;
|
||
let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: 2.0 * lw.max(lh) * s };
|
||
let (sd, cd) = dir.sin_cos();
|
||
let to_layer = |q: [f64; 2]| [origin[0] + q[0] * cd - q[1] * sd, origin[1] + q[0] * sd + q[1] * cd];
|
||
// Pattern-space bounds of the layer rectangle.
|
||
let corners = [[0.0, 0.0], [lw, 0.0], [lw, lh], [0.0, lh]].map(|c: [f64; 2]| {
|
||
let (dx, dy) = (c[0] - origin[0], c[1] - origin[1]);
|
||
[dx * cd + dy * sd, -dx * sd + dy * cd]
|
||
});
|
||
let (px0, px1) = (corners.iter().map(|c| c[0]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[0]).fold(f64::NEG_INFINITY, f64::max));
|
||
let (py0, py1) = (corners.iter().map(|c| c[1]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[1]).fold(f64::NEG_INFINITY, f64::max));
|
||
let cells = pattern_cells(kind, cell, px0, py0, px1, py1, seed);
|
||
let g3 = [gdir.sin() * ginc.cos() * gravity * unit, -gdir.cos() * ginc.cos() * gravity * unit, ginc.sin() * gravity * unit];
|
||
let src = b.img.clone();
|
||
let mut pieces: Vec<Piece> = cells
|
||
.par_iter()
|
||
.enumerate()
|
||
.filter_map(|(i, poly)| {
|
||
let i = i as u32;
|
||
let lp: Vec<[f64; 2]> = poly.iter().map(|&q| to_layer(q)).collect();
|
||
let (mnx, mxx) = (lp.iter().map(|v| v[0]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[0]).fold(f64::NEG_INFINITY, f64::max));
|
||
let (mny, mxy) = (lp.iter().map(|v| v[1]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[1]).fold(f64::NEG_INFINITY, f64::max));
|
||
if mxx < 0.0 || mxy < 0.0 || mnx > lw || mny > lh {
|
||
return None;
|
||
}
|
||
let c = [lp.iter().map(|v| v[0]).sum::<f64>() / lp.len() as f64, lp.iter().map(|v| v[1]).sum::<f64>() / lp.len() as f64];
|
||
// Broken when inside a force sphere (centred `depth` in front of the layer).
|
||
let mut v = [0.0f64; 3];
|
||
let mut broken = false;
|
||
for (fp, fd, fr, fs) in forces {
|
||
let rad = fr * lw;
|
||
if rad <= 0.0 {
|
||
continue;
|
||
}
|
||
let dv = [c[0] - fp[0], c[1] - fp[1], fd * lw];
|
||
let dist = (dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2]).sqrt();
|
||
if dist < rad {
|
||
broken = true;
|
||
let f = fs * (1.0 - dist / rad) * unit / dist.max(1e-6);
|
||
for q in 0..3 {
|
||
v[q] += dv[q] * f;
|
||
}
|
||
}
|
||
}
|
||
if (broken && render == 1) || (!broken && render == 2) {
|
||
return None;
|
||
}
|
||
let mut pos = [c[0], c[1], 0.0];
|
||
let mut r = rot_axis([1.0, 0.0, 0.0], 0.0);
|
||
if broken && t > 0.0 {
|
||
let mass = 1.0 + mass_var * hs(i, 1, seed) as f64;
|
||
for q in 0..3 {
|
||
v[q] = v[q] / mass + randomness * unit * hs(i, 2 + q as u32, seed) as f64 * 2.0;
|
||
}
|
||
let (f1, f2) = drag_factors(k, t);
|
||
for q in 0..3 {
|
||
pos[q] += v[q] * f1 + g3[q] * f2;
|
||
}
|
||
let axis = match tumble {
|
||
1 => [0.0, 0.0, 0.0],
|
||
2 => [1.0, 0.0, 0.0],
|
||
3 => [0.0, 1.0, 0.0],
|
||
4 => [0.0, 0.0, 1.0],
|
||
5 => [hs(i, 6, seed) as f64, hs(i, 7, seed) as f64, 0.0],
|
||
6 => [hs(i, 6, seed) as f64, 0.0, hs(i, 8, seed) as f64],
|
||
7 => [0.0, hs(i, 7, seed) as f64, hs(i, 8, seed) as f64],
|
||
_ => [hs(i, 6, seed) as f64, hs(i, 7, seed) as f64, hs(i, 8, seed) as f64],
|
||
};
|
||
let speed = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt() / unit;
|
||
let omega = rot_speed * std::f64::consts::TAU * (0.5 + h(i, 9, seed) as f64) * speed.min(10.0);
|
||
r = rot_axis(axis, omega * f1);
|
||
}
|
||
let tex: Vec<[f32; 2]> = lp.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect();
|
||
let c0 = [(c[0] * s + b.offset[0]) as f32, (c[1] * s + b.offset[1]) as f32];
|
||
let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s];
|
||
let mut pc = Piece::new(&tex, c0, &r, posb, cam)?;
|
||
pc.shade = ambient + (1.0 - ambient) * intensity * Piece::facing(&r);
|
||
Some(pc)
|
||
})
|
||
.collect();
|
||
sort_far_first(&mut pieces);
|
||
let mut out = Image::new(src.width, src.height);
|
||
if view == 0 {
|
||
draw_pieces(&mut out, &pieces, &src, None);
|
||
} else {
|
||
// Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces.
|
||
let animated = view == 2 || view == 4;
|
||
let mut lines = Vec::new();
|
||
for pc in &pieces {
|
||
let n = pc.n as usize;
|
||
let pts: Vec<[f32; 2]> = if animated {
|
||
let fw = inv3(&pc.inv);
|
||
match fw {
|
||
Some(m) => pc.poly[..n]
|
||
.iter()
|
||
.map(|v| {
|
||
let (a, bb) = ((v[0] - pc.c0[0]) as f64, (v[1] - pc.c0[1]) as f64);
|
||
let w = m[2][0] * a + m[2][1] * bb + m[2][2];
|
||
[((m[0][0] * a + m[0][1] * bb + m[0][2]) / w) as f32, ((m[1][0] * a + m[1][1] * bb + m[1][2]) / w) as f32]
|
||
})
|
||
.collect(),
|
||
None => continue,
|
||
}
|
||
} else {
|
||
pc.poly[..n].to_vec()
|
||
};
|
||
for e in 0..n {
|
||
let (a, c) = (pts[e], pts[(e + 1) % n]);
|
||
lines.push(Sprite::new(a[0], a[1], 0.5, [0.75, 0.85, 1.0, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] }));
|
||
}
|
||
}
|
||
if view >= 3 {
|
||
for (fp, _, fr, _) in forces {
|
||
let (fx, fy) = b.to_px(fp);
|
||
let rad = fr * lw * s;
|
||
for k in 0..64 {
|
||
let (a0, a1) = (k as f64 / 64.0 * std::f64::consts::TAU, (k + 1) as f64 / 64.0 * std::f64::consts::TAU);
|
||
let (x0, y0) = (fx + rad * a0.cos(), fy + rad * a0.sin());
|
||
let (x1, y1) = (fx + rad * a1.cos(), fy + rad * a1.sin());
|
||
lines.push(Sprite::new(x0 as f32, y0 as f32, 0.5, [1.0, 0.3, 0.3, 1.0], Shape::Line { dx: (x1 - x0) as f32, dy: (y1 - y0) as f32 }));
|
||
}
|
||
}
|
||
}
|
||
out = splat(src.width, src.height, &lines, Acc::Over);
|
||
}
|
||
b.img = out;
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ Caustics
|
||
|
||
/// Sample with repeat mode: 0 Once (transparent outside), 1 Tiled, 2 Reflected.
|
||
fn sample_repeat(img: &Image, x: f64, y: f64, mode: u32) -> Px {
|
||
let (w, hh) = (img.width as f64, img.height as f64);
|
||
match mode {
|
||
1 => img.sample_bilinear_clamped(x.rem_euclid(w), y.rem_euclid(hh)),
|
||
2 => {
|
||
let m = |v: f64, n: f64| {
|
||
let r = v.rem_euclid(2.0 * n);
|
||
if r > n { 2.0 * n - r } else { r }
|
||
};
|
||
img.sample_bilinear_clamped(m(x, w), m(y, hh))
|
||
}
|
||
_ => img.sample_bilinear(x, y),
|
||
}
|
||
}
|
||
|
||
fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let s = b.scale;
|
||
let mut bottom = layer_or_self(ctx, &b, "bottom/bottom", true, pr.e("bottom/bottomSizeDiffers") == 1);
|
||
let blur = pr.f("bottom/blur") * s;
|
||
if blur > 0.05 {
|
||
bottom = effectcraft_raster::gaussian_blur(&bottom, blur * 0.5, blur * 0.5, false);
|
||
}
|
||
let scaling = pr.f("bottom/scaling").max(0.01);
|
||
let repeat = pr.e("bottom/repeatMode");
|
||
let wave_h = pr.f("water/waveHeight");
|
||
let smoothing = pr.f("water/smoothing") * s;
|
||
let depth = pr.f("water/waterDepth");
|
||
let ior = pr.f("water/refractiveIndex").max(1.0);
|
||
let surf = pr.color("water/surfaceColor");
|
||
let surf_op = pr.f("water/surfaceOpacity") as f32;
|
||
let cstr = pr.f("water/causticsStrength") as f32;
|
||
let li = pr.f("lighting/lightIntensity") as f32;
|
||
let lc = pr.color("lighting/lightColor");
|
||
let lpos = pr.v2("lighting/lightPosition");
|
||
let lheight = pr.f("lighting/lightHeight").max(0.01);
|
||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||
let diffuse = pr.f("material/diffuse") as f32;
|
||
let specular = pr.f("material/specular") as f32;
|
||
let sharp = pr.f("material/highlightSharpness").max(1.0) as f32;
|
||
let (w, hh) = (b.img.width as usize, b.img.height as usize);
|
||
// Height field of the water surface (luminance of the chosen layer; none = flat).
|
||
let height = ctx.layer_param("water/waterSurface", true).map(|o| {
|
||
let img = crate::util::fit_layer(ctx, &b, &o, true);
|
||
let pl = Plane::luma(&img);
|
||
if smoothing > 0.05 { gauss_plane(&pl, smoothing * 0.5, smoothing * 0.5) } else { pl }
|
||
});
|
||
let lw = ctx.layer_size[0] * s;
|
||
let k = wave_h * depth * (1.0 - 1.0 / ior) * lw * 2.0;
|
||
let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s);
|
||
let (lx, ly) = b.to_px(lpos);
|
||
let lv = {
|
||
let v = [(lx - cx) / lw.max(1.0), (ly - cy) / lw.max(1.0), lheight];
|
||
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||
[(v[0] / n) as f32, (v[1] / n) as f32, (v[2] / n) as f32]
|
||
};
|
||
let grad = |x: usize, y: usize| -> (f64, f64) {
|
||
match &height {
|
||
Some(pl) => {
|
||
let gx = (pl.get_clamped(x as i64 + 1, y as i64) - pl.get_clamped(x as i64 - 1, y as i64)) as f64 * 0.5;
|
||
let gy = (pl.get_clamped(x as i64, y as i64 + 1) - pl.get_clamped(x as i64, y as i64 - 1)) as f64 * 0.5;
|
||
(gx, gy)
|
||
}
|
||
None => (0.0, 0.0),
|
||
}
|
||
};
|
||
// Displacement and its Jacobian (caustic focusing) per pixel.
|
||
let disp: Vec<(f64, f64)> = (0..w * hh)
|
||
.into_par_iter()
|
||
.map(|i| {
|
||
let (gx, gy) = grad(i % w, i / w);
|
||
(-gx * k, -gy * k)
|
||
})
|
||
.collect();
|
||
b.img.rows_mut().for_each(|(y, row)| {
|
||
for (x, px) in row.iter_mut().enumerate() {
|
||
let i = y * w + x;
|
||
let (dx, dy) = disp[i];
|
||
let d = |xx: usize, yy: usize| disp[yy.min(hh - 1) * w + xx.min(w - 1)];
|
||
let (dxr, _) = d(x + 1, y);
|
||
let (_, dyd) = d(x, y + 1);
|
||
let (dxl, _) = d(x.saturating_sub(1), y);
|
||
let (_, dyu) = d(x, y.saturating_sub(1));
|
||
let jac = (1.0 + (dxr - dxl) * 0.5) * (1.0 + (dyd - dyu) * 0.5);
|
||
let focus = ((1.0 / jac.abs().max(0.2)) as f32 - 1.0).clamp(-1.0, 4.0);
|
||
let sx = cx + (x as f64 + 0.5 + dx - cx) / scaling;
|
||
let sy = cy + (y as f64 + 0.5 + dy - cy) / scaling;
|
||
let p = sample_repeat(&bottom, sx, sy, repeat);
|
||
let (c, a) = unpremul(p);
|
||
let (gx, gy) = grad(x, y);
|
||
let n = {
|
||
let v = [-gx * wave_h * 20.0, -gy * wave_h * 20.0, 1.0];
|
||
let m = (v[0] * v[0] + v[1] * v[1] + 1.0).sqrt();
|
||
[(v[0] / m) as f32, (v[1] / m) as f32, (v[2] / m) as f32]
|
||
};
|
||
let nl = (n[0] * lv[0] + n[1] * lv[1] + n[2] * lv[2]).max(0.0);
|
||
let hv = {
|
||
let v = [lv[0], lv[1], lv[2] + 1.0];
|
||
let m = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||
[v[0] / m, v[1] / m, v[2] / m]
|
||
};
|
||
let spec = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(sharp) * specular * li;
|
||
let light = ambient + diffuse * li * nl + cstr * focus * li;
|
||
let cc: [f32; 3] = std::array::from_fn(|q| {
|
||
let base = c[q] * light * lc[q];
|
||
base * (1.0 - surf_op) + surf[q] * surf_op * (ambient + diffuse * li * nl) + spec * lc[q]
|
||
});
|
||
let a = (a + surf_op * (1.0 - a)).min(1.0);
|
||
*px = [cc[0] * a, cc[1] * a, cc[2] * a, a];
|
||
}
|
||
});
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ Wave World
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct Waves {
|
||
nx: usize,
|
||
ny: usize,
|
||
u: Vec<f32>,
|
||
v: Vec<f32>,
|
||
}
|
||
|
||
struct Producer {
|
||
ring: bool,
|
||
pos: [f32; 2],
|
||
len: f32,
|
||
width: f32,
|
||
angle: f32,
|
||
amp: f32,
|
||
freq: f32,
|
||
phase: f32,
|
||
}
|
||
|
||
impl Producer {
|
||
/// Weight of a grid point (layer-fraction coordinates, x in widths, y in widths).
|
||
fn weight(&self, x: f32, y: f32) -> f32 {
|
||
let (dx, dy) = (x - self.pos[0], y - self.pos[1]);
|
||
let (sa, ca) = self.angle.sin_cos();
|
||
let (u, v) = (dx * ca + dy * sa, -dx * sa + dy * ca);
|
||
let d = if self.ring {
|
||
let (a, bb) = ((self.len * 0.5).max(1e-3), (self.width * 0.5).max(1e-3));
|
||
((u / a).powi(2) + (v / bb).powi(2)).sqrt()
|
||
} else {
|
||
let l = (self.len * 0.5).max(1e-3);
|
||
let wd = (self.width * 0.5).max(1e-3);
|
||
(u.abs() / l).max(v.abs() / wd)
|
||
};
|
||
(1.0 - d).clamp(0.0, 1.0).min(1.0)
|
||
}
|
||
}
|
||
|
||
static WAVE_CACHE: SimCache<Waves> = SimCache::new(4);
|
||
|
||
fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let [lw, lh] = ctx.layer_size;
|
||
let res = pr.f("simulation/gridResolution").clamp(1.0, 400.0) as usize;
|
||
let res = if pr.b("simulation/gridResDownsamples") { ((res as f64) * b.scale).round().max(1.0) as usize } else { res };
|
||
let nx = res.max(2) + 1;
|
||
let ny = ((res as f64 * lh / lw.max(1.0)).round() as usize).max(2) + 1;
|
||
let speed = pr.f("simulation/waveSpeed").max(0.0) as f32;
|
||
let damping = pr.f("simulation/damping").max(0.0) as f32;
|
||
let reflect = pr.e("simulation/reflectEdges");
|
||
let preroll = pr.f("simulation/preRoll").max(0.0);
|
||
let prods: Vec<Producer> = [1, 2]
|
||
.iter()
|
||
.map(|k| {
|
||
let pos = pr.v2(&format!("producer{k}/producer{k}Position"));
|
||
Producer {
|
||
ring: pr.e(&format!("producer{k}/producer{k}Type")) == 0,
|
||
pos: [(pos[0] / lw.max(1.0)) as f32, (pos[1] / lw.max(1.0)) as f32],
|
||
len: pr.f(&format!("producer{k}/producer{k}Length")) as f32,
|
||
width: pr.f(&format!("producer{k}/producer{k}Width")) as f32,
|
||
angle: (pr.f(&format!("producer{k}/producer{k}Angle")) as f32).to_radians(),
|
||
amp: pr.f(&format!("producer{k}/producer{k}Amplitude")) as f32,
|
||
freq: pr.f(&format!("producer{k}/producer{k}Frequency")) as f32,
|
||
phase: (pr.f(&format!("producer{k}/producer{k}Phase")) as f32).to_radians(),
|
||
}
|
||
})
|
||
.collect();
|
||
let sps = crate::sim::SPS as f32;
|
||
// Wave speed in layer widths per second → cells per second; sub-steps keep it stable.
|
||
let cps = speed * (nx - 1) as f32;
|
||
let sub = ((cps * 2.0 / sps).ceil() as usize).clamp(1, 64);
|
||
let dt = 1.0 / (sps * sub as f32);
|
||
let cell = 1.0 / (nx - 1) as f32;
|
||
let weights: Vec<Vec<f32>> = prods.iter().map(|p| (0..nx * ny).map(|i| p.weight((i % nx) as f32 * cell, (i / nx) as f32 * cell)).collect()).collect();
|
||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: if pr.b("simulation/gridResDownsamples") { b.scale } else { 1.0 } }, 4);
|
||
let steps = crate::sim::steps_at(ctx.time + preroll);
|
||
let st = WAVE_CACHE.run(
|
||
key,
|
||
steps,
|
||
|| Waves { nx, ny, u: vec![0.0; nx * ny], v: vec![0.0; nx * ny] },
|
||
|w, step| {
|
||
for k in 0..sub {
|
||
let t = (step as f32 + k as f32 / sub as f32) / sps - preroll as f32;
|
||
let c2 = cps * cps;
|
||
let mut acc = vec![0.0f32; nx * ny];
|
||
for y in 0..ny {
|
||
for x in 0..nx {
|
||
let i = y * nx + x;
|
||
let g = |xx: usize, yy: usize| w.u[yy * nx + xx];
|
||
let l = g(x.saturating_sub(1), y) + g((x + 1).min(nx - 1), y) + g(x, y.saturating_sub(1)) + g(x, (y + 1).min(ny - 1)) - 4.0 * w.u[i];
|
||
acc[i] = c2 * l - damping * 4.0 * w.v[i];
|
||
}
|
||
}
|
||
for i in 0..nx * ny {
|
||
w.v[i] += acc[i] * dt;
|
||
w.u[i] += w.v[i] * dt;
|
||
}
|
||
// Producers drive the surface.
|
||
for (p, wt) in prods.iter().zip(&weights) {
|
||
if p.amp == 0.0 {
|
||
continue;
|
||
}
|
||
let target = p.amp * (std::f32::consts::TAU * p.freq * t + p.phase).sin();
|
||
for i in 0..nx * ny {
|
||
if wt[i] > 0.0 {
|
||
w.u[i] += (target - w.u[i]) * wt[i];
|
||
w.v[i] *= 1.0 - wt[i];
|
||
}
|
||
}
|
||
}
|
||
// Edges: reflective ones mirror; the others absorb in a thin sponge layer.
|
||
let refl = |side: u32| reflect == 5 || reflect == side;
|
||
let sponge = 4usize.min(nx / 4).max(1);
|
||
for y in 0..ny {
|
||
for x in 0..nx {
|
||
let i = y * nx + x;
|
||
let mut f = 1.0f32;
|
||
if !refl(1) && x < sponge {
|
||
f = f.min(x as f32 / sponge as f32);
|
||
}
|
||
if !refl(3) && nx - 1 - x < sponge {
|
||
f = f.min((nx - 1 - x) as f32 / sponge as f32);
|
||
}
|
||
if !refl(2) && y < sponge {
|
||
f = f.min(y as f32 / sponge as f32);
|
||
}
|
||
if !refl(4) && ny - 1 - y < sponge {
|
||
f = f.min((ny - 1 - y) as f32 / sponge as f32);
|
||
}
|
||
if f < 1.0 {
|
||
let k = 0.85 + 0.15 * f;
|
||
w.u[i] *= k;
|
||
w.v[i] *= k;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
);
|
||
let s = b.scale;
|
||
let (bw, bh) = (b.img.width, b.img.height);
|
||
let sample = |x: f64, y: f64| -> f32 {
|
||
let gx = (x / lw.max(1.0) * (st.nx - 1) as f64).clamp(0.0, (st.nx - 1) as f64);
|
||
let gy = (y / lw.max(1.0) * (st.nx - 1) as f64).clamp(0.0, (st.ny - 1) as f64);
|
||
let (x0, y0) = (gx.floor() as usize, gy.floor() as usize);
|
||
let (x1, y1) = ((x0 + 1).min(st.nx - 1), (y0 + 1).min(st.ny - 1));
|
||
let (tx, ty) = ((gx - x0 as f64) as f32, (gy - y0 as f64) as f32);
|
||
let g = |xx: usize, yy: usize| st.u[yy * st.nx + xx];
|
||
let a = g(x0, y0) + (g(x1, y0) - g(x0, y0)) * tx;
|
||
let c = g(x0, y1) + (g(x1, y1) - g(x0, y1)) * tx;
|
||
a + (c - a) * ty
|
||
};
|
||
if pr.e("view") == 1 {
|
||
let bright = pr.f("heightMapControls/brightness") as f32;
|
||
let contrast = pr.f("heightMapControls/contrast") as f32;
|
||
let gamma = pr.f("heightMapControls/gamma").max(0.01) as f32;
|
||
let alpha = 1.0 - pr.f("heightMapControls/transparency") as f32;
|
||
b.img.rows_mut().for_each(|(y, row)| {
|
||
for (x, px) in row.iter_mut().enumerate() {
|
||
let lx = (x as f64 + 0.5 - b.offset[0]) / s;
|
||
let ly = (y as f64 + 0.5 - b.offset[1]) / s;
|
||
let v = (bright + contrast * sample(lx, ly)).clamp(0.0, 1.0).powf(1.0 / gamma);
|
||
*px = [v * alpha, v * alpha, v * alpha, alpha];
|
||
}
|
||
});
|
||
} else {
|
||
// Wireframe preview: the grid seen at an angle, heights lifting the lines.
|
||
let lift = lh as f32 * 0.25;
|
||
let to = |x: usize, y: usize| -> [f32; 2] {
|
||
let lx = x as f64 / (st.nx - 1) as f64 * lw;
|
||
let ly = y as f64 / (st.nx - 1) as f64 * lw;
|
||
let (px, py) = b.to_px([lx, lh * 0.25 + ly * 0.6]);
|
||
[px as f32, py as f32 - st.u[y * st.nx + x] * lift * s as f32]
|
||
};
|
||
let mut lines = Vec::new();
|
||
for y in 0..st.ny {
|
||
for x in 0..st.nx {
|
||
let a = to(x, y);
|
||
if x + 1 < st.nx {
|
||
let c = to(x + 1, y);
|
||
lines.push(Sprite::new(a[0], a[1], 0.4, [0.4, 0.9, 0.4, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] }));
|
||
}
|
||
if y + 1 < st.ny {
|
||
let c = to(x, y + 1);
|
||
lines.push(Sprite::new(a[0], a[1], 0.4, [0.4, 0.9, 0.4, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] }));
|
||
}
|
||
}
|
||
}
|
||
let wire = splat(bw, bh, &lines, Acc::Over);
|
||
b.img = crate::sim::combine(&Image::filled(bw, bh, [0.0, 0.0, 0.0, 1.0]), &wire, 0);
|
||
}
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ Foam
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct FoamBubble {
|
||
p: [f32; 2],
|
||
v: [f32; 2],
|
||
age: f32,
|
||
size: f32,
|
||
id: u32,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default)]
|
||
struct FoamState {
|
||
b: Vec<FoamBubble>,
|
||
carry: f32,
|
||
next: u32,
|
||
}
|
||
|
||
static FOAM_CACHE: SimCache<FoamState> = SimCache::new(4);
|
||
|
||
/// Foam steps once per frame at 30 frames per second.
|
||
const FOAM_FPS: f64 = 30.0;
|
||
|
||
/// Age (in simulation steps) at which bubble `id` pops: its lifespan, shortened at random for
|
||
/// frail bubbles (`frailty` 0 = every bubble reaches `lifespan`, 1 = uniformly early).
|
||
fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 {
|
||
lifespan * (1.0 - frailty * (hs(id, 11, seed) * 0.5 + 0.5))
|
||
}
|
||
|
||
fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let unit = lw * 0.01;
|
||
let prod = pr.v2("producer/producerPoint");
|
||
let (psx, psy) = (pr.f("producer/producerXSize") as f32 * lw, pr.f("producer/producerYSize") as f32 * lh);
|
||
let porient = (pr.f("producer/producerOrientation") as f32).to_radians();
|
||
let rate = pr.f("producer/productionRate").max(0.0) as f32;
|
||
let size = pr.f("bubbles/size").max(0.0) as f32;
|
||
let size_var = pr.f("bubbles/sizeVariance") as f32;
|
||
let lifespan = pr.f("bubbles/lifespan").max(1.0) as f32;
|
||
let growth = pr.f("bubbles/growthSpeed").max(0.0001) as f32;
|
||
// Strength: weaker bubbles tend to pop before their lifespan (at 20 or more every bubble
|
||
// lives the full lifespan); each bubble gets its own pop age.
|
||
let frailty = 1.0 - (pr.f("bubbles/strength") as f32 / 20.0).clamp(0.0, 1.0);
|
||
let init_speed = pr.f("physics/initialSpeed") as f32;
|
||
let init_dir = (pr.f("physics/initialDirection") as f32).to_radians();
|
||
let wind = pr.f("physics/windSpeed") as f32;
|
||
let wind_dir = (pr.f("physics/windDirection") as f32).to_radians();
|
||
let turb = pr.f("physics/turbulence") as f32;
|
||
let repulsion = pr.f("physics/repulsion") as f32;
|
||
let viscosity = pr.f("physics/viscosity").clamp(0.0, 1.0) as f32;
|
||
let sticky = pr.f("physics/stickiness").clamp(0.0, 1.0) as f32;
|
||
let universe = pr.f("universeSize").max(0.01) as f32;
|
||
let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x68e31da4) ^ ctx.seed;
|
||
let wvec = [wind_dir.sin() * wind * unit, -wind_dir.cos() * wind * unit];
|
||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 5);
|
||
let steps = if ctx.time <= 0.0 { 0 } else { (ctx.time * FOAM_FPS + 1e-6).floor() as u64 };
|
||
let st = FOAM_CACHE.run(key, steps, FoamState::default, |st, step| {
|
||
let t = step as f32 / FOAM_FPS as f32;
|
||
// Births.
|
||
st.carry += rate;
|
||
let n = st.carry.floor() as u32;
|
||
st.carry -= n as f32;
|
||
for _ in 0..n {
|
||
if st.b.len() >= 3000 {
|
||
break;
|
||
}
|
||
let id = st.next;
|
||
st.next = st.next.wrapping_add(1);
|
||
let (ux, uy) = (hs(id, 1, seed) * 0.5 * psx, hs(id, 2, seed) * 0.5 * psy);
|
||
let (so, co) = porient.sin_cos();
|
||
let p = [prod[0] as f32 + ux * co - uy * so, prod[1] as f32 + ux * so + uy * co];
|
||
let v = [init_dir.sin() * init_speed * unit, -init_dir.cos() * init_speed * unit];
|
||
let sz = (size * (1.0 + size_var * hs(id, 3, seed))).max(0.01);
|
||
st.b.push(FoamBubble { p, v, age: 0.0, size: sz, id });
|
||
}
|
||
// Forces: drift towards the wind, turbulence, viscosity.
|
||
for q in st.b.iter_mut() {
|
||
let n1 = value_noise(q.p[0] / (lw * 0.15), q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5;
|
||
let n2 = value_noise(q.p[0] / (lw * 0.15) + 19.0, q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5;
|
||
for k in 0..2 {
|
||
q.v[k] += (wvec[k] - q.v[k]) * 0.1;
|
||
q.v[k] *= 1.0 - viscosity * 0.5;
|
||
}
|
||
q.v[0] += n1 * turb * unit;
|
||
q.v[1] += n2 * turb * unit;
|
||
}
|
||
// Repulsion between overlapping bubbles (grid hashed, deterministic order).
|
||
let rad = |q: &FoamBubble| q.size * unit * 3.0 * (q.age * growth).min(1.0);
|
||
if repulsion > 0.0 && st.b.len() > 1 {
|
||
let maxr = st.b.iter().map(rad).fold(0.0f32, f32::max).max(1.0);
|
||
let cs = maxr * 2.0;
|
||
let mut grid: std::collections::HashMap<(i32, i32), Vec<usize>> = std::collections::HashMap::new();
|
||
for (i, q) in st.b.iter().enumerate() {
|
||
grid.entry(((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32)).or_default().push(i);
|
||
}
|
||
let mut push = vec![[0.0f32; 2]; st.b.len()];
|
||
for (i, q) in st.b.iter().enumerate() {
|
||
let (gx, gy) = ((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32);
|
||
for dy in -1..=1 {
|
||
for dx in -1..=1 {
|
||
let Some(list) = grid.get(&(gx + dx, gy + dy)) else { continue };
|
||
for &j in list {
|
||
if j <= i {
|
||
continue;
|
||
}
|
||
let o = &st.b[j];
|
||
let (ddx, ddy) = (o.p[0] - q.p[0], o.p[1] - q.p[1]);
|
||
let d = (ddx * ddx + ddy * ddy).sqrt().max(1e-4);
|
||
let overlap = rad(q) + rad(o) - d;
|
||
if overlap > 0.0 {
|
||
let f = overlap * 0.5 * repulsion.min(4.0) * (1.0 - sticky * 0.5) / d;
|
||
push[i][0] -= ddx * f;
|
||
push[i][1] -= ddy * f;
|
||
push[j][0] += ddx * f;
|
||
push[j][1] += ddy * f;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (q, d) in st.b.iter_mut().zip(&push) {
|
||
q.p[0] += d[0].clamp(-maxr, maxr);
|
||
q.p[1] += d[1].clamp(-maxr, maxr);
|
||
}
|
||
}
|
||
let (ux0, uy0) = (-(universe - 1.0) * 0.5 * lw, -(universe - 1.0) * 0.5 * lh);
|
||
let (ux1, uy1) = (lw - ux0, lh - uy0);
|
||
for q in st.b.iter_mut() {
|
||
q.p[0] += q.v[0];
|
||
q.p[1] += q.v[1];
|
||
q.age += 1.0;
|
||
}
|
||
st.b.retain(|q| q.age < foam_pop_age(q.id, lifespan, frailty, seed) && q.p[0] > ux0 && q.p[0] < ux1 && q.p[1] > uy0 && q.p[1] < uy1);
|
||
});
|
||
let view = pr.e("view");
|
||
let zoom = pr.f("zoom").max(0.01) as f32;
|
||
let texture = pr.e("rendering/bubbleTexture");
|
||
let blend = pr.e("rendering/blendMode");
|
||
let s = b.scale as f32;
|
||
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
||
let mut order: Vec<&FoamBubble> = st.b.iter().collect();
|
||
if blend == 2 {
|
||
// Solid New on Top: oldest first.
|
||
order.sort_by(|a, c| c.age.total_cmp(&a.age).then(a.id.cmp(&c.id)));
|
||
} else if blend == 1 {
|
||
order.sort_by(|a, c| a.age.total_cmp(&c.age).then(a.id.cmp(&c.id)));
|
||
}
|
||
let sprites: Vec<Sprite> = order
|
||
.iter()
|
||
.map(|q| {
|
||
let r = q.size * unit * 3.0 * (q.age * growth).min(1.0) * zoom * s;
|
||
let (bx, by) = b.to_px([(cx + (q.p[0] - cx) * zoom) as f64, (cy + (q.p[1] - cy) * zoom) as f64]);
|
||
let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0);
|
||
// Draft and Draft + Flow Map show the wireframe-style bubbles.
|
||
if view != 2 {
|
||
Sprite::new(bx as f32, by as f32, r, [0.55, 0.75, 1.0, fade], Shape::Bubble)
|
||
} else {
|
||
let (c, shape) = match texture {
|
||
1 => ([0.95, 0.95, 0.92], Shape::Faded),
|
||
2 => ([1.0, 0.6, 0.8], Shape::Bubble),
|
||
3 => ([0.8, 0.85, 0.75], Shape::Bubble),
|
||
4 => ([0.97, 0.97, 0.95], Shape::Sphere),
|
||
_ => ([1.0, 1.0, 1.0], Shape::Bubble),
|
||
};
|
||
Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], fade], shape)
|
||
}
|
||
})
|
||
.collect();
|
||
b.img = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view == 2 { Acc::Add } else { Acc::Over });
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ specs
|
||
|
||
fn producer_params(k: u32, amp: f64, pos: (f64, f64)) -> Vec<crate::ParamSpec> {
|
||
let ids: [&'static str; 8] = match k {
|
||
1 => [
|
||
"producer1/producer1Type",
|
||
"producer1/producer1Position",
|
||
"producer1/producer1Length",
|
||
"producer1/producer1Width",
|
||
"producer1/producer1Angle",
|
||
"producer1/producer1Amplitude",
|
||
"producer1/producer1Frequency",
|
||
"producer1/producer1Phase",
|
||
],
|
||
_ => [
|
||
"producer2/producer2Type",
|
||
"producer2/producer2Position",
|
||
"producer2/producer2Length",
|
||
"producer2/producer2Width",
|
||
"producer2/producer2Angle",
|
||
"producer2/producer2Amplitude",
|
||
"producer2/producer2Frequency",
|
||
"producer2/producer2Phase",
|
||
],
|
||
};
|
||
vec![
|
||
p(ids[0], "Type", Value::Enum(0), popup(&["Ring", "Line"])),
|
||
p(ids[1], "Position", pt(pos.0, pos.1), ParamUi::Point),
|
||
p(ids[2], "Height/Length", num(0.1), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||
p(ids[3], "Width", num(0.1), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||
p(ids[4], "Angle", num(0.0), ParamUi::Angle),
|
||
p(ids[5], "Amplitude", num(amp), slider(-5.0, 5.0, -1.0, 1.0, 3)),
|
||
p(ids[6], "Frequency", num(1.0), slider(0.0, 30.0, 0.0, 5.0, 3)),
|
||
p(ids[7], "Phase", num(0.0), ParamUi::Angle),
|
||
]
|
||
}
|
||
|
||
fn card_dance_params() -> Vec<crate::ParamSpec> {
|
||
let mut v = vec![
|
||
p("rowsColumns", "Rows & Columns", Value::Enum(0), popup(&["Independent", "Columns Follows Rows"])),
|
||
p("rows", "Rows", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||
p("columns", "Columns", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||
p("backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer),
|
||
p("gradientLayer1", "Gradient Layer 1", Value::Layer(None), ParamUi::Layer),
|
||
p("gradientLayer2", "Gradient Layer 2", Value::Layer(None), ParamUi::Layer),
|
||
];
|
||
let names: [[&'static str; 4]; 8] = [
|
||
["xPosition/xPosSource", "xPosition/xPosMultiplier", "xPosition/xPosOffset", "X Position"],
|
||
["yPosition/yPosSource", "yPosition/yPosMultiplier", "yPosition/yPosOffset", "Y Position"],
|
||
["zPosition/zPosSource", "zPosition/zPosMultiplier", "zPosition/zPosOffset", "Z Position"],
|
||
["xRotation/xRotSource", "xRotation/xRotMultiplier", "xRotation/xRotOffset", "X Rotation"],
|
||
["yRotation/yRotSource", "yRotation/yRotMultiplier", "yRotation/yRotOffset", "Y Rotation"],
|
||
["zRotation/zRotSource", "zRotation/zRotMultiplier", "zRotation/zRotOffset", "Z Rotation"],
|
||
["xScale/xScaleSource", "xScale/xScaleMultiplier", "xScale/xScaleOffset", "X Scale"],
|
||
["yScale/yScaleSource", "yScale/yScaleMultiplier", "yScale/yScaleOffset", "Y Scale"],
|
||
];
|
||
debug_assert_eq!(names.len(), CD_PROPS.len());
|
||
for (i, n) in names.iter().enumerate() {
|
||
let rot = (3..6).contains(&i);
|
||
let scale = i >= 6;
|
||
let (lo, hi) = if rot { (-3600.0, 3600.0) } else { (-100.0, 100.0) };
|
||
v.push(p(n[0], "Source", Value::Enum(0), popup(CD_SOURCES)));
|
||
v.push(p(n[1], "Multiplier", num(0.0), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2)));
|
||
v.push(p(n[2], "Offset", num(if scale { 1.0 } else { 0.0 }), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2)));
|
||
}
|
||
v.extend([
|
||
p("cameraPosition/cameraZ", "Z Position", num(2.0), slider(0.05, 10.0, 0.5, 5.0, 2)),
|
||
p("cameraPosition/focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||
p("lighting/ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
p("material/diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
]);
|
||
v
|
||
}
|
||
|
||
pub fn specs() -> Vec<EffectSpec> {
|
||
vec![
|
||
spec(
|
||
"ec.sim.ccballaction",
|
||
"CC Ball Action",
|
||
vec![
|
||
p("scatter", "Scatter", num(0.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
|
||
p("rotationAxis", "Rotation Axis", Value::Enum(0), popup(&["X Axis", "Y Axis", "Z Axis", "XY Axis", "XZ Axis", "YZ Axis", "XYZ Axis"])),
|
||
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||
p("twistProperty", "Twist Property", Value::Enum(0), popup(&["X Axis", "Y Axis", "Radius", "Brightness"])),
|
||
p("twistAngle", "Twist Angle", num(0.0), ParamUi::Angle),
|
||
p("gridSpacing", "Grid Spacing", num(4.0), slider(1.0, 200.0, 1.0, 50.0, 0)),
|
||
p("ballSize", "Ball Size", num(40.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||
p("instabilityState", "Instability State", num(0.0), ParamUi::Angle),
|
||
],
|
||
ball_action,
|
||
),
|
||
spec(
|
||
"ec.sim.ccpixelpolly",
|
||
"CC Pixel Polly",
|
||
vec![
|
||
p("force", "Force", num(100.0), slider(-1000.0, 1000.0, -300.0, 300.0, 1)),
|
||
p("gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||
p("spinning", "Spinning", num(0.0), ParamUi::Angle),
|
||
p("forceCenter", "Force Center", pt(0.5, 0.5), ParamUi::Point),
|
||
p("directionRandomness", "Direction Randomness", num(10.0), pct()),
|
||
p("speedRandomness", "Speed Randomness", num(10.0), pct()),
|
||
p("gridSpacing", "Grid Spacing", num(10.0), slider(1.0, 500.0, 2.0, 100.0, 0)),
|
||
p("object", "Object", Value::Enum(0), popup(&["Polygon", "Textured Polygon", "Square", "Textured Square"])),
|
||
p("enableDepthSort", "Enable Depth Sort", Value::Bool(true), ParamUi::Checkbox),
|
||
p("startTime", "Start Time (sec)", num(0.0), slider(-1000.0, 1000.0, 0.0, 10.0, 2)),
|
||
],
|
||
pixel_polly,
|
||
),
|
||
spec(
|
||
"ec.sim.ccscatterize",
|
||
"CC Scatterize",
|
||
vec![
|
||
p("scatter", "Scatter", num(0.0), slider(0.0, 1000.0, 0.0, 200.0, 1)),
|
||
p("rightTwist", "Right Twist", num(0.0), ParamUi::Angle),
|
||
p("leftTwist", "Left Twist", num(0.0), ParamUi::Angle),
|
||
p("transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Add"])),
|
||
],
|
||
scatterize,
|
||
),
|
||
spec("ec.sim.carddance", "Card Dance", card_dance_params(), card_dance),
|
||
spec(
|
||
"ec.sim.caustics",
|
||
"Caustics",
|
||
vec![
|
||
p("bottom/bottom", "Bottom", Value::Layer(None), ParamUi::Layer),
|
||
p("bottom/scaling", "Scaling", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 3)),
|
||
p("bottom/repeatMode", "Repeat Mode", Value::Enum(2), popup(&["Once", "Tiled", "Reflected"])),
|
||
p("bottom/bottomSizeDiffers", "If Layer Size Differs", Value::Enum(1), popup(&["Center", "Stretch to Fit"])),
|
||
p("bottom/blur", "Blur", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||
p("water/waterSurface", "Water Surface", Value::Layer(None), ParamUi::Layer),
|
||
p("water/waveHeight", "Wave Height", num(0.2), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||
p("water/smoothing", "Smoothing", num(5.0), slider(0.0, 100.0, 0.0, 50.0, 1)),
|
||
p("water/waterDepth", "Water Depth", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("water/refractiveIndex", "Refractive Index", num(1.2), slider(1.0, 3.0, 1.0, 2.0, 3)),
|
||
p("water/surfaceColor", "Surface Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||
p("water/surfaceOpacity", "Surface Opacity", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("water/causticsStrength", "Caustics Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||
p("lighting/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||
p("lighting/lightPosition", "Light Position", pt(0.0, 0.0), ParamUi::Point),
|
||
p("lighting/lightHeight", "Light Height", num(1.0), slider(0.0, 10.0, 0.0, 4.0, 2)),
|
||
p("lighting/ambientLight", "Ambient Light", num(0.35), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
p("material/diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
p("material/specular", "Specular Reflection", num(0.2), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
p("material/highlightSharpness", "Highlight Sharpness", num(15.0), slider(1.0, 100.0, 1.0, 100.0, 1)),
|
||
],
|
||
caustics,
|
||
),
|
||
spec(
|
||
"ec.sim.waveworld",
|
||
"Wave World",
|
||
{
|
||
let mut v = vec![
|
||
p("view", "View", Value::Enum(0), popup(&["Wireframe Preview", "Height Map"])),
|
||
p("heightMapControls/brightness", "Brightness", num(0.5), slider(-1.0, 2.0, 0.0, 1.0, 3)),
|
||
p("heightMapControls/contrast", "Contrast", num(0.75), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||
p("heightMapControls/gamma", "Gamma Adjustment", num(1.0), slider(0.01, 10.0, 0.2, 5.0, 3)),
|
||
p("heightMapControls/transparency", "Transparency", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("simulation/gridResolution", "Grid Resolution", num(60.0), slider(1.0, 400.0, 1.0, 200.0, 0)),
|
||
p("simulation/gridResDownsamples", "Grid Res Downsamples", Value::Bool(false), ParamUi::Checkbox),
|
||
p("simulation/waveSpeed", "Wave Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||
p("simulation/damping", "Damping", num(0.05), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||
p("simulation/reflectEdges", "Reflect Edges", Value::Enum(0), popup(&["None", "Left", "Top", "Right", "Bottom", "All"])),
|
||
p("simulation/preRoll", "Pre-roll (seconds)", num(0.0), slider(0.0, 30.0, 0.0, 10.0, 2)),
|
||
];
|
||
v.extend(producer_params(1, 0.5, (0.5, 0.5)));
|
||
v.extend(producer_params(2, 0.0, (0.25, 0.25)));
|
||
v
|
||
},
|
||
wave_world,
|
||
),
|
||
spec(
|
||
"ec.sim.shatter",
|
||
"Shatter",
|
||
vec![
|
||
p(
|
||
"view",
|
||
"View",
|
||
Value::Enum(1),
|
||
popup(&["Rendered", "Wireframe Front View", "Wireframe", "Wireframe Front View + Forces", "Wireframe + Forces"]),
|
||
),
|
||
p("render", "Render", Value::Enum(0), popup(&["All", "Layer", "Pieces"])),
|
||
p("shape/pattern", "Pattern", Value::Enum(0), popup(SHATTER_PATTERNS)),
|
||
p("shape/repetitions", "Repetitions", num(10.0), slider(1.0, 500.0, 1.0, 100.0, 2)),
|
||
p("shape/direction", "Direction", num(0.0), ParamUi::Angle),
|
||
p("shape/origin", "Origin", pt(0.5, 0.5), ParamUi::Point),
|
||
p("shape/extrusionDepth", "Extrusion Depth", num(0.05), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("force1/force1Position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||
p("force1/force1Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||
p("force1/force1Radius", "Radius", num(0.4), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||
p("force1/force1Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)),
|
||
p("force2/force2Position", "Position", pt(0.25, 0.25), ParamUi::Point),
|
||
p("force2/force2Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||
p("force2/force2Radius", "Radius", num(0.0), slider(0.0, 4.0, 0.0, 2.0, 3)),
|
||
p("force2/force2Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)),
|
||
p("physics/rotationSpeed", "Rotation Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)),
|
||
p("physics/tumbleAxis", "Tumble Axis", Value::Enum(0), popup(&["Free", "None", "X", "Y", "Z", "XY", "XZ", "YZ"])),
|
||
p("physics/randomness", "Randomness", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("physics/massVariance", "Mass Variance", num(30.0), pct()),
|
||
p("physics/gravity", "Gravity", num(3.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||
p("physics/gravityDirection", "Gravity Direction", num(180.0), ParamUi::Angle),
|
||
p("physics/gravityInclination", "Gravity Inclination", num(0.0), slider(-90.0, 90.0, -90.0, 90.0, 1)),
|
||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||
p("lighting/ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||
],
|
||
shatter,
|
||
),
|
||
spec(
|
||
"ec.sim.foam",
|
||
"Foam",
|
||
vec![
|
||
p("view", "View", Value::Enum(0), popup(&["Draft", "Draft + Flow Map", "Rendered"])),
|
||
p("producer/producerPoint", "Producer Point", pt(0.5, 0.5), ParamUi::Point),
|
||
p("producer/producerXSize", "Producer X Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||
p("producer/producerYSize", "Producer Y Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||
p("producer/producerOrientation", "Producer Orientation", num(0.0), ParamUi::Angle),
|
||
p("producer/productionRate", "Production Rate", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 3)),
|
||
p("bubbles/size", "Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||
p("bubbles/sizeVariance", "Size Variance", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("bubbles/lifespan", "Lifespan", num(300.0), slider(1.0, 30_000.0, 1.0, 1000.0, 1)),
|
||
p("bubbles/growthSpeed", "Bubble Growth Speed", num(0.1), slider(0.0001, 10.0, 0.0001, 1.0, 3)),
|
||
p("bubbles/strength", "Strength", num(10.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||
p("physics/initialSpeed", "Initial Speed", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||
p("physics/initialDirection", "Initial Direction", num(0.0), ParamUi::Angle),
|
||
p("physics/windSpeed", "Wind Speed", num(0.5), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||
p("physics/windDirection", "Wind Direction", num(90.0), ParamUi::Angle),
|
||
p("physics/turbulence", "Turbulence", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||
p("physics/repulsion", "Repulsion", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||
p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("physics/stickiness", "Stickiness", num(0.75), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||
p("zoom", "Zoom", num(1.0), slider(0.01, 50.0, 0.1, 5.0, 3)),
|
||
p("universeSize", "Universe Size", num(1.0), slider(0.01, 10.0, 0.5, 3.0, 3)),
|
||
p("rendering/blendMode", "Blend Mode", Value::Enum(0), popup(&["Transparent", "Solid Old on Top", "Solid New on Top"])),
|
||
p("rendering/bubbleTexture", "Bubble Texture", Value::Enum(0), popup(&["Default Bubble", "Spit", "Bubblegum", "Dishwater", "Milky"])),
|
||
p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||
],
|
||
foam,
|
||
),
|
||
]
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::{EffectEnv, run_fx};
|
||
|
||
fn ramp(w: u32, hh: u32) -> Image {
|
||
let mut img = Image::new(w, hh);
|
||
for y in 0..hh {
|
||
for x in 0..w {
|
||
img.set(x, y, [x as f32 / w as f32, y as f32 / hh as f32, 0.4, 1.0]);
|
||
}
|
||
}
|
||
img
|
||
}
|
||
|
||
fn run(id: &str, vals: &[(&str, Value)], img: Image, t: f64) -> Image {
|
||
run_fx(id, vals, img, t, EffectEnv::default()).img
|
||
}
|
||
|
||
fn max_diff(a: &Image, b: &Image) -> f32 {
|
||
a.data.iter().zip(&b.data).map(|(p, q)| (0..4).map(|k| (p[k] - q[k]).abs()).fold(0.0, f32::max)).fold(0.0, f32::max)
|
||
}
|
||
|
||
fn sum_diff(a: &Image, b: &Image) -> f32 {
|
||
a.data.iter().zip(&b.data).map(|(p, q)| (0..4).map(|k| (p[k] - q[k]).abs()).sum::<f32>()).sum()
|
||
}
|
||
|
||
#[test]
|
||
fn piece_at_rest_is_identity() {
|
||
let src = ramp(16, 16);
|
||
let cam = Cam { c: [8.0, 8.0], d: 64.0 };
|
||
let id = rot_axis([1.0, 0.0, 0.0], 0.0);
|
||
let pc = Piece::new(&[[0.0, 0.0], [16.0, 0.0], [16.0, 16.0], [0.0, 16.0]], [8.0, 8.0], &id, [8.0, 8.0, 0.0], cam).unwrap();
|
||
let mut out = Image::new(16, 16);
|
||
draw_pieces(&mut out, &[pc], &src, None);
|
||
assert!(max_diff(&out, &src) < 1e-4);
|
||
}
|
||
|
||
#[test]
|
||
fn ball_action_draws_balls() {
|
||
let v = [("ballSize", num(80.0))];
|
||
let a = run("ec.sim.ccballaction", &v, ramp(32, 32), 0.0);
|
||
assert_eq!(a, run("ec.sim.ccballaction", &v, ramp(32, 32), 0.0));
|
||
assert!(a.data.iter().any(|p| p[3] > 0.9) && a.data.iter().any(|p| p[3] == 0.0), "balls with gaps");
|
||
let none = run("ec.sim.ccballaction", &[("ballSize", num(0.0))], ramp(32, 32), 0.0);
|
||
assert!(none.data.iter().all(|p| p[3] == 0.0));
|
||
let sc = run("ec.sim.ccballaction", &[("ballSize", num(80.0)), ("scatter", num(20.0))], ramp(32, 32), 0.0);
|
||
assert!(sum_diff(&a, &sc) > 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn pixel_polly_breaks_after_start() {
|
||
let v = [("forceCenter", pt(16.0, 16.0)), ("gridSpacing", num(4.0))];
|
||
let before = run("ec.sim.ccpixelpolly", &v, ramp(32, 32), 0.0);
|
||
assert_eq!(before, ramp(32, 32), "intact before the start time");
|
||
let a = run("ec.sim.ccpixelpolly", &v, ramp(32, 32), 0.5);
|
||
assert_eq!(a, run("ec.sim.ccpixelpolly", &v, ramp(32, 32), 0.5));
|
||
assert!(sum_diff(&a, &ramp(32, 32)) > 10.0);
|
||
let tex = run("ec.sim.ccpixelpolly", &[("forceCenter", pt(16.0, 16.0)), ("gridSpacing", num(4.0)), ("object", Value::Enum(3))], ramp(32, 32), 0.5);
|
||
assert!(sum_diff(&a, &tex) > 0.1);
|
||
}
|
||
|
||
#[test]
|
||
fn scatterize_identity_and_scatter() {
|
||
assert_eq!(run("ec.sim.ccscatterize", &[], ramp(24, 24), 0.0), ramp(24, 24));
|
||
let a = run("ec.sim.ccscatterize", &[("scatter", num(5.0))], ramp(24, 24), 0.0);
|
||
assert_eq!(a, run("ec.sim.ccscatterize", &[("scatter", num(5.0))], ramp(24, 24), 0.0));
|
||
assert!(sum_diff(&a, &ramp(24, 24)) > 1.0);
|
||
let tw = run("ec.sim.ccscatterize", &[("leftTwist", num(80.0)), ("rightTwist", num(80.0))], ramp(24, 24), 0.0);
|
||
assert!(sum_diff(&tw, &ramp(24, 24)) > 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn card_dance_identity_and_rotation() {
|
||
let src = ramp(30, 20);
|
||
let a = run("ec.sim.carddance", &[], src.clone(), 0.0);
|
||
assert!(max_diff(&a, &src) < 0.02, "flat cards reproduce the layer: {}", max_diff(&a, &src));
|
||
let r = run("ec.sim.carddance", &[("yRotation/yRotSource", Value::Enum(1)), ("yRotation/yRotMultiplier", num(90.0))], src.clone(), 0.0);
|
||
assert_eq!(r, run("ec.sim.carddance", &[("yRotation/yRotSource", Value::Enum(1)), ("yRotation/yRotMultiplier", num(90.0))], src.clone(), 0.0));
|
||
assert!(sum_diff(&r, &src) > 5.0);
|
||
let z = run("ec.sim.carddance", &[("zPosition/zPosOffset", num(-1.0))], src.clone(), 0.0);
|
||
assert!(sum_diff(&z, &src) > 5.0, "cards pulled towards the camera grow");
|
||
}
|
||
|
||
#[test]
|
||
fn shatter_at_rest_then_breaks() {
|
||
let v = [("view", Value::Enum(0)), ("force1/force1Position", pt(16.0, 16.0)), ("shape/origin", pt(16.0, 16.0))];
|
||
let src = ramp(32, 32);
|
||
let t0 = run("ec.sim.shatter", &v, src.clone(), 0.0);
|
||
assert!(max_diff(&t0, &src) < 0.03, "{}", max_diff(&t0, &src));
|
||
let t1 = run("ec.sim.shatter", &v, src.clone(), 0.5);
|
||
assert_eq!(t1, run("ec.sim.shatter", &v, src.clone(), 0.5));
|
||
assert!(sum_diff(&t1, &src) > 10.0);
|
||
for pat in 0..SHATTER_PATTERNS.len() as u32 {
|
||
let vv =
|
||
[("view", Value::Enum(0)), ("shape/pattern", Value::Enum(pat)), ("force1/force1Position", pt(16.0, 16.0)), ("shape/origin", pt(16.0, 16.0))];
|
||
let r = run("ec.sim.shatter", &vv, src.clone(), 0.0);
|
||
assert!(max_diff(&r, &src) < 0.05, "pattern {pat} tiles the layer: {}", max_diff(&r, &src));
|
||
}
|
||
let wire = run("ec.sim.shatter", &[("shape/repetitions", num(3.0))], src.clone(), 0.0);
|
||
assert!(wire.data.iter().any(|p| p[3] > 0.5) && wire.data.iter().any(|p| p[3] == 0.0));
|
||
let layer_only =
|
||
run("ec.sim.shatter", &[("view", Value::Enum(0)), ("render", Value::Enum(1)), ("force1/force1Position", pt(16.0, 16.0))], src.clone(), 0.5);
|
||
assert!(layer_only.get(16, 16)[3] == 0.0, "broken centre removed");
|
||
}
|
||
|
||
#[test]
|
||
fn caustics_flat_water() {
|
||
let src = ramp(24, 24);
|
||
let a = run(
|
||
"ec.sim.caustics",
|
||
&[
|
||
("water/surfaceOpacity", num(0.0)),
|
||
("lighting/ambientLight", num(0.0)),
|
||
("material/diffuse", num(1.0)),
|
||
("lighting/lightHeight", num(1000.0)),
|
||
("material/specular", num(0.0)),
|
||
],
|
||
src.clone(),
|
||
0.0,
|
||
);
|
||
assert!(max_diff(&a, &src) < 0.01, "flat water, light overhead = bottom: {}", max_diff(&a, &src));
|
||
let b = run("ec.sim.caustics", &[], src.clone(), 0.0);
|
||
assert_eq!(b, run("ec.sim.caustics", &[], src.clone(), 0.0));
|
||
assert!(sum_diff(&b, &src) > 0.1);
|
||
}
|
||
|
||
#[test]
|
||
fn wave_world_seek_and_waves() {
|
||
let v = vec![("view", Value::Enum(1)), ("producer1/producer1Position", pt(16.0, 16.0)), ("simulation/gridResolution", num(24.0))];
|
||
let _ = run("ec.sim.waveworld", &v, Image::new(32, 32), 0.5);
|
||
let a = run("ec.sim.waveworld", &v, Image::new(32, 32), 1.0);
|
||
let mut v2 = v.clone();
|
||
v2.push(("heightMapControls/transparency", num(0.0)));
|
||
assert_eq!(a, run("ec.sim.waveworld", &v2, Image::new(32, 32), 1.0));
|
||
// Waves spread: some pixels differ from the flat-water grey.
|
||
assert!(a.data.iter().any(|p| (p[0] - 0.5).abs() > 0.05), "waves");
|
||
let flat = run("ec.sim.waveworld", &[("view", Value::Enum(1)), ("producer1/producer1Amplitude", num(0.0))], Image::new(32, 32), 1.0);
|
||
assert!(flat.data.iter().all(|p| (p[0] - 0.5).abs() < 1e-5));
|
||
let wire =
|
||
run("ec.sim.waveworld", &[("producer1/producer1Position", pt(16.0, 16.0)), ("simulation/gridResolution", num(10.0))], Image::new(32, 32), 0.3);
|
||
assert!(wire.data.iter().any(|p| p[1] > 0.3));
|
||
}
|
||
|
||
#[test]
|
||
fn wave_world_steps_match_direct() {
|
||
// Fresh caches: resumed simulation equals simulation from scratch.
|
||
let v = vec![
|
||
("view", Value::Enum(1)),
|
||
("producer1/producer1Position", pt(16.0, 16.0)),
|
||
("simulation/gridResolution", num(20.0)),
|
||
("simulation/damping", num(0.07)),
|
||
];
|
||
let a = run("ec.sim.waveworld", &v, Image::new(32, 32), 1.3);
|
||
let mut v2 = v.clone();
|
||
v2.push(("simulation/damping", num(0.07000001)));
|
||
// Different key: computed from scratch at 1.3 directly vs via 0.4 (both fresh keys).
|
||
let _ = run("ec.sim.waveworld", &v2, Image::new(32, 32), 0.4);
|
||
let b = run("ec.sim.waveworld", &v2, Image::new(32, 32), 1.3);
|
||
assert!(max_diff(&a, &b) < 1e-3, "{}", max_diff(&a, &b));
|
||
}
|
||
|
||
#[test]
|
||
fn foam_bubbles_drift_and_seek() {
|
||
let v = vec![("producer/producerPoint", pt(16.0, 16.0)), ("view", Value::Enum(2)), ("producer/productionRate", num(2.0))];
|
||
let none = run("ec.sim.foam", &v, Image::new(32, 32), 0.0);
|
||
assert!(none.data.iter().all(|p| p[3] == 0.0));
|
||
let _ = run("ec.sim.foam", &v, Image::new(32, 32), 0.6);
|
||
let a = run("ec.sim.foam", &v, Image::new(32, 32), 1.2);
|
||
let mut v2 = v.clone();
|
||
v2.push(("randomSeed", num(1.0)));
|
||
assert_eq!(a, run("ec.sim.foam", &v2, Image::new(32, 32), 1.2));
|
||
assert!(a.data.iter().any(|p| p[3] > 0.05), "bubbles");
|
||
let draft = run("ec.sim.foam", &[("producer/producerPoint", pt(16.0, 16.0)), ("producer/productionRate", num(2.0))], Image::new(32, 32), 1.0);
|
||
assert!(draft.data.iter().any(|p| p[3] > 0.05));
|
||
}
|
||
|
||
#[test]
|
||
fn foam_strength_controls_early_popping() {
|
||
// Strong bubbles all live their full lifespan; weak ones pop at random earlier.
|
||
assert_eq!(foam_pop_age(7, 300.0, 0.0, 1), 300.0);
|
||
let early = (0..200).filter(|&id| foam_pop_age(id, 300.0, 1.0, 1) < 150.0).count();
|
||
assert!(early > 60 && early < 140, "{early}");
|
||
let count = |strength: f64| {
|
||
let v = vec![
|
||
("producer/producerPoint", pt(16.0, 16.0)),
|
||
("producer/productionRate", num(3.0)),
|
||
("bubbles/lifespan", num(40.0)),
|
||
("bubbles/strength", num(strength)),
|
||
("physics/windSpeed", num(0.0)),
|
||
];
|
||
run("ec.sim.foam", &v, Image::new(32, 32), 1.5).data.iter().map(|p| p[3]).sum::<f32>()
|
||
};
|
||
assert!(count(0.0) < count(20.0) * 0.9, "{} vs {}", count(0.0), count(20.0));
|
||
}
|
||
}
|