blizz-ui 3.0.0-dev.9

Self-rendering terminal UI components for the blizz wizard
Documentation
//! ASCII mascot frame, radial entrance, tile dissolve FX, and optional bobbing motion.

mod frames;

pub mod bobbing;
pub mod fx;

pub use bobbing::{
  AnimationExit, AnimationOptions, BobbingFrame, BobbingPhase, bobbing_frame, bobbing_origin,
  cycle_duration, render_cycle_frame,
};
pub use frames::{
  MASCOT, MascotFrames, centered_origin, lines, queue_centered, queue_frame, queue_frame_owned,
  size,
};
pub use fx::*;

#[cfg(not(tarpaulin_include))]
pub use bobbing::run_bobbing_animation;

use std::io::Write;

use rand::rngs::ThreadRng;

use crate::RenderContext;
use crate::layout::{centered_block_origin, top_two_thirds};

/// One frame’s mascot FX progression from the compositor (wizard loop).
///
/// [`DissolveState`](fx::DissolveState) pairs with [`dissolve_tick`](Self::dissolve_tick);
/// [`entrance_tick`](Self::entrance_tick) pairs with [`MascotComponent`]'s cached distance map.
#[derive(Clone, Copy, Debug)]
pub struct MascotEffectFrame<'a> {
  pub dissolve_state: Option<&'a fx::DissolveState>,
  pub entrance_tick: usize,
  pub dissolve_tick: usize,
}

/// Wizard mascot with entrance/dissolve progress, floating bobbing, and cached distance map.
#[derive(Clone, Debug)]
pub struct MascotComponent {
  pub frames: MascotFrames,
  pub entrance: f64,
  pub dissolve: f64,
  pub floating: bool,
  distance_map: fx::DistanceMap,
}

impl MascotComponent {
  pub fn blizz() -> Self {
    let frames = MascotFrames::blizz();
    let distance_map = fx::build_distance_map(frames.lines());
    Self {
      frames,
      entrance: 0.0,
      dissolve: 0.0,
      floating: false,
      distance_map,
    }
  }

  #[cfg(not(tarpaulin_include))]
  pub fn render<W: Write>(
    &self,
    writer: &mut W,
    ctx: &RenderContext,
    rng: &mut ThreadRng,
    effect: &MascotEffectFrame<'_>,
  ) -> std::io::Result<()> {
    if self.entrance <= 0.0 && self.dissolve <= 0.0 {
      return Ok(());
    }

    if self.dissolve >= 1.0 {
      return Ok(());
    }

    let region = top_two_thirds(ctx.terminal_size);
    let origin = if self.floating {
      let bob = bobbing::bobbing_frame(ctx.elapsed);
      bobbing::bobbing_origin(region, self.frames.size(), bob.row_offset)
    } else {
      centered_block_origin(region, self.frames.size())
    };

    let lines = self.frames.lines();
    if self.dissolve > 0.0
      && let Some(state) = effect.dissolve_state
    {
      let frame = fx::dissolve_frame(lines, state, effect.dissolve_tick, rng);
      return queue_frame_owned(writer, origin, &frame);
    }

    if self.entrance < 1.0 {
      let frame = fx::entrance_frame(lines, &self.distance_map, effect.entrance_tick, rng);
      return queue_frame_owned(writer, origin, &frame);
    }

    queue_frame(writer, origin, lines)
  }
}