Files
llimphi/llimphi-mesh/examples/bones_demo.rs
T
Sergio d251eb32b6 chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
El repo publico se habia quedado en julio: el README anunciaba vello 0.7 +
wgpu 27 (dos bumps atras) y faltaban los crates del ultimo mes.

- refresco con scripts/actualizar-standalone.py --llimphi (cargo check OK)
- llegan llimphi-cpu, llimphi-hybrid, llimphi-glifos y llimphi-wire-escena:
  los cuatro caminos a pixeles con un solo compositor
- MANUAL.md / SDD.md / LEEME.md sincronizados con el monorepo
- README: versiones al dia, install por crates.io (cargo add llimphi),
  seccion "beyond the 2D widget kit" y el indice de crates que apuntaba
  al propio README ahora apunta a MANUAL.md §19
2026-08-06 17:26:11 +00:00

222 lines
7.0 KiB
Rust

//! Demo del Tier 4: una tira **texturizada deformada por una cadena de huesos**
//! que ondula como una bandera/tentáculo.
//!
//! La cadena de huesos (`llimphi_anim::skel`) se re-posa cada frame con una onda
//! senoidal de fase creciente; el skinning (LBS) deforma los vértices; y
//! `llimphi-mesh` pinta la malla texturizada (warp piecewise-affine) con un
//! wireframe encima para ver la deformación. **Espacio** alterna el wireframe.
//!
//! Todo Rust nativo, sobre vello 0.7 — cero C++.
//!
//! Corre con:
//! `cargo run -p llimphi-mesh --example bones_demo --release`
use std::sync::Arc;
use std::time::Duration;
use llimphi_anim::skel::{Mesh, Pose, Skeleton, Vertex};
use llimphi_mesh::{fit_transform, paint_textured, paint_wireframe, rest_bounds};
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, FlexDirection, Size, Style},
AlignItems, JustifyContent, Rect,
};
use llimphi_ui::llimphi_raster::kurbo::{Point, Vec2};
use llimphi_ui::llimphi_raster::peniko::{
Blob, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat,
};
use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, NamedKey, PaintRect, View};
const SEG: usize = 8; // segmentos de la tira (= huesos de la cadena, +1 raíz)
const STEP: f64 = 30.0; // largo de cada hueso/segmento
const STRIP_H: f64 = 72.0; // alto de la tira
/// Construye la cadena de huesos (raíz + SEG eslabones) y la malla-tira atada a
/// ella (rigid por columna), con UVs de 0..1 a lo ancho/alto.
fn build() -> (Skeleton, Mesh) {
let mut s = Skeleton::new();
// Raíz anclada a la izquierda; cada eslabón se traslada STEP en x respecto
// al padre. Bone i tiene id i (orden de inserción).
s.add_bone(None, Pose::translate(Vec2::new(0.0, STRIP_H / 2.0)));
for _ in 1..=SEG {
s.add_bone(Some(s.len() - 1), Pose::translate(Vec2::new(STEP, 0.0)));
}
s.bind();
let mut m = Mesh::new();
for i in 0..=SEG {
let x = i as f64 * STEP;
let u = i as f64 / SEG as f64;
m.vertices.push(Vertex::rigid(Point::new(x, 0.0), (u, 0.0), i)); // borde sup
m.vertices.push(Vertex::rigid(Point::new(x, STRIP_H), (u, 1.0), i)); // borde inf
}
for i in 0..SEG {
let (t0, t1) = ((2 * i) as u32, (2 * (i + 1)) as u32);
let (b0, b1) = ((2 * i + 1) as u32, (2 * (i + 1) + 1) as u32);
m.triangles.push([t0, t1, b1]);
m.triangles.push([t0, b1, b0]);
}
(s, m)
}
/// Textura procedural: tablero de ajedrez (para ver el warp).
fn checker(n: u32, sq: u32) -> ImageBrush {
let mut px = Vec::with_capacity((n * n * 4) as usize);
for y in 0..n {
for x in 0..n {
let on = ((x / sq + y / sq) % 2) == 0;
let (r, g, b) = if on { (70, 130, 205) } else { (240, 180, 70) };
px.extend_from_slice(&[r, g, b, 255]);
}
}
ImageBrush::new(ImageData {
data: Blob::new(Arc::new(px)),
format: ImageFormat::Rgba8,
alpha_type: ImageAlphaType::Alpha,
width: n,
height: n,
})
}
#[derive(Clone)]
enum Msg {
Tick,
ToggleWire,
}
struct Model {
skel: Skeleton,
mesh: Mesh,
image: ImageBrush,
t: f64,
wireframe: bool,
}
struct Demo;
const TICK: Duration = Duration::from_millis(16);
impl App for Demo {
type Model = Model;
type Msg = Msg;
fn title() -> &'static str {
"llimphi · malla deformada por huesos"
}
fn initial_size() -> (u32, u32) {
(520, 420)
}
fn init(handle: &Handle<Self::Msg>) -> Self::Model {
let (skel, mesh) = build();
handle.spawn_periodic(TICK, || Msg::Tick);
Model {
skel,
mesh,
image: checker(8, 1),
t: 0.0,
wireframe: true,
}
}
fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
match msg {
Msg::Tick => {
model.t += TICK.as_secs_f64();
// Onda viajera: cada hueso oscila con fase creciente → la tira
// ondula desde el ancla hacia la punta.
for i in 1..=SEG {
let r = 0.38 * (model.t * 3.0 - i as f64 * 0.6).sin();
model
.skel
.set_pose(i, Pose::new(Vec2::new(STEP, 0.0), r, Vec2::new(1.0, 1.0)));
}
model.skel.update();
}
Msg::ToggleWire => model.wireframe = !model.wireframe,
}
model
}
fn on_key(_: &Self::Model, e: &KeyEvent) -> Option<Self::Msg> {
if e.state == KeyState::Pressed && e.key == Key::Named(NamedKey::Space) {
Some(Msg::ToggleWire)
} else {
None
}
}
fn view(model: &Self::Model) -> View<Self::Msg> {
// Deformar aquí (lectura) y mover los datos al closure de pintura.
let positions = model.mesh.deform(&model.skel);
let mesh = model.mesh.clone();
let image = model.image.clone();
let wire = model.wireframe;
let stage = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
flex_grow: 1.0,
..Default::default()
})
.paint_with(move |scene, _ts, rect| {
// Margen para que la onda no se recorte contra el borde.
let inner = PaintRect {
x: rect.x + 50.0,
y: rect.y + 50.0,
w: (rect.w - 100.0).max(1.0),
h: (rect.h - 100.0).max(1.0),
};
let xf = fit_transform(rest_bounds(&mesh), inner);
paint_textured(scene, &mesh, &positions, xf, &image);
if wire {
paint_wireframe(
scene,
&mesh,
&positions,
xf,
Color::from_rgba8(255, 255, 255, 120),
1.2,
);
}
});
let hint = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(30.0_f32),
},
align_items: Some(AlignItems::CENTER),
justify_content: Some(JustifyContent::CENTER),
..Default::default()
})
.text(
"tira texturizada deformada por una cadena de huesos · Espacio: wireframe".to_string(),
13.0,
Color::from_rgba8(140, 155, 180, 255),
);
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
padding: Rect {
left: length(8.0_f32),
right: length(8.0_f32),
top: length(8.0_f32),
bottom: length(8.0_f32),
},
..Default::default()
})
.fill(Color::from_rgba8(18, 22, 30, 255))
.children(vec![stage, hint])
}
}
fn main() {
llimphi_ui::run::<Demo>();
}