| // Copyright 2025 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! Types for paints. | |
| use crate::TextureId; | |
| use crate::geometry::RectU16; | |
| use crate::pixmap::{PixelMetadata, Pixmap}; | |
| use alloc::sync::Arc; | |
| pub use peniko::Color; | |
| use peniko::{ | |
| Gradient, | |
| color::{AlphaColor, PremulRgba8, Srgb}, | |
| }; | |
| /// A paint that needs to be resolved via its index. | |
| // In the future, we might add additional flags, that's why we have | |
| // this thin wrapper around u32, so we can change the underlying | |
| // representation without breaking the API. | |
| #[derive(Debug, Clone, PartialEq, Eq)] | |
| pub struct IndexedPaint(u32); | |
| impl IndexedPaint { | |
| /// Create a new indexed paint from an index. | |
| pub fn new(index: usize) -> Self { | |
| Self(u32::try_from(index).expect("exceeded the maximum number of paints")) | |
| } | |
| /// Return the index of the paint. | |
| pub fn index(&self) -> usize { | |
| usize::try_from(self.0).unwrap() | |
| } | |
| } | |
| /// A paint used internally by a rendering frontend to store how a draw should be painted. | |
| /// There are only two types of paint: | |
| /// | |
| /// 1) Simple solid colors, which are stored in premultiplied representation so that | |
| /// the renderer doesn't have to recompute it. | |
| /// 2) Indexed paints, which can represent any arbitrary, more complex paint that is | |
| /// determined by the frontend. The intended way of using this is to store a vector | |
| /// of paints and store its index inside `IndexedPaint`. | |
| #[derive(Debug, Clone, PartialEq)] | |
| pub enum Paint { | |
| /// A premultiplied RGBA8 color. | |
| Solid(PremulColor), | |
| /// A paint that needs to be resolved via an index. | |
| Indexed(IndexedPaint), | |
| } | |
| impl From<AlphaColor<Srgb>> for Paint { | |
| fn from(value: AlphaColor<Srgb>) -> Self { | |
| Self::Solid(PremulColor::from_alpha_color(value)) | |
| } | |
| } | |
| /// Opaque image handle | |
| #[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] | |
| pub struct ImageId(u32); | |
| impl ImageId { | |
| // TODO: make this private in future | |
| /// Create a new image id from a u32. | |
| pub fn new(value: u32) -> Self { | |
| Self(value) | |
| } | |
| /// Return the image id as a u32. | |
| pub fn as_u32(&self) -> u32 { | |
| self.0 | |
| } | |
| } | |
| /// Bitmap source used by `Image`. | |
| #[derive(Debug, Clone)] | |
| pub enum ImageSource { | |
| /// Pixmap pixels travel with the scene packet. | |
| Pixmap(Arc<Pixmap>), | |
| // TODO: Explore whether we can merge opaque ID and external texture in some form? | |
| /// Pixmap pixels were registered earlier; this is just a handle. | |
| OpaqueId { | |
| /// The image handle. | |
| id: ImageId, | |
| /// Whether the image may contain non-opaque pixels. | |
| may_have_transparency: bool, | |
| }, | |
| /// An externally owned texture supplied to the renderer at render time. | |
| ExternalTexture { | |
| /// Opaque external texture handle. | |
| id: TextureId, | |
| /// Source region to sample from in texel coordinates. | |
| source_region: RectU16, | |
| /// Whether the source region may contain non-opaque pixels. | |
| may_have_transparency: bool, | |
| }, | |
| } | |
| impl ImageSource { | |
| /// Create an [`ImageSource`] from a pre-registered image handle. | |
| /// | |
| /// Conservatively assumes the image may have non-opaque pixels. | |
| /// Use [`Self::opaque_id_with_transparency_hint`] when you know the image is fully opaque. | |
| pub fn opaque_id(id: ImageId) -> Self { | |
| Self::OpaqueId { | |
| id, | |
| may_have_transparency: true, | |
| } | |
| } | |
| /// Create an [`ImageSource`] from a pre-registered image handle, | |
| /// with an explicit hint about whether the image may have non-opaque pixels. | |
| pub fn opaque_id_with_transparency_hint(id: ImageId, may_have_transparency: bool) -> Self { | |
| Self::OpaqueId { | |
| id, | |
| may_have_transparency, | |
| } | |
| } | |
| /// Create an image source backed by a texture supplied to the renderer at render time. | |
| /// | |
| /// # Panics | |
| /// | |
| /// Panics if `source_region` is empty. | |
| pub fn external_texture( | |
| texture_id: TextureId, | |
| source_region: RectU16, | |
| may_have_transparency: bool, | |
| ) -> Self { | |
| assert!( | |
| !source_region.is_empty(), | |
| "external texture source regions must not be empty" | |
| ); | |
| Self::ExternalTexture { | |
| id: texture_id, | |
| source_region, | |
| may_have_transparency, | |
| } | |
| } | |
| /// Returns whether this image source may contain non-opaque pixels. | |
| pub fn may_have_transparency(&self) -> bool { | |
| match self { | |
| Self::Pixmap(p) => p.may_have_transparency(), | |
| Self::OpaqueId { | |
| may_have_transparency, | |
| .. | |
| } | |
| | Self::ExternalTexture { | |
| may_have_transparency, | |
| .. | |
| } => *may_have_transparency, | |
| } | |
| } | |
| /// Convert a [`peniko::ImageData`] to an [`ImageSource`]. | |
| /// | |
| /// This is a somewhat lossy conversion, as the image data data is transformed to | |
| /// [premultiplied RGBA8](`PremulRgba8`). | |
| /// | |
| /// # Panics | |
| /// | |
| /// This panics if `image` has a `width` or `height` greater than `u16::MAX`. | |
| pub fn from_peniko_image_data(image: &peniko::ImageData) -> Self { | |
| // TODO: how do we deal with `peniko::ImageFormat` growing? See also | |
| // <https://github.com/linebender/vello/pull/996#discussion_r2080510863>. | |
| assert!( | |
| image.width <= u16::MAX as u32 && image.height <= u16::MAX as u32, | |
| "The image is too big. Its width and height can be no larger than {} pixels.", | |
| u16::MAX, | |
| ); | |
| let width = image.width.try_into().unwrap(); | |
| let height = image.height.try_into().unwrap(); | |
| // Unfortunately, we have to create a new allocation, because pixmap requires | |
| // a real vector. | |
| // TODO: Figure out a better story for this. | |
| let mut rgba = image.data.data().to_vec(); | |
| match image.format { | |
| peniko::ImageFormat::Rgba8 => {} | |
| peniko::ImageFormat::Bgra8 => { | |
| // TODO: SIMDify | |
| for pixel in rgba.chunks_exact_mut(4) { | |
| pixel.swap(0, 2); | |
| } | |
| } | |
| format => unimplemented!("Unsupported image format: {format:?}"), | |
| } | |
| let pixmap = Pixmap::from_parts( | |
| rgba, | |
| width, | |
| height, | |
| PixelMetadata::new(image.alpha_type, true), | |
| ); | |
| Self::Pixmap(Arc::new(pixmap)) | |
| } | |
| } | |
| /// An image. | |
| pub type Image = peniko::ImageBrush<ImageSource>; | |
| /// Trait for resolving opaque image IDs to pixmaps at rasterization time. | |
| /// | |
| /// This allows delaying the resolution of `ImageSource::OpaqueId` until the | |
| /// image is actually needed during rasterization, enabling patterns like | |
| /// dynamic sprite atlases where the image data may be updated between | |
| /// encoding and rendering. | |
| pub trait ImageResolver: Send + Sync { | |
| /// Resolve an `ImageId` to its pixmap data. | |
| /// | |
| /// This method may be called repeatedly (dozens or even hundreds of times | |
| /// per frame) and should therefore be very fast. | |
| /// | |
| /// Returns `None` if the image ID is not found in the registry. | |
| fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>>; | |
| } | |
| /// A no-op image resolver that always returns `None`. | |
| #[derive(Debug, Clone, Copy, Default)] | |
| pub struct NoOpImageResolver; | |
| impl ImageResolver for NoOpImageResolver { | |
| fn resolve(&self, _id: ImageId) -> Option<Arc<Pixmap>> { | |
| None | |
| } | |
| } | |
| /// A premultiplied color. | |
| #[derive(Debug, Clone, PartialEq, Copy)] | |
| pub struct PremulColor { | |
| premul_u8: PremulRgba8, | |
| premul_f32: peniko::color::PremulColor<Srgb>, | |
| } | |
| impl PremulColor { | |
| /// Create a new premultiplied color. | |
| pub fn from_alpha_color(color: AlphaColor<Srgb>) -> Self { | |
| Self::from_premul_color(color.premultiply()) | |
| } | |
| /// Create a new premultiplied color from `peniko::PremulColor`. | |
| pub fn from_premul_color(color: peniko::color::PremulColor<Srgb>) -> Self { | |
| Self { | |
| premul_u8: color.to_rgba8(), | |
| premul_f32: color, | |
| } | |
| } | |
| /// Return the color as a premultiplied RGBA8 color. | |
| pub fn as_premul_rgba8(&self) -> PremulRgba8 { | |
| self.premul_u8 | |
| } | |
| /// Return the color as a premultiplied RGBAF32 color. | |
| pub fn as_premul_f32(&self) -> peniko::color::PremulColor<Srgb> { | |
| self.premul_f32 | |
| } | |
| /// Return whether the color is opaque (i.e. doesn't have transparency). | |
| pub fn is_opaque(&self) -> bool { | |
| self.premul_f32.components[3] == 1.0 | |
| } | |
| /// Return whether the color is fully transparent. | |
| pub fn is_transparent(&self) -> bool { | |
| self.premul_f32.components[3] == 0.0 | |
| } | |
| } | |
| /// How tint color is applied to an image. | |
| #[derive(Copy, Clone, Debug, PartialEq, Eq)] | |
| #[repr(u8)] | |
| pub enum TintMode { | |
| /// Alpha-mask tinting: `tint_premul * source.alpha`. | |
| /// | |
| /// The source image's alpha channel is used as a coverage mask, | |
| /// and the result is filled with the premultiplied tint color. | |
| /// This is the standard approach for glyph / monochrome image tinting. | |
| AlphaMask = 0, | |
| /// Component-wise multiply: `source * tint`. | |
| /// | |
| /// Each channel of the source pixel is multiplied by the corresponding | |
| /// channel of the tint color. This works well for full-color images. | |
| Multiply = 1, | |
| } | |
| impl TintMode { | |
| /// Return the discriminant as a `u32`. | |
| pub fn as_u32(self) -> u32 { | |
| self as u32 | |
| } | |
| } | |
| /// A tint applied to image paints. | |
| #[derive(Copy, Clone, Debug, PartialEq)] | |
| pub struct Tint { | |
| /// The tint color. | |
| pub color: Color, | |
| /// How the tint is applied. | |
| pub mode: TintMode, | |
| } | |
| /// A kind of paint that can be used for filling and stroking shapes. | |
| pub type PaintType = peniko::Brush<Image, Gradient>; | |
| #[cfg(test)] | |
| mod tests { | |
| use super::ImageSource; | |
| use alloc::sync::Arc; | |
| fn image_data(pixels: &[u8], alpha_type: peniko::ImageAlphaType) -> peniko::ImageData { | |
| peniko::ImageData { | |
| data: peniko::Blob::new(Arc::new(pixels.to_vec())), | |
| format: peniko::ImageFormat::Rgba8, | |
| alpha_type, | |
| width: (pixels.len() / 4) as u32, | |
| height: 1, | |
| } | |
| } | |
| #[test] | |
| fn from_peniko_image_data_computes_transparency_hint() { | |
| let opaque = image_data( | |
| &[10, 20, 30, 255, 40, 50, 60, 255], | |
| peniko::ImageAlphaType::Alpha, | |
| ); | |
| assert!(!ImageSource::from_peniko_image_data(&opaque).may_have_transparency()); | |
| let translucent = image_data( | |
| &[10, 20, 30, 255, 40, 50, 60, 128], | |
| peniko::ImageAlphaType::Alpha, | |
| ); | |
| assert!(ImageSource::from_peniko_image_data(&translucent).may_have_transparency()); | |
| let premultiplied = image_data( | |
| &[10, 20, 30, 255, 40, 50, 60, 255], | |
| peniko::ImageAlphaType::AlphaPremultiplied, | |
| ); | |
| assert!(ImageSource::from_peniko_image_data(&premultiplied).may_have_transparency()); | |
| } | |
| } |