use crate::visual_fx::{BackdropFx, FxContext};
use ftui_render::cell::PackedRgba;
#[inline]
fn xorshift32(state: &mut u32) -> u32 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
*state = x;
x
}
pub struct ScreenMeltFx {
column_offsets: Vec<i32>,
frozen_frame: Vec<PackedRgba>,
inner: Box<dyn BackdropFx>,
inner_buf: Vec<PackedRgba>,
started: bool,
done: bool,
rng_seed: u32,
last_width: u16,
last_height: u16,
}
impl ScreenMeltFx {
pub fn new(inner: Box<dyn BackdropFx>) -> Self {
Self::with_seed(inner, 0xDEAD_BEEF)
}
pub fn with_seed(inner: Box<dyn BackdropFx>, seed: u32) -> Self {
Self {
column_offsets: Vec::new(),
frozen_frame: Vec::new(),
inner,
inner_buf: Vec::new(),
started: false,
done: false,
rng_seed: seed,
last_width: 0,
last_height: 0,
}
}
pub fn reset(&mut self) {
self.started = false;
self.done = false;
}
pub fn is_done(&self) -> bool {
self.done
}
pub fn inner(&self) -> &dyn BackdropFx {
&*self.inner
}
pub fn inner_mut(&mut self) -> &mut dyn BackdropFx {
&mut *self.inner
}
fn init_offsets(&mut self, width: u16) {
let w = width as usize;
if w > self.column_offsets.len() {
self.column_offsets.resize(w, 0);
}
let mut rng = self.rng_seed | 1;
let first = -((xorshift32(&mut rng) % 16) as i32);
self.column_offsets[0] = first;
for x in 1..w {
let r = (xorshift32(&mut rng) % 3) as i32 - 1; let prev = self.column_offsets[x - 1];
self.column_offsets[x] = (prev + r).clamp(-15, 0);
}
}
fn advance(&mut self) {
if self.done {
return;
}
let w = self.last_width as usize;
let h = self.last_height as i32;
let mut all_done = true;
for x in 0..w {
let y = self.column_offsets[x];
if y < 0 {
self.column_offsets[x] = y + 1;
all_done = false;
} else if y < h {
let dy = (y + 1).min(8);
self.column_offsets[x] = y + dy;
all_done = false;
}
}
self.done = all_done;
}
}
impl BackdropFx for ScreenMeltFx {
fn name(&self) -> &'static str {
"Screen Melt"
}
fn resize(&mut self, width: u16, height: u16) {
self.inner.resize(width, height);
}
fn render(&mut self, ctx: FxContext<'_>, out: &mut [PackedRgba]) {
let w = ctx.width as usize;
let h = ctx.height as usize;
if w == 0 || h == 0 {
return;
}
let len = w * h;
if self.inner_buf.len() < len {
self.inner_buf.resize(len, PackedRgba::rgb(0, 0, 0));
}
self.inner.render(ctx, &mut self.inner_buf[..len]);
if !self.started {
self.last_width = ctx.width;
self.last_height = ctx.height;
if self.frozen_frame.len() < len {
self.frozen_frame.resize(len, PackedRgba::rgb(0, 0, 0));
}
self.frozen_frame[..len].copy_from_slice(&out[..len]);
self.init_offsets(ctx.width);
self.started = true;
}
if self.last_width != ctx.width || self.last_height != ctx.height {
self.last_width = ctx.width;
self.last_height = ctx.height;
self.init_offsets(ctx.width);
}
if self.done {
out[..len].copy_from_slice(&self.inner_buf[..len]);
return;
}
self.advance();
let frozen_len = self.frozen_frame.len();
let h_i32 = h as i32;
for y in 0..h {
let row_base = y * w;
let y_i32 = y as i32;
for x in 0..w {
let idx = row_base + x;
let offset = self.column_offsets[x];
if y_i32 < offset {
let src_y = y_i32 - offset + h_i32;
if src_y >= 0 && (src_y as usize) < h {
let src_idx = src_y as usize * w + x;
if src_idx < frozen_len {
out[idx] = self.frozen_frame[src_idx];
}
}
} else if offset >= 0 {
out[idx] = self.inner_buf[idx];
} else {
if idx < frozen_len {
out[idx] = self.frozen_frame[idx];
}
}
}
}
}
}
impl std::fmt::Debug for ScreenMeltFx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScreenMeltFx")
.field("started", &self.started)
.field("done", &self.done)
.field("last_width", &self.last_width)
.field("last_height", &self.last_height)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::visual_fx::{FxQuality, ThemeInputs};
struct SolidFx {
color: PackedRgba,
}
impl BackdropFx for SolidFx {
fn name(&self) -> &'static str {
"Solid"
}
fn render(&mut self, ctx: FxContext<'_>, out: &mut [PackedRgba]) {
let len = ctx.width as usize * ctx.height as usize;
for p in out.iter_mut().take(len) {
*p = self.color;
}
}
}
fn default_theme() -> ThemeInputs {
ThemeInputs::default_dark()
}
fn make_ctx(width: u16, height: u16, frame: u64) -> FxContext<'static> {
let theme = Box::leak(Box::new(default_theme()));
FxContext {
width,
height,
frame,
time_seconds: frame as f64 / 60.0,
quality: FxQuality::Full,
theme,
}
}
#[test]
fn melt_starts_and_completes() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 100];
let ctx = make_ctx(10, 10, 0);
melt.render(ctx, &mut buf);
assert!(!melt.is_done());
for frame in 1..200 {
let ctx = make_ctx(10, 10, frame);
melt.render(ctx, &mut buf);
if melt.is_done() {
break;
}
}
assert!(melt.is_done(), "Melt should complete within 200 frames");
let ctx = make_ctx(10, 10, 200);
melt.render(ctx, &mut buf);
assert_eq!(buf[0], PackedRgba::rgb(255, 0, 0));
}
#[test]
fn melt_zero_dimensions() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let ctx = make_ctx(0, 0, 0);
let mut buf = vec![];
melt.render(ctx, &mut buf);
}
#[test]
fn melt_deterministic() {
let inner1 = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let inner2 = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt1 = ScreenMeltFx::with_seed(inner1, 42);
let mut melt2 = ScreenMeltFx::with_seed(inner2, 42);
let mut buf1 = vec![PackedRgba::rgb(0, 0, 255); 200];
let mut buf2 = vec![PackedRgba::rgb(0, 0, 255); 200];
for frame in 0..20 {
let ctx = make_ctx(20, 10, frame);
melt1.render(ctx, &mut buf1);
melt2.render(ctx, &mut buf2);
assert_eq!(buf1, buf2, "Frame {frame} should be identical");
}
}
#[test]
fn melt_reset() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 100];
for frame in 0..200 {
let ctx = make_ctx(10, 10, frame);
melt.render(ctx, &mut buf);
}
assert!(melt.is_done());
melt.reset();
assert!(!melt.is_done());
assert!(!melt.started);
}
#[test]
fn xorshift_nonzero_output() {
let mut state = 1u32;
for _ in 0..100 {
let v = xorshift32(&mut state);
assert_ne!(v, 0, "xorshift32 should not produce zero");
}
}
#[test]
fn xorshift_deterministic() {
let mut s1 = 42u32;
let mut s2 = 42u32;
for _ in 0..50 {
assert_eq!(xorshift32(&mut s1), xorshift32(&mut s2));
}
}
#[test]
fn xorshift_different_seeds_diverge() {
let mut s1 = 1u32;
let mut s2 = 2u32;
let v1 = xorshift32(&mut s1);
let v2 = xorshift32(&mut s2);
assert_ne!(v1, v2, "different seeds should produce different output");
}
#[test]
fn init_offsets_range() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 123);
melt.column_offsets.resize(80, 0);
melt.init_offsets(80);
for (i, &offset) in melt.column_offsets.iter().take(80).enumerate() {
assert!(
(-15..=0).contains(&offset),
"column {i} offset {offset} out of [-15, 0]"
);
}
}
#[test]
fn init_offsets_adjacent_differ_by_at_most_one() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 99);
melt.column_offsets.resize(80, 0);
melt.init_offsets(80);
for x in 1..80 {
let diff = (melt.column_offsets[x] - melt.column_offsets[x - 1]).abs();
assert!(
diff <= 1,
"adjacent columns {}/{} differ by {diff}",
x - 1,
x
);
}
}
#[test]
fn init_offsets_deterministic_with_same_seed() {
let inner1 = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let inner2 = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut m1 = ScreenMeltFx::with_seed(inner1, 55);
let mut m2 = ScreenMeltFx::with_seed(inner2, 55);
m1.column_offsets.resize(40, 0);
m2.column_offsets.resize(40, 0);
m1.init_offsets(40);
m2.init_offsets(40);
assert_eq!(
&m1.column_offsets[..40],
&m2.column_offsets[..40],
"same seed should produce same offsets"
);
}
#[test]
fn advance_negative_offsets_increment_by_one() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.column_offsets = vec![-10];
melt.last_width = 1;
melt.last_height = 20;
melt.advance();
assert_eq!(melt.column_offsets[0], -9);
}
#[test]
fn advance_positive_offsets_accelerate() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.last_width = 1;
melt.last_height = 100;
melt.column_offsets = vec![0];
melt.advance();
assert_eq!(melt.column_offsets[0], 1);
melt.column_offsets = vec![3];
melt.advance();
assert_eq!(melt.column_offsets[0], 7);
melt.column_offsets = vec![10];
melt.advance();
assert_eq!(melt.column_offsets[0], 18); }
#[test]
fn advance_marks_done_when_all_columns_past_bottom() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.last_width = 3;
melt.last_height = 10;
melt.column_offsets = vec![10, 10, 10];
melt.advance();
assert!(melt.done);
}
#[test]
fn advance_not_done_if_any_column_active() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.last_width = 3;
melt.last_height = 10;
melt.column_offsets = vec![10, 5, 10];
melt.advance();
assert!(!melt.done);
}
#[test]
fn advance_noop_when_already_done() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.done = true;
melt.last_width = 1;
melt.last_height = 10;
melt.column_offsets = vec![5];
melt.advance();
assert_eq!(melt.column_offsets[0], 5, "should not change when done");
}
#[test]
fn first_render_captures_frozen_frame() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 25];
let ctx = make_ctx(5, 5, 0);
melt.render(ctx, &mut buf);
assert!(melt.started);
assert_eq!(melt.frozen_frame[0], PackedRgba::rgb(0, 0, 255));
}
#[test]
fn completed_melt_shows_inner_effect() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 255, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(100, 100, 100); 25];
for frame in 0..300 {
let ctx = make_ctx(5, 5, frame);
melt.render(ctx, &mut buf);
if melt.is_done() {
break;
}
}
assert!(melt.is_done());
let ctx = make_ctx(5, 5, 300);
melt.render(ctx, &mut buf);
for (i, &px) in buf.iter().enumerate() {
assert_eq!(px, PackedRgba::rgb(0, 255, 0), "pixel {i} should be green");
}
}
#[test]
fn different_seeds_produce_different_patterns() {
let inner1 = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let inner2 = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut m1 = ScreenMeltFx::with_seed(inner1, 1);
let mut m2 = ScreenMeltFx::with_seed(inner2, 999);
let mut buf1 = vec![PackedRgba::rgb(0, 0, 255); 200];
let mut buf2 = vec![PackedRgba::rgb(0, 0, 255); 200];
for frame in 0..5 {
let ctx = make_ctx(20, 10, frame);
m1.render(ctx, &mut buf1);
m2.render(ctx, &mut buf2);
}
assert_ne!(
buf1, buf2,
"different seeds should produce different patterns"
);
}
#[test]
fn name_returns_expected() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let melt = ScreenMeltFx::new(inner);
assert_eq!(melt.name(), "Screen Melt");
}
#[test]
fn debug_format_includes_fields() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let melt = ScreenMeltFx::new(inner);
let debug = format!("{melt:?}");
assert!(debug.contains("ScreenMeltFx"));
assert!(debug.contains("started"));
assert!(debug.contains("done"));
}
#[test]
fn inner_accessor() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let melt = ScreenMeltFx::new(inner);
assert_eq!(melt.inner().name(), "Solid");
}
#[test]
fn inner_mut_accessor() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
assert_eq!(melt.inner_mut().name(), "Solid");
}
#[test]
fn reset_allows_restart() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 100];
for frame in 0..200 {
let ctx = make_ctx(10, 10, frame);
melt.render(ctx, &mut buf);
}
assert!(melt.is_done());
melt.reset();
let ctx = make_ctx(10, 10, 300);
melt.render(ctx, &mut buf);
assert!(melt.started);
assert!(!melt.is_done(), "should be animating again after reset");
}
#[test]
fn melt_completes_in_bounded_frames() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 0); 8000];
let mut frames = 0;
for frame in 0..500 {
let ctx = make_ctx(80, 100, frame);
melt.render(ctx, &mut buf);
frames = frame;
if melt.is_done() {
break;
}
}
assert!(
melt.is_done(),
"80x100 melt should complete within 500 frames (took {frames})"
);
}
#[test]
fn is_done_initially_false() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let melt = ScreenMeltFx::new(inner);
assert!(!melt.is_done());
}
#[test]
fn melt_1x1_grid() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 1];
for frame in 0..50 {
let ctx = make_ctx(1, 1, frame);
melt.render(ctx, &mut buf);
if melt.is_done() {
break;
}
}
assert!(melt.is_done(), "1x1 melt should complete quickly");
}
#[test]
fn melt_zero_width_nonzero_height() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let ctx = make_ctx(0, 10, 0);
let mut buf = vec![];
melt.render(ctx, &mut buf);
assert!(!melt.started);
}
#[test]
fn melt_nonzero_width_zero_height() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let ctx = make_ctx(10, 0, 0);
let mut buf = vec![];
melt.render(ctx, &mut buf);
assert!(!melt.started);
}
#[test]
fn advance_acceleration_capped_at_8() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.last_width = 1;
melt.last_height = 1000;
melt.column_offsets = vec![20];
melt.advance();
assert_eq!(melt.column_offsets[0], 28);
melt.column_offsets = vec![100];
melt.advance();
assert_eq!(melt.column_offsets[0], 108);
}
#[test]
fn advance_zero_offset_boundary() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.last_width = 1;
melt.last_height = 100;
melt.column_offsets = vec![0];
melt.advance();
assert_eq!(melt.column_offsets[0], 1);
}
#[test]
fn init_offsets_single_column() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 42);
melt.column_offsets.resize(1, 0);
melt.init_offsets(1);
assert!(
(-15..=0).contains(&melt.column_offsets[0]),
"single column offset {} out of range",
melt.column_offsets[0]
);
}
#[test]
fn default_seed_is_deadbeef() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let melt = ScreenMeltFx::new(inner);
assert_eq!(melt.rng_seed, 0xDEAD_BEEF);
}
#[test]
fn inner_buf_grows_only() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 200];
let ctx = make_ctx(20, 10, 0);
melt.render(ctx, &mut buf);
let cap_after_large = melt.inner_buf.capacity();
let mut buf2 = vec![PackedRgba::rgb(0, 0, 255); 20];
melt.reset();
let ctx2 = make_ctx(5, 4, 0);
melt.render(ctx2, &mut buf2);
assert!(
melt.inner_buf.capacity() >= cap_after_large,
"inner_buf capacity should not shrink"
);
}
#[test]
fn dimension_change_reinits_offsets() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 100];
let ctx = make_ctx(10, 10, 0);
melt.render(ctx, &mut buf);
let _offsets_10 = melt.column_offsets[..10].to_vec();
for frame in 1..5 {
let ctx = make_ctx(10, 10, frame);
melt.render(ctx, &mut buf);
}
let mut buf2 = vec![PackedRgba::rgb(0, 0, 255); 200];
let ctx = make_ctx(20, 10, 5);
melt.render(ctx, &mut buf2);
let offsets_20 = melt.column_offsets[..10].to_vec();
for (i, &o) in offsets_20.iter().enumerate() {
assert!(
(-15..=0).contains(&o),
"reinited column {i} offset {o} out of [-15, 0]"
);
}
assert!(melt.column_offsets.len() >= 20);
}
#[test]
fn xorshift_no_short_cycle() {
let mut state = 1u32;
let first = xorshift32(&mut state);
let mut seen_first = false;
for _ in 0..1000 {
if xorshift32(&mut state) == first {
seen_first = true;
break;
}
}
assert!(!seen_first, "xorshift32 should not have a short cycle");
}
#[test]
fn frozen_frame_preserves_pattern() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(255, 0, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf: Vec<PackedRgba> = (0..25)
.map(|i| {
if i % 2 == 0 {
PackedRgba::rgb(10, 20, 30)
} else {
PackedRgba::rgb(200, 200, 200)
}
})
.collect();
let original = buf.clone();
let ctx = make_ctx(5, 5, 0);
melt.render(ctx, &mut buf);
for (i, expected) in original.iter().enumerate().take(25) {
assert_eq!(
melt.frozen_frame[i], *expected,
"frozen frame pixel {i} should match original"
);
}
}
#[test]
fn resize_propagates_to_inner() {
struct TrackingFx {
resized: bool,
last_w: u16,
last_h: u16,
}
impl BackdropFx for TrackingFx {
fn name(&self) -> &'static str {
"Tracking"
}
fn resize(&mut self, w: u16, h: u16) {
self.resized = true;
self.last_w = w;
self.last_h = h;
}
fn render(&mut self, _ctx: FxContext<'_>, _out: &mut [PackedRgba]) {}
}
let inner = Box::new(TrackingFx {
resized: false,
last_w: 0,
last_h: 0,
});
let mut melt = ScreenMeltFx::new(inner);
melt.resize(40, 20);
assert_eq!(melt.inner().name(), "Tracking");
}
#[test]
fn melt_wide_2x2() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 255, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 4];
for frame in 0..50 {
let ctx = make_ctx(2, 2, frame);
melt.render(ctx, &mut buf);
if melt.is_done() {
break;
}
}
assert!(melt.is_done(), "2x2 melt should complete");
let ctx = make_ctx(2, 2, 100);
melt.render(ctx, &mut buf);
assert!(buf.iter().all(|&px| px == PackedRgba::rgb(0, 255, 0)));
}
#[test]
fn melt_tall_1x20() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 255, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 20];
for frame in 0..100 {
let ctx = make_ctx(1, 20, frame);
melt.render(ctx, &mut buf);
if melt.is_done() {
break;
}
}
assert!(
melt.is_done(),
"1x20 melt should complete within 100 frames"
);
}
#[test]
fn melt_wide_100x1() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 255, 0),
});
let mut melt = ScreenMeltFx::new(inner);
let mut buf = vec![PackedRgba::rgb(0, 0, 255); 100];
for frame in 0..50 {
let ctx = make_ctx(100, 1, frame);
melt.render(ctx, &mut buf);
if melt.is_done() {
break;
}
}
assert!(
melt.is_done(),
"100x1 melt should complete quickly (height=1)"
);
}
#[test]
fn column_offsets_all_negative_means_not_done() {
let inner = Box::new(SolidFx {
color: PackedRgba::rgb(0, 0, 0),
});
let mut melt = ScreenMeltFx::with_seed(inner, 1);
melt.last_width = 3;
melt.last_height = 10;
melt.column_offsets = vec![-5, -3, -1];
melt.advance();
assert!(!melt.done, "negative offsets mean melt is still pending");
assert_eq!(melt.column_offsets, vec![-4, -2, 0]);
}
}