rae 0.1.11

Renderer-neutral widget, layout, sanitization, and GLSL shader primitives for Rust desktop tools.
Documentation
use crate::{catmull_rom, clamp01, lerp, AnimationCurve, MotionSpec, Rgba, Vec2};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MaterialTokens {
    pub fill: Rgba,
    pub stroke: Rgba,
    pub glow: Rgba,
    pub shadow: Rgba,
    pub blur_px: f32,
    pub stroke_px: f32,
    pub glow_px: f32,
    pub elevation: f32,
}

impl MaterialTokens {
    pub fn glass(accent: Rgba) -> Self {
        Self {
            fill: Rgba::rgba8(12, 20, 42, 192),
            stroke: accent.with_alpha(0.42),
            glow: accent.with_alpha(0.58),
            shadow: Rgba::rgba8(0, 0, 0, 128),
            blur_px: 22.0,
            stroke_px: 1.4,
            glow_px: 36.0,
            elevation: 4.0,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ShapeAnchor {
    Center,
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
    EdgeLeft,
    EdgeRight,
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum MorphShape {
    RoundedCard,
    Capsule,
    Blob,
    Ticket,
    NotchedPanel,
    DockIsland,
    CommandLens,
}

impl MorphShape {
    pub fn default_topology(self) -> CardTopology {
        match self {
            Self::RoundedCard => CardTopology {
                corner_radius: 22.0,
                edge_wave: 0.0,
                notch_depth: 0.0,
                blob_strength: 0.0,
                aspect_pull: 0.0,
                anchor: ShapeAnchor::Center,
            },
            Self::Capsule => CardTopology {
                corner_radius: 999.0,
                edge_wave: 0.0,
                notch_depth: 0.0,
                blob_strength: 0.0,
                aspect_pull: 0.35,
                anchor: ShapeAnchor::Center,
            },
            Self::Blob => CardTopology {
                corner_radius: 52.0,
                edge_wave: 18.0,
                notch_depth: 0.0,
                blob_strength: 0.85,
                aspect_pull: 0.1,
                anchor: ShapeAnchor::Center,
            },
            Self::Ticket => CardTopology {
                corner_radius: 18.0,
                edge_wave: 2.0,
                notch_depth: 18.0,
                blob_strength: 0.0,
                aspect_pull: 0.0,
                anchor: ShapeAnchor::EdgeLeft,
            },
            Self::NotchedPanel => CardTopology {
                corner_radius: 26.0,
                edge_wave: 0.0,
                notch_depth: 26.0,
                blob_strength: 0.16,
                aspect_pull: 0.0,
                anchor: ShapeAnchor::TopRight,
            },
            Self::DockIsland => CardTopology {
                corner_radius: 38.0,
                edge_wave: 8.0,
                notch_depth: 0.0,
                blob_strength: 0.28,
                aspect_pull: 0.48,
                anchor: ShapeAnchor::BottomRight,
            },
            Self::CommandLens => CardTopology {
                corner_radius: 30.0,
                edge_wave: 4.0,
                notch_depth: 10.0,
                blob_strength: 0.08,
                aspect_pull: 0.18,
                anchor: ShapeAnchor::TopLeft,
            },
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CardTopology {
    pub corner_radius: f32,
    pub edge_wave: f32,
    pub notch_depth: f32,
    pub blob_strength: f32,
    pub aspect_pull: f32,
    pub anchor: ShapeAnchor,
}

impl CardTopology {
    pub fn mix(self, other: Self, amount: f32) -> Self {
        let amount = clamp01(amount);
        Self {
            corner_radius: lerp(self.corner_radius, other.corner_radius, amount),
            edge_wave: lerp(self.edge_wave, other.edge_wave, amount),
            notch_depth: lerp(self.notch_depth, other.notch_depth, amount),
            blob_strength: lerp(self.blob_strength, other.blob_strength, amount),
            aspect_pull: lerp(self.aspect_pull, other.aspect_pull, amount),
            anchor: if amount < 0.5 {
                self.anchor
            } else {
                other.anchor
            },
        }
    }

    pub fn uniform_pack(self) -> [f32; 4] {
        [
            self.corner_radius,
            self.edge_wave,
            self.notch_depth,
            self.blob_strength,
        ]
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MorphProfile {
    pub from: MorphShape,
    pub to: MorphShape,
    pub motion: MotionSpec,
    pub overshoot: f32,
}

impl MorphProfile {
    pub fn material_default(from: MorphShape, to: MorphShape) -> Self {
        Self {
            from,
            to,
            motion: MotionSpec::new(520, 0, AnimationCurve::Emphasized),
            overshoot: 0.08,
        }
    }

    pub fn sample(self, elapsed_ms: u32) -> MorphState {
        let progress = self.motion.progress(elapsed_ms);
        let eased = if self.overshoot > 0.0 {
            let overshoot = catmull_rom(0.0, 0.0, 1.0 + self.overshoot, 1.0, progress);
            overshoot.clamp(0.0, 1.0 + self.overshoot)
        } else {
            progress
        };
        MorphState {
            progress,
            eased_progress: eased,
            topology: self
                .from
                .default_topology()
                .mix(self.to.default_topology(), eased.min(1.0)),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MorphState {
    pub progress: f32,
    pub eased_progress: f32,
    pub topology: CardTopology,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MorphCacheKey {
    pub from: &'static str,
    pub to: &'static str,
    pub width_px: u16,
    pub height_px: u16,
    pub frame_bucket: u16,
}

impl MorphCacheKey {
    pub fn new(from: MorphShape, to: MorphShape, size: Vec2, progress: f32) -> Self {
        Self {
            from: shape_name(from),
            to: shape_name(to),
            width_px: size.x.max(0.0).round() as u16,
            height_px: size.y.max(0.0).round() as u16,
            frame_bucket: (clamp01(progress) * 240.0).round() as u16,
        }
    }
}

fn shape_name(shape: MorphShape) -> &'static str {
    match shape {
        MorphShape::RoundedCard => "rounded_card",
        MorphShape::Capsule => "capsule",
        MorphShape::Blob => "blob",
        MorphShape::Ticket => "ticket",
        MorphShape::NotchedPanel => "notched_panel",
        MorphShape::DockIsland => "dock_island",
        MorphShape::CommandLens => "command_lens",
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn morph_profile_samples_between_shapes() {
        let profile = MorphProfile::material_default(MorphShape::RoundedCard, MorphShape::Blob);
        let start = profile.sample(0).topology;
        let end = profile.sample(10_000).topology;

        assert!(end.blob_strength > start.blob_strength);
        assert!(end.edge_wave > start.edge_wave);
    }

    #[test]
    fn cache_key_quantizes_progress() {
        let a = MorphCacheKey::new(
            MorphShape::RoundedCard,
            MorphShape::Capsule,
            Vec2::new(100.2, 50.7),
            0.5,
        );
        let b = MorphCacheKey::new(
            MorphShape::RoundedCard,
            MorphShape::Capsule,
            Vec2::new(100.2, 50.7),
            0.501,
        );

        assert_eq!(a.width_px, 100);
        assert_eq!(a.height_px, 51);
        assert_eq!(a.frame_bucket, b.frame_bucket);
    }
}