// ============================================================================= // pincel :: el color, el degradé y la imagen que NO viaja // ----------------------------------------------------------------------------- // Los colores viajan en RGBA8, no en los cuatro f32 de `peniko::Color`. Es la // misma convención que ya usa `llimphi-wire-view` (`type Rgba = [u8; 4]`), y la // pérdida es nominal: el destino es un framebuffer de 8 bits por canal, así que // la precisión extra se tira en el último paso de todas formas. Son 4 bytes en // vez de 16, por orden, en una escena que puede tener cientos. // // La imagen es el caso interesante y la única decisión de fondo de este módulo. // Mandar los píxeles por el IR sería exactamente lo que W4 vino a evitar: 768 KB // por fotograma es el número de la «jaula pura» que el plan descarta. Así que la // orden lleva sólo una HUELLA del blob, y el reproductor resuelve contra su // propio registro. La consecuencia honesta: transferir el blob la primera vez es // un canal aparte, fuera del IR por fotograma — un `sys_` del kernel o un // objeto del DAG por hash, que es como wawa mueve todo lo demás. // ============================================================================= use alloc::vec::Vec; use peniko::color::{DynamicColor, Srgb}; use peniko::{ Brush, Color, ColorStop, ColorStops, Extend, Gradient, GradientKind, ImageBrush, LinearGradientPosition, RadialGradientPosition, SweepGradientPosition, }; use serde::{Deserialize, Serialize}; /// Color en el wire: RGBA de 8 bits, igual que `llimphi-wire-view`. pub type Rgba = [u8; 4]; /// Empaqueta un color de peniko. pub fn color_a_wire(c: Color) -> Rgba { let r = c.to_rgba8(); [r.r, r.g, r.b, r.a] } /// Reconstruye un color de peniko. pub fn wire_a_color(w: Rgba) -> Color { Color::from_rgba8(w[0], w[1], w[2], w[3]) } /// Modo de extensión de un degradé. Espeja `peniko::Extend`. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ExtiendeWire { #[default] Pad, Repetir, Reflejar, } impl ExtiendeWire { fn de(e: Extend) -> Self { match e { Extend::Pad => Self::Pad, Extend::Repeat => Self::Repetir, Extend::Reflect => Self::Reflejar, } } fn a_peniko(self) -> Extend { match self { Self::Pad => Extend::Pad, Self::Repetir => Extend::Repeat, Self::Reflejar => Extend::Reflect, } } } /// Una parada de color del degradé. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct ParadaWire { /// Posición normalizada, `0.0..=1.0`. pub offset: f32, pub color: Rgba, } /// La geometría del degradé. Espeja `peniko::GradientKind` en f32. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum ClaseDegradeWire { Lineal { inicio: [f32; 2], fin: [f32; 2], }, Radial { centro_inicio: [f32; 2], radio_inicio: f32, centro_fin: [f32; 2], radio_fin: f32, }, Barrido { centro: [f32; 2], angulo_inicio: f32, angulo_fin: f32, }, } /// Un pincel listo para viajar. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum PincelWire { /// El 95% de los casos en una UI. Solido(Rgba), /// Degradé. **Se pierde el espacio de interpolación**: peniko permite /// interpolar en Oklab, Lch y compañía, y el wire asume sRGB. Es la misma /// decisión que tomó el color RGBA8 —la UI de Llimphi no usa esos espacios /// hoy— y está anotada acá para que el día que se usen no sea un misterio. Degrade { clase: ClaseDegradeWire, extiende: ExtiendeWire, paradas: Vec, }, /// Imagen: sólo la huella del blob (ver el encabezado del módulo). Imagen { huella: u64 }, } impl PincelWire { /// Empaqueta un pincel de peniko. pub fn de(pincel: &Brush) -> Self { match pincel { Brush::Solid(c) => Self::Solido(color_a_wire(*c)), Brush::Gradient(g) => Self::Degrade { clase: clase_a_wire(&g.kind), extiende: ExtiendeWire::de(g.extend), paradas: g .stops .iter() .map(|s| ParadaWire { offset: s.offset, color: color_a_wire(s.color.to_alpha_color::()), }) .collect(), }, Brush::Image(img) => Self::Imagen { huella: huella_imagen(img), }, } } /// Reconstruye el pincel del lado nativo. /// /// Un pincel de imagen no se puede reconstruir sin el registro de blobs, así /// que devuelve `None` y el reproductor decide qué hacer — que es mejor que /// inventar un color y pintar algo que nadie pidió. pub fn a_peniko(&self) -> Option { match self { Self::Solido(c) => Some(Brush::Solid(wire_a_color(*c))), Self::Degrade { clase, extiende, paradas, } => { let mut stops = ColorStops::new(); for p in paradas { stops.push(ColorStop { offset: p.offset, color: DynamicColor::from_alpha_color(wire_a_color(p.color)), }); } Some(Brush::Gradient(Gradient { kind: wire_a_clase(clase), extend: extiende.a_peniko(), stops, ..Default::default() })) } Self::Imagen { .. } => None, } } } fn clase_a_wire(k: &GradientKind) -> ClaseDegradeWire { match k { GradientKind::Linear(l) => ClaseDegradeWire::Lineal { inicio: [l.start.x as f32, l.start.y as f32], fin: [l.end.x as f32, l.end.y as f32], }, GradientKind::Radial(r) => ClaseDegradeWire::Radial { centro_inicio: [r.start_center.x as f32, r.start_center.y as f32], radio_inicio: r.start_radius, centro_fin: [r.end_center.x as f32, r.end_center.y as f32], radio_fin: r.end_radius, }, GradientKind::Sweep(s) => ClaseDegradeWire::Barrido { centro: [s.center.x as f32, s.center.y as f32], angulo_inicio: s.start_angle, angulo_fin: s.end_angle, }, } } fn wire_a_clase(c: &ClaseDegradeWire) -> GradientKind { match c { ClaseDegradeWire::Lineal { inicio, fin } => GradientKind::Linear(LinearGradientPosition { start: kurbo::Point::new(inicio[0] as f64, inicio[1] as f64), end: kurbo::Point::new(fin[0] as f64, fin[1] as f64), }), ClaseDegradeWire::Radial { centro_inicio, radio_inicio, centro_fin, radio_fin, } => GradientKind::Radial(RadialGradientPosition { start_center: kurbo::Point::new(centro_inicio[0] as f64, centro_inicio[1] as f64), start_radius: *radio_inicio, end_center: kurbo::Point::new(centro_fin[0] as f64, centro_fin[1] as f64), end_radius: *radio_fin, }), ClaseDegradeWire::Barrido { centro, angulo_inicio, angulo_fin, } => GradientKind::Sweep(SweepGradientPosition { center: kurbo::Point::new(centro[0] as f64, centro[1] as f64), start_angle: *angulo_inicio, end_angle: *angulo_fin, }), } } /// **Los píxeles de una imagen, para que puedan cruzar.** /// /// La orden [`crate::OrdenWire::Imagen`] viaja **por huella**: la escena dice /// «acá va la imagen tal» y no arrastra el buffer, porque la misma miniatura /// aparece en veinte órdenes y mandarla veinte veces sería absurdo. El precio /// es que el que reproduce necesita un **registro** donde buscar esa huella — /// y hasta hoy ese registro no lo llenaba nadie: el reproductor contaba la /// imagen en `imagenes_sin_blob` y NO la dibujaba. En el explorador enjaulado /// eso es toda miniatura y toda previa **en negro**, que fue el reporte. /// /// Esto es lo que faltaba: el blob viaja **una vez** junto a la escena que lo /// estrena, y el anfitrión lo guarda por huella para los cuadros siguientes. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BlobImagen { pub huella: u64, pub ancho: u32, pub alto: u32, /// `peniko::ImageFormat` como número: `0` = RGBA8, `1` = BGRA8. pub formato: u8, /// `peniko::ImageAlphaType` como número: `0` = separado, `1` = premultiplicado. pub alfa: u8, pub datos: Vec, } /// El blob de una imagen, listo para viajar. pub fn blob_de(img: &ImageBrush) -> BlobImagen { BlobImagen { huella: huella_imagen(img), ancho: img.image.width, alto: img.image.height, formato: img.image.format as u8, alfa: img.image.alpha_type as u8, datos: img.image.data.data().to_vec(), } } /// Reconstruye el pincel de imagen del lado nativo. /// /// El **muestreo va por defecto** y no viaja: es lo único del `ImageBrush` que /// no entra en la huella, así que mandarlo obligaría a decidir qué pasa cuando /// dos órdenes comparten huella y difieren en muestreo. Con el defecto, una /// miniatura se ve igual de los dos lados; el día que alguien necesite otro, el /// campo entra en la huella primero. pub fn imagen_de_blob(b: &BlobImagen) -> ImageBrush { ImageBrush::new(peniko::ImageData { data: peniko::Blob::from(b.datos.clone()), format: if b.formato == 1 { peniko::ImageFormat::Bgra8 } else { peniko::ImageFormat::Rgba8 }, alpha_type: if b.alfa == 1 { peniko::ImageAlphaType::AlphaPremultiplied } else { peniko::ImageAlphaType::Alpha }, width: b.ancho, height: b.alto, }) } /// Huella FNV-1a de 64 bits del contenido de una imagen. /// /// FNV y no BLAKE3 a propósito: no es una huella criptográfica ni tiene que /// serlo —el registro de blobs vive del lado nativo y no acepta nada que no le /// hayan dado antes—, corre en el guest (donde cada byte de código pesa) y son /// veinte líneas sin dependencias. Entran las dimensiones y el formato además de /// los píxeles, para que dos imágenes con el mismo buffer pero distinta /// interpretación no colisionen. pub fn huella_imagen(img: &ImageBrush) -> u64 { const BASE: u64 = 0xcbf2_9ce4_8422_2325; const PRIMO: u64 = 0x0000_0100_0000_01b3; let mut h = BASE; let comer = |b: u8, h: &mut u64| { *h ^= b as u64; *h = h.wrapping_mul(PRIMO); }; for b in img.image.width.to_le_bytes() { comer(b, &mut h); } for b in img.image.height.to_le_bytes() { comer(b, &mut h); } comer(img.image.format as u8, &mut h); for b in img.image.data.data().iter() { comer(*b, &mut h); } h }