use fframes::Svgr;
use crate::core::{Component, ComponentMeta};
use crate::ctx::{RenderCtx, char_centers, text_node_centered};
use crate::motion::{REFERENCE_VMIN, entry_frames, letter_channels};
#[derive(Debug, Clone, PartialEq)]
pub struct TopDownLettersConfig {
pub text: String,
pub stagger: usize,
pub distance: f32,
pub font_size: f32,
pub color: String,
pub font_weight: u16,
pub char_duration_frames: usize,
pub char_travel_frames: usize,
pub speed: f32,
pub tail: usize,
pub tracking_em: f32,
pub reduced_motion: bool,
}
impl Default for TopDownLettersConfig {
fn default() -> Self {
Self {
text: "Top down letters.".to_owned(),
stagger: 3,
distance: 46.0,
font_size: 72.0,
color: "#e6edf3".to_owned(),
font_weight: 600,
char_duration_frames: 12,
char_travel_frames: 7,
speed: 1.0,
tail: 0,
tracking_em: -0.05,
reduced_motion: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TopDownLetters {
pub config: TopDownLettersConfig,
}
impl TopDownLetters {
pub fn new(config: TopDownLettersConfig) -> Self {
Self { config }
}
pub fn meta_static() -> ComponentMeta {
ComponentMeta {
id: "top-down-letters",
name: "Top Down Letters",
summary: "Letters drop into place from above on a wide staircase stagger.",
feature: "top-down-letters",
}
}
}
impl Component for TopDownLetters {
fn meta(&self) -> ComponentMeta {
Self::meta_static()
}
fn duration(&self) -> usize {
entry_frames(
self.config.text.chars().count(),
self.config.char_duration_frames,
self.config.stagger,
self.config.speed,
) + self.config.tail
}
fn background(&self) -> Option<&'static str> {
Some("#0d1117")
}
fn show(&self, ctx: &mut RenderCtx<'_>) -> Svgr<'_> {
let config = &self.config;
let family = ctx.fonts.sans.clone();
let unit = ctx.vmin() / REFERENCE_VMIN;
let size = config.font_size * unit;
let (centres, _) = char_centers(ctx, &family, &config.text, size);
let center_x = ctx.width() / 2.0;
let baseline = ctx.height() / 2.0 + ctx.baseline_offset(&family, size, size * 1.2);
let frame = ctx.frame();
let mut nodes: Vec<Svgr<'_>> = Vec::new();
for (index, (character, dx)) in config.text.chars().zip(centres).enumerate() {
let (opacity, y) = letter_channels(
frame,
index,
config.stagger,
config.char_duration_frames,
config.char_travel_frames,
config.distance,
config.reduced_motion,
config.speed,
unit,
-1.0,
);
nodes.push(text_node_centered(
&character.to_string(),
center_x + dx,
baseline + y,
family.clone(),
size,
config.font_weight,
config.tracking_em,
config.color.clone(),
opacity,
));
}
let nodes = Svgr::from(nodes);
fframes::svgr!(<g>{nodes}</g>)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drops_from_above() {
let component = TopDownLetters::default();
let config = &component.config;
let (opacity, y) = letter_channels(
0.0,
0,
config.stagger,
config.char_duration_frames,
config.char_travel_frames,
config.distance,
config.reduced_motion,
config.speed,
1.0,
-1.0,
);
assert!(opacity.abs() < 1e-6);
assert!(
y < 0.0,
"expected the character above its resting place, got {y}"
);
}
#[test]
fn duration_matches_the_documented_formula() {
let component = TopDownLetters::default();
assert_eq!(component.config.text.chars().count(), 17);
assert_eq!(component.duration(), 60);
assert_eq!(component.meta().id, "top-down-letters");
}
}