Preview App typst_timer 示例输出
//! Two seekable millisecond timers for comparing Typst evaluation strategies.
//!
//! `typst_timer_atlas` compiles individual glyphs once, while
//! `typst_timer_recompile` creates a new [`TypstText`] from the complete
//! timestamp on every frame.
//!
//! Each timer animation owns its [`Timer`] (including the duration). The
//! displayed elapsed milliseconds and the playback duration are both derived
//! from that same field.

use std::sync::Arc;

use ranim::{
    color::{AlphaColor, Srgb, palettes::manim},
    glam::{DVec3, dvec3},
    items::vitem::{VItem, geometry::Rectangle},
    prelude::*,
    utils::rate_functions::linear,
};
use ranim_items::vitem::typst::TypstText;

const TIMER_Y: f64 = 0.35;
const GLYPH_HEIGHT: f64 = 1.55;
const CELL_WIDTH: f64 = 0.92;
const BAR_WIDTH: f64 = 8.1;

/// Self-contained timer configuration.
///
/// The duration lives here instead of in a global constant, and each timer
/// animation owns a copy. Both the elapsed-milliseconds mapping and the
/// playback duration passed to [`PlaybackExt::with_duration`] are derived
/// from this field, so they can never drift apart.
#[derive(Clone, Copy)]
struct Timer {
    duration_secs: f64,
}

impl Timer {
    fn new(duration_secs: f64) -> Self {
        assert!(
            duration_secs.is_finite() && duration_secs >= 0.0,
            "timer duration must be finite and non-negative"
        );
        Self { duration_secs }
    }

    fn milliseconds_at_alpha(self, alpha: f64) -> u64 {
        (alpha.clamp(0.0, 1.0) * self.duration_secs * 1_000.0).floor() as u64
    }
}

fn rectangle(
    width: f64,
    height: f64,
    position: DVec3,
    color: AlphaColor<Srgb>,
    opacity: f32,
) -> VItem {
    let mut rectangle = VItem::from(Rectangle::new(width, height).with(|rectangle| {
        rectangle
            .set_stroke_opacity(0.0)
            .set_fill_color(color)
            .set_fill_opacity(opacity);
    }));
    rectangle.move_to(position);
    rectangle
}

fn glyph_vitems(character: &str, scale: f64) -> Vec<VItem> {
    let mut glyph = Vec::<VItem>::from(TypstText::new(character));
    glyph.scale(DVec3::splat(scale)).move_to(DVec3::ZERO);
    glyph
}

fn timer_glyph_scale() -> f64 {
    let reference = TypstText::new("0");
    let [min, max] = reference.aabb();
    GLYPH_HEIGHT / (max.y - min.y)
}

struct GlyphAtlas {
    digits: Vec<Vec<VItem>>,
    colon: Vec<VItem>,
    dot: Vec<VItem>,
}

impl GlyphAtlas {
    fn new() -> Self {
        let scale = timer_glyph_scale();
        Self {
            digits: (0..=9)
                .map(|digit| glyph_vitems(&digit.to_string(), scale))
                .collect(),
            colon: glyph_vitems(":", scale),
            dot: glyph_vitems(".", scale),
        }
    }

    fn glyph(&self, character: char) -> &[VItem] {
        match character {
            '0'..='9' => &self.digits[character as usize - '0' as usize],
            ':' => &self.colon,
            '.' => &self.dot,
            _ => unreachable!("the timer only contains digits and separators"),
        }
    }
}

fn glyph_color(index: usize, character: char) -> AlphaColor<Srgb> {
    if index >= 6 {
        manim::TEAL_C
    } else if matches!(character, ':' | '.') {
        manim::GREY_B
    } else {
        manim::WHITE
    }
}

fn glyph_position(index: usize) -> DVec3 {
    dvec3((index as f64 - 4.0) * CELL_WIDTH, TIMER_Y, 0.1)
}

fn push_progress_bar(alpha: f64, output: &mut Vec<VItem>) {
    let progress_width = BAR_WIDTH * alpha;
    output.push(rectangle(
        BAR_WIDTH,
        0.055,
        dvec3(0.0, -1.25, 0.0),
        manim::GREY_D,
        0.7,
    ));
    if progress_width > 0.0 {
        output.push(rectangle(
            progress_width,
            0.075,
            dvec3((progress_width - BAR_WIDTH) * 0.5, -1.25, 0.02),
            manim::TEAL_C,
            0.95,
        ));
    }
}

fn format_milliseconds(milliseconds: u64) -> [char; 9] {
    let minutes = (milliseconds / 60_000) % 100;
    let seconds = (milliseconds / 1_000) % 60;
    let millis = milliseconds % 1_000;
    [
        char::from_digit((minutes / 10) as u32, 10).unwrap(),
        char::from_digit((minutes % 10) as u32, 10).unwrap(),
        ':',
        char::from_digit((seconds / 10) as u32, 10).unwrap(),
        char::from_digit((seconds % 10) as u32, 10).unwrap(),
        '.',
        char::from_digit((millis / 100) as u32, 10).unwrap(),
        char::from_digit(((millis / 10) % 10) as u32, 10).unwrap(),
        char::from_digit((millis % 10) as u32, 10).unwrap(),
    ]
}

struct AtlasTimerEval {
    atlas: Arc<GlyphAtlas>,
    timer: Timer,
}

impl AtlasTimerEval {
    fn new(atlas: Arc<GlyphAtlas>, timer: Timer) -> Self {
        Self { atlas, timer }
    }

    fn into_animation(self) -> impl IntoAnimNode {
        let duration_secs = self.timer.duration_secs;
        self.with_duration(duration_secs).with_rate_func(linear)
    }
}

impl Eval for AtlasTimerEval {
    type Output = Vec<VItem>;

    fn eval_alpha(&self, alpha: f64) -> Self::Output {
        let alpha = alpha.clamp(0.0, 1.0);
        let characters = format_milliseconds(self.timer.milliseconds_at_alpha(alpha));
        let mut output = Vec::new();

        for (index, character) in characters.into_iter().enumerate() {
            let mut glyph = self.atlas.glyph(character).to_vec();
            glyph
                .set_fill_color(glyph_color(index, character))
                .shift(glyph_position(index));
            output.extend(glyph);
        }

        push_progress_bar(alpha, &mut output);
        output
    }
}

struct RecompileTimerEval {
    glyph_scale: f64,
    timer: Timer,
}

impl RecompileTimerEval {
    fn new(glyph_scale: f64, timer: Timer) -> Self {
        Self { glyph_scale, timer }
    }

    fn into_animation(self) -> impl IntoAnimNode {
        let duration_secs = self.timer.duration_secs;
        self.with_duration(duration_secs).with_rate_func(linear)
    }
}

impl Eval for RecompileTimerEval {
    type Output = Vec<VItem>;

    fn eval_alpha(&self, alpha: f64) -> Self::Output {
        let alpha = alpha.clamp(0.0, 1.0);
        let characters = format_milliseconds(self.timer.milliseconds_at_alpha(alpha));
        let timestamp = characters.iter().collect::<String>();
        let glyphs = Vec::<VItem>::from(TypstText::new(&timestamp));
        let mut output = Vec::new();

        for (index, (character, mut glyph)) in characters.into_iter().zip(glyphs).enumerate() {
            glyph
                .scale(DVec3::splat(self.glyph_scale))
                .move_to(glyph_position(index))
                .set_fill_color(glyph_color(index, character));
            output.push(glyph);
        }

        push_progress_bar(alpha, &mut output);
        output
    }
}

fn timer_camera(timer: Timer) -> impl IntoAnimNode {
    CameraFrame {
        frame_height: 5.0,
        ..Default::default()
    }
    .show()
    .with_duration(timer.duration_secs)
}

fn timer_label(timer: Timer) -> impl IntoAnimNode {
    let mut label = TypstText::new("ELAPSED TIME");
    label
        .scale_to(ScaleHint::PorportionalY(0.32))
        .move_to(DVec3::Y * 2.0)
        .set_fill_color(manim::GREY_B);

    label.show().with_duration(timer.duration_secs)
}

#[scene(clear_color = "#080a10", name = "typst_timer")]
#[wasm_demo_doc]
#[output(fps = 60, dir = "./output/typst_timer")]
fn typst_timer_atlas(r: &mut RanimScene) {
    let timer = Timer::new(10.0);
    let atlas = Arc::new(GlyphAtlas::new());
    r.play(timer_camera(timer));
    r.play(stack![
        AtlasTimerEval::new(atlas, timer).into_animation(),
        timer_label(timer),
    ]);
    r.insert_time_mark(3.456, TimeMark::Capture("preview.png".to_string()));
}

#[scene(clear_color = "#080a10")]
#[wasm_demo_doc]
#[output(fps = 60, dir = "./output/typst_timer")]
fn typst_timer_recompile(r: &mut RanimScene) {
    let timer = Timer::new(10.0);
    r.play(timer_camera(timer));
    r.play(stack![
        RecompileTimerEval::new(timer_glyph_scale(), timer).into_animation(),
        timer_label(timer),
    ]);
    r.insert_time_mark(3.456, TimeMark::Capture("preview.png".to_string()));
}

#[test]
fn timer_derives_milliseconds_from_its_own_duration() {
    assert_eq!(Timer::new(10.0).milliseconds_at_alpha(0.0), 0);
    assert_eq!(Timer::new(2.0).milliseconds_at_alpha(0.5), 1_000);
    assert_eq!(Timer::new(2.0).milliseconds_at_alpha(1.0), 2_000);
}

#[test]
fn formats_milliseconds() {
    assert_eq!(
        format_milliseconds(0),
        ['0', '0', ':', '0', '0', '.', '0', '0', '0']
    );
    assert_eq!(
        format_milliseconds(63_456),
        ['0', '1', ':', '0', '3', '.', '4', '5', '6']
    );
}