gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ 5ae7c737031521b9a873f3d7cff36261fbf7ae06 widgets/splash/src/lib.rs Rust · 283 lines · 9.0 KB History Who touched what raw

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

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}