layout/display_list/
gradient.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use app_units::Au;
6use euclid::Size2D;
7use style::Zero;
8use style::color::mix::ColorInterpolationMethod;
9use style::properties::ComputedValues;
10use style::values::computed::image::{EndingShape, Gradient, LineDirection};
11use style::values::computed::{Angle, AngleOrPercentage, Color, LengthPercentage, Position};
12use style::values::generics::image::{
13    Circle, ColorStop, Ellipse, GradientFlags, GradientItem, ShapeExtent,
14};
15use webrender_api::units::LayoutPixel;
16use webrender_api::{
17    self as wr, ConicGradient as WebRenderConicGradient, Gradient as WebRenderLinearGradient,
18    RadialGradient as WebRenderRadialGradient, units,
19};
20use wr::ColorF;
21
22pub(super) enum WebRenderGradient {
23    Linear(WebRenderLinearGradient),
24    Radial(WebRenderRadialGradient),
25    Conic(WebRenderConicGradient),
26}
27
28pub(super) fn build(
29    style: &ComputedValues,
30    gradient: &Gradient,
31    size: Size2D<f32, LayoutPixel>,
32    builder: &mut super::DisplayListBuilder,
33) -> WebRenderGradient {
34    match gradient {
35        Gradient::Linear {
36            items,
37            direction,
38            color_interpolation_method,
39            flags,
40            compat_mode: _,
41        } => build_linear(
42            style,
43            items,
44            direction,
45            color_interpolation_method,
46            *flags,
47            size,
48            builder,
49        ),
50        Gradient::Radial {
51            shape,
52            position,
53            color_interpolation_method,
54            items,
55            flags,
56            compat_mode: _,
57        } => build_radial(
58            style,
59            items,
60            shape,
61            position,
62            color_interpolation_method,
63            *flags,
64            size,
65            builder,
66        ),
67        Gradient::Conic {
68            angle,
69            position,
70            color_interpolation_method,
71            items,
72            flags,
73        } => build_conic(
74            style,
75            *angle,
76            position,
77            *color_interpolation_method,
78            items,
79            *flags,
80            size,
81            builder,
82        ),
83    }
84}
85
86/// <https://drafts.csswg.org/css-images-3/#linear-gradients>
87pub(super) fn build_linear(
88    style: &ComputedValues,
89    items: &[GradientItem<Color, LengthPercentage>],
90    line_direction: &LineDirection,
91    _color_interpolation_method: &ColorInterpolationMethod,
92    flags: GradientFlags,
93    gradient_box: Size2D<f32, LayoutPixel>,
94    builder: &mut super::DisplayListBuilder,
95) -> WebRenderGradient {
96    use style::values::specified::position::HorizontalPositionKeyword::*;
97    use style::values::specified::position::VerticalPositionKeyword::*;
98    use units::LayoutVector2D as Vec2;
99
100    // A vector of length 1.0 in the direction of the gradient line
101    let direction = match line_direction {
102        LineDirection::Horizontal(Right) => Vec2::new(1., 0.),
103        LineDirection::Vertical(Top) => Vec2::new(0., -1.),
104        LineDirection::Horizontal(Left) => Vec2::new(-1., 0.),
105        LineDirection::Vertical(Bottom) => Vec2::new(0., 1.),
106
107        LineDirection::Angle(angle) => {
108            let radians = angle.radians();
109            // “`0deg` points upward,
110            //  and positive angles represent clockwise rotation,
111            //  so `90deg` point toward the right.”
112            Vec2::new(radians.sin(), -radians.cos())
113        },
114
115        LineDirection::Corner(horizontal, vertical) => {
116            // “If the argument instead specifies a corner of the box such as `to top left`,
117            //  the gradient line must be angled such that it points
118            //  into the same quadrant as the specified corner,
119            //  and is perpendicular to a line intersecting
120            //  the two neighboring corners of the gradient box.”
121
122            // Note that that last line is a diagonal of the gradient box rectangle,
123            // since two neighboring corners of a third corner
124            // are necessarily opposite to each other.
125
126            // `{ x: gradient_box.width, y: gradient_box.height }` is such a diagonal vector,
127            // from the bottom left corner to the top right corner of the gradient box.
128            // (Both coordinates are positive.)
129            // Changing either or both signs produces the other three (oriented) diagonals.
130
131            // Swapping the coordinates `{ x: gradient_box.height, y: gradient_box.height }`
132            // produces a vector perpendicular to some diagonal of the rectangle.
133            // Finally, we choose the sign of each cartesian coordinate
134            // such that our vector points to the desired quadrant.
135
136            let x = match horizontal {
137                Right => gradient_box.height,
138                Left => -gradient_box.height,
139            };
140            let y = match vertical {
141                Top => -gradient_box.width,
142                Bottom => gradient_box.width,
143            };
144
145            // `{ x, y }` is now a vector of arbitrary length
146            // with the same direction as the gradient line.
147            // This normalizes the length to 1.0:
148            Vec2::new(x, y).normalize()
149        },
150    };
151
152    // This formula is given as `abs(W * sin(A)) + abs(H * cos(A))` in a note in the spec, under
153    // https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
154    //
155    // Sketch of a proof:
156    //
157    // * Take the top side of the gradient box rectangle. It is a segment of length `W`
158    // * Project onto the gradient line. You get a segment of length `abs(W * sin(A))`
159    // * Similarly, the left side of the rectangle (length `H`)
160    //   projects to a segment of length `abs(H * cos(A))`
161    // * These two segments add up to exactly the gradient line.
162    //
163    // See the illustration in the example under
164    // https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
165    let gradient_line_length =
166        (gradient_box.width * direction.x).abs() + (gradient_box.height * direction.y).abs();
167
168    let half_gradient_line = direction * (gradient_line_length / 2.);
169    let center = (gradient_box / 2.).to_vector().to_point();
170    let start_point = center - half_gradient_line;
171    let end_point = center + half_gradient_line;
172
173    let mut color_stops =
174        gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length));
175    let stops = fixup_stops(&mut color_stops);
176    let extend_mode = if flags.contains(GradientFlags::REPEATING) {
177        wr::ExtendMode::Repeat
178    } else {
179        wr::ExtendMode::Clamp
180    };
181    WebRenderGradient::Linear(builder.wr().create_gradient(
182        start_point,
183        end_point,
184        stops,
185        extend_mode,
186    ))
187}
188
189/// <https://drafts.csswg.org/css-images-3/#radial-gradients>
190#[allow(clippy::too_many_arguments)]
191pub(super) fn build_radial(
192    style: &ComputedValues,
193    items: &[GradientItem<Color, LengthPercentage>],
194    shape: &EndingShape,
195    center: &Position,
196    _color_interpolation_method: &ColorInterpolationMethod,
197    flags: GradientFlags,
198    gradient_box: Size2D<f32, LayoutPixel>,
199    builder: &mut super::DisplayListBuilder,
200) -> WebRenderGradient {
201    let center = units::LayoutPoint::new(
202        center
203            .horizontal
204            .to_used_value(Au::from_f32_px(gradient_box.width))
205            .to_f32_px(),
206        center
207            .vertical
208            .to_used_value(Au::from_f32_px(gradient_box.height))
209            .to_f32_px(),
210    );
211    let radii = match shape {
212        EndingShape::Circle(circle) => {
213            let radius = match circle {
214                Circle::Radius(r) => r.0.px(),
215                Circle::Extent(extent) => match extent {
216                    ShapeExtent::ClosestSide | ShapeExtent::Contain => {
217                        let vec = abs_vector_to_corner(gradient_box, center, f32::min);
218                        vec.x.min(vec.y)
219                    },
220                    ShapeExtent::FarthestSide => {
221                        let vec = abs_vector_to_corner(gradient_box, center, f32::max);
222                        vec.x.max(vec.y)
223                    },
224                    ShapeExtent::ClosestCorner => {
225                        abs_vector_to_corner(gradient_box, center, f32::min).length()
226                    },
227                    ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
228                        abs_vector_to_corner(gradient_box, center, f32::max).length()
229                    },
230                },
231            };
232            units::LayoutSize::new(radius, radius)
233        },
234        EndingShape::Ellipse(Ellipse::Radii(rx, ry)) => units::LayoutSize::new(
235            rx.0.to_used_value(Au::from_f32_px(gradient_box.width))
236                .to_f32_px(),
237            ry.0.to_used_value(Au::from_f32_px(gradient_box.height))
238                .to_f32_px(),
239        ),
240        EndingShape::Ellipse(Ellipse::Extent(extent)) => match extent {
241            ShapeExtent::ClosestSide | ShapeExtent::Contain => {
242                abs_vector_to_corner(gradient_box, center, f32::min).to_size()
243            },
244            ShapeExtent::FarthestSide => {
245                abs_vector_to_corner(gradient_box, center, f32::max).to_size()
246            },
247            ShapeExtent::ClosestCorner => {
248                abs_vector_to_corner(gradient_box, center, f32::min).to_size() *
249                    (std::f32::consts::FRAC_1_SQRT_2 * 2.0)
250            },
251            ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
252                abs_vector_to_corner(gradient_box, center, f32::max).to_size() *
253                    (std::f32::consts::FRAC_1_SQRT_2 * 2.0)
254            },
255        },
256    };
257
258    /// Returns the distance to the nearest or farthest sides in the respective dimension,
259    /// depending on `select`.
260    fn abs_vector_to_corner(
261        gradient_box: units::LayoutSize,
262        center: units::LayoutPoint,
263        select: impl Fn(f32, f32) -> f32,
264    ) -> units::LayoutVector2D {
265        let left = center.x.abs();
266        let top = center.y.abs();
267        let right = (gradient_box.width - center.x).abs();
268        let bottom = (gradient_box.height - center.y).abs();
269        units::LayoutVector2D::new(select(left, right), select(top, bottom))
270    }
271
272    // “The gradient line’s starting point is at the center of the gradient,
273    //  and it extends toward the right, with the ending point on the point
274    //  where the gradient line intersects the ending shape.”
275    let gradient_line_length = radii.width;
276
277    let mut color_stops =
278        gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length));
279    let stops = fixup_stops(&mut color_stops);
280    let extend_mode = if flags.contains(GradientFlags::REPEATING) {
281        wr::ExtendMode::Repeat
282    } else {
283        wr::ExtendMode::Clamp
284    };
285    WebRenderGradient::Radial(builder.wr().create_radial_gradient(
286        center,
287        radii,
288        stops,
289        extend_mode,
290    ))
291}
292
293/// <https://drafts.csswg.org/css-images-4/#conic-gradients>
294#[allow(clippy::too_many_arguments)]
295fn build_conic(
296    style: &ComputedValues,
297    angle: Angle,
298    center: &Position,
299    _color_interpolation_method: ColorInterpolationMethod,
300    items: &[GradientItem<Color, AngleOrPercentage>],
301    flags: GradientFlags,
302    gradient_box: Size2D<f32, LayoutPixel>,
303    builder: &mut super::DisplayListBuilder<'_>,
304) -> WebRenderGradient {
305    let center = units::LayoutPoint::new(
306        center
307            .horizontal
308            .to_used_value(Au::from_f32_px(gradient_box.width))
309            .to_f32_px(),
310        center
311            .vertical
312            .to_used_value(Au::from_f32_px(gradient_box.height))
313            .to_f32_px(),
314    );
315    let mut color_stops = conic_gradient_items_to_color_stops(style, items);
316    let stops = fixup_stops(&mut color_stops);
317    let extend_mode = if flags.contains(GradientFlags::REPEATING) {
318        wr::ExtendMode::Repeat
319    } else {
320        wr::ExtendMode::Clamp
321    };
322    WebRenderGradient::Conic(builder.wr().create_conic_gradient(
323        center,
324        angle.radians(),
325        stops,
326        extend_mode,
327    ))
328}
329
330fn conic_gradient_items_to_color_stops(
331    style: &ComputedValues,
332    items: &[GradientItem<Color, AngleOrPercentage>],
333) -> Vec<ColorStop<ColorF, f32>> {
334    // Remove color transititon hints, which are not supported yet.
335    // https://drafts.csswg.org/css-images-4/#color-transition-hint
336    //
337    // This gives an approximation of the gradient that might be visibly wrong,
338    // but maybe better than not parsing that value at all?
339    // It’s debatble whether that’s better or worse
340    // than not parsing and allowing authors to set a fallback.
341    // Either way, the best outcome is to add support.
342    // Gecko does so by approximating the non-linear interpolation
343    // by up to 10 piece-wise linear segments (9 intermediate color stops)
344    items
345        .iter()
346        .filter_map(|item| {
347            match item {
348                GradientItem::SimpleColorStop(color) => Some(ColorStop {
349                    color: super::rgba(style.resolve_color(color)),
350                    position: None,
351                }),
352                GradientItem::ComplexColorStop { color, position } => Some(ColorStop {
353                    color: super::rgba(style.resolve_color(color)),
354                    position: match position {
355                        AngleOrPercentage::Percentage(percentage) => Some(percentage.0),
356                        AngleOrPercentage::Angle(angle) => Some(angle.degrees() / 360.),
357                    },
358                }),
359                // FIXME: approximate like in:
360                // https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391
361                GradientItem::InterpolationHint(_) => None,
362            }
363        })
364        .collect()
365}
366
367fn gradient_items_to_color_stops(
368    style: &ComputedValues,
369    items: &[GradientItem<Color, LengthPercentage>],
370    gradient_line_length: Au,
371) -> Vec<ColorStop<ColorF, f32>> {
372    // Remove color transititon hints, which are not supported yet.
373    // https://drafts.csswg.org/css-images-4/#color-transition-hint
374    //
375    // This gives an approximation of the gradient that might be visibly wrong,
376    // but maybe better than not parsing that value at all?
377    // It’s debatble whether that’s better or worse
378    // than not parsing and allowing authors to set a fallback.
379    // Either way, the best outcome is to add support.
380    // Gecko does so by approximating the non-linear interpolation
381    // by up to 10 piece-wise linear segments (9 intermediate color stops)
382    items
383        .iter()
384        .filter_map(|item| {
385            match item {
386                GradientItem::SimpleColorStop(color) => Some(ColorStop {
387                    color: super::rgba(style.resolve_color(color)),
388                    position: None,
389                }),
390                GradientItem::ComplexColorStop { color, position } => Some(ColorStop {
391                    color: super::rgba(style.resolve_color(color)),
392                    position: Some(if gradient_line_length.is_zero() {
393                        0.
394                    } else {
395                        position
396                            .to_used_value(gradient_line_length)
397                            .scale_by(1. / gradient_line_length.to_f32_px())
398                            .to_f32_px()
399                    }),
400                }),
401                // FIXME: approximate like in:
402                // https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391
403                GradientItem::InterpolationHint(_) => None,
404            }
405        })
406        .collect()
407}
408
409/// <https://drafts.csswg.org/css-images-4/#color-stop-fixup>
410fn fixup_stops(stops: &mut [ColorStop<ColorF, f32>]) -> Vec<wr::GradientStop> {
411    assert!(!stops.is_empty());
412
413    // https://drafts.csswg.org/css-images-4/#color-stop-fixup
414    if let first_position @ None = &mut stops.first_mut().unwrap().position {
415        *first_position = Some(0.);
416    }
417    if let last_position @ None = &mut stops.last_mut().unwrap().position {
418        *last_position = Some(1.);
419    }
420
421    let mut iter = stops.iter_mut();
422    let mut max_so_far = iter.next().unwrap().position.unwrap();
423    for stop in iter {
424        if let Some(position) = &mut stop.position {
425            if *position < max_so_far {
426                *position = max_so_far
427            } else {
428                max_so_far = *position
429            }
430        }
431    }
432
433    let mut wr_stops = Vec::with_capacity(stops.len());
434    let mut iter = stops.iter().enumerate();
435    let (_, first) = iter.next().unwrap();
436    let first_stop_position = first.position.unwrap();
437    wr_stops.push(wr::GradientStop {
438        offset: first_stop_position,
439        color: first.color,
440    });
441    if stops.len() == 1 {
442        wr_stops.push(wr_stops[0]);
443    }
444
445    let mut last_positioned_stop_index = 0;
446    let mut last_positioned_stop_position = first_stop_position;
447    for (i, stop) in iter {
448        if let Some(position) = stop.position {
449            let step_count = i - last_positioned_stop_index;
450            if step_count > 1 {
451                let step = (position - last_positioned_stop_position) / step_count as f32;
452                for j in 1..step_count {
453                    let color = stops[last_positioned_stop_index + j].color;
454                    let offset = last_positioned_stop_position + j as f32 * step;
455                    wr_stops.push(wr::GradientStop { offset, color })
456                }
457            }
458            last_positioned_stop_index = i;
459            last_positioned_stop_position = position;
460            wr_stops.push(wr::GradientStop {
461                offset: position,
462                color: stop.color,
463            })
464        }
465    }
466
467    wr_stops
468}