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/physics_skeletons_demo.rs Rust · 211 siq'ikuna · 7.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

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 **chévere**: leyes físicas sobre varios esqueletos.
2//!
3//! Cinco tentáculos: cada uno es una cadena de huesos con una malla texturizada
4//! skinneada, manejada por **física Verlet** (`llimphi_anim::physics`) — cuelgan
5//! del techo, se balancean bajo gravedad y rebotan en el piso. **Mové el mouse**
6//! y el cursor los **empuja** (campo de repulsión). Cero keyframes: la física
7//! posa los esqueletos, el skinning deforma las mallas.
8//!
9//! Todo Rust nativo sobre vello 0.7. Pensado también como fondo vivo de
10//! lock/greeter.
11//!
12//! Corre con:
13//!   `cargo run -p llimphi-mesh --example physics_skeletons_demo --release`
14
15use std::sync::Arc;
16use std::time::Duration;
17
18use llimphi_anim::physics::{pose_chain_from_points, Physics};
19use llimphi_anim::skel::{BoneId, Mesh, Pose, Skeleton, Vertex};
20use llimphi_mesh::paint_textured;
21use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style};
22use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, Vec2};
23use llimphi_ui::llimphi_raster::peniko::{
24    Blob, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat,
25};
26use llimphi_ui::{App, Handle, PaintRect, View};
27
28const SEGS: usize = 9;
29const SEG_LEN: f64 = 26.0;
30const THICK: f64 = 22.0;
31const FLOOR_Y: f64 = 372.0;
32
33/// Un tentáculo: física (partículas en cadena) + esqueleto + malla skinneada.
34struct Rope {
35    phys: Physics,
36    skel: Skeleton,
37    mesh: Mesh,
38    bones: Vec<BoneId>,
39    image: ImageBrush,
40}
41
42fn checker(a: (u8, u8, u8), b: (u8, u8, u8)) -> ImageBrush {
43    let n = 8u32;
44    let mut px = Vec::with_capacity((n * n * 4) as usize);
45    for y in 0..n {
46        for x in 0..n {
47            let (r, g, bl) = if (x + y) % 2 == 0 { a } else { b };
48            px.extend_from_slice(&[r, g, bl, 255]);
49        }
50    }
51    ImageBrush::new(ImageData {
52        data: Blob::new(Arc::new(px)),
53        format: ImageFormat::Rgba8,
54        alpha_type: ImageAlphaType::Alpha,
55        width: n,
56        height: n,
57    })
58}
59
60fn make_rope(anchor: Point, image: ImageBrush) -> Rope {
61    // Física: partículas desde el ancla (fija) hacia abajo.
62    let mut phys = Physics::new();
63    phys.floor_y = Some(FLOOR_Y);
64    let mut prev = phys.particle(anchor, true);
65    for i in 1..=SEGS {
66        let p = phys.particle(Point::new(anchor.x, anchor.y + i as f64 * SEG_LEN), false);
67        phys.link(prev, p);
68        prev = p;
69    }
70
71    // Esqueleto en bind pose recto desde el ORIGEN (a lo largo de +x); la física
72    // lo reubica/orienta cada frame con pose_chain_from_points.
73    let mut skel = Skeleton::new();
74    let mut bones = vec![skel.add_bone(None, Pose::identity())];
75    for _ in 1..=SEGS {
76        bones.push(skel.add_bone(Some(*bones.last().unwrap()), Pose::translate(Vec2::new(SEG_LEN, 0.0))));
77    }
78    skel.bind();
79
80    // Malla-tira en reposo (recta desde el origen), rigid por columna al hueso i.
81    let mut mesh = Mesh::new();
82    for i in 0..=SEGS {
83        let x = i as f64 * SEG_LEN;
84        let u = i as f64 / SEGS as f64;
85        // La punta se afina (tentáculo).
86        let half = THICK * 0.5 * (1.0 - 0.6 * (i as f64 / SEGS as f64));
87        mesh.vertices.push(Vertex::rigid(Point::new(x, -half), (u, 0.0), bones[i]));
88        mesh.vertices.push(Vertex::rigid(Point::new(x, half), (u, 1.0), bones[i]));
89    }
90    for i in 0..SEGS {
91        let (t0, t1) = ((2 * i) as u32, (2 * (i + 1)) as u32);
92        let (b0, b1) = ((2 * i + 1) as u32, (2 * (i + 1) + 1) as u32);
93        mesh.triangles.push([t0, t1, b1]);
94        mesh.triangles.push([t0, b1, b0]);
95    }
96
97    Rope { phys, skel, mesh, bones, image }
98}
99
100#[derive(Clone)]
101enum Msg {
102    Tick,
103    Cursor(Option<(f64, f64)>),
104}
105
106struct Model {
107    ropes: Vec<Rope>,
108    cursor: Option<Point>,
109}
110
111struct Demo;
112
113const TICK: Duration = Duration::from_millis(16);
114
115impl App for Demo {
116    type Model = Model;
117    type Msg = Msg;
118
119    fn title() -> &'static str {
120        "llimphi · física sobre esqueletos"
121    }
122    fn initial_size() -> (u32, u32) {
123        (700, 440)
124    }
125
126    fn init(handle: &Handle<Self::Msg>) -> Self::Model {
127        let palette = [
128            ((90, 180, 230), (40, 90, 140)),
129            ((235, 130, 100), (150, 60, 50)),
130            ((130, 220, 150), (50, 120, 70)),
131            ((220, 180, 90), (140, 100, 40)),
132            ((200, 130, 220), (110, 60, 140)),
133        ];
134        let ropes = palette
135            .iter()
136            .enumerate()
137            .map(|(i, (a, b))| {
138                let x = 110.0 + i as f64 * 120.0;
139                make_rope(Point::new(x, 40.0), checker(*a, *b))
140            })
141            .collect();
142        handle.spawn_periodic(TICK, || Msg::Tick);
143        Model { ropes, cursor: None }
144    }
145
146    fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
147        match msg {
148            Msg::Tick => {
149                let dt = TICK.as_secs_f64();
150                let cursor = model.cursor;
151                for rope in &mut model.ropes {
152                    rope.phys.step(dt, 8);
153                    if let Some(c) = cursor {
154                        rope.phys.repel(c, 90.0, 28.0);
155                    }
156                    let pts = rope.phys.positions();
157                    pose_chain_from_points(&mut rope.skel, &rope.bones, &pts);
158                }
159            }
160            Msg::Cursor(p) => model.cursor = p.map(|(x, y)| Point::new(x, y)),
161        }
162        model
163    }
164
165    fn view(model: &Self::Model) -> View<Self::Msg> {
166        // Deformar todas las mallas ahora; mover los datos al closure.
167        let painted: Vec<(Mesh, Vec<Point>, ImageBrush)> = model
168            .ropes
169            .iter()
170            .map(|r| (r.mesh.clone(), r.mesh.deform(&r.skel), r.image.clone()))
171            .collect();
172
173        View::new(Style {
174            size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
175            ..Default::default()
176        })
177        .fill(Color::from_rgba8(16, 18, 26, 255))
178        .paint_with(move |scene, _ts, rect| {
179            let to_screen = Affine::translate((rect.x as f64, rect.y as f64));
180            // Piso.
181            let floor = PaintRect {
182                x: rect.x,
183                y: rect.y + FLOOR_Y as f32,
184                w: rect.w,
185                h: (rect.h - FLOOR_Y as f32).max(0.0),
186            };
187            let mut fp = llimphi_ui::llimphi_raster::kurbo::BezPath::new();
188            fp.move_to((floor.x as f64, floor.y as f64));
189            fp.line_to((floor.x as f64 + floor.w as f64, floor.y as f64));
190            fp.line_to((floor.x as f64 + floor.w as f64, floor.y as f64 + floor.h as f64));
191            fp.line_to((floor.x as f64, floor.y as f64 + floor.h as f64));
192            fp.close_path();
193            scene.fill(
194                llimphi_ui::llimphi_raster::peniko::Fill::NonZero,
195                Affine::IDENTITY,
196                &llimphi_ui::llimphi_raster::peniko::Brush::Solid(Color::from_rgba8(26, 28, 38, 255)),
197                None,
198                &fp,
199            );
200            for (mesh, positions, image) in &painted {
201                paint_textured(scene, mesh, positions, to_screen, image);
202            }
203        })
204        .on_pointer_move_at(|lx, ly, _w, _h| Some(Msg::Cursor(Some((lx as f64, ly as f64)))))
205        .on_pointer_leave(Msg::Cursor(None))
206    }
207}
208
209fn main() {
210    llimphi_ui::run::<Demo>();
211}