| // Copyright 2025 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! Paints for drawing shapes. |
| |
| use crate::blurred_rounded_rect::BlurredRoundedRectangle; |
| use crate::color::palette::css::BLACK; |
| use crate::color::{ColorSpaceTag, HueDirection, Srgb, gradient}; |
| use crate::kurbo::{Affine, Point, Vec2}; |
| use crate::math::compute_erf7; |
| use crate::peniko::{ColorStop, Extend, Gradient, GradientKind, ImageQuality}; |
| use crate::pixmap::Pixmap; |
| use alloc::borrow::Cow; |
| use alloc::sync::Arc; |
| use alloc::vec::Vec; |
| use core::f32::consts::PI; |
| use core::iter; |
| use smallvec::SmallVec; |
| use vello_api::paint::{Image, IndexedPaint, Paint, PremulColor}; |
| |
| const DEGENERATE_THRESHOLD: f32 = 1.0e-6; |
| const NUDGE_VAL: f32 = 1.0e-7; |
| |
| /// A trait for encoding gradients. |
| pub trait EncodeExt: private::Sealed { |
| /// Encode the gradient and push it into a vector of encoded paints, returning |
| /// the corresponding paint in the process. This will also validate the gradient. |
| fn encode_into(&self, paints: &mut Vec<EncodedPaint>, transform: Affine) -> Paint; |
| } |
| |
| impl EncodeExt for Gradient { |
| /// Encode the gradient into a paint. |
| fn encode_into(&self, paints: &mut Vec<EncodedPaint>, transform: Affine) -> Paint { |
| // First make sure that the gradient is valid and not degenerate. |
| if let Err(paint) = validate(self) { |
| return paint; |
| } |
| |
| let mut has_opacities = self.stops.iter().any(|s| s.color.components[3] != 1.0); |
| let pad = self.extend == Extend::Pad; |
| |
| let mut stops = Cow::Borrowed(&self.stops.0); |
| // For each gradient type, before doing anything we first translate it such that |
| // one of the points of the gradient lands on the origin (0, 0). We do this because |
| // it makes things simpler and allows for some optimizations for certain calculations. |
| let (x_offset, y_offset); |
| // The start/end range of the color line. We use this to resolve the extend of the gradient. |
| // Currently radial gradients uses normalized values between 0.0 and 1.0, for sweep and |
| // linear gradients different values are used (TODO: Would be nice to make this more consistent). |
| let mut clamp_range = (0.0, 1.0); |
| |
| let kind = match self.kind { |
| GradientKind::Linear { start, end } => { |
| // For linear gradients, we want to interpolate the color along the line that is |
| // formed by `start` and `end`. |
| let mut p0 = start; |
| let mut p1 = end; |
| |
| // For simplicity, ensure that the gradient line always goes from left to right. |
| if p0.x >= p1.x { |
| core::mem::swap(&mut p0, &mut p1); |
| |
| stops = Cow::Owned( |
| stops |
| .iter() |
| .rev() |
| .map(|s| ColorStop { |
| offset: 1.0 - s.offset, |
| color: s.color, |
| }) |
| .collect::<SmallVec<[ColorStop; 4]>>(), |
| ); |
| } |
| |
| // Double the length of the iterator, and append stops in reverse order in case |
| // we have the extend `Reflect`. |
| // Then we can treat it the same as a repeated gradient. |
| if self.extend == Extend::Reflect { |
| p1.x += p1.x - p0.x; |
| p1.y += p1.y - p0.y; |
| stops = Cow::Owned(apply_reflect(&stops)); |
| } |
| |
| // To translate p0 to the origin of the coordinate system, we need to apply |
| // the negative. |
| x_offset = -p0.x as f32; |
| y_offset = -p0.y as f32; |
| |
| let dx = p1.x as f32 + x_offset; |
| let dy = p1.y as f32 + y_offset; |
| // In order to calculate where a pixel lies along the gradient line (the line made up |
| // by the two points of the linear gradient), we need to calculate its position |
| // on the gradient line. Remember that our gradient line always start at the origin |
| // (0, 0). Therefore, we can simply calculate the normal vector of the line, |
| // and then, for each pixel that we render, we calculate the distance to the line. |
| // That distance then corresponds to our position on the gradient line, and allows |
| // us to resolve which color stops we need to load and how to interpolate them. |
| let norm = (-dy, dx); |
| |
| // We precalculate some values so that we can more easily calculate the distance |
| // from the position of the pixel to the line of the normal vector. See |
| // here for the formula: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_two_points |
| |
| // The denominator, i.e. sqrt((y_2 - y_1)^2 + (x_2 - x_1)^2). Since x_1 and y_1 |
| // are always 0, this shortens to sqrt(y_2^2 + x_2^2). |
| let distance = (norm.1 * norm.1 + norm.0 * norm.0).sqrt(); |
| // This corresponds to (y_2 - y_1) in the formula, but because of the above reasons |
| // shortens to y_2. |
| let y2_minus_y1 = norm.1; |
| // This corresponds to (x_2 - x_1) in the formula, but because of the above reasons |
| // shortens to x_2. |
| let x2_minus_x1 = norm.0; |
| // Note that we can completely disregard the x_2 * y_1 - y_2 * x_1 factor, since |
| // y_1 and x_1 are both 0. |
| |
| let end_val = (dx * dx + dy * dy).sqrt(); |
| clamp_range = (0.0, end_val); |
| |
| EncodedKind::Linear(LinearKind { |
| distance, |
| y2_minus_y1, |
| x2_minus_x1, |
| }) |
| } |
| GradientKind::Radial { |
| start_center, |
| start_radius, |
| end_center, |
| end_radius, |
| } => { |
| // For radial gradients, we conceptually interpolate a circle from c0 with radius |
| // r0 to the circle at c1 with radius r1. |
| let c0 = start_center; |
| let mut c1 = end_center; |
| let r0 = start_radius; |
| let mut r1 = end_radius; |
| |
| // Same story as for linear gradients, mutate stops so that reflect and repeat |
| // can be treated the same. |
| if self.extend == Extend::Reflect { |
| c1 += c1 - c0; |
| r1 += r1 - r0; |
| stops = Cow::Owned(apply_reflect(&stops)); |
| } |
| |
| // Similarly to linear gradients, ensure that c0 lands on the origin (0, 0). |
| x_offset = -c0.x as f32; |
| y_offset = -c0.y as f32; |
| |
| let end_point = c1 - c0; |
| |
| let dist = (end_point.x * end_point.x + end_point.y * end_point.y).sqrt() as f32; |
| let c0_in_c1 = r1 >= r0 + dist; |
| let c1_in_c0 = r0 >= r1 + dist; |
| let cone_like = !(c0_in_c1 || c1_in_c0); |
| // If the inner circle is not completely contained within the outer circle, the gradient |
| // can deform into a cone-like structure where some areas of the shape are not defined. |
| // Because of this, we might need opacities and source-over compositing in that case. |
| has_opacities |= cone_like; |
| |
| EncodedKind::Radial(RadialKind { |
| c1: (end_point.x as f32, end_point.y as f32), |
| r0, |
| r1, |
| cone_like, |
| }) |
| } |
| GradientKind::Sweep { |
| center, |
| start_angle, |
| end_angle, |
| } => { |
| // For sweep gradients, the position on the "color line" is defined by the |
| // angle towards the gradient center. |
| let start_angle = start_angle.to_radians(); |
| let mut end_angle = end_angle.to_radians(); |
| |
| // Same as before, reduce `Reflect` to `Repeat`. |
| if self.extend == Extend::Reflect { |
| end_angle += end_angle - start_angle; |
| stops = Cow::Owned(apply_reflect(&stops)); |
| } |
| |
| // Make sure the center of the gradient falls on the origin (0, 0). |
| x_offset = -center.x as f32; |
| y_offset = -center.y as f32; |
| clamp_range = (start_angle, end_angle); |
| |
| EncodedKind::Sweep(SweepKind) |
| } |
| }; |
| |
| let ranges = encode_stops( |
| &stops, |
| clamp_range.0, |
| clamp_range.1, |
| pad, |
| self.interpolation_cs, |
| self.hue_direction, |
| ); |
| |
| // This represents the transform that needs to be applied to the starting point of a |
| // command before starting with the rendering. |
| // First we need to account for a potential offset of the gradient (x_offset/y_offset), then |
| // we account for the fact that we sample in the center of a pixel and not in the corner by |
| // adding 0.5. |
| // Finally, we need to apply the _inverse_ transform to the point so that we can account |
| // for the transform on the gradient. |
| let transform = Affine::translate((f64::from(x_offset) + 0.5, f64::from(y_offset) + 0.5)) |
| * transform.inverse(); |
| |
| // One possible approach of calculating the positions would be to apply the above |
| // transform to _each_ pixel that we render in the wide tile. However, a much better |
| // approach is to apply the transform once for the first pixel, |
| // and from then on only apply incremental updates to the current x/y position |
| // that we calculated in the beginning. |
| // |
| // Remember that we render wide tiles in column major order (i.e. we first calculate the |
| // values for a specific x for all Tile::HEIGHT y by incrementing y by 1, and then finally |
| // we increment the x position by 1 and start from the beginning). If we want to implement |
| // the above approach of incrementally updating the position, we need to calculate |
| // how the x/y unit vectors are affected by the transform, and then use this as the |
| // step delta for a step in the x/y direction. |
| let (x_advance, y_advance) = x_y_advances(&transform); |
| |
| let encoded = EncodedGradient { |
| kind, |
| transform, |
| x_advance, |
| y_advance, |
| ranges, |
| pad, |
| has_opacities, |
| clamp_range, |
| }; |
| |
| let idx = paints.len(); |
| paints.push(encoded.into()); |
| |
| Paint::Indexed(IndexedPaint::new(idx)) |
| } |
| } |
| |
| /// Returns a fallback paint in case the gradient is invalid. |
| /// |
| /// The paint will be either black or contain the color of the first stop of the gradient. |
| fn validate(gradient: &Gradient) -> Result<(), Paint> { |
| let black = Err(BLACK.into()); |
| |
| // Gradients need at least two stops. |
| if gradient.stops.is_empty() { |
| return black; |
| } |
| |
| let first = Err(gradient.stops[0].color.to_alpha_color::<Srgb>().into()); |
| |
| if gradient.stops.len() == 1 { |
| return first; |
| } |
| |
| // First stop must be at offset 0.0 and last offset must be at 1.0. |
| if gradient.stops[0].offset != 0.0 || gradient.stops[gradient.stops.len() - 1].offset != 1.0 { |
| return first; |
| } |
| |
| for stops in gradient.stops.windows(2) { |
| let f = stops[0]; |
| let n = stops[1]; |
| |
| // Offsets must be between 0 and 1. |
| if f.offset > 1.0 || f.offset < 0.0 { |
| return first; |
| } |
| |
| // Stops must be sorted by ascending offset. |
| if f.offset > n.offset { |
| return first; |
| } |
| } |
| |
| let degenerate_point = |p1: &Point, p2: &Point| { |
| (p1.x - p2.x).abs() as f32 <= DEGENERATE_THRESHOLD |
| && (p1.y - p2.y).abs() as f32 <= DEGENERATE_THRESHOLD |
| }; |
| |
| let degenerate_val = |v1: f32, v2: f32| (v2 - v1).abs() <= DEGENERATE_THRESHOLD; |
| |
| match &gradient.kind { |
| GradientKind::Linear { start, end } => { |
| // Start and end points must not be too close together. |
| if degenerate_point(start, end) { |
| return first; |
| } |
| } |
| GradientKind::Radial { |
| start_center, |
| start_radius, |
| end_center, |
| end_radius, |
| } => { |
| // Radii must not be negative. |
| if *start_radius < 0.0 || *end_radius < 0.0 { |
| return first; |
| } |
| |
| // Radii and center points must not be close to the same. |
| if degenerate_point(start_center, end_center) |
| && degenerate_val(*start_radius, *end_radius) |
| { |
| return first; |
| } |
| } |
| GradientKind::Sweep { |
| start_angle, |
| end_angle, |
| .. |
| } => { |
| // The end angle must be larger than the start angle. |
| if degenerate_val(*start_angle, *end_angle) { |
| return first; |
| } |
| |
| if end_angle <= start_angle { |
| return first; |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Extend the stops so that we can treat a repeated gradient like a reflected gradient. |
| fn apply_reflect(stops: &[ColorStop]) -> SmallVec<[ColorStop; 4]> { |
| let first_half = stops.iter().map(|s| ColorStop { |
| offset: s.offset / 2.0, |
| color: s.color, |
| }); |
| |
| let second_half = stops.iter().rev().map(|s| ColorStop { |
| offset: 0.5 + (1.0 - s.offset) / 2.0, |
| color: s.color, |
| }); |
| |
| first_half.chain(second_half).collect::<SmallVec<_>>() |
| } |
| |
| /// Encode all stops into a sequence of ranges. |
| fn encode_stops( |
| stops: &[ColorStop], |
| start: f32, |
| end: f32, |
| pad: bool, |
| cs: ColorSpaceTag, |
| hue_dir: HueDirection, |
| ) -> Vec<GradientRange> { |
| struct EncodedColorStop { |
| offset: f32, |
| color: vello_api::color::PremulColor<Srgb>, |
| } |
| |
| // Create additional (SRGB-encoded) stops in-between to approximate the color space we want to |
| // interpolate in. |
| let interpolated_stops = stops |
| .windows(2) |
| .flat_map(|s| { |
| let left_stop = &s[0]; |
| let right_stop = &s[1]; |
| |
| let interpolated = |
| gradient::<Srgb>(left_stop.color, right_stop.color, cs, hue_dir, 0.01); |
| |
| interpolated.map(|st| EncodedColorStop { |
| offset: left_stop.offset + (right_stop.offset - left_stop.offset) * st.0, |
| color: st.1, |
| }) |
| }) |
| .collect::<Vec<_>>(); |
| |
| let create_range = |left_stop: &EncodedColorStop, right_stop: &EncodedColorStop| { |
| let clamp = |mut color: [f32; 4]| { |
| // The linear approximation of the gradient can produce values slightly outside of |
| // [0.0, 1.0], so clamp them. |
| for c in &mut color { |
| *c = c.clamp(0.0, 1.0); |
| } |
| |
| color |
| }; |
| |
| let x0 = start + (end - start) * left_stop.offset; |
| let x1 = start + (end - start) * right_stop.offset; |
| let c0 = clamp(left_stop.color.components); |
| let c1 = clamp(right_stop.color.components); |
| |
| // Given two positions x0 and x1 as well as two corresponding colors c0 and c1, |
| // the delta that needs to be applied to c0 to calculate the color of x between x0 and x1 |
| // is calculated by c0 + ((x - x0) / (x1 - x0)) * (c1 - c0). |
| // We can precompute the (c1 - c0)/(x1 - x0) part for each color component. |
| |
| // We call this method with two same stops for `left_range` and `right_range`, so make |
| // sure we don't actually end up with a 0 here. |
| let x1_minus_x0 = (x1 - x0).max(NUDGE_VAL); |
| let mut factors_f32 = [0.0; 4]; |
| |
| for i in 0..4 { |
| let c1_minus_c0 = c1[i] - c0[i]; |
| factors_f32[i] = c1_minus_c0 / x1_minus_x0; |
| } |
| |
| GradientRange { |
| x0, |
| x1, |
| c0: PremulColor::from_premul_color(left_stop.color), |
| factors_f32, |
| } |
| }; |
| |
| // Note: this could use `Iterator::map_windows` once stabilized, meaning `interpolated_stops` |
| // no longer needs to be collected. |
| let stop_ranges = interpolated_stops.windows(2).map(|s| { |
| let left_stop = &s[0]; |
| let right_stop = &s[1]; |
| |
| create_range(left_stop, right_stop) |
| }); |
| |
| if pad { |
| // We handle padding by inserting dummy stops in the beginning and end with a very big |
| // range. |
| let left_range = iter::once({ |
| let first_stop = interpolated_stops.first().unwrap(); |
| let mut encoded_range = create_range(first_stop, first_stop); |
| encoded_range.x0 = f32::MIN; |
| encoded_range |
| }); |
| |
| let right_range = iter::once({ |
| let last_stop = interpolated_stops.last().unwrap(); |
| let mut encoded_range = create_range(last_stop, last_stop); |
| encoded_range.x1 = f32::MAX; |
| encoded_range |
| }); |
| |
| left_range.chain(stop_ranges.chain(right_range)).collect() |
| } else { |
| stop_ranges.collect() |
| } |
| } |
| |
| pub(crate) fn x_y_advances(transform: &Affine) -> (Vec2, Vec2) { |
| let scale_skew_transform = { |
| let c = transform.as_coeffs(); |
| Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]) |
| }; |
| |
| let x_advance = scale_skew_transform * Point::new(1.0, 0.0); |
| let y_advance = scale_skew_transform * Point::new(0.0, 1.0); |
| |
| ( |
| Vec2::new(x_advance.x, x_advance.y), |
| Vec2::new(y_advance.x, y_advance.y), |
| ) |
| } |
| |
| impl private::Sealed for Image {} |
| |
| impl EncodeExt for Image { |
| fn encode_into(&self, paints: &mut Vec<EncodedPaint>, transform: Affine) -> Paint { |
| let idx = paints.len(); |
| |
| let transform = transform.inverse(); |
| // TODO: This is somewhat expensive for large images, maybe it's not worth optimizing |
| // non-opaque images in the first place.. |
| let has_opacities = self.pixmap.data().iter().any(|pixel| pixel.a != 255); |
| |
| let (x_advance, y_advance) = x_y_advances(&transform); |
| |
| let encoded = EncodedImage { |
| pixmap: self.pixmap.clone(), |
| extends: (self.x_extend, self.y_extend), |
| quality: self.quality, |
| has_opacities, |
| transform, |
| x_advance, |
| y_advance, |
| }; |
| |
| paints.push(EncodedPaint::Image(encoded)); |
| |
| Paint::Indexed(IndexedPaint::new(idx)) |
| } |
| } |
| |
| /// An encoded paint. |
| #[derive(Debug)] |
| pub enum EncodedPaint { |
| /// An encoded gradient. |
| Gradient(EncodedGradient), |
| /// An encoded image. |
| Image(EncodedImage), |
| /// A blurred, rounded rectangle. |
| BlurredRoundedRect(EncodedBlurredRoundedRectangle), |
| } |
| |
| impl From<EncodedGradient> for EncodedPaint { |
| fn from(value: EncodedGradient) -> Self { |
| Self::Gradient(value) |
| } |
| } |
| |
| impl From<EncodedBlurredRoundedRectangle> for EncodedPaint { |
| fn from(value: EncodedBlurredRoundedRectangle) -> Self { |
| Self::BlurredRoundedRect(value) |
| } |
| } |
| |
| /// An encoded image. |
| #[derive(Debug)] |
| pub struct EncodedImage { |
| /// The underlying pixmap of the image. |
| pub pixmap: Arc<Pixmap>, |
| /// The extends in the horizontal and vertical direction. |
| pub extends: (Extend, Extend), |
| /// The rendering quality of the image. |
| pub quality: ImageQuality, |
| /// Whether the image has opacities. |
| pub has_opacities: bool, |
| /// A transform to apply to the image. |
| pub transform: Affine, |
| /// The advance in image coordinates for one step in the x direction. |
| pub x_advance: Vec2, |
| /// The advance in image coordinates for one step in the y direction. |
| pub y_advance: Vec2, |
| } |
| |
| /// Computed properties of a linear gradient. |
| #[derive(Debug)] |
| pub struct LinearKind { |
| distance: f32, |
| y2_minus_y1: f32, |
| x2_minus_x1: f32, |
| } |
| |
| /// Computed properties of a radial gradient. |
| #[derive(Debug)] |
| pub struct RadialKind { |
| c1: (f32, f32), |
| r0: f32, |
| r1: f32, |
| cone_like: bool, |
| } |
| |
| impl RadialKind { |
| fn pos_inner(&self, pos: &Point) -> Option<f32> { |
| // The values for a radial gradient can be calculated for any t as follow: |
| // Let x(t) = (x_1 - x_0)*t + x_0 (since x_0 is always 0, this shortens to x_1 * t) |
| // Let y(t) = (y_1 - y_0)*t + y_0 (since y_0 is always 0, this shortens to y_1 * t) |
| // Let r(t) = (r_1 - r_0)*t + r_0 |
| // Given a pixel at a position (x_2, y_2), we need to find the largest t such that |
| // (x_2 - x(t))^2 + (y - y_(t))^2 = r_t()^2, i.e. the circle with the interpolated |
| // radius and center position needs to intersect the pixel we are processing. |
| // |
| // You can reformulate this problem to a quadratic equation (TODO: add derivation. Since |
| // I'm not sure if that code will stay the same after performance optimizations I haven't |
| // written this down yet), to which we then simply need to find the solutions. |
| |
| let r0 = self.r0; |
| let dx = self.c1.0; |
| let dy = self.c1.1; |
| let dr = self.r1 - self.r0; |
| |
| let px = pos.x as f32; |
| let py = pos.y as f32; |
| |
| let a = dx * dx + dy * dy - dr * dr; |
| let b = -2.0 * (px * dx + py * dy + r0 * dr); |
| let c = px * px + py * py - r0 * r0; |
| |
| let discriminant = b * b - 4.0 * a * c; |
| |
| // No solution available. |
| if discriminant < 0.0 { |
| return None; |
| } |
| |
| let sqrt_d = discriminant.sqrt(); |
| let t1 = (-b - sqrt_d) / (2.0 * a); |
| let t2 = (-b + sqrt_d) / (2.0 * a); |
| |
| let max = t1.max(t2); |
| let min = t1.min(t2); |
| |
| // We only want values for `t` where the interpolated radius is actually positive. |
| if self.r0 + dr * max < 0.0 { |
| if self.r0 + dr * min < 0.0 { |
| None |
| } else { |
| Some(min) |
| } |
| } else { |
| Some(max) |
| } |
| } |
| } |
| |
| /// Computed properties of a sweep gradient. |
| #[derive(Debug)] |
| pub struct SweepKind; |
| |
| /// A kind of encoded gradient. |
| #[derive(Debug)] |
| pub enum EncodedKind { |
| /// An encoded linear gradient. |
| Linear(LinearKind), |
| /// An encoded radial gradient. |
| Radial(RadialKind), |
| /// An encoded sweep gradient. |
| Sweep(SweepKind), |
| } |
| |
| /// An encoded gradient. |
| #[derive(Debug)] |
| pub struct EncodedGradient { |
| /// The underlying kind of gradient. |
| pub kind: EncodedKind, |
| /// A transform that needs to be applied to the position of the first processed pixel. |
| pub transform: Affine, |
| /// How much to advance into the x/y direction for one step in the x direction. |
| pub x_advance: Vec2, |
| /// How much to advance into the x/y direction for one step in the y direction. |
| pub y_advance: Vec2, |
| /// The color ranges of the gradient. |
| pub ranges: Vec<GradientRange>, |
| /// Whether the gradient should be padded. |
| pub pad: bool, |
| /// Whether the gradient requires `source_over` compositing. |
| pub has_opacities: bool, |
| /// The values that should be used for clamping when applying the extend. |
| pub clamp_range: (f32, f32), |
| } |
| |
| /// An encoded ange between two color stops. |
| #[derive(Debug, Clone)] |
| pub struct GradientRange { |
| /// The start value of the range. |
| pub x0: f32, |
| /// The end value of the range. |
| pub x1: f32, |
| /// The start color of the range. |
| pub c0: PremulColor, |
| /// The interpolation factors of the range. |
| pub factors_f32: [f32; 4], |
| } |
| |
| /// Sampling positions in a gradient. |
| pub trait GradientLike { |
| /// Given a position, return the position on the gradient range. |
| fn cur_pos(&self, pos: &Point) -> f32; |
| /// Whether the gradient is possibly not defined over the whole domain of points. |
| fn has_undefined(&self) -> bool; |
| /// Whether the current position is defined in the gradient. If `has_undefined` returns `false`, |
| /// this will return false for all possible points. |
| fn is_defined(&self, pos: &Point) -> bool; |
| } |
| |
| impl GradientLike for SweepKind { |
| fn cur_pos(&self, pos: &Point) -> f32 { |
| // The position in a sweep gradient is simply determined by its angle from the origin. |
| let angle = (-pos.y as f32).atan2(pos.x as f32); |
| |
| if angle >= 0.0 { |
| angle |
| } else { |
| angle + 2.0 * PI |
| } |
| } |
| |
| fn has_undefined(&self) -> bool { |
| false |
| } |
| |
| fn is_defined(&self, _: &Point) -> bool { |
| true |
| } |
| } |
| |
| impl GradientLike for LinearKind { |
| fn cur_pos(&self, pos: &Point) -> f32 { |
| // The position of a point relative to a linear gradient is determined by its distance |
| // to the normal vector. See `encode_into` for more information. |
| (pos.x as f32 * self.y2_minus_y1 - pos.y as f32 * self.x2_minus_x1) / self.distance |
| } |
| |
| fn has_undefined(&self) -> bool { |
| false |
| } |
| |
| fn is_defined(&self, _: &Point) -> bool { |
| true |
| } |
| } |
| |
| impl GradientLike for RadialKind { |
| fn cur_pos(&self, pos: &Point) -> f32 { |
| self.pos_inner(pos).unwrap_or(0.0) |
| } |
| |
| fn has_undefined(&self) -> bool { |
| self.cone_like |
| } |
| |
| fn is_defined(&self, pos: &Point) -> bool { |
| self.pos_inner(pos).is_some() |
| } |
| } |
| |
| /// An encoded blurred, rounded rectangle. |
| #[derive(Debug)] |
| pub struct EncodedBlurredRoundedRectangle { |
| /// An component for computing the blur effect. |
| pub exponent: f32, |
| /// An component for computing the blur effect. |
| pub recip_exponent: f32, |
| /// An component for computing the blur effect. |
| pub scale: f32, |
| /// An component for computing the blur effect. |
| pub std_dev_inv: f32, |
| /// An component for computing the blur effect. |
| pub min_edge: f32, |
| /// An component for computing the blur effect. |
| pub w: f32, |
| /// An component for computing the blur effect. |
| pub h: f32, |
| /// An component for computing the blur effect. |
| pub width: f32, |
| /// An component for computing the blur effect. |
| pub height: f32, |
| /// An component for computing the blur effect. |
| pub r1: f32, |
| /// The base color for the blurred rectangle. |
| pub color: PremulColor, |
| /// A transform that needs to be applied to the position of the first processed pixel. |
| pub transform: Affine, |
| /// How much to advance into the x/y direction for one step in the x direction. |
| pub x_advance: Vec2, |
| /// How much to advance into the x/y direction for one step in the y direction. |
| pub y_advance: Vec2, |
| } |
| |
| impl private::Sealed for BlurredRoundedRectangle {} |
| |
| impl EncodeExt for BlurredRoundedRectangle { |
| fn encode_into(&self, paints: &mut Vec<EncodedPaint>, transform: Affine) -> Paint { |
| let rect = { |
| // Ensure rectangle has positive width/height. |
| let mut rect = self.rect; |
| |
| if self.rect.x0 > self.rect.x1 { |
| core::mem::swap(&mut rect.x0, &mut rect.x1); |
| } |
| |
| if self.rect.y0 > self.rect.y1 { |
| core::mem::swap(&mut rect.x0, &mut rect.x1); |
| } |
| |
| rect |
| }; |
| |
| let transform = Affine::translate((-rect.x0, -rect.y0)) * transform.inverse(); |
| |
| let (x_advance, y_advance) = x_y_advances(&transform); |
| |
| let width = rect.width() as f32; |
| let height = rect.height() as f32; |
| let radius = self.radius.min(0.5 * width.min(height)); |
| |
| // To avoid divide by 0; potentially should be a bigger number for antialiasing. |
| let std_dev = self.std_dev.max(1e-6); |
| |
| let min_edge = width.min(height); |
| let rmax = 0.5 * min_edge; |
| let r0 = radius.hypot(std_dev * 1.15).min(rmax); |
| let r1 = radius.hypot(std_dev * 2.0).min(rmax); |
| |
| let exponent = 2.0 * r1 / r0; |
| |
| let std_dev_inv = std_dev.recip(); |
| |
| // Pull in long end (make less eccentric). |
| let delta = 1.25 |
| * std_dev |
| * ((-(0.5 * std_dev_inv * width).powi(2)).exp() |
| - (-(0.5 * std_dev_inv * height).powi(2)).exp()); |
| let w = width + delta.min(0.0); |
| let h = height - delta.max(0.0); |
| |
| let recip_exponent = exponent.recip(); |
| let scale = 0.5 * compute_erf7(std_dev_inv * 0.5 * (w.max(h) - 0.5 * radius)); |
| |
| let encoded = EncodedBlurredRoundedRectangle { |
| exponent, |
| recip_exponent, |
| width, |
| height, |
| scale, |
| r1, |
| std_dev_inv, |
| min_edge, |
| color: PremulColor::from_alpha_color(self.color), |
| w, |
| h, |
| transform, |
| x_advance, |
| y_advance, |
| }; |
| |
| let idx = paints.len(); |
| paints.push(encoded.into()); |
| |
| Paint::Indexed(IndexedPaint::new(idx)) |
| } |
| } |
| |
| mod private { |
| #[expect(unnameable_types, reason = "Sealed trait pattern.")] |
| pub trait Sealed {} |
| |
| impl Sealed for super::Gradient {} |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::{EncodeExt, Gradient}; |
| use crate::color::DynamicColor; |
| use crate::color::palette::css::{BLACK, BLUE, GREEN}; |
| use crate::kurbo::Point; |
| use crate::peniko::{ColorStop, ColorStops, GradientKind}; |
| use alloc::vec; |
| use smallvec::smallvec; |
| use vello_api::kurbo::Affine; |
| |
| #[test] |
| fn gradient_missing_stops() { |
| let mut buf = vec![]; |
| |
| let gradient = Gradient { |
| kind: GradientKind::Linear { |
| start: Point::new(0.0, 0.0), |
| end: Point::new(20.0, 0.0), |
| }, |
| ..Default::default() |
| }; |
| |
| assert_eq!( |
| gradient.encode_into(&mut buf, Affine::IDENTITY), |
| BLACK.into() |
| ); |
| } |
| |
| #[test] |
| fn gradient_one_stop() { |
| let mut buf = vec![]; |
| |
| let gradient = Gradient { |
| kind: GradientKind::Linear { |
| start: Point::new(0.0, 0.0), |
| end: Point::new(20.0, 0.0), |
| }, |
| stops: ColorStops(smallvec![ColorStop { |
| offset: 0.0, |
| color: DynamicColor::from_alpha_color(GREEN), |
| }]), |
| ..Default::default() |
| }; |
| |
| // Should return the color of the first stop. |
| assert_eq!( |
| gradient.encode_into(&mut buf, Affine::IDENTITY), |
| GREEN.into() |
| ); |
| } |
| |
| #[test] |
| fn gradient_not_padded_stops() { |
| let mut buf = vec![]; |
| |
| let gradient = Gradient { |
| kind: GradientKind::Linear { |
| start: Point::new(0.0, 0.0), |
| end: Point::new(20.0, 0.0), |
| }, |
| stops: ColorStops(smallvec![ |
| ColorStop { |
| offset: 0.0, |
| color: DynamicColor::from_alpha_color(GREEN), |
| }, |
| ColorStop { |
| offset: 0.5, |
| color: DynamicColor::from_alpha_color(BLUE), |
| }, |
| ]), |
| ..Default::default() |
| }; |
| |
| assert_eq!( |
| gradient.encode_into(&mut buf, Affine::IDENTITY), |
| GREEN.into() |
| ); |
| } |
| |
| #[test] |
| fn gradient_not_sorted_stops() { |
| let mut buf = vec![]; |
| |
| let gradient = Gradient { |
| kind: GradientKind::Linear { |
| start: Point::new(0.0, 0.0), |
| end: Point::new(20.0, 0.0), |
| }, |
| stops: ColorStops(smallvec![ |
| ColorStop { |
| offset: 1.0, |
| color: DynamicColor::from_alpha_color(GREEN), |
| }, |
| ColorStop { |
| offset: 0.0, |
| color: DynamicColor::from_alpha_color(BLUE), |
| }, |
| ]), |
| ..Default::default() |
| }; |
| |
| assert_eq!( |
| gradient.encode_into(&mut buf, Affine::IDENTITY), |
| GREEN.into() |
| ); |
| } |
| |
| #[test] |
| fn gradient_linear_degenerate() { |
| let mut buf = vec![]; |
| |
| let gradient = Gradient { |
| kind: GradientKind::Linear { |
| start: Point::new(0.0, 0.0), |
| end: Point::new(0.0, 0.0), |
| }, |
| stops: ColorStops(smallvec![ |
| ColorStop { |
| offset: 0.0, |
| color: DynamicColor::from_alpha_color(GREEN), |
| }, |
| ColorStop { |
| offset: 1.0, |
| color: DynamicColor::from_alpha_color(BLUE), |
| }, |
| ]), |
| ..Default::default() |
| }; |
| |
| assert_eq!( |
| gradient.encode_into(&mut buf, Affine::IDENTITY), |
| GREEN.into() |
| ); |
| } |
| |
| #[test] |
| fn gradient_radial_degenerate() { |
| let mut buf = vec![]; |
| |
| let gradient = Gradient { |
| kind: GradientKind::Radial { |
| start_center: Point::new(0.0, 0.0), |
| start_radius: 20.0, |
| end_center: Point::new(0.0, 0.0), |
| end_radius: 20.0, |
| }, |
| stops: ColorStops(smallvec![ |
| ColorStop { |
| offset: 0.0, |
| color: DynamicColor::from_alpha_color(GREEN), |
| }, |
| ColorStop { |
| offset: 1.0, |
| color: DynamicColor::from_alpha_color(BLUE), |
| }, |
| ]), |
| ..Default::default() |
| }; |
| |
| assert_eq!( |
| gradient.encode_into(&mut buf, Affine::IDENTITY), |
| GREEN.into() |
| ); |
| } |
| } |