Skip to main content

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, ColorMixItem, HueInterpolationMethod, mix_many};
9use style::color::{AbsoluteColor, ColorSpace};
10use style::properties::ComputedValues;
11use style::values::computed::image::{EndingShape, Gradient, LineDirection};
12use style::values::computed::{Angle, AngleOrPercentage, Color, LengthPercentage, Position};
13use style::values::generics::color::ColorMixFlags;
14use style::values::generics::image::{
15    Circle, ColorStop, Ellipse, GradientFlags, GradientItem, ShapeExtent,
16};
17use webrender_api::units::LayoutPixel;
18use webrender_api::{
19    self as wr, ConicGradient as WebRenderConicGradient, ExtendMode,
20    Gradient as WebRenderLinearGradient, RadialGradient as WebRenderRadialGradient, units,
21};
22
23pub(super) enum WebRenderGradient {
24    Linear(WebRenderLinearGradient),
25    Radial(WebRenderRadialGradient),
26    Conic(WebRenderConicGradient),
27}
28
29pub(super) fn build(
30    style: &ComputedValues,
31    gradient: &Gradient,
32    size: Size2D<f32, LayoutPixel>,
33    builder: &mut super::DisplayListBuilder,
34) -> WebRenderGradient {
35    match gradient {
36        Gradient::Linear {
37            items,
38            direction,
39            color_interpolation_method,
40            flags,
41            compat_mode: _,
42        } => build_linear(
43            style,
44            items,
45            direction,
46            color_interpolation_method,
47            *flags,
48            size,
49            builder,
50        ),
51        Gradient::Radial {
52            shape,
53            position,
54            color_interpolation_method,
55            items,
56            flags,
57            compat_mode: _,
58        } => build_radial(
59            style,
60            items,
61            shape,
62            position,
63            color_interpolation_method,
64            *flags,
65            size,
66            builder,
67        ),
68        Gradient::Conic {
69            angle,
70            position,
71            color_interpolation_method,
72            items,
73            flags,
74        } => build_conic(
75            style,
76            *angle,
77            position,
78            color_interpolation_method,
79            items,
80            *flags,
81            size,
82            builder,
83        ),
84    }
85}
86
87/// <https://drafts.csswg.org/css-images-3/#linear-gradients>
88pub(super) fn build_linear(
89    style: &ComputedValues,
90    items: &[GradientItem<Color, LengthPercentage>],
91    line_direction: &LineDirection,
92    color_interpolation_method: &ColorInterpolationMethod,
93    flags: GradientFlags,
94    gradient_box: Size2D<f32, LayoutPixel>,
95    builder: &mut super::DisplayListBuilder,
96) -> WebRenderGradient {
97    use style::values::specified::position::HorizontalPositionKeyword::*;
98    use style::values::specified::position::VerticalPositionKeyword::*;
99    use units::LayoutVector2D as Vec2;
100
101    // A vector of length 1.0 in the direction of the gradient line
102    let direction = match line_direction {
103        LineDirection::Horizontal(Right) => Vec2::new(1., 0.),
104        LineDirection::Vertical(Top) => Vec2::new(0., -1.),
105        LineDirection::Horizontal(Left) => Vec2::new(-1., 0.),
106        LineDirection::Vertical(Bottom) => Vec2::new(0., 1.),
107
108        LineDirection::Angle(angle) => {
109            let radians = angle.radians();
110            // “`0deg` points upward,
111            //  and positive angles represent clockwise rotation,
112            //  so `90deg` point toward the right.”
113            Vec2::new(radians.sin(), -radians.cos())
114        },
115
116        LineDirection::Corner(horizontal, vertical) => {
117            // “If the argument instead specifies a corner of the box such as `to top left`,
118            //  the gradient line must be angled such that it points
119            //  into the same quadrant as the specified corner,
120            //  and is perpendicular to a line intersecting
121            //  the two neighboring corners of the gradient box.”
122
123            // Note that that last line is a diagonal of the gradient box rectangle,
124            // since two neighboring corners of a third corner
125            // are necessarily opposite to each other.
126
127            // `{ x: gradient_box.width, y: gradient_box.height }` is such a diagonal vector,
128            // from the bottom left corner to the top right corner of the gradient box.
129            // (Both coordinates are positive.)
130            // Changing either or both signs produces the other three (oriented) diagonals.
131
132            // Swapping the coordinates `{ x: gradient_box.height, y: gradient_box.height }`
133            // produces a vector perpendicular to some diagonal of the rectangle.
134            // Finally, we choose the sign of each cartesian coordinate
135            // such that our vector points to the desired quadrant.
136
137            let x = match horizontal {
138                Right => gradient_box.height,
139                Left => -gradient_box.height,
140            };
141            let y = match vertical {
142                Top => -gradient_box.width,
143                Bottom => gradient_box.width,
144            };
145
146            // `{ x, y }` is now a vector of arbitrary length
147            // with the same direction as the gradient line.
148            // This normalizes the length to 1.0:
149            Vec2::new(x, y).normalize()
150        },
151    };
152
153    // This formula is given as `abs(W * sin(A)) + abs(H * cos(A))` in a note in the spec, under
154    // https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
155    //
156    // Sketch of a proof:
157    //
158    // * Take the top side of the gradient box rectangle. It is a segment of length `W`
159    // * Project onto the gradient line. You get a segment of length `abs(W * sin(A))`
160    // * Similarly, the left side of the rectangle (length `H`)
161    //   projects to a segment of length `abs(H * cos(A))`
162    // * These two segments add up to exactly the gradient line.
163    //
164    // See the illustration in the example under
165    // https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
166    let gradient_line_length =
167        (gradient_box.width * direction.x).abs() + (gradient_box.height * direction.y).abs();
168
169    let half_gradient_line = direction * (gradient_line_length / 2.);
170    let center = (gradient_box / 2.).to_vector().to_point();
171    let start_point = center - half_gradient_line;
172    let end_point = center + half_gradient_line;
173
174    let extend_mode = if flags.contains(GradientFlags::REPEATING) {
175        wr::ExtendMode::Repeat
176    } else {
177        wr::ExtendMode::Clamp
178    };
179
180    let mut color_stops =
181        gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length));
182    let stops = create_webrender_stops(&mut color_stops, color_interpolation_method, extend_mode);
183
184    WebRenderGradient::Linear(builder.wr().create_gradient(
185        start_point,
186        end_point,
187        stops,
188        extend_mode,
189    ))
190}
191
192/// <https://drafts.csswg.org/css-images-3/#radial-gradients>
193#[expect(clippy::too_many_arguments)]
194pub(super) fn build_radial(
195    style: &ComputedValues,
196    items: &[GradientItem<Color, LengthPercentage>],
197    shape: &EndingShape,
198    center: &Position,
199    color_interpolation_method: &ColorInterpolationMethod,
200    flags: GradientFlags,
201    gradient_box: Size2D<f32, LayoutPixel>,
202    builder: &mut super::DisplayListBuilder,
203) -> WebRenderGradient {
204    let center = units::LayoutPoint::new(
205        center
206            .horizontal
207            .to_used_value(Au::from_f32_px(gradient_box.width))
208            .to_f32_px(),
209        center
210            .vertical
211            .to_used_value(Au::from_f32_px(gradient_box.height))
212            .to_f32_px(),
213    );
214    let radii = match shape {
215        EndingShape::Circle(circle) => {
216            let radius = match circle {
217                Circle::Radius(r) => r.0.px(),
218                Circle::Extent(extent) => match extent {
219                    ShapeExtent::ClosestSide | ShapeExtent::Contain => {
220                        let vec = abs_vector_to_corner(gradient_box, center, f32::min);
221                        vec.x.min(vec.y)
222                    },
223                    ShapeExtent::FarthestSide => {
224                        let vec = abs_vector_to_corner(gradient_box, center, f32::max);
225                        vec.x.max(vec.y)
226                    },
227                    ShapeExtent::ClosestCorner => {
228                        abs_vector_to_corner(gradient_box, center, f32::min).length()
229                    },
230                    ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
231                        abs_vector_to_corner(gradient_box, center, f32::max).length()
232                    },
233                },
234            };
235            units::LayoutSize::new(radius, radius)
236        },
237        EndingShape::Ellipse(Ellipse::Radii(rx, ry)) => units::LayoutSize::new(
238            rx.0.to_used_value(Au::from_f32_px(gradient_box.width))
239                .to_f32_px(),
240            ry.0.to_used_value(Au::from_f32_px(gradient_box.height))
241                .to_f32_px(),
242        ),
243        EndingShape::Ellipse(Ellipse::Extent(extent)) => match extent {
244            ShapeExtent::ClosestSide | ShapeExtent::Contain => {
245                abs_vector_to_corner(gradient_box, center, f32::min).to_size()
246            },
247            ShapeExtent::FarthestSide => {
248                abs_vector_to_corner(gradient_box, center, f32::max).to_size()
249            },
250            ShapeExtent::ClosestCorner => {
251                abs_vector_to_corner(gradient_box, center, f32::min).to_size() *
252                    (std::f32::consts::FRAC_1_SQRT_2 * 2.0)
253            },
254            ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
255                abs_vector_to_corner(gradient_box, center, f32::max).to_size() *
256                    (std::f32::consts::FRAC_1_SQRT_2 * 2.0)
257            },
258        },
259    };
260
261    /// Returns the distance to the nearest or farthest sides in the respective dimension,
262    /// depending on `select`.
263    fn abs_vector_to_corner(
264        gradient_box: units::LayoutSize,
265        center: units::LayoutPoint,
266        select: impl Fn(f32, f32) -> f32,
267    ) -> units::LayoutVector2D {
268        let left = center.x.abs();
269        let top = center.y.abs();
270        let right = (gradient_box.width - center.x).abs();
271        let bottom = (gradient_box.height - center.y).abs();
272        units::LayoutVector2D::new(select(left, right), select(top, bottom))
273    }
274
275    // “The gradient line’s starting point is at the center of the gradient,
276    //  and it extends toward the right, with the ending point on the point
277    //  where the gradient line intersects the ending shape.”
278    let gradient_line_length = radii.width;
279
280    let extend_mode = if flags.contains(GradientFlags::REPEATING) {
281        wr::ExtendMode::Repeat
282    } else {
283        wr::ExtendMode::Clamp
284    };
285
286    let mut color_stops =
287        gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length));
288    let stops = create_webrender_stops(&mut color_stops, color_interpolation_method, extend_mode);
289
290    WebRenderGradient::Radial(builder.wr().create_radial_gradient(
291        center,
292        radii,
293        stops,
294        extend_mode,
295    ))
296}
297
298/// <https://drafts.csswg.org/css-images-4/#conic-gradients>
299#[expect(clippy::too_many_arguments)]
300fn build_conic(
301    style: &ComputedValues,
302    angle: Angle,
303    center: &Position,
304    color_interpolation_method: &ColorInterpolationMethod,
305    items: &[GradientItem<Color, AngleOrPercentage>],
306    flags: GradientFlags,
307    gradient_box: Size2D<f32, LayoutPixel>,
308    builder: &mut super::DisplayListBuilder<'_>,
309) -> WebRenderGradient {
310    let center = units::LayoutPoint::new(
311        center
312            .horizontal
313            .to_used_value(Au::from_f32_px(gradient_box.width))
314            .to_f32_px(),
315        center
316            .vertical
317            .to_used_value(Au::from_f32_px(gradient_box.height))
318            .to_f32_px(),
319    );
320
321    let extend_mode = if flags.contains(GradientFlags::REPEATING) {
322        wr::ExtendMode::Repeat
323    } else {
324        wr::ExtendMode::Clamp
325    };
326
327    let mut color_stops = conic_gradient_items_to_color_stops(style, items);
328    let stops = create_webrender_stops(&mut color_stops, color_interpolation_method, extend_mode);
329
330    WebRenderGradient::Conic(builder.wr().create_conic_gradient(
331        center,
332        angle.radians(),
333        stops,
334        extend_mode,
335    ))
336}
337
338fn conic_gradient_items_to_color_stops(
339    style: &ComputedValues,
340    items: &[GradientItem<Color, AngleOrPercentage>],
341) -> Vec<ColorStop<AbsoluteColor, f32>> {
342    // Remove color transititon hints, which are not supported yet.
343    // https://drafts.csswg.org/css-images-4/#color-transition-hint
344    //
345    // This gives an approximation of the gradient that might be visibly wrong,
346    // but maybe better than not parsing that value at all?
347    // It’s debatble whether that’s better or worse
348    // than not parsing and allowing authors to set a fallback.
349    // Either way, the best outcome is to add support.
350    // Gecko does so by approximating the non-linear interpolation
351    // by up to 10 piece-wise linear segments (9 intermediate color stops)
352    items
353        .iter()
354        .filter_map(|item| {
355            match item {
356                GradientItem::SimpleColorStop(color) => Some(ColorStop {
357                    color: style.resolve_color(color),
358                    position: None,
359                }),
360                GradientItem::ComplexColorStop { color, position } => Some(ColorStop {
361                    color: style.resolve_color(color),
362                    position: match position {
363                        AngleOrPercentage::Percentage(percentage) => Some(percentage.0),
364                        AngleOrPercentage::Angle(angle) => Some(angle.degrees() / 360.),
365                    },
366                }),
367                // FIXME: approximate like in:
368                // https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391
369                GradientItem::InterpolationHint(_) => None,
370            }
371        })
372        .collect()
373}
374
375fn gradient_items_to_color_stops(
376    style: &ComputedValues,
377    items: &[GradientItem<Color, LengthPercentage>],
378    gradient_line_length: Au,
379) -> Vec<ColorStop<AbsoluteColor, f32>> {
380    // Remove color transititon hints, which are not supported yet.
381    // https://drafts.csswg.org/css-images-4/#color-transition-hint
382    //
383    // This gives an approximation of the gradient that might be visibly wrong,
384    // but maybe better than not parsing that value at all?
385    // It’s debatble whether that’s better or worse
386    // than not parsing and allowing authors to set a fallback.
387    // Either way, the best outcome is to add support.
388    // Gecko does so by approximating the non-linear interpolation
389    // by up to 10 piece-wise linear segments (9 intermediate color stops)
390    items
391        .iter()
392        .filter_map(|item| {
393            match item {
394                GradientItem::SimpleColorStop(color) => Some(ColorStop {
395                    color: style.resolve_color(color),
396                    position: None,
397                }),
398                GradientItem::ComplexColorStop { color, position } => Some(ColorStop {
399                    color: style.resolve_color(color),
400                    position: Some(if gradient_line_length.is_zero() {
401                        0.
402                    } else {
403                        position
404                            .to_used_value(gradient_line_length)
405                            .scale_by(1. / gradient_line_length.to_f32_px())
406                            .to_f32_px()
407                    }),
408                }),
409                // FIXME: approximate like in:
410                // https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391
411                GradientItem::InterpolationHint(_) => None,
412            }
413        })
414        .collect()
415}
416
417fn create_webrender_stops(
418    stops: &mut [ColorStop<AbsoluteColor, f32>],
419    interpolation_method: &ColorInterpolationMethod,
420    extend_mode: ExtendMode,
421) -> Vec<wr::GradientStop> {
422    let stops = fixup_stops(stops);
423    if interpolation_method.space != ColorSpace::Srgb {
424        return interpolate_gradient_stop_colors(&stops, interpolation_method, extend_mode);
425    }
426
427    stops
428        .iter()
429        .map(|stop| wr::GradientStop {
430            color: super::rgba(stop.color),
431            offset: stop.position,
432        })
433        .collect()
434}
435
436#[derive(Clone, Copy)]
437struct UsedColorStop {
438    color: AbsoluteColor,
439    position: f32,
440}
441
442/// <https://drafts.csswg.org/css-images-4/#color-stop-fixup>
443fn fixup_stops(stops: &mut [ColorStop<AbsoluteColor, f32>]) -> Vec<UsedColorStop> {
444    assert!(!stops.is_empty());
445
446    // https://drafts.csswg.org/css-images-4/#color-stop-fixup
447    if let first_position @ None = &mut stops.first_mut().unwrap().position {
448        *first_position = Some(0.);
449    }
450    if let last_position @ None = &mut stops.last_mut().unwrap().position {
451        *last_position = Some(1.);
452    }
453
454    let mut iter = stops.iter_mut();
455    let mut max_so_far = iter.next().unwrap().position.unwrap();
456    for stop in iter {
457        if let Some(position) = &mut stop.position {
458            if *position < max_so_far {
459                *position = max_so_far
460            } else {
461                max_so_far = *position
462            }
463        }
464    }
465
466    let mut used_color_stops = Vec::with_capacity(stops.len());
467    let mut iter = stops.iter().enumerate();
468    let (_, first) = iter.next().unwrap();
469    let first_stop_position = first.position.unwrap();
470    used_color_stops.push(UsedColorStop {
471        position: first_stop_position,
472        color: first.color,
473    });
474    if stops.len() == 1 {
475        used_color_stops.push(used_color_stops[0]);
476    }
477
478    let mut last_positioned_stop_index = 0;
479    let mut last_positioned_stop_position = first_stop_position;
480    for (i, stop) in iter {
481        if let Some(position) = stop.position {
482            let step_count = i - last_positioned_stop_index;
483            if step_count > 1 {
484                let step = (position - last_positioned_stop_position) / step_count as f32;
485                for j in 1..step_count {
486                    let color = stops[last_positioned_stop_index + j].color;
487                    let position = last_positioned_stop_position + j as f32 * step;
488                    used_color_stops.push(UsedColorStop { position, color })
489                }
490            }
491            last_positioned_stop_index = i;
492            last_positioned_stop_position = position;
493            used_color_stops.push(UsedColorStop {
494                position,
495                color: stop.color,
496            })
497        }
498    }
499
500    used_color_stops
501}
502
503/// This is a port of Gecko's WrColorStopInterpolator:
504///
505/// See
506/// <https://searchfox.org/firefox-main/rev/4b851f6b592ecf1112ee47dd25e8de28c892ad67/layout/painting/nsCSSRenderingGradients.cpp#1200>
507fn interpolate_gradient_stop_colors(
508    stops: &[UsedColorStop],
509    interpolation_method: &ColorInterpolationMethod,
510    extend_mode: wr::ExtendMode,
511) -> Vec<wr::GradientStop> {
512    // This could be made tunable, but at 1.0/128 the error is largely
513    // irrelevant, as WebRender re-encodes it to 128 pairs of stops.
514    //
515    // Note that we don't attempt to place the positions of these stops
516    // precisely at intervals, we just add this many extra stops across the
517    // range where it is convenient.
518    const FULL_RANGE_EXTRA_STOPS: usize = 128;
519
520    // This indicates that we want to extend the end position on the last stop,
521    // which only matters if this is a CSS non-repeating gradient with
522    // StyleHueInterpolationMethod::Longer (only valid for hsl/hwb/lch/oklch).
523    //
524    // For the specific case of longer hue interpolation on a CSS non-repeating
525    // gradient, we have to pretend there is another stop at position=1.0 that
526    // duplicates the last stop, this is probably only used for things like a
527    // color wheel.  No such problem for SVG as it doesn't have that complexity.
528    let extend = extend_mode == wr::ExtendMode::Clamp &&
529        interpolation_method.hue == HueInterpolationMethod::Longer;
530
531    // We always emit at least two stops (start and end) for each input stop,
532    // which avoids ambiguity with incomplete oklch/lch/hsv/hsb color stops for
533    // the last stop pair, where the last color stop can't be interpreted on its
534    // own because it actually depends on the previous stop.
535    let mut output = Vec::with_capacity(stops.len() * 2 + FULL_RANGE_EXTRA_STOPS);
536
537    // This loop intentionally iterates extra stops at the beginning and end
538    // if extending was requested, or in the degenerate case where only one
539    // color stop was specified.
540    let extend = extend || stops.len() == 1;
541    let mut iter_stops = stops.len() - 1;
542    if extend {
543        iter_stops += 2;
544    }
545
546    for index in 0..iter_stops {
547        let this_index = if extend {
548            index.saturating_sub(1)
549        } else {
550            index
551        };
552
553        let next_index = if extend && (index == iter_stops - 1 || index == 0) {
554            this_index
555        } else {
556            this_index + 1
557        };
558
559        let start = &stops[this_index];
560        let end = &stops[next_index];
561        let mut start_position = start.position;
562        let mut end_position = end.position;
563
564        // For CSS non-repeating gradients with longer hue specified, we have to
565        // pretend there is a stop beyond the last stop, and one before the first.
566        // This is never the case on SVG gradients as they only use shorter hue.
567        //
568        // See https://bugzilla.mozilla.org/show_bug.cgi?id=1885716 for more info.
569        let mut extra_stops = 0;
570        if extend {
571            // If we're extending, we just need a single new stop, which will
572            // duplicate the end being extended; do not create interpolated stops
573            // within the extension area!
574            if index == 0 {
575                start_position = start_position.min(0.0);
576                extra_stops = 1;
577            }
578            if index == iter_stops - 1 {
579                end_position = end_position.max(1.0);
580                extra_stops = 1;
581            }
582        }
583
584        if extra_stops == 0 {
585            // Within the actual gradient range, figure out how many extra stops
586            // to use for this section of the gradient.
587            extra_stops = (end_position * FULL_RANGE_EXTRA_STOPS as f32).floor() as u32;
588            extra_stops = extra_stops.clamp(1, FULL_RANGE_EXTRA_STOPS as u32);
589        }
590
591        let step = 1.0 / (extra_stops as f32);
592        for extra_stop in 0..=extra_stops {
593            let progress = (extra_stop as f32) * step;
594            let position = start_position + progress * (end_position - start_position);
595
596            let start_color = start.color;
597            let end_color = end.color;
598            let color = mix_many(
599                *interpolation_method,
600                [
601                    ColorMixItem::new(start_color, 1.0 - progress),
602                    ColorMixItem::new(end_color, progress),
603                ],
604                ColorMixFlags::empty(),
605            );
606
607            output.push(wr::GradientStop {
608                color: super::rgba(color),
609                offset: position,
610            });
611        }
612    }
613
614    output
615}