Skip to main content

blizz_ui/components/mascot/
mod.rs

1//! ASCII mascot frame, radial entrance, tile dissolve FX, and optional bobbing motion.
2
3mod frames;
4
5pub mod bobbing;
6pub mod fx;
7
8pub use bobbing::{
9  AnimationExit, AnimationOptions, BobbingFrame, BobbingPhase, bobbing_frame, bobbing_origin,
10  cycle_duration, render_cycle_frame,
11};
12pub use frames::{
13  MASCOT, MascotFrames, centered_origin, lines, queue_centered, queue_frame, queue_frame_owned,
14  size,
15};
16pub use fx::*;
17
18#[cfg(not(tarpaulin_include))]
19pub use bobbing::run_bobbing_animation;
20
21use std::io::Write;
22
23use rand::rngs::ThreadRng;
24
25use crate::RenderContext;
26use crate::layout::{centered_block_origin, top_two_thirds};
27
28/// One frame’s mascot FX progression from the compositor (wizard loop).
29///
30/// [`DissolveState`](fx::DissolveState) pairs with [`dissolve_tick`](Self::dissolve_tick);
31/// [`entrance_tick`](Self::entrance_tick) pairs with [`MascotComponent`]'s cached distance map.
32#[derive(Clone, Copy, Debug)]
33pub struct MascotEffectFrame<'a> {
34  pub dissolve_state: Option<&'a fx::DissolveState>,
35  pub entrance_tick: usize,
36  pub dissolve_tick: usize,
37}
38
39/// Wizard mascot with entrance/dissolve progress, floating bobbing, and cached distance map.
40#[derive(Clone, Debug)]
41pub struct MascotComponent {
42  pub frames: MascotFrames,
43  pub entrance: f64,
44  pub dissolve: f64,
45  pub floating: bool,
46  distance_map: fx::DistanceMap,
47}
48
49impl MascotComponent {
50  pub fn blizz() -> Self {
51    let frames = MascotFrames::blizz();
52    let distance_map = fx::build_distance_map(frames.lines());
53    Self {
54      frames,
55      entrance: 0.0,
56      dissolve: 0.0,
57      floating: false,
58      distance_map,
59    }
60  }
61
62  #[cfg(not(tarpaulin_include))]
63  pub fn render<W: Write>(
64    &self,
65    writer: &mut W,
66    ctx: &RenderContext,
67    rng: &mut ThreadRng,
68    effect: &MascotEffectFrame<'_>,
69  ) -> std::io::Result<()> {
70    if self.entrance <= 0.0 && self.dissolve <= 0.0 {
71      return Ok(());
72    }
73
74    if self.dissolve >= 1.0 {
75      return Ok(());
76    }
77
78    let region = top_two_thirds(ctx.terminal_size);
79    let origin = if self.floating {
80      let bob = bobbing::bobbing_frame(ctx.elapsed);
81      bobbing::bobbing_origin(region, self.frames.size(), bob.row_offset)
82    } else {
83      centered_block_origin(region, self.frames.size())
84    };
85
86    let lines = self.frames.lines();
87    if self.dissolve > 0.0
88      && let Some(state) = effect.dissolve_state
89    {
90      let frame = fx::dissolve_frame(lines, state, effect.dissolve_tick, rng);
91      return queue_frame_owned(writer, origin, &frame);
92    }
93
94    if self.entrance < 1.0 {
95      let frame = fx::entrance_frame(lines, &self.distance_map, effect.entrance_tick, rng);
96      return queue_frame_owned(writer, origin, &frame);
97    }
98
99    queue_frame(writer, origin, lines)
100  }
101}