gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main llimphi-wire-escena/src/pincel.rs Rust · 308 líneas · 11.0 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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}