/* -*- 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 */ #include "nsCSSRenderingGradients.h" #include #include "gfx2DGlue.h" #include "mozilla/ArrayUtils.h" #include "mozilla/ComputedStyle.h" #include "mozilla/DebugOnly.h" #include "mozilla/gfx/2D.h" #include "mozilla/gfx/Helpers.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/ProfilerLabels.h" #include "nsLayoutUtils.h" #include "nsStyleConsts.h" #include "nsPresContext.h" #include "nsPoint.h" #include "nsRect.h" #include "nsCSSColorUtils.h" #include "gfxContext.h" #include "nsStyleStructInlines.h" #include "nsCSSProps.h" #include "gfxUtils.h" #include "gfxGradientCache.h" #include "mozilla/layers/StackingContextHelper.h" #include "mozilla/layers/WebRenderLayerManager.h" #include "mozilla/webrender/WebRenderTypes.h" #include "mozilla/webrender/WebRenderAPI.h" #include "Units.h" using namespace mozilla; using namespace mozilla::gfx; static CSSPoint ResolvePosition(const Position& aPos, const CSSSize& aSize) { CSSCoord h = aPos.horizontal.ResolveToCSSPixels(aSize.width); CSSCoord v = aPos.vertical.ResolveToCSSPixels(aSize.height); return CSSPoint(h, v); } // 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 ComputeLinearGradientLine( nsPresContext* aPresContext, const StyleGradient& aGradient, const CSSSize& aBoxSize) { using X = StyleHorizontalPositionKeyword; using Y = StyleVerticalPositionKeyword; const StyleLineDirection& direction = aGradient.AsLinear().direction; const bool isModern = aGradient.AsLinear().compat_mode == StyleGradientCompatMode::Modern; CSSPoint center(aBoxSize.width / 2, aBoxSize.height / 2); switch (direction.tag) { case StyleLineDirection::Tag::Angle: { double angle = direction.AsAngle().ToRadians(); if (isModern) { angle = M_PI_2 - angle; } CSSPoint end = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); CSSPoint start = CSSPoint(aBoxSize.width, aBoxSize.height) - end; return {start, end}; } case StyleLineDirection::Tag::Vertical: { CSSPoint start(center.x, 0); CSSPoint end(center.x, aBoxSize.height); if (isModern == (direction.AsVertical() == Y::Top)) { std::swap(start.y, end.y); } return {start, end}; } case StyleLineDirection::Tag::Horizontal: { CSSPoint start(0, center.y); CSSPoint end(aBoxSize.width, center.y); if (isModern == (direction.AsHorizontal() == X::Left)) { std::swap(start.x, end.x); } return {start, end}; } case StyleLineDirection::Tag::Corner: { const auto& corner = direction.AsCorner(); const X& h = corner._0; const Y& v = corner._1; if (isModern) { float xSign = h == X::Right ? 1.0 : -1.0; float ySign = v == Y::Top ? 1.0 : -1.0; double angle = atan2(ySign * aBoxSize.width, xSign * aBoxSize.height); CSSPoint end = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); CSSPoint start = CSSPoint(aBoxSize.width, aBoxSize.height) - end; return {start, end}; } CSSCoord startX = h == X::Left ? 0.0 : aBoxSize.width; CSSCoord startY = v == Y::Top ? 0.0 : aBoxSize.height; CSSPoint start(startX, startY); CSSPoint end = CSSPoint(aBoxSize.width, aBoxSize.height) - start; return {start, end}; } default: break; } MOZ_ASSERT_UNREACHABLE("Unknown line direction"); return {CSSPoint(), CSSPoint()}; } using EndingShape = StyleGenericEndingShape; using RadialGradientRadii = Variant>; static RadialGradientRadii ComputeRadialGradientRadii(const EndingShape& aShape, const CSSSize& aSize) { if (aShape.IsCircle()) { auto& circle = aShape.AsCircle(); if (circle.IsExtent()) { return RadialGradientRadii(circle.AsExtent()); } CSSCoord radius = circle.AsRadius().ToCSSPixels(); return RadialGradientRadii(std::make_pair(radius, radius)); } auto& ellipse = aShape.AsEllipse(); if (ellipse.IsExtent()) { return RadialGradientRadii(ellipse.AsExtent()); } auto& radii = ellipse.AsRadii(); return RadialGradientRadii( std::make_pair(radii._0.ResolveToCSSPixels(aSize.width), radii._1.ResolveToCSSPixels(aSize.height))); } // 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 ComputeRadialGradientLine(const StyleGradient& aGradient, const CSSSize& aBoxSize) { const auto& 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()) { switch (radii.as()) { 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>(); 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 ComputeConicGradientProperties( const StyleGradient& aGradient, const CSSSize& aBoxSize) { const auto& conic = aGradient.AsConic(); const Position& position = conic.position; float angle = static_cast(conic.angle.ToRadians()); CSSPoint center = ResolvePosition(position, aBoxSize); return {center, angle}; } static float Interpolate(float aF1, float aF2, float aFrac) { return aF1 + aFrac * (aF2 - aF1); } static StyleAnimatedRGBA Interpolate(const StyleAnimatedRGBA& aLeft, const StyleAnimatedRGBA& 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, &aRight, &aLeft, aFrac); } static nscoord FindTileStart(nscoord aDirtyCoord, nscoord aTilePos, nscoord aTileDim) { NS_ASSERTION(aTileDim > 0, "Non-positive tile dimension"); double multiples = floor(double(aDirtyCoord - aTilePos) / aTileDim); return NSToCoordRound(multiples * aTileDim + aTilePos); } 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; } static bool RectIsBeyondLinearGradientEdge(const gfxRect& aRect, const gfxMatrix& aPatternMatrix, const nsTArray& aStops, const gfxPoint& aGradientStart, const gfxPoint& aGradientEnd, StyleAnimatedRGBA* aOutEdgeColor) { gfxFloat topLeft = LinearGradientStopPositionForPoint( aGradientStart, aGradientEnd, aPatternMatrix.TransformPoint(aRect.TopLeft())); gfxFloat topRight = LinearGradientStopPositionForPoint( aGradientStart, aGradientEnd, aPatternMatrix.TransformPoint(aRect.TopRight())); gfxFloat bottomLeft = LinearGradientStopPositionForPoint( aGradientStart, aGradientEnd, aPatternMatrix.TransformPoint(aRect.BottomLeft())); gfxFloat bottomRight = LinearGradientStopPositionForPoint( aGradientStart, aGradientEnd, aPatternMatrix.TransformPoint(aRect.BottomRight())); const ColorStop& firstStop = aStops[0]; if (topLeft < firstStop.mPosition && topRight < firstStop.mPosition && bottomLeft < firstStop.mPosition && bottomRight < firstStop.mPosition) { *aOutEdgeColor = firstStop.mColor; return true; } const ColorStop& lastStop = aStops.LastElement(); if (topLeft >= lastStop.mPosition && topRight >= lastStop.mPosition && bottomLeft >= lastStop.mPosition && bottomRight >= lastStop.mPosition) { *aOutEdgeColor = lastStop.mColor; return true; } return false; } static void ResolveMidpoints(nsTArray& stops) { for (size_t x = 1; x < stops.Length() - 1;) { if (!stops[x].mIsMidpoint) { x++; continue; } const auto& color1 = stops[x - 1].mColor; const auto& 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; } float midpoint = (offset - offset1) / (offset2 - offset1); ColorStop newStops[9]; if (midpoint > .5f) { for (size_t y = 0; y < 7; y++) { newStops[y].mPosition = offset1 + (offset - offset1) * (7 + y) / 13; } newStops[7].mPosition = offset + (offset2 - offset) / 3; newStops[8].mPosition = offset + (offset2 - offset) * 2 / 3; } else { newStops[0].mPosition = offset1 + (offset - offset1) / 3; newStops[1].mPosition = offset1 + (offset - offset1) * 2 / 3; 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. const float relativeOffset = (newStop.mPosition - offset1) / (offset2 - offset1); const float multiplier = powf(relativeOffset, logf(.5f) / logf(midpoint)); const float red = color1.red + multiplier * (color2.red - color1.red); const float green = color1.green + multiplier * (color2.green - color1.green); const float blue = color1.blue + multiplier * (color2.blue - color1.blue); const float alpha = color1.alpha + multiplier * (color2.alpha - color1.alpha); newStop.mColor = {red, green, blue, alpha}; } stops.ReplaceElementsAt(x, 1, newStops, 9); x += 9; } } static StyleAnimatedRGBA TransparentColor(const StyleAnimatedRGBA& 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. static const float kAlphaIncrementPerGradientStep = 0.1f; static void ResolvePremultipliedAlpha(nsTArray& 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(y) / stepCount; ColorStop newStop( Interpolate(leftStop.mPosition, rightStop.mPosition, frac), false, Interpolate(leftStop.mColor, rightStop.mColor, frac)); aStops.InsertElementAt(x, newStop); x++; } } } } static ColorStop InterpolateColorStop(const ColorStop& aFirst, const ColorStop& aSecond, double aPosition, const StyleAnimatedRGBA& aDefault) { MOZ_ASSERT(aFirst.mPosition <= aPosition); MOZ_ASSERT(aPosition <= aSecond.mPosition); double delta = aSecond.mPosition - aFirst.mPosition; if (delta < 1e-6) { return ColorStop(aPosition, false, aDefault); } return ColorStop(aPosition, false, Interpolate(aFirst.mColor, aSecond.mColor, (aPosition - aFirst.mPosition) / delta)); } // Clamp and extend the given ColorStop array in-place to fit exactly into the // range [0, 1]. static void ClampColorStops(nsTArray& 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) { const auto 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; } } MOZ_ASSERT(aStops[0].mPosition >= -1e6); MOZ_ASSERT(aStops.LastElement().mPosition - 1 <= 1e6); // 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)); } } namespace mozilla { template static StyleAnimatedRGBA GetSpecifiedColor( const StyleGenericGradientItem& aItem, const ComputedStyle& aStyle) { if (aItem.IsInterpolationHint()) { return {0.0f, 0.0f, 0.0f, 0.0f}; } const StyleColor& c = aItem.IsSimpleColorStop() ? aItem.AsSimpleColorStop() : aItem.AsComplexColorStop().color; nscolor color = c.CalcColor(aStyle); return {NS_GET_R(color) / 255.0f, NS_GET_G(color) / 255.0f, NS_GET_B(color) / 255.0f, NS_GET_A(color) / 255.0f}; } static Maybe GetSpecifiedGradientPosition( const StyleGenericGradientItem& aItem, CSSCoord aLineLength) { if (aItem.IsSimpleColorStop()) { return Nothing(); } const LengthPercentage& pos = aItem.IsComplexColorStop() ? aItem.AsComplexColorStop().position : aItem.AsInterpolationHint(); if (pos.ConvertsToPercentage()) { return Some(pos.ToPercentage()); } if (aLineLength < 1e-6) { return Some(0.0); } return Some(pos.ResolveToCSSPixels(aLineLength) / aLineLength); } // aLineLength argument is unused for conic-gradients. static Maybe GetSpecifiedGradientPosition( const StyleGenericGradientItem& aItem, CSSCoord aLineLength) { if (aItem.IsSimpleColorStop()) { return Nothing(); } const StyleAngleOrPercentage& pos = aItem.IsComplexColorStop() ? aItem.AsComplexColorStop().position : aItem.AsInterpolationHint(); if (pos.IsPercentage()) { return Some(pos.AsPercentage()._0); } return Some(pos.AsAngle().ToRadians() / (2 * M_PI)); } template static nsTArray ComputeColorStopsForItems( ComputedStyle* aComputedStyle, Span> aItems, CSSCoord aLineLength) { MOZ_ASSERT(aItems.Length() >= 2, "The parser should reject gradients with less than two stops"); nsTArray 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 firstUnsetPosition; for (size_t i = 0; i < aItems.Length(); ++i) { const auto& stop = aItems[i]; double position; Maybe specifiedPosition = GetSpecifiedGradientPosition(stop, aLineLength); if (specifiedPosition) { position = *specifiedPosition; } else if (i == 0) { // First stop defaults to position 0.0 position = 0.0; } else if (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(); } } return stops; } static nsTArray ComputeColorStops(ComputedStyle* aComputedStyle, const StyleGradient& aGradient, CSSCoord aLineLength) { if (aGradient.IsLinear()) { return ComputeColorStopsForItems( aComputedStyle, aGradient.AsLinear().items.AsSpan(), aLineLength); } if (aGradient.IsRadial()) { return ComputeColorStopsForItems( aComputedStyle, aGradient.AsRadial().items.AsSpan(), aLineLength); } return ComputeColorStopsForItems( aComputedStyle, aGradient.AsConic().items.AsSpan(), aLineLength); } nsCSSGradientRenderer nsCSSGradientRenderer::Create( nsPresContext* aPresContext, ComputedStyle* aComputedStyle, const StyleGradient& aGradient, const nsSize& aIntrinsicSize) { auto srcSize = CSSSize::FromAppUnits(aIntrinsicSize); // Compute "gradient line" start and end relative to the intrinsic size of // the gradient. CSSPoint lineStart, lineEnd, center; // center is for conic gradients only CSSCoord radiusX = 0, radiusY = 0; // for radial gradients only float angle = 0.0; // for conic gradients only if (aGradient.IsLinear()) { std::tie(lineStart, lineEnd) = ComputeLinearGradientLine(aPresContext, aGradient, srcSize); } else if (aGradient.IsRadial()) { std::tie(lineStart, lineEnd, radiusX, radiusY) = ComputeRadialGradientLine(aGradient, srcSize); } else { MOZ_ASSERT(aGradient.IsConic()); std::tie(center, angle) = ComputeConicGradientProperties(aGradient, srcSize); } // Avoid sending Infs or Nans to downwind draw targets. if (!lineStart.IsFinite() || !lineEnd.IsFinite()) { lineStart = lineEnd = CSSPoint(0, 0); } if (!center.IsFinite()) { center = CSSPoint(0, 0); } CSSCoord lineLength = NS_hypot(lineEnd.x - lineStart.x, lineEnd.y - lineStart.y); // Build color stop array and compute stop positions nsTArray stops = ComputeColorStops(aComputedStyle, aGradient, lineLength); ResolveMidpoints(stops); nsCSSGradientRenderer renderer; renderer.mPresContext = aPresContext; renderer.mGradient = &aGradient; renderer.mStops = std::move(stops); renderer.mLineStart = { aPresContext->CSSPixelsToDevPixels(lineStart.x), aPresContext->CSSPixelsToDevPixels(lineStart.y), }; renderer.mLineEnd = { aPresContext->CSSPixelsToDevPixels(lineEnd.x), aPresContext->CSSPixelsToDevPixels(lineEnd.y), }; renderer.mRadiusX = aPresContext->CSSPixelsToDevPixels(radiusX); renderer.mRadiusY = aPresContext->CSSPixelsToDevPixels(radiusY); renderer.mCenter = { aPresContext->CSSPixelsToDevPixels(center.x), aPresContext->CSSPixelsToDevPixels(center.y), }; renderer.mAngle = angle; return renderer; } void nsCSSGradientRenderer::Paint(gfxContext& aContext, const nsRect& aDest, const nsRect& aFillArea, const nsSize& aRepeatSize, const CSSIntRect& aSrc, const nsRect& aDirtyRect, float aOpacity) { AUTO_PROFILER_LABEL("nsCSSGradientRenderer::Paint", GRAPHICS); if (aDest.IsEmpty() || aFillArea.IsEmpty()) { return; } nscoord appUnitsPerDevPixel = mPresContext->AppUnitsPerDevPixel(); gfxFloat lineLength = NS_hypot(mLineEnd.x - mLineStart.x, mLineEnd.y - mLineStart.y); bool cellContainsFill = aDest.Contains(aFillArea); // 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->AsLinear().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().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().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 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); } else if (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(); if (!mGradient->Repeating() && !zeroRadius) { mStops.AppendElement(ColorStop(firstStop, false, firstColor)); } mStops.AppendElement(ColorStop(firstStop, false, lastColor)); } ResolvePremultipliedAlpha(mStops); bool isRepeat = mGradient->Repeating() || forceRepeatToCoverTiles; // 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 rawStops(mStops.Length()); rawStops.SetLength(mStops.Length()); for (uint32_t i = 0; i < mStops.Length(); i++) { rawStops[i].color = ToDeviceColor(mStops[i].mColor); rawStops[i].color.a *= aOpacity; rawStops[i].offset = stopScale * (mStops[i].mPosition - stopOrigin); } RefPtr gs = gfxGradientCache::GetOrCreateGradientStops( aContext.GetDrawTarget(), rawStops, isRepeat ? gfx::ExtendMode::REPEAT : gfx::ExtendMode::CLAMP); gradientPattern->SetColorStops(gs); // 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; gfxRect areaToFill = nsLayoutUtils::RectToGfxRect(aFillArea, appUnitsPerDevPixel); gfxRect dirtyAreaToFill = nsLayoutUtils::RectToGfxRect(dirty, appUnitsPerDevPixel); dirtyAreaToFill.RoundOut(); Matrix ctm = aContext.CurrentMatrix(); bool isCTMPreservingAxisAlignedRectangles = ctm.PreservesAxisAlignedRectangles(); // xStart/yStart are the top-left corner of the top-left tile. nscoord xStart = FindTileStart(dirty.x, aDest.x, aRepeatSize.width); nscoord yStart = FindTileStart(dirty.y, aDest.y, aRepeatSize.height); nscoord xEnd = forceRepeatToCoverTiles ? xStart + aDest.width : dirty.XMost(); nscoord yEnd = forceRepeatToCoverTiles ? yStart + aDest.height : dirty.YMost(); if (TryPaintTilesWithExtendMode(aContext, gradientPattern, xStart, yStart, dirtyAreaToFill, aDest, aRepeatSize, forceRepeatToCoverTiles)) { 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); gfxRect dirtyFillRect = fillRect.Intersect(dirtyAreaToFill); gfxRect fillRectRelativeToTile = dirtyFillRect - tileRect.TopLeft(); StyleAnimatedRGBA edgeColor{0.0f}; if (mGradient->IsLinear() && !isRepeat && RectIsBeyondLinearGradientEdge(fillRectRelativeToTile, matrix, mStops, gradientStart, gradientEnd, &edgeColor)) { edgeColor.alpha *= aOpacity; aContext.SetColor(ToSRGBColor(edgeColor)); } else { aContext.SetMatrixDouble( aContext.CurrentMatrixDouble().Copy().PreTranslate( tileRect.TopLeft())); aContext.SetPattern(gradientPattern); } aContext.Fill(); aContext.SetMatrix(ctm); } } } 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) { return false; } nscoord appUnitsPerDevPixel = mPresContext->AppUnitsPerDevPixel(); // 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); if (!canUseExtendModeForTiling) { return false; } IntSize tileSize{ NSAppUnitsToIntPixels(aDest.width, appUnitsPerDevPixel), NSAppUnitsToIntPixels(aDest.height, appUnitsPerDevPixel), }; // Check whether this is a reasonable surface size and doesn't overflow // before doing calculations with the tile size if (!Factory::ReasonableSurfaceSize(tileSize)) { return false; } // 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) { return false; } // Draw the gradient pattern into a surface for our single tile RefPtr tileSurface; { RefPtr tileTarget = aContext.GetDrawTarget()->CreateSimilarDrawTarget( tileSize, gfx::SurfaceFormat::B8G8R8A8); if (!tileTarget || !tileTarget->IsValid()) { return false; } RefPtr tileContext = gfxContext::CreateOrNull(tileTarget); tileContext->SetPattern(aGradientPattern); tileContext->Paint(); tileContext = nullptr; tileSurface = tileTarget->Snapshot(); tileTarget = nullptr; } // Draw the gradient using tileSurface as a repeating pattern masked by // the dirtyRect Matrix tileTransform = Matrix::Translation( NSAppUnitsToFloatPixels(aXStart, appUnitsPerDevPixel), NSAppUnitsToFloatPixels(aYStart, appUnitsPerDevPixel)); aContext.NewPath(); aContext.Rectangle(aDirtyAreaToFill); aContext.Fill(SurfacePattern(tileSurface, ExtendMode::REPEAT, tileTransform)); return true; } void nsCSSGradientRenderer::BuildWebRenderParameters( float aOpacity, wr::ExtendMode& aMode, nsTArray& aStops, LayoutDevicePoint& aLineStart, LayoutDevicePoint& aLineEnd, LayoutDeviceSize& aGradientRadius, LayoutDevicePoint& aGradientCenter, float& aGradientAngle) { aMode = mGradient->Repeating() ? wr::ExtendMode::Repeat : wr::ExtendMode::Clamp; 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 = mStops[i].mPosition; } aLineStart = LayoutDevicePoint(mLineStart.x, mLineStart.y); aLineEnd = LayoutDevicePoint(mLineEnd.x, mLineEnd.y); aGradientRadius = LayoutDeviceSize(mRadiusX, mRadiusY); aGradientCenter = LayoutDevicePoint(mCenter.x, mCenter.y); aGradientAngle = mAngle; } void nsCSSGradientRenderer::BuildWebRenderDisplayItems( wr::DisplayListBuilder& aBuilder, const layers::StackingContextHelper& aSc, const nsRect& aDest, const nsRect& aFillArea, const nsSize& aRepeatSize, const CSSIntRect& aSrc, bool aIsBackfaceVisible, float aOpacity) { if (aDest.IsEmpty() || aFillArea.IsEmpty()) { return; } wr::ExtendMode extendMode; nsTArray stops; LayoutDevicePoint lineStart; LayoutDevicePoint lineEnd; LayoutDeviceSize gradientRadius; LayoutDevicePoint gradientCenter; float gradientAngle; BuildWebRenderParameters(aOpacity, extendMode, stops, lineStart, lineEnd, gradientRadius, gradientCenter, gradientAngle); nscoord appUnitsPerDevPixel = mPresContext->AppUnitsPerDevPixel(); nsPoint firstTile = nsPoint(FindTileStart(aFillArea.x, aDest.x, aRepeatSize.width), FindTileStart(aFillArea.y, aDest.y, aRepeatSize.height)); // Translate the parameters into device coordinates LayoutDeviceRect clipBounds = LayoutDevicePixel::FromAppUnits(aFillArea, appUnitsPerDevPixel); LayoutDeviceRect firstTileBounds = LayoutDevicePixel::FromAppUnits( nsRect(firstTile, aDest.Size()), appUnitsPerDevPixel); LayoutDeviceSize tileRepeat = LayoutDevicePixel::FromAppUnits(aRepeatSize, appUnitsPerDevPixel); // 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))); lineStart.x = (lineStart.x - srcTransform.x) * srcTransform.width; lineStart.y = (lineStart.y - srcTransform.y) * srcTransform.height; gradientCenter.x = (gradientCenter.x - srcTransform.x) * srcTransform.width; gradientCenter.y = (gradientCenter.y - srcTransform.y) * srcTransform.height; if (mGradient->IsLinear()) { lineEnd.x = (lineEnd.x - srcTransform.x) * srcTransform.width; lineEnd.y = (lineEnd.y - srcTransform.y) * srcTransform.height; aBuilder.PushLinearGradient( mozilla::wr::ToLayoutRect(gradientBounds), mozilla::wr::ToLayoutRect(clipBounds), aIsBackfaceVisible, mozilla::wr::ToLayoutPoint(lineStart), mozilla::wr::ToLayoutPoint(lineEnd), stops, extendMode, mozilla::wr::ToLayoutSize(firstTileBounds.Size()), mozilla::wr::ToLayoutSize(tileSpacing)); } else if (mGradient->IsRadial()) { gradientRadius.width *= srcTransform.width; gradientRadius.height *= srcTransform.height; aBuilder.PushRadialGradient( mozilla::wr::ToLayoutRect(gradientBounds), mozilla::wr::ToLayoutRect(clipBounds), aIsBackfaceVisible, mozilla::wr::ToLayoutPoint(lineStart), mozilla::wr::ToLayoutSize(gradientRadius), stops, extendMode, mozilla::wr::ToLayoutSize(firstTileBounds.Size()), mozilla::wr::ToLayoutSize(tileSpacing)); } else { MOZ_ASSERT(mGradient->IsConic()); aBuilder.PushConicGradient( mozilla::wr::ToLayoutRect(gradientBounds), mozilla::wr::ToLayoutRect(clipBounds), aIsBackfaceVisible, mozilla::wr::ToLayoutPoint(gradientCenter), gradientAngle, stops, extendMode, mozilla::wr::ToLayoutSize(firstTileBounds.Size()), mozilla::wr::ToLayoutSize(tileSpacing)); } } } // namespace mozilla