gled-model 2.30.0

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
pub mod multiplied_curve;
pub mod point;
pub mod polynomial;
pub mod pos;

use self::point::CurvePoint;
use super::AssetTrait;
use polynomial::BezierCurve;
use pos::CurvePos;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Curve {
    /// First and last point move together, so the curve loops seamlessly.
    pub linked: bool,
    pub points: Vec<CurvePoint>,
}
impl Eq for Curve {}

impl Default for Curve {
    fn default() -> Self {
        Self::alternating()
    }
}

impl Curve {
    pub(crate) fn alternating() -> Self {
        let points = Self::linear_points(vec![
            (0.0, 1.0),
            (1.0, 0.0),
            (2.0, 1.0),
            (3.0, 0.0),
            (4.0, 1.0),
        ]);
        Self {
            linked: true,
            points,
        }
    }

    pub fn invert_y_axis(&mut self) {
        self.points
            .iter_mut()
            .for_each(|point| point.invert_y_axis());
    }

    pub fn invert_x_axis(&mut self) {
        self.points
            .iter_mut()
            .for_each(|point| point.invert_x_axis());
    }

    fn linear_points(points: Vec<(f32, f32)>) -> Vec<CurvePoint> {
        let mut linear_points = Vec::with_capacity(2 * points.len() - 1);
        let mut prev = None;
        for (i, (x, y)) in points.iter().enumerate() {
            if let Some((prev_x, prev_y)) = prev {
                linear_points.push(CurvePoint::Bezier(CurvePos::new(
                    (prev_x + x) / 2.0,
                    (prev_y + y) / 2.0,
                )));
            }

            let pos = CurvePos::new(*x, *y);
            linear_points.push(if i == 0 {
                CurvePoint::First(pos)
            } else if i == points.len() - 1 {
                CurvePoint::Last(pos)
            } else {
                CurvePoint::Inner(pos)
            });

            prev = Some((x, y));
        }
        linear_points
    }

    pub fn value(&self, beat_position: f32) -> f32 {
        assert!(
            (0.0..=4.0).contains(&beat_position),
            "Beat position out of range 0.0..=4.0: {beat_position}"
        );

        let value = self
            .points
            .iter()
            .position(|point| point.pos().x > beat_position)
            .and_then(|i| {
                self.points.get(i).and_then(|point| {
                    if point.is_bezier() {
                        self.points.get(i - 1).and_then(|start| {
                            self.points
                                .get(i + 1)
                                .map(|end| BezierCurve::new(start.pos(), point.pos(), end.pos()))
                        })
                    } else {
                        self.points.get(i - 1).and_then(|bezier| {
                            self.points.get(i - 2).map(|start| {
                                BezierCurve::new(start.pos(), bezier.pos(), point.pos())
                            })
                        })
                    }
                })
            })
            .map(|curve| curve.value(beat_position))
            .unwrap_or_default();

        // invert the value as the curve is upside down
        1.0 - value
    }
}

impl AssetTrait for Curve {
    const DIR_NAME: &'static str = "curves";
    const NAME: &'static str = "Curve";
    const SHOW_NAME_IF_SELECTED: bool = false;
}