use cssparser::{Parser, Token, match_ignore_ascii_case};
use std::fmt;
use tiny_skia::PremultipliedColorU8;
use typed_builder::TypedBuilder;
use super::gradient_utils::{
GradientOverlayTile, adaptive_lut_size_with_visible_samples, build_color_lut_with_interpolation,
resolve_stops_along_axis,
};
use crate::{
layout::style::{
ColorInterpolationMethod, CssDescriptorKind, CssToken, FromCss, GradientStop, GradientStops,
Length, LengthDefaultsToZero, MakeComputed, ObjectPosition, ParseResult, ResolvedGradientStop,
ToCss, declare_enum_from_css_impl, unexpected_token,
},
rendering::{RenderContext, Sizing},
};
#[derive(Debug, Clone, PartialEq, TypedBuilder)]
#[non_exhaustive]
pub struct RadialGradient {
#[builder(default)]
pub repeating: bool,
#[builder(default)]
pub shape: RadialShape,
#[builder(default)]
pub size: RadialSize,
#[builder(default)]
pub center: ObjectPosition,
#[builder(default)]
pub interpolation: ColorInterpolationMethod,
#[builder(setter(into))]
pub stops: Box<[GradientStop]>,
}
impl MakeComputed for RadialGradient {
fn make_computed(&mut self, sizing: &Sizing) {
self.center.make_computed(sizing);
self.stops.make_computed(sizing);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub enum RadialShape {
Circle,
#[default]
Ellipse,
}
declare_enum_from_css_impl!(
RadialShape,
"circle" => RadialShape::Circle,
"ellipse" => RadialShape::Ellipse,
);
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub enum RadialSize {
ClosestSide,
FarthestSide,
ClosestCorner,
#[default]
FarthestCorner,
Explicit {
radius_x: LengthDefaultsToZero,
radius_y: LengthDefaultsToZero,
},
}
impl<'i> FromCss<'i> for RadialSize {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let location = input.current_source_location();
let ident = input.expect_ident()?;
match_ignore_ascii_case! { &ident,
"closest-side" => Ok(RadialSize::ClosestSide),
"farthest-side" => Ok(RadialSize::FarthestSide),
"closest-corner" => Ok(RadialSize::ClosestCorner),
"farthest-corner" => Ok(RadialSize::FarthestCorner),
_ => Err(unexpected_token!(location, &Token::Ident(ident.clone()))),
}
}
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Keyword("closest-side"),
CssToken::Keyword("farthest-side"),
CssToken::Keyword("closest-corner"),
CssToken::Keyword("farthest-corner"),
];
}
impl MakeComputed for RadialSize {}
#[derive(Debug, Clone)]
pub(crate) struct RadialGradientTile {
pub width: u32,
pub height: u32,
pub cx: f32,
pub cy: f32,
pub inv_radius_x: f32,
pub inv_radius_y: f32,
pub radius_scale: f32,
pub repeating: bool,
pub repeat_start: f32,
pub repeat_period: f32,
pub position_to_lut_scale: f32,
pub fully_opaque: bool,
pub color_lut: Vec<PremultipliedColorU8>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct RadialGradientRowState {
dx2: f32,
dx2_step: f32,
dx2_step_delta: f32,
dy2: f32,
max_lut_index: usize,
}
impl RadialGradientTile {
#[inline(always)]
pub(crate) fn outer_sample(&self) -> Option<PremultipliedColorU8> {
self.color_lut.last().copied()
}
#[inline(always)]
pub(crate) fn non_repeating_active_span(
&self,
src_x_start: u32,
src_x_end: u32,
src_y: u32,
) -> Option<(u32, u32)> {
if self.repeating || src_x_start >= src_x_end {
return None;
}
let dy = (src_y as f32 - self.cy) * self.inv_radius_y;
let dy2 = dy * dy;
if dy2 >= 1.0 {
return Some((src_x_start, src_x_start));
}
let max_dx = (1.0 - dy2).sqrt() / self.inv_radius_x;
let active_start = (self.cx - max_dx).floor() as i32 + 1;
let active_end = (self.cx + max_dx).ceil() as i32;
let clamped_start = active_start.max(src_x_start as i32).min(src_x_end as i32) as u32;
let clamped_end = active_end.max(clamped_start as i32).min(src_x_end as i32) as u32;
Some((clamped_start, clamped_end))
}
#[inline(always)]
pub(crate) fn lut_index_for_distance_px_with_len(
&self,
distance_px: f32,
lut_len: usize,
) -> usize {
if lut_len <= 1 {
return 0;
}
let position_px = if self.repeating && self.repeat_period > 1e-6 {
(distance_px - self.repeat_start).rem_euclid(self.repeat_period)
} else {
distance_px.clamp(0.0, self.radius_scale)
};
((position_px * self.position_to_lut_scale).round() as usize).min(lut_len - 1)
}
pub fn new(gradient: &RadialGradient, width: u32, height: u32, context: &RenderContext) -> Self {
let cx = Length::from(gradient.center.0.x).to_px(&context.sizing, width as f32);
let cy = Length::from(gradient.center.0.y).to_px(&context.sizing, height as f32);
let dx_left = cx;
let dx_right = width as f32 - cx;
let dy_top = cy;
let dy_bottom = height as f32 - cy;
let (radius_x, radius_y) = match (gradient.shape, gradient.size) {
(shape, RadialSize::Explicit { radius_x, radius_y }) => {
let resolved_radius_x = radius_x.to_px(&context.sizing, width as f32).max(0.0);
let resolved_radius_y = radius_y.to_px(&context.sizing, height as f32).max(0.0);
match shape {
RadialShape::Circle => {
let r = resolved_radius_x.max(resolved_radius_y);
(r, r)
}
RadialShape::Ellipse => (resolved_radius_x, resolved_radius_y),
}
}
(RadialShape::Ellipse, RadialSize::FarthestCorner) => {
(dx_left.max(dx_right), dy_top.max(dy_bottom))
}
(RadialShape::Circle, RadialSize::FarthestCorner) => {
let candidates = [
(cx, cy),
(cx, dy_bottom),
(dx_right, cy),
(dx_right, dy_bottom),
];
let r = candidates
.iter()
.map(|(dx, dy)| (dx * dx + dy * dy).sqrt())
.fold(0.0_f32, f32::max);
(r, r)
}
(RadialShape::Ellipse, RadialSize::FarthestSide) => {
(dx_left.max(dx_right), dy_top.max(dy_bottom))
}
(RadialShape::Ellipse, RadialSize::ClosestSide) => {
(dx_left.min(dx_right), dy_top.min(dy_bottom))
}
(RadialShape::Circle, RadialSize::FarthestSide) => {
let r = dx_left.max(dx_right).max(dy_top.max(dy_bottom));
(r, r)
}
(RadialShape::Circle, RadialSize::ClosestSide) => {
let r = dx_left.min(dx_right).min(dy_top.min(dy_bottom));
(r, r)
}
(RadialShape::Ellipse, RadialSize::ClosestCorner) => {
let f_rx = dx_left.max(dx_right);
let f_ry = dy_top.max(dy_bottom);
let corners = [
(dx_left, dy_top),
(dx_right, dy_top),
(dx_left, dy_bottom),
(dx_right, dy_bottom),
];
let distances = corners.map(|(dx, dy)| (dx * dx + dy * dy).sqrt());
let dist_to_closest_corner = distances.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let dist_to_farthest_corner = distances.iter().fold(0.0f32, |a, &b| a.max(b));
let ratio = dist_to_closest_corner / dist_to_farthest_corner.max(1e-6);
(f_rx * ratio, f_ry * ratio)
}
(RadialShape::Circle, RadialSize::ClosestCorner) => {
let candidates = [
(cx, cy),
(cx, dy_bottom),
(dx_right, cy),
(dx_right, dy_bottom),
];
let r = candidates
.iter()
.map(|(dx, dy)| (dx * dx + dy * dy).sqrt())
.fold(f32::INFINITY, f32::min);
(r, r)
}
};
let radius_scale = radius_x.max(radius_y);
let resolved_stops = resolve_stops_along_axis(&gradient.stops, radius_scale.max(1e-6), context);
let (repeating, repeat_start, repeat_period, lut_axis_length, lut_resolved_stops) = if gradient
.repeating
&& let (Some(first), Some(last)) = (resolved_stops.first(), resolved_stops.last())
{
let repeat_start = first.position;
let repeat_period = (last.position - first.position).max(0.0);
if repeat_period > 1e-6 {
let shifted = resolved_stops
.iter()
.map(|stop| ResolvedGradientStop {
color: stop.color,
position: stop.position - repeat_start,
})
.collect();
(true, repeat_start, repeat_period, repeat_period, shifted)
} else {
(false, 0.0, 0.0, radius_scale, resolved_stops)
}
} else {
(false, 0.0, 0.0, radius_scale, resolved_stops)
};
let lut_size = adaptive_lut_size_with_visible_samples(
(radius_scale.ceil() as usize).saturating_add(1),
lut_axis_length,
&lut_resolved_stops,
);
let color_lut = build_color_lut_with_interpolation(
&lut_resolved_stops,
lut_axis_length,
lut_size,
gradient.interpolation.color_space,
gradient.interpolation.hue_direction,
);
let lut_len = color_lut.len();
let inv_radius_x = radius_x.max(1e-6).recip();
let inv_radius_y = radius_y.max(1e-6).recip();
let position_to_lut_scale = if lut_axis_length.abs() <= f32::EPSILON || lut_len <= 1 {
0.0
} else {
(lut_len - 1) as f32 / lut_axis_length
};
let fully_opaque = color_lut.iter().all(|p| p.alpha() == u8::MAX);
RadialGradientTile {
width,
height,
cx,
cy,
inv_radius_x,
inv_radius_y,
radius_scale,
repeating,
repeat_start,
repeat_period,
position_to_lut_scale,
fully_opaque,
color_lut,
}
}
}
impl GradientOverlayTile for RadialGradientTile {
type RowState = RadialGradientRowState;
#[inline(always)]
fn width(&self) -> u32 {
self.width
}
#[inline(always)]
fn height(&self) -> u32 {
self.height
}
#[inline(always)]
fn lut_len(&self) -> usize {
self.color_lut.len()
}
#[inline(always)]
fn sample_at(&self, lut_idx: usize) -> PremultipliedColorU8 {
self.color_lut[lut_idx]
}
#[inline(always)]
fn sample_pixel(&self, x: u32, y: u32) -> PremultipliedColorU8 {
if self.color_lut.is_empty() {
return PremultipliedColorU8::TRANSPARENT;
}
if self.color_lut.len() == 1 {
return self.color_lut[0];
}
let dx = (x as f32 - self.cx) * self.inv_radius_x;
let dy = (y as f32 - self.cy) * self.inv_radius_y;
let normalized_distance = (dx * dx + dy * dy).sqrt();
let distance_px = normalized_distance * self.radius_scale;
let lut_idx = self.lut_index_for_distance_px_with_len(distance_px, self.color_lut.len());
self.color_lut[lut_idx]
}
#[inline(always)]
fn begin_row(&self, src_x_start: u32, src_y: u32, lut_len: usize) -> Self::RowState {
let dy = (src_y as f32 - self.cy) * self.inv_radius_y;
let dx = (src_x_start as f32 - self.cx) * self.inv_radius_x;
let dx_step = self.inv_radius_x;
RadialGradientRowState {
dx2: dx * dx,
dx2_step: 2.0 * dx * dx_step + dx_step * dx_step,
dx2_step_delta: 2.0 * dx_step * dx_step,
dy2: dy * dy,
max_lut_index: lut_len.saturating_sub(1),
}
}
#[inline(always)]
fn next_lut_index(&self, row_state: &mut Self::RowState) -> usize {
if !self.repeating && row_state.dy2 >= 1.0 {
return row_state.max_lut_index;
}
let normalized_distance = (row_state.dx2 + row_state.dy2).sqrt();
let distance_px = normalized_distance * self.radius_scale;
let lut_idx = self.lut_index_for_distance_px_with_len(distance_px, row_state.max_lut_index + 1);
row_state.dx2 += row_state.dx2_step;
row_state.dx2_step += row_state.dx2_step_delta;
lut_idx
}
#[inline(always)]
fn fully_opaque(&self) -> bool {
self.fully_opaque
}
}
impl<'i> FromCss<'i> for RadialGradient {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, RadialGradient> {
let location = input.current_source_location();
let name = input.expect_function()?;
let repeating = match_ignore_ascii_case! { &name,
"radial-gradient" => false,
"repeating-radial-gradient" => true,
_ => return Err(unexpected_token!(location, &Token::Function(name.clone()))),
};
input.parse_nested_block(|input| {
let mut shape = RadialShape::Ellipse;
let mut size = RadialSize::FarthestCorner;
let mut center = ObjectPosition::default();
let mut interpolation = ColorInterpolationMethod::default();
loop {
if let Ok(s) = input.try_parse(RadialShape::from_css) {
shape = s;
continue;
}
if let Ok(s) = input.try_parse(RadialSize::from_css) {
size = s;
continue;
}
if let Ok(radius_x) = input.try_parse(LengthDefaultsToZero::from_css) {
let radius_y = input
.try_parse(LengthDefaultsToZero::from_css)
.unwrap_or(radius_x);
size = RadialSize::Explicit { radius_x, radius_y };
continue;
}
if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
center = ObjectPosition::from_css(input)?;
continue;
}
if let Ok(parsed_interpolation) = input.try_parse(ColorInterpolationMethod::from_css) {
interpolation = parsed_interpolation;
continue;
}
input.try_parse(Parser::expect_comma).ok();
break;
}
let stops = GradientStops::from_css(input)?;
Ok(RadialGradient {
repeating,
shape,
size,
center,
interpolation,
stops: stops.into_boxed_slice(),
})
})
}
const VALID_TOKENS: &'static [CssToken] =
&[CssToken::Descriptor(CssDescriptorKind::RadialGradientFn)];
}
impl ToCss for RadialSize {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Self::ClosestSide => dest.write_str("closest-side"),
Self::FarthestSide => dest.write_str("farthest-side"),
Self::ClosestCorner => dest.write_str("closest-corner"),
Self::FarthestCorner => dest.write_str("farthest-corner"),
Self::Explicit { radius_x, radius_y } => {
radius_x.to_css(dest)?;
if radius_x != radius_y {
dest.write_char(' ')?;
radius_y.to_css(dest)?;
}
Ok(())
}
}
}
}
impl ToCss for RadialGradient {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
let name = if self.repeating {
"repeating-radial-gradient"
} else {
"radial-gradient"
};
dest.write_str(name)?;
dest.write_char('(')?;
let mut first = true;
let mut shape_size_buf = String::new();
if self.shape != RadialShape::Ellipse {
self.shape.to_css(&mut shape_size_buf)?;
}
if self.size != RadialSize::FarthestCorner {
if !shape_size_buf.is_empty() {
shape_size_buf.push(' ');
}
self.size.to_css(&mut shape_size_buf)?;
}
let mut center_buf = String::new();
self.center.to_css(&mut center_buf)?;
let is_center_default = center_buf == "center center" || center_buf == "50% 50%";
if !shape_size_buf.is_empty() || !is_center_default {
dest.write_str(&shape_size_buf)?;
if !is_center_default {
if !shape_size_buf.is_empty() {
dest.write_char(' ')?;
}
dest.write_str("at ")?;
dest.write_str(¢er_buf)?;
}
first = false;
}
let mut interp_buf = String::new();
self.interpolation.to_css(&mut interp_buf)?;
if !interp_buf.is_empty() {
if !first {
dest.write_str(", ")?;
}
dest.write_str(&interp_buf)?;
first = false;
}
for stop in self.stops.iter() {
if !first {
dest.write_str(", ")?;
}
stop.to_css(dest)?;
first = false;
}
dest.write_char(')')
}
}
#[cfg(test)]
mod tests {
use color::{ColorSpaceTag, HueDirection};
use tiny_skia::ColorU8;
use super::*;
use crate::layout::Viewport;
use crate::layout::style::{
BackgroundPosition, Color, Length, LengthDefaultsToZero, PositionComponent, PositionKeywordX,
PositionKeywordY, SpacePair, StopPosition,
};
use crate::{GlobalContext, rendering::RenderContext};
#[test]
fn test_parse_radial_gradient_basic() {
let gradient = RadialGradient::from_str("radial-gradient(#ff0000, #0000ff)");
assert_eq!(
gradient,
Ok(
RadialGradient::builder()
.stops([
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: None,
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: None,
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_with_interpolation_color_space() {
assert_eq!(
RadialGradient::from_str("radial-gradient(in oklab, red, blue)"),
Ok(
RadialGradient::builder()
.interpolation(ColorInterpolationMethod {
color_space: ColorSpaceTag::Oklab,
hue_direction: HueDirection::Shorter,
})
.stops([
GradientStop::ColorHint {
color: Color::from_rgb(0xff0000).into(),
hint: None,
},
GradientStop::ColorHint {
color: Color::from_rgb(0x0000ff).into(),
hint: None,
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_circle_farthest_side() {
let gradient =
RadialGradient::from_str("radial-gradient(circle farthest-side, #ff0000, #0000ff)");
assert_eq!(
gradient,
Ok(
RadialGradient::builder()
.shape(RadialShape::Circle)
.size(RadialSize::FarthestSide)
.stops([
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: None,
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: None,
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_ellipse_at_left_top() {
let gradient =
RadialGradient::from_str("radial-gradient(ellipse at left top, #ff0000, #0000ff)");
assert_eq!(
gradient,
Ok(
RadialGradient::builder()
.center(BackgroundPosition::<false>(SpacePair::from_pair(
PositionComponent::KeywordX(PositionKeywordX::Left),
PositionComponent::KeywordY(PositionKeywordY::Top),
)))
.stops([
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: None,
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: None,
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_size_then_position() {
let gradient =
RadialGradient::from_str("radial-gradient(farthest-corner at 25% 70%, #ffffff, #000000)");
assert_eq!(
gradient,
Ok(
RadialGradient::builder()
.center(BackgroundPosition::<false>(SpacePair::from_pair(
Length::Percentage(25.0).into(),
Length::Percentage(70.0).into(),
)))
.stops([
GradientStop::ColorHint {
color: Color::white().into(),
hint: None,
},
GradientStop::ColorHint {
color: Color::black().into(),
hint: None,
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_circle_farthest_side_with_stops() {
let gradient = RadialGradient::from_str(
"radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%)",
);
assert_eq!(
gradient,
Ok(
RadialGradient::builder()
.shape(RadialShape::Circle)
.center(BackgroundPosition::<false>(SpacePair::from_single(
PositionComponent::Length(Length::Px(25.0),)
)))
.stops([
GradientStop::ColorHint {
color: Color([211, 211, 211, 255]).into(),
hint: Some(StopPosition(Length::Percentage(2.0))),
},
GradientStop::ColorHint {
color: Color::transparent().into(),
hint: Some(StopPosition(Length::Percentage(0.0))),
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_with_stop_positions() {
let gradient =
RadialGradient::from_str("radial-gradient(circle, #ff0000 0%, #00ff00 50%, #0000ff 100%)");
assert_eq!(
gradient,
Ok(
RadialGradient::builder()
.shape(RadialShape::Circle)
.stops([
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: Some(StopPosition(Length::Percentage(0.0))),
},
GradientStop::ColorHint {
color: Color([0, 255, 0, 255]).into(),
hint: Some(StopPosition(Length::Percentage(50.0))),
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: Some(StopPosition(Length::Percentage(100.0))),
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_with_double_position_color_stop() {
assert_eq!(
RadialGradient::from_str("radial-gradient(circle, red 10% 20%, blue)"),
Ok(
RadialGradient::builder()
.shape(RadialShape::Circle)
.stops([
GradientStop::ColorHint {
color: Color::from_rgb(0xff0000).into(),
hint: Some(StopPosition(Length::Percentage(10.0))),
},
GradientStop::ColorHint {
color: Color::from_rgb(0xff0000).into(),
hint: Some(StopPosition(Length::Percentage(20.0))),
},
GradientStop::ColorHint {
color: Color::from_rgb(0x0000ff).into(),
hint: None,
},
])
.build()
)
);
}
#[test]
fn test_parse_radial_gradient_with_explicit_ellipse_radii() {
let gradient = RadialGradient::from_str(
"radial-gradient(ellipse 60% 60% at 50% 50%, rgba(255, 53, 53, 0.10) 0%, transparent 70%)",
);
assert!(match gradient {
Ok(RadialGradient {
shape: RadialShape::Ellipse,
size:
RadialSize::Explicit {
radius_x: LengthDefaultsToZero::Percentage(radius_x),
radius_y: LengthDefaultsToZero::Percentage(radius_y),
},
center:
BackgroundPosition::<false>(SpacePair {
x: PositionComponent::Length(Length::Percentage(center_x)),
y: PositionComponent::Length(Length::Percentage(center_y)),
}),
stops,
..
}) => {
(radius_x - 60.0).abs() < 1e-3
&& (radius_y - 60.0).abs() < 1e-3
&& (center_x - 50.0).abs() < 1e-3
&& (center_y - 50.0).abs() < 1e-3
&& stops.len() == 2
}
_ => false,
});
}
#[test]
fn resolve_stops_percentage_and_px_radial() {
let gradient = RadialGradient::builder()
.stops([
GradientStop::ColorHint {
color: Color::black().into(),
hint: Some(StopPosition(Length::Percentage(0.0))),
},
GradientStop::ColorHint {
color: Color::black().into(),
hint: Some(StopPosition(Length::Percentage(50.0))),
},
GradientStop::ColorHint {
color: Color::black().into(),
hint: Some(StopPosition(Length::Px(100.0))),
},
])
.build();
let context = GlobalContext::default();
let render_context = RenderContext::new_test(&context, Viewport::new((200, 100)));
let resolved = resolve_stops_along_axis(
&gradient.stops,
render_context
.sizing
.viewport
.size
.width
.unwrap_or_default() as f32,
&render_context,
);
assert_eq!(resolved.len(), 3);
assert!((resolved[0].position - 0.0).abs() < 1e-3);
assert_eq!(resolved[1].position, resolved[2].position);
}
#[test]
fn resolve_stops_equal_positions_distributed_radial() {
let gradient = RadialGradient::builder()
.stops([
GradientStop::ColorHint {
color: Color::black().into(),
hint: Some(StopPosition(Length::Px(0.0))),
},
GradientStop::ColorHint {
color: Color::black().into(),
hint: Some(StopPosition(Length::Px(0.0))),
},
GradientStop::ColorHint {
color: Color::black().into(),
hint: Some(StopPosition(Length::Px(0.0))),
},
])
.build();
let context = GlobalContext::default();
let render_context = RenderContext::new_test(&context, Viewport::new((200, 100)));
let resolved = resolve_stops_along_axis(
&gradient.stops,
render_context
.sizing
.viewport
.size
.width
.unwrap_or_default() as f32,
&render_context,
);
assert_eq!(resolved.len(), 3);
assert!(resolved[0].position >= 0.0);
assert!(resolved[1].position >= resolved[0].position);
assert!(resolved[2].position >= resolved[1].position);
}
#[test]
fn test_radial_gradient_at() {
let gradient = RadialGradient::builder()
.shape(RadialShape::Circle)
.stops([
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(), hint: Some(StopPosition(Length::Percentage(0.0))),
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(), hint: Some(StopPosition(Length::Percentage(100.0))),
},
])
.build();
let context = GlobalContext::default();
let dummy_context = RenderContext::new_test(&context, Viewport::new((100, 100)));
let tile = RadialGradientTile::new(&gradient, 100, 100, &dummy_context);
let color_center = tile.sample_pixel(50, 50).demultiply();
assert_eq!(color_center, ColorU8::from_rgba(255, 0, 0, 255));
let color_far = tile.sample_pixel(200, 200).demultiply();
assert_eq!(color_far, ColorU8::from_rgba(0, 0, 255, 255));
}
#[test]
fn test_repeating_radial_gradient_rings() {
let gradient = RadialGradient::builder()
.repeating(true)
.shape(RadialShape::Circle)
.size(RadialSize::Explicit {
radius_x: LengthDefaultsToZero::Px(20.0),
radius_y: LengthDefaultsToZero::Px(20.0),
})
.stops([
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: Some(StopPosition(Length::Px(0.0))),
},
GradientStop::ColorHint {
color: Color([255, 0, 0, 255]).into(),
hint: Some(StopPosition(Length::Px(5.0))),
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: Some(StopPosition(Length::Px(5.0))),
},
GradientStop::ColorHint {
color: Color([0, 0, 255, 255]).into(),
hint: Some(StopPosition(Length::Px(10.0))),
},
])
.build();
let context = GlobalContext::default();
let render_context = RenderContext::new_test(&context, Viewport::new((40, 40)));
let tile = RadialGradientTile::new(&gradient, 40, 40, &render_context);
assert_eq!(
[
tile.sample_pixel(22, 20).demultiply(),
tile.sample_pixel(27, 20).demultiply(),
tile.sample_pixel(32, 20).demultiply(),
tile.sample_pixel(37, 20).demultiply(),
],
[
ColorU8::from_rgba(255, 0, 0, 255),
ColorU8::from_rgba(0, 0, 255, 255),
ColorU8::from_rgba(255, 0, 0, 255),
ColorU8::from_rgba(0, 0, 255, 255),
]
);
}
#[test]
fn test_radial_gradient_ellipse_closest_corner() {
let gradient = RadialGradient::builder()
.center(BackgroundPosition::<false>(SpacePair::from_pair(
Length::Px(20.0).into(),
Length::Px(20.0).into(),
)))
.size(RadialSize::ClosestCorner)
.stops([
GradientStop::ColorHint {
color: Color::black().into(),
hint: Some(StopPosition(Length::Percentage(0.0))),
},
GradientStop::ColorHint {
color: Color::white().into(),
hint: Some(StopPosition(Length::Percentage(100.0))),
},
])
.build();
let context = GlobalContext::default();
let dummy_context = RenderContext::new_test(&context, Viewport::new((100, 100)));
let tile = RadialGradientTile::new(&gradient, 100, 100, &dummy_context);
assert!((tile.inv_radius_x - (1.0 / 20.0)).abs() < 1e-3);
assert!((tile.inv_radius_y - (1.0 / 20.0)).abs() < 1e-3);
}
}