pub use fenestra_anim::CubicBezier;
pub const SP0: f32 = 0.0;
pub const SP0_5: f32 = 2.0;
pub const SP1: f32 = 4.0;
pub const SP2: f32 = 8.0;
pub const SP3: f32 = 12.0;
pub const SP4: f32 = 16.0;
pub const SP5: f32 = 20.0;
pub const SP6: f32 = 24.0;
pub const SP8: f32 = 32.0;
pub const SP10: f32 = 40.0;
pub const SP12: f32 = 48.0;
pub const SP16: f32 = 64.0;
pub const MEASURE_CH: f32 = 52.0;
pub const R_SM: f32 = 6.0;
pub const R_MD: f32 = 10.0;
pub const R_LG: f32 = 14.0;
pub const R_XL: f32 = 20.0;
pub const R_FULL: f32 = f32::INFINITY;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RadiusScale {
pub sm: f32,
pub md: f32,
pub lg: f32,
pub xl: f32,
}
impl RadiusScale {
#[must_use]
pub fn from_base(base: f32) -> Self {
let b = base.max(0.0);
Self {
sm: b * 0.6,
md: b,
lg: b * 1.4,
xl: b * 2.0,
}
}
#[must_use]
pub fn sharp() -> Self {
Self {
sm: 1.0,
md: 2.0,
lg: 3.0,
xl: 4.0,
}
}
#[must_use]
pub fn soft() -> Self {
Self::from_base(16.0)
}
}
impl Default for RadiusScale {
fn default() -> Self {
Self::from_base(R_MD)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Elevation {
#[default]
Shadowed,
Flat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextSize {
Xs,
Sm,
#[default]
Base,
Lg,
Xl,
Xl2,
Xl3,
}
impl TextSize {
pub const fn px(self) -> f32 {
match self {
Self::Xs => 12.0,
Self::Sm => 14.0,
Self::Base => 16.0,
Self::Lg => 20.0,
Self::Xl => 25.0,
Self::Xl2 => 31.0,
Self::Xl3 => 39.0,
}
}
pub const fn line_height(self) -> f32 {
match self {
Self::Xs | Self::Sm | Self::Base => 1.5,
Self::Lg => 1.4,
Self::Xl | Self::Xl2 => 1.25,
Self::Xl3 => 1.15,
}
}
pub fn letter_spacing(self) -> f32 {
tracking_em(self.px())
}
}
#[must_use]
pub fn tracking_em(px: f32) -> f32 {
-0.0223 + 0.185 * (-0.1745 * px).exp()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Weight {
#[default]
Regular,
Medium,
Semibold,
}
impl Weight {
pub const fn value(self) -> f32 {
match self {
Self::Regular => 400.0,
Self::Medium => 500.0,
Self::Semibold => 600.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FamilyRole {
#[default]
Sans,
Mono,
Display,
Serif,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ShadowToken {
Xs,
Sm,
Md,
Lg,
Xl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MotionDuration {
Micro,
Fast,
#[default]
Base,
Slow,
}
impl MotionDuration {
pub const fn ms(self) -> f32 {
match self {
Self::Micro => 100.0,
Self::Fast => 120.0,
Self::Base => 200.0,
Self::Slow => 300.0,
}
}
pub const fn exit_ms(self) -> f32 {
self.ms() * 0.75
}
}
pub const EASE_STANDARD: CubicBezier = CubicBezier {
x1: 0.2,
y1: 0.0,
x2: 0.0,
y2: 1.0,
};
pub const EASE_DECELERATE: CubicBezier = CubicBezier {
x1: 0.0,
y1: 0.0,
x2: 0.2,
y2: 1.0,
};
pub const EASE_ACCELERATE: CubicBezier = CubicBezier {
x1: 0.4,
y1: 0.0,
x2: 1.0,
y2: 1.0,
};
pub const EASE_EXIT: CubicBezier = EASE_ACCELERATE;
pub const EASE_IN_OUT_CUBIC: CubicBezier = CubicBezier {
x1: 0.65,
y1: 0.0,
x2: 0.35,
y2: 1.0,
};
#[derive(Debug, Clone, Copy)]
pub struct FocusRing {
pub width: f32,
pub offset: f32,
pub alpha: f32,
}
pub const FOCUS_RING: FocusRing = FocusRing {
width: 3.0,
offset: 0.0,
alpha: 0.5,
};
pub const PRESS_SCALE: f32 = 0.97;
pub const GLASS_LIGHT_DEG: f32 = 0.0;
pub const GRADIENT_STEPS: usize = 16;
#[derive(Debug, Clone, Copy)]
pub struct StateLayer {
pub hover: f32,
pub focus: f32,
pub press: f32,
pub drag: f32,
pub disabled_container: f32,
pub disabled_content: f32,
}
pub const STATE_LAYER: StateLayer = StateLayer {
hover: 0.08,
focus: 0.12,
press: 0.12,
drag: 0.16,
disabled_container: 0.12,
disabled_content: 0.38,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tracking_curve_tightens_with_size_toward_its_asymptote() {
assert!(tracking_em(12.0) > tracking_em(16.0));
assert!(tracking_em(16.0) > tracking_em(31.0));
assert!((tracking_em(12.0) - 0.0005).abs() < 0.002);
assert!((tracking_em(16.0) - -0.0110).abs() < 0.002);
assert!(tracking_em(96.0) > -0.0223);
assert!((tracking_em(96.0) - -0.0223).abs() < 0.0005);
}
#[test]
fn text_size_tracking_matches_the_formula() {
for size in [TextSize::Xs, TextSize::Base, TextSize::Xl3] {
assert_eq!(size.letter_spacing(), tracking_em(size.px()));
}
}
#[test]
fn easing_families_have_the_right_curvature() {
assert!(EASE_DECELERATE.eval(0.25) > 0.25);
assert!(EASE_ACCELERATE.eval(0.25) < 0.25);
assert!(EASE_STANDARD.eval(0.25) > 0.25);
for e in [EASE_STANDARD, EASE_DECELERATE, EASE_ACCELERATE] {
assert_eq!(e.eval(0.0), 0.0);
assert_eq!(e.eval(1.0), 1.0);
}
}
#[test]
fn exit_is_a_quarter_quicker_than_entrance() {
for d in [
MotionDuration::Micro,
MotionDuration::Fast,
MotionDuration::Base,
MotionDuration::Slow,
] {
assert!((d.exit_ms() - d.ms() * 0.75).abs() < 1e-3);
}
assert_eq!(MotionDuration::Micro.ms(), 100.0);
}
#[test]
fn press_scale_is_a_subtle_shrink() {
assert!((0.96..=0.97).contains(&PRESS_SCALE));
}
#[test]
fn radius_scale_default_matches_the_constants() {
let r = RadiusScale::default();
assert_eq!(r.sm, R_SM);
assert_eq!(r.md, R_MD);
assert_eq!(r.lg, R_LG);
assert_eq!(r.xl, R_XL);
let tight = RadiusScale::from_base(5.0);
assert_eq!(tight.md, 5.0);
assert!(tight.sm < tight.md && tight.md < tight.lg && tight.lg < tight.xl);
}
}