Files
kiln/kiln-tui/src/transition.rs
T
2026-06-15 09:15:30 -05:00

138 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<u32> {
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<u32>,
/// Which cells have been revealed (show `new_buf`).
revealed: Vec<bool>,
/// 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 }
}