summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert O'Callahan <robert@ocallahan.org>2010-05-11 13:58:10 -0400
committerJeff Muizelaar <jmuizelaar@mozilla.com>2010-05-11 13:58:10 -0400
commit8302952dcff20a1d2de194152ace810c7056f994 (patch)
tree9fbff1e45dd7716c3acb3cb5a3c374c108352549
parent1bda2334b32394a821e6286fbc76617e68da3895 (diff)
quartz: Don't fallback to pixman for repeating radial gradients.
Figuring out where the outer circle should move to is tricky. I hope the algebra in there is understandable. This is a nice performance improvement, probably because we avoid painting the gradient over the entire clipBox (which is usually the entire surface). I tried to write reftests that compared a repeating radial gradient to a non-repeating gradient with manually repeated stops, but it didn't work because the rasterization was slightly different --- I'm not sure why. This patch also forces us to use pixman for all degenerate cases where the circles intersect. This at least makes us consistent across platforms. From https://bugzilla.mozilla.org/show_bug.cgi?id=508227
-rw-r--r--src/cairo-quartz-surface.c203
1 files changed, 180 insertions, 23 deletions
diff --git a/src/cairo-quartz-surface.c b/src/cairo-quartz-surface.c
index ff039cd5..5a5a791b 100644
--- a/src/cairo-quartz-surface.c
+++ b/src/cairo-quartz-surface.c
@@ -818,10 +818,10 @@ CreateGradientFunction (const cairo_gradient_pattern_t *gpat)
}
static CGFunctionRef
-CreateRepeatingGradientFunction (cairo_quartz_surface_t *surface,
- const cairo_gradient_pattern_t *gpat,
- CGPoint *start, CGPoint *end,
- CGAffineTransform matrix)
+CreateRepeatingLinearGradientFunction (cairo_quartz_surface_t *surface,
+ const cairo_gradient_pattern_t *gpat,
+ CGPoint *start, CGPoint *end,
+ CGAffineTransform matrix)
{
cairo_pattern_t *pat;
cairo_quartz_float_t input_value_range[2];
@@ -901,6 +901,146 @@ CreateRepeatingGradientFunction (cairo_quartz_surface_t *surface,
&callbacks);
}
+static void
+UpdateRadialParameterToIncludePoint(double *max_t, CGPoint *center,
+ double dr, double dx, double dy,
+ double x, double y)
+{
+ /* Compute a parameter t such that a circle centered at
+ (center->x + dx*t, center->y + dy*t) with radius dr*t contains the
+ point (x,y).
+
+ Let px = x - center->x, py = y - center->y.
+ Parameter values for which t is on the circle are given by
+ (px - dx*t)^2 + (py - dy*t)^2 = (t*dr)^2
+
+ Solving for t using the quadratic formula, and simplifying, we get
+ numerator = dx*px + dy*py +-
+ sqrt( dr^2*(px^2 + py^2) - (dx*py - dy*px)^2 )
+ denominator = dx^2 + dy^2 - dr^2
+ t = numerator/denominator
+
+ In CreateRepeatingRadialGradientFunction we know the outer circle
+ contains the inner circle. Therefore the distance between the circle
+ centers plus the radius of the inner circle is less than the radius of
+ the outer circle. (This is checked in _cairo_quartz_setup_radial_source.)
+ Therefore
+ dx^2 + dy^2 < dr^2
+ So the denominator is negative and the larger solution for t is given by
+ numerator = dx*px + dy*py -
+ sqrt( dr^2*(px^2 + py^2) - (dx*py - dy*px)^2 )
+ denominator = dx^2 + dy^2 - dr^2
+ t = numerator/denominator
+ dx^2 + dy^2 < dr^2 also ensures that the operand of sqrt is positive.
+ */
+ double px = x - center->x;
+ double py = y - center->y;
+ double dx_py_minus_dy_px = dx*py - dy*px;
+ double numerator = dx*px + dy*py -
+ sqrt (dr*dr*(px*px + py*py) - dx_py_minus_dy_px*dx_py_minus_dy_px);
+ double denominator = dx*dx + dy*dy - dr*dr;
+ double t = numerator/denominator;
+
+ if (*max_t < t) {
+ *max_t = t;
+ }
+}
+
+/* This must only be called when one of the circles properly contains the other */
+static CGFunctionRef
+CreateRepeatingRadialGradientFunction (cairo_quartz_surface_t *surface,
+ const cairo_gradient_pattern_t *gpat,
+ CGPoint *start, double *start_radius,
+ CGPoint *end, double *end_radius)
+{
+ CGRect clip = CGContextGetClipBoundingBox (surface->cgContext);
+ CGAffineTransform transform;
+ cairo_pattern_t *pat;
+ float input_value_range[2];
+ float output_value_ranges[8] = { 0.f, 1.f, 0.f, 1.f, 0.f, 1.f, 0.f, 1.f };
+ CGFunctionCallbacks callbacks = {
+ 0, ComputeGradientValue, (CGFunctionReleaseInfoCallback) cairo_pattern_destroy
+ };
+ CGPoint *inner;
+ double *inner_radius;
+ CGPoint *outer;
+ double *outer_radius;
+ /* minimum and maximum t-parameter values that will make our gradient
+ cover the clipBox */
+ double t_min, t_max, t_temp;
+ /* outer minus inner */
+ double dr, dx, dy;
+
+ _cairo_quartz_cairo_matrix_to_quartz (&gpat->base.matrix, &transform);
+ /* clip is in cairo device coordinates; get it into cairo user space */
+ clip = CGRectApplyAffineTransform (clip, transform);
+
+ if (*start_radius < *end_radius) {
+ /* end circle contains start circle */
+ inner = start;
+ outer = end;
+ inner_radius = start_radius;
+ outer_radius = end_radius;
+ } else {
+ /* start circle contains end circle */
+ inner = end;
+ outer = start;
+ inner_radius = end_radius;
+ outer_radius = start_radius;
+ }
+
+ dr = *outer_radius - *inner_radius;
+ dx = outer->x - inner->x;
+ dy = outer->y - inner->y;
+
+ t_min = -(*inner_radius/dr);
+ inner->x += t_min*dx;
+ inner->y += t_min*dy;
+ *inner_radius = 0.;
+
+ t_temp = 0.;
+ UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
+ clip.origin.x, clip.origin.y);
+ UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
+ clip.origin.x + clip.size.width, clip.origin.y);
+ UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
+ clip.origin.x + clip.size.width, clip.origin.y + clip.size.height);
+ UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
+ clip.origin.x, clip.origin.y + clip.size.height);
+ /* UpdateRadialParameterToIncludePoint assumes t=0 means radius 0.
+ But for the parameter values we use with Quartz, t_min means radius 0.
+ Also, add a small fudge factor to avoid rounding issues. Since the
+ circles are alway expanding and containing the earlier circles, this is
+ OK. */
+ t_temp += 1e-6;
+ t_max = t_min + t_temp;
+ outer->x = inner->x + t_temp*dx;
+ outer->y = inner->y + t_temp*dy;
+ *outer_radius = t_temp*dr;
+
+ /* set the input range for the function -- the function knows how to
+ map values outside of 0.0 .. 1.0 to that range for REPEAT/REFLECT. */
+ if (*start_radius < *end_radius) {
+ input_value_range[0] = t_min;
+ input_value_range[1] = t_max;
+ } else {
+ input_value_range[0] = -t_max;
+ input_value_range[1] = -t_min;
+ }
+
+ if (_cairo_pattern_create_copy (&pat, &gpat->base))
+ /* quartz doesn't deal very well with malloc failing, so there's
+ * not much point in us trying either */
+ return NULL;
+
+ return CGFunctionCreate (pat,
+ 1,
+ input_value_range,
+ 4,
+ output_value_ranges,
+ &callbacks);
+}
+
/* Obtain a CGImageRef from a #cairo_surface_t * */
typedef struct {
@@ -1240,13 +1380,14 @@ _cairo_quartz_setup_linear_source (cairo_quartz_surface_t *surface,
_cairo_fixed_to_double (lpat->p2.y));
if (abspat->extend == CAIRO_EXTEND_NONE ||
- abspat->extend == CAIRO_EXTEND_PAD)
+ abspat->extend == CAIRO_EXTEND_PAD)
{
gradFunc = CreateGradientFunction (&lpat->base);
} else {
- gradFunc = CreateRepeatingGradientFunction (surface,
- &lpat->base,
- &start, &end, surface->sourceTransform);
+ gradFunc = CreateRepeatingLinearGradientFunction (surface,
+ &lpat->base,
+ &start, &end,
+ surface->sourceTransform);
}
surface->sourceShading = CGShadingCreateAxial (rgb,
@@ -1270,6 +1411,15 @@ _cairo_quartz_setup_radial_source (cairo_quartz_surface_t *surface,
CGFunctionRef gradFunc;
CGColorSpaceRef rgb;
bool extend = abspat->extend == CAIRO_EXTEND_PAD;
+ double c1x = _cairo_fixed_to_double (rpat->c1.x);
+ double c1y = _cairo_fixed_to_double (rpat->c1.y);
+ double c2x = _cairo_fixed_to_double (rpat->c2.x);
+ double c2y = _cairo_fixed_to_double (rpat->c2.y);
+ double r1 = _cairo_fixed_to_double (rpat->r1);
+ double r2 = _cairo_fixed_to_double (rpat->r2);
+ double dx = c1x - c2x;
+ double dy = c1y - c2y;
+ double centerDistance = sqrt (dx*dx + dy*dy);
if (rpat->base.n_stops == 0) {
CGContextSetRGBStrokeColor (surface->cgContext, 0., 0., 0., 0.);
@@ -1277,15 +1427,15 @@ _cairo_quartz_setup_radial_source (cairo_quartz_surface_t *surface,
return DO_SOLID;
}
- if (abspat->extend == CAIRO_EXTEND_REPEAT ||
- abspat->extend == CAIRO_EXTEND_REFLECT)
- {
- /* I started trying to map these to Quartz, but it's much harder
- * then the linear case (I think it would involve doing multiple
- * Radial shadings). So, instead, let's just render an image
- * for pixman to draw the shading into, and use that.
+ if (r2 <= centerDistance + r1 + 1e-6 && /* circle 2 doesn't contain circle 1 */
+ r1 <= centerDistance + r2 + 1e-6) { /* circle 1 doesn't contain circle 2 */
+ /* Quartz handles cases where neither circle contains the other very
+ * differently from pixman.
+ * Whatever the correct behaviour is, let's at least have only pixman's
+ * implementation to worry about.
+ * Note that this also catches the cases where r1 == r2.
*/
- return _cairo_quartz_setup_fallback_source (surface, &rpat->base.base);
+ return _cairo_quartz_setup_fallback_source (surface, abspat);
}
mat = abspat->matrix;
@@ -1294,18 +1444,25 @@ _cairo_quartz_setup_radial_source (cairo_quartz_surface_t *surface,
rgb = CGColorSpaceCreateDeviceRGB();
- start = CGPointMake (_cairo_fixed_to_double (rpat->c1.x),
- _cairo_fixed_to_double (rpat->c1.y));
- end = CGPointMake (_cairo_fixed_to_double (rpat->c2.x),
- _cairo_fixed_to_double (rpat->c2.y));
+ start = CGPointMake (c1x, c1y);
+ end = CGPointMake (c2x, c2y);
- gradFunc = CreateGradientFunction (&rpat->base);
+ if (abspat->extend == CAIRO_EXTEND_NONE ||
+ abspat->extend == CAIRO_EXTEND_PAD)
+ {
+ gradFunc = CreateGradientFunction (&rpat->base);
+ } else {
+ gradFunc = CreateRepeatingRadialGradientFunction (surface,
+ &rpat->base,
+ &start, &r1,
+ &end, &r2);
+ }
surface->sourceShading = CGShadingCreateRadial (rgb,
start,
- _cairo_fixed_to_double (rpat->r1),
+ r1,
end,
- _cairo_fixed_to_double (rpat->r2),
+ r2,
gradFunc,
extend, extend);