//! 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(×tamp)); 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_and_formats_them() { 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); 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'] );}
//! 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(×tamp));
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_and_formats_them() {
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);
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']
);
}