mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
use core::time::Duration;

use crate::animation::Timed;
use crate::mesh::{Animation, Posing};

/// The weight a fade has run out at.
const WHOLE: f32 = 1.0;

/// What a draw is posed by: what the machine it was drawn under plays, the
/// fade out of the state that machine left, and the instant a hold of that
/// machine stands at.
///
/// The clips of the mesh drawn resolve it to a [`Posing`] at the instant
/// the frame reads it, so no clip length of its own reaches the draw.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Running {
    pub(crate) posed: Posed,
    pub(crate) fading: Option<Fading>,
    /// The instant the machine was held at, and nothing at all while its
    /// clock runs.
    pub(crate) held: Option<Duration>,
}

impl Running {
    /// The pose the draw holds at `now`, over `clips`, which a held
    /// machine reads at the instant it was held at instead.
    pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Posing {
        let at = self.held.unwrap_or(now);
        // A pose at fixed times is that pose, and a machine's own is what
        // it plays over what it fades out of.
        let (Posed::Playing(motion), Some(fading)) = (self.posed, self.fading) else {
            return self.posed.posing(at, clips);
        };

        motion.blended_over(fading.under.posing(at, clips), fading.weight(at), at, clips)
    }

    /// A draw posed at the fixed times `posing` holds, which no clock
    /// moves.
    #[cfg(all(test, feature = "offscreen"))]
    pub(crate) fn stopped(posing: Posing) -> Self {
        Self {
            posed: Posed::Stopped(posing),
            fading: None,
            held: None,
        }
    }
}

/// What a pose is read from: a motion on the machine's own clock, or a
/// pose stopped at fixed times.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Posed {
    Playing(Timed),
    Stopped(Posing),
}

impl Posed {
    /// The pose it holds at `now`, over `clips`.
    fn posing(self, now: Duration, clips: &[Animation]) -> Posing {
        match self {
            Self::Playing(motion) => motion.posing(now, clips),
            Self::Stopped(posing) => posing,
        }
    }

    /// The same, started `span` later, which a stopped pose is untouched
    /// by.
    pub(crate) fn shifted(self, span: Duration) -> Self {
        match self {
            Self::Playing(motion) => Self::Playing(motion.shifted(span)),
            stopped @ Self::Stopped(_) => stopped,
        }
    }
}

/// The fade out of the state a machine left: what lies under what it
/// entered, the instant the fade started and how long it takes.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Fading {
    pub(crate) under: Posed,
    pub(crate) from: Duration,
    pub(crate) over: Duration,
}

impl Fading {
    /// How far of the way from what lies under it to the motion that
    /// entered the pose lies at `now`, a fraction in `0.0..=1.0`.
    pub(crate) fn weight(&self, now: Duration) -> f32 {
        now.saturating_sub(self.from)
            .div_duration_f32(self.over)
            .clamp(0.0, WHOLE)
    }

    /// The same, with every instant it holds `span` later.
    pub(crate) fn shifted(self, span: Duration) -> Self {
        Self {
            under: self.under.shifted(span),
            from: self.from.checked_add(span).unwrap_or(self.from),
            ..self
        }
    }
}