gecko-dev/layout/painting/nsCSSRenderingGradients.cpp
Kartikaya Gupta 00ef028ed3 Bug 1416267 - Update gfxContext matrix functions to avoid flip-flopping between float and double matrices. r=jrmuizel
The core of this change is in gfxContext.*:
- change gfxContext::CurrentMatrix() and gfxContext::SetMatrix() to
  return and take a Matrix respectively, instead of converting to
  and from a gfxMatrix (which uses doubles). These functions therefore
  will now match the native representation of the transform in gfxContext.
- add two new functions CurrentMatrixDouble() and SetMatrixDouble() that
  do what the old CurrentMatrix() and SetMatrix() used to do, i.e.
  convert between the float matrix and the double matrix.

The rest of the change is just updating the call sites to avoid round-
tripping between floats and doubles where possible. Call sites that are
hard to fix are migrated to the new XXXDouble functions which preserves
the existing behaviour.

MozReview-Commit-ID: 5sbBpLUus3U
2017-11-10 21:14:09 -05:00

1111 lines
44 KiB
C++

/* -*- 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 "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 "nsStyleStructInlines.h"
#include "nsCSSProps.h"
#include "mozilla/Telemetry.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 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<ColorStop>& aStops,
const gfxPoint& aGradientStart,
const gfxPoint& aGradientEnd,
Color* 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<ColorStop>& 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<ColorStop>& aStops)
{
for (size_t x = 1; x < aStops.Length(); x++) {
const ColorStop leftStop = aStops[x - 1];
const ColorStop rightStop = aStops[x];
// if the left and right stop have the same alpha value, we don't need
// to do anything
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<float>(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<ColorStop>& aStops)
{
MOZ_ASSERT(aStops.Length() > 0);
// If all stops are outside the range, then get rid of everything and replace
// with a single colour.
if (aStops.Length() < 2 || aStops[0].mPosition > 1 ||
aStops.LastElement().mPosition < 0) {
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 {
nsCSSGradientRenderer
nsCSSGradientRenderer::Create(nsPresContext* aPresContext,
nsStyleGradient* aGradient,
const nsSize& aIntrinsicSize)
{
nscoord appUnitsPerDevPixel = aPresContext->AppUnitsPerDevPixel();
gfxSize srcSize = gfxSize(gfxFloat(aIntrinsicSize.width)/appUnitsPerDevPixel,
gfxFloat(aIntrinsicSize.height)/appUnitsPerDevPixel);
// 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<ColorStop> 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
double previousPosition = firstUnsetPosition > 0
? stops[firstUnsetPosition - 1].mPosition
: stops[i - 1].mPosition;
position = std::max(position, previousPosition);
}
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;
}
}
ResolveMidpoints(stops);
nsCSSGradientRenderer renderer;
renderer.mPresContext = aPresContext;
renderer.mGradient = aGradient;
renderer.mStops = std::move(stops);
renderer.mLineStart = lineStart;
renderer.mLineEnd = lineEnd;
renderer.mRadiusX = radiusX;
renderer.mRadiusY = radiusY;
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);
Telemetry::AutoTimer<Telemetry::GRADIENT_DURATION, Telemetry::Microsecond> gradientTimer;
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->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR &&
(mLineStart.x == mLineEnd.x) != (mLineStart.y == mLineEnd.y) &&
aRepeatSize.width == aDest.width && aRepeatSize.height == aDest.height &&
!mGradient->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 (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->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && firstStop < 0.0) {
if (mGradient->mRepeating) {
// 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 =
InterpolateColor(mStops[i].mColor, mStops[i + 1].mColor, frac);
}
}
}
}
}
firstStop = mStops[0].mPosition;
MOZ_ASSERT(firstStop >= 0.0, "Failed to fix stop offsets");
}
if (mGradient->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR && !mGradient->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 = 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->mShape != NS_STYLE_GRADIENT_SHAPE_LINEAR &&
(mRadiusX < 1e-6 || mRadiusY < 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 (mGradient->mRepeating || zeroRadius) {
mRadiusX = mRadiusY = 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 (!mGradient->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<gfxPattern> gradientPattern;
gfxPoint gradientStart;
gfxPoint gradientEnd;
if (mGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR) {
// 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 {
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);
}
}
// 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(mPresContext->CSSPixelsToAppUnits(aSrc.width))/aDest.width,
gfxFloat(mPresContext->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(mStops[0].mColor);
Color lastColor(mStops.LastElement().mColor);
mStops.Clear();
if (!mGradient->mRepeating && !zeroRadius) {
mStops.AppendElement(ColorStop(firstStop, false, firstColor));
}
mStops.AppendElement(ColorStop(firstStop, false, lastColor));
}
ResolvePremultipliedAlpha(mStops);
bool isRepeat = mGradient->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<gfx::GradientStop> rawStops(mStops.Length());
rawStops.SetLength(mStops.Length());
for(uint32_t i = 0; i < mStops.Length(); i++) {
rawStops[i].color = mStops[i].mColor;
rawStops[i].color.a *= aOpacity;
rawStops[i].offset = stopScale * (mStops[i].mPosition - stopOrigin);
}
RefPtr<mozilla::gfx::GradientStops> 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();
// 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();
Color edgeColor;
if (mGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR && !isRepeat &&
RectIsBeyondLinearGradientEdge(fillRectRelativeToTile, matrix, mStops,
gradientStart, gradientEnd, &edgeColor)) {
edgeColor.a *= aOpacity;
aContext.SetColor(edgeColor);
} else {
aContext.SetMatrixDouble(
aContext.CurrentMatrixDouble().Copy().PreTranslate(tileRect.TopLeft()));
aContext.SetPattern(gradientPattern);
}
aContext.Fill();
aContext.SetMatrix(ctm);
}
}
}
void
nsCSSGradientRenderer::BuildWebRenderParameters(float aOpacity,
wr::ExtendMode& aMode,
nsTArray<wr::GradientStop>& aStops,
LayoutDevicePoint& aLineStart,
LayoutDevicePoint& aLineEnd,
LayoutDeviceSize& aGradientRadius)
{
aMode = mGradient->mRepeating ? wr::ExtendMode::Repeat : wr::ExtendMode::Clamp;
aStops.SetLength(mStops.Length());
for(uint32_t i = 0; i < mStops.Length(); i++) {
aStops[i].color.r = mStops[i].mColor.r;
aStops[i].color.g = mStops[i].mColor.g;
aStops[i].color.b = mStops[i].mColor.b;
aStops[i].color.a = mStops[i].mColor.a * aOpacity;
aStops[i].offset = mStops[i].mPosition;
}
aLineStart = LayoutDevicePoint(mLineStart.x, mLineStart.y);
aLineEnd = LayoutDevicePoint(mLineEnd.x, mLineEnd.y);
aGradientRadius = LayoutDeviceSize(mRadiusX, mRadiusY);
}
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<wr::GradientStop> stops;
LayoutDevicePoint lineStart;
LayoutDevicePoint lineEnd;
LayoutDeviceSize gradientRadius;
BuildWebRenderParameters(aOpacity, extendMode, stops, lineStart, lineEnd, gradientRadius);
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();
// Make the rects relative to the parent stacking context
wr::LayoutRect wrClipBounds = aSc.ToRelativeLayoutRect(clipBounds);
wr::LayoutRect wrGradientBounds = aSc.ToRelativeLayoutRect(gradientBounds);
// srcTransform is used for scaling the gradient to match aSrc
LayoutDeviceRect srcTransform = LayoutDeviceRect(mPresContext->CSSPixelsToAppUnits(aSrc.x),
mPresContext->CSSPixelsToAppUnits(aSrc.y),
aDest.width / ((float)mPresContext->CSSPixelsToAppUnits(aSrc.width)),
aDest.height / ((float)mPresContext->CSSPixelsToAppUnits(aSrc.height)));
lineStart.x = (lineStart.x - srcTransform.x) * srcTransform.width;
lineStart.y = (lineStart.y - srcTransform.y) * srcTransform.height;
if (mGradient->mShape == NS_STYLE_GRADIENT_SHAPE_LINEAR) {
lineEnd.x = (lineEnd.x - srcTransform.x) * srcTransform.width;
lineEnd.y = (lineEnd.y - srcTransform.y) * srcTransform.height;
aBuilder.PushLinearGradient(
wrGradientBounds,
wrClipBounds,
aIsBackfaceVisible,
mozilla::wr::ToLayoutPoint(lineStart),
mozilla::wr::ToLayoutPoint(lineEnd),
stops,
extendMode,
mozilla::wr::ToLayoutSize(firstTileBounds.Size()),
mozilla::wr::ToLayoutSize(tileSpacing));
} else {
gradientRadius.width *= srcTransform.width;
gradientRadius.height *= srcTransform.height;
aBuilder.PushRadialGradient(
wrGradientBounds,
wrClipBounds,
aIsBackfaceVisible,
mozilla::wr::ToLayoutPoint(lineStart),
mozilla::wr::ToLayoutSize(gradientRadius),
stops,
extendMode,
mozilla::wr::ToLayoutSize(firstTileBounds.Size()),
mozilla::wr::ToLayoutSize(tileSpacing));
}
}
} // namespace mozilla