cranpose_liquid/motion.rs
1//! Liquid motion: the spring presets every component shares, and the press
2//! interaction (scale + specular boost) that makes glass feel physical.
3
4use cranpose_animation::{spring, AnimationType};
5use cranpose_core::State;
6use cranpose_macros::composable;
7use cranpose_ui::Modifier;
8use cranpose_ui::MutableInteractionSource;
9use cranpose_ui_graphics::GraphicsLayer;
10
11/// Named springs used across the Liquid components (value-space, velocity
12/// preserving).
13pub struct LiquidMotion;
14
15impl LiquidMotion {
16 /// Snappy interactions: presses, toggles, selection moves.
17 pub fn snappy() -> AnimationType {
18 spring(0.85, 900.0)
19 }
20
21 /// The droplet feel: visible overshoot for morphing shapes.
22 pub fn bouncy() -> AnimationType {
23 spring(0.55, 500.0)
24 }
25
26 /// Gentle settle for large surfaces (sheets, menus).
27 pub fn smooth() -> AnimationType {
28 spring(1.0, 400.0)
29 }
30
31 /// The leading edge of a stretching selection blob (runs ahead).
32 pub fn blob_leading() -> AnimationType {
33 spring(0.8, 900.0)
34 }
35
36 /// The trailing edge of a stretching selection blob (drags behind, giving
37 /// the droplet elongation while in motion).
38 pub fn blob_trailing() -> AnimationType {
39 spring(0.9, 380.0)
40 }
41}
42
43/// Press feedback for glass controls, per the Liquid Glass law: touched glass
44/// GROWS (spring scale toward `pressed_scale` — never smaller) and turns MORE
45/// TRANSPARENT (the returned content alpha dips while pressed, the reference
46/// "…" dots fading as the button lifts). Returns the pressed state so callers
47/// can also boost the specular highlight.
48///
49/// Apply the returned modifier *outside* the glass effect so the whole lens
50/// scales together; apply the content alpha to the label/icon layer.
51#[composable]
52pub fn liquid_press_scale(
53 modifier: Modifier,
54 interaction_source: MutableInteractionSource,
55 pressed_scale: f32,
56) -> (Modifier, State<bool>, State<f32>) {
57 let pressed = interaction_source.collectIsPressedAsState();
58 let scale = cranpose_animation::animateFloatAsState(
59 if pressed.get() {
60 pressed_scale.max(1.0)
61 } else {
62 1.0
63 },
64 LiquidMotion::snappy(),
65 "liquid-press-scale",
66 );
67 let content_alpha = cranpose_animation::animateFloatAsState(
68 // The reference down-state ghosts glyphs hard (the menu button's
69 // dots drop to ~30% while held).
70 if pressed.get() { 0.35 } else { 1.0 },
71 LiquidMotion::smooth(),
72 "liquid-press-content",
73 );
74 let modifier = modifier.graphics_layer(move || {
75 let scale = scale.get();
76 GraphicsLayer {
77 scale_x: scale,
78 scale_y: scale,
79 ..Default::default()
80 }
81 });
82 (modifier, pressed, content_alpha)
83}