tawasuyu

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Definitions

⎇ main widgets/wawa-mark/src/lib.rs Rust · 314 lines · 11.9 KB History Who touched what raw

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! `llimphi-widget-wawa-mark` — sello vectorial del SO wawa.
2//!
3//! ## Spec (revisión 2026-05-29)
4//!
5//! Identidad nominal **implícita**: el rombo de fondo lleva la paleta
6//! oficial (Azul Índigo / Púrpura Profundo) y los trazos blancos forman
7//! las letras **"WA"** pero geométricamente — no son tipografía, son
8//! aristas internas que rebotan en los mismos 45° del rombo, así dan
9//! sensación de facetas talladas dentro del diamante.
10//!
11//! ### Composición
12//!
13//! 1. **Rombo de fondo** — degradado vertical inmaculado, sin sutura
14//!    visible: índigo arriba, púrpura abajo. El degradado lineal cubre
15//!    toda la altura del rombo (no sólo la mitad), de modo que el cambio
16//!    de tono es continuo.
17//! 2. **Trazo "WA"** — un único `BezPath` con dos subtrazos:
18//!    - **W** (izquierda): zigzag de 4 segmentos, todos a 45° (matching
19//!      las aristas del rombo). Picos en la sutura azul/púrpura
20//!      (y = 0.50), valles en y = 0.60. Cinco vértices, cuatro segmentos.
21//!    - **A** (derecha): triángulo abierto formado por dos legs a 45°
22//!      + un crossbar horizontal a mitad de altura. Tres segmentos.
23//!    Las strokes diagonales (6 de las 7) son paralelas a las aristas
24//!    del rombo, por eso "leen" como filos cortados del diamante en vez
25//!    de letras pintadas encima.
26//! 3. **Merkle Core** — punto luminoso con halo en el pico central de
27//!    la W (sobre la sutura, donde azul y púrpura se encuentran). Es el
28//!    nodo raíz que amarra el sistema.
29//!
30//! ### Geometría (en coords normalizadas `[0, 1] × [0, 1]` del rect)
31//!
32//! ```text
33//!                            Top
34//!                             ◇
35//!                            / \
36//!                           /   \              ← azul índigo
37//!                          /     \
38//!                P0    P2★    P4 A1
39//!                  ●─.   ●  .─●  ●─.  .─●     ← y = 0.50 (sutura)
40//!                     ╲ ╱ ╲ ╱     ╲      ╱
41//!                      ╳   ╳       ╲────╱     ← crossbar A (y=0.55)
42//!                     ╱ ╲ ╱ ╲     ╱      ╲
43//!                  ●─'   ●  '─●  ●─'    '─●  ← y = 0.60 (valles/pies)
44//!                P1    P3    A0    A2
45//!                          ↑
46//!                          gap entre W y A
47//!     Left  ◇─────────────────────────────────◇  Right
48//!                          (sutura, y = 0.50)
49//!                          /
50//!                         /
51//!                        /                     ← púrpura profundo
52//!                       /
53//!                      ◇
54//!                    Bottom
55//! ```
56//!
57//! Las strokes diagonales todas a slope ±1, igual que las aristas del
58//! rombo. El crossbar de la A es la única horizontal — concesión mínima
59//! a la legibilidad de la letra, queda subordinado al patrón diamante.
60//!
61//! ## Uso
62//!
63//! ```ignore
64//! use llimphi_widget_wawa_mark::{wawa_mark_view, WawaMarkPalette};
65//!
66//! // En un view:
67//! View::new(Style { size: Size { width: length(128.0), height: length(128.0) }, ..Default::default() })
68//!     .children(vec![wawa_mark_view(&WawaMarkPalette::default())])
69//! ```
70//!
71//! El widget rellena el rect del padre — pasarle un tamaño cuadrado para
72//! que el rombo no se distorsione (lo respeta igual, pero queda mejor
73//! cuadrado).
74
75#![forbid(unsafe_code)]
76
77
78// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
79// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
80// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
81#![no_std]
82
83extern crate alloc;
84
85use llimphi_compositor::llimphi_layout::taffy::{
86    prelude::{percent, Size, Style},
87    Position,
88};
89use llimphi_compositor::peniko::kurbo::{Affine, BezPath, Circle, Point, Stroke};
90use llimphi_compositor::peniko::{color::AlphaColor, Color, Fill, Gradient, Mix};
91use llimphi_compositor::View;
92
93/// Paleta del sello. Los defaults corresponden a la especificación
94/// oficial (Azul Índigo + Púrpura Profundo + trazo blanco + acento
95/// cyan-eléctrico para el Merkle Core).
96#[derive(Debug, Clone, Copy)]
97pub struct WawaMarkPalette {
98    /// Color superior del degradado (tope del rombo).
99    pub indigo: Color,
100    /// Color inferior del degradado (base del rombo).
101    pub purple: Color,
102    /// Color del trazo de la 'W' implícita.
103    pub stroke: Color,
104    /// Color del Merkle Core (nodo central). Halo se deriva con alpha
105    /// reducido del mismo color.
106    pub core: Color,
107}
108
109impl Default for WawaMarkPalette {
110    fn default() -> Self {
111        Self {
112            // Azul Índigo profundo — saturación alta, valor medio.
113            indigo: Color::from_rgba8(46, 56, 168, 255),
114            // Púrpura Profundo — más violeta, valor menor.
115            purple: Color::from_rgba8(76, 32, 122, 255),
116            // Blanco con leve calidez para no quemar contra el púrpura.
117            stroke: Color::from_rgba8(240, 240, 248, 255),
118            // Cyan eléctrico — el "color del cursor del osciloscopio".
119            core: Color::from_rgba8(120, 240, 255, 255),
120        }
121    }
122}
123
124/// Construye el `View` que pinta el sello dentro del rect del padre.
125/// El widget se posiciona absolute al 100% del padre — pasarle un
126/// contenedor con tamaño cuadrado para evitar distorsión.
127pub fn wawa_mark_view<Msg: Clone + 'static>(palette: &WawaMarkPalette) -> View<Msg> {
128    let p = *palette;
129    View::new(Style {
130        position: Position::Absolute,
131        size: Size {
132            width: percent(1.0_f32),
133            height: percent(1.0_f32),
134        },
135        ..Default::default()
136    })
137    .paint_with(move |scene, _ts, rect| paint_mark(scene, rect, &p))
138}
139
140/// Pintor puro — recibe el `Scene`, el rect de pintura y la paleta.
141/// Expuesto por separado para que apps avanzadas puedan reusar el
142/// painter dentro de canvas custom (splash de boot, about box, etc.)
143/// sin pasar por la fachada `View`.
144pub fn paint_mark(
145    scene: &mut llimphi_compositor::Lienzo,
146    rect: llimphi_compositor::PaintRect,
147    palette: &WawaMarkPalette,
148) {
149    // Encajamos el rombo en el menor de los lados del rect, centrado.
150    // Así el sello mantiene su proporción incluso si el rect no es
151    // cuadrado (pero degrada gracilmente).
152    let side = rect.w.min(rect.h) as f64;
153    let cx = rect.x as f64 + rect.w as f64 * 0.5;
154    let cy = rect.y as f64 + rect.h as f64 * 0.5;
155    let half = side * 0.5;
156
157    // === 1) Rombo de fondo con degradado vertical ===
158    //
159    // Construimos el rombo como BezPath (4 segmentos rectos) en coords
160    // absolutas. El degradado lineal va de (cx, top) a (cx, bot) — toda
161    // la altura del rombo — para que el cambio de tono sea continuo y
162    // sin sutura visible.
163    let top = Point::new(cx, cy - half);
164    let right = Point::new(cx + half, cy);
165    let bot = Point::new(cx, cy + half);
166    let left = Point::new(cx - half, cy);
167
168    let mut rhombus = BezPath::new();
169    rhombus.move_to(top);
170    rhombus.line_to(right);
171    rhombus.line_to(bot);
172    rhombus.line_to(left);
173    rhombus.close_path();
174
175    let gradient = Gradient::new_linear(top, bot)
176        .with_stops([palette.indigo, palette.purple].as_slice());
177
178    scene.fill(Fill::NonZero, Affine::IDENTITY, &gradient, None, &rhombus);
179
180    // === 2) "WA" implícita ===
181    //
182    // Coords en porcentaje del rombo (origen = esquina top-left del bbox
183    // del rombo = (cx-half, cy-half), unidad = side). Toda stroke diagonal
184    // tiene |dy/dx| = 1 (paralela a las aristas del rombo) — por eso lee
185    // como faceta del diamante en vez de letra dibujada encima.
186    let coord = |fx: f64, fy: f64| -> Point {
187        Point::new(
188            cx - half + fx * side,
189            cy - half + fy * side,
190        )
191    };
192
193    // Unidad de escala: span vertical de las letras. dx==dy en cada leg
194    // hace que las strokes corran a 45° exactos (mismo ángulo que las
195    // aristas del rombo). Probado para que WA quede inscrita con holgura
196    // en el rombo a cualquier escala — al achicar (32px) sigue legible,
197    // al ampliar (300px) no se ve disperso.
198    let unit: f64 = 0.10;
199    // Línea de picos en la sutura azul/púrpura.
200    let top_y = 0.50;
201    // Línea de valles/pies en el cuadrante púrpura inferior.
202    let bot_y = top_y + unit;
203
204    // ---- W (zigzag de 4 segmentos) ----
205    // Centramos la composición WA: span total ≈ 0.61 (W 0.36 + gap 0.03
206    // + A 0.18 + holgura). Empezamos en x = 0.19 para que el centro
207    // óptico de WA caiga cerca de x = 0.50.
208    let w_left = 0.20;
209    let p0 = coord(w_left + 0.0 * unit, top_y);
210    let p1 = coord(w_left + 1.0 * unit, bot_y);
211    let p2 = coord(w_left + 2.0 * unit, top_y);
212    let p3 = coord(w_left + 3.0 * unit, bot_y);
213    let p4 = coord(w_left + 4.0 * unit, top_y);
214
215    // ---- A (legs + crossbar) ----
216    // Gap entre W y A — apenas un respiro para que no se confundan en
217    // un solo zigzag.
218    let gap = 0.04;
219    let a_left = w_left + 4.0 * unit + gap;
220    let a0 = coord(a_left + 0.0 * unit, bot_y);
221    let a1 = coord(a_left + 1.0 * unit, top_y);
222    let a2 = coord(a_left + 2.0 * unit, bot_y);
223    // Crossbar a mitad de altura, en el tercio interno de cada leg para
224    // que no toque las puntas (queda más A que H).
225    let cross_y = (top_y + bot_y) * 0.5 + 0.005; // un toque debajo del medio óptico
226    let c_offset = 0.30 * unit;
227    let cb0 = coord(a_left + 0.0 * unit + c_offset, cross_y);
228    let cb1 = coord(a_left + 2.0 * unit - c_offset, cross_y);
229
230    // Un único BezPath con cuatro subtrazos (move_to abre subtrazo nuevo).
231    let mut wa = BezPath::new();
232    // W
233    wa.move_to(p0);
234    wa.line_to(p1);
235    wa.line_to(p2);
236    wa.line_to(p3);
237    wa.line_to(p4);
238    // A — legs.
239    wa.move_to(a0);
240    wa.line_to(a1);
241    wa.line_to(a2);
242    // A — crossbar (horizontal, único trazo no diagonal).
243    wa.move_to(cb0);
244    wa.line_to(cb1);
245
246    // Espesor escalable: ~2.0% del lado del rombo. Levemente más fino
247    // que la W sola, porque ahora hay 7 strokes en vez de 4 y conviene
248    // bajar densidad.
249    let stroke_w = (side * 0.020).max(1.0);
250    let stroke = Stroke::new(stroke_w)
251        .with_join(llimphi_compositor::peniko::kurbo::Join::Miter)
252        .with_caps(llimphi_compositor::peniko::kurbo::Cap::Butt);
253
254    scene.stroke(
255        &stroke,
256        Affine::IDENTITY,
257        palette.stroke,
258        None,
259        &wa,
260    );
261
262    // === 3) Merkle Core ===
263    //
264    // Sobre P2 — pico central de la W, en la sutura exacta entre azul y
265    // púrpura. Halo amplio semi-transparente + núcleo opaco compacto
266    // dan sensación de glow sin blur real.
267    let core_r = (side * 0.018).max(1.2);
268    let halo_r = core_r * 2.6;
269    let halo_color = with_alpha(palette.core, 0.30);
270    scene.push_layer(Fill::NonZero, Mix::Normal, 1.0, Affine::IDENTITY, &Circle::new(p2, halo_r));
271    scene.fill(
272        Fill::NonZero,
273        Affine::IDENTITY,
274        halo_color,
275        None,
276        &Circle::new(p2, halo_r),
277    );
278    scene.pop_layer();
279    scene.fill(
280        Fill::NonZero,
281        Affine::IDENTITY,
282        palette.core,
283        None,
284        &Circle::new(p2, core_r),
285    );
286}
287
288/// Devuelve `color` con su alpha multiplicado por `mult` (no reemplazado).
289/// Mantenemos la cromaticidad intacta.
290fn with_alpha(color: Color, mult: f32) -> Color {
291    let [r, g, b, a] = color.components;
292    AlphaColor::new([r, g, b, a * mult])
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn default_palette_has_distinct_indigo_and_purple() {
301        let p = WawaMarkPalette::default();
302        assert_ne!(p.indigo.components, p.purple.components);
303        assert_ne!(p.stroke.components, p.core.components);
304    }
305
306    #[test]
307    fn with_alpha_multiplies_not_replaces() {
308        let c = Color::from_rgba8(100, 100, 100, 255);
309        let halved = with_alpha(c, 0.5);
310        assert!((halved.components[3] - 0.5).abs() < 1e-3);
311        // RGB intactos.
312        assert!((halved.components[0] - c.components[0]).abs() < 1e-3);
313    }
314}