tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
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}