blob: d9393773054da25702240428fc51971329dd4ca4 [file]
// 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::{FloatExt, compute_erf7};
use crate::paint::{Image, IndexedPaint, Paint, PremulColor};
use crate::peniko::{ColorStop, Extend, Gradient, GradientKind, ImageQuality};
use crate::pixmap::Pixmap;
use alloc::borrow::Cow;
use alloc::sync::Arc;
use alloc::vec::Vec;
#[cfg(not(feature = "multithreading"))]
use core::cell::OnceCell;
use core::f32::consts::PI;
use core::iter;
#[cfg(feature = "multithreading")]
use once_cell::sync::OnceCell;
use smallvec::SmallVec;
#[cfg(not(feature = "std"))]
use peniko::kurbo::common::FloatFuncs as _;
const DEGENERATE_THRESHOLD: f32 = 1.0e-6;
const NUDGE_VAL: f32 = 1.0e-7;
const PIXEL_CENTER_OFFSET: f64 = 0.5;
#[cfg(feature = "std")]
fn exp(val: f32) -> f32 {
val.exp()
}
#[cfg(not(feature = "std"))]
fn exp(val: f32) -> f32 {
#[cfg(feature = "libm")]
return libm::expf(val);
#[cfg(not(feature = "libm"))]
compile_error!("vello_common requires either the `std` or `libm` feature");
}
/// 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 base_transform;
let mut stops = Cow::Borrowed(&self.stops.0);
let kind = match self.kind {
GradientKind::Linear {
start: p0,
end: mut p1,
} => {
// 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));
}
// We update the transform currently in-place, such that the gradient line always
// starts at the point (0, 0) and ends at the point (1, 0). This simplifies the
// calculation for the current position along the gradient line a lot.
base_transform = ts_from_line_to_line(p0, p1, Point::ZERO, Point::new(1.0, 0.0));
EncodedKind::Linear(LinearKind)
}
GradientKind::Radial {
start_center: c0,
start_radius: r0,
end_center: mut c1,
end_radius: mut r1,
} => {
// The implementation of radial gradients is translated from Skia.
// See:
// - <https://skia.org/docs/dev/design/conical/>
// - <https://github.com/google/skia/blob/main/src/shaders/gradients/SkConicalGradient.h>
// - <https://github.com/google/skia/blob/main/src/shaders/gradients/SkConicalGradient.cpp>
// 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));
}
let d_radius = r1 - r0;
// <https://github.com/google/skia/blob/1e07a4b16973cf716cb40b72dd969e961f4dd950/src/shaders/gradients/SkConicalGradient.cpp#L83-L112>
let radial_kind = if ((c1 - c0).length() as f32).is_nearly_zero() {
base_transform = Affine::translate((-c1.x, -c1.y));
base_transform = base_transform.then_scale(1.0 / r0.max(r1) as f64);
let scale = r1.max(r0) / d_radius;
let bias = -r0 / d_radius;
RadialKind::Radial { bias, scale }
} else {
base_transform =
ts_from_line_to_line(c0, c1, Point::ZERO, Point::new(1.0, 0.0));
if (r1 - r0).is_nearly_zero() {
let scaled_r0 = r1 / (c1 - c0).length() as f32;
RadialKind::Strip {
scaled_r0_squared: scaled_r0 * scaled_r0,
}
} else {
let d_center = (c0 - c1).length() as f32;
let focal_data =
FocalData::create(r0 / d_center, r1 / d_center, &mut base_transform);
let fp0 = 1.0 / focal_data.fr1;
let fp1 = focal_data.f_focal_x;
RadialKind::Focal {
focal_data,
fp0,
fp1,
}
}
};
// Even if the gradient has no stops with transparency, we might have to force
// alpha-compositing in case the radial gradient is undefined in certain positions,
// in which case the resulting color will be transparent and thus the gradient overall
// must be treated as non-opaque.
has_opacities |= radial_kind.has_undefined();
EncodedKind::Radial(radial_kind)
}
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), to make
// angle calculation easier.
let x_offset = -center.x as f32;
let y_offset = -center.y as f32;
base_transform = Affine::translate((x_offset as f64, y_offset as f64));
EncodedKind::Sweep(SweepKind {
start_angle,
// Save the inverse so that we can use a multiplication in the shader instead.
inv_angle_delta: 1.0 / (end_angle - start_angle),
})
}
};
let ranges = encode_stops(&stops, 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 the base transform of the shader, 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_ paint transform to the point so that we can account
// for the paint transform of the render context.
let transform = base_transform
* transform.inverse()
* Affine::translate((PIXEL_CENTER_OFFSET, PIXEL_CENTER_OFFSET));
// 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 in each wide tile,
// and from then on only apply incremental updates to the current x/y position
// that we calculate based on the transform.
//
// 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,
u8_lut: OnceCell::new(),
f32_lut: OnceCell::new(),
};
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],
pad: bool,
cs: ColorSpaceTag,
hue_dir: HueDirection,
) -> Vec<GradientRange> {
struct EncodedColorStop {
offset: f32,
color: crate::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 = left_stop.offset;
let x1 = right_stop.offset;
let c0 = clamp(left_stop.color.components);
let c1 = clamp(right_stop.color.components);
// We calculate a bias and scale factor, such that we can simply calculate
// bias + x * scale to get the interpolated color, where x is between x0 and x1,
// to calculate the resulting color.
// Apply a nudge value because we sometimes call `create_range` with the same offset
// to create the padded stops.
let x1_minus_x0 = (x1 - x0).max(NUDGE_VAL);
let mut scale = [0.0; 4];
let mut bias = c0;
for i in 0..4 {
scale[i] = (c1[i] - c0[i]) / x1_minus_x0;
bias[i] = c0[i] - x0 * scale[i];
}
GradientRange { x1, bias, scale }
};
// 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();
create_range(first_stop, first_stop)
});
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();
// Similarly to gradients, apply a 0.5 offset so we sample at the center of
// a pixel.
let transform = transform.inverse() * Affine::translate((0.5, 0.5));
// 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;
/// Focal data for a radial gradient.
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct FocalData {
fr1: f32,
f_focal_x: f32,
f_is_swapped: bool,
}
impl FocalData {
/// Create a new `FocalData` with the given radii and update the matrix.
pub fn create(mut r0: f32, mut r1: f32, matrix: &mut Affine) -> Self {
let mut swapped = false;
let mut f_focal_x = r0 / (r0 - r1);
if (f_focal_x - 1.0).is_nearly_zero() {
*matrix = matrix.then_translate(Vec2::new(-1.0, 0.0));
*matrix = matrix.then_scale_non_uniform(-1.0, 1.0);
core::mem::swap(&mut r0, &mut r1);
f_focal_x = 0.0;
swapped = true;
}
let focal_matrix = ts_from_line_to_line(
Point::new(f_focal_x as f64, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
);
*matrix = focal_matrix * *matrix;
let fr1 = r1 / (1.0 - f_focal_x).abs();
let data = Self {
fr1,
f_focal_x,
f_is_swapped: swapped,
};
if data.is_focal_on_circle() {
*matrix = matrix.then_scale(0.5);
} else {
*matrix = matrix.then_scale_non_uniform(
(fr1 / (fr1 * fr1 - 1.0)) as f64,
1.0 / (fr1 * fr1 - 1.0).abs().sqrt() as f64,
);
}
*matrix = matrix.then_scale((1.0 - f_focal_x).abs() as f64);
data
}
/// Whether the focal is on the circle.
pub fn is_focal_on_circle(&self) -> bool {
(1.0 - self.fr1).is_nearly_zero()
}
/// Whether the focal points have been swapped.
pub fn is_swapped(&self) -> bool {
self.f_is_swapped
}
/// Whether the gradient is well-behaved.
pub fn is_well_behaved(&self) -> bool {
!self.is_focal_on_circle() && self.fr1 > 1.0
}
/// Whether the gradient is natively focal.
pub fn is_natively_focal(&self) -> bool {
self.f_focal_x.is_nearly_zero()
}
}
/// A radial gradient.
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum RadialKind {
/// A radial gradient, i.e. the start and end center points are the same.
Radial {
/// The `bias` value (from the Skia implementation).
///
/// It is a correction factor that accounts for the fact that the focal center might not
/// lie on the inner circle (if r0 > 0).
bias: f32,
/// The `scale` value (from the Skia implementation).
///
/// It is a scaling factor that maps from r0 to r1.
scale: f32,
},
/// A strip gradient, i.e. the start and end radius are the same.
Strip {
/// The squared value of `scaled_r0` (from the Skia implementation).
scaled_r0_squared: f32,
},
/// A general, two-point conical gradient.
Focal {
/// The focal data (from the Skia implementation).
focal_data: FocalData,
/// The `fp0` value (from the Skia implementation).
fp0: f32,
/// The `fp1` value (from the Skia implementation).
fp1: f32,
},
}
impl RadialKind {
fn pos_inner(&self, pos: Point) -> Option<f32> {
match self {
Self::Radial { bias, scale } => {
let mut radius = pos.to_vec2().length() as f32;
radius = bias + radius * scale;
Some(radius)
}
Self::Strip { scaled_r0_squared } => {
let p1 = scaled_r0_squared - pos.y as f32 * pos.y as f32;
if p1 < 0.0 {
None
} else {
Some(pos.x as f32 + p1.sqrt())
}
}
Self::Focal {
focal_data,
fp0,
fp1,
} => {
let x = pos.x as f32;
let y = pos.y as f32;
let mut t = if focal_data.is_focal_on_circle() {
// xy_to_2pt_conical_focal_on_circle
x + y * y / x
} else if focal_data.is_well_behaved() {
// xy_to_2pt_conical_well_behaved
(x * x + y * y).sqrt() - x * fp0
} else if focal_data.is_swapped() || (1.0 - focal_data.f_focal_x < 0.0) {
// xy_to_2pt_conical_smaller
-(x * x - y * y).sqrt() - x * fp0
} else {
// xy_to_2pt_conical_greater
(x * x - y * y).sqrt() - x * fp0
};
if !focal_data.is_well_behaved() {
// mask_2pt_conical_degenerates
let is_degenerate = t <= 0.0 || t.is_nan();
if is_degenerate {
return None;
}
}
if 1.0 - focal_data.f_focal_x < 0.0 {
// negate_x
t = -t;
}
if !focal_data.is_natively_focal() {
// alter_2pt_conical_compensate_focal
t += fp1;
}
if focal_data.is_swapped() {
// alter_2pt_conical_unswap
t = 1.0 - t;
}
Some(t)
}
}
}
}
/// Computed properties of a sweep gradient.
#[derive(Debug)]
pub struct SweepKind {
start_angle: f32,
inv_angle_delta: f32,
}
/// 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.
ranges: Vec<GradientRange>,
/// Whether the gradient should be padded.
pub pad: bool,
/// Whether the gradient requires `source_over` compositing.
pub has_opacities: bool,
u8_lut: OnceCell<GradientLut<u8>>,
f32_lut: OnceCell<GradientLut<f32>>,
}
impl EncodedGradient {
/// Get the lookup table for sampling u8-based gradient values.
pub fn u8_lut(&self) -> &GradientLut<u8> {
self.u8_lut.get_or_init(|| GradientLut::new(&self.ranges))
}
/// Get the lookup table for sampling f32-based gradient values.
pub fn f32_lut(&self) -> &GradientLut<f32> {
self.f32_lut.get_or_init(|| GradientLut::new(&self.ranges))
}
}
/// An encoded ange between two color stops.
#[derive(Debug, Clone)]
pub struct GradientRange {
/// The end value of the range.
pub x1: f32,
/// A bias to apply when interpolating the color (in this case just the values of the start
/// color of the gradient).
pub bias: [f32; 4],
/// The scale factors of the range. By calculating bias + x * factors (where x is
/// between 0.0 and 1.0), we can interpolate between start and end color of the gradient range.
pub scale: [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);
let adjusted_angle = if angle >= 0.0 {
angle
} else {
angle + 2.0 * PI
};
(adjusted_angle - self.start_angle) * self.inv_angle_delta
}
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 along a linear gradient is determined by where we are along the
// gradient line. Since during encoding, we have applied a transformation such that
// the gradient line always goes from (0, 0) to (1, 0), the position along the
// gradient line is simply determined by the current x coordinate!
pos.x as f32
}
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 {
match self {
Self::Radial { .. } => false,
Self::Strip { .. } => true,
Self::Focal { focal_data, .. } => !focal_data.is_well_behaved(),
}
}
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
* (exp(-(0.5 * std_dev_inv * width).powi(2))
- exp(-(0.5 * std_dev_inv * height).powi(2)));
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))
}
}
/// Calculates the transform necessary to map the line spanned by points src1, src2 to
/// the line spanned by dst1, dst2.
///
/// This creates a transformation that maps any line segment to any other line segment.
/// For gradients, we use this to transform the gradient line to a standard form (0,0) → (1,0).
///
/// Copied from <https://github.com/linebender/tiny-skia/blob/68b198a7210a6bbf752b43d6bc4db62445730313/src/shaders/radial_gradient.rs#L182>
fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine {
let unit_to_line1 = unit_to_line(src1, src2);
// Calculate the transform necessary to map line1 to the unit vector.
let line1_to_unit = unit_to_line1.inverse();
// Then map the unit vector to line2.
let unit_to_line2 = unit_to_line(dst1, dst2);
unit_to_line2 * line1_to_unit
}
/// Calculate the transform necessary to map the unit vector to the line spanned by the points
/// `p1` and `p2`.
fn unit_to_line(p0: Point, p1: Point) -> Affine {
Affine::new([
p1.y - p0.y,
p0.x - p1.x,
p1.x - p0.x,
p1.y - p0.y,
p0.x,
p0.y,
])
}
mod private {
#[expect(unnameable_types, reason = "Sealed trait pattern.")]
pub trait Sealed {}
impl Sealed for super::Gradient {}
}
/// A helper trait for converting a premultiplied f32 color to `Self`.
pub trait FromF32Color: Sized {
/// Convert from a premultiplied f32 color to `Self`.
fn from_f32(color: &[f32; 4]) -> [Self; 4];
}
impl FromF32Color for f32 {
fn from_f32(color: &[f32; 4]) -> [Self; 4] {
*color
}
}
impl FromF32Color for u8 {
fn from_f32(color: &[f32; 4]) -> [Self; 4] {
[
(color[0] * 255.0 + 0.5) as Self,
(color[1] * 255.0 + 0.5) as Self,
(color[2] * 255.0 + 0.5) as Self,
(color[3] * 255.0 + 0.5) as Self,
]
}
}
/// A lookup table for sampled gradient values.
#[derive(Debug)]
pub struct GradientLut<T: Copy + Clone + FromF32Color> {
// Note that this representation of sampled values _might_ change in the future.
// Right now, we are storing them on a pixel-basis, but in the future we might instead store
// the values by their channel (i.e. store R, G, B, A values separately).
lut: Vec<[T; 4]>,
scale: f32,
}
impl<T: Copy + Clone + FromF32Color> GradientLut<T> {
/// Create a new lookup table.
fn new(ranges: &[GradientRange]) -> Self {
// Somewhat arbitrary, but we use 1024 samples for
// more than 2 stops, and 512 for just 2 stops. Blend2D does
// something similar.
let lut_size = if ranges.len() > 1 { 1024 } else { 512 };
let mut lut = Vec::with_capacity(lut_size);
let inv_lut_size = 1.0 / lut_size as f32;
let mut cur_idx = 0;
(0..lut_size).for_each(|idx| {
let t_val = idx as f32 * inv_lut_size;
while ranges[cur_idx].x1 < t_val {
cur_idx += 1;
}
let range = &ranges[cur_idx];
let mut interpolated = [0.0_f32; 4];
let bias = range.bias;
for (comp_idx, comp) in interpolated.iter_mut().enumerate() {
*comp = bias[comp_idx] + range.scale[comp_idx] * t_val;
}
lut.push(T::from_f32(&interpolated));
});
let scale = lut.len() as f32 - 1.0;
Self { lut, scale }
}
/// Get the sample value at a specific index.
#[inline(always)]
pub fn get(&self, idx: usize) -> [T; 4] {
self.lut[idx]
}
/// Get the scale factor by which to scale the parametric value to
/// compute the correct lookup index.
#[inline(always)]
pub fn scale_factor(&self) -> f32 {
self.scale
}
}
#[cfg(test)]
mod tests {
use super::{EncodeExt, Gradient};
use crate::color::DynamicColor;
use crate::color::palette::css::{BLACK, BLUE, GREEN};
use crate::kurbo::{Affine, Point};
use crate::peniko::{ColorStop, ColorStops, GradientKind};
use alloc::vec;
use smallvec::smallvec;
#[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()
);
}
}