tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-lottie/examples/state_machine_demo.rs Rust · 216 lines · 7.7 KB History Who touched what raw

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Demo interactiva del Tier 1+2: máquina de estados de animación (`llimphi-anim`)
2//! manejando dos clips Lottie con **crossfade**, manejada por un input vivo.
3//!
4//! Dos estados: `idle` (círculo azul que late) ⇄ `walk` (cuadrado naranja que
5//! gira), con un blend de 0.35 s entre ellos. El input booleano `moving` dispara
6//! las transiciones en ambos sentidos. **Espacio** o el botón lo togglean —
7//! ves el crossfade disolver una animación en la otra.
8//!
9//! Esto es el cableado canónico inputs↔app (Tier 2): el `Model` tiene una
10//! `Instance`, un `spawn_periodic` la avanza por frame (`advance(dt)`), los
11//! eventos setean inputs (`set_bool`), y `view` pinta `render_frame()` vía
12//! `state_machine_view`. El núcleo de estados no sabe de Lottie ni de vello.
13//!
14//! Corre con:
15//!   `cargo run -p llimphi-lottie --example state_machine_demo --release`
16
17use std::time::Duration;
18
19use llimphi_anim::{Condition, StateMachine};
20use llimphi_lottie::{state_machine_view, LottieAsset};
21use llimphi_ui::llimphi_layout::taffy::{
22    prelude::{length, percent, FlexDirection, Size, Style},
23    AlignItems, JustifyContent, Rect,
24};
25use llimphi_ui::llimphi_raster::peniko::Color;
26use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, NamedKey, View};
27
28// ClipId 0 = idle, 1 = walk.
29const IDLE: u32 = 0;
30const WALK: u32 = 1;
31
32/// idle: círculo azul cuya opacidad late (100→35→100) en 2 s.
33const IDLE_LOTTIE: &str = r#"{
34  "v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
35  "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
36    "ks":{"o":{"a":1,"k":[
37        {"i":{"x":[0.5],"y":[0.5]},"o":{"x":[0.5],"y":[0.5]},"t":0,"s":[100]},
38        {"i":{"x":[0.5],"y":[0.5]},"o":{"x":[0.5],"y":[0.5]},"t":30,"s":[35]},
39        {"t":60,"s":[100]}]},
40      "r":{"a":0,"k":0},"p":{"a":0,"k":[50,50]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]}},
41    "shapes":[{"ty":"gr","it":[
42      {"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[72,72]}},
43      {"ty":"fl","c":{"a":0,"k":[0.30,0.55,0.95]},"o":{"a":0,"k":100}},
44      {"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]}]}]}"#;
45
46/// walk: cuadrado naranja redondeado que gira 360° en 2 s.
47const WALK_LOTTIE: &str = r#"{
48  "v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
49  "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
50    "ks":{"o":{"a":0,"k":100},
51      "r":{"a":1,"k":[
52        {"i":{"x":[0.5],"y":[0.5]},"o":{"x":[0.5],"y":[0.5]},"t":0,"s":[0]},
53        {"t":60,"s":[360]}]},
54      "p":{"a":0,"k":[50,50]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]}},
55    "shapes":[{"ty":"gr","it":[
56      {"ty":"rc","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[60,60]},"r":{"a":0,"k":10}},
57      {"ty":"fl","c":{"a":0,"k":[0.95,0.55,0.15]},"o":{"a":0,"k":100}},
58      {"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]}]}]}"#;
59
60#[derive(Clone)]
61enum Msg {
62    /// Tick de animación: avanza la máquina por `dt`.
63    Tick,
64    /// Togglea el input `moving` (Espacio o el botón).
65    ToggleMoving,
66}
67
68struct Model {
69    inst: llimphi_anim::Instance,
70    clips: Vec<LottieAsset>,
71    moving: bool,
72}
73
74struct Demo;
75
76/// Período del tick → también el `dt` que le pasamos a `advance` (fijo, alcanza
77/// para una demo; una app real mediría el dt real).
78const TICK: Duration = Duration::from_millis(16);
79
80impl App for Demo {
81    type Model = Model;
82    type Msg = Msg;
83
84    fn title() -> &'static str {
85        "llimphi · state machine + crossfade"
86    }
87
88    fn initial_size() -> (u32, u32) {
89        (420, 520)
90    }
91
92    fn init(handle: &Handle<Self::Msg>) -> Self::Model {
93        let idle = LottieAsset::from_str(IDLE_LOTTIE).expect("idle lottie");
94        let walk = LottieAsset::from_str(WALK_LOTTIE).expect("walk lottie");
95
96        let mut sm = StateMachine::new();
97        let s_idle = sm.add_state("idle", IDLE, 1.0, true);
98        let s_walk = sm.add_state("walk", WALK, 1.0, true);
99        sm.set_entry(s_idle);
100        // El mismo input `moving` rige ambos sentidos, con blend de 0.35 s.
101        sm.transition(s_idle, s_walk, vec![Condition::bool("moving", true)], 0.35);
102        sm.transition(s_walk, s_idle, vec![Condition::bool("moving", false)], 0.35);
103
104        // Corre el reloj de animación (~60 fps).
105        handle.spawn_periodic(TICK, || Msg::Tick);
106
107        Model {
108            inst: sm.instance(),
109            clips: vec![idle, walk],
110            moving: false,
111        }
112    }
113
114    fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
115        match msg {
116            Msg::Tick => model.inst.advance(TICK.as_secs_f64()),
117            Msg::ToggleMoving => {
118                model.moving = !model.moving;
119                model.inst.set_bool("moving", model.moving);
120            }
121        }
122        model
123    }
124
125    fn on_key(_: &Self::Model, e: &KeyEvent) -> Option<Self::Msg> {
126        if e.state == KeyState::Pressed && e.key == Key::Named(NamedKey::Space) {
127            Some(Msg::ToggleMoving)
128        } else {
129            None
130        }
131    }
132
133    fn view(model: &Self::Model) -> View<Self::Msg> {
134        // Caja cuadrada que aloja la animación (state_machine_view es absolute-fill).
135        let stage = View::new(Style {
136            size: Size {
137                width: length(280.0_f32),
138                height: length(280.0_f32),
139            },
140            flex_shrink: 0.0,
141            align_items: Some(AlignItems::CENTER),
142            justify_content: Some(JustifyContent::CENTER),
143            ..Default::default()
144        })
145        .children(vec![state_machine_view::<Msg>(
146            model.inst.render_frame(),
147            model.clips.clone(),
148        )]);
149
150        let estado = if model.inst.is_transitioning() {
151            "· · · crossfade · · ·".to_string()
152        } else {
153            format!("estado: {}", model.inst.current_state())
154        };
155        let status = View::new(Style {
156            size: Size {
157                width: percent(1.0_f32),
158                height: length(28.0_f32),
159            },
160            align_items: Some(AlignItems::CENTER),
161            justify_content: Some(JustifyContent::CENTER),
162            ..Default::default()
163        })
164        .text(estado, 18.0, Color::from_rgba8(180, 200, 230, 255));
165
166        let label = if model.moving {
167            "moving = true   (Espacio para parar)"
168        } else {
169            "moving = false   (Espacio para mover)"
170        };
171        let boton = View::new(Style {
172            size: Size {
173                width: length(280.0_f32),
174                height: length(52.0_f32),
175            },
176            align_items: Some(AlignItems::CENTER),
177            justify_content: Some(JustifyContent::CENTER),
178            ..Default::default()
179        })
180        .fill(if model.moving {
181            Color::from_rgba8(200, 120, 50, 255)
182        } else {
183            Color::from_rgba8(50, 110, 190, 255)
184        })
185        .radius(12.0)
186        .text(label.to_string(), 16.0, Color::from_rgba8(245, 245, 250, 255))
187        .on_click(Msg::ToggleMoving);
188
189        View::new(Style {
190            flex_direction: FlexDirection::Column,
191            size: Size {
192                width: percent(1.0_f32),
193                height: percent(1.0_f32),
194            },
195            gap: Size {
196                width: length(0.0_f32),
197                height: length(20.0_f32),
198            },
199            align_items: Some(AlignItems::CENTER),
200            justify_content: Some(JustifyContent::CENTER),
201            padding: Rect {
202                left: length(24.0_f32),
203                right: length(24.0_f32),
204                top: length(24.0_f32),
205                bottom: length(24.0_f32),
206            },
207            ..Default::default()
208        })
209        .fill(Color::from_rgba8(18, 22, 30, 255))
210        .children(vec![stage, status, boton])
211    }
212}
213
214fn main() {
215    llimphi_ui::run::<Demo>();
216}