/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 http://mozilla.org/MPL/2.0/. */
/* utility functions for drawing borders and backgrounds */
// Given a box with size aBoxSize and origin (0,0), and an angle aAngle, // and a starting point for the gradient line aStart, find the endpoint of // the gradient line --- the intersection of the gradient line with a line // perpendicular to aAngle that passes through the farthest corner in the // direction aAngle. static CSSPoint ComputeGradientLineEndFromAngle(const CSSPoint& aStart, double aAngle, const CSSSize& aBoxSize) { double dx = cos(-aAngle); double dy = sin(-aAngle);
CSSPoint farthestCorner(dx > 0 ? aBoxSize.width : 0,
dy > 0 ? aBoxSize.height : 0);
CSSPoint delta = farthestCorner - aStart; double u = delta.x * dy - delta.y * dx; return farthestCorner + CSSPoint(-u * dy, u * dx);
}
// Compute the start and end points of the gradient line for a linear gradient. static std::tuple<CSSPoint, CSSPoint> ComputeLinearGradientLine(
nsPresContext* aPresContext, const StyleGradient& aGradient, const CSSSize& aBoxSize) { using X = StyleHorizontalPositionKeyword; using Y = StyleVerticalPositionKeyword;
// Compute the start and end points of the gradient line for a radial gradient. // Also returns the horizontal and vertical radii defining the circle or // ellipse to use. static std::tuple<CSSPoint, CSSPoint, CSSCoord, CSSCoord>
ComputeRadialGradientLine(const StyleGradient& aGradient, const CSSSize& aBoxSize) { constauto& radial = aGradient.AsRadial(); const EndingShape& endingShape = radial.shape; const Position& position = radial.position;
CSSPoint start = ResolvePosition(position, aBoxSize);
// Compute gradient shape: the x and y radii of an ellipse.
CSSCoord radiusX, radiusY;
CSSCoord leftDistance = Abs(start.x);
CSSCoord rightDistance = Abs(aBoxSize.width - start.x);
CSSCoord topDistance = Abs(start.y);
CSSCoord bottomDistance = Abs(aBoxSize.height - start.y);
auto radii = ComputeRadialGradientRadii(endingShape, aBoxSize); if (radii.is<StyleShapeExtent>()) { switch (radii.as<StyleShapeExtent>()) { case StyleShapeExtent::ClosestSide:
radiusX = std::min(leftDistance, rightDistance);
radiusY = std::min(topDistance, bottomDistance); if (endingShape.IsCircle()) {
radiusX = radiusY = std::min(radiusX, radiusY);
} break; case StyleShapeExtent::ClosestCorner: { // Compute x and y distances to nearest corner
CSSCoord offsetX = std::min(leftDistance, rightDistance);
CSSCoord offsetY = std::min(topDistance, bottomDistance); if (endingShape.IsCircle()) {
radiusX = radiusY = NS_hypot(offsetX, offsetY);
} else { // maintain aspect ratio
radiusX = offsetX * M_SQRT2;
radiusY = offsetY * M_SQRT2;
} break;
} case StyleShapeExtent::FarthestSide:
radiusX = std::max(leftDistance, rightDistance);
radiusY = std::max(topDistance, bottomDistance); if (endingShape.IsCircle()) {
radiusX = radiusY = std::max(radiusX, radiusY);
} break; case StyleShapeExtent::FarthestCorner: { // Compute x and y distances to nearest corner
CSSCoord offsetX = std::max(leftDistance, rightDistance);
CSSCoord offsetY = std::max(topDistance, bottomDistance); if (endingShape.IsCircle()) {
radiusX = radiusY = NS_hypot(offsetX, offsetY);
} else { // maintain aspect ratio
radiusX = offsetX * M_SQRT2;
radiusY = offsetY * M_SQRT2;
} break;
} default:
MOZ_ASSERT_UNREACHABLE("Unknown shape extent keyword?");
radiusX = radiusY = 0;
}
} else { auto pair = radii.as<std::pair<CSSCoord, CSSCoord>>();
radiusX = pair.first;
radiusY = pair.second;
}
// The gradient line end point is where the gradient line intersects // the ellipse.
CSSPoint end = start + CSSPoint(radiusX, 0); return {start, end, radiusX, radiusY};
}
// Compute the center and the start angle of the conic gradient. static std::tuple<CSSPoint, float> ComputeConicGradientProperties( const StyleGradient& aGradient, const CSSSize& aBoxSize) { constauto& conic = aGradient.AsConic(); const Position& position = conic.position; float angle = static_cast<float>(conic.angle.ToRadians());
CSSPoint center = ResolvePosition(position, aBoxSize);
static StyleAbsoluteColor Interpolate(const StyleAbsoluteColor& aLeft, const StyleAbsoluteColor& aRight, float aFrac) { // NOTE: This has to match the interpolation method that WebRender uses which // right now is sRGB. In the future we should implement interpolation in more // gradient color-spaces. static constexpr auto kMethod = StyleColorInterpolationMethod{
StyleColorSpace::Srgb,
StyleHueInterpolationMethod::Shorter,
}; return Servo_InterpolateColor(kMethod, &aLeft, &aRight, aFrac);
}
static gfxFloat LinearGradientStopPositionForPoint( const gfxPoint& aGradientStart, const gfxPoint& aGradientEnd, const gfxPoint& aPoint) {
gfxPoint d = aGradientEnd - aGradientStart;
gfxPoint p = aPoint - aGradientStart; /** * Compute a parameter t such that a line perpendicular to the * d vector, passing through aGradientStart + d*t, also * passes through aPoint. * * t is given by * (p.x - d.x*t)*d.x + (p.y - d.y*t)*d.y = 0 * * Solving for t we get * numerator = d.x*p.x + d.y*p.y * denominator = d.x^2 + d.y^2 * t = numerator/denominator * * In nsCSSRendering::PaintGradient we know the length of d * is not zero.
*/ double numerator = d.x.value * p.x.value + d.y.value * p.y.value; double denominator = d.x.value * d.x.value + d.y.value * d.y.value; return numerator / denominator;
}
staticvoid ResolveMidpoints(nsTArray<ColorStop>& stops) { for (size_t x = 1; x < stops.Length() - 1;) { if (!stops[x].mIsMidpoint) {
x++; continue;
}
constauto& color1 = stops[x - 1].mColor; constauto& color2 = stops[x + 1].mColor; float offset1 = stops[x - 1].mPosition; float offset2 = stops[x + 1].mPosition; float offset = stops[x].mPosition; // check if everything coincides. If so, ignore the midpoint. if (offset - offset1 == offset2 - offset) {
stops.RemoveElementAt(x); continue;
}
// Check if we coincide with the left colorstop. if (offset1 == offset) { // Morph the midpoint to a regular stop with the color of the next // color stop.
stops[x].mColor = color2;
stops[x].mIsMidpoint = false; continue;
}
// Check if we coincide with the right colorstop. if (offset2 == offset) { // Morph the midpoint to a regular stop with the color of the previous // color stop.
stops[x].mColor = color1;
stops[x].mIsMidpoint = false; continue;
}
for (size_t y = 0; y < 7; y++) {
newStops[y + 2].mPosition = offset + (offset2 - offset) * y / 13;
}
} // calculate colors
for (auto& newStop : newStops) { // Calculate the intermediate color stops per the formula of the CSS // images spec. http://dev.w3.org/csswg/css-images/#color-stop-syntax 9 // points were chosen since it is the minimum number of stops that always // give the smoothest appearace regardless of midpoint position and // difference in luminance of the end points. constfloat relativeOffset =
(newStop.mPosition - offset1) / (offset2 - offset1); constfloat multiplier = powf(relativeOffset, logf(.5f) / logf(midpoint));
auto srgb1 = color1.ToColorSpace(StyleColorSpace::Srgb); auto srgb2 = color2.ToColorSpace(StyleColorSpace::Srgb);
stops.ReplaceElementsAt(x, 1, newStops, 9);
x += 9;
}
}
static StyleAbsoluteColor TransparentColor(const StyleAbsoluteColor& aColor) { auto color = aColor;
color.alpha = 0.0f; return color;
}
// Adjusts and adds color stops in such a way that drawing the gradient with // unpremultiplied interpolation looks nearly the same as if it were drawn with // premultiplied interpolation. staticconstfloat kAlphaIncrementPerGradientStep = 0.1f; staticvoid ResolvePremultipliedAlpha(nsTArray<ColorStop>& aStops) { for (size_t x = 1; x < aStops.Length(); x++) { const ColorStop leftStop = aStops[x - 1]; const ColorStop rightStop = aStops[x];
// if the left and right stop have the same alpha value, we don't need // to do anything. Hardstops should be instant, and also should never // require dealing with interpolation. if (leftStop.mColor.alpha == rightStop.mColor.alpha ||
leftStop.mPosition == rightStop.mPosition) { continue;
}
// Is the stop on the left 100% transparent? If so, have it adopt the color // of the right stop if (leftStop.mColor.alpha == 0) {
aStops[x - 1].mColor = TransparentColor(rightStop.mColor); continue;
}
// Is the stop on the right completely transparent? // If so, duplicate it and assign it the color on the left. if (rightStop.mColor.alpha == 0) {
ColorStop newStop = rightStop;
newStop.mColor = TransparentColor(leftStop.mColor);
aStops.InsertElementAt(x, newStop);
x++; continue;
}
// Now handle cases where one or both of the stops are partially // transparent. if (leftStop.mColor.alpha != 1.0f || rightStop.mColor.alpha != 1.0f) { // Calculate how many extra steps. We do a step per 10% transparency.
size_t stepCount =
NSToIntFloor(fabsf(leftStop.mColor.alpha - rightStop.mColor.alpha) /
kAlphaIncrementPerGradientStep); for (size_t y = 1; y < stepCount; y++) { float frac = static_cast<float>(y) / stepCount;
ColorStop newStop(
Interpolate(leftStop.mPosition, rightStop.mPosition, frac), false,
Interpolate(leftStop.mColor, rightStop.mColor, frac));
aStops.InsertElementAt(x, newStop);
x++;
}
}
}
}
// Clamp and extend the given ColorStop array in-place to fit exactly into the // range [0, 1]. staticvoid ClampColorStops(nsTArray<ColorStop>& aStops) {
MOZ_ASSERT(aStops.Length() > 0);
// If all stops are outside the range, then get rid of everything and replace // with a single colour. if (aStops.Length() < 2 || aStops[0].mPosition > 1 ||
aStops.LastElement().mPosition < 0) { constauto c = aStops[0].mPosition > 1 ? aStops[0].mColor
: aStops.LastElement().mColor;
aStops.Clear();
aStops.AppendElement(ColorStop(0, false, c)); return;
}
// Create the 0 and 1 points if they fall in the range of |aStops|, and // discard all stops outside the range [0, 1]. // XXX: If we have stops positioned at 0 or 1, we only keep the innermost of // those stops. This should be fine for the current user(s) of this function. for (size_t i = aStops.Length() - 1; i > 0; i--) { if (aStops[i - 1].mPosition < 1 && aStops[i].mPosition >= 1) { // Add a point to position 1.
aStops[i] =
InterpolateColorStop(aStops[i - 1], aStops[i], /* aPosition = */ 1, aStops[i - 1].mColor); // Remove all the elements whose position is greater than 1.
aStops.RemoveLastElements(aStops.Length() - (i + 1));
} if (aStops[i - 1].mPosition <= 0 && aStops[i].mPosition > 0) { // Add a point to position 0.
aStops[i - 1] =
InterpolateColorStop(aStops[i - 1], aStops[i], /* aPosition = */ 0, aStops[i].mColor); // Remove all of the preceding stops -- they are all negative.
aStops.RemoveElementsAt(0, i - 1); break;
}
}
// The end points won't exist yet if they don't fall in the original range of // |aStops|. Create them if needed. if (aStops[0].mPosition > 0) {
aStops.InsertElementAt(0, ColorStop(0, false, aStops[0].mColor));
} if (aStops.LastElement().mPosition < 1) {
aStops.AppendElement(ColorStop(1, false, aStops.LastElement().mColor));
}
}
template <typename T> static nsTArray<ColorStop> ComputeColorStopsForItems(
ComputedStyle* aComputedStyle,
Span<const StyleGenericGradientItem<StyleColor, T>> aItems,
CSSCoord aLineLength) {
MOZ_ASSERT(!aItems.IsEmpty(), "The parser should reject gradients with no stops");
nsTArray<ColorStop> stops(aItems.Length());
// If there is a run of stops before stop i that did not have specified // positions, then this is the index of the first stop in that run.
Maybe<size_t> firstUnsetPosition; for (size_t i = 0; i < aItems.Length(); ++i) { constauto& stop = aItems[i]; double position;
if (specifiedPosition) {
position = *specifiedPosition;
} elseif (i == 0) { // First stop defaults to position 0.0
position = 0.0;
} elseif (i == aItems.Length() - 1) { // Last stop defaults to position 1.0
position = 1.0;
} else { // Other stops with no specified position get their position assigned // later by interpolation, see below. // Remember where the run of stops with no specified position starts, // if it starts here. if (firstUnsetPosition.isNothing()) {
firstUnsetPosition.emplace(i);
}
MOZ_ASSERT(!stop.IsInterpolationHint(), "Interpolation hints always specify position"); auto color = GetSpecifiedColor(stop, *aComputedStyle);
stops.AppendElement(ColorStop(0, false, color)); continue;
}
if (i > 0) { // Prevent decreasing stop positions by advancing this position // to the previous stop position, if necessary double previousPosition = firstUnsetPosition
? stops[*firstUnsetPosition - 1].mPosition
: stops[i - 1].mPosition;
position = std::max(position, previousPosition);
} auto stopColor = GetSpecifiedColor(stop, *aComputedStyle);
stops.AppendElement(
ColorStop(position, stop.IsInterpolationHint(), stopColor)); if (firstUnsetPosition) { // Interpolate positions for all stops that didn't have a specified // position double p = stops[*firstUnsetPosition - 1].mPosition; double d = (stops[i].mPosition - p) / (i - *firstUnsetPosition + 1); for (size_t j = *firstUnsetPosition; j < i; ++j) {
p += d;
stops[j].mPosition = p;
}
firstUnsetPosition.reset();
}
}
// If a non-repeating linear gradient is axis-aligned and there are no gaps // between tiles, we can optimise away most of the work by converting to a // repeating linear gradient and filling the whole destination rect at once. bool forceRepeatToCoverTiles =
mGradient->IsLinear() &&
(mLineStart.x == mLineEnd.x) != (mLineStart.y == mLineEnd.y) &&
aRepeatSize.width == aDest.width && aRepeatSize.height == aDest.height &&
!(mGradient->Repeating()) && !aSrc.IsEmpty() && !cellContainsFill;
gfxMatrix matrix; if (forceRepeatToCoverTiles) { // Length of the source rectangle along the gradient axis. double rectLen; // The position of the start of the rectangle along the gradient. double offset;
// The gradient line is "backwards". Flip the line upside down to make // things easier, and then rotate the matrix to turn everything back the // right way up. if (mLineStart.x > mLineEnd.x || mLineStart.y > mLineEnd.y) {
std::swap(mLineStart, mLineEnd);
matrix.PreScale(-1, -1);
}
// Fit the gradient line exactly into the source rect. // aSrc is relative to aIntrinsincSize. // srcRectDev will be relative to srcSize, so in the same coordinate space // as lineStart / lineEnd.
gfxRect srcRectDev = nsLayoutUtils::RectToGfxRect(
CSSPixel::ToAppUnits(aSrc), appUnitsPerDevPixel); if (mLineStart.x != mLineEnd.x) {
rectLen = srcRectDev.width;
offset = (srcRectDev.x - mLineStart.x) / lineLength;
mLineStart.x = srcRectDev.x;
mLineEnd.x = srcRectDev.XMost();
} else {
rectLen = srcRectDev.height;
offset = (srcRectDev.y - mLineStart.y) / lineLength;
mLineStart.y = srcRectDev.y;
mLineEnd.y = srcRectDev.YMost();
}
// Adjust gradient stop positions for the new gradient line. double scale = lineLength / rectLen; for (size_t i = 0; i < mStops.Length(); i++) {
mStops[i].mPosition = (mStops[i].mPosition - offset) * fabs(scale);
}
// Clamp or extrapolate gradient stops to exactly [0, 1].
ClampColorStops(mStops);
lineLength = rectLen;
}
// Eliminate negative-position stops if the gradient is radial. double firstStop = mStops[0].mPosition; if (mGradient->IsRadial() && firstStop < 0.0) { if (mGradient->AsRadial().flags & StyleGradientFlags::REPEATING) { // Choose an instance of the repeated pattern that gives us all positive // stop-offsets. double lastStop = mStops[mStops.Length() - 1].mPosition; double stopDelta = lastStop - firstStop; // If all the stops are in approximately the same place then logic below // will kick in that makes us draw just the last stop color, so don't // try to do anything in that case. We certainly need to avoid // dividing by zero. if (stopDelta >= 1e-6) { double instanceCount = ceil(-firstStop / stopDelta); // Advance stops by instanceCount multiples of the period of the // repeating gradient. double offset = instanceCount * stopDelta; for (uint32_t i = 0; i < mStops.Length(); i++) {
mStops[i].mPosition += offset;
}
}
} else { // Move negative-position stops to position 0.0. We may also need // to set the color of the stop to the color the gradient should have // at the center of the ellipse. for (uint32_t i = 0; i < mStops.Length(); i++) { double pos = mStops[i].mPosition; if (pos < 0.0) {
mStops[i].mPosition = 0.0; // If this is the last stop, we don't need to adjust the color, // it will fill the entire area. if (i < mStops.Length() - 1) { double nextPos = mStops[i + 1].mPosition; // If nextPos is approximately equal to pos, then we don't // need to adjust the color of this stop because it's // not going to be displayed. // If nextPos is negative, we don't need to adjust the color of // this stop since it's not going to be displayed because // nextPos will also be moved to 0.0. if (nextPos >= 0.0 && nextPos - pos >= 1e-6) { // Compute how far the new position 0.0 is along the interval // between pos and nextPos. // XXX Color interpolation (in cairo, too) should use the // CSS 'color-interpolation' property! float frac = float((0.0 - pos) / (nextPos - pos));
mStops[i].mColor =
Interpolate(mStops[i].mColor, mStops[i + 1].mColor, frac);
}
}
}
}
}
firstStop = mStops[0].mPosition;
MOZ_ASSERT(firstStop >= 0.0, "Failed to fix stop offsets");
}
if (mGradient->IsRadial() &&
!(mGradient->AsRadial().flags & StyleGradientFlags::REPEATING)) { // Direct2D can only handle a particular class of radial gradients because // of the way the it specifies gradients. Setting firstStop to 0, when we // can, will help us stay on the fast path. Currently we don't do this // for repeating gradients but we could by adjusting the stop collection // to start at 0
firstStop = 0;
}
double lastStop = mStops[mStops.Length() - 1].mPosition; // Cairo gradients must have stop positions in the range [0, 1]. So, // stop positions will be normalized below by subtracting firstStop and then // multiplying by stopScale. double stopScale; double stopOrigin = firstStop; double stopEnd = lastStop; double stopDelta = lastStop - firstStop; bool zeroRadius =
mGradient->IsRadial() && (mRadiusX < 1e-6 || mRadiusY < 1e-6); if (stopDelta < 1e-6 || (!mGradient->IsConic() && lineLength < 1e-6) ||
zeroRadius) { // Stops are all at the same place. Map all stops to 0.0. // For repeating radial gradients, or for any radial gradients with // a zero radius, we need to fill with the last stop color, so just set // both radii to 0. if (mGradient->Repeating() || zeroRadius) {
mRadiusX = mRadiusY = 0.0;
}
stopDelta = 0.0;
}
// Don't normalize non-repeating or degenerate gradients below 0..1 // This keeps the gradient line as large as the box and doesn't // lets us avoiding having to get padding correct for stops // at 0 and 1 if (!mGradient->Repeating() || stopDelta == 0.0) {
stopOrigin = std::min(stopOrigin, 0.0);
stopEnd = std::max(stopEnd, 1.0);
}
stopScale = 1.0 / (stopEnd - stopOrigin);
// Create the gradient pattern.
RefPtr<gfxPattern> gradientPattern;
gfxPoint gradientStart;
gfxPoint gradientEnd; if (mGradient->IsLinear()) { // Compute the actual gradient line ends we need to pass to cairo after // stops have been normalized.
gradientStart = mLineStart + (mLineEnd - mLineStart) * stopOrigin;
gradientEnd = mLineStart + (mLineEnd - mLineStart) * stopEnd;
if (stopDelta == 0.0) { // Stops are all at the same place. For repeating gradients, this will // just paint the last stop color. We don't need to do anything. // For non-repeating gradients, this should render as two colors, one // on each "side" of the gradient line segment, which is a point. All // our stops will be at 0.0; we just need to set the direction vector // correctly.
gradientEnd = gradientStart + (mLineEnd - mLineStart);
}
gradientPattern = new gfxPattern(gradientStart.x, gradientStart.y,
gradientEnd.x, gradientEnd.y);
} elseif (mGradient->IsRadial()) {
NS_ASSERTION(firstStop >= 0.0, "Negative stops not allowed for radial gradients");
// To form an ellipse, we'll stretch a circle vertically, if necessary. // So our radii are based on radiusX. double innerRadius = mRadiusX * stopOrigin; double outerRadius = mRadiusX * stopEnd; if (stopDelta == 0.0) { // Stops are all at the same place. See above (except we now have // the inside vs. outside of an ellipse).
outerRadius = innerRadius + 1;
}
gradientPattern = new gfxPattern(mLineStart.x, mLineStart.y, innerRadius,
mLineStart.x, mLineStart.y, outerRadius); if (mRadiusX != mRadiusY) { // Stretch the circles into ellipses vertically by setting a transform // in the pattern. // Recall that this is the transform from user space to pattern space. // So to stretch the ellipse by factor of P vertically, we scale // user coordinates by 1/P.
matrix.PreTranslate(mLineStart);
matrix.PreScale(1.0, mRadiusX / mRadiusY);
matrix.PreTranslate(-mLineStart);
}
} else {
gradientPattern = new gfxPattern(mCenter.x, mCenter.y, mAngle, stopOrigin, stopEnd);
} // Use a pattern transform to take account of source and dest rects
matrix.PreTranslate(gfxPoint(mPresContext->CSSPixelsToDevPixels(aSrc.x),
mPresContext->CSSPixelsToDevPixels(aSrc.y)));
matrix.PreScale(
gfxFloat(nsPresContext::CSSPixelsToAppUnits(aSrc.width)) / aDest.width,
gfxFloat(nsPresContext::CSSPixelsToAppUnits(aSrc.height)) / aDest.height);
gradientPattern->SetMatrix(matrix);
if (stopDelta == 0.0) { // Non-repeating gradient with all stops in same place -> just add // first stop and last stop, both at position 0. // Repeating gradient with all stops in the same place, or radial // gradient with radius of 0 -> just paint the last stop color. // We use firstStop offset to keep |stops| with same units (will later // normalize to 0). auto firstColor(mStops[0].mColor); auto lastColor(mStops.LastElement().mColor);
mStops.Clear();
// Now set normalized color stops in pattern. // Offscreen gradient surface cache (not a tile): // On some backends (e.g. D2D), the GradientStops object holds an offscreen // surface which is a lookup table used to evaluate the gradient. This surface // can use much memory (ram and/or GPU ram) and can be expensive to create. So // we cache it. The cache key correlates 1:1 with the arguments for // CreateGradientStops (also the implied backend type) Note that GradientStop // is a simple struct with a stop value (while GradientStops has the surface).
nsTArray<gfx::GradientStop> rawStops(mStops.Length());
StyleColorInterpolationMethod styleColorInterpolationMethod =
mGradient->ColorInterpolationMethod(); if (styleColorInterpolationMethod.space != StyleColorSpace::Srgb ||
gfxPlatform::GetCMSMode() == CMSMode::All) { class MOZ_STACK_CLASS GradientStopInterpolator final
: public ColorStopInterpolator<GradientStopInterpolator> { public:
GradientStopInterpolator( const nsTArray<ColorStop>& aStops, const StyleColorInterpolationMethod& aStyleColorInterpolationMethod, bool aExtend, nsTArray<gfx::GradientStop>& aResult)
: ColorStopInterpolator(aStops, aStyleColorInterpolationMethod,
aExtend),
mStops(aResult) {} void CreateStop(float aPosition, gfx::DeviceColor aColor) {
mStops.AppendElement(gfx::GradientStop{aPosition, aColor});
}
// Paint gradient tiles. This isn't terribly efficient, but doing it this // way is simple and sure to get pixel-snapping right. We could speed things // up by drawing tiles into temporary surfaces and copying those to the // destination, but after pixel-snapping tiles may not all be the same size.
nsRect dirty; if (!dirty.IntersectRect(aDirtyRect, aFillArea)) { return;
}
// x and y are the top-left corner of the tile to draw for (nscoord y = yStart; y < yEnd; y += aRepeatSize.height) { for (nscoord x = xStart; x < xEnd; x += aRepeatSize.width) { // The coordinates of the tile
gfxRect tileRect = nsLayoutUtils::RectToGfxRect(
nsRect(x, y, aDest.width, aDest.height), appUnitsPerDevPixel); // The actual area to fill with this tile is the intersection of this // tile with the overall area we're supposed to be filling
gfxRect fillRect =
forceRepeatToCoverTiles ? areaToFill : tileRect.Intersect(areaToFill); // Try snapping the fill rect. Snap its top-left and bottom-right // independently to preserve the orientation.
gfxPoint snappedFillRectTopLeft = fillRect.TopLeft();
gfxPoint snappedFillRectTopRight = fillRect.TopRight();
gfxPoint snappedFillRectBottomRight = fillRect.BottomRight(); // Snap three points instead of just two to ensure we choose the // correct orientation if there's a reflection. if (isCTMPreservingAxisAlignedRectangles &&
aContext.UserToDevicePixelSnapped(snappedFillRectTopLeft, true) &&
aContext.UserToDevicePixelSnapped(snappedFillRectBottomRight, true) &&
aContext.UserToDevicePixelSnapped(snappedFillRectTopRight, true)) { if (snappedFillRectTopLeft.x == snappedFillRectBottomRight.x ||
snappedFillRectTopLeft.y == snappedFillRectBottomRight.y) { // Nothing to draw; avoid scaling by zero and other weirdness that // could put the context in an error state. continue;
} // Set the context's transform to the transform that maps fillRect to // snappedFillRect. The part of the gradient that was going to // exactly fill fillRect will fill snappedFillRect instead.
gfxMatrix transform = gfxUtils::TransformRectToRect(
fillRect, snappedFillRectTopLeft, snappedFillRectTopRight,
snappedFillRectBottomRight);
aContext.SetMatrixDouble(transform);
}
aContext.NewPath();
aContext.Rectangle(fillRect);
bool nsCSSGradientRenderer::TryPaintTilesWithExtendMode(
gfxContext& aContext, gfxPattern* aGradientPattern, nscoord aXStart,
nscoord aYStart, const gfxRect& aDirtyAreaToFill, const nsRect& aDest, const nsSize& aRepeatSize, bool aForceRepeatToCoverTiles) { // If we have forced a non-repeating gradient to repeat to cover tiles, // then it will be faster to just paint it once using that optimization if (aForceRepeatToCoverTiles) { returnfalse;
}
// We can only use this fast path if we don't have to worry about pixel // snapping, and there is no spacing between tiles. We could handle spacing // by increasing the size of tileSurface and leaving it transparent, but I'm // not sure it's worth it bool canUseExtendModeForTiling = (aXStart % appUnitsPerDevPixel == 0) &&
(aYStart % appUnitsPerDevPixel == 0) &&
(aDest.width % appUnitsPerDevPixel == 0) &&
(aDest.height % appUnitsPerDevPixel == 0) &&
(aRepeatSize.width == aDest.width) &&
(aRepeatSize.height == aDest.height);
// Check whether this is a reasonable surface size and doesn't overflow // before doing calculations with the tile size if (!Factory::ReasonableSurfaceSize(tileSize)) { returnfalse;
}
// We only want to do this when there are enough tiles to justify the // overhead of painting to an offscreen surface. The heuristic here // is when we will be painting at least 16 tiles or more, this is kind // of arbitrary bool shouldUseExtendModeForTiling =
aDirtyAreaToFill.Area() > (tileSize.width * tileSize.height) * 16.0;
if (!shouldUseExtendModeForTiling) { returnfalse;
}
// Draw the gradient pattern into a surface for our single tile
RefPtr<gfx::SourceSurface> tileSurface;
{
RefPtr<gfx::DrawTarget> tileTarget =
aContext.GetDrawTarget()->CreateSimilarDrawTarget(
tileSize, gfx::SurfaceFormat::B8G8R8A8); if (!tileTarget || !tileTarget->IsValid()) { returnfalse;
}
// Draw the gradient using tileSurface as a repeating pattern masked by // the dirtyRect
Matrix tileTransform = Matrix::Translation(
NSAppUnitsToFloatPixels(aXStart, appUnitsPerDevPixel),
NSAppUnitsToFloatPixels(aYStart, appUnitsPerDevPixel));
void CreateStops() {
mResult.SetLengthAndRetainStorage(0); // We always emit at least two stops (start and end) for each input stop, // which avoids ambiguity with incomplete oklch/lch/hsv/hsb color stops for // the last stop pair, where the last color stop can't be interpreted on its // own because it actually depends on the previous stop.
mResult.SetLength(mStops.Length() * 2 + kFullRangeExtraStops);
mOutputStop = 0;
ColorStopInterpolator::CreateStops();
mResult.SetLength(mOutputStop);
}
// If the interpolation space is not sRGB, or if color management is active, // we need to add additional stops so that the sRGB interpolation in WebRender // still closely approximates the correct curves. We prefer avoiding this if // the gradient is simple because WebRender has fast rendering of linear // gradients with 2 stops (which represent >99% of all gradients on the web). // // WebRender doesn't have easy access to StyleAbsoluteColor and CMS display // color correction, so we just expand the gradient stop table significantly // so that gamma and hue interpolation errors become imperceptible. // // This always turns into 128 pairs of stops inside WebRender as an // implementation detail, so the number of stops we generate here should have // very little impact on performance as the texture upload is always the same, // except for the special linear gradient 2-stop case, and it is gpucache so // if it does not change it is not re-uploaded. // // Color management bugs that this addresses: // * https://bugzilla.mozilla.org/show_bug.cgi?id=939387 // * https://bugzilla.mozilla.org/show_bug.cgi?id=1248178
StyleColorInterpolationMethod styleColorInterpolationMethod =
mGradient->ColorInterpolationMethod(); // For colorspaces supported by WebRender (Srgb, Hsl, Hwb) we technically do // not need to add extra stops, but the only one of those colorspaces that // appears frequently is Srgb, and Srgb still needs extra stops if CMS is // enabled. Hsl/Hwb need extra stops if StyleHueInterpolationMethod is not // Shorter, or if CMS is enabled. // // It's probably best to keep this logic as simple as possible, see // https://bugzilla.mozilla.org/show_bug.cgi?id=1885716 for an example of // what can happen if we try to be clever here. if (styleColorInterpolationMethod.space != StyleColorSpace::Srgb ||
gfxPlatform::GetCMSMode() == CMSMode::All) { // For the specific case of longer hue interpolation on a CSS non-repeating // gradient, we have to pretend there is another stop at position=1.0 that // duplicates the last stop, this is probably only used for things like a // color wheel. No such problem for SVG as it doesn't have that complexity. bool extend = aMode == wr::ExtendMode::Clamp &&
styleColorInterpolationMethod.hue ==
StyleHueInterpolationMethod::Longer;
WrColorStopInterpolator interpolator(mStops, styleColorInterpolationMethod,
aOpacity, aStops, extend);
interpolator.CreateStops();
} else {
aStops.SetLength(mStops.Length()); for (uint32_t i = 0; i < mStops.Length(); i++) {
aStops[i].color = wr::ToColorF(ToDeviceColor(mStops[i].mColor));
aStops[i].color.a *= aOpacity;
aStops[i].offset = (float)mStops[i].mPosition;
}
}
// Calculate the bounds of the gradient display item, which starts at the // first tile and extends to the end of clip bounds
LayoutDevicePoint tileToClip =
clipBounds.BottomRight() - firstTileBounds.TopLeft();
LayoutDeviceRect gradientBounds = LayoutDeviceRect(
firstTileBounds.TopLeft(), LayoutDeviceSize(tileToClip.x, tileToClip.y));
// Calculate the tile spacing, which is the repeat size minus the tile size
LayoutDeviceSize tileSpacing = tileRepeat - firstTileBounds.Size();
// srcTransform is used for scaling the gradient to match aSrc
LayoutDeviceRect srcTransform = LayoutDeviceRect(
nsPresContext::CSSPixelsToAppUnits(aSrc.x),
nsPresContext::CSSPixelsToAppUnits(aSrc.y),
aDest.width / ((float)nsPresContext::CSSPixelsToAppUnits(aSrc.width)),
aDest.height / ((float)nsPresContext::CSSPixelsToAppUnits(aSrc.height)));