| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| use app_units::Au; |
| use euclid::Size2D; |
| use style::Zero; |
| use style::color::mix::ColorInterpolationMethod; |
| use style::properties::ComputedValues; |
| use style::values::computed::image::{EndingShape, Gradient, LineDirection}; |
| use style::values::computed::{Angle, AngleOrPercentage, Color, LengthPercentage, Position}; |
| use style::values::generics::image::{ |
| Circle, ColorStop, Ellipse, GradientFlags, GradientItem, ShapeExtent, |
| }; |
| use webrender_api::units::LayoutPixel; |
| use webrender_api::{ |
| self as wr, ConicGradient as WebRenderConicGradient, Gradient as WebRenderLinearGradient, |
| RadialGradient as WebRenderRadialGradient, units, |
| }; |
| use wr::ColorF; |
| |
| pub(super) enum WebRenderGradient { |
| Linear(WebRenderLinearGradient), |
| Radial(WebRenderRadialGradient), |
| Conic(WebRenderConicGradient), |
| } |
| |
| pub(super) fn build( |
| style: &ComputedValues, |
| gradient: &Gradient, |
| size: Size2D<f32, LayoutPixel>, |
| builder: &mut super::DisplayListBuilder, |
| ) -> WebRenderGradient { |
| match gradient { |
| Gradient::Linear { |
| items, |
| direction, |
| color_interpolation_method, |
| flags, |
| compat_mode: _, |
| } => build_linear( |
| style, |
| items, |
| direction, |
| color_interpolation_method, |
| *flags, |
| size, |
| builder, |
| ), |
| Gradient::Radial { |
| shape, |
| position, |
| color_interpolation_method, |
| items, |
| flags, |
| compat_mode: _, |
| } => build_radial( |
| style, |
| items, |
| shape, |
| position, |
| color_interpolation_method, |
| *flags, |
| size, |
| builder, |
| ), |
| Gradient::Conic { |
| angle, |
| position, |
| color_interpolation_method, |
| items, |
| flags, |
| } => build_conic( |
| style, |
| *angle, |
| position, |
| *color_interpolation_method, |
| items, |
| *flags, |
| size, |
| builder, |
| ), |
| } |
| } |
| |
| /// <https://drafts.csswg.org/css-images-3/#linear-gradients> |
| pub(super) fn build_linear( |
| style: &ComputedValues, |
| items: &[GradientItem<Color, LengthPercentage>], |
| line_direction: &LineDirection, |
| _color_interpolation_method: &ColorInterpolationMethod, |
| flags: GradientFlags, |
| gradient_box: Size2D<f32, LayoutPixel>, |
| builder: &mut super::DisplayListBuilder, |
| ) -> WebRenderGradient { |
| use style::values::specified::position::HorizontalPositionKeyword::*; |
| use style::values::specified::position::VerticalPositionKeyword::*; |
| use units::LayoutVector2D as Vec2; |
| |
| // A vector of length 1.0 in the direction of the gradient line |
| let direction = match line_direction { |
| LineDirection::Horizontal(Right) => Vec2::new(1., 0.), |
| LineDirection::Vertical(Top) => Vec2::new(0., -1.), |
| LineDirection::Horizontal(Left) => Vec2::new(-1., 0.), |
| LineDirection::Vertical(Bottom) => Vec2::new(0., 1.), |
| |
| LineDirection::Angle(angle) => { |
| let radians = angle.radians(); |
| // “`0deg` points upward, |
| // and positive angles represent clockwise rotation, |
| // so `90deg` point toward the right.” |
| Vec2::new(radians.sin(), -radians.cos()) |
| }, |
| |
| LineDirection::Corner(horizontal, vertical) => { |
| // “If the argument instead specifies a corner of the box such as `to top left`, |
| // the gradient line must be angled such that it points |
| // into the same quadrant as the specified corner, |
| // and is perpendicular to a line intersecting |
| // the two neighboring corners of the gradient box.” |
| |
| // Note that that last line is a diagonal of the gradient box rectangle, |
| // since two neighboring corners of a third corner |
| // are necessarily opposite to each other. |
| |
| // `{ x: gradient_box.width, y: gradient_box.height }` is such a diagonal vector, |
| // from the bottom left corner to the top right corner of the gradient box. |
| // (Both coordinates are positive.) |
| // Changing either or both signs produces the other three (oriented) diagonals. |
| |
| // Swapping the coordinates `{ x: gradient_box.height, y: gradient_box.height }` |
| // produces a vector perpendicular to some diagonal of the rectangle. |
| // Finally, we choose the sign of each cartesian coordinate |
| // such that our vector points to the desired quadrant. |
| |
| let x = match horizontal { |
| Right => gradient_box.height, |
| Left => -gradient_box.height, |
| }; |
| let y = match vertical { |
| Top => -gradient_box.width, |
| Bottom => gradient_box.width, |
| }; |
| |
| // `{ x, y }` is now a vector of arbitrary length |
| // with the same direction as the gradient line. |
| // This normalizes the length to 1.0: |
| Vec2::new(x, y).normalize() |
| }, |
| }; |
| |
| // This formula is given as `abs(W * sin(A)) + abs(H * cos(A))` in a note in the spec, under |
| // https://drafts.csswg.org/css-images-3/#linear-gradient-syntax |
| // |
| // Sketch of a proof: |
| // |
| // * Take the top side of the gradient box rectangle. It is a segment of length `W` |
| // * Project onto the gradient line. You get a segment of length `abs(W * sin(A))` |
| // * Similarly, the left side of the rectangle (length `H`) |
| // projects to a segment of length `abs(H * cos(A))` |
| // * These two segments add up to exactly the gradient line. |
| // |
| // See the illustration in the example under |
| // https://drafts.csswg.org/css-images-3/#linear-gradient-syntax |
| let gradient_line_length = |
| (gradient_box.width * direction.x).abs() + (gradient_box.height * direction.y).abs(); |
| |
| let half_gradient_line = direction * (gradient_line_length / 2.); |
| let center = (gradient_box / 2.).to_vector().to_point(); |
| let start_point = center - half_gradient_line; |
| let end_point = center + half_gradient_line; |
| |
| let mut color_stops = |
| gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length)); |
| let stops = fixup_stops(&mut color_stops); |
| let extend_mode = if flags.contains(GradientFlags::REPEATING) { |
| wr::ExtendMode::Repeat |
| } else { |
| wr::ExtendMode::Clamp |
| }; |
| WebRenderGradient::Linear(builder.wr().create_gradient( |
| start_point, |
| end_point, |
| stops, |
| extend_mode, |
| )) |
| } |
| |
| /// <https://drafts.csswg.org/css-images-3/#radial-gradients> |
| #[allow(clippy::too_many_arguments)] |
| pub(super) fn build_radial( |
| style: &ComputedValues, |
| items: &[GradientItem<Color, LengthPercentage>], |
| shape: &EndingShape, |
| center: &Position, |
| _color_interpolation_method: &ColorInterpolationMethod, |
| flags: GradientFlags, |
| gradient_box: Size2D<f32, LayoutPixel>, |
| builder: &mut super::DisplayListBuilder, |
| ) -> WebRenderGradient { |
| let center = units::LayoutPoint::new( |
| center |
| .horizontal |
| .to_used_value(Au::from_f32_px(gradient_box.width)) |
| .to_f32_px(), |
| center |
| .vertical |
| .to_used_value(Au::from_f32_px(gradient_box.height)) |
| .to_f32_px(), |
| ); |
| let radii = match shape { |
| EndingShape::Circle(circle) => { |
| let radius = match circle { |
| Circle::Radius(r) => r.0.px(), |
| Circle::Extent(extent) => match extent { |
| ShapeExtent::ClosestSide | ShapeExtent::Contain => { |
| let vec = abs_vector_to_corner(gradient_box, center, f32::min); |
| vec.x.min(vec.y) |
| }, |
| ShapeExtent::FarthestSide => { |
| let vec = abs_vector_to_corner(gradient_box, center, f32::max); |
| vec.x.max(vec.y) |
| }, |
| ShapeExtent::ClosestCorner => { |
| abs_vector_to_corner(gradient_box, center, f32::min).length() |
| }, |
| ShapeExtent::FarthestCorner | ShapeExtent::Cover => { |
| abs_vector_to_corner(gradient_box, center, f32::max).length() |
| }, |
| }, |
| }; |
| units::LayoutSize::new(radius, radius) |
| }, |
| EndingShape::Ellipse(Ellipse::Radii(rx, ry)) => units::LayoutSize::new( |
| rx.0.to_used_value(Au::from_f32_px(gradient_box.width)) |
| .to_f32_px(), |
| ry.0.to_used_value(Au::from_f32_px(gradient_box.height)) |
| .to_f32_px(), |
| ), |
| EndingShape::Ellipse(Ellipse::Extent(extent)) => match extent { |
| ShapeExtent::ClosestSide | ShapeExtent::Contain => { |
| abs_vector_to_corner(gradient_box, center, f32::min).to_size() |
| }, |
| ShapeExtent::FarthestSide => { |
| abs_vector_to_corner(gradient_box, center, f32::max).to_size() |
| }, |
| ShapeExtent::ClosestCorner => { |
| abs_vector_to_corner(gradient_box, center, f32::min).to_size() * |
| (std::f32::consts::FRAC_1_SQRT_2 * 2.0) |
| }, |
| ShapeExtent::FarthestCorner | ShapeExtent::Cover => { |
| abs_vector_to_corner(gradient_box, center, f32::max).to_size() * |
| (std::f32::consts::FRAC_1_SQRT_2 * 2.0) |
| }, |
| }, |
| }; |
| |
| /// Returns the distance to the nearest or farthest sides in the respective dimension, |
| /// depending on `select`. |
| fn abs_vector_to_corner( |
| gradient_box: units::LayoutSize, |
| center: units::LayoutPoint, |
| select: impl Fn(f32, f32) -> f32, |
| ) -> units::LayoutVector2D { |
| let left = center.x.abs(); |
| let top = center.y.abs(); |
| let right = (gradient_box.width - center.x).abs(); |
| let bottom = (gradient_box.height - center.y).abs(); |
| units::LayoutVector2D::new(select(left, right), select(top, bottom)) |
| } |
| |
| // “The gradient line’s starting point is at the center of the gradient, |
| // and it extends toward the right, with the ending point on the point |
| // where the gradient line intersects the ending shape.” |
| let gradient_line_length = radii.width; |
| |
| let mut color_stops = |
| gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length)); |
| let stops = fixup_stops(&mut color_stops); |
| let extend_mode = if flags.contains(GradientFlags::REPEATING) { |
| wr::ExtendMode::Repeat |
| } else { |
| wr::ExtendMode::Clamp |
| }; |
| WebRenderGradient::Radial(builder.wr().create_radial_gradient( |
| center, |
| radii, |
| stops, |
| extend_mode, |
| )) |
| } |
| |
| /// <https://drafts.csswg.org/css-images-4/#conic-gradients> |
| #[allow(clippy::too_many_arguments)] |
| fn build_conic( |
| style: &ComputedValues, |
| angle: Angle, |
| center: &Position, |
| _color_interpolation_method: ColorInterpolationMethod, |
| items: &[GradientItem<Color, AngleOrPercentage>], |
| flags: GradientFlags, |
| gradient_box: Size2D<f32, LayoutPixel>, |
| builder: &mut super::DisplayListBuilder<'_>, |
| ) -> WebRenderGradient { |
| let center = units::LayoutPoint::new( |
| center |
| .horizontal |
| .to_used_value(Au::from_f32_px(gradient_box.width)) |
| .to_f32_px(), |
| center |
| .vertical |
| .to_used_value(Au::from_f32_px(gradient_box.height)) |
| .to_f32_px(), |
| ); |
| let mut color_stops = conic_gradient_items_to_color_stops(style, items); |
| let stops = fixup_stops(&mut color_stops); |
| let extend_mode = if flags.contains(GradientFlags::REPEATING) { |
| wr::ExtendMode::Repeat |
| } else { |
| wr::ExtendMode::Clamp |
| }; |
| WebRenderGradient::Conic(builder.wr().create_conic_gradient( |
| center, |
| angle.radians(), |
| stops, |
| extend_mode, |
| )) |
| } |
| |
| fn conic_gradient_items_to_color_stops( |
| style: &ComputedValues, |
| items: &[GradientItem<Color, AngleOrPercentage>], |
| ) -> Vec<ColorStop<ColorF, f32>> { |
| // Remove color transititon hints, which are not supported yet. |
| // https://drafts.csswg.org/css-images-4/#color-transition-hint |
| // |
| // This gives an approximation of the gradient that might be visibly wrong, |
| // but maybe better than not parsing that value at all? |
| // It’s debatble whether that’s better or worse |
| // than not parsing and allowing authors to set a fallback. |
| // Either way, the best outcome is to add support. |
| // Gecko does so by approximating the non-linear interpolation |
| // by up to 10 piece-wise linear segments (9 intermediate color stops) |
| items |
| .iter() |
| .filter_map(|item| { |
| match item { |
| GradientItem::SimpleColorStop(color) => Some(ColorStop { |
| color: super::rgba(style.resolve_color(color)), |
| position: None, |
| }), |
| GradientItem::ComplexColorStop { color, position } => Some(ColorStop { |
| color: super::rgba(style.resolve_color(color)), |
| position: match position { |
| AngleOrPercentage::Percentage(percentage) => Some(percentage.0), |
| AngleOrPercentage::Angle(angle) => Some(angle.degrees() / 360.), |
| }, |
| }), |
| // FIXME: approximate like in: |
| // https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391 |
| GradientItem::InterpolationHint(_) => None, |
| } |
| }) |
| .collect() |
| } |
| |
| fn gradient_items_to_color_stops( |
| style: &ComputedValues, |
| items: &[GradientItem<Color, LengthPercentage>], |
| gradient_line_length: Au, |
| ) -> Vec<ColorStop<ColorF, f32>> { |
| // Remove color transititon hints, which are not supported yet. |
| // https://drafts.csswg.org/css-images-4/#color-transition-hint |
| // |
| // This gives an approximation of the gradient that might be visibly wrong, |
| // but maybe better than not parsing that value at all? |
| // It’s debatble whether that’s better or worse |
| // than not parsing and allowing authors to set a fallback. |
| // Either way, the best outcome is to add support. |
| // Gecko does so by approximating the non-linear interpolation |
| // by up to 10 piece-wise linear segments (9 intermediate color stops) |
| items |
| .iter() |
| .filter_map(|item| { |
| match item { |
| GradientItem::SimpleColorStop(color) => Some(ColorStop { |
| color: super::rgba(style.resolve_color(color)), |
| position: None, |
| }), |
| GradientItem::ComplexColorStop { color, position } => Some(ColorStop { |
| color: super::rgba(style.resolve_color(color)), |
| position: Some(if gradient_line_length.is_zero() { |
| 0. |
| } else { |
| position |
| .to_used_value(gradient_line_length) |
| .scale_by(1. / gradient_line_length.to_f32_px()) |
| .to_f32_px() |
| }), |
| }), |
| // FIXME: approximate like in: |
| // https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391 |
| GradientItem::InterpolationHint(_) => None, |
| } |
| }) |
| .collect() |
| } |
| |
| /// <https://drafts.csswg.org/css-images-4/#color-stop-fixup> |
| fn fixup_stops(stops: &mut [ColorStop<ColorF, f32>]) -> Vec<wr::GradientStop> { |
| assert!(!stops.is_empty()); |
| |
| // https://drafts.csswg.org/css-images-4/#color-stop-fixup |
| if let first_position @ None = &mut stops.first_mut().unwrap().position { |
| *first_position = Some(0.); |
| } |
| if let last_position @ None = &mut stops.last_mut().unwrap().position { |
| *last_position = Some(1.); |
| } |
| |
| let mut iter = stops.iter_mut(); |
| let mut max_so_far = iter.next().unwrap().position.unwrap(); |
| for stop in iter { |
| if let Some(position) = &mut stop.position { |
| if *position < max_so_far { |
| *position = max_so_far |
| } else { |
| max_so_far = *position |
| } |
| } |
| } |
| |
| let mut wr_stops = Vec::with_capacity(stops.len()); |
| let mut iter = stops.iter().enumerate(); |
| let (_, first) = iter.next().unwrap(); |
| let first_stop_position = first.position.unwrap(); |
| wr_stops.push(wr::GradientStop { |
| offset: first_stop_position, |
| color: first.color, |
| }); |
| if stops.len() == 1 { |
| wr_stops.push(wr_stops[0]); |
| } |
| |
| let mut last_positioned_stop_index = 0; |
| let mut last_positioned_stop_position = first_stop_position; |
| for (i, stop) in iter { |
| if let Some(position) = stop.position { |
| let step_count = i - last_positioned_stop_index; |
| if step_count > 1 { |
| let step = (position - last_positioned_stop_position) / step_count as f32; |
| for j in 1..step_count { |
| let color = stops[last_positioned_stop_index + j].color; |
| let offset = last_positioned_stop_position + j as f32 * step; |
| wr_stops.push(wr::GradientStop { offset, color }) |
| } |
| } |
| last_positioned_stop_index = i; |
| last_positioned_stop_position = position; |
| wr_stops.push(wr::GradientStop { |
| offset: position, |
| color: stop.color, |
| }) |
| } |
| } |
| |
| wr_stops |
| } |