Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Background follow ups #25594

Merged
merged 10 commits into from Jan 25, 2020

Render gradients

  • Loading branch information
SimonSapin committed Jan 24, 2020
commit 632e731760cd6b41fa33952c80197f94549fab6e
@@ -0,0 +1,336 @@
/* 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 style::properties::ComputedValues;
use style::values::computed::image::{EndingShape, Gradient, LineDirection};
use style::values::computed::{GradientItem, Length, Position};
use style::values::generics::image::GenericGradientKind as Kind;
use style::values::generics::image::{Circle, ColorStop, Ellipse, ShapeExtent};
use webrender_api::{self as wr, units};

pub(super) fn build(
style: &ComputedValues,
gradient: &Gradient,
layer: &super::background::Layer,
builder: &mut super::DisplayListBuilder,
) {
let extend_mode = if gradient.repeating {
wr::ExtendMode::Repeat
} else {
wr::ExtendMode::Clamp
};
match &gradient.kind {
Kind::Linear(line_direction) => build_linear(
style,
&gradient.items,
line_direction,
extend_mode,
&layer,
builder,
),
Kind::Radial(ending_shape, center) => build_radial(
style,
&gradient.items,
ending_shape,
center,
extend_mode,
&layer,
builder,
),
}
}

/// https://drafts.csswg.org/css-images-3/#linear-gradients
pub(super) fn build_linear(
style: &ComputedValues,
items: &[GradientItem],
line_direction: &LineDirection,
extend_mode: wr::ExtendMode,
layer: &super::background::Layer,
builder: &mut super::DisplayListBuilder,
) {
use style::values::specified::position::HorizontalPositionKeyword::*;
use style::values::specified::position::VerticalPositionKeyword::*;
use webrender_api::units::LayoutVector2D as Vec2;
let gradient_box = layer.tile_size;

// 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 comment has been minimized.

Copy link
@mrobinson

mrobinson Jan 24, 2020

Member

Extra line here?

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 stops = stops_fixup(style, items, Length::new(gradient_line_length));
let linear_gradient = builder
.wr
.create_gradient(start_point, end_point, stops, extend_mode);
builder.wr.push_gradient(
&layer.common,
layer.bounds,
linear_gradient,
layer.tile_size,
layer.tile_spacing,
)
}

/// https://drafts.csswg.org/css-images-3/#radial-gradients
pub(super) fn build_radial(
style: &ComputedValues,
items: &[GradientItem],
shape: &EndingShape,
center: &Position,
extend_mode: wr::ExtendMode,
layer: &super::background::Layer,
builder: &mut super::DisplayListBuilder,
) {
let gradient_box = layer.tile_size;
let center = units::LayoutPoint::new(
center
.horizontal
.percentage_relative_to(Length::new(gradient_box.width))
.px(),
center
.vertical
.percentage_relative_to(Length::new(gradient_box.height))
.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.percentage_relative_to(Length::new(gradient_box.width))
.px(),
ry.0.percentage_relative_to(Length::new(gradient_box.height))
.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 stops = stops_fixup(style, items, Length::new(gradient_line_length));
let radial_gradient = builder
.wr
.create_radial_gradient(center, radii, stops, extend_mode);
builder.wr.push_radial_gradient(
&layer.common,
layer.bounds,
radial_gradient,
layer.tile_size,
layer.tile_spacing,
)
}

/// https://drafts.csswg.org/css-images-4/#color-stop-fixup
fn stops_fixup(

This comment has been minimized.

Copy link
@mrobinson

mrobinson Jan 24, 2020

Member

Maybe name this fixup_stops to make the function name a verb?

This comment has been minimized.

Copy link
@SimonSapin

SimonSapin Jan 25, 2020

Author Member

Done

style: &ComputedValues,
items: &[GradientItem],
gradient_line_length: Length,
) -> Vec<wr::GradientStop> {
// 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)
let mut stops = Vec::with_capacity(items.len());
for item in items {
match item {
GradientItem::SimpleColorStop(color) => stops.push(ColorStop {
color: super::rgba(style.resolve_color(*color)),
position: None,
}),
GradientItem::ComplexColorStop { color, position } => stops.push(ColorStop {
color: super::rgba(style.resolve_color(*color)),
position: Some(if gradient_line_length.px() == 0. {
0.
} else {
position.percentage_relative_to(gradient_line_length).px() /
gradient_line_length.px()
}),
}),
GradientItem::InterpolationHint(_) => {
// FIXME: approximate like in:
// https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391
},
}
}
assert!(stops.len() >= 2);

// 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,
});

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
}
@@ -19,6 +19,7 @@ use style::values::specified::ui::CursorKind;
use webrender_api::{self as wr, units};

mod background;
mod gradient;

#[derive(Clone, Copy)]
pub struct WebRenderImageInfo {
@@ -282,8 +283,17 @@ impl<'a> BuilderForBoxFragment<'a> {
match layer {
ImageLayer::None => {},
ImageLayer::Image(image) => match image {
Image::Gradient(_gradient) => {
// TODO
Image::Gradient(gradient) => {
let intrinsic = IntrinsicSizes {
width: None,
height: None,
ratio: None,
};
if let Some(layer) =
&background::layout_layer(self, builder, index, intrinsic)
{
gradient::build(&self.fragment.style, gradient, layer, builder)
}
},
Image::Url(image_url) => {
// FIXME: images won’t always have in intrinsic width or height
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.