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
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "llimphi-widget-wawa-mark"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-widget-wawa-mark — sello vectorial de wawa: rombo con degradado azul índigo → púrpura profundo + 'W' implícita en trazo blanco continuo + Merkle Core luminoso en la sutura. Sin tipografía, todo geometría."
[dependencies]
llimphi-ui = { workspace = true }
llimphi-theme = { workspace = true }
@@ -0,0 +1,85 @@
//! Demo del sello wawa. Tres tamaños sobre fondo oscuro neutro.
//!
//! `cargo run -p llimphi-widget-wawa-mark --example wawa_mark_demo --release`
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::{App, Handle, View};
use llimphi_widget_wawa_mark::{wawa_mark_view, WawaMarkPalette};
struct Demo;
impl App for Demo {
type Model = ();
type Msg = ();
fn title() -> &'static str {
"wawa · sello"
}
fn initial_size() -> (u32, u32) {
(820, 420)
}
fn init(_: &Handle<Self::Msg>) {}
fn update(model: Self::Model, _: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
model
}
fn view(_: &Self::Model) -> View<Self::Msg> {
let palette = WawaMarkPalette::default();
let frame = |side: f32| -> View<()> {
View::new(Style {
size: Size {
width: length(side),
height: length(side),
},
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
..Default::default()
})
.children(vec![wawa_mark_view(&palette)])
};
let row = View::new(Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::SpaceEvenly),
gap: Size {
width: length(24.0_f32),
height: length(0.0_f32),
},
..Default::default()
})
.children(vec![frame(72.0), frame(160.0), frame(288.0)]);
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
padding: Rect {
left: length(32.0_f32),
right: length(32.0_f32),
top: length(32.0_f32),
bottom: length(32.0_f32),
},
..Default::default()
})
// Fondo grafito neutro para que el rombo destaque sin competir.
.fill(Color::from_rgba8(18, 18, 22, 255))
.children(vec![row])
}
}
fn main() {
llimphi_ui::run::<Demo>();
}
+306
View File
@@ -0,0 +1,306 @@
//! `llimphi-widget-wawa-mark` — sello vectorial del SO wawa.
//!
//! ## Spec (revisión 2026-05-29)
//!
//! Identidad nominal **implícita**: el rombo de fondo lleva la paleta
//! oficial (Azul Índigo / Púrpura Profundo) y los trazos blancos forman
//! las letras **"WA"** pero geométricamente — no son tipografía, son
//! aristas internas que rebotan en los mismos 45° del rombo, así dan
//! sensación de facetas talladas dentro del diamante.
//!
//! ### Composición
//!
//! 1. **Rombo de fondo** — degradado vertical inmaculado, sin sutura
//! visible: índigo arriba, púrpura abajo. El degradado lineal cubre
//! toda la altura del rombo (no sólo la mitad), de modo que el cambio
//! de tono es continuo.
//! 2. **Trazo "WA"** — un único `BezPath` con dos subtrazos:
//! - **W** (izquierda): zigzag de 4 segmentos, todos a 45° (matching
//! las aristas del rombo). Picos en la sutura azul/púrpura
//! (y = 0.50), valles en y = 0.60. Cinco vértices, cuatro segmentos.
//! - **A** (derecha): triángulo abierto formado por dos legs a 45°
//! + un crossbar horizontal a mitad de altura. Tres segmentos.
//! Las strokes diagonales (6 de las 7) son paralelas a las aristas
//! del rombo, por eso "leen" como filos cortados del diamante en vez
//! de letras pintadas encima.
//! 3. **Merkle Core** — punto luminoso con halo en el pico central de
//! la W (sobre la sutura, donde azul y púrpura se encuentran). Es el
//! nodo raíz que amarra el sistema.
//!
//! ### Geometría (en coords normalizadas `[0, 1] × [0, 1]` del rect)
//!
//! ```text
//! Top
//! ◇
//! / \
//! / \ ← azul índigo
//! / \
//! P0 P2★ P4 A1
//! ●─. ● .─● ●─. .─● ← y = 0.50 (sutura)
//! ╲ ╱ ╲ ╱ ╲ ╱
//! ╳ ╳ ╲────╱ ← crossbar A (y=0.55)
//! ╱ ╲ ╱ ╲ ╱ ╲
//! ●─' ● '─● ●─' '─● ← y = 0.60 (valles/pies)
//! P1 P3 A0 A2
//! ↑
//! gap entre W y A
//! Left ◇─────────────────────────────────◇ Right
//! (sutura, y = 0.50)
//! /
//! /
//! / ← púrpura profundo
//! /
//! ◇
//! Bottom
//! ```
//!
//! Las strokes diagonales todas a slope ±1, igual que las aristas del
//! rombo. El crossbar de la A es la única horizontal — concesión mínima
//! a la legibilidad de la letra, queda subordinado al patrón diamante.
//!
//! ## Uso
//!
//! ```ignore
//! use llimphi_widget_wawa_mark::{wawa_mark_view, WawaMarkPalette};
//!
//! // En un view:
//! View::new(Style { size: Size { width: length(128.0), height: length(128.0) }, ..Default::default() })
//! .children(vec![wawa_mark_view(&WawaMarkPalette::default())])
//! ```
//!
//! El widget rellena el rect del padre — pasarle un tamaño cuadrado para
//! que el rombo no se distorsione (lo respeta igual, pero queda mejor
//! cuadrado).
#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::{
prelude::{percent, Size, Style},
Position,
};
use llimphi_ui::llimphi_raster::kurbo::{Affine, BezPath, Circle, Point, Stroke};
use llimphi_ui::llimphi_raster::peniko::{color::AlphaColor, Color, Fill, Gradient, Mix};
use llimphi_ui::View;
/// Paleta del sello. Los defaults corresponden a la especificación
/// oficial (Azul Índigo + Púrpura Profundo + trazo blanco + acento
/// cyan-eléctrico para el Merkle Core).
#[derive(Debug, Clone, Copy)]
pub struct WawaMarkPalette {
/// Color superior del degradado (tope del rombo).
pub indigo: Color,
/// Color inferior del degradado (base del rombo).
pub purple: Color,
/// Color del trazo de la 'W' implícita.
pub stroke: Color,
/// Color del Merkle Core (nodo central). Halo se deriva con alpha
/// reducido del mismo color.
pub core: Color,
}
impl Default for WawaMarkPalette {
fn default() -> Self {
Self {
// Azul Índigo profundo — saturación alta, valor medio.
indigo: Color::from_rgba8(46, 56, 168, 255),
// Púrpura Profundo — más violeta, valor menor.
purple: Color::from_rgba8(76, 32, 122, 255),
// Blanco con leve calidez para no quemar contra el púrpura.
stroke: Color::from_rgba8(240, 240, 248, 255),
// Cyan eléctrico — el "color del cursor del osciloscopio".
core: Color::from_rgba8(120, 240, 255, 255),
}
}
}
/// Construye el `View` que pinta el sello dentro del rect del padre.
/// El widget se posiciona absolute al 100% del padre — pasarle un
/// contenedor con tamaño cuadrado para evitar distorsión.
pub fn wawa_mark_view<Msg: Clone + 'static>(palette: &WawaMarkPalette) -> View<Msg> {
let p = *palette;
View::new(Style {
position: Position::Absolute,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
..Default::default()
})
.paint_with(move |scene, _ts, rect| paint_mark(scene, rect, &p))
}
/// Pintor puro — recibe el `Scene`, el rect de pintura y la paleta.
/// Expuesto por separado para que apps avanzadas puedan reusar el
/// painter dentro de canvas custom (splash de boot, about box, etc.)
/// sin pasar por la fachada `View`.
pub fn paint_mark(
scene: &mut llimphi_ui::llimphi_raster::vello::Scene,
rect: llimphi_ui::PaintRect,
palette: &WawaMarkPalette,
) {
// Encajamos el rombo en el menor de los lados del rect, centrado.
// Así el sello mantiene su proporción incluso si el rect no es
// cuadrado (pero degrada gracilmente).
let side = rect.w.min(rect.h) as f64;
let cx = rect.x as f64 + rect.w as f64 * 0.5;
let cy = rect.y as f64 + rect.h as f64 * 0.5;
let half = side * 0.5;
// === 1) Rombo de fondo con degradado vertical ===
//
// Construimos el rombo como BezPath (4 segmentos rectos) en coords
// absolutas. El degradado lineal va de (cx, top) a (cx, bot) — toda
// la altura del rombo — para que el cambio de tono sea continuo y
// sin sutura visible.
let top = Point::new(cx, cy - half);
let right = Point::new(cx + half, cy);
let bot = Point::new(cx, cy + half);
let left = Point::new(cx - half, cy);
let mut rhombus = BezPath::new();
rhombus.move_to(top);
rhombus.line_to(right);
rhombus.line_to(bot);
rhombus.line_to(left);
rhombus.close_path();
let gradient = Gradient::new_linear(top, bot)
.with_stops([palette.indigo, palette.purple].as_slice());
scene.fill(Fill::NonZero, Affine::IDENTITY, &gradient, None, &rhombus);
// === 2) "WA" implícita ===
//
// Coords en porcentaje del rombo (origen = esquina top-left del bbox
// del rombo = (cx-half, cy-half), unidad = side). Toda stroke diagonal
// tiene |dy/dx| = 1 (paralela a las aristas del rombo) — por eso lee
// como faceta del diamante en vez de letra dibujada encima.
let coord = |fx: f64, fy: f64| -> Point {
Point::new(
cx - half + fx * side,
cy - half + fy * side,
)
};
// Unidad de escala: span vertical de las letras. dx==dy en cada leg
// hace que las strokes corran a 45° exactos (mismo ángulo que las
// aristas del rombo). Probado para que WA quede inscrita con holgura
// en el rombo a cualquier escala — al achicar (32px) sigue legible,
// al ampliar (300px) no se ve disperso.
let unit: f64 = 0.10;
// Línea de picos en la sutura azul/púrpura.
let top_y = 0.50;
// Línea de valles/pies en el cuadrante púrpura inferior.
let bot_y = top_y + unit;
// ---- W (zigzag de 4 segmentos) ----
// Centramos la composición WA: span total ≈ 0.61 (W 0.36 + gap 0.03
// + A 0.18 + holgura). Empezamos en x = 0.19 para que el centro
// óptico de WA caiga cerca de x = 0.50.
let w_left = 0.20;
let p0 = coord(w_left + 0.0 * unit, top_y);
let p1 = coord(w_left + 1.0 * unit, bot_y);
let p2 = coord(w_left + 2.0 * unit, top_y);
let p3 = coord(w_left + 3.0 * unit, bot_y);
let p4 = coord(w_left + 4.0 * unit, top_y);
// ---- A (legs + crossbar) ----
// Gap entre W y A — apenas un respiro para que no se confundan en
// un solo zigzag.
let gap = 0.04;
let a_left = w_left + 4.0 * unit + gap;
let a0 = coord(a_left + 0.0 * unit, bot_y);
let a1 = coord(a_left + 1.0 * unit, top_y);
let a2 = coord(a_left + 2.0 * unit, bot_y);
// Crossbar a mitad de altura, en el tercio interno de cada leg para
// que no toque las puntas (queda más A que H).
let cross_y = (top_y + bot_y) * 0.5 + 0.005; // un toque debajo del medio óptico
let c_offset = 0.30 * unit;
let cb0 = coord(a_left + 0.0 * unit + c_offset, cross_y);
let cb1 = coord(a_left + 2.0 * unit - c_offset, cross_y);
// Un único BezPath con cuatro subtrazos (move_to abre subtrazo nuevo).
let mut wa = BezPath::new();
// W
wa.move_to(p0);
wa.line_to(p1);
wa.line_to(p2);
wa.line_to(p3);
wa.line_to(p4);
// A — legs.
wa.move_to(a0);
wa.line_to(a1);
wa.line_to(a2);
// A — crossbar (horizontal, único trazo no diagonal).
wa.move_to(cb0);
wa.line_to(cb1);
// Espesor escalable: ~2.0% del lado del rombo. Levemente más fino
// que la W sola, porque ahora hay 7 strokes en vez de 4 y conviene
// bajar densidad.
let stroke_w = (side * 0.020).max(1.0);
let stroke = Stroke::new(stroke_w)
.with_join(llimphi_ui::llimphi_raster::kurbo::Join::Miter)
.with_caps(llimphi_ui::llimphi_raster::kurbo::Cap::Butt);
scene.stroke(
&stroke,
Affine::IDENTITY,
palette.stroke,
None,
&wa,
);
// === 3) Merkle Core ===
//
// Sobre P2 — pico central de la W, en la sutura exacta entre azul y
// púrpura. Halo amplio semi-transparente + núcleo opaco compacto
// dan sensación de glow sin blur real.
let core_r = (side * 0.018).max(1.2);
let halo_r = core_r * 2.6;
let halo_color = with_alpha(palette.core, 0.30);
scene.push_layer(Mix::Normal, 1.0, Affine::IDENTITY, &Circle::new(p2, halo_r));
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
halo_color,
None,
&Circle::new(p2, halo_r),
);
scene.pop_layer();
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
palette.core,
None,
&Circle::new(p2, core_r),
);
}
/// Devuelve `color` con su alpha multiplicado por `mult` (no reemplazado).
/// Mantenemos la cromaticidad intacta.
fn with_alpha(color: Color, mult: f32) -> Color {
let [r, g, b, a] = color.components;
AlphaColor::new([r, g, b, a * mult])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_palette_has_distinct_indigo_and_purple() {
let p = WawaMarkPalette::default();
assert_ne!(p.indigo.components, p.purple.components);
assert_ne!(p.stroke.components, p.core.components);
}
#[test]
fn with_alpha_multiplies_not_replaces() {
let c = Color::from_rgba8(100, 100, 100, 255);
let halved = with_alpha(c, 0.5);
assert!((halved.components[3] - 0.5).abs() < 1e-3);
// RGB intactos.
assert!((halved.components[0] - c.components[0]).abs() < 1e-3);
}
}