mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Shapes: Wiggle Paths has Spatial Phase, which slides its wiggle along the path
Wiggle Paths had Size, Detail, Points, Wiggles/Second, Correlation, Temporal Phase and Random Seed, but no Spatial Phase, which After Effects lists among its properties (ADBE Vector Spatial Phase). Spatial Phase now moves each point's own displacement along its subpath: 360 degrees is once around, and a fraction of a point is interpolated between neighbors. At 0 degrees a path draws as before. A project saved without the property gets it at 0 degrees, after Temporal Phase, when it opens. Scripts can read and set every Wiggle Paths property by its match name.
This commit is contained in:
+35
-10
@@ -533,12 +533,15 @@ pub struct WiggleParams {
|
||||
pub correlation: f64,
|
||||
/// Temporal phase in degrees (360° = one wiggle period).
|
||||
pub phase_deg: f64,
|
||||
/// Spatial phase in degrees: slides each subpath's pattern of displacements along its points
|
||||
/// (360° = once around the subpath).
|
||||
pub spatial_phase_deg: f64,
|
||||
pub seed: f64,
|
||||
}
|
||||
|
||||
impl Default for WiggleParams {
|
||||
fn default() -> Self {
|
||||
WiggleParams { size: 10.0, detail: 10.0, smooth: true, speed: 2.0, correlation: 50.0, phase_deg: 0.0, seed: 0.0 }
|
||||
WiggleParams { size: 10.0, detail: 10.0, smooth: true, speed: 2.0, correlation: 50.0, phase_deg: 0.0, spatial_phase_deg: 0.0, seed: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,9 +571,16 @@ fn noise(seed: u64, idx: u64, t: f64) -> f64 {
|
||||
v.clamp(-1.0, 1.0)
|
||||
}
|
||||
|
||||
/// Catmull-Rom interpolation between `p1` (f = 0) and `p2` (f = 1).
|
||||
fn catmull_rom(p0: f64, p1: f64, p2: f64, p3: f64, f: f64) -> f64 {
|
||||
0.5 * (2.0 * p1 + (-p0 + p2) * f + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * f * f + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * f * f * f)
|
||||
}
|
||||
|
||||
/// Wiggle Paths: subdivides the paths (`detail` points per segment) and displaces every point by
|
||||
/// smooth random noise of up to `size` pixels that evolves over `time_s` seconds at `speed`
|
||||
/// wiggles per second. Deterministic for a given seed and time.
|
||||
/// wiggles per second. The spatial phase moves each point's own share of the noise along the
|
||||
/// subpath, and a fraction of a point is interpolated between its neighbors. Deterministic for a
|
||||
/// given seed and time.
|
||||
pub fn wiggle(paths: &[BezPath], w: &WiggleParams, time_s: f64) -> Vec<BezPath> {
|
||||
if w.size == 0.0 {
|
||||
return paths.to_vec();
|
||||
@@ -580,6 +590,7 @@ pub fn wiggle(paths: &[BezPath], w: &WiggleParams, time_s: f64) -> Vec<BezPath>
|
||||
let t = time_s * w.speed + w.phase_deg / 360.0;
|
||||
let corr = (w.correlation / 100.0).clamp(0.0, 1.0);
|
||||
let shared = Vec2::new(noise(seed, u64::MAX, t), noise(seed, u64::MAX - 1, t));
|
||||
let turns = if w.spatial_phase_deg.is_finite() { (w.spatial_phase_deg / 360.0).rem_euclid(1.0) } else { 0.0 };
|
||||
let mut idx: u64 = 0;
|
||||
paths
|
||||
.iter()
|
||||
@@ -587,14 +598,28 @@ pub fn wiggle(paths: &[BezPath], w: &WiggleParams, time_s: f64) -> Vec<BezPath>
|
||||
let mut out = BezPath::new();
|
||||
for sub in subs(p) {
|
||||
let s = resample(&sub, m);
|
||||
let disp: Vec<Vec2> = s
|
||||
.iter()
|
||||
.map(|_| {
|
||||
let own = Vec2::new(noise(seed, idx * 2, t + idx as f64 * 0.37), noise(seed, idx * 2 + 1, t + idx as f64 * 0.61));
|
||||
idx += 1;
|
||||
(shared * corr + own * (1.0 - corr)) * w.size
|
||||
})
|
||||
.collect();
|
||||
let n = s.len();
|
||||
let first = idx;
|
||||
let own = |j: usize| {
|
||||
let i = first + j as u64;
|
||||
Vec2::new(noise(seed, i * 2, t + i as f64 * 0.37), noise(seed, i * 2 + 1, t + i as f64 * 0.61))
|
||||
};
|
||||
let along = |j: usize| {
|
||||
if turns == 0.0 {
|
||||
return own(j);
|
||||
}
|
||||
let u = (j as f64 + turns * n as f64).rem_euclid(n as f64);
|
||||
let k = u.floor();
|
||||
let f = u - k;
|
||||
let k = k as usize % n;
|
||||
if f == 0.0 {
|
||||
return own(k);
|
||||
}
|
||||
let (p0, p1, p2, p3) = (own((k + n - 1) % n), own(k), own((k + 1) % n), own((k + 2) % n));
|
||||
Vec2::new(catmull_rom(p0.x, p1.x, p2.x, p3.x, f).clamp(-1.0, 1.0), catmull_rom(p0.y, p1.y, p2.y, p3.y, f).clamp(-1.0, 1.0))
|
||||
};
|
||||
let disp: Vec<Vec2> = (0..n).map(|j| (shared * corr + along(j) * (1.0 - corr)) * w.size).collect();
|
||||
idx += n as u64;
|
||||
emit(&s, &disp, sub.closed, w.smooth, 1.0 / 3.0, &mut out);
|
||||
}
|
||||
out
|
||||
|
||||
@@ -317,6 +317,38 @@ fn wiggle_full_correlation_translates() {
|
||||
assert_eq!(zero[0], s[0]);
|
||||
}
|
||||
|
||||
/// Spatial Phase moves the pattern of displacements along the path: 360° is once around, and a
|
||||
/// whole number of points' worth gives each point the displacement of the point that far ahead.
|
||||
#[test]
|
||||
fn wiggle_spatial_phase_moves_the_pattern_along_the_path() {
|
||||
let s = [sq(0.0, 0.0, 100.0)];
|
||||
let w = WiggleParams { detail: 3.0, correlation: 0.0, smooth: false, ..Default::default() };
|
||||
// Displacements are proportional to Size: twice the size less once the size.
|
||||
let disp = |w: WiggleParams| -> Vec<Vec2> {
|
||||
let a = on_curve(&wiggle(&s, &w, 0.4)[0]);
|
||||
let b = on_curve(&wiggle(&s, &WiggleParams { size: w.size * 2.0, ..w }, 0.4)[0]);
|
||||
a.iter().zip(&b).map(|(a, b)| *b - *a).take(16).collect()
|
||||
};
|
||||
let d0 = disp(w);
|
||||
let n = d0.len();
|
||||
assert_eq!(n, 16);
|
||||
for k in [1, 5] {
|
||||
let dk = disp(WiggleParams { spatial_phase_deg: 360.0 * k as f64 / n as f64, ..w });
|
||||
for j in 0..n {
|
||||
let e = d0[(j + k) % n];
|
||||
assert!(close(dk[j].x, e.x, 1e-9) && close(dk[j].y, e.y, 1e-9), "{k} ahead, point {j}: {:?} vs {e:?}", dk[j]);
|
||||
}
|
||||
}
|
||||
// Whole turns either way leave the path as it is.
|
||||
let base = wiggle(&s, &w, 0.4);
|
||||
assert_eq!(wiggle(&s, &WiggleParams { spatial_phase_deg: 360.0, ..w }, 0.4), base);
|
||||
assert_eq!(wiggle(&s, &WiggleParams { spatial_phase_deg: -720.0, ..w }, 0.4), base);
|
||||
// Half a point ahead, the displacements change and stay within Size.
|
||||
let half = disp(WiggleParams { spatial_phase_deg: 180.0 / n as f64, ..w });
|
||||
assert!(half.iter().zip(&d0).any(|(h, d)| (*h - *d).hypot() > 1e-3));
|
||||
assert!(half.iter().all(|h| h.x.abs() <= 10.0 + 1e-9 && h.y.abs() <= 10.0 + 1e-9));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- merge / booleans
|
||||
|
||||
fn inclusion_exclusion(a: BezPath, b: BezPath) {
|
||||
|
||||
Reference in New Issue
Block a user