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 BottomUpLettersConfig {
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 BottomUpLettersConfig {
fn default() -> Self {
Self {
text: "Bottom up 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,
}
}
}
pub fn config_channels(
frame: f32,
index: usize,
config: &BottomUpLettersConfig,
unit: f32,
direction: f32,
) -> (f32, f32) {
letter_channels(
frame,
index,
config.stagger,
config.char_duration_frames,
config.char_travel_frames,
config.distance,
config.reduced_motion,
config.speed,
unit,
direction,
)
}
#[derive(Debug, Clone, Default)]
pub struct BottomUpLetters {
pub config: BottomUpLettersConfig,
}
impl BottomUpLetters {
pub fn new(config: BottomUpLettersConfig) -> Self {
Self { config }
}
pub fn meta_static() -> ComponentMeta {
ComponentMeta {
id: "bottom-up-letters",
name: "Bottom Up Letters",
summary: "Letters slide up into place on a wide staircase stagger.",
feature: "bottom-up-letters",
}
}
}
impl Component for BottomUpLetters {
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) = config_channels(frame, index, config, 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 letters_wait_above_or_below_until_their_turn() {
let config = BottomUpLettersConfig::default();
let (opacity, y) = config_channels(0.0, 0, &config, 1.0, 1.0);
assert!(opacity.abs() < 1e-6);
assert!((y - config.distance).abs() < 1e-6);
let (opacity, y) = config_channels(0.0, 0, &config, 1.0, -1.0);
assert!((y + config.distance).abs() < 1e-6);
assert!(opacity.abs() < 1e-6);
let (opacity, y) = config_channels(200.0, 0, &config, 1.0, 1.0);
assert!((opacity - 1.0).abs() < 1e-6);
assert!(y.abs() < 1e-6);
}
#[test]
fn stagger_holds_later_letters_back() {
let config = BottomUpLettersConfig::default();
let (opacity, _) = config_channels(2.0, 1, &config, 1.0, 1.0);
assert!(opacity.abs() < 1e-6);
}
#[test]
fn duration_matches_the_documented_formula() {
let component = BottomUpLetters::default();
assert_eq!(component.config.text.chars().count(), 18);
assert_eq!(component.duration(), 63);
}
#[test]
fn reduced_motion_drops_the_travel() {
let config = BottomUpLettersConfig {
reduced_motion: true,
..BottomUpLettersConfig::default()
};
let (_, y) = config_channels(5.0, 0, &config, 1.5, 1.0);
assert!(y.abs() < 1e-6);
}
}