easing_fixed/
lib.rs

1// Copyright 2016 John Ward under MIT
2#![no_std]
3use fixed::types::I16F16;
4use fixed_exp2::FixedPowF;
5use fixed_trigonometry::sin;
6pub type Fix = I16F16;
7
8macro_rules! easer {
9    ($f:ident, $t:ident, $e:expr) => {
10        pub struct $t {
11            start: Fix,
12            dist: Fix,
13            step: u64,
14            steps: u64,
15        }
16
17        pub fn $f(start: Fix, end: Fix, steps: u64) -> $t {
18            $t {
19                start,
20                dist: end - start,
21                step: 0,
22                steps,
23            }
24        }
25
26        impl $t {
27            pub fn at(x: Fix, start: Fix, dist: Fix) -> Fix {
28                Fix::from_num($e(x)).mul_add(dist, start)
29            }
30            pub fn at_normalized(x: Fix) -> Fix {
31                Self::at(x, Fix::from_num(0), Fix::from_num(1))
32            }
33        }
34
35        impl Iterator for $t {
36            type Item = Fix;
37
38            fn next(&mut self) -> Option<Fix> {
39                self.step += 1;
40                if self.step > self.steps {
41                    None
42                } else {
43                    let x: Fix = Fix::from_num(self.step) / Fix::from_num(self.steps);
44                    Some(Self::at(x, self.start, self.dist))
45                }
46            }
47        }
48    };
49}
50
51easer!(linear, Linear, |x: Fix| { x });
52easer!(quad_in, QuadIn, |x: Fix| { x * x });
53easer!(quad_out, QuadOut, |x: Fix| {
54    -(x * (x - Fix::from_num(2)))
55});
56easer!(quad_inout, QuadInOut, |x: Fix| -> Fix {
57    if x < Fix::from_num(0.5) {
58        Fix::from_num(2) * x * x
59    } else {
60        (Fix::from_num(-2) * x * x) + x.mul_add(Fix::from_num(4), Fix::from_num(-1))
61    }
62});
63easer!(cubic_in, CubicIn, |x: Fix| { x * x * x });
64easer!(cubic_out, CubicOut, |x: Fix| {
65    let y = x - Fix::from_num(1);
66    y * y * y + Fix::from_num(1)
67});
68easer!(cubic_inout, CubicInOut, |x: Fix| {
69    if x < Fix::from_num(0.5) {
70        Fix::from_num(4) * x * x * x
71    } else {
72        let y = x.mul_add(2.into(), Fix::from_num(-2));
73        (y * y * y).mul_add(Fix::from_num(0.5), Fix::from_num(1))
74    }
75});
76easer!(quartic_in, QuarticIn, |x: Fix| { x * x * x * x });
77easer!(quartic_out, QuarticOut, |x: Fix| {
78    let y = x - Fix::from_num(1);
79    (y * y * y).mul_add(Fix::from_num(1) - x, Fix::from_num(1))
80});
81easer!(quartic_inout, QuarticInOut, |x: Fix| {
82    if x < Fix::from_num(0.5) {
83        Fix::from_num(8) * x * x * x * x
84    } else {
85        let y = x - Fix::from_num(1);
86        (y * y * y * y).mul_add(Fix::from_num(-8), Fix::from_num(1))
87    }
88});
89easer!(sin_in, SinIn, |x: Fix| {
90    let y = (x - Fix::from_num(1)) * Fix::FRAC_PI_2;
91    sin(y) + Fix::from_num(1)
92});
93easer!(sin_out, SinOut, |x: Fix| {
94    sin(x * Fix::FRAC_PI_2)
95});
96easer!(sin_inout, SinInOut, |x: Fix| {
97    if x < Fix::from_num(0.5) {
98        Fix::from_num(0.5)
99            * (Fix::from_num(1) - (x * x).mul_add(Fix::from_num(-4), Fix::from_num(1)).sqrt())
100    } else {
101        Fix::from_num(0.5)
102            * ((x.mul_add(Fix::from_num(-2), Fix::from_num(3))
103                * x.mul_add(Fix::from_num(2), Fix::from_num(-1)))
104            .sqrt()
105                + Fix::from_num(1))
106    }
107});
108easer!(exp_in, ExpIn, |x: Fix| {
109    if x == 0. {
110        Fix::from_num(0)
111    } else {
112        Fix::from_num(2).powf(Fix::from_num(10) * (x - Fix::from_num(1)))
113    }
114});
115
116easer!(exp_out, ExpOut, |x: Fix| {
117    if x == Fix::from_num(1) {
118        Fix::from_num(1)
119    } else {
120        Fix::from_num(2).powf(-Fix::from_num(10) * x) * Fix::from_num(-1) + Fix::from_num(1)
121    }
122});
123easer!(exp_inout, ExpInOut, |x: Fix| {
124    if x == Fix::from_num(1) {
125        Fix::from_num(1)
126    } else if x == 0. {
127        Fix::from_num(0)
128    } else if x < Fix::from_num(0.5) {
129        Fix::from_num(2).powf(x.mul_add(Fix::from_num(20), Fix::from_num(-10))) * Fix::from_num(0.5)
130    } else {
131        Fix::from_num(2)
132            .powf(x.mul_add(Fix::from_num(-20), Fix::from_num(10)))
133            .mul_add(Fix::from_num(-0.5), Fix::from_num(1))
134    }
135});
136easer!(smoothstep, SmoothStep, |x: Fix| {
137    if x == Fix::from_num(1) {
138        Fix::from_num(1)
139    } else if x == 0. {
140        Fix::from_num(0)
141    } else {
142        x * x * (Fix::from_num(3) - Fix::from_num(2) * x)
143    }
144});
145
146#[cfg(test)]
147mod test {
148    // acceptable relative error (0.015%)
149    const ERROR_MARGIN_FAC: f64 = 0.00015;
150    extern crate std;
151
152    use std::{fs::File, iter::zip, path::PathBuf, vec, vec::Vec, eprintln, format};
153
154    use anyhow::anyhow;
155
156    use super::*;
157    macro_rules! function {
158        () => {{
159            fn f() {}
160            fn type_name_of<T>(_: T) -> &'static str {
161                std::any::type_name::<T>()
162            }
163            let name = type_name_of(f);
164            let full_path = name.strip_suffix("::f").unwrap();
165            full_path.split("::").last().unwrap()
166        }};
167    }
168
169    /// accept a Vec of `is_data` if it is within the defined error margin of `ought_data`
170    fn must_be_withing_error_margin_else_write(
171        test_name: &str,
172        ought_data: Vec<f64>,
173        is_data: Vec<Fix>,
174    ) -> anyhow::Result<()> {
175        let min = ought_data
176            .iter()
177            .min_by(|a, b| a.partial_cmp(b).unwrap())
178            .unwrap();
179        let max = ought_data
180            .iter()
181            .max_by(|a, b| a.partial_cmp(b).unwrap())
182            .unwrap();
183        let value_range = max - min;
184
185        let max_error = (value_range * ERROR_MARGIN_FAC).abs();
186        let inside_margin = |is: f64, ought: f64| -> bool {
187            let delta = (is - ought).abs();
188
189            let res = delta <= max_error;
190            if !res {
191                eprintln!("measured error outside of acceptable margin: {is} <> {ought}");
192            }
193            res
194        };
195
196        let mut ok = true;
197        for (ought, is) in zip(ought_data.iter(), is_data.iter()) {
198            let is = is.to_num::<f64>();
199            if !inside_margin(is, *ought) {
200                ok = false;
201            }
202        }
203
204        if !ok {
205            let root = option_env!("CARGO_MANIFEST_DIR")
206                .ok_or(anyhow!("missing env var CARGO_MANIFEST_DIR"))?;
207            let root = PathBuf::from(root);
208            let target_path = root.join("jupyter-tests");
209
210            let ought_file = File::create(target_path.join(format!("{test_name}-ought.json")))?;
211            let is_file = File::create(target_path.join(format!("{test_name}-is.json")))?;
212
213            let is_converted = is_data
214                .into_iter()
215                .map(|fix| fix.to_num())
216                .collect::<Vec<f64>>();
217            serde_json::to_writer(ought_file, &ought_data)?;
218            serde_json::to_writer(is_file, &is_converted)?;
219
220            panic!("{test_name} outside of error {ERROR_MARGIN_FAC} margin: {is_converted:?} <> {ought_data:?}");
221        }
222        Ok(())
223    }
224
225    #[test]
226    fn at() {
227        let res = ExpInOut::at(Fix::from_num(0.5), Fix::from_num(10.), Fix::from_num(100));
228        assert_eq!(res, Fix::from_num(60.));
229    }
230
231    #[test]
232    fn at_normalized() {
233        let res = ExpInOut::at_normalized(Fix::from_num(0.75));
234        assert_eq!(res, Fix::from_num(0.984375));
235    }
236
237    #[test]
238    fn linear_test() -> anyhow::Result<()> {
239        let model = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
240        let res: Vec<Fix> = linear(Fix::from_num(0), Fix::from_num(1), 10).collect();
241        must_be_withing_error_margin_else_write(function!(), model, res)
242    }
243
244    #[test]
245    fn quad_in_test() -> anyhow::Result<()> {
246        let model = vec![
247            100., 400., 900., 1600., 2500., 3600., 4900., 6400., 8100., 10000.,
248        ];
249        let res: Vec<Fix> = quad_in(Fix::from_num(0), Fix::from_num(10000), 10).collect();
250        must_be_withing_error_margin_else_write(function!(), model, res)
251    }
252
253    #[test]
254    fn quad_out_test() -> anyhow::Result<()> {
255        let model = vec![
256            1900., 3600., 5100., 6400., 7500., 8400., 9100., 9600., 9900., 10000.,
257        ];
258        let res: Vec<Fix> = quad_out(Fix::from_num(0), Fix::from_num(10000), 10).collect();
259        must_be_withing_error_margin_else_write(function!(), model, res)
260    }
261
262    #[test]
263    fn quad_inout_test() -> anyhow::Result<()> {
264        let model = vec![
265            200., 800., 1800., 3200., 5000., 6800., 8200., 9200., 9800., 10000.,
266        ];
267        let res: Vec<Fix> = quad_inout(Fix::from_num(0), Fix::from_num(10000), 10).collect();
268        must_be_withing_error_margin_else_write(function!(), model, res)
269    }
270
271    #[test]
272    fn cubic_in_test() -> anyhow::Result<()> {
273        let model = vec![
274            10., 80., 270., 640., 1250., 2160., 3430., 5120., 7290., 10000.,
275        ];
276        let res: Vec<Fix> = cubic_in(Fix::from_num(0), Fix::from_num(10000), 10).collect();
277        must_be_withing_error_margin_else_write(function!(), model, res)
278    }
279
280    #[test]
281    fn cubic_out_test() -> anyhow::Result<()> {
282        let model = vec![
283            2710., 4880., 6570., 7840., 8750., 9360., 9730., 9920., 9990., 10000.,
284        ];
285        let res: Vec<Fix> = cubic_out(Fix::from_num(0), Fix::from_num(10000), 10).collect();
286        must_be_withing_error_margin_else_write(function!(), model, res)
287    }
288
289    #[test]
290    fn quartic_in_test() -> anyhow::Result<()> {
291        let model = vec![1., 16., 81., 256., 625., 1296., 2401., 4096., 6561., 10000.];
292        let res: Vec<Fix> = quartic_in(Fix::from_num(0), Fix::from_num(10000), 10).collect();
293        must_be_withing_error_margin_else_write(function!(), model, res)
294    }
295
296    #[test]
297    fn quartic_out_test() -> anyhow::Result<()> {
298        let model = vec![
299            3439., 5904., 7599., 8704., 9375., 9744., 9919., 9984., 9999., 10000.,
300        ];
301        let res: Vec<Fix> = quartic_out(Fix::from_num(0), Fix::from_num(10000), 10).collect();
302        must_be_withing_error_margin_else_write(function!(), model, res)
303    }
304
305    #[test]
306    fn quartic_inout_test() -> anyhow::Result<()> {
307        let model = vec![
308            8., 128., 648., 2048., 5000., 7952., 9352., 9872., 9992., 10000.,
309        ];
310        let res: Vec<Fix> = quartic_inout(Fix::from_num(0), Fix::from_num(10000), 10).collect();
311        must_be_withing_error_margin_else_write(function!(), model, res)
312    }
313
314    #[test]
315    fn sin_in_test() -> anyhow::Result<()> {
316        let model = vec![
317            123.116594,
318            489.434837,
319            1089.934758,
320            1909.830056,
321            2928.932188,
322            4122.147477,
323            5460.095003,
324            6909.830056,
325            8435.655350,
326            10000.,
327        ];
328        let res: Vec<Fix> = sin_in(Fix::from_num(0), Fix::from_num(10000), 10).collect();
329        must_be_withing_error_margin_else_write(function!(), model, res)
330    }
331
332    #[test]
333    fn sin_out_test() -> anyhow::Result<()> {
334        let model = vec![
335            1564.344650,
336            3090.169944,
337            4539.904997,
338            5877.852523,
339            7071.067812,
340            8090.169944,
341            8910.065242,
342            9510.565163,
343            9876.883406,
344            10000.,
345        ];
346        let res: Vec<Fix> = sin_out(Fix::from_num(0), Fix::from_num(10000), 10).collect();
347        must_be_withing_error_margin_else_write(function!(), model, res)
348    }
349
350    #[test]
351    fn sin_inout_test() -> anyhow::Result<()> {
352        let model = vec![
353            101.020514,
354            417.424305,
355            1000.,
356            2000.,
357            5000.,
358            8000.,
359            9000.,
360            9582.575695,
361            9898.979486,
362            10000.,
363        ];
364        let res: Vec<Fix> = sin_inout(Fix::from_num(0), Fix::from_num(10000), 10).collect();
365        must_be_withing_error_margin_else_write(function!(), model, res)
366    }
367
368    #[test]
369    fn exp_in_test() -> anyhow::Result<()> {
370        let model = vec![
371            19.53125, 39.0625, 78.125, 156.25, 312.5, 625., 1250., 2500., 5000., 10000.,
372        ];
373        let res: Vec<Fix> = exp_in(Fix::from_num(0), Fix::from_num(10000), 10).collect();
374        must_be_withing_error_margin_else_write(function!(), model, res)
375    }
376
377    #[test]
378    fn exp_out_test() -> anyhow::Result<()> {
379        let model = vec![
380            5000., 7500., 8750., 9375., 9687.5, 9843.75, 9921.875, 9960.9375, 9980.46875, 10000.,
381        ];
382        let res: Vec<Fix> = exp_out(Fix::from_num(0), Fix::from_num(10000), 10).collect();
383        must_be_withing_error_margin_else_write(function!(), model, res)
384    }
385
386    #[test]
387    fn exp_inout_test() -> anyhow::Result<()> {
388        let model = vec![
389            19.53125, 78.125, 312.5, 1250., 5000., 8750., 9687.5, 9921.875, 9980.46875, 10000.,
390        ];
391        let res: Vec<Fix> = exp_inout(Fix::from_num(0), Fix::from_num(10000), 10).collect();
392        must_be_withing_error_margin_else_write(function!(), model, res)
393    }
394
395    #[test]
396    fn smoothstep_test() -> anyhow::Result<()> {
397        let model = vec![
398            280.00000000000006,
399            1040.0000000000002,
400            2160.0,
401            3520.000000000001,
402            5000.0,
403            6480.0,
404            7839.999999999999,
405            8960.000000000002,
406            9720.0,
407            10000.,
408        ];
409        let res: Vec<Fix> = smoothstep(Fix::from_num(0), Fix::from_num(10000), 10).collect();
410        must_be_withing_error_margin_else_write(function!(), model, res)
411    }
412}