tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1
llimphi-wire-escena/src/pincel.rs
Historia
Quién tocó qué
crudo
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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/// Huella FNV-1a de 64 bits del contenido de una imagen.
220///
221/// FNV y no BLAKE3 a propósito: no es una huella criptográfica ni tiene que
222/// serlo —el registro de blobs vive del lado nativo y no acepta nada que no le
223/// hayan dado antes—, corre en el guest (donde cada byte de código pesa) y son
224/// veinte líneas sin dependencias. Entran las dimensiones y el formato además de
225/// los píxeles, para que dos imágenes con el mismo buffer pero distinta
226/// interpretación no colisionen.
227pub fn huella_imagen(img: &ImageBrush) -> u64 {
228 const BASE: u64 = 0xcbf2_9ce4_8422_2325;
229 const PRIMO: u64 = 0x0000_0100_0000_01b3;
230
231 let mut h = BASE;
232 let comer = |b: u8, h: &mut u64| {
233 *h ^= b as u64;
234 *h = h.wrapping_mul(PRIMO);
235 };
236 for b in img.image.width.to_le_bytes() {
237 comer(b, &mut h);
238 }
239 for b in img.image.height.to_le_bytes() {
240 comer(b, &mut h);
241 }
242 comer(img.image.format as u8, &mut h);
243 for b in img.image.data.data().iter() {
244 comer(*b, &mut h);
245 }
246 h
247}