tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}