tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Sutichaykuna

⎇ main llimphi-mesh/examples/bones_demo.rs Rust · 221 siq'ikuna · 7.0 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! Demo del Tier 4: una tira **texturizada deformada por una cadena de huesos**
2//! que ondula como una bandera/tentáculo.
3//!
4//! La cadena de huesos (`llimphi_anim::skel`) se re-posa cada frame con una onda
5//! senoidal de fase creciente; el skinning (LBS) deforma los vértices; y
6//! `llimphi-mesh` pinta la malla texturizada (warp piecewise-affine) con un
7//! wireframe encima para ver la deformación. **Espacio** alterna el wireframe.
8//!
9//! Todo Rust nativo, sobre vello 0.7 — cero C++.
10//!
11//! Corre con:
12//!   `cargo run -p llimphi-mesh --example bones_demo --release`
13
14use std::sync::Arc;
15use std::time::Duration;
16
17use llimphi_anim::skel::{Mesh, Pose, Skeleton, Vertex};
18use llimphi_mesh::{fit_transform, paint_textured, paint_wireframe, rest_bounds};
19use llimphi_ui::llimphi_layout::taffy::{
20    prelude::{length, percent, FlexDirection, Size, Style},
21    AlignItems, JustifyContent, Rect,
22};
23use llimphi_ui::llimphi_raster::kurbo::{Point, Vec2};
24use llimphi_ui::llimphi_raster::peniko::{
25    Blob, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat,
26};
27use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, NamedKey, PaintRect, View};
28
29const SEG: usize = 8; // segmentos de la tira (= huesos de la cadena, +1 raíz)
30const STEP: f64 = 30.0; // largo de cada hueso/segmento
31const STRIP_H: f64 = 72.0; // alto de la tira
32
33/// Construye la cadena de huesos (raíz + SEG eslabones) y la malla-tira atada a
34/// ella (rigid por columna), con UVs de 0..1 a lo ancho/alto.
35fn build() -> (Skeleton, Mesh) {
36    let mut s = Skeleton::new();
37    // Raíz anclada a la izquierda; cada eslabón se traslada STEP en x respecto
38    // al padre. Bone i tiene id i (orden de inserción).
39    s.add_bone(None, Pose::translate(Vec2::new(0.0, STRIP_H / 2.0)));
40    for _ in 1..=SEG {
41        s.add_bone(Some(s.len() - 1), Pose::translate(Vec2::new(STEP, 0.0)));
42    }
43    s.bind();
44
45    let mut m = Mesh::new();
46    for i in 0..=SEG {
47        let x = i as f64 * STEP;
48        let u = i as f64 / SEG as f64;
49        m.vertices.push(Vertex::rigid(Point::new(x, 0.0), (u, 0.0), i)); // borde sup
50        m.vertices.push(Vertex::rigid(Point::new(x, STRIP_H), (u, 1.0), i)); // borde inf
51    }
52    for i in 0..SEG {
53        let (t0, t1) = ((2 * i) as u32, (2 * (i + 1)) as u32);
54        let (b0, b1) = ((2 * i + 1) as u32, (2 * (i + 1) + 1) as u32);
55        m.triangles.push([t0, t1, b1]);
56        m.triangles.push([t0, b1, b0]);
57    }
58    (s, m)
59}
60
61/// Textura procedural: tablero de ajedrez (para ver el warp).
62fn checker(n: u32, sq: u32) -> ImageBrush {
63    let mut px = Vec::with_capacity((n * n * 4) as usize);
64    for y in 0..n {
65        for x in 0..n {
66            let on = ((x / sq + y / sq) % 2) == 0;
67            let (r, g, b) = if on { (70, 130, 205) } else { (240, 180, 70) };
68            px.extend_from_slice(&[r, g, b, 255]);
69        }
70    }
71    ImageBrush::new(ImageData {
72        data: Blob::new(Arc::new(px)),
73        format: ImageFormat::Rgba8,
74        alpha_type: ImageAlphaType::Alpha,
75        width: n,
76        height: n,
77    })
78}
79
80#[derive(Clone)]
81enum Msg {
82    Tick,
83    ToggleWire,
84}
85
86struct Model {
87    skel: Skeleton,
88    mesh: Mesh,
89    image: ImageBrush,
90    t: f64,
91    wireframe: bool,
92}
93
94struct Demo;
95
96const TICK: Duration = Duration::from_millis(16);
97
98impl App for Demo {
99    type Model = Model;
100    type Msg = Msg;
101
102    fn title() -> &'static str {
103        "llimphi · malla deformada por huesos"
104    }
105    fn initial_size() -> (u32, u32) {
106        (520, 420)
107    }
108
109    fn init(handle: &Handle<Self::Msg>) -> Self::Model {
110        let (skel, mesh) = build();
111        handle.spawn_periodic(TICK, || Msg::Tick);
112        Model {
113            skel,
114            mesh,
115            image: checker(8, 1),
116            t: 0.0,
117            wireframe: true,
118        }
119    }
120
121    fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
122        match msg {
123            Msg::Tick => {
124                model.t += TICK.as_secs_f64();
125                // Onda viajera: cada hueso oscila con fase creciente → la tira
126                // ondula desde el ancla hacia la punta.
127                for i in 1..=SEG {
128                    let r = 0.38 * (model.t * 3.0 - i as f64 * 0.6).sin();
129                    model
130                        .skel
131                        .set_pose(i, Pose::new(Vec2::new(STEP, 0.0), r, Vec2::new(1.0, 1.0)));
132                }
133                model.skel.update();
134            }
135            Msg::ToggleWire => model.wireframe = !model.wireframe,
136        }
137        model
138    }
139
140    fn on_key(_: &Self::Model, e: &KeyEvent) -> Option<Self::Msg> {
141        if e.state == KeyState::Pressed && e.key == Key::Named(NamedKey::Space) {
142            Some(Msg::ToggleWire)
143        } else {
144            None
145        }
146    }
147
148    fn view(model: &Self::Model) -> View<Self::Msg> {
149        // Deformar aquí (lectura) y mover los datos al closure de pintura.
150        let positions = model.mesh.deform(&model.skel);
151        let mesh = model.mesh.clone();
152        let image = model.image.clone();
153        let wire = model.wireframe;
154
155        let stage = View::new(Style {
156            size: Size {
157                width: percent(1.0_f32),
158                height: percent(1.0_f32),
159            },
160            flex_grow: 1.0,
161            ..Default::default()
162        })
163        .paint_with(move |scene, _ts, rect| {
164            // Margen para que la onda no se recorte contra el borde.
165            let inner = PaintRect {
166                x: rect.x + 50.0,
167                y: rect.y + 50.0,
168                w: (rect.w - 100.0).max(1.0),
169                h: (rect.h - 100.0).max(1.0),
170            };
171            let xf = fit_transform(rest_bounds(&mesh), inner);
172            paint_textured(scene, &mesh, &positions, xf, &image);
173            if wire {
174                paint_wireframe(
175                    scene,
176                    &mesh,
177                    &positions,
178                    xf,
179                    Color::from_rgba8(255, 255, 255, 120),
180                    1.2,
181                );
182            }
183        });
184
185        let hint = View::new(Style {
186            size: Size {
187                width: percent(1.0_f32),
188                height: length(30.0_f32),
189            },
190            align_items: Some(AlignItems::CENTER),
191            justify_content: Some(JustifyContent::CENTER),
192            ..Default::default()
193        })
194        .text(
195            "tira texturizada deformada por una cadena de huesos · Espacio: wireframe".to_string(),
196            13.0,
197            Color::from_rgba8(140, 155, 180, 255),
198        );
199
200        View::new(Style {
201            flex_direction: FlexDirection::Column,
202            size: Size {
203                width: percent(1.0_f32),
204                height: percent(1.0_f32),
205            },
206            padding: Rect {
207                left: length(8.0_f32),
208                right: length(8.0_f32),
209                top: length(8.0_f32),
210                bottom: length(8.0_f32),
211            },
212            ..Default::default()
213        })
214        .fill(Color::from_rgba8(18, 22, 30, 255))
215        .children(vec![stage, hint])
216    }
217}
218
219fn main() {
220    llimphi_ui::run::<Demo>();
221}