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);
}
}