use crate::color::{Color, ToColorColor};
use color::DynamicColor;
use kurbo::{self, Affine, Point, Rect, Vec2};
use peniko::{self, ColorStop, Gradient, LinearGradientPosition, SweepGradientPosition};
use style::color::AbsoluteColor;
use style::{
OwnedSlice,
values::{
computed::{
Angle, AngleOrPercentage, CSSPixelLength, Gradient as StyloGradient, LengthPercentage,
LineDirection, Percentage,
},
generics::{
NonNegative,
color::GenericColor,
image::{
EndingShape, GenericCircle, GenericEllipse, GenericEndingShape, GenericGradient,
GenericGradientItem, GradientFlags, ShapeExtent,
},
position::GenericPosition,
},
specified::{
percentage::ToPercentage,
position::{HorizontalPositionKeyword, VerticalPositionKeyword},
},
},
};
type GradientItem<T> = GenericGradientItem<GenericColor<Percentage>, T>;
type LinearGradient<'a> = (
&'a LineDirection,
&'a [GradientItem<LengthPercentage>],
GradientFlags,
);
type RadialGradient<'a> = (
&'a EndingShape<NonNegative<CSSPixelLength>, NonNegative<LengthPercentage>>,
&'a GenericPosition<LengthPercentage, LengthPercentage>,
&'a OwnedSlice<GenericGradientItem<GenericColor<Percentage>, LengthPercentage>>,
GradientFlags,
);
type ConicGradient<'a> = (
&'a Angle,
&'a GenericPosition<LengthPercentage, LengthPercentage>,
&'a OwnedSlice<GenericGradientItem<GenericColor<Percentage>, AngleOrPercentage>>,
GradientFlags,
);
pub(crate) fn to_peniko_gradient(
gradient: &StyloGradient,
origin_rect: Rect,
bounding_box: Rect,
scale: f64,
current_color: &AbsoluteColor,
) -> (peniko::Gradient, Option<Affine>) {
match gradient {
GenericGradient::Linear {
direction,
items,
flags,
..
} => linear_gradient(
(direction, items, *flags),
origin_rect,
bounding_box,
scale,
current_color,
),
GenericGradient::Radial {
shape,
position,
items,
flags,
..
} => radial_gradient((shape, position, items, *flags), origin_rect, current_color),
GenericGradient::Conic {
angle,
position,
items,
flags,
..
} => conic_gradient((angle, position, items, *flags), origin_rect, current_color),
}
}
fn linear_gradient(
gradient: LinearGradient,
rect: Rect,
bounding_box: Rect,
scale: f64,
current_color: &AbsoluteColor,
) -> (peniko::Gradient, Option<Affine>) {
let (direction, items, flags) = gradient;
let center = bounding_box.center();
let (start, end) = match direction {
LineDirection::Angle(angle) => {
let angle = -angle.radians64() + std::f64::consts::PI;
let offset_length =
rect.width() / 2.0 * angle.sin().abs() + rect.height() / 2.0 * angle.cos().abs();
let offset_vec = Vec2::new(angle.sin(), angle.cos()) * offset_length;
(center - offset_vec, center + offset_vec)
}
LineDirection::Horizontal(horizontal) => {
let start = Point::new(rect.x0, rect.y0 + rect.height() / 2.0);
let end = Point::new(rect.x1, rect.y0 + rect.height() / 2.0);
match horizontal {
HorizontalPositionKeyword::Right => (start, end),
HorizontalPositionKeyword::Left => (end, start),
}
}
LineDirection::Vertical(vertical) => {
let start = Point::new(rect.x0 + rect.width() / 2.0, rect.y0);
let end = Point::new(rect.x0 + rect.width() / 2.0, rect.y1);
match vertical {
VerticalPositionKeyword::Top => (end, start),
VerticalPositionKeyword::Bottom => (start, end),
}
}
LineDirection::Corner(horizontal, vertical) => {
let (start_x, end_x) = match horizontal {
HorizontalPositionKeyword::Right => (rect.x0, rect.x1),
HorizontalPositionKeyword::Left => (rect.x1, rect.x0),
};
let (start_y, end_y) = match vertical {
VerticalPositionKeyword::Top => (rect.y1, rect.y0),
VerticalPositionKeyword::Bottom => (rect.y0, rect.y1),
};
(Point::new(start_x, start_y), Point::new(end_x, end_y))
}
};
let gradient_length = CSSPixelLength::new((start.distance(end) / scale) as f32);
let repeating = flags.contains(GradientFlags::REPEATING);
let mut gradient = peniko::Gradient::new_linear(start, end).with_extend(if repeating {
peniko::Extend::Repeat
} else {
peniko::Extend::Pad
});
let (first_offset, last_offset) = resolve_length_color_stops(
current_color,
items,
gradient_length,
&mut gradient,
repeating,
);
if repeating && gradient.stops.len() > 1 {
gradient.kind = peniko::GradientKind::Linear(LinearGradientPosition {
start: start + (end - start) * first_offset as f64,
end: end + (start - end) * (1.0 - last_offset) as f64,
});
}
(gradient, None)
}
fn radial_gradient(
gradient: RadialGradient,
rect: Rect,
current_color: &AbsoluteColor,
) -> (peniko::Gradient, Option<Affine>) {
let (shape, position, items, flags) = gradient;
let repeating = flags.contains(GradientFlags::REPEATING);
let mut gradient = peniko::Gradient::new_radial((0.0, 0.0), 1.0).with_extend(if repeating {
peniko::Extend::Repeat
} else {
peniko::Extend::Pad
});
let (width_px, height_px) = (
position
.horizontal
.resolve(CSSPixelLength::new(rect.width() as f32))
.px() as f64,
position
.vertical
.resolve(CSSPixelLength::new(rect.height() as f32))
.px() as f64,
);
let gradient_scale: Option<Vec2> = match shape {
GenericEndingShape::Circle(circle) => {
let scale = match circle {
GenericCircle::Extent(extent) => match extent {
ShapeExtent::FarthestSide => width_px
.max(rect.width() - width_px)
.max(height_px.max(rect.height() - height_px)),
ShapeExtent::ClosestSide => width_px
.min(rect.width() - width_px)
.min(height_px.min(rect.height() - height_px)),
ShapeExtent::FarthestCorner => {
(width_px.max(rect.width() - width_px)
+ height_px.max(rect.height() - height_px))
* 0.5_f64.sqrt()
}
ShapeExtent::ClosestCorner => {
(width_px.min(rect.width() - width_px)
+ height_px.min(rect.height() - height_px))
* 0.5_f64.sqrt()
}
_ => 0.0,
},
GenericCircle::Radius(radius) => radius.0.px() as f64,
};
Some(Vec2::new(scale, scale))
}
GenericEndingShape::Ellipse(ellipse) => match ellipse {
GenericEllipse::Extent(extent) => match extent {
ShapeExtent::FarthestCorner | ShapeExtent::FarthestSide => {
let mut scale = Vec2::new(
width_px.max(rect.width() - width_px),
height_px.max(rect.height() - height_px),
);
if *extent == ShapeExtent::FarthestCorner {
scale *= 2.0_f64.sqrt();
}
Some(scale)
}
ShapeExtent::ClosestCorner | ShapeExtent::ClosestSide => {
let mut scale = Vec2::new(
width_px.min(rect.width() - width_px),
height_px.min(rect.height() - height_px),
);
if *extent == ShapeExtent::ClosestCorner {
scale *= 2.0_f64.sqrt();
}
Some(scale)
}
_ => None,
},
GenericEllipse::Radii(x, y) => Some(Vec2::new(
x.0.resolve(CSSPixelLength::new(rect.width() as f32)).px() as f64,
y.0.resolve(CSSPixelLength::new(rect.height() as f32)).px() as f64,
)),
},
};
let gradient_transform = {
if let Some(gradient_scale) = gradient_scale {
let (first_offset, last_offset) = resolve_length_color_stops(
current_color,
items,
CSSPixelLength::new(gradient_scale.x as f32),
&mut gradient,
repeating,
);
let scale = if repeating && gradient.stops.len() >= 2 {
(last_offset - first_offset) as f64
} else {
1.0
};
Some(
Affine::scale_non_uniform(gradient_scale.x * scale, gradient_scale.y * scale)
.then_translate(get_translation(position, rect)),
)
} else {
None
}
};
(gradient, gradient_transform)
}
fn conic_gradient(
gradient: ConicGradient,
rect: Rect,
current_color: &AbsoluteColor,
) -> (peniko::Gradient, Option<Affine>) {
let (angle, position, items, flags) = gradient;
let repeating = flags.contains(GradientFlags::REPEATING);
let mut gradient = peniko::Gradient::new_sweep((0.0, 0.0), 0.0, std::f32::consts::PI * 2.0)
.with_extend(if repeating {
peniko::Extend::Repeat
} else {
peniko::Extend::Pad
});
let (first_offset, last_offset) = resolve_angle_color_stops(
current_color,
items,
CSSPixelLength::new(1.0),
&mut gradient,
repeating,
);
if repeating && gradient.stops.len() >= 2 {
gradient.kind = peniko::GradientKind::Sweep(SweepGradientPosition {
center: Point::new(0.0, 0.0),
start_angle: std::f32::consts::PI * 2.0 * first_offset,
end_angle: std::f32::consts::PI * 2.0 * last_offset,
});
}
let gradient_transform = Some(
Affine::rotate(angle.radians() as f64 - std::f64::consts::PI / 2.0)
.then_translate(get_translation(position, rect)),
);
(gradient, gradient_transform)
}
#[inline]
fn resolve_length_color_stops(
current_color: &AbsoluteColor,
items: &[GradientItem<LengthPercentage>],
gradient_length: CSSPixelLength,
gradient: &mut Gradient,
repeating: bool,
) -> (f32, f32) {
resolve_color_stops(
current_color,
items,
gradient_length,
gradient,
repeating,
|gradient_length: CSSPixelLength, position: &LengthPercentage| -> Option<f32> {
position
.to_percentage_of(gradient_length)
.map(|percentage| percentage.to_percentage())
},
)
}
#[inline]
fn resolve_color_stops<T>(
current_color: &AbsoluteColor,
items: &[GradientItem<T>],
gradient_length: CSSPixelLength,
gradient: &mut Gradient,
repeating: bool,
item_resolver: impl Fn(CSSPixelLength, &T) -> Option<f32>,
) -> (f32, f32) {
let mut hint: Option<f32> = None;
for (idx, item) in items.iter().enumerate() {
let (color, offset) = match item {
GenericGradientItem::SimpleColorStop(color) => {
let step = 1.0 / (items.len() as f32 - 1.0);
(
color.resolve_to_absolute(current_color).as_dynamic_color(),
step * idx as f32,
)
}
GenericGradientItem::ComplexColorStop { color, position } => {
let offset = item_resolver(gradient_length, position);
if let Some(offset) = offset {
(
color.resolve_to_absolute(current_color).as_dynamic_color(),
offset,
)
} else {
continue;
}
}
GenericGradientItem::InterpolationHint(position) => {
hint = item_resolver(gradient_length, position);
continue;
}
};
if idx == 0 && !repeating && offset != 0.0 {
gradient
.stops
.push(peniko::ColorStop { color, offset: 0.0 });
}
match hint {
None => gradient.stops.push(peniko::ColorStop { color, offset }),
Some(hint) => {
let &last_stop = gradient.stops.last().unwrap();
if hint <= last_stop.offset {
match gradient.stops.len() {
0 => (),
1 => {
gradient.stops.pop();
}
_ => {
let prev_stop = gradient.stops[gradient.stops.len() - 2];
if prev_stop.offset == hint {
gradient.stops.pop();
}
}
}
gradient.stops.push(peniko::ColorStop {
color,
offset: hint,
});
} else if hint >= offset {
gradient.stops.push(peniko::ColorStop {
color: last_stop.color,
offset: hint,
});
gradient.stops.push(peniko::ColorStop {
color,
offset: last_stop.offset,
});
} else if hint == (last_stop.offset + offset) / 2.0 {
gradient.stops.push(peniko::ColorStop { color, offset });
} else {
let mid_point = (hint - last_stop.offset) / (offset - last_stop.offset);
let mut interpolate_stop = |cur_offset: f32| {
let relative_offset =
(cur_offset - last_stop.offset) / (offset - last_stop.offset);
let multiplier = relative_offset.powf(0.5f32.log(mid_point));
let [last_r, last_g, last_b, last_a] = last_stop.color.components;
let [r, g, b, a] = color.components;
let color = Color::new([
(last_r + multiplier * (r - last_r)),
(last_g + multiplier * (g - last_g)),
(last_b + multiplier * (b - last_b)),
(last_a + multiplier * (a - last_a)),
]);
gradient.stops.push(peniko::ColorStop {
color: DynamicColor::from_alpha_color(color),
offset: cur_offset,
});
};
if mid_point > 0.5 {
for i in 0..7 {
interpolate_stop(
last_stop.offset
+ (hint - last_stop.offset) * (7.0 + i as f32) / 13.0,
);
}
interpolate_stop(hint + (offset - hint) / 3.0);
interpolate_stop(hint + (offset - hint) * 2.0 / 3.0);
} else {
interpolate_stop(last_stop.offset + (hint - last_stop.offset) / 3.0);
interpolate_stop(last_stop.offset + (hint - last_stop.offset) * 2.0 / 3.0);
for i in 0..7 {
interpolate_stop(hint + (offset - hint) * (i as f32) / 13.0);
}
}
gradient.stops.push(peniko::ColorStop { color, offset });
}
}
}
}
if repeating && gradient.stops.len() > 1 {
let first_offset = gradient.stops.first().unwrap().offset;
let last_offset = gradient.stops.last().unwrap().offset;
if first_offset != 0.0 || last_offset != 1.0 {
let scale_inv = 1e-7_f32.max(1.0 / (last_offset - first_offset));
for stop in &mut *gradient.stops {
stop.offset = (stop.offset - first_offset) * scale_inv;
}
}
(first_offset, last_offset)
} else {
if gradient.stops.len() > 1 {
let last_stop = &gradient.stops.last().unwrap();
if last_stop.offset < 1.0 {
let last_stop = ColorStop {
offset: 1.0,
..(**last_stop)
};
gradient.stops.push(last_stop);
}
}
(0.0, 1.0)
}
}
#[inline]
fn resolve_angle_color_stops(
current_color: &AbsoluteColor,
items: &[GradientItem<AngleOrPercentage>],
gradient_length: CSSPixelLength,
gradient: &mut Gradient,
repeating: bool,
) -> (f32, f32) {
resolve_color_stops(
current_color,
items,
gradient_length,
gradient,
repeating,
|_gradient_length: CSSPixelLength, position: &AngleOrPercentage| -> Option<f32> {
match position {
AngleOrPercentage::Angle(angle) => {
Some(angle.radians() / (std::f64::consts::PI * 2.0) as f32)
}
AngleOrPercentage::Percentage(percentage) => Some(percentage.to_percentage()),
}
},
)
}
#[inline]
fn get_translation(
position: &GenericPosition<LengthPercentage, LengthPercentage>,
rect: Rect,
) -> Vec2 {
Vec2::new(
rect.x0
+ position
.horizontal
.resolve(CSSPixelLength::new(rect.width() as f32))
.px() as f64,
rect.y0
+ position
.vertical
.resolve(CSSPixelLength::new(rect.height() as f32))
.px() as f64,
)
}