2026-06-13 18:43:29 -05:00
|
|
|
|
//! Board-change wipe animation.
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! When the player steps through a portal both the old and new boards are rendered
|
2026-06-15 09:15:30 -05:00
|
|
|
|
//! 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.
|
2026-06-13 18:43:29 -05:00
|
|
|
|
|
|
|
|
|
|
use kiln_core::Board;
|
|
|
|
|
|
use ratatui::{
|
|
|
|
|
|
buffer::Buffer,
|
|
|
|
|
|
layout::Rect,
|
|
|
|
|
|
widgets::Widget,
|
|
|
|
|
|
};
|
2026-06-15 09:15:30 -05:00
|
|
|
|
use crate::animation::{Animation, AnimationLayer};
|
|
|
|
|
|
use crate::input::InputMode;
|
2026-06-13 18:43:29 -05:00
|
|
|
|
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.
|
2026-06-15 09:15:30 -05:00
|
|
|
|
/// Implements [`Animation`]: [`update`](Animation::update) marks newly-due cells
|
|
|
|
|
|
/// as revealed each frame; [`draw`](Animation::draw) blends old and new buffers.
|
2026-06-13 18:43:29 -05:00
|
|
|
|
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.
|
2026-06-15 09:15:30 -05:00
|
|
|
|
elapsed: f32,
|
2026-06-13 18:43:29 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl TransitionAnimation {
|
|
|
|
|
|
/// Render both boards into off-screen buffers and build the LFSR sequence.
|
|
|
|
|
|
///
|
2026-06-15 09:15:30 -05:00
|
|
|
|
/// `area` must match the area that will be passed to [`Animation::draw`].
|
2026-06-13 18:43:29 -05:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-15 09:15:30 -05:00
|
|
|
|
}
|
2026-06-13 18:43:29 -05:00
|
|
|
|
|
2026-06-15 09:15:30 -05:00
|
|
|
|
impl Animation for TransitionAnimation {
|
|
|
|
|
|
fn update(&mut self, dt: f32) -> bool {
|
2026-06-13 18:43:29 -05:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 09:15:30 -05:00
|
|
|
|
fn draw(&self, area: Rect, buf: &mut Buffer) {
|
2026-06-13 18:43:29 -05:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-15 09:15:30 -05:00
|
|
|
|
|
|
|
|
|
|
fn layer(&self) -> AnimationLayer { AnimationLayer::Board }
|
|
|
|
|
|
|
|
|
|
|
|
fn input_mode_after(&self) -> InputMode { InputMode::Board }
|
2026-06-13 18:43:29 -05:00
|
|
|
|
}
|