tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
1//! `llimphi-widget-splash` — splash de arranque tawasuyu.
2//!
3//! Identidad visual del SO al boot: cuatro cuadrantes ordenados como
4//! una cruz andina, cada uno con su nombre quechua y color simbólico,
5//! que **entran en secuencia** con un tween de fade+escala.
6//!
7//! Los cuadrantes (en orden de entrada):
8//! 1. `unanchay` — PERCIBIR — cyan (índigo claro)
9//! 2. `yachay` — CONOCER — verde aurora
10//! 3. `ruway` — HACER — naranja sunset
11//! 4. `ukupacha` — RAÍZ — púrpura profundo
12//!
13//! Cada cuadrante hace fade-in + slight scale-up, con un offset de
14//! `motion::NORMAL / 2` entre uno y el siguiente. La app pasa un
15//! `Instant` de inicio y el splash calcula las fases relativas — no
16//! requiere ningún tween del modelo.
17//!
18//! Cuando el splash termina (todos visibles), la app puede:
19//! - mantenerlo unos segundos más como pantalla de carga,
20//! - hacer un fade-out completo cuando el sistema esté listo,
21//! - o reemplazarlo por la UI principal.
22
23#![forbid(unsafe_code)]
24
25use std::time::Instant;
26
27use llimphi_ui::llimphi_layout::taffy::{
28 prelude::{length, percent, FlexDirection, Size, Style},
29 AlignItems, JustifyContent, Rect,
30};
31use llimphi_ui::llimphi_raster::peniko::Color;
32use llimphi_ui::llimphi_text::Alignment;
33use llimphi_ui::View;
34use llimphi_motion::motion;
35
36/// Datos de un cuadrante: nombre quechua, glosa breve y color.
37#[derive(Debug, Clone, Copy)]
38pub struct Quadrant {
39 pub name: &'static str,
40 pub gloss: &'static str,
41 pub color: Color,
42}
43
44/// Los cuatro cuadrantes canónicos, en orden de entrada al splash.
45pub fn quadrants() -> [Quadrant; 4] {
46 [
47 Quadrant {
48 name: "unanchay",
49 gloss: "PERCIBIR",
50 color: Color::from_rgba8(110, 160, 230, 255),
51 },
52 Quadrant {
53 name: "yachay",
54 gloss: "CONOCER",
55 color: Color::from_rgba8(110, 220, 180, 255),
56 },
57 Quadrant {
58 name: "ruway",
59 gloss: "HACER",
60 color: Color::from_rgba8(232, 160, 90, 255),
61 },
62 Quadrant {
63 name: "ukupacha",
64 gloss: "RAÍZ",
65 color: Color::from_rgba8(160, 110, 220, 255),
66 },
67 ]
68}
69
70/// Construye el splash. `started_at` es el `Instant` de origen — el
71/// splash calcula las fases relativas. La app puede llamar `animate(handle,
72/// motion::SLOW * 3, …)` para forzar repaints durante la animación.
73///
74/// `bg`: color de fondo (típico: `theme.bg_app`).
75/// `fg_text`: color del título/glosa.
76pub fn splash_view<Msg: Clone + 'static>(
77 started_at: Instant,
78 bg: Color,
79 fg_text: Color,
80) -> View<Msg> {
81 let elapsed = started_at.elapsed().as_secs_f32();
82 let stagger = motion::NORMAL.as_secs_f32() * 0.45;
83 let per_quad = motion::NORMAL.as_secs_f32();
84 let quads = quadrants();
85
86 let cells: Vec<View<Msg>> = quads
87 .iter()
88 .enumerate()
89 .map(|(i, q)| {
90 let local_t = ((elapsed - i as f32 * stagger) / per_quad).clamp(0.0, 1.0);
91 let eased = motion::ease_out_cubic(local_t);
92 quadrant_cell(q, eased, fg_text)
93 })
94 .collect();
95
96 // 2×2 grid: row 0 = unanchay + yachay; row 1 = ruway + ukupacha.
97 let row = |a: View<Msg>, b: View<Msg>| -> View<Msg> {
98 View::new(Style {
99 flex_direction: FlexDirection::Row,
100 size: Size {
101 width: percent(1.0_f32),
102 height: percent(0.5_f32),
103 },
104 gap: Size {
105 width: length(12.0_f32),
106 height: length(0.0_f32),
107 },
108 ..Default::default()
109 })
110 .children(vec![a, b])
111 };
112 let mut iter = cells.into_iter();
113 let r0 = row(iter.next().unwrap(), iter.next().unwrap());
114 let r1 = row(iter.next().unwrap(), iter.next().unwrap());
115
116 let grid = View::new(Style {
117 flex_direction: FlexDirection::Column,
118 size: Size {
119 width: length(420.0_f32),
120 height: length(280.0_f32),
121 },
122 gap: Size {
123 width: length(0.0_f32),
124 height: length(12.0_f32),
125 },
126 flex_shrink: 0.0,
127 ..Default::default()
128 })
129 .children(vec![r0, r1]);
130
131 // Título "tawasuyu" debajo, también fade-in pero al final.
132 let title_t = ((elapsed - 4.0 * stagger) / per_quad).clamp(0.0, 1.0);
133 let title_alpha = motion::ease_out_cubic(title_t);
134 let title = View::new(Style {
135 size: Size {
136 width: length(420.0_f32),
137 height: length(32.0_f32),
138 },
139 align_items: Some(AlignItems::Center),
140 justify_content: Some(JustifyContent::Center),
141 flex_shrink: 0.0,
142 ..Default::default()
143 })
144 .text_aligned("tawasuyu", 22.0, fg_text, Alignment::Center)
145 .alpha(title_alpha);
146
147 View::new(Style {
148 flex_direction: FlexDirection::Column,
149 size: Size {
150 width: percent(1.0_f32),
151 height: percent(1.0_f32),
152 },
153 align_items: Some(AlignItems::Center),
154 justify_content: Some(JustifyContent::Center),
155 gap: Size {
156 width: length(0.0_f32),
157 height: length(28.0_f32),
158 },
159 padding: Rect {
160 left: length(0.0_f32),
161 right: length(0.0_f32),
162 top: length(0.0_f32),
163 bottom: length(0.0_f32),
164 },
165 ..Default::default()
166 })
167 .fill(bg)
168 .children(vec![grid, title])
169}
170
171fn quadrant_cell<Msg: Clone + 'static>(
172 quad: &Quadrant,
173 progress: f32,
174 fg_text: Color,
175) -> View<Msg> {
176 // El cuadrante "entra" con fade y un leve drift desde abajo (10px).
177 // El drift lo representamos con un padding-top que tiende a cero;
178 // como llimphi no expone translate por nodo (sólo position absolute),
179 // metemos el contenido en un wrapper con padding decreciente.
180 let drift = (1.0 - progress) * 10.0;
181
182 let name = View::new(Style {
183 size: Size {
184 width: percent(1.0_f32),
185 height: length(28.0_f32),
186 },
187 flex_shrink: 0.0,
188 ..Default::default()
189 })
190 .text_aligned(quad.name, 16.0, fg_text, Alignment::Center);
191
192 let gloss = View::new(Style {
193 size: Size {
194 width: percent(1.0_f32),
195 height: length(16.0_f32),
196 },
197 flex_shrink: 0.0,
198 ..Default::default()
199 })
200 .text_aligned(quad.gloss, 10.0, quad.color, Alignment::Center);
201
202 let inner = View::new(Style {
203 flex_direction: FlexDirection::Column,
204 size: Size {
205 width: percent(1.0_f32),
206 height: percent(1.0_f32),
207 },
208 align_items: Some(AlignItems::Center),
209 justify_content: Some(JustifyContent::Center),
210 gap: Size {
211 width: length(0.0_f32),
212 height: length(6.0_f32),
213 },
214 padding: Rect {
215 left: length(0.0_f32),
216 right: length(0.0_f32),
217 top: length(drift),
218 bottom: length(0.0_f32),
219 },
220 ..Default::default()
221 })
222 .children(vec![name, gloss]);
223
224 // Fondo del cuadrante con gradient vertical en el color semántico:
225 // alpha 50 arriba → alpha 12 abajo. Da volumen al cuadrante (más
226 // intenso cerca del accent strip del top) y un efecto "halo descendente"
227 // que ayuda a leer la cruz andina como cuatro luces que emergen del
228 // centro. Antes: alpha 30 uniforme.
229 let border = with_alpha8(quad.color, 90);
230 let bg_top = with_alpha8(quad.color, 50);
231 let bg_bot = with_alpha8(quad.color, 12);
232
233 let cell_radius = llimphi_theme::radius::MD;
234
235 View::new(Style {
236 flex_direction: FlexDirection::Column,
237 size: Size {
238 width: percent(1.0_f32),
239 height: percent(1.0_f32),
240 },
241 flex_grow: 1.0,
242 ..Default::default()
243 })
244 .paint_with(move |scene, _ts, rect| {
245 use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, RoundedRect};
246 use llimphi_ui::llimphi_raster::peniko::{Fill, Gradient};
247
248 if rect.w <= 0.0 || rect.h <= 0.0 {
249 return;
250 }
251 let x0 = rect.x as f64;
252 let y0 = rect.y as f64;
253 let x1 = (rect.x + rect.w) as f64;
254 let y1 = (rect.y + rect.h) as f64;
255 let rr = RoundedRect::new(x0, y0, x1, y1, cell_radius);
256 let gradient = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y1))
257 .with_stops([bg_top, bg_bot].as_slice());
258 scene.fill(Fill::NonZero, Affine::IDENTITY, &gradient, None, &rr);
259 })
260 .radius(cell_radius)
261 .clip(true)
262 .alpha(progress)
263 .children(vec![
264 // Línea accent superior — 2px del color del cuadrante a alta
265 // intensidad, ancla del gradiente que cae.
266 View::new(Style {
267 size: Size {
268 width: percent(1.0_f32),
269 height: length(2.0_f32),
270 },
271 flex_shrink: 0.0,
272 ..Default::default()
273 })
274 .fill(border),
275 inner,
276 ])
277}
278
279fn with_alpha8(c: Color, a: u8) -> Color {
280 let [r, g, b, _] = c.components;
281 use llimphi_ui::llimphi_raster::peniko::color::AlphaColor;
282 AlphaColor::new([r, g, b, a as f32 / 255.0])
283}