tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-mesh/examples/clips_to_bones_demo.rs Rust · 248 líneas · 8.1 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! Demo que **cierra el círculo** del motor: máquina de estados → animación de
2//! huesos → skinning → malla deformada.
3//!
4//! Dos clips son `BoneAnimation`s (animaciones esqueletales keyframeadas):
5//! `rest` (tira recta) y `wave` (onda viajera). Una máquina de estados
6//! (`llimphi-anim`) transiciona entre ellos por el input `active`, y
7//! `pose_from_render_frame` posa el esqueleto **blendeando poses** durante el
8//! crossfade — así la tira se *ease-in* a la onda en vez de saltar. El skinning
9//! deforma la malla texturizada (`llimphi-mesh`).
10//!
11//! **Espacio** alterna `active` (rest ⇄ wave). Todo Rust nativo sobre vello 0.7.
12//!
13//! Corre con:
14//!   `cargo run -p llimphi-mesh --example clips_to_bones_demo --release`
15
16use std::f64::consts::PI;
17use std::sync::Arc;
18use std::time::Duration;
19
20use llimphi_anim::skel::{
21    pose_from_render_frame, BoneAnimation, BoneTrack, Mesh, Pose, PoseKey, Skeleton, Vertex,
22};
23use llimphi_anim::{Condition, Instance, StateMachine};
24use llimphi_mesh::{fit_transform, paint_textured, paint_wireframe, rest_bounds};
25use llimphi_ui::llimphi_layout::taffy::{
26    prelude::{length, percent, FlexDirection, Size, Style},
27    AlignItems, JustifyContent, Rect,
28};
29use llimphi_ui::llimphi_raster::kurbo::{Point, Vec2};
30use llimphi_ui::llimphi_raster::peniko::{
31    Blob, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat,
32};
33use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, NamedKey, PaintRect, View};
34
35const SEG: usize = 8;
36const STEP: f64 = 30.0;
37const STRIP_H: f64 = 72.0;
38
39/// Cadena de huesos (raíz anclada + SEG eslabones) + malla-tira atada rigid.
40fn build_rig() -> (Skeleton, Mesh) {
41    let mut s = Skeleton::new();
42    s.add_bone(None, Pose::translate(Vec2::new(0.0, STRIP_H / 2.0)));
43    for _ in 1..=SEG {
44        s.add_bone(Some(s.len() - 1), Pose::translate(Vec2::new(STEP, 0.0)));
45    }
46    s.bind();
47
48    let mut m = Mesh::new();
49    for i in 0..=SEG {
50        let x = i as f64 * STEP;
51        let u = i as f64 / SEG as f64;
52        m.vertices.push(Vertex::rigid(Point::new(x, 0.0), (u, 0.0), i));
53        m.vertices.push(Vertex::rigid(Point::new(x, STRIP_H), (u, 1.0), i));
54    }
55    for i in 0..SEG {
56        let (t0, t1) = ((2 * i) as u32, (2 * (i + 1)) as u32);
57        let (b0, b1) = ((2 * i + 1) as u32, (2 * (i + 1) + 1) as u32);
58        m.triangles.push([t0, t1, b1]);
59        m.triangles.push([t0, b1, b0]);
60    }
61    (s, m)
62}
63
64/// Pose de un eslabón: mantiene la translación STEP (estructura de la cadena) y
65/// anima sólo la rotación.
66fn link_pose(rot: f64) -> Pose {
67    Pose::new(Vec2::new(STEP, 0.0), rot, Vec2::new(1.0, 1.0))
68}
69
70/// Clip "recto": todos los eslabones a rotación 0 (un keyframe).
71fn straight_anim() -> BoneAnimation {
72    let tracks = (1..=SEG)
73        .map(|i| BoneTrack {
74            bone: i,
75            keys: vec![PoseKey { t: 0.0, pose: link_pose(0.0) }],
76        })
77        .collect();
78    BoneAnimation { duration: 1.0, looping: true, tracks }
79}
80
81/// Clip "onda": cada eslabón keyframeado con una senoidal de fase creciente.
82fn wave_anim(amp: f64, period: f64, nkeys: usize) -> BoneAnimation {
83    let tracks = (1..=SEG)
84        .map(|i| {
85            let keys = (0..=nkeys)
86                .map(|k| {
87                    let t = period * k as f64 / nkeys as f64;
88                    let r = amp * (2.0 * PI * t / period - i as f64 * 0.6).sin();
89                    PoseKey { t, pose: link_pose(r) }
90                })
91                .collect();
92            BoneTrack { bone: i, keys }
93        })
94        .collect();
95    BoneAnimation { duration: period, looping: true, tracks }
96}
97
98fn checker(n: u32, sq: u32) -> ImageBrush {
99    let mut px = Vec::with_capacity((n * n * 4) as usize);
100    for y in 0..n {
101        for x in 0..n {
102            let on = ((x / sq + y / sq) % 2) == 0;
103            let (r, g, b) = if on { (70, 130, 205) } else { (240, 180, 70) };
104            px.extend_from_slice(&[r, g, b, 255]);
105        }
106    }
107    ImageBrush::new(ImageData {
108        data: Blob::new(Arc::new(px)),
109        format: ImageFormat::Rgba8,
110        alpha_type: ImageAlphaType::Alpha,
111        width: n,
112        height: n,
113    })
114}
115
116#[derive(Clone)]
117enum Msg {
118    Tick,
119    Toggle,
120}
121
122struct Model {
123    sm: Instance,
124    skel: Skeleton,
125    mesh: Mesh,
126    clips: Vec<BoneAnimation>,
127    image: ImageBrush,
128    active: bool,
129}
130
131struct Demo;
132
133const TICK: Duration = Duration::from_millis(16);
134
135impl App for Demo {
136    type Model = Model;
137    type Msg = Msg;
138
139    fn title() -> &'static str {
140        "llimphi · clips → huesos → malla"
141    }
142    fn initial_size() -> (u32, u32) {
143        (560, 420)
144    }
145
146    fn init(handle: &Handle<Self::Msg>) -> Self::Model {
147        let (skel, mesh) = build_rig();
148        let clips = vec![straight_anim(), wave_anim(0.42, 2.2, 16)];
149
150        let mut sm = StateMachine::new();
151        let rest = sm.add_state("rest", 0, 1.0, true);
152        let wave = sm.add_state("wave", 1, 1.0, true);
153        sm.set_entry(rest);
154        sm.transition(rest, wave, vec![Condition::bool("active", true)], 0.5);
155        sm.transition(wave, rest, vec![Condition::bool("active", false)], 0.5);
156
157        handle.spawn_periodic(TICK, || Msg::Tick);
158
159        Model {
160            sm: sm.instance(),
161            skel,
162            mesh,
163            clips,
164            image: checker(8, 1),
165            active: false,
166        }
167    }
168
169    fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
170        match msg {
171            Msg::Tick => {
172                model.sm.advance(TICK.as_secs_f64());
173                // El estado/transición de la máquina posa el esqueleto (blend de
174                // poses durante el crossfade). pose_from_render_frame llama
175                // skel.update() internamente.
176                let frame = model.sm.render_frame();
177                pose_from_render_frame(&mut model.skel, &frame, &model.clips);
178            }
179            Msg::Toggle => {
180                model.active = !model.active;
181                model.sm.set_bool("active", model.active);
182            }
183        }
184        model
185    }
186
187    fn on_key(_: &Self::Model, e: &KeyEvent) -> Option<Self::Msg> {
188        if e.state == KeyState::Pressed && e.key == Key::Named(NamedKey::Space) {
189            Some(Msg::Toggle)
190        } else {
191            None
192        }
193    }
194
195    fn view(model: &Self::Model) -> View<Self::Msg> {
196        let positions = model.mesh.deform(&model.skel);
197        let mesh = model.mesh.clone();
198        let image = model.image.clone();
199
200        let stage = View::new(Style {
201            size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
202            flex_grow: 1.0,
203            ..Default::default()
204        })
205        .paint_with(move |scene, _ts, rect| {
206            let inner = PaintRect {
207                x: rect.x + 50.0,
208                y: rect.y + 50.0,
209                w: (rect.w - 100.0).max(1.0),
210                h: (rect.h - 100.0).max(1.0),
211            };
212            let xf = fit_transform(rest_bounds(&mesh), inner);
213            paint_textured(scene, &mesh, &positions, xf, &image);
214            paint_wireframe(scene, &mesh, &positions, xf, Color::from_rgba8(255, 255, 255, 110), 1.2);
215        });
216
217        let label = if model.sm.is_transitioning() {
218            "· · · blend de poses · · ·".to_string()
219        } else {
220            format!("estado: {}   (Espacio: rest ⇄ wave)", model.sm.current_state())
221        };
222        let status = View::new(Style {
223            size: Size { width: percent(1.0_f32), height: length(30.0_f32) },
224            align_items: Some(AlignItems::Center),
225            justify_content: Some(JustifyContent::Center),
226            ..Default::default()
227        })
228        .text(label, 14.0, Color::from_rgba8(150, 165, 190, 255));
229
230        View::new(Style {
231            flex_direction: FlexDirection::Column,
232            size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
233            padding: Rect {
234                left: length(8.0_f32),
235                right: length(8.0_f32),
236                top: length(8.0_f32),
237                bottom: length(8.0_f32),
238            },
239            ..Default::default()
240        })
241        .fill(Color::from_rgba8(18, 22, 30, 255))
242        .children(vec![stage, status])
243    }
244}
245
246fn main() {
247    llimphi_ui::run::<Demo>();
248}