use crate::TextureId;
use crate::blurred_rounded_rect::BlurredRoundedRectangle;
use crate::color::palette::css::BLACK;
use crate::color::{ColorSpaceTag, HueDirection, Srgb, gradient};
use crate::geometry::RectU16;
use crate::kurbo::{Affine, Point, Vec2};
use crate::math::{FloatExt, compute_erf7};
use crate::paint::{Image, ImageSource, IndexedPaint, Paint, PremulColor, Tint};
use crate::peniko::{ColorStop, ColorStops, Extend, Gradient, GradientKind, ImageQuality};
use alloc::borrow::Cow;
use alloc::fmt::Debug;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(not(feature = "multithreading"))]
use core::cell::OnceCell;
use core::hash::{Hash, Hasher};
use fearless_simd::{Simd, SimdBase, SimdFloat, f32x4, f32x16, mask32x4, mask32x16};
use peniko::color::cache_key::{BitEq, BitHash, CacheKey};
use peniko::color::gradient_unpremultiplied;
use peniko::{
ImageSampler, InterpolationAlphaSpace, LinearGradientPosition, RadialGradientPosition,
SweepGradientPosition,
};
use smallvec::ToSmallVec;
#[cfg(feature = "multithreading")]
use std::sync::OnceLock as OnceCell;
use crate::simd::{Splat4thExt, element_wise_splat};
#[cfg(not(feature = "std"))]
use peniko::kurbo::common::FloatFuncs as _;
const DEGENERATE_THRESHOLD: f32 = 1.0e-6;
const NUDGE_VAL: f32 = 1.0e-7;
#[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");
}
pub trait EncodeExt: private::Sealed {
fn encode_into(
&self,
paints: &mut Vec<EncodedPaint>,
transform: Affine,
tint: Option<Tint>,
) -> Paint;
}
impl EncodeExt for Gradient {
fn encode_into(
&self,
paints: &mut Vec<EncodedPaint>,
transform: Affine,
_tint: Option<Tint>,
) -> Paint {
if let Err(paint) = validate(self) {
return paint;
}
let mut may_have_transparency = self.stops.iter().any(|s| s.color.components[3] != 1.0);
let mut base_transform;
let mut stops = Cow::Borrowed(&self.stops.0);
let first_stop = &stops[0];
let last_stop = &stops[stops.len() - 1];
if first_stop.offset != 0.0 || last_stop.offset != 1.0 {
let mut vec = stops.to_smallvec();
if first_stop.offset != 0.0 {
let mut first_stop = *first_stop;
first_stop.offset = 0.0;
vec.insert(0, first_stop);
}
if last_stop.offset != 1.0 {
let mut last_stop = *last_stop;
last_stop.offset = 1.0;
vec.push(last_stop);
}
stops = Cow::Owned(vec);
}
let kind = match self.kind {
GradientKind::Linear(LinearGradientPosition { start: p0, end: p1 }) => {
base_transform = ts_from_line_to_line(p0, p1, Point::ZERO, Point::new(1.0, 0.0));
EncodedKind::Linear(LinearKind)
}
GradientKind::Radial(RadialGradientPosition {
start_center: c0,
start_radius: r0,
end_center: c1,
end_radius: r1,
}) => {
let d_radius = r1 - r0;
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,
}
}
};
may_have_transparency |= radial_kind.has_undefined();
EncodedKind::Radial(radial_kind)
}
GradientKind::Sweep(SweepGradientPosition {
center,
start_angle,
end_angle,
}) => {
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,
inv_angle_delta: 1.0 / (end_angle - start_angle),
})
}
};
let ranges = encode_stops(
&stops,
self.interpolation_cs,
self.hue_direction,
self.interpolation_alpha_space,
);
let transform = base_transform * transform.inverse();
let (x_advance, y_advance) = x_y_advances(&transform);
let cache_key = CacheKey(GradientCacheKey {
stops: self.stops.clone(),
interpolation_cs: self.interpolation_cs,
hue_direction: self.hue_direction,
});
let has_undefined = kind.has_undefined();
let encoded = EncodedGradient {
cache_key,
kind,
has_undefined,
transform,
x_advance,
y_advance,
ranges,
extend: self.extend,
may_have_transparency,
u8_lut: OnceCell::new(),
f32_lut: OnceCell::new(),
};
let idx = paints.len();
paints.push(encoded.into());
Paint::Indexed(IndexedPaint::new(idx))
}
}
fn validate(gradient: &Gradient) -> Result<(), Paint> {
let black = Err(BLACK.into());
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;
}
for stops in gradient.stops.windows(2) {
let f = stops[0];
let n = stops[1];
if !(0.0..=1.0).contains(&f.offset) {
return first;
}
if f.offset > n.offset {
return first;
}
}
let last = gradient.stops.last().unwrap();
if !(0.0..=1.0).contains(&last.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(LinearGradientPosition { start, end }) => {
if degenerate_point(start, end) {
return first;
}
}
GradientKind::Radial(RadialGradientPosition {
start_center,
start_radius,
end_center,
end_radius,
}) => {
if *start_radius < 0.0 || *end_radius < 0.0 {
return first;
}
if degenerate_point(start_center, end_center)
&& degenerate_val(*start_radius, *end_radius)
{
return first;
}
}
GradientKind::Sweep(SweepGradientPosition {
start_angle,
end_angle,
..
}) => {
if degenerate_val(*start_angle, *end_angle) {
return first;
}
if end_angle <= start_angle {
return first;
}
}
}
Ok(())
}
fn encode_stops(
stops: &[ColorStop],
cs: ColorSpaceTag,
hue_dir: HueDirection,
interpolation_alpha_space: InterpolationAlphaSpace,
) -> Vec<GradientRange> {
#[derive(Debug)]
struct EncodedColorStop {
offset: f32,
color: crate::color::AlphaColor<Srgb>,
}
let create_range = |left_stop: &EncodedColorStop, right_stop: &EncodedColorStop| {
let clamp = |mut color: [f32; 4]| {
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 = if interpolation_alpha_space == InterpolationAlphaSpace::Unpremultiplied {
clamp(left_stop.color.components)
} else {
clamp(left_stop.color.premultiply().components)
};
let c1 = if interpolation_alpha_space == InterpolationAlphaSpace::Unpremultiplied {
clamp(right_stop.color.components)
} else {
clamp(right_stop.color.premultiply().components)
};
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,
interpolation_alpha_space,
}
};
if cs != ColorSpaceTag::Srgb {
let interpolated_stops = if interpolation_alpha_space
== InterpolationAlphaSpace::Premultiplied
{
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.un_premultiply(),
})
})
.collect::<Vec<_>>()
} else {
stops
.windows(2)
.flat_map(|s| {
let left_stop = &s[0];
let right_stop = &s[1];
let interpolated = gradient_unpremultiplied::<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<_>>()
};
interpolated_stops
.windows(2)
.map(|s| {
let left_stop = &s[0];
let right_stop = &s[1];
create_range(left_stop, right_stop)
})
.collect()
} else {
stops
.windows(2)
.map(|c| {
let c0 = EncodedColorStop {
offset: c[0].offset,
color: c[0].color.to_alpha_color::<Srgb>(),
};
let c1 = EncodedColorStop {
offset: c[1].offset,
color: c[1].color.to_alpha_color::<Srgb>(),
};
create_range(&c0, &c1)
})
.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,
tint: Option<Tint>,
) -> Paint {
let idx = paints.len();
let mut sampler = self.sampler;
if sampler.alpha != 1.0 {
unimplemented!("Applying opacity to image commands");
}
let c = transform.as_coeffs();
if (c[0] as f32 - 1.0).is_nearly_zero()
&& (c[1] as f32).is_nearly_zero()
&& (c[2] as f32).is_nearly_zero()
&& (c[3] as f32 - 1.0).is_nearly_zero()
&& ((c[4] - c[4].floor()) as f32).is_nearly_zero()
&& ((c[5] - c[5].floor()) as f32).is_nearly_zero()
&& sampler.quality == ImageQuality::Medium
{
sampler.quality = ImageQuality::Low;
}
let transform = transform.inverse();
let (x_advance, y_advance) = x_y_advances(&transform);
let tint_has_opacity = tint.as_ref().is_some_and(|t| t.color.components[3] < 1.0);
let encoded = EncodedImage {
may_have_transparency: self.image.may_have_transparency() || tint_has_opacity,
source: self.image.clone(),
sampler,
transform,
x_advance,
y_advance,
tint,
};
paints.push(EncodedPaint::Image(encoded));
Paint::Indexed(IndexedPaint::new(idx))
}
}
#[derive(Debug)]
pub enum EncodedPaint {
Gradient(EncodedGradient),
Image(EncodedImage),
ExternalTexture(EncodedExternalTexture),
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)
}
}
#[derive(Debug)]
pub struct EncodedImage {
pub source: ImageSource,
pub sampler: ImageSampler,
pub may_have_transparency: bool,
pub transform: Affine,
pub x_advance: Vec2,
pub y_advance: Vec2,
pub tint: Option<Tint>,
}
#[derive(Debug)]
pub struct EncodedExternalTexture {
pub texture_id: TextureId,
pub source_region: RectU16,
pub sampler: ImageSampler,
pub may_have_transparency: bool,
pub transform: Affine,
pub tint: Option<Tint>,
}
#[derive(Debug, Copy, Clone)]
pub struct LinearKind;
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct FocalData {
pub fr1: f32,
pub f_focal_x: f32,
pub f_is_swapped: bool,
}
impl FocalData {
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
}
pub fn is_focal_on_circle(&self) -> bool {
(1.0 - self.fr1).is_nearly_zero()
}
pub fn is_swapped(&self) -> bool {
self.f_is_swapped
}
pub fn is_well_behaved(&self) -> bool {
!self.is_focal_on_circle() && self.fr1 > 1.0
}
pub fn is_natively_focal(&self) -> bool {
self.f_focal_x.is_nearly_zero()
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum RadialKind {
Radial {
bias: f32,
scale: f32,
},
Strip {
scaled_r0_squared: f32,
},
Focal {
focal_data: FocalData,
fp0: f32,
fp1: f32,
},
}
impl RadialKind {
pub fn has_undefined(&self) -> bool {
match self {
Self::Radial { .. } => false,
Self::Strip { .. } => true,
Self::Focal { focal_data, .. } => !focal_data.is_well_behaved(),
}
}
}
#[derive(Debug)]
pub struct SweepKind {
pub start_angle: f32,
pub inv_angle_delta: f32,
}
#[derive(Debug)]
pub enum EncodedKind {
Linear(LinearKind),
Radial(RadialKind),
Sweep(SweepKind),
}
impl EncodedKind {
fn has_undefined(&self) -> bool {
match self {
Self::Radial(radial_kind) => radial_kind.has_undefined(),
_ => false,
}
}
}
#[derive(Debug)]
pub struct EncodedGradient {
pub cache_key: CacheKey<GradientCacheKey>,
pub kind: EncodedKind,
pub has_undefined: bool,
pub transform: Affine,
pub x_advance: Vec2,
pub y_advance: Vec2,
pub ranges: Vec<GradientRange>,
pub extend: Extend,
pub may_have_transparency: bool,
u8_lut: OnceCell<GradientLut<u8>>,
f32_lut: OnceCell<GradientLut<f32>>,
}
impl EncodedGradient {
pub fn u8_lut<S: Simd>(&self, simd: S) -> &GradientLut<u8> {
self.u8_lut
.get_or_init(|| GradientLut::new(simd, &self.ranges))
}
pub fn f32_lut<S: Simd>(&self, simd: S) -> &GradientLut<f32> {
self.f32_lut
.get_or_init(|| GradientLut::new(simd, &self.ranges))
}
}
#[derive(Debug, Clone)]
pub struct GradientCacheKey {
pub stops: ColorStops,
pub interpolation_cs: ColorSpaceTag,
pub hue_direction: HueDirection,
}
impl BitHash for GradientCacheKey {
fn bit_hash<H: Hasher>(&self, state: &mut H) {
self.stops.bit_hash(state);
core::mem::discriminant(&self.interpolation_cs).hash(state);
core::mem::discriminant(&self.hue_direction).hash(state);
}
}
impl BitEq for GradientCacheKey {
fn bit_eq(&self, other: &Self) -> bool {
self.stops.bit_eq(&other.stops)
&& self.interpolation_cs == other.interpolation_cs
&& self.hue_direction == other.hue_direction
}
}
#[derive(Debug, Clone)]
pub struct GradientRange {
pub x1: f32,
pub bias: [f32; 4],
pub scale: [f32; 4],
pub interpolation_alpha_space: InterpolationAlphaSpace,
}
#[derive(Debug)]
pub struct EncodedBlurredRoundedRectangle {
pub exponent: f32,
pub recip_exponent: f32,
pub scale: f32,
pub std_dev_inv: f32,
pub min_edge: f32,
pub w: f32,
pub h: f32,
pub width: f32,
pub height: f32,
pub r1: f32,
pub color: PremulColor,
pub transform: Affine,
pub x_advance: Vec2,
pub y_advance: Vec2,
}
impl private::Sealed for BlurredRoundedRectangle {}
impl EncodeExt for BlurredRoundedRectangle {
fn encode_into(
&self,
paints: &mut Vec<EncodedPaint>,
transform: Affine,
_tint: Option<Tint>,
) -> Paint {
let rect = {
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.y0, &mut rect.y1);
}
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));
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();
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))
}
}
fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine {
let unit_to_line1 = unit_to_line(src1, src2);
let line1_to_unit = unit_to_line1.inverse();
let unit_to_line2 = unit_to_line(dst1, dst2);
unit_to_line2 * line1_to_unit
}
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,
])
}
pub trait FromF32Color: Sized + Debug + Copy + Clone {
const ZERO: Self;
fn from_f32<S: Simd>(color: f32x4<S>) -> [Self; 4];
}
impl FromF32Color for f32 {
const ZERO: Self = 0.0;
fn from_f32<S: Simd>(color: f32x4<S>) -> [Self; 4] {
color.into()
}
}
impl FromF32Color for u8 {
const ZERO: Self = 0;
fn from_f32<S: Simd>(mut color: f32x4<S>) -> [Self; 4] {
let simd = color.simd;
color = color.mul_add(f32x4::splat(simd, 255.0), f32x4::splat(simd, 0.5));
[
color[0] as Self,
color[1] as Self,
color[2] as Self,
color[3] as Self,
]
}
}
#[derive(Debug)]
pub struct GradientLut<T: FromF32Color> {
lut: Vec<[T; 4]>,
scale: f32,
}
impl<T: FromF32Color> GradientLut<T> {
fn new<S: Simd>(simd: S, ranges: &[GradientRange]) -> Self {
let lut_size = determine_lut_size(ranges);
let mut lut = vec![[T::ZERO; 4]; lut_size];
let ramps = {
let mut ramps = Vec::with_capacity(ranges.len());
let mut prev_idx = 0;
for range in ranges {
let max_idx = (range.x1 * lut_size as f32) as usize;
ramps.push((prev_idx..max_idx, range));
prev_idx = max_idx;
}
ramps
};
let scale = lut_size as f32 - 1.0;
let inv_lut_scale = f32x4::splat(simd, 1.0 / scale);
let add_factor = f32x4::from_slice(simd, &[0.0, 1.0, 2.0, 3.0]) * inv_lut_scale;
for (ramp_range, range) in ramps {
let biases = f32x16::block_splat(f32x4::from_slice(simd, &range.bias));
let scales = f32x16::block_splat(f32x4::from_slice(simd, &range.scale));
ramp_range.clone().step_by(4).for_each(|idx| {
let t_vals = f32x4::splat(simd, idx as f32).mul_add(inv_lut_scale, add_factor);
let t_vals = element_wise_splat(simd, t_vals);
let mut result = scales.mul_add(t_vals, biases);
let alphas = result.splat_4th();
if range.interpolation_alpha_space == InterpolationAlphaSpace::Unpremultiplied {
result = {
let mask =
mask32x16::block_splat(mask32x4::from_slice(simd, &[-1, -1, -1, 0]));
simd.select_f32x16(mask, result * alphas, alphas)
};
}
result = result.min(1.0).min(alphas);
let (im1, im2) = simd.split_f32x16(result);
let (r1, r2) = simd.split_f32x8(im1);
let (r3, r4) = simd.split_f32x8(im2);
let rs = [r1, r2, r3, r4].map(T::from_f32);
let lut = &mut lut[idx..(idx + 4).min(lut_size)];
lut.copy_from_slice(&rs[..lut.len()]);
});
}
Self { lut, scale }
}
#[inline(always)]
pub fn get(&self, idx: usize) -> [T; 4] {
self.lut[idx]
}
#[inline(always)]
pub fn lut(&self) -> &[[T; 4]] {
&self.lut
}
#[inline(always)]
pub fn width(&self) -> usize {
self.lut.len()
}
#[inline(always)]
pub fn scale_factor(&self) -> f32 {
self.scale
}
}
pub const MAX_GRADIENT_LUT_SIZE: usize = 4096;
fn determine_lut_size(ranges: &[GradientRange]) -> usize {
let stop_len = match ranges.len() {
1 => 256,
2 => 512,
_ => 1024,
};
let mut last_x1 = 0.0;
let mut min_size = 0;
for x1 in ranges.iter().map(|e| e.x1) {
let res = ((1.0 / (x1 - last_x1)).ceil() as usize)
.min(MAX_GRADIENT_LUT_SIZE)
.next_power_of_two();
min_size = min_size.max(res);
last_x1 = x1;
}
stop_len.max(min_size)
}
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::{Affine, Point};
use crate::peniko::{ColorStop, ColorStops};
use alloc::vec;
use peniko::{LinearGradientPosition, RadialGradientPosition};
use smallvec::smallvec;
#[test]
fn gradient_missing_stops() {
let mut buf = vec![];
let gradient = Gradient {
kind: LinearGradientPosition {
start: Point::new(0.0, 0.0),
end: Point::new(20.0, 0.0),
}
.into(),
..Default::default()
};
assert_eq!(
gradient.encode_into(&mut buf, Affine::IDENTITY, None),
BLACK.into()
);
}
#[test]
fn gradient_one_stop() {
let mut buf = vec![];
let gradient = Gradient {
kind: LinearGradientPosition {
start: Point::new(0.0, 0.0),
end: Point::new(20.0, 0.0),
}
.into(),
stops: ColorStops(smallvec![ColorStop {
offset: 0.0,
color: DynamicColor::from_alpha_color(GREEN),
}]),
..Default::default()
};
assert_eq!(
gradient.encode_into(&mut buf, Affine::IDENTITY, None),
GREEN.into()
);
}
#[test]
fn gradient_not_sorted_stops() {
let mut buf = vec![];
let gradient = Gradient {
kind: LinearGradientPosition {
start: Point::new(0.0, 0.0),
end: Point::new(20.0, 0.0),
}
.into(),
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, None),
GREEN.into()
);
}
#[test]
fn gradient_linear_degenerate() {
let mut buf = vec![];
let gradient = Gradient {
kind: LinearGradientPosition {
start: Point::new(0.0, 0.0),
end: Point::new(0.0, 0.0),
}
.into(),
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, None),
GREEN.into()
);
}
#[test]
fn gradient_last_stop_with_infinity_offset() {
let mut buf = vec![];
let gradient = Gradient {
kind: LinearGradientPosition {
start: Point::new(0.0, 0.0),
end: Point::new(20.0, 0.0),
}
.into(),
stops: ColorStops(smallvec![
ColorStop {
offset: 0.0,
color: DynamicColor::from_alpha_color(GREEN),
},
ColorStop {
offset: f32::INFINITY,
color: DynamicColor::from_alpha_color(BLUE),
},
]),
..Default::default()
};
assert_eq!(
gradient.encode_into(&mut buf, Affine::IDENTITY, None),
GREEN.into()
);
}
#[test]
fn gradient_stop_with_nan_offset() {
let mut buf = vec![];
let gradient = Gradient {
kind: LinearGradientPosition {
start: Point::new(0.0, 0.0),
end: Point::new(20.0, 0.0),
}
.into(),
stops: ColorStops(smallvec![
ColorStop {
offset: 0.0,
color: DynamicColor::from_alpha_color(GREEN),
},
ColorStop {
offset: f32::NAN,
color: DynamicColor::from_alpha_color(BLUE),
},
]),
..Default::default()
};
assert_eq!(
gradient.encode_into(&mut buf, Affine::IDENTITY, None),
GREEN.into()
);
}
#[test]
fn gradient_radial_degenerate() {
let mut buf = vec![];
let gradient = Gradient {
kind: RadialGradientPosition {
start_center: Point::new(0.0, 0.0),
start_radius: 20.0,
end_center: Point::new(0.0, 0.0),
end_radius: 20.0,
}
.into(),
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, None),
GREEN.into()
);
}
}