From db5037c2f6c20d017b82304751992461cc145ddb Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Tue, 14 Mar 2017 01:05:41 -0500 Subject: [PATCH] Bug 1341101 part 1 - Move nsCSSRendering::PaintGradient into its own file r=jrmuizel MozReview-Commit-ID: 1DyZ8TpcA3m --HG-- rename : layout/painting/nsCSSRendering.cpp => layout/painting/nsCSSRenderingGradients.cpp extra : rebase_source : e328771c2e4c19e23e720683b983cfb22d2cf9f8 --- layout/painting/moz.build | 2 + layout/painting/nsCSSRendering.cpp | 954 ------------------- layout/painting/nsCSSRenderingGradients.cpp | 992 ++++++++++++++++++++ layout/painting/nsCSSRenderingGradients.h | 41 + layout/painting/nsImageRenderer.cpp | 9 +- 5 files changed, 1040 insertions(+), 958 deletions(-) create mode 100644 layout/painting/nsCSSRenderingGradients.cpp create mode 100644 layout/painting/nsCSSRenderingGradients.h diff --git a/layout/painting/moz.build b/layout/painting/moz.build index db0a81cf2b2a..b810bcd97d2d 100644 --- a/layout/painting/moz.build +++ b/layout/painting/moz.build @@ -16,6 +16,7 @@ EXPORTS += [ 'FrameLayerBuilder.h', 'LayerState.h', 'nsCSSRenderingBorders.h', + 'nsCSSRenderingGradients.h', 'nsDisplayItemTypes.h', 'nsDisplayItemTypesList.h', 'nsDisplayList.h', @@ -39,6 +40,7 @@ UNIFIED_SOURCES += [ 'MaskLayerImageCache.cpp', 'nsCSSRendering.cpp', 'nsCSSRenderingBorders.cpp', + 'nsCSSRenderingGradients.cpp', 'nsDisplayList.cpp', 'nsDisplayListInvalidation.cpp', 'nsImageRenderer.cpp', diff --git a/layout/painting/nsCSSRendering.cpp b/layout/painting/nsCSSRendering.cpp index 41f1a32ac5c1..01c5cafa9558 100644 --- a/layout/painting/nsCSSRendering.cpp +++ b/layout/painting/nsCSSRendering.cpp @@ -418,17 +418,6 @@ protected: } }; -// A resolved color stop, with a specific position along the gradient line and -// a color. -struct ColorStop { - ColorStop(): mPosition(0), mIsMidpoint(false) {} - ColorStop(double aPosition, bool aIsMidPoint, const Color& aColor) : - mPosition(aPosition), mIsMidpoint(aIsMidPoint), mColor(aColor) {} - double mPosition; // along the gradient line; 0=start, 1=end - bool mIsMidpoint; - Color mColor; -}; - /* Local functions */ static nscolor MakeBevelColor(mozilla::Side whichSide, uint8_t style, nscolor aBackgroundColor, @@ -2400,949 +2389,6 @@ nsCSSRendering::DetermineBackgroundColor(nsPresContext* aPresContext, return bgColor; } -static gfxFloat -ConvertGradientValueToPixels(const nsStyleCoord& aCoord, - gfxFloat aFillLength, - int32_t aAppUnitsPerPixel) -{ - switch (aCoord.GetUnit()) { - case eStyleUnit_Percent: - return aCoord.GetPercentValue() * aFillLength; - case eStyleUnit_Coord: - return NSAppUnitsToFloatPixels(aCoord.GetCoordValue(), aAppUnitsPerPixel); - case eStyleUnit_Calc: { - const nsStyleCoord::Calc *calc = aCoord.GetCalcValue(); - return calc->mPercent * aFillLength + - NSAppUnitsToFloatPixels(calc->mLength, aAppUnitsPerPixel); - } - default: - NS_WARNING("Unexpected coord unit"); - return 0; - } -} - -// 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 gfxPoint -ComputeGradientLineEndFromAngle(const gfxPoint& aStart, - double aAngle, - const gfxSize& aBoxSize) -{ - double dx = cos(-aAngle); - double dy = sin(-aAngle); - gfxPoint farthestCorner(dx > 0 ? aBoxSize.width : 0, - dy > 0 ? aBoxSize.height : 0); - gfxPoint delta = farthestCorner - aStart; - double u = delta.x*dy - delta.y*dx; - return farthestCorner + gfxPoint(-u*dy, u*dx); -} - -// Compute the start and end points of the gradient line for a linear gradient. -static void -ComputeLinearGradientLine(nsPresContext* aPresContext, - nsStyleGradient* aGradient, - const gfxSize& aBoxSize, - gfxPoint* aLineStart, - gfxPoint* aLineEnd) -{ - if (aGradient->mBgPosX.GetUnit() == eStyleUnit_None) { - double angle; - if (aGradient->mAngle.IsAngleValue()) { - angle = aGradient->mAngle.GetAngleValueInRadians(); - if (!aGradient->mLegacySyntax) { - angle = M_PI_2 - angle; - } - } else { - angle = -M_PI_2; // defaults to vertical gradient starting from top - } - gfxPoint center(aBoxSize.width/2, aBoxSize.height/2); - *aLineEnd = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); - *aLineStart = gfxPoint(aBoxSize.width, aBoxSize.height) - *aLineEnd; - } else if (!aGradient->mLegacySyntax) { - float xSign = aGradient->mBgPosX.GetPercentValue() * 2 - 1; - float ySign = 1 - aGradient->mBgPosY.GetPercentValue() * 2; - double angle = atan2(ySign * aBoxSize.width, xSign * aBoxSize.height); - gfxPoint center(aBoxSize.width/2, aBoxSize.height/2); - *aLineEnd = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); - *aLineStart = gfxPoint(aBoxSize.width, aBoxSize.height) - *aLineEnd; - } else { - int32_t appUnitsPerPixel = aPresContext->AppUnitsPerDevPixel(); - *aLineStart = gfxPoint( - ConvertGradientValueToPixels(aGradient->mBgPosX, aBoxSize.width, - appUnitsPerPixel), - ConvertGradientValueToPixels(aGradient->mBgPosY, aBoxSize.height, - appUnitsPerPixel)); - if (aGradient->mAngle.IsAngleValue()) { - MOZ_ASSERT(aGradient->mLegacySyntax); - double angle = aGradient->mAngle.GetAngleValueInRadians(); - *aLineEnd = ComputeGradientLineEndFromAngle(*aLineStart, angle, aBoxSize); - } else { - // No angle, the line end is just the reflection of the start point - // through the center of the box - *aLineEnd = gfxPoint(aBoxSize.width, aBoxSize.height) - *aLineStart; - } - } -} - -// 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 void -ComputeRadialGradientLine(nsPresContext* aPresContext, - nsStyleGradient* aGradient, - const gfxSize& aBoxSize, - gfxPoint* aLineStart, - gfxPoint* aLineEnd, - double* aRadiusX, - double* aRadiusY) -{ - if (aGradient->mBgPosX.GetUnit() == eStyleUnit_None) { - // Default line start point is the center of the box - *aLineStart = gfxPoint(aBoxSize.width/2, aBoxSize.height/2); - } else { - int32_t appUnitsPerPixel = aPresContext->AppUnitsPerDevPixel(); - *aLineStart = gfxPoint( - ConvertGradientValueToPixels(aGradient->mBgPosX, aBoxSize.width, - appUnitsPerPixel), - ConvertGradientValueToPixels(aGradient->mBgPosY, aBoxSize.height, - appUnitsPerPixel)); - } - - // Compute gradient shape: the x and y radii of an ellipse. - double radiusX, radiusY; - double leftDistance = Abs(aLineStart->x); - double rightDistance = Abs(aBoxSize.width - aLineStart->x); - double topDistance = Abs(aLineStart->y); - double bottomDistance = Abs(aBoxSize.height - aLineStart->y); - switch (aGradient->mSize) { - case NS_STYLE_GRADIENT_SIZE_CLOSEST_SIDE: - radiusX = std::min(leftDistance, rightDistance); - radiusY = std::min(topDistance, bottomDistance); - if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { - radiusX = radiusY = std::min(radiusX, radiusY); - } - break; - case NS_STYLE_GRADIENT_SIZE_CLOSEST_CORNER: { - // Compute x and y distances to nearest corner - double offsetX = std::min(leftDistance, rightDistance); - double offsetY = std::min(topDistance, bottomDistance); - if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { - radiusX = radiusY = NS_hypot(offsetX, offsetY); - } else { - // maintain aspect ratio - radiusX = offsetX*M_SQRT2; - radiusY = offsetY*M_SQRT2; - } - break; - } - case NS_STYLE_GRADIENT_SIZE_FARTHEST_SIDE: - radiusX = std::max(leftDistance, rightDistance); - radiusY = std::max(topDistance, bottomDistance); - if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { - radiusX = radiusY = std::max(radiusX, radiusY); - } - break; - case NS_STYLE_GRADIENT_SIZE_FARTHEST_CORNER: { - // Compute x and y distances to nearest corner - double offsetX = std::max(leftDistance, rightDistance); - double offsetY = std::max(topDistance, bottomDistance); - if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { - radiusX = radiusY = NS_hypot(offsetX, offsetY); - } else { - // maintain aspect ratio - radiusX = offsetX*M_SQRT2; - radiusY = offsetY*M_SQRT2; - } - break; - } - case NS_STYLE_GRADIENT_SIZE_EXPLICIT_SIZE: { - int32_t appUnitsPerPixel = aPresContext->AppUnitsPerDevPixel(); - radiusX = ConvertGradientValueToPixels(aGradient->mRadiusX, - aBoxSize.width, appUnitsPerPixel); - radiusY = ConvertGradientValueToPixels(aGradient->mRadiusY, - aBoxSize.height, appUnitsPerPixel); - break; - } - default: - radiusX = radiusY = 0; - MOZ_ASSERT(false, "unknown radial gradient sizing method"); - } - *aRadiusX = radiusX; - *aRadiusY = radiusY; - - double angle; - if (aGradient->mAngle.IsAngleValue()) { - angle = aGradient->mAngle.GetAngleValueInRadians(); - } else { - // Default angle is 0deg - angle = 0.0; - } - - // The gradient line end point is where the gradient line intersects - // the ellipse. - *aLineEnd = *aLineStart + gfxPoint(radiusX*cos(-angle), radiusY*sin(-angle)); -} - - -static float Interpolate(float aF1, float aF2, float aFrac) -{ - return aF1 + aFrac * (aF2 - aF1); -} - -// Returns aFrac*aC2 + (1 - aFrac)*C1. The interpolation is done -// in unpremultiplied space, which is what SVG gradients and cairo -// gradients expect. -static Color -InterpolateColor(const Color& aC1, const Color& aC2, float aFrac) -{ - double other = 1 - aFrac; - return Color(aC2.r*aFrac + aC1.r*other, - aC2.g*aFrac + aC1.g*other, - aC2.b*aFrac + aC1.b*other, - aC2.a*aFrac + aC1.a*other); -} - -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 * p.x + d.y * p.y; - double denominator = d.x * d.x + d.y * d.y; - return numerator / denominator; -} - -static bool -RectIsBeyondLinearGradientEdge(const gfxRect& aRect, - const gfxMatrix& aPatternMatrix, - const nsTArray& aStops, - const gfxPoint& aGradientStart, - const gfxPoint& aGradientEnd, - Color* aOutEdgeColor) -{ - gfxFloat topLeft = LinearGradientStopPositionForPoint( - aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.TopLeft())); - gfxFloat topRight = LinearGradientStopPositionForPoint( - aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.TopRight())); - gfxFloat bottomLeft = LinearGradientStopPositionForPoint( - aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.BottomLeft())); - gfxFloat bottomRight = LinearGradientStopPositionForPoint( - aGradientStart, aGradientEnd, aPatternMatrix.Transform(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; - } - - Color color1 = stops[x-1].mColor; - Color 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 (size_t y = 0; y < 9; y++) { - // 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. - float relativeOffset = (newStops[y].mPosition - offset1) / (offset2 - offset1); - float multiplier = powf(relativeOffset, logf(.5f) / logf(midpoint)); - - gfx::Float red = color1.r + multiplier * (color2.r - color1.r); - gfx::Float green = color1.g + multiplier * (color2.g - color1.g); - gfx::Float blue = color1.b + multiplier * (color2.b - color1.b); - gfx::Float alpha = color1.a + multiplier * (color2.a - color1.a); - - newStops[y].mColor = Color(red, green, blue, alpha); - } - - stops.ReplaceElementsAt(x, 1, newStops, 9); - x += 9; - } -} - -static Color -Premultiply(const Color& aColor) -{ - gfx::Float a = aColor.a; - return Color(aColor.r * a, aColor.g * a, aColor.b * a, a); -} - -static Color -Unpremultiply(const Color& aColor) -{ - gfx::Float a = aColor.a; - return (a > 0.f) - ? Color(aColor.r / a, aColor.g / a, aColor.b / a, a) - : aColor; -} - -static Color -TransparentColor(Color aColor) { - aColor.a = 0; - return aColor; -} - -// 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 - if (leftStop.mColor.a == rightStop.mColor.a) { - continue; - } - - // Is the stop on the left 100% transparent? If so, have it adopt the color - // of the right stop - if (leftStop.mColor.a == 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.a == 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.a != 1.0f || rightStop.mColor.a != 1.0f) { - Color premulLeftColor = Premultiply(leftStop.mColor); - Color premulRightColor = Premultiply(rightStop.mColor); - // Calculate how many extra steps. We do a step per 10% transparency. - size_t stepCount = NSToIntFloor(fabsf(leftStop.mColor.a - rightStop.mColor.a) / kAlphaIncrementPerGradientStep); - for (size_t y = 1; y < stepCount; y++) { - float frac = static_cast(y) / stepCount; - ColorStop newStop(Interpolate(leftStop.mPosition, rightStop.mPosition, frac), false, - Unpremultiply(InterpolateColor(premulLeftColor, premulRightColor, frac))); - aStops.InsertElementAt(x, newStop); - x++; - } - } - } -} - -static ColorStop -InterpolateColorStop(const ColorStop& aFirst, const ColorStop& aSecond, - double aPosition, const Color& 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, - Unpremultiply(InterpolateColor(Premultiply(aFirst.mColor), - Premultiply(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) { - Color 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.RemoveElementsAt(i + 1, 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)); - } -} - -void -nsCSSRendering::PaintGradient(nsPresContext* aPresContext, - gfxContext& aContext, - nsStyleGradient* aGradient, - const nsRect& aDirtyRect, - const nsRect& aDest, - const nsRect& aFillArea, - const nsSize& aRepeatSize, - const CSSIntRect& aSrc, - const nsSize& aIntrinsicSize, - float aOpacity) -{ - PROFILER_LABEL("nsCSSRendering", "PaintGradient", - js::ProfileEntry::Category::GRAPHICS); - - Telemetry::AutoTimer gradientTimer; - if (aDest.IsEmpty() || aFillArea.IsEmpty()) { - return; - } - - nscoord appUnitsPerDevPixel = aPresContext->AppUnitsPerDevPixel(); - gfxSize srcSize = gfxSize(gfxFloat(aIntrinsicSize.width)/appUnitsPerDevPixel, - gfxFloat(aIntrinsicSize.height)/appUnitsPerDevPixel); - - bool cellContainsFill = aDest.Contains(aFillArea); - - // Compute "gradient line" start and end relative to the intrinsic size of - // the gradient. - gfxPoint lineStart, lineEnd; - double radiusX = 0, radiusY = 0; // for radial gradients only - if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR) { - ComputeLinearGradientLine(aPresContext, aGradient, srcSize, - &lineStart, &lineEnd); - } else { - ComputeRadialGradientLine(aPresContext, aGradient, srcSize, - &lineStart, &lineEnd, &radiusX, &radiusY); - } - // Avoid sending Infs or Nans to downwind draw targets. - if (!lineStart.IsFinite() || !lineEnd.IsFinite()) { - lineStart = lineEnd = gfxPoint(0, 0); - } - gfxFloat lineLength = NS_hypot(lineEnd.x - lineStart.x, - lineEnd.y - lineStart.y); - - MOZ_ASSERT(aGradient->mStops.Length() >= 2, - "The parser should reject gradients with less than two stops"); - - // Build color stop array and compute stop positions - nsTArray stops; - // 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, otherwise - // it's -1. - int32_t firstUnsetPosition = -1; - for (uint32_t i = 0; i < aGradient->mStops.Length(); ++i) { - const nsStyleGradientStop& stop = aGradient->mStops[i]; - double position; - switch (stop.mLocation.GetUnit()) { - case eStyleUnit_None: - if (i == 0) { - // First stop defaults to position 0.0 - position = 0.0; - } else if (i == aGradient->mStops.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. - // Remeber where the run of stops with no specified position starts, - // if it starts here. - if (firstUnsetPosition < 0) { - firstUnsetPosition = i; - } - stops.AppendElement(ColorStop(0, stop.mIsInterpolationHint, - Color::FromABGR(stop.mColor))); - continue; - } - break; - case eStyleUnit_Percent: - position = stop.mLocation.GetPercentValue(); - break; - case eStyleUnit_Coord: - position = lineLength < 1e-6 ? 0.0 : - stop.mLocation.GetCoordValue() / appUnitsPerDevPixel / lineLength; - break; - case eStyleUnit_Calc: - nsStyleCoord::Calc *calc; - calc = stop.mLocation.GetCalcValue(); - position = calc->mPercent + - ((lineLength < 1e-6) ? 0.0 : - (NSAppUnitsToFloatPixels(calc->mLength, appUnitsPerDevPixel) / lineLength)); - break; - default: - MOZ_ASSERT(false, "Unknown stop position type"); - } - - if (i > 0) { - // Prevent decreasing stop positions by advancing this position - // to the previous stop position, if necessary - position = std::max(position, stops[i - 1].mPosition); - } - stops.AppendElement(ColorStop(position, stop.mIsInterpolationHint, - Color::FromABGR(stop.mColor))); - if (firstUnsetPosition > 0) { - // 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 (uint32_t j = firstUnsetPosition; j < i; ++j) { - p += d; - stops[j].mPosition = p; - } - firstUnsetPosition = -1; - } - } - - // 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 = - aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR && - (lineStart.x == lineEnd.x) != (lineStart.y == lineEnd.y) && - aRepeatSize.width == aDest.width && aRepeatSize.height == aDest.height && - !aGradient->mRepeating && !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 (lineStart.x > lineEnd.x || lineStart.y > lineEnd.y) { - std::swap(lineStart, lineEnd); - matrix.Scale(-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 (lineStart.x != lineEnd.x) { - rectLen = srcRectDev.width; - offset = (srcRectDev.x - lineStart.x) / lineLength; - lineStart.x = srcRectDev.x; - lineEnd.x = srcRectDev.XMost(); - } else { - rectLen = srcRectDev.height; - offset = (srcRectDev.y - lineStart.y) / lineLength; - lineStart.y = srcRectDev.y; - lineEnd.y = srcRectDev.YMost(); - } - - // Adjust gradient stop positions for the new gradient line. - double scale = lineLength / rectLen; - for (size_t i = 0; i < stops.Length(); i++) { - stops[i].mPosition = (stops[i].mPosition - offset) * fabs(scale); - } - - // Clamp or extrapolate gradient stops to exactly [0, 1]. - ClampColorStops(stops); - - lineLength = rectLen; - } - - // Eliminate negative-position stops if the gradient is radial. - double firstStop = stops[0].mPosition; - if (aGradient->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && firstStop < 0.0) { - if (aGradient->mRepeating) { - // Choose an instance of the repeated pattern that gives us all positive - // stop-offsets. - double lastStop = stops[stops.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 < stops.Length(); i++) { - stops[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 < stops.Length(); i++) { - double pos = stops[i].mPosition; - if (pos < 0.0) { - stops[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 < stops.Length() - 1) { - double nextPos = stops[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)); - stops[i].mColor = - InterpolateColor(stops[i].mColor, stops[i + 1].mColor, frac); - } - } - } - } - } - firstStop = stops[0].mPosition; - MOZ_ASSERT(firstStop >= 0.0, "Failed to fix stop offsets"); - } - - if (aGradient->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && !aGradient->mRepeating) { - // 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 = stops[stops.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 = aGradient->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && - (radiusX < 1e-6 || radiusY < 1e-6); - if (stopDelta < 1e-6 || 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 (aGradient->mRepeating || zeroRadius) { - radiusX = radiusY = 0.0; - } - stopDelta = 0.0; - lastStop = firstStop; - } - - // 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 (!aGradient->mRepeating || 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 (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR) { - // Compute the actual gradient line ends we need to pass to cairo after - // stops have been normalized. - gradientStart = lineStart + (lineEnd - lineStart)*stopOrigin; - gradientEnd = lineStart + (lineEnd - lineStart)*stopEnd; - gfxPoint gradientStopStart = lineStart + (lineEnd - lineStart)*firstStop; - gfxPoint gradientStopEnd = lineStart + (lineEnd - lineStart)*lastStop; - - 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 + (lineEnd - lineStart); - gradientStopEnd = gradientStopStart + (lineEnd - lineStart); - } - - gradientPattern = new gfxPattern(gradientStart.x, gradientStart.y, - gradientEnd.x, gradientEnd.y); - } else { - 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 = radiusX*stopOrigin; - double outerRadius = radiusX*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(lineStart.x, lineStart.y, innerRadius, - lineStart.x, lineStart.y, outerRadius); - if (radiusX != radiusY) { - // 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.Translate(lineStart); - matrix.Scale(1.0, radiusX/radiusY); - matrix.Translate(-lineStart); - } - } - // Use a pattern transform to take account of source and dest rects - matrix.Translate(gfxPoint(aPresContext->CSSPixelsToDevPixels(aSrc.x), - aPresContext->CSSPixelsToDevPixels(aSrc.y))); - matrix.Scale(gfxFloat(aPresContext->CSSPixelsToAppUnits(aSrc.width))/aDest.width, - gfxFloat(aPresContext->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). - Color firstColor(stops[0].mColor); - Color lastColor(stops.LastElement().mColor); - stops.Clear(); - - if (!aGradient->mRepeating && !zeroRadius) { - stops.AppendElement(ColorStop(firstStop, false, firstColor)); - } - stops.AppendElement(ColorStop(firstStop, false, lastColor)); - } - - ResolveMidpoints(stops); - ResolvePremultipliedAlpha(stops); - - bool isRepeat = aGradient->mRepeating || 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(stops.Length()); - rawStops.SetLength(stops.Length()); - for(uint32_t i = 0; i < stops.Length(); i++) { - rawStops[i].color = stops[i].mColor; - rawStops[i].color.a *= aOpacity; - rawStops[i].offset = stopScale * (stops[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(); - - gfxMatrix 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(); - - // 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.SetMatrix(transform); - } - aContext.NewPath(); - aContext.Rectangle(fillRect); - - gfxRect dirtyFillRect = fillRect.Intersect(dirtyAreaToFill); - gfxRect fillRectRelativeToTile = dirtyFillRect - tileRect.TopLeft(); - Color edgeColor; - if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR && !isRepeat && - RectIsBeyondLinearGradientEdge(fillRectRelativeToTile, matrix, stops, - gradientStart, gradientEnd, &edgeColor)) { - edgeColor.a *= aOpacity; - aContext.SetColor(edgeColor); - } else { - aContext.SetMatrix( - aContext.CurrentMatrix().Copy().Translate(tileRect.TopLeft())); - aContext.SetPattern(gradientPattern); - } - aContext.Fill(); - aContext.SetMatrix(ctm); - } - } -} - static CompositionOp DetermineCompositionOp(const nsCSSRendering::PaintBGParams& aParams, const nsStyleImageLayers& aLayers, diff --git a/layout/painting/nsCSSRenderingGradients.cpp b/layout/painting/nsCSSRenderingGradients.cpp new file mode 100644 index 000000000000..1811da9c2cb8 --- /dev/null +++ b/layout/painting/nsCSSRenderingGradients.cpp @@ -0,0 +1,992 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +// vim:cindent:ts=2:et:sw=2: +/* 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 "gfx2DGlue.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Helpers.h" +#include "mozilla/MathAlgorithms.h" + +#include "nsStyleConsts.h" +#include "nsPresContext.h" +#include "nsPoint.h" +#include "nsRect.h" +#include "nsStyleContext.h" +#include "nsCSSColorUtils.h" +#include "gfxContext.h" +#include "nsRenderingContext.h" +#include "nsStyleStructInlines.h" +#include "nsCSSProps.h" +#include "mozilla/Telemetry.h" +#include "gfxUtils.h" +#include "gfxGradientCache.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +// A resolved color stop, with a specific position along the gradient line and +// a color. +struct ColorStop { + ColorStop(): mPosition(0), mIsMidpoint(false) {} + ColorStop(double aPosition, bool aIsMidPoint, const Color& aColor) : + mPosition(aPosition), mIsMidpoint(aIsMidPoint), mColor(aColor) {} + double mPosition; // along the gradient line; 0=start, 1=end + bool mIsMidpoint; + Color mColor; +}; + + +static gfxFloat +ConvertGradientValueToPixels(const nsStyleCoord& aCoord, + gfxFloat aFillLength, + int32_t aAppUnitsPerPixel) +{ + switch (aCoord.GetUnit()) { + case eStyleUnit_Percent: + return aCoord.GetPercentValue() * aFillLength; + case eStyleUnit_Coord: + return NSAppUnitsToFloatPixels(aCoord.GetCoordValue(), aAppUnitsPerPixel); + case eStyleUnit_Calc: { + const nsStyleCoord::Calc *calc = aCoord.GetCalcValue(); + return calc->mPercent * aFillLength + + NSAppUnitsToFloatPixels(calc->mLength, aAppUnitsPerPixel); + } + default: + NS_WARNING("Unexpected coord unit"); + return 0; + } +} + +// 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 gfxPoint +ComputeGradientLineEndFromAngle(const gfxPoint& aStart, + double aAngle, + const gfxSize& aBoxSize) +{ + double dx = cos(-aAngle); + double dy = sin(-aAngle); + gfxPoint farthestCorner(dx > 0 ? aBoxSize.width : 0, + dy > 0 ? aBoxSize.height : 0); + gfxPoint delta = farthestCorner - aStart; + double u = delta.x*dy - delta.y*dx; + return farthestCorner + gfxPoint(-u*dy, u*dx); +} + +// Compute the start and end points of the gradient line for a linear gradient. +static void +ComputeLinearGradientLine(nsPresContext* aPresContext, + nsStyleGradient* aGradient, + const gfxSize& aBoxSize, + gfxPoint* aLineStart, + gfxPoint* aLineEnd) +{ + if (aGradient->mBgPosX.GetUnit() == eStyleUnit_None) { + double angle; + if (aGradient->mAngle.IsAngleValue()) { + angle = aGradient->mAngle.GetAngleValueInRadians(); + if (!aGradient->mLegacySyntax) { + angle = M_PI_2 - angle; + } + } else { + angle = -M_PI_2; // defaults to vertical gradient starting from top + } + gfxPoint center(aBoxSize.width/2, aBoxSize.height/2); + *aLineEnd = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); + *aLineStart = gfxPoint(aBoxSize.width, aBoxSize.height) - *aLineEnd; + } else if (!aGradient->mLegacySyntax) { + float xSign = aGradient->mBgPosX.GetPercentValue() * 2 - 1; + float ySign = 1 - aGradient->mBgPosY.GetPercentValue() * 2; + double angle = atan2(ySign * aBoxSize.width, xSign * aBoxSize.height); + gfxPoint center(aBoxSize.width/2, aBoxSize.height/2); + *aLineEnd = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); + *aLineStart = gfxPoint(aBoxSize.width, aBoxSize.height) - *aLineEnd; + } else { + int32_t appUnitsPerPixel = aPresContext->AppUnitsPerDevPixel(); + *aLineStart = gfxPoint( + ConvertGradientValueToPixels(aGradient->mBgPosX, aBoxSize.width, + appUnitsPerPixel), + ConvertGradientValueToPixels(aGradient->mBgPosY, aBoxSize.height, + appUnitsPerPixel)); + if (aGradient->mAngle.IsAngleValue()) { + MOZ_ASSERT(aGradient->mLegacySyntax); + double angle = aGradient->mAngle.GetAngleValueInRadians(); + *aLineEnd = ComputeGradientLineEndFromAngle(*aLineStart, angle, aBoxSize); + } else { + // No angle, the line end is just the reflection of the start point + // through the center of the box + *aLineEnd = gfxPoint(aBoxSize.width, aBoxSize.height) - *aLineStart; + } + } +} + +// 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 void +ComputeRadialGradientLine(nsPresContext* aPresContext, + nsStyleGradient* aGradient, + const gfxSize& aBoxSize, + gfxPoint* aLineStart, + gfxPoint* aLineEnd, + double* aRadiusX, + double* aRadiusY) +{ + if (aGradient->mBgPosX.GetUnit() == eStyleUnit_None) { + // Default line start point is the center of the box + *aLineStart = gfxPoint(aBoxSize.width/2, aBoxSize.height/2); + } else { + int32_t appUnitsPerPixel = aPresContext->AppUnitsPerDevPixel(); + *aLineStart = gfxPoint( + ConvertGradientValueToPixels(aGradient->mBgPosX, aBoxSize.width, + appUnitsPerPixel), + ConvertGradientValueToPixels(aGradient->mBgPosY, aBoxSize.height, + appUnitsPerPixel)); + } + + // Compute gradient shape: the x and y radii of an ellipse. + double radiusX, radiusY; + double leftDistance = Abs(aLineStart->x); + double rightDistance = Abs(aBoxSize.width - aLineStart->x); + double topDistance = Abs(aLineStart->y); + double bottomDistance = Abs(aBoxSize.height - aLineStart->y); + switch (aGradient->mSize) { + case NS_STYLE_GRADIENT_SIZE_CLOSEST_SIDE: + radiusX = std::min(leftDistance, rightDistance); + radiusY = std::min(topDistance, bottomDistance); + if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { + radiusX = radiusY = std::min(radiusX, radiusY); + } + break; + case NS_STYLE_GRADIENT_SIZE_CLOSEST_CORNER: { + // Compute x and y distances to nearest corner + double offsetX = std::min(leftDistance, rightDistance); + double offsetY = std::min(topDistance, bottomDistance); + if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { + radiusX = radiusY = NS_hypot(offsetX, offsetY); + } else { + // maintain aspect ratio + radiusX = offsetX*M_SQRT2; + radiusY = offsetY*M_SQRT2; + } + break; + } + case NS_STYLE_GRADIENT_SIZE_FARTHEST_SIDE: + radiusX = std::max(leftDistance, rightDistance); + radiusY = std::max(topDistance, bottomDistance); + if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { + radiusX = radiusY = std::max(radiusX, radiusY); + } + break; + case NS_STYLE_GRADIENT_SIZE_FARTHEST_CORNER: { + // Compute x and y distances to nearest corner + double offsetX = std::max(leftDistance, rightDistance); + double offsetY = std::max(topDistance, bottomDistance); + if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_CIRCULAR) { + radiusX = radiusY = NS_hypot(offsetX, offsetY); + } else { + // maintain aspect ratio + radiusX = offsetX*M_SQRT2; + radiusY = offsetY*M_SQRT2; + } + break; + } + case NS_STYLE_GRADIENT_SIZE_EXPLICIT_SIZE: { + int32_t appUnitsPerPixel = aPresContext->AppUnitsPerDevPixel(); + radiusX = ConvertGradientValueToPixels(aGradient->mRadiusX, + aBoxSize.width, appUnitsPerPixel); + radiusY = ConvertGradientValueToPixels(aGradient->mRadiusY, + aBoxSize.height, appUnitsPerPixel); + break; + } + default: + radiusX = radiusY = 0; + MOZ_ASSERT(false, "unknown radial gradient sizing method"); + } + *aRadiusX = radiusX; + *aRadiusY = radiusY; + + double angle; + if (aGradient->mAngle.IsAngleValue()) { + angle = aGradient->mAngle.GetAngleValueInRadians(); + } else { + // Default angle is 0deg + angle = 0.0; + } + + // The gradient line end point is where the gradient line intersects + // the ellipse. + *aLineEnd = *aLineStart + gfxPoint(radiusX*cos(-angle), radiusY*sin(-angle)); +} + + +static float Interpolate(float aF1, float aF2, float aFrac) +{ + return aF1 + aFrac * (aF2 - aF1); +} + +// Returns aFrac*aC2 + (1 - aFrac)*C1. The interpolation is done +// in unpremultiplied space, which is what SVG gradients and cairo +// gradients expect. +static Color +InterpolateColor(const Color& aC1, const Color& aC2, float aFrac) +{ + double other = 1 - aFrac; + return Color(aC2.r*aFrac + aC1.r*other, + aC2.g*aFrac + aC1.g*other, + aC2.b*aFrac + aC1.b*other, + aC2.a*aFrac + aC1.a*other); +} + +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 * p.x + d.y * p.y; + double denominator = d.x * d.x + d.y * d.y; + return numerator / denominator; +} + +static bool +RectIsBeyondLinearGradientEdge(const gfxRect& aRect, + const gfxMatrix& aPatternMatrix, + const nsTArray& aStops, + const gfxPoint& aGradientStart, + const gfxPoint& aGradientEnd, + Color* aOutEdgeColor) +{ + gfxFloat topLeft = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.TopLeft())); + gfxFloat topRight = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.TopRight())); + gfxFloat bottomLeft = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, aPatternMatrix.Transform(aRect.BottomLeft())); + gfxFloat bottomRight = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, aPatternMatrix.Transform(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; + } + + Color color1 = stops[x-1].mColor; + Color 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 (size_t y = 0; y < 9; y++) { + // 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. + float relativeOffset = (newStops[y].mPosition - offset1) / (offset2 - offset1); + float multiplier = powf(relativeOffset, logf(.5f) / logf(midpoint)); + + gfx::Float red = color1.r + multiplier * (color2.r - color1.r); + gfx::Float green = color1.g + multiplier * (color2.g - color1.g); + gfx::Float blue = color1.b + multiplier * (color2.b - color1.b); + gfx::Float alpha = color1.a + multiplier * (color2.a - color1.a); + + newStops[y].mColor = Color(red, green, blue, alpha); + } + + stops.ReplaceElementsAt(x, 1, newStops, 9); + x += 9; + } +} + +static Color +Premultiply(const Color& aColor) +{ + gfx::Float a = aColor.a; + return Color(aColor.r * a, aColor.g * a, aColor.b * a, a); +} + +static Color +Unpremultiply(const Color& aColor) +{ + gfx::Float a = aColor.a; + return (a > 0.f) + ? Color(aColor.r / a, aColor.g / a, aColor.b / a, a) + : aColor; +} + +static Color +TransparentColor(Color aColor) { + aColor.a = 0; + return aColor; +} + +// 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 + if (leftStop.mColor.a == rightStop.mColor.a) { + continue; + } + + // Is the stop on the left 100% transparent? If so, have it adopt the color + // of the right stop + if (leftStop.mColor.a == 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.a == 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.a != 1.0f || rightStop.mColor.a != 1.0f) { + Color premulLeftColor = Premultiply(leftStop.mColor); + Color premulRightColor = Premultiply(rightStop.mColor); + // Calculate how many extra steps. We do a step per 10% transparency. + size_t stepCount = NSToIntFloor(fabsf(leftStop.mColor.a - rightStop.mColor.a) / kAlphaIncrementPerGradientStep); + for (size_t y = 1; y < stepCount; y++) { + float frac = static_cast(y) / stepCount; + ColorStop newStop(Interpolate(leftStop.mPosition, rightStop.mPosition, frac), false, + Unpremultiply(InterpolateColor(premulLeftColor, premulRightColor, frac))); + aStops.InsertElementAt(x, newStop); + x++; + } + } + } +} + +static ColorStop +InterpolateColorStop(const ColorStop& aFirst, const ColorStop& aSecond, + double aPosition, const Color& 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, + Unpremultiply(InterpolateColor(Premultiply(aFirst.mColor), + Premultiply(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) { + Color 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.RemoveElementsAt(i + 1, 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 { + +void +nsCSSGradientRenderer::Paint(nsPresContext* aPresContext, + gfxContext& aContext, + nsStyleGradient* aGradient, + const nsRect& aDirtyRect, + const nsRect& aDest, + const nsRect& aFillArea, + const nsSize& aRepeatSize, + const CSSIntRect& aSrc, + const nsSize& aIntrinsicSize, + float aOpacity) +{ + PROFILER_LABEL("nsCSSRendering", "PaintGradient", + js::ProfileEntry::Category::GRAPHICS); + + Telemetry::AutoTimer gradientTimer; + if (aDest.IsEmpty() || aFillArea.IsEmpty()) { + return; + } + + nscoord appUnitsPerDevPixel = aPresContext->AppUnitsPerDevPixel(); + gfxSize srcSize = gfxSize(gfxFloat(aIntrinsicSize.width)/appUnitsPerDevPixel, + gfxFloat(aIntrinsicSize.height)/appUnitsPerDevPixel); + + bool cellContainsFill = aDest.Contains(aFillArea); + + // Compute "gradient line" start and end relative to the intrinsic size of + // the gradient. + gfxPoint lineStart, lineEnd; + double radiusX = 0, radiusY = 0; // for radial gradients only + if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR) { + ComputeLinearGradientLine(aPresContext, aGradient, srcSize, + &lineStart, &lineEnd); + } else { + ComputeRadialGradientLine(aPresContext, aGradient, srcSize, + &lineStart, &lineEnd, &radiusX, &radiusY); + } + // Avoid sending Infs or Nans to downwind draw targets. + if (!lineStart.IsFinite() || !lineEnd.IsFinite()) { + lineStart = lineEnd = gfxPoint(0, 0); + } + gfxFloat lineLength = NS_hypot(lineEnd.x - lineStart.x, + lineEnd.y - lineStart.y); + + MOZ_ASSERT(aGradient->mStops.Length() >= 2, + "The parser should reject gradients with less than two stops"); + + // Build color stop array and compute stop positions + nsTArray stops; + // 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, otherwise + // it's -1. + int32_t firstUnsetPosition = -1; + for (uint32_t i = 0; i < aGradient->mStops.Length(); ++i) { + const nsStyleGradientStop& stop = aGradient->mStops[i]; + double position; + switch (stop.mLocation.GetUnit()) { + case eStyleUnit_None: + if (i == 0) { + // First stop defaults to position 0.0 + position = 0.0; + } else if (i == aGradient->mStops.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. + // Remeber where the run of stops with no specified position starts, + // if it starts here. + if (firstUnsetPosition < 0) { + firstUnsetPosition = i; + } + stops.AppendElement(ColorStop(0, stop.mIsInterpolationHint, + Color::FromABGR(stop.mColor))); + continue; + } + break; + case eStyleUnit_Percent: + position = stop.mLocation.GetPercentValue(); + break; + case eStyleUnit_Coord: + position = lineLength < 1e-6 ? 0.0 : + stop.mLocation.GetCoordValue() / appUnitsPerDevPixel / lineLength; + break; + case eStyleUnit_Calc: + nsStyleCoord::Calc *calc; + calc = stop.mLocation.GetCalcValue(); + position = calc->mPercent + + ((lineLength < 1e-6) ? 0.0 : + (NSAppUnitsToFloatPixels(calc->mLength, appUnitsPerDevPixel) / lineLength)); + break; + default: + MOZ_ASSERT(false, "Unknown stop position type"); + } + + if (i > 0) { + // Prevent decreasing stop positions by advancing this position + // to the previous stop position, if necessary + position = std::max(position, stops[i - 1].mPosition); + } + stops.AppendElement(ColorStop(position, stop.mIsInterpolationHint, + Color::FromABGR(stop.mColor))); + if (firstUnsetPosition > 0) { + // 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 (uint32_t j = firstUnsetPosition; j < i; ++j) { + p += d; + stops[j].mPosition = p; + } + firstUnsetPosition = -1; + } + } + + // 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 = + aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR && + (lineStart.x == lineEnd.x) != (lineStart.y == lineEnd.y) && + aRepeatSize.width == aDest.width && aRepeatSize.height == aDest.height && + !aGradient->mRepeating && !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 (lineStart.x > lineEnd.x || lineStart.y > lineEnd.y) { + std::swap(lineStart, lineEnd); + matrix.Scale(-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 (lineStart.x != lineEnd.x) { + rectLen = srcRectDev.width; + offset = (srcRectDev.x - lineStart.x) / lineLength; + lineStart.x = srcRectDev.x; + lineEnd.x = srcRectDev.XMost(); + } else { + rectLen = srcRectDev.height; + offset = (srcRectDev.y - lineStart.y) / lineLength; + lineStart.y = srcRectDev.y; + lineEnd.y = srcRectDev.YMost(); + } + + // Adjust gradient stop positions for the new gradient line. + double scale = lineLength / rectLen; + for (size_t i = 0; i < stops.Length(); i++) { + stops[i].mPosition = (stops[i].mPosition - offset) * fabs(scale); + } + + // Clamp or extrapolate gradient stops to exactly [0, 1]. + ClampColorStops(stops); + + lineLength = rectLen; + } + + // Eliminate negative-position stops if the gradient is radial. + double firstStop = stops[0].mPosition; + if (aGradient->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && firstStop < 0.0) { + if (aGradient->mRepeating) { + // Choose an instance of the repeated pattern that gives us all positive + // stop-offsets. + double lastStop = stops[stops.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 < stops.Length(); i++) { + stops[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 < stops.Length(); i++) { + double pos = stops[i].mPosition; + if (pos < 0.0) { + stops[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 < stops.Length() - 1) { + double nextPos = stops[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)); + stops[i].mColor = + InterpolateColor(stops[i].mColor, stops[i + 1].mColor, frac); + } + } + } + } + } + firstStop = stops[0].mPosition; + MOZ_ASSERT(firstStop >= 0.0, "Failed to fix stop offsets"); + } + + if (aGradient->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && !aGradient->mRepeating) { + // 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 = stops[stops.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 = aGradient->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && + (radiusX < 1e-6 || radiusY < 1e-6); + if (stopDelta < 1e-6 || 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 (aGradient->mRepeating || zeroRadius) { + radiusX = radiusY = 0.0; + } + stopDelta = 0.0; + lastStop = firstStop; + } + + // 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 (!aGradient->mRepeating || 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 (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR) { + // Compute the actual gradient line ends we need to pass to cairo after + // stops have been normalized. + gradientStart = lineStart + (lineEnd - lineStart)*stopOrigin; + gradientEnd = lineStart + (lineEnd - lineStart)*stopEnd; + gfxPoint gradientStopStart = lineStart + (lineEnd - lineStart)*firstStop; + gfxPoint gradientStopEnd = lineStart + (lineEnd - lineStart)*lastStop; + + 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 + (lineEnd - lineStart); + gradientStopEnd = gradientStopStart + (lineEnd - lineStart); + } + + gradientPattern = new gfxPattern(gradientStart.x, gradientStart.y, + gradientEnd.x, gradientEnd.y); + } else { + 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 = radiusX*stopOrigin; + double outerRadius = radiusX*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(lineStart.x, lineStart.y, innerRadius, + lineStart.x, lineStart.y, outerRadius); + if (radiusX != radiusY) { + // 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.Translate(lineStart); + matrix.Scale(1.0, radiusX/radiusY); + matrix.Translate(-lineStart); + } + } + // Use a pattern transform to take account of source and dest rects + matrix.Translate(gfxPoint(aPresContext->CSSPixelsToDevPixels(aSrc.x), + aPresContext->CSSPixelsToDevPixels(aSrc.y))); + matrix.Scale(gfxFloat(aPresContext->CSSPixelsToAppUnits(aSrc.width))/aDest.width, + gfxFloat(aPresContext->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). + Color firstColor(stops[0].mColor); + Color lastColor(stops.LastElement().mColor); + stops.Clear(); + + if (!aGradient->mRepeating && !zeroRadius) { + stops.AppendElement(ColorStop(firstStop, false, firstColor)); + } + stops.AppendElement(ColorStop(firstStop, false, lastColor)); + } + + ResolveMidpoints(stops); + ResolvePremultipliedAlpha(stops); + + bool isRepeat = aGradient->mRepeating || 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(stops.Length()); + rawStops.SetLength(stops.Length()); + for(uint32_t i = 0; i < stops.Length(); i++) { + rawStops[i].color = stops[i].mColor; + rawStops[i].color.a *= aOpacity; + rawStops[i].offset = stopScale * (stops[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(); + + gfxMatrix 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(); + + // 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.SetMatrix(transform); + } + aContext.NewPath(); + aContext.Rectangle(fillRect); + + gfxRect dirtyFillRect = fillRect.Intersect(dirtyAreaToFill); + gfxRect fillRectRelativeToTile = dirtyFillRect - tileRect.TopLeft(); + Color edgeColor; + if (aGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR && !isRepeat && + RectIsBeyondLinearGradientEdge(fillRectRelativeToTile, matrix, stops, + gradientStart, gradientEnd, &edgeColor)) { + edgeColor.a *= aOpacity; + aContext.SetColor(edgeColor); + } else { + aContext.SetMatrix( + aContext.CurrentMatrix().Copy().Translate(tileRect.TopLeft())); + aContext.SetPattern(gradientPattern); + } + aContext.Fill(); + aContext.SetMatrix(ctm); + } + } +} + +} // namespace mozilla diff --git a/layout/painting/nsCSSRenderingGradients.h b/layout/painting/nsCSSRenderingGradients.h new file mode 100644 index 000000000000..935d412d700b --- /dev/null +++ b/layout/painting/nsCSSRenderingGradients.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsCSSRenderingGradients_h__ +#define nsCSSRenderingGradients_h__ + +#include "nsLayoutUtils.h" +#include "nsStyleStruct.h" +#include "Units.h" + +namespace mozilla { + +class nsCSSGradientRenderer final { +public: + /** + * Render a gradient for an element. + * aDest is the rect for a single tile of the gradient on the destination. + * aFill is the rect on the destination to be covered by repeated tiling of + * the gradient. + * aSrc is the part of the gradient to be rendered into a tile (aDest), if + * aSrc and aDest are different sizes, the image will be scaled to map aSrc + * onto aDest. + * aIntrinsicSize is the size of the source gradient. + */ + static void Paint(nsPresContext* aPresContext, + gfxContext& aContext, + nsStyleGradient* aGradient, + const nsRect& aDirtyRect, + const nsRect& aDest, + const nsRect& aFill, + const nsSize& aRepeatSize, + const mozilla::CSSIntRect& aSrc, + const nsSize& aIntrinsiceSize, + float aOpacity = 1.0); +}; + +} // namespace mozilla + +#endif /* nsCSSRenderingGradients_h__ */ diff --git a/layout/painting/nsImageRenderer.cpp b/layout/painting/nsImageRenderer.cpp index 87d8605c9161..64150dfa12a2 100644 --- a/layout/painting/nsImageRenderer.cpp +++ b/layout/painting/nsImageRenderer.cpp @@ -7,6 +7,7 @@ /* utility functions for drawing borders and backgrounds */ #include "nsImageRenderer.h" +#include "nsCSSRenderingGradients.h" nsSize CSSSizeOrRatio::ComputeConcreteSize() const @@ -508,10 +509,10 @@ nsImageRenderer::Draw(nsPresContext* aPresContext, } case eStyleImageType_Gradient: { - nsCSSRendering::PaintGradient(aPresContext, *ctx, - mGradientData, aDirtyRect, - aDest, aFill, aRepeatSize, aSrc, mSize, - aOpacity); + nsCSSGradientRenderer::Paint(aPresContext, *ctx, + mGradientData, aDirtyRect, + aDest, aFill, aRepeatSize, aSrc, mSize, + aOpacity); break; } case eStyleImageType_Element: