gioser

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Definiciones

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-wire-escena/src/pincel.rs Rust · 247 líneas · 8.6 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · 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/// 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}