Render gradients

This commit is contained in:
Simon Sapin 2020-01-23 17:12:57 +01:00
parent f39c3ff38b
commit 632e731760
2 changed files with 348 additions and 2 deletions

View file

@ -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.
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 lines 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(
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?
// Its debatble whether thats 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
}

View file

@ -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 wont always have in intrinsic width or height