//! Board-change wipe animation. //! //! When the player steps through a portal both the old and new boards are rendered //! into off-screen ratatui [`Buffer`]s at init time. Each call to //! [`Animation::update`] advances the LFSR-driven reveal bitmap, and //! [`Animation::draw`] blends the two buffers — old cells for unrevealed positions, //! new cells for revealed ones. use kiln_core::Board; use ratatui::{ buffer::Buffer, layout::Rect, widgets::Widget, }; use crate::animation::{Animation, AnimationLayer}; use crate::input::InputMode; use crate::render::BoardWidget; /// How long the wipe lasts in seconds. pub const TRANSITION_DURATION: f32 = 0.25; /// Advance a 16-bit Galois LFSR one step. /// /// Polynomial 0xB400 gives a maximal period of 65 535, covering every realistic /// terminal viewport (e.g. 256 × 200 = 51 200 cells). fn lfsr_step(state: u16) -> u16 { let lsb = state & 1; let s = state >> 1; if lsb != 0 { s ^ 0xB400 } else { s } } /// Build a permutation of `[0, total)` using the 16-bit LFSR. /// /// The LFSR visits every value in `1..=65535` exactly once per cycle. /// Mapping `state → state - 1` gives `0..=65534`; values in `[0, total)` are /// kept in encounter order. `total` must not exceed 65 535. fn lfsr_sequence(total: usize) -> Vec { let mut seq = Vec::with_capacity(total); let mut state: u16 = 1; // non-zero start; LFSR never visits 0 loop { let idx = state as u32 - 1; if (idx as usize) < total { seq.push(idx); if seq.len() == total { break; } } state = lfsr_step(state); if state == 1 { break; } // full cycle — shouldn't happen for total <= 65535 } seq } /// A board-to-board wipe animation driven by an LFSR. /// /// Both boards are rendered into off-screen [`Buffer`]s once at construction. /// Implements [`Animation`]: [`update`](Animation::update) marks newly-due cells /// as revealed each frame; [`draw`](Animation::draw) blends old and new buffers. pub struct TransitionAnimation { /// Pre-rendered old board. old_buf: Buffer, /// Pre-rendered new board. new_buf: Buffer, /// LFSR-ordered permutation of screen-cell indices `[0, width×height)`. reveal_order: Vec, /// Which cells have been revealed (show `new_buf`). revealed: Vec, /// How many entries of `reveal_order` are currently revealed. revealed_count: usize, /// Elapsed seconds since the animation started. elapsed: f32, } impl TransitionAnimation { /// Render both boards into off-screen buffers and build the LFSR sequence. /// /// `area` must match the area that will be passed to [`Animation::draw`]. pub fn new(old: &Board, new: &Board, area: Rect) -> Self { let total = area.width as usize * area.height as usize; let mut old_buf = Buffer::empty(area); BoardWidget::new(old).render(area, &mut old_buf); let mut new_buf = Buffer::empty(area); BoardWidget::new(new).render(area, &mut new_buf); let reveal_order = lfsr_sequence(total); Self { old_buf, new_buf, reveal_order, revealed: vec![false; total], revealed_count: 0, elapsed: 0.0, } } } impl Animation for TransitionAnimation { fn update(&mut self, dt: f32) -> bool { self.elapsed += dt; let total = self.revealed.len(); let target = ((self.elapsed / TRANSITION_DURATION) * total as f32) as usize; let target = target.min(total); // Mark each newly revealed cell in the bitmap. for &idx in &self.reveal_order[self.revealed_count..target] { self.revealed[idx as usize] = true; } self.revealed_count = target; self.elapsed >= TRANSITION_DURATION } fn draw(&self, area: Rect, buf: &mut Buffer) { let w = area.width as usize; for row in 0..area.height { for col in 0..area.width { let idx = row as usize * w + col as usize; // Out-of-range cells (e.g. after a terminal resize) default to new board. let src = if self.revealed.get(idx).copied().unwrap_or(true) { &self.new_buf } else { &self.old_buf }; let pos = (area.x + col, area.y + row); if let (Some(s), Some(d)) = (src.cell(pos), buf.cell_mut(pos)) { *d = s.clone(); } } } } fn layer(&self) -> AnimationLayer { AnimationLayer::Board } fn input_mode_after(&self) -> InputMode { InputMode::Board } }