feat: llimphi standalone — framework UI soberano extraído del monorepo

Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "llimphi-widget-splash"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-widget-splash — splash de arranque gioser: cuatro cuadrantes (unanchay/yachay/ruway/ukupacha) animados con tween de entrada secuencial. Identidad visual del SO."
[dependencies]
llimphi-ui = { workspace = true }
llimphi-theme = { workspace = true }
llimphi-motion = { workspace = true }
+283
View File
@@ -0,0 +1,283 @@
//! `llimphi-widget-splash` — splash de arranque gioser.
//!
//! Identidad visual del SO al boot: cuatro cuadrantes ordenados como
//! una cruz andina, cada uno con su nombre quechua y color simbólico,
//! que **entran en secuencia** con un tween de fade+escala.
//!
//! Los cuadrantes (en orden de entrada):
//! 1. `unanchay` — PERCIBIR — cyan (índigo claro)
//! 2. `yachay` — CONOCER — verde aurora
//! 3. `ruway` — HACER — naranja sunset
//! 4. `ukupacha` — RAÍZ — púrpura profundo
//!
//! Cada cuadrante hace fade-in + slight scale-up, con un offset de
//! `motion::NORMAL / 2` entre uno y el siguiente. La app pasa un
//! `Instant` de inicio y el splash calcula las fases relativas — no
//! requiere ningún tween del modelo.
//!
//! Cuando el splash termina (todos visibles), la app puede:
//! - mantenerlo unos segundos más como pantalla de carga,
//! - hacer un fade-out completo cuando el sistema esté listo,
//! - o reemplazarlo por la UI principal.
#![forbid(unsafe_code)]
use std::time::Instant;
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, FlexDirection, Size, Style},
AlignItems, JustifyContent, Rect,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::llimphi_text::Alignment;
use llimphi_ui::View;
use llimphi_motion::motion;
/// Datos de un cuadrante: nombre quechua, glosa breve y color.
#[derive(Debug, Clone, Copy)]
pub struct Quadrant {
pub name: &'static str,
pub gloss: &'static str,
pub color: Color,
}
/// Los cuatro cuadrantes canónicos, en orden de entrada al splash.
pub fn quadrants() -> [Quadrant; 4] {
[
Quadrant {
name: "unanchay",
gloss: "PERCIBIR",
color: Color::from_rgba8(110, 160, 230, 255),
},
Quadrant {
name: "yachay",
gloss: "CONOCER",
color: Color::from_rgba8(110, 220, 180, 255),
},
Quadrant {
name: "ruway",
gloss: "HACER",
color: Color::from_rgba8(232, 160, 90, 255),
},
Quadrant {
name: "ukupacha",
gloss: "RAÍZ",
color: Color::from_rgba8(160, 110, 220, 255),
},
]
}
/// Construye el splash. `started_at` es el `Instant` de origen — el
/// splash calcula las fases relativas. La app puede llamar `animate(handle,
/// motion::SLOW * 3, …)` para forzar repaints durante la animación.
///
/// `bg`: color de fondo (típico: `theme.bg_app`).
/// `fg_text`: color del título/glosa.
pub fn splash_view<Msg: Clone + 'static>(
started_at: Instant,
bg: Color,
fg_text: Color,
) -> View<Msg> {
let elapsed = started_at.elapsed().as_secs_f32();
let stagger = motion::NORMAL.as_secs_f32() * 0.45;
let per_quad = motion::NORMAL.as_secs_f32();
let quads = quadrants();
let cells: Vec<View<Msg>> = quads
.iter()
.enumerate()
.map(|(i, q)| {
let local_t = ((elapsed - i as f32 * stagger) / per_quad).clamp(0.0, 1.0);
let eased = motion::ease_out_cubic(local_t);
quadrant_cell(q, eased, fg_text)
})
.collect();
// 2×2 grid: row 0 = unanchay + yachay; row 1 = ruway + ukupacha.
let row = |a: View<Msg>, b: View<Msg>| -> View<Msg> {
View::new(Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0_f32),
height: percent(0.5_f32),
},
gap: Size {
width: length(12.0_f32),
height: length(0.0_f32),
},
..Default::default()
})
.children(vec![a, b])
};
let mut iter = cells.into_iter();
let r0 = row(iter.next().unwrap(), iter.next().unwrap());
let r1 = row(iter.next().unwrap(), iter.next().unwrap());
let grid = View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: length(420.0_f32),
height: length(280.0_f32),
},
gap: Size {
width: length(0.0_f32),
height: length(12.0_f32),
},
flex_shrink: 0.0,
..Default::default()
})
.children(vec![r0, r1]);
// Título "gioser" debajo, también fade-in pero al final.
let title_t = ((elapsed - 4.0 * stagger) / per_quad).clamp(0.0, 1.0);
let title_alpha = motion::ease_out_cubic(title_t);
let title = View::new(Style {
size: Size {
width: length(420.0_f32),
height: length(32.0_f32),
},
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
flex_shrink: 0.0,
..Default::default()
})
.text_aligned("gioser", 22.0, fg_text, Alignment::Center)
.alpha(title_alpha);
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
gap: Size {
width: length(0.0_f32),
height: length(28.0_f32),
},
padding: Rect {
left: length(0.0_f32),
right: length(0.0_f32),
top: length(0.0_f32),
bottom: length(0.0_f32),
},
..Default::default()
})
.fill(bg)
.children(vec![grid, title])
}
fn quadrant_cell<Msg: Clone + 'static>(
quad: &Quadrant,
progress: f32,
fg_text: Color,
) -> View<Msg> {
// El cuadrante "entra" con fade y un leve drift desde abajo (10px).
// El drift lo representamos con un padding-top que tiende a cero;
// como llimphi no expone translate por nodo (sólo position absolute),
// metemos el contenido en un wrapper con padding decreciente.
let drift = (1.0 - progress) * 10.0;
let name = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(28.0_f32),
},
flex_shrink: 0.0,
..Default::default()
})
.text_aligned(quad.name, 16.0, fg_text, Alignment::Center);
let gloss = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(16.0_f32),
},
flex_shrink: 0.0,
..Default::default()
})
.text_aligned(quad.gloss, 10.0, quad.color, Alignment::Center);
let inner = View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
gap: Size {
width: length(0.0_f32),
height: length(6.0_f32),
},
padding: Rect {
left: length(0.0_f32),
right: length(0.0_f32),
top: length(drift),
bottom: length(0.0_f32),
},
..Default::default()
})
.children(vec![name, gloss]);
// Fondo del cuadrante con gradient vertical en el color semántico:
// alpha 50 arriba → alpha 12 abajo. Da volumen al cuadrante (más
// intenso cerca del accent strip del top) y un efecto "halo descendente"
// que ayuda a leer la cruz andina como cuatro luces que emergen del
// centro. Antes: alpha 30 uniforme.
let border = with_alpha8(quad.color, 90);
let bg_top = with_alpha8(quad.color, 50);
let bg_bot = with_alpha8(quad.color, 12);
let cell_radius = llimphi_theme::radius::MD;
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
flex_grow: 1.0,
..Default::default()
})
.paint_with(move |scene, _ts, rect| {
use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, RoundedRect};
use llimphi_ui::llimphi_raster::peniko::{Fill, Gradient};
if rect.w <= 0.0 || rect.h <= 0.0 {
return;
}
let x0 = rect.x as f64;
let y0 = rect.y as f64;
let x1 = (rect.x + rect.w) as f64;
let y1 = (rect.y + rect.h) as f64;
let rr = RoundedRect::new(x0, y0, x1, y1, cell_radius);
let gradient = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y1))
.with_stops([bg_top, bg_bot].as_slice());
scene.fill(Fill::NonZero, Affine::IDENTITY, &gradient, None, &rr);
})
.radius(cell_radius)
.clip(true)
.alpha(progress)
.children(vec![
// Línea accent superior — 2px del color del cuadrante a alta
// intensidad, ancla del gradiente que cae.
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(2.0_f32),
},
flex_shrink: 0.0,
..Default::default()
})
.fill(border),
inner,
])
}
fn with_alpha8(c: Color, a: u8) -> Color {
let [r, g, b, _] = c.components;
use llimphi_ui::llimphi_raster::peniko::color::AlphaColor;
AlphaColor::new([r, g, b, a as f32 / 255.0])
}