tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1// =============================================================================
2// pincel :: el color, el degradé y la imagen que NO viaja
3// -----------------------------------------------------------------------------
4// Los colores viajan en RGBA8, no en los cuatro f32 de `peniko::Color`. Es la
5// misma convención que ya usa `llimphi-wire-view` (`type Rgba = [u8; 4]`), y la
6// pérdida es nominal: el destino es un framebuffer de 8 bits por canal, así que
7// la precisión extra se tira en el último paso de todas formas. Son 4 bytes en
8// vez de 16, por orden, en una escena que puede tener cientos.
9//
10// La imagen es el caso interesante y la única decisión de fondo de este módulo.
11// Mandar los píxeles por el IR sería exactamente lo que W4 vino a evitar: 768 KB
12// por fotograma es el número de la «jaula pura» que el plan descarta. Así que la
13// orden lleva sólo una HUELLA del blob, y el reproductor resuelve contra su
14// propio registro. La consecuencia honesta: transferir el blob la primera vez es
15// un canal aparte, fuera del IR por fotograma — un `sys_` del kernel o un
16// objeto del DAG por hash, que es como wawa mueve todo lo demás.
17// =============================================================================
18
19use alloc::vec::Vec;
20
21use peniko::color::{DynamicColor, Srgb};
22use peniko::{
23 Brush, Color, ColorStop, ColorStops, Extend, Gradient, GradientKind, ImageBrush,
24 LinearGradientPosition, RadialGradientPosition, SweepGradientPosition,
25};
26use serde::{Deserialize, Serialize};
27
28/// Color en el wire: RGBA de 8 bits, igual que `llimphi-wire-view`.
29pub type Rgba = [u8; 4];
30
31/// Empaqueta un color de peniko.
32pub fn color_a_wire(c: Color) -> Rgba {
33 let r = c.to_rgba8();
34 [r.r, r.g, r.b, r.a]
35}
36
37/// Reconstruye un color de peniko.
38pub fn wire_a_color(w: Rgba) -> Color {
39 Color::from_rgba8(w[0], w[1], w[2], w[3])
40}
41
42/// Modo de extensión de un degradé. Espeja `peniko::Extend`.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub enum ExtiendeWire {
45 #[default]
46 Pad,
47 Repetir,
48 Reflejar,
49}
50
51impl ExtiendeWire {
52 fn de(e: Extend) -> Self {
53 match e {
54 Extend::Pad => Self::Pad,
55 Extend::Repeat => Self::Repetir,
56 Extend::Reflect => Self::Reflejar,
57 }
58 }
59
60 fn a_peniko(self) -> Extend {
61 match self {
62 Self::Pad => Extend::Pad,
63 Self::Repetir => Extend::Repeat,
64 Self::Reflejar => Extend::Reflect,
65 }
66 }
67}
68
69/// Una parada de color del degradé.
70#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
71pub struct ParadaWire {
72 /// Posición normalizada, `0.0..=1.0`.
73 pub offset: f32,
74 pub color: Rgba,
75}
76
77/// La geometría del degradé. Espeja `peniko::GradientKind` en f32.
78#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
79pub enum ClaseDegradeWire {
80 Lineal {
81 inicio: [f32; 2],
82 fin: [f32; 2],
83 },
84 Radial {
85 centro_inicio: [f32; 2],
86 radio_inicio: f32,
87 centro_fin: [f32; 2],
88 radio_fin: f32,
89 },
90 Barrido {
91 centro: [f32; 2],
92 angulo_inicio: f32,
93 angulo_fin: f32,
94 },
95}
96
97/// Un pincel listo para viajar.
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99pub enum PincelWire {
100 /// El 95% de los casos en una UI.
101 Solido(Rgba),
102 /// Degradé. **Se pierde el espacio de interpolación**: peniko permite
103 /// interpolar en Oklab, Lch y compañía, y el wire asume sRGB. Es la misma
104 /// decisión que tomó el color RGBA8 —la UI de Llimphi no usa esos espacios
105 /// hoy— y está anotada acá para que el día que se usen no sea un misterio.
106 Degrade {
107 clase: ClaseDegradeWire,
108 extiende: ExtiendeWire,
109 paradas: Vec<ParadaWire>,
110 },
111 /// Imagen: sólo la huella del blob (ver el encabezado del módulo).
112 Imagen { huella: u64 },
113}
114
115impl PincelWire {
116 /// Empaqueta un pincel de peniko.
117 pub fn de(pincel: &Brush) -> Self {
118 match pincel {
119 Brush::Solid(c) => Self::Solido(color_a_wire(*c)),
120 Brush::Gradient(g) => Self::Degrade {
121 clase: clase_a_wire(&g.kind),
122 extiende: ExtiendeWire::de(g.extend),
123 paradas: g
124 .stops
125 .iter()
126 .map(|s| ParadaWire {
127 offset: s.offset,
128 color: color_a_wire(s.color.to_alpha_color::<Srgb>()),
129 })
130 .collect(),
131 },
132 Brush::Image(img) => Self::Imagen {
133 huella: huella_imagen(img),
134 },
135 }
136 }
137
138 /// Reconstruye el pincel del lado nativo.
139 ///
140 /// Un pincel de imagen no se puede reconstruir sin el registro de blobs, así
141 /// que devuelve `None` y el reproductor decide qué hacer — que es mejor que
142 /// inventar un color y pintar algo que nadie pidió.
143 pub fn a_peniko(&self) -> Option<Brush> {
144 match self {
145 Self::Solido(c) => Some(Brush::Solid(wire_a_color(*c))),
146 Self::Degrade {
147 clase,
148 extiende,
149 paradas,
150 } => {
151 let mut stops = ColorStops::new();
152 for p in paradas {
153 stops.push(ColorStop {
154 offset: p.offset,
155 color: DynamicColor::from_alpha_color(wire_a_color(p.color)),
156 });
157 }
158 Some(Brush::Gradient(Gradient {
159 kind: wire_a_clase(clase),
160 extend: extiende.a_peniko(),
161 stops,
162 ..Default::default()
163 }))
164 }
165 Self::Imagen { .. } => None,
166 }
167 }
168}
169
170fn clase_a_wire(k: &GradientKind) -> ClaseDegradeWire {
171 match k {
172 GradientKind::Linear(l) => ClaseDegradeWire::Lineal {
173 inicio: [l.start.x as f32, l.start.y as f32],
174 fin: [l.end.x as f32, l.end.y as f32],
175 },
176 GradientKind::Radial(r) => ClaseDegradeWire::Radial {
177 centro_inicio: [r.start_center.x as f32, r.start_center.y as f32],
178 radio_inicio: r.start_radius,
179 centro_fin: [r.end_center.x as f32, r.end_center.y as f32],
180 radio_fin: r.end_radius,
181 },
182 GradientKind::Sweep(s) => ClaseDegradeWire::Barrido {
183 centro: [s.center.x as f32, s.center.y as f32],
184 angulo_inicio: s.start_angle,
185 angulo_fin: s.end_angle,
186 },
187 }
188}
189
190fn wire_a_clase(c: &ClaseDegradeWire) -> GradientKind {
191 match c {
192 ClaseDegradeWire::Lineal { inicio, fin } => GradientKind::Linear(LinearGradientPosition {
193 start: kurbo::Point::new(inicio[0] as f64, inicio[1] as f64),
194 end: kurbo::Point::new(fin[0] as f64, fin[1] as f64),
195 }),
196 ClaseDegradeWire::Radial {
197 centro_inicio,
198 radio_inicio,
199 centro_fin,
200 radio_fin,
201 } => GradientKind::Radial(RadialGradientPosition {
202 start_center: kurbo::Point::new(centro_inicio[0] as f64, centro_inicio[1] as f64),
203 start_radius: *radio_inicio,
204 end_center: kurbo::Point::new(centro_fin[0] as f64, centro_fin[1] as f64),
205 end_radius: *radio_fin,
206 }),
207 ClaseDegradeWire::Barrido {
208 centro,
209 angulo_inicio,
210 angulo_fin,
211 } => GradientKind::Sweep(SweepGradientPosition {
212 center: kurbo::Point::new(centro[0] as f64, centro[1] as f64),
213 start_angle: *angulo_inicio,
214 end_angle: *angulo_fin,
215 }),
216 }
217}
218
219/// **Los píxeles de una imagen, para que puedan cruzar.**
220///
221/// La orden [`crate::OrdenWire::Imagen`] viaja **por huella**: la escena dice
222/// «acá va la imagen tal» y no arrastra el buffer, porque la misma miniatura
223/// aparece en veinte órdenes y mandarla veinte veces sería absurdo. El precio
224/// es que el que reproduce necesita un **registro** donde buscar esa huella —
225/// y hasta hoy ese registro no lo llenaba nadie: el reproductor contaba la
226/// imagen en `imagenes_sin_blob` y NO la dibujaba. En el explorador enjaulado
227/// eso es toda miniatura y toda previa **en negro**, que fue el reporte.
228///
229/// Esto es lo que faltaba: el blob viaja **una vez** junto a la escena que lo
230/// estrena, y el anfitrión lo guarda por huella para los cuadros siguientes.
231#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
232pub struct BlobImagen {
233 pub huella: u64,
234 pub ancho: u32,
235 pub alto: u32,
236 /// `peniko::ImageFormat` como número: `0` = RGBA8, `1` = BGRA8.
237 pub formato: u8,
238 /// `peniko::ImageAlphaType` como número: `0` = separado, `1` = premultiplicado.
239 pub alfa: u8,
240 pub datos: Vec<u8>,
241}
242
243/// El blob de una imagen, listo para viajar.
244pub fn blob_de(img: &ImageBrush) -> BlobImagen {
245 BlobImagen {
246 huella: huella_imagen(img),
247 ancho: img.image.width,
248 alto: img.image.height,
249 formato: img.image.format as u8,
250 alfa: img.image.alpha_type as u8,
251 datos: img.image.data.data().to_vec(),
252 }
253}
254
255/// Reconstruye el pincel de imagen del lado nativo.
256///
257/// El **muestreo va por defecto** y no viaja: es lo único del `ImageBrush` que
258/// no entra en la huella, así que mandarlo obligaría a decidir qué pasa cuando
259/// dos órdenes comparten huella y difieren en muestreo. Con el defecto, una
260/// miniatura se ve igual de los dos lados; el día que alguien necesite otro, el
261/// campo entra en la huella primero.
262pub fn imagen_de_blob(b: &BlobImagen) -> ImageBrush {
263 ImageBrush::new(peniko::ImageData {
264 data: peniko::Blob::from(b.datos.clone()),
265 format: if b.formato == 1 {
266 peniko::ImageFormat::Bgra8
267 } else {
268 peniko::ImageFormat::Rgba8
269 },
270 alpha_type: if b.alfa == 1 {
271 peniko::ImageAlphaType::AlphaPremultiplied
272 } else {
273 peniko::ImageAlphaType::Alpha
274 },
275 width: b.ancho,
276 height: b.alto,
277 })
278}
279
280/// Huella FNV-1a de 64 bits del contenido de una imagen.
281///
282/// FNV y no BLAKE3 a propósito: no es una huella criptográfica ni tiene que
283/// serlo —el registro de blobs vive del lado nativo y no acepta nada que no le
284/// hayan dado antes—, corre en el guest (donde cada byte de código pesa) y son
285/// veinte líneas sin dependencias. Entran las dimensiones y el formato además de
286/// los píxeles, para que dos imágenes con el mismo buffer pero distinta
287/// interpretación no colisionen.
288pub fn huella_imagen(img: &ImageBrush) -> u64 {
289 const BASE: u64 = 0xcbf2_9ce4_8422_2325;
290 const PRIMO: u64 = 0x0000_0100_0000_01b3;
291
292 let mut h = BASE;
293 let comer = |b: u8, h: &mut u64| {
294 *h ^= b as u64;
295 *h = h.wrapping_mul(PRIMO);
296 };
297 for b in img.image.width.to_le_bytes() {
298 comer(b, &mut h);
299 }
300 for b in img.image.height.to_le_bytes() {
301 comer(b, &mut h);
302 }
303 comer(img.image.format as u8, &mut h);
304 for b in img.image.data.data().iter() {
305 comer(*b, &mut h);
306 }
307 h
308}