M9.3: Transition effects (CC Glass Wipe, CC Image Wipe, CC Jaws, CC Line Sweep, CC Twister, CC WarpoMatic)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8503f816ae
commit
0afa24b88f
|
|
@ -1,7 +1,544 @@
|
|||
//! Transition effects, batch 2.
|
||||
//! Transition effects, batch 2: CC Glass Wipe, CC Image Wipe, CC Jaws, CC Line Sweep,
|
||||
//! CC Twister and CC WarpoMatic.
|
||||
//!
|
||||
//! Written from the public descriptions of these transitions (gradient-driven wipes with glass
|
||||
//! refraction or warping, jagged "jaws" opening, staggered line sweeps, a ribbon twist that
|
||||
//! reveals a back side); the maths is our own. Completion 0 leaves the layer untouched;
|
||||
//! completion 100 leaves it fully transparent (or showing the revealed / back-side layer).
|
||||
|
||||
use crate::EffectSpec;
|
||||
use effectcraft_color::rgb_to_hsl;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::util::{Plane, fit_layer, gauss_plane, layer_or_self, lerp4, smoothstep, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Transition", params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
fn pct() -> ParamUi {
|
||||
slider(0.0, 100.0, 0.0, 100.0, 1)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
fn completion(ctx: &EffectCtx) -> f32 {
|
||||
(ctx.params.f("completion") / 100.0).clamp(0.0, 1.0) as f32
|
||||
}
|
||||
|
||||
/// The layer in layer parameter `id` fitted to `b`, if one is chosen.
|
||||
fn reveal(ctx: &EffectCtx, b: &Buf, id: &str) -> Option<Image> {
|
||||
ctx.layer_param(id, true).map(|o| fit_layer(ctx, b, &o, false))
|
||||
}
|
||||
|
||||
const GRAD_PROPS: [&str; 7] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness", "Saturation"];
|
||||
|
||||
fn prop_plane(img: &Image, prop: u32) -> Plane {
|
||||
Plane::from_image(img, |p| {
|
||||
let (c, a) = unpremul(p);
|
||||
match prop {
|
||||
0 => c[0] * a,
|
||||
1 => c[1] * a,
|
||||
2 => c[2] * a,
|
||||
3 => a,
|
||||
4 => effectcraft_color::luminance(c[0], c[1], c[2]) * a,
|
||||
5 => rgb_to_hsl(c[0], c[1], c[2]).2 * a,
|
||||
_ => rgb_to_hsl(c[0], c[1], c[2]).1 * a,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Fraction of a pixel with gradient value `g` that has transitioned, for a soft band of
|
||||
/// half-width `w`: none at completion 0, all at completion 1.
|
||||
#[inline]
|
||||
fn wipe_t(g: f32, c: f32, w: f32) -> f32 {
|
||||
let w = w.max(1e-3);
|
||||
let edge = c * (1.0 + 2.0 * w) - w;
|
||||
1.0 - smoothstep(edge - w, edge + w, g)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn grad(pl: &Plane, x: usize, y: usize) -> (f32, f32) {
|
||||
let (xi, yi) = (x as i64, y as i64);
|
||||
((pl.get_clamped(xi + 1, yi) - pl.get_clamped(xi - 1, yi)) * 0.5, (pl.get_clamped(xi, yi + 1) - pl.get_clamped(xi, yi - 1)) * 0.5)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Glass Wipe
|
||||
|
||||
fn glass_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = completion(ctx);
|
||||
if c <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let gimg = layer_or_self(ctx, &b, "gradientLayer", true, false);
|
||||
let mut g = prop_plane(&gimg, 4);
|
||||
let soft = (ctx.params.f("softness") / 100.0) as f32;
|
||||
let sm = ctx.params.f("softness") * b.scale * 0.2;
|
||||
if sm > 0.05 {
|
||||
g = gauss_plane(&g, sm, sm);
|
||||
}
|
||||
let disp = (ctx.params.f("displacementAmount") * b.scale * 4.0) as f32;
|
||||
let rev = reveal(ctx, &b, "layerToReveal");
|
||||
let src = b.img.clone();
|
||||
let w = soft * 0.5 + 0.01;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let t = wipe_t(g.get(x, y), c, w);
|
||||
let band = 4.0 * t * (1.0 - t);
|
||||
let (gx, gy) = grad(&g, x, y);
|
||||
let (fx, fy) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
let (ox, oy) = ((gx * disp * band) as f64 * 10.0, (gy * disp * band) as f64 * 10.0);
|
||||
let a = if band > 1e-4 { src.sample_bilinear(fx + ox, fy + oy) } else { src.data[y * src.width as usize + x] };
|
||||
let r = match &rev {
|
||||
Some(r) if band > 1e-4 => r.sample_bilinear(fx - ox, fy - oy),
|
||||
Some(r) => r.data[y * r.width as usize + x],
|
||||
None => [0.0; 4],
|
||||
};
|
||||
*px = lerp4(a, r, t);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Image Wipe
|
||||
|
||||
fn image_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = completion(ctx);
|
||||
if c <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let gimg = layer_or_self(ctx, &b, "layer", true, false);
|
||||
let mut g = prop_plane(&gimg, ctx.params.e("property"));
|
||||
let bl = ctx.params.f("blur") * b.scale * 0.5;
|
||||
if bl > 0.05 {
|
||||
g = gauss_plane(&g, bl, bl);
|
||||
}
|
||||
if ctx.params.b("inverseGradient") {
|
||||
g = g.map(|v| 1.0 - v);
|
||||
}
|
||||
let mut w = (ctx.params.f("borderSoftness") / 100.0) as f32 * 0.5;
|
||||
if ctx.params.b("autoSoftness") {
|
||||
w = w.max(0.02);
|
||||
}
|
||||
b.img.data.par_iter_mut().zip(g.data.par_iter()).for_each(|(px, &gv)| {
|
||||
let k = 1.0 - wipe_t(gv, c, w);
|
||||
*px = px.map(|v| v * k);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Jaws
|
||||
|
||||
fn jaws(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = completion(ctx) as f64;
|
||||
if c <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let s = b.scale;
|
||||
let (cx, cy) = b.to_px(ctx.params.v2("center"));
|
||||
let ang = ctx.params.f("direction").to_radians();
|
||||
let (sn, cs) = ang.sin_cos();
|
||||
let width = (ctx.params.f("width") * s).max(1.0);
|
||||
let amp = ctx.params.f("height") / 100.0 * width * 0.5;
|
||||
let shape = ctx.params.e("shape");
|
||||
// Far enough at completion 1 that both halves (teeth included) have left the buffer.
|
||||
let (bw, bh) = (b.img.width as f64, b.img.height as f64);
|
||||
let vmax = [(0.0, 0.0), (bw, 0.0), (0.0, bh), (bw, bh)].iter().map(|&(x, y)| (-(x - cx) * sn + (y - cy) * cs).abs()).fold(0.0, f64::max);
|
||||
let d = c * (vmax + 2.0 * amp + 2.0);
|
||||
// Tooth profile in [-amp, amp] along the cut.
|
||||
let profile = move |u: f64| -> f64 {
|
||||
let ph = (u / width).rem_euclid(1.0);
|
||||
match shape {
|
||||
0 => amp * (1.0 - 4.0 * (ph - 0.5).abs()),
|
||||
1 => {
|
||||
// RoboJaw: square teeth with sloped flanks.
|
||||
let t = (1.0 - 4.0 * (ph - 0.5).abs()) * 2.0;
|
||||
amp * t.clamp(-1.0, 1.0)
|
||||
}
|
||||
2 => {
|
||||
if ph < 0.5 {
|
||||
amp
|
||||
} else {
|
||||
-amp
|
||||
}
|
||||
}
|
||||
_ => amp * (ph * std::f64::consts::TAU).sin(),
|
||||
}
|
||||
};
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (dx, dy) = (x as f64 + 0.5 - cx, y as f64 + 0.5 - cy);
|
||||
// u along the cut, v across it.
|
||||
let (u, v) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||||
let f = profile(u);
|
||||
let back = |vs: f64| {
|
||||
let (sx, sy) = (u * cs - vs * sn + cx, u * sn + vs * cs + cy);
|
||||
src.sample_bilinear(sx, sy)
|
||||
};
|
||||
*px = if v + d < f {
|
||||
back(v + d)
|
||||
} else if v - d >= f {
|
||||
back(v - d)
|
||||
} else {
|
||||
[0.0; 4]
|
||||
};
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Line Sweep
|
||||
|
||||
fn line_sweep(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = completion(ctx) as f64;
|
||||
if c <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let s = b.scale;
|
||||
let ang = ctx.params.f("direction").to_radians();
|
||||
let (sn, cs) = ang.sin_cos();
|
||||
let thick = (ctx.params.f("thickness") * s).max(1.0);
|
||||
let slant = ctx.params.f("slant") * s;
|
||||
let flip = ctx.params.b("flipDirection");
|
||||
// Projected extents of the buffer along the sweep (u) and across it (v).
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
let corners = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)];
|
||||
let proj = |x: f64, y: f64| (x * cs + y * sn, -x * sn + y * cs);
|
||||
let (mut u0, mut u1, mut v0, mut v1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
|
||||
for (x, y) in corners {
|
||||
let (u, v) = proj(x, y);
|
||||
u0 = u0.min(u);
|
||||
u1 = u1.max(u);
|
||||
v0 = v0.min(v);
|
||||
v1 = v1.max(v);
|
||||
}
|
||||
let nstr = ((v1 - v0) / thick).ceil().max(1.0);
|
||||
let total = (u1 - u0) + slant.abs() * nstr + 1.0;
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (u, v) = proj(x as f64 + 0.5, y as f64 + 0.5);
|
||||
let k = ((v - v0) / thick).floor();
|
||||
let k = if flip { nstr - 1.0 - k } else { k };
|
||||
let stagger = if slant >= 0.0 { k } else { nstr - 1.0 - k };
|
||||
let front = u0 + c * total - slant.abs() * stagger;
|
||||
if u < front {
|
||||
*px = [0.0; 4];
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Twister
|
||||
|
||||
fn twister(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = completion(ctx) as f64;
|
||||
if c <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let (cx, cy) = b.to_px(ctx.params.v2("center"));
|
||||
let ang = ctx.params.f("axis").to_radians();
|
||||
let (sn, cs) = ang.sin_cos();
|
||||
let shading = ctx.params.b("shading");
|
||||
let half = (b.img.width as f64).hypot(b.img.height as f64) * 0.5;
|
||||
let src = b.img.clone();
|
||||
let back = reveal(ctx, &b, "backside");
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (dx, dy) = (x as f64 + 0.5 - cx, y as f64 + 0.5 - cy);
|
||||
// u along the twist axis, v across it.
|
||||
let (u, v) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||||
let s01 = ((u / half) * 0.5 + 0.5).clamp(0.0, 1.0);
|
||||
let th = std::f64::consts::PI * (c * 2.0 - s01).clamp(0.0, 1.0);
|
||||
let k = th.cos();
|
||||
if k.abs() < 1e-4 {
|
||||
*px = [0.0; 4];
|
||||
continue;
|
||||
}
|
||||
let vs = v / k;
|
||||
let (sx, sy) = (u * cs - vs * sn + cx, u * sn + vs * cs + cy);
|
||||
let mut o: Px = if k > 0.0 {
|
||||
src.sample_bilinear(sx, sy)
|
||||
} else {
|
||||
// Back side: the layer itself is seen from behind (mirrored across the axis); a
|
||||
// Backside layer is mapped so it reads upright when fully flipped.
|
||||
match &back {
|
||||
Some(bk) => bk.sample_bilinear(u * cs + vs * sn + cx, u * sn - vs * cs + cy),
|
||||
None => src.sample_bilinear(sx, sy),
|
||||
}
|
||||
};
|
||||
if shading {
|
||||
let l = (0.4 + 0.6 * k.abs()) as f32;
|
||||
o = [o[0] * l, o[1] * l, o[2] * l, o[3]];
|
||||
}
|
||||
*px = o;
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC WarpoMatic
|
||||
|
||||
fn warpomatic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = completion(ctx);
|
||||
if c <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let rimg = layer_or_self(ctx, &b, "reactorLayer", true, false);
|
||||
let mut m = prop_plane(&rimg, 4);
|
||||
let sm = ctx.params.f("smoothness") * b.scale * 0.5;
|
||||
if sm > 0.05 {
|
||||
m = gauss_plane(&m, sm, sm);
|
||||
}
|
||||
m = match ctx.params.e("reactor") {
|
||||
1 => {
|
||||
// Contrast differences: distance from the local mean.
|
||||
let mean = gauss_plane(&m, 8.0 * b.scale, 8.0 * b.scale);
|
||||
m.zip_map(&mean, |a, b| ((a - b).abs() * 4.0).min(1.0))
|
||||
}
|
||||
2 => {
|
||||
// Local differences: gradient magnitude.
|
||||
let mut o = Plane::new(m.w, m.h);
|
||||
o.data.par_chunks_mut(m.w.max(1)).enumerate().for_each(|(y, row)| {
|
||||
for (x, v) in row.iter_mut().enumerate() {
|
||||
let (gx, gy) = grad(&m, x, y);
|
||||
*v = ((gx * gx + gy * gy).sqrt() * 8.0).min(1.0);
|
||||
}
|
||||
});
|
||||
o
|
||||
}
|
||||
_ => m,
|
||||
};
|
||||
let amt = (ctx.params.f("warpAmount") * b.scale) as f32 * 20.0;
|
||||
let dir = ctx.params.e("warpDirection");
|
||||
let span = (ctx.params.f("blendSpan") / 100.0) as f32 * 0.5;
|
||||
let rev = reveal(ctx, &b, "layerToReveal");
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let q = wipe_t(m.get(x, y), c, span);
|
||||
let (gx, gy) = grad(&m, x, y);
|
||||
let (fx, fy) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
let (ox, oy) = ((gx * amt * q) as f64, (gy * amt * q) as f64);
|
||||
let (ix, iy) = ((gx * amt * (1.0 - q)) as f64, (gy * amt * (1.0 - q)) as f64);
|
||||
let (ix, iy) = match dir {
|
||||
1 => (-ix, -iy),
|
||||
2 => (-iy, ix),
|
||||
_ => (ix, iy),
|
||||
};
|
||||
let out = if q < 1.0 { src.sample_bilinear(fx + ox, fy + oy) } else { [0.0; 4] };
|
||||
let inc = match &rev {
|
||||
Some(r) if q > 0.0 => r.sample_bilinear(fx + ix, fy + iy),
|
||||
_ => [0.0; 4],
|
||||
};
|
||||
*px = lerp4(out, inc, q);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ specs
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
let done = || p("completion", "Completion", num(0.0), pct());
|
||||
vec![
|
||||
spec(
|
||||
"ec.transition.ccglasswipe",
|
||||
"CC Glass Wipe",
|
||||
vec![
|
||||
done(),
|
||||
p("layerToReveal", "Layer to Reveal", Value::Layer(None), ParamUi::Layer),
|
||||
p("gradientLayer", "Gradient Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("softness", "Softness", num(10.0), pct()),
|
||||
p("displacementAmount", "Displacement Amount", num(10.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
],
|
||||
glass_wipe,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.ccimagewipe",
|
||||
"CC Image Wipe",
|
||||
vec![
|
||||
done(),
|
||||
p("borderSoftness", "Border Softness", num(0.0), pct()),
|
||||
p("autoSoftness", "Auto Softness", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("layer", "Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("property", "Property", Value::Enum(4), popup(&GRAD_PROPS)),
|
||||
p("blur", "Blur", num(0.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("inverseGradient", "Inverse Gradient", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
image_wipe,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.ccjaws",
|
||||
"CC Jaws",
|
||||
vec![
|
||||
done(),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("height", "Height", num(50.0), pct()),
|
||||
p("width", "Width", num(50.0), slider(1.0, 1000.0, 1.0, 200.0, 1)),
|
||||
p("shape", "Shape", Value::Enum(0), popup(&["Spikes", "RoboJaw", "Block", "Waves"])),
|
||||
],
|
||||
jaws,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.cclinesweep",
|
||||
"CC Line Sweep",
|
||||
vec![
|
||||
done(),
|
||||
p("direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("thickness", "Thickness", num(20.0), slider(1.0, 500.0, 1.0, 100.0, 1)),
|
||||
p("slant", "Slant", num(20.0), slider(-500.0, 500.0, -100.0, 100.0, 1)),
|
||||
p("flipDirection", "Flip Direction", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
line_sweep,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.cctwister",
|
||||
"CC Twister",
|
||||
vec![
|
||||
done(),
|
||||
p("backside", "Backside", Value::Layer(None), ParamUi::Layer),
|
||||
p("shading", "Shading", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("axis", "Axis", num(45.0), ParamUi::Angle),
|
||||
],
|
||||
twister,
|
||||
),
|
||||
spec(
|
||||
"ec.transition.ccwarpomatic",
|
||||
"CC WarpoMatic",
|
||||
vec![
|
||||
done(),
|
||||
p("layerToReveal", "Layer to Reveal", Value::Layer(None), ParamUi::Layer),
|
||||
p("reactorLayer", "Reactor Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("reactor", "Reactor", Value::Enum(0), popup(&["Brightness", "Contrast Differences", "Local Differences"])),
|
||||
p("smoothness", "Smoothness", num(10.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("warpAmount", "Warp Amount", num(50.0), slider(-1000.0, 1000.0, -200.0, 200.0, 1)),
|
||||
p("warpDirection", "Warp Direction", Value::Enum(0), popup(&["Joint", "Opposing", "Twisting"])),
|
||||
p("blendSpan", "Blend Span", num(10.0), pct()),
|
||||
],
|
||||
warpomatic,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{EffectEnv, run_fx};
|
||||
|
||||
fn ramp(w: u32, h: u32) -> Image {
|
||||
let mut img = Image::new(w, h);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
img.set(x, y, [x as f32 / w as f32, y as f32 / h as f32, 0.3, 1.0]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
fn run(id: &str, vals: &[(&str, Value)], img: Image) -> Buf {
|
||||
run_fx(id, vals, img, 0.0, EffectEnv::default())
|
||||
}
|
||||
|
||||
const ALL: [&str; 6] = [
|
||||
"ec.transition.ccglasswipe",
|
||||
"ec.transition.ccimagewipe",
|
||||
"ec.transition.ccjaws",
|
||||
"ec.transition.cclinesweep",
|
||||
"ec.transition.cctwister",
|
||||
"ec.transition.ccwarpomatic",
|
||||
];
|
||||
|
||||
fn centre(id: &str) -> Vec<(&'static str, Value)> {
|
||||
match id {
|
||||
"ec.transition.ccjaws" => vec![("center", pt(20.0, 16.0)), ("width", num(10.0))],
|
||||
"ec.transition.cctwister" => vec![("center", pt(20.0, 16.0))],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_completion_is_identity() {
|
||||
let img = ramp(40, 32);
|
||||
for id in ALL {
|
||||
let o = run(id, ¢re(id), img.clone());
|
||||
assert_eq!(o.img.data, img.data, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_completion_is_transparent() {
|
||||
let img = ramp(40, 32);
|
||||
for id in ALL {
|
||||
if id == "ec.transition.cctwister" {
|
||||
continue;
|
||||
}
|
||||
let mut v = centre(id);
|
||||
v.push(("completion", num(100.0)));
|
||||
let o = run(id, &v, img.clone());
|
||||
assert!(o.img.data.iter().all(|p| p[3].abs() < 1e-5), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midway_deterministic_and_partial() {
|
||||
let img = ramp(40, 32);
|
||||
for id in ALL {
|
||||
let mut v = centre(id);
|
||||
v.push(("completion", num(50.0)));
|
||||
let a = run(id, &v, img.clone());
|
||||
let b = run(id, &v, img.clone());
|
||||
assert_eq!(a.img.data, b.img.data, "{id}");
|
||||
let opaque = a.img.data.iter().filter(|p| p[3] > 0.99).count();
|
||||
let clear = a.img.data.iter().filter(|p| p[3] < 0.01).count();
|
||||
if id != "ec.transition.cctwister" {
|
||||
assert!(opaque > 0 && clear > 0, "{id}: {opaque} {clear}");
|
||||
}
|
||||
assert_ne!(a.img.data, img.data, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twister_full_shows_mirrored_back() {
|
||||
let img = ramp(40, 32);
|
||||
let o = run(
|
||||
"ec.transition.cctwister",
|
||||
&[("completion", num(100.0)), ("shading", Value::Bool(false)), ("axis", num(0.0)), ("center", pt(20.0, 16.0))],
|
||||
img.clone(),
|
||||
);
|
||||
// Axis horizontal through the centre: the back is the source flipped vertically.
|
||||
for (x, y) in [(5usize, 4usize), (30, 20), (12, 27)] {
|
||||
let got = o.img.data[y * 40 + x];
|
||||
let want = img.data[(31 - y) * 40 + x];
|
||||
assert!((got[1] - want[1]).abs() < 1e-3 && (got[0] - want[0]).abs() < 1e-3, "{x},{y} {got:?} {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_sweep_flip_reverses_stagger() {
|
||||
let img = ramp(40, 32);
|
||||
let a = run("ec.transition.cclinesweep", &[("completion", num(40.0))], img.clone());
|
||||
let b = run("ec.transition.cclinesweep", &[("completion", num(40.0)), ("flipDirection", Value::Bool(true))], img.clone());
|
||||
let cleared = |o: &Buf, y: usize| (0..40).filter(|&x| o.img.data[y * 40 + x][3] < 0.01).count();
|
||||
// Unflipped: lower stripes trail; flipped: upper stripes trail.
|
||||
assert!(cleared(&a, 2) > cleared(&a, 30));
|
||||
assert!(cleared(&b, 2) < cleared(&b, 30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_wipe_follows_gradient() {
|
||||
let img = ramp(40, 32);
|
||||
let o = run("ec.transition.ccimagewipe", &[("completion", num(50.0)), ("property", Value::Enum(0))], img.clone());
|
||||
// Red ramps left→right: dark (left) side goes first.
|
||||
assert!(o.img.data[16 * 40 + 2][3] < 0.01);
|
||||
assert!(o.img.data[16 * 40 + 37][3] > 0.99);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue