Skip to main content

use_modular_scale/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive modular typography scale helpers.
3//!
4//! These helpers keep common scale ratios explicit and deterministic.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_modular_scale::{ScaleRatio, modular_scale, scale_down, scale_up};
10//!
11//! let values = modular_scale(16.0, ScaleRatio::MajorThird, -1, 1).unwrap();
12//!
13//! assert!((scale_up(16.0, ScaleRatio::MajorThird, 1).unwrap() - 20.0).abs() < 1.0e-12);
14//! assert!((scale_down(16.0, ScaleRatio::MajorThird, 1).unwrap() - 12.8).abs() < 1.0e-12);
15//! assert_eq!(values.len(), 3);
16//! ```
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum ScaleRatio {
20    MinorSecond,
21    MajorSecond,
22    MinorThird,
23    MajorThird,
24    PerfectFourth,
25    AugmentedFourth,
26    PerfectFifth,
27    GoldenRatio,
28    Custom(f64),
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ModularScaleError {
33    InvalidBaseSize,
34    InvalidRatio,
35    InvalidStepRange,
36}
37
38fn validate_positive_base(base_px: f64) -> Result<f64, ModularScaleError> {
39    if !base_px.is_finite() || base_px <= 0.0 {
40        Err(ModularScaleError::InvalidBaseSize)
41    } else {
42        Ok(base_px)
43    }
44}
45
46fn validate_ratio(value: f64) -> Result<f64, ModularScaleError> {
47    if !value.is_finite() || value <= 1.0 {
48        Err(ModularScaleError::InvalidRatio)
49    } else {
50        Ok(value)
51    }
52}
53
54fn scale_for_step(base_px: f64, ratio: f64, step: isize) -> f64 {
55    base_px * ratio.powf(step as f64)
56}
57
58impl ScaleRatio {
59    pub fn value(&self) -> Result<f64, ModularScaleError> {
60        match self {
61            Self::MinorSecond => Ok(1.067),
62            Self::MajorSecond => Ok(1.125),
63            Self::MinorThird => Ok(1.2),
64            Self::MajorThird => Ok(1.25),
65            Self::PerfectFourth => Ok(1.333),
66            Self::AugmentedFourth => Ok(1.414),
67            Self::PerfectFifth => Ok(1.5),
68            Self::GoldenRatio => Ok(1.618_033_988_75),
69            Self::Custom(value) => validate_ratio(*value),
70        }
71    }
72}
73
74pub fn scale_up(base_px: f64, ratio: ScaleRatio, steps: usize) -> Result<f64, ModularScaleError> {
75    let base_px = validate_positive_base(base_px)?;
76    let ratio = validate_ratio(ratio.value()?)?;
77    Ok(scale_for_step(base_px, ratio, steps as isize))
78}
79
80pub fn scale_down(base_px: f64, ratio: ScaleRatio, steps: usize) -> Result<f64, ModularScaleError> {
81    let base_px = validate_positive_base(base_px)?;
82    let ratio = validate_ratio(ratio.value()?)?;
83    Ok(scale_for_step(base_px, ratio, -(steps as isize)))
84}
85
86pub fn modular_scale(
87    base_px: f64,
88    ratio: ScaleRatio,
89    min_step: isize,
90    max_step: isize,
91) -> Result<Vec<f64>, ModularScaleError> {
92    if max_step < min_step {
93        return Err(ModularScaleError::InvalidStepRange);
94    }
95
96    let base_px = validate_positive_base(base_px)?;
97    let ratio = validate_ratio(ratio.value()?)?;
98
99    Ok((min_step..=max_step)
100        .map(|step| scale_for_step(base_px, ratio, step))
101        .collect())
102}
103
104#[cfg(test)]
105mod tests {
106    use super::{modular_scale, scale_down, scale_up, ModularScaleError, ScaleRatio};
107
108    #[test]
109    fn generates_modular_scale_values() {
110        let values = modular_scale(16.0, ScaleRatio::MajorThird, -1, 1).unwrap();
111
112        assert_eq!(values.len(), 3);
113        assert!((values[0] - 12.8).abs() < 1.0e-12);
114        assert!((values[1] - 16.0).abs() < 1.0e-12);
115        assert!((values[2] - 20.0).abs() < 1.0e-12);
116        assert!((scale_up(16.0, ScaleRatio::MajorThird, 2).unwrap() - 25.0).abs() < 1.0e-12);
117        assert!((scale_down(16.0, ScaleRatio::MajorThird, 1).unwrap() - 12.8).abs() < 1.0e-12);
118    }
119
120    #[test]
121    fn rejects_invalid_scale_inputs() {
122        assert_eq!(
123            scale_up(0.0, ScaleRatio::MajorThird, 1),
124            Err(ModularScaleError::InvalidBaseSize)
125        );
126        assert_eq!(
127            scale_down(16.0, ScaleRatio::Custom(1.0), 1),
128            Err(ModularScaleError::InvalidRatio)
129        );
130        assert_eq!(
131            modular_scale(16.0, ScaleRatio::MajorThird, 2, 1),
132            Err(ModularScaleError::InvalidStepRange)
133        );
134    }
135}