tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definitions

⎇ main llimphi-mesh/examples/lottie_rive_demo.rs Rust · 241 lines · 8.2 KB History Who touched what raw

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 **lottie + rive** en una sola app: los dos paradigmas de animación del
2//! motor nativo trabajando juntos.
3//!
4//! - **rive (esqueletal + IK)**: un brazo de 2 huesos con una malla texturizada
5//!   skinneada, que **persigue el cursor** resolviendo IK de 2 huesos cada frame
6//!   (`llimphi_anim::constraint::solve_two_bone_ik`).
7//! - **lottie**: un pulso vectorial (`llimphi-lottie` sobre el fork de velato)
8//!   reproducido en la **punta del brazo** y otro marcando el **objetivo** bajo
9//!   el cursor.
10//!
11//! Mové el mouse: el brazo alcanza el cursor (IK), el Lottie late en la punta y
12//! en el blanco. **F** alterna el codo (flip de la solución IK). Todo Rust
13//! nativo sobre vello 0.7, cero C++.
14//!
15//! Corre con:
16//!   `cargo run -p llimphi-mesh --example lottie_rive_demo --release`
17
18use std::sync::Arc;
19use std::time::Duration;
20
21use llimphi_anim::constraint::solve_two_bone_ik;
22use llimphi_anim::skel::{BoneId, Mesh, Pose, Skeleton, Vertex, Weight};
23use llimphi_lottie::LottieAsset;
24use llimphi_mesh::{paint_textured, paint_wireframe};
25use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style};
26use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, Vec2};
27use llimphi_ui::llimphi_raster::peniko::{
28    Blob, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat,
29};
30use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, PaintRect, View};
31
32// Brazo en coordenadas locales del lienzo (px). Ancla a la izquierda-centro.
33const ANCHOR: (f64, f64) = (110.0, 200.0);
34const L1: f64 = 95.0; // hueso superior
35const L2: f64 = 95.0; // hueso inferior
36const ARM_H: f64 = 40.0; // grosor del brazo
37const COLS: usize = 6; // columnas de la malla a lo largo del brazo
38
39/// Pulso azul: círculo cuya opacidad late en 1.5 s.
40const PULSE_LOTTIE: &str = r#"{
41  "v":"5.5.2","fr":30,"ip":0,"op":45,"w":100,"h":100,
42  "layers":[{"ty":4,"ip":0,"op":45,"st":0,"sr":1,
43    "ks":{"o":{"a":1,"k":[
44        {"i":{"x":[0.5],"y":[0.5]},"o":{"x":[0.5],"y":[0.5]},"t":0,"s":[100]},
45        {"i":{"x":[0.5],"y":[0.5]},"o":{"x":[0.5],"y":[0.5]},"t":22,"s":[25]},
46        {"t":45,"s":[100]}]},
47      "r":{"a":0,"k":0},"p":{"a":0,"k":[50,50]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]}},
48    "shapes":[{"ty":"gr","it":[
49      {"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[60,60]}},
50      {"ty":"fl","c":{"a":0,"k":[0.45,0.75,1.0]},"o":{"a":0,"k":100}},
51      {"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]}]}]}"#;
52
53/// Brazo de 2 huesos + malla-tira skinneada (peso suave alrededor del codo).
54fn build_arm() -> (Skeleton, Mesh, BoneId, BoneId) {
55    let mut s = Skeleton::new();
56    let a = s.add_bone(None, Pose::translate(Vec2::new(ANCHOR.0, ANCHOR.1)));
57    let b = s.add_bone(Some(a), Pose::translate(Vec2::new(L1, 0.0)));
58    s.bind();
59    s.update();
60
61    let total = L1 + L2;
62    let blend = total * 0.16; // ancho del blend de peso en el codo
63    let mut m = Mesh::new();
64    for i in 0..=COLS {
65        let p = total * i as f64 / COLS as f64; // posición a lo largo del brazo
66        // Peso suave: 1→A antes del codo, 0→A (todo B) después.
67        let wa = (1.0 - (p - (L1 - blend)) / (2.0 * blend)).clamp(0.0, 1.0);
68        let weights = vec![
69            Weight { bone: a, weight: wa },
70            Weight { bone: b, weight: 1.0 - wa },
71        ];
72        let u = i as f64 / COLS as f64;
73        let x = ANCHOR.0 + p;
74        m.vertices.push(Vertex {
75            rest: Point::new(x, ANCHOR.1 - ARM_H / 2.0),
76            uv: (u, 0.0),
77            weights: weights.clone(),
78        });
79        m.vertices.push(Vertex {
80            rest: Point::new(x, ANCHOR.1 + ARM_H / 2.0),
81            uv: (u, 1.0),
82            weights,
83        });
84    }
85    for i in 0..COLS {
86        let (t0, t1) = ((2 * i) as u32, (2 * (i + 1)) as u32);
87        let (b0, b1) = ((2 * i + 1) as u32, (2 * (i + 1) + 1) as u32);
88        m.triangles.push([t0, t1, b1]);
89        m.triangles.push([t0, b1, b0]);
90    }
91    (s, m, a, b)
92}
93
94fn checker(n: u32, sq: u32) -> ImageBrush {
95    let mut px = Vec::with_capacity((n * n * 4) as usize);
96    for y in 0..n {
97        for x in 0..n {
98            let on = ((x / sq + y / sq) % 2) == 0;
99            let (r, g, b) = if on { (235, 110, 90) } else { (250, 200, 120) };
100            px.extend_from_slice(&[r, g, b, 255]);
101        }
102    }
103    ImageBrush::new(ImageData {
104        data: Blob::new(Arc::new(px)),
105        format: ImageFormat::Rgba8,
106        alpha_type: ImageAlphaType::Alpha,
107        width: n,
108        height: n,
109    })
110}
111
112#[derive(Clone)]
113enum Msg {
114    Tick,
115    Target(f64, f64),
116    Flip,
117}
118
119struct Model {
120    skel: Skeleton,
121    mesh: Mesh,
122    upper: BoneId,
123    lower: BoneId,
124    image: ImageBrush,
125    pulse: LottieAsset,
126    target: Point,
127    t: f64,
128    flip: bool,
129}
130
131struct Demo;
132
133const TICK: Duration = Duration::from_millis(16);
134const TIP_LOCAL: Vec2 = Vec2::new(L2, 0.0);
135
136impl App for Demo {
137    type Model = Model;
138    type Msg = Msg;
139
140    fn title() -> &'static str {
141        "llimphi · lottie + rive"
142    }
143    fn initial_size() -> (u32, u32) {
144        (640, 420)
145    }
146
147    fn init(handle: &Handle<Self::Msg>) -> Self::Model {
148        let (skel, mesh, upper, lower) = build_arm();
149        handle.spawn_periodic(TICK, || Msg::Tick);
150        Model {
151            skel,
152            mesh,
153            upper,
154            lower,
155            image: checker(8, 1),
156            pulse: LottieAsset::from_str(PULSE_LOTTIE).expect("pulse lottie"),
157            target: Point::new(360.0, 150.0),
158            t: 0.0,
159            flip: false,
160        }
161    }
162
163    fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
164        match msg {
165            Msg::Tick => {
166                model.t += TICK.as_secs_f64();
167                // IK cada frame: el brazo alcanza el objetivo (cursor).
168                solve_two_bone_ik(
169                    &mut model.skel,
170                    model.upper,
171                    model.lower,
172                    TIP_LOCAL,
173                    model.target,
174                    model.flip,
175                );
176            }
177            Msg::Target(x, y) => model.target = Point::new(x, y),
178            Msg::Flip => model.flip = !model.flip,
179        }
180        model
181    }
182
183    fn on_key(_: &Self::Model, e: &KeyEvent) -> Option<Self::Msg> {
184        if e.state == KeyState::Pressed {
185            if let Key::Character(c) = &e.key {
186                if c.as_str() == "f" {
187                    return Some(Msg::Flip);
188                }
189            }
190        }
191        None
192    }
193
194    fn view(model: &Self::Model) -> View<Self::Msg> {
195        let positions = model.mesh.deform(&model.skel);
196        // Punta del brazo (mundo local) = world(lower) · tip_local.
197        let tip_local = model.skel.world(model.lower) * Point::new(TIP_LOCAL.x, TIP_LOCAL.y);
198        let mesh = model.mesh.clone();
199        let image = model.image.clone();
200        let pulse = model.pulse.clone();
201        let target = model.target;
202        let t = model.t;
203
204        View::new(Style {
205            size: Size {
206                width: percent(1.0_f32),
207                height: percent(1.0_f32),
208            },
209            ..Default::default()
210        })
211        .fill(Color::from_rgba8(18, 22, 30, 255))
212        .paint_with(move |scene, _ts, rect| {
213            // Coords locales → pantalla (el lienzo arranca en rect.x/rect.y).
214            let to_screen = Affine::translate((rect.x as f64, rect.y as f64));
215            // Brazo skinneado (rive).
216            paint_textured(scene, &mesh, &positions, to_screen, &image);
217            paint_wireframe(
218                scene,
219                &mesh,
220                &positions,
221                to_screen,
222                Color::from_rgba8(255, 255, 255, 90),
223                1.0,
224            );
225            // Pulso lottie en la punta del brazo y en el objetivo.
226            let pulse_rect = |p: Point, s: f64| PaintRect {
227                x: (rect.x as f64 + p.x - s) as f32,
228                y: (rect.y as f64 + p.y - s) as f32,
229                w: (s * 2.0) as f32,
230                h: (s * 2.0) as f32,
231            };
232            pulse.paint_at_time(scene, pulse_rect(target, 30.0), t);
233            pulse.paint_at_time(scene, pulse_rect(tip_local, 22.0), t);
234        })
235        .on_pointer_move_at(|lx, ly, _w, _h| Some(Msg::Target(lx as f64, ly as f64)))
236    }
237}
238
239fn main() {
240    llimphi_ui::run::<Demo>();
241}