use eframe::egui::{
self,
text::{LayoutJob, TextFormat},
};
pub(crate) const VISIBLE_SECS: f64 = 30.0; pub(crate) const FADE_SECS: f64 = 1.0; pub(crate) const CUTOFF_SECS: f64 = VISIBLE_SECS + FADE_SECS;
pub(crate) struct Glyph {
pub ch: char,
pub birth: f64, }
pub(crate) fn fade_factor(age: f64) -> f32 {
if age <= VISIBLE_SECS {
1.0
} else if age >= CUTOFF_SECS {
0.0
} else {
(1.0 - (age - VISIBLE_SECS) / FADE_SECS) as f32
}
}
pub(crate) fn quantize_alpha(factor: f32) -> u8 {
(factor.clamp(0.0, 1.0) * 255.0).round() as u8
}
pub(crate) const SHAKE_SECS: f64 = 0.4; pub(crate) const SHAKE_AMPLITUDE: f32 = 12.0; pub(crate) const SHAKE_FREQ: f64 = 47.0;
pub(crate) fn shake_offset(since: f64) -> f32 {
if since <= 0.0 || since >= SHAKE_SECS {
0.0
} else {
let decay = 1.0 - since / SHAKE_SECS; (SHAKE_AMPLITUDE as f64 * decay * (since * SHAKE_FREQ).sin()) as f32
}
}
pub(crate) fn clear_factor(age_since_clear: f64) -> f32 {
if age_since_clear <= 0.0 {
1.0
} else if age_since_clear >= FADE_SECS {
0.0
} else {
(1.0 - age_since_clear / FADE_SECS) as f32
}
}
pub(crate) fn glyph_factor(birth: f64, now: f64, visibility_head: f64) -> f32 {
let mut f = fade_factor(now - birth);
if birth < visibility_head {
f *= clear_factor(now - visibility_head);
}
f
}
pub(crate) fn runs(glyphs: &[Glyph], now: f64, visibility_head: f64) -> Vec<(String, u8)> {
let mut out: Vec<(String, u8)> = Vec::new();
for g in glyphs {
let alpha = quantize_alpha(glyph_factor(g.birth, now, visibility_head));
match out.last_mut() {
Some((s, a)) if *a == alpha => s.push(g.ch),
_ => out.push((g.ch.to_string(), alpha)),
}
}
out
}
pub(crate) fn word_count(s: &str) -> usize {
s.split_whitespace().count()
}
pub(crate) fn build_job(
glyphs: &[Glyph],
now: f64,
visibility_head: f64,
font: egui::FontId,
base: egui::Color32,
max_width: f32,
) -> LayoutJob {
let mut job = LayoutJob::default();
job.wrap.max_width = max_width;
for (text, alpha) in runs(glyphs, now, visibility_head) {
let color = base.gamma_multiply(alpha as f32 / 255.0);
job.append(
&text,
0.0,
TextFormat {
font_id: font.clone(),
color,
..Default::default()
},
);
}
job
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_count_counts_whitespace_tokens() {
assert_eq!(word_count(""), 0);
assert_eq!(word_count(" "), 0);
assert_eq!(word_count("hello"), 1);
assert_eq!(word_count("hello world"), 2);
assert_eq!(word_count(" multiple spaces\tand\nnewlines "), 4);
}
#[test]
fn fade_factor_thresholds() {
assert_eq!(fade_factor(0.0), 1.0);
assert_eq!(fade_factor(30.0), 1.0);
assert!((fade_factor(30.5) - 0.5).abs() < 1e-6);
assert_eq!(fade_factor(31.0), 0.0);
assert_eq!(fade_factor(100.0), 0.0);
}
#[test]
fn quantize_alpha_rounds() {
assert_eq!(quantize_alpha(1.0), 255);
assert_eq!(quantize_alpha(0.0), 0);
assert_eq!(quantize_alpha(0.5), 128);
}
#[test]
fn runs_coalesce_same_age() {
let glyphs = [
Glyph {
ch: 'a',
birth: 0.0,
},
Glyph {
ch: 'b',
birth: 0.0,
},
Glyph {
ch: 'c',
birth: 0.0,
},
];
assert_eq!(runs(&glyphs, 0.0, 0.0), vec![("abc".to_string(), 255)]);
}
#[test]
fn runs_split_on_alpha_change() {
let glyphs = [
Glyph {
ch: 'x',
birth: 100.0,
},
Glyph {
ch: 'y',
birth: 0.0,
},
];
assert_eq!(
runs(&glyphs, 100.0, 0.0),
vec![("x".to_string(), 255), ("y".to_string(), 0)]
);
}
#[test]
fn clear_factor_immediate() {
assert_eq!(clear_factor(0.0), 1.0);
assert!((clear_factor(0.5) - 0.5).abs() < 1e-6);
assert_eq!(clear_factor(1.0), 0.0);
assert_eq!(clear_factor(5.0), 0.0);
}
#[test]
fn glyph_factor_unaffected_after_head() {
let head = 5.0;
let birth = 5.0;
assert_eq!(glyph_factor(birth, birth, head), 1.0);
}
#[test]
fn shake_offset_at_rest_outside_window() {
assert_eq!(shake_offset(-1.0), 0.0);
assert_eq!(shake_offset(0.0), 0.0);
assert_eq!(shake_offset(SHAKE_SECS), 0.0);
assert_eq!(shake_offset(SHAKE_SECS + 1.0), 0.0);
}
#[test]
fn shake_offset_bounded_and_decays() {
let mut nonzero = 0;
let mut t = 0.005_f64;
while t < SHAKE_SECS {
let env = SHAKE_AMPLITUDE * (1.0 - (t / SHAKE_SECS) as f32);
assert!(shake_offset(t).abs() <= env + 1e-3);
if shake_offset(t).abs() > 0.1 {
nonzero += 1;
}
t += 0.005;
}
assert!(nonzero > 0, "shake should be visibly nonzero mid-window");
}
#[test]
fn glyph_factor_clears_older_glyphs() {
let birth = 0.0;
let head = 10.0;
assert_eq!(glyph_factor(birth, 10.0, head), 1.0);
assert!((glyph_factor(birth, 10.5, head) - 0.5).abs() < 1e-6);
assert_eq!(glyph_factor(birth, 11.0, head), 0.0);
}
}