tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}