1#![forbid(unsafe_code)]
2#[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}