1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "PathHelpers.h"
8 
9 namespace mozilla {
10 namespace gfx {
11 
12 UserDataKey sDisablePixelSnapping;
13 
AppendRectToPath(PathBuilder * aPathBuilder,const Rect & aRect,bool aDrawClockwise)14 void AppendRectToPath(PathBuilder* aPathBuilder, const Rect& aRect,
15                       bool aDrawClockwise) {
16   if (aDrawClockwise) {
17     aPathBuilder->MoveTo(aRect.TopLeft());
18     aPathBuilder->LineTo(aRect.TopRight());
19     aPathBuilder->LineTo(aRect.BottomRight());
20     aPathBuilder->LineTo(aRect.BottomLeft());
21   } else {
22     aPathBuilder->MoveTo(aRect.TopRight());
23     aPathBuilder->LineTo(aRect.TopLeft());
24     aPathBuilder->LineTo(aRect.BottomLeft());
25     aPathBuilder->LineTo(aRect.BottomRight());
26   }
27   aPathBuilder->Close();
28 }
29 
AppendRoundedRectToPath(PathBuilder * aPathBuilder,const Rect & aRect,const RectCornerRadii & aRadii,bool aDrawClockwise)30 void AppendRoundedRectToPath(PathBuilder* aPathBuilder, const Rect& aRect,
31                              const RectCornerRadii& aRadii,
32                              bool aDrawClockwise) {
33   // For CW drawing, this looks like:
34   //
35   //  ...******0**      1    C
36   //              ****
37   //                  ***    2
38   //                     **
39   //                       *
40   //                        *
41   //                         3
42   //                         *
43   //                         *
44   //
45   // Where 0, 1, 2, 3 are the control points of the Bezier curve for
46   // the corner, and C is the actual corner point.
47   //
48   // At the start of the loop, the current point is assumed to be
49   // the point adjacent to the top left corner on the top
50   // horizontal.  Note that corner indices start at the top left and
51   // continue clockwise, whereas in our loop i = 0 refers to the top
52   // right corner.
53   //
54   // When going CCW, the control points are swapped, and the first
55   // corner that's drawn is the top left (along with the top segment).
56   //
57   // There is considerable latitude in how one chooses the four
58   // control points for a Bezier curve approximation to an ellipse.
59   // For the overall path to be continuous and show no corner at the
60   // endpoints of the arc, points 0 and 3 must be at the ends of the
61   // straight segments of the rectangle; points 0, 1, and C must be
62   // collinear; and points 3, 2, and C must also be collinear.  This
63   // leaves only two free parameters: the ratio of the line segments
64   // 01 and 0C, and the ratio of the line segments 32 and 3C.  See
65   // the following papers for extensive discussion of how to choose
66   // these ratios:
67   //
68   //   Dokken, Tor, et al. "Good approximation of circles by
69   //      curvature-continuous Bezier curves."  Computer-Aided
70   //      Geometric Design 7(1990) 33--41.
71   //   Goldapp, Michael. "Approximation of circular arcs by cubic
72   //      polynomials." Computer-Aided Geometric Design 8(1991) 227--238.
73   //   Maisonobe, Luc. "Drawing an elliptical arc using polylines,
74   //      quadratic, or cubic Bezier curves."
75   //      http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
76   //
77   // We follow the approach in section 2 of Goldapp (least-error,
78   // Hermite-type approximation) and make both ratios equal to
79   //
80   //          2   2 + n - sqrt(2n + 28)
81   //  alpha = - * ---------------------
82   //          3           n - 4
83   //
84   // where n = 3( cbrt(sqrt(2)+1) - cbrt(sqrt(2)-1) ).
85   //
86   // This is the result of Goldapp's equation (10b) when the angle
87   // swept out by the arc is pi/2, and the parameter "a-bar" is the
88   // expression given immediately below equation (21).
89   //
90   // Using this value, the maximum radial error for a circle, as a
91   // fraction of the radius, is on the order of 0.2 x 10^-3.
92   // Neither Dokken nor Goldapp discusses error for a general
93   // ellipse; Maisonobe does, but his choice of control points
94   // follows different constraints, and Goldapp's expression for
95   // 'alpha' gives much smaller radial error, even for very flat
96   // ellipses, than Maisonobe's equivalent.
97   //
98   // For the various corners and for each axis, the sign of this
99   // constant changes, or it might be 0 -- it's multiplied by the
100   // appropriate multiplier from the list before using.
101 
102   const Float alpha = Float(0.55191497064665766025);
103 
104   typedef struct {
105     Float a, b;
106   } twoFloats;
107 
108   twoFloats cwCornerMults[4] = {{-1, 0},  // cc == clockwise
109                                 {0, -1},
110                                 {+1, 0},
111                                 {0, +1}};
112   twoFloats ccwCornerMults[4] = {{+1, 0},  // ccw == counter-clockwise
113                                  {0, -1},
114                                  {-1, 0},
115                                  {0, +1}};
116 
117   twoFloats* cornerMults = aDrawClockwise ? cwCornerMults : ccwCornerMults;
118 
119   Point cornerCoords[] = {aRect.TopLeft(), aRect.TopRight(),
120                           aRect.BottomRight(), aRect.BottomLeft()};
121 
122   Point pc, p0, p1, p2, p3;
123 
124   if (aDrawClockwise) {
125     aPathBuilder->MoveTo(
126         Point(aRect.X() + aRadii[eCornerTopLeft].width, aRect.Y()));
127   } else {
128     aPathBuilder->MoveTo(Point(
129         aRect.X() + aRect.Width() - aRadii[eCornerTopRight].width, aRect.Y()));
130   }
131 
132   for (int i = 0; i < 4; ++i) {
133     // the corner index -- either 1 2 3 0 (cw) or 0 3 2 1 (ccw)
134     int c = aDrawClockwise ? ((i + 1) % 4) : ((4 - i) % 4);
135 
136     // i+2 and i+3 respectively.  These are used to index into the corner
137     // multiplier table, and were deduced by calculating out the long form
138     // of each corner and finding a pattern in the signs and values.
139     int i2 = (i + 2) % 4;
140     int i3 = (i + 3) % 4;
141 
142     pc = cornerCoords[c];
143 
144     if (aRadii[c].width > 0.0 && aRadii[c].height > 0.0) {
145       p0.x = pc.x + cornerMults[i].a * aRadii[c].width;
146       p0.y = pc.y + cornerMults[i].b * aRadii[c].height;
147 
148       p3.x = pc.x + cornerMults[i3].a * aRadii[c].width;
149       p3.y = pc.y + cornerMults[i3].b * aRadii[c].height;
150 
151       p1.x = p0.x + alpha * cornerMults[i2].a * aRadii[c].width;
152       p1.y = p0.y + alpha * cornerMults[i2].b * aRadii[c].height;
153 
154       p2.x = p3.x - alpha * cornerMults[i3].a * aRadii[c].width;
155       p2.y = p3.y - alpha * cornerMults[i3].b * aRadii[c].height;
156 
157       aPathBuilder->LineTo(p0);
158       aPathBuilder->BezierTo(p1, p2, p3);
159     } else {
160       aPathBuilder->LineTo(pc);
161     }
162   }
163 
164   aPathBuilder->Close();
165 }
166 
AppendEllipseToPath(PathBuilder * aPathBuilder,const Point & aCenter,const Size & aDimensions)167 void AppendEllipseToPath(PathBuilder* aPathBuilder, const Point& aCenter,
168                          const Size& aDimensions) {
169   Size halfDim = aDimensions / 2.f;
170   Rect rect(aCenter - Point(halfDim.width, halfDim.height), aDimensions);
171   RectCornerRadii radii(halfDim.width, halfDim.height);
172 
173   AppendRoundedRectToPath(aPathBuilder, rect, radii);
174 }
175 
SnapLineToDevicePixelsForStroking(Point & aP1,Point & aP2,const DrawTarget & aDrawTarget,Float aLineWidth)176 bool SnapLineToDevicePixelsForStroking(Point& aP1, Point& aP2,
177                                        const DrawTarget& aDrawTarget,
178                                        Float aLineWidth) {
179   Matrix mat = aDrawTarget.GetTransform();
180   if (mat.HasNonTranslation()) {
181     return false;
182   }
183   if (aP1.x != aP2.x && aP1.y != aP2.y) {
184     return false;  // not a horizontal or vertical line
185   }
186   Point p1 = aP1 + mat.GetTranslation();  // into device space
187   Point p2 = aP2 + mat.GetTranslation();
188   p1.Round();
189   p2.Round();
190   p1 -= mat.GetTranslation();  // back into user space
191   p2 -= mat.GetTranslation();
192 
193   aP1 = p1;
194   aP2 = p2;
195 
196   bool lineWidthIsOdd = (int(aLineWidth) % 2) == 1;
197   if (lineWidthIsOdd) {
198     if (aP1.x == aP2.x) {
199       // snap vertical line, adding 0.5 to align it to be mid-pixel:
200       aP1 += Point(0.5, 0);
201       aP2 += Point(0.5, 0);
202     } else {
203       // snap horizontal line, adding 0.5 to align it to be mid-pixel:
204       aP1 += Point(0, 0.5);
205       aP2 += Point(0, 0.5);
206     }
207   }
208   return true;
209 }
210 
StrokeSnappedEdgesOfRect(const Rect & aRect,DrawTarget & aDrawTarget,const ColorPattern & aColor,const StrokeOptions & aStrokeOptions)211 void StrokeSnappedEdgesOfRect(const Rect& aRect, DrawTarget& aDrawTarget,
212                               const ColorPattern& aColor,
213                               const StrokeOptions& aStrokeOptions) {
214   if (aRect.IsEmpty()) {
215     return;
216   }
217 
218   Point p1 = aRect.TopLeft();
219   Point p2 = aRect.BottomLeft();
220   SnapLineToDevicePixelsForStroking(p1, p2, aDrawTarget,
221                                     aStrokeOptions.mLineWidth);
222   aDrawTarget.StrokeLine(p1, p2, aColor, aStrokeOptions);
223 
224   p1 = aRect.BottomLeft();
225   p2 = aRect.BottomRight();
226   SnapLineToDevicePixelsForStroking(p1, p2, aDrawTarget,
227                                     aStrokeOptions.mLineWidth);
228   aDrawTarget.StrokeLine(p1, p2, aColor, aStrokeOptions);
229 
230   p1 = aRect.TopLeft();
231   p2 = aRect.TopRight();
232   SnapLineToDevicePixelsForStroking(p1, p2, aDrawTarget,
233                                     aStrokeOptions.mLineWidth);
234   aDrawTarget.StrokeLine(p1, p2, aColor, aStrokeOptions);
235 
236   p1 = aRect.TopRight();
237   p2 = aRect.BottomRight();
238   SnapLineToDevicePixelsForStroking(p1, p2, aDrawTarget,
239                                     aStrokeOptions.mLineWidth);
240   aDrawTarget.StrokeLine(p1, p2, aColor, aStrokeOptions);
241 }
242 
243 // The logic for this comes from _cairo_stroke_style_max_distance_from_path
MaxStrokeExtents(const StrokeOptions & aStrokeOptions,const Matrix & aTransform)244 Margin MaxStrokeExtents(const StrokeOptions& aStrokeOptions,
245                         const Matrix& aTransform) {
246   double styleExpansionFactor = 0.5f;
247 
248   if (aStrokeOptions.mLineCap == CapStyle::SQUARE) {
249     styleExpansionFactor = M_SQRT1_2;
250   }
251 
252   if (aStrokeOptions.mLineJoin == JoinStyle::MITER &&
253       styleExpansionFactor < M_SQRT2 * aStrokeOptions.mMiterLimit) {
254     styleExpansionFactor = M_SQRT2 * aStrokeOptions.mMiterLimit;
255   }
256 
257   styleExpansionFactor *= aStrokeOptions.mLineWidth;
258 
259   double dx = styleExpansionFactor * hypot(aTransform._11, aTransform._21);
260   double dy = styleExpansionFactor * hypot(aTransform._22, aTransform._12);
261 
262   // Even if the stroke only partially covers a pixel, it must still render to
263   // full pixels. Round up to compensate for this.
264   dx = ceil(dx);
265   dy = ceil(dy);
266 
267   return Margin(dy, dx, dy, dx);
268 }
269 
270 }  // namespace gfx
271 }  // namespace mozilla
272