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/. */
7 #include "PathHelpers.h"
12 UserDataKey sDisablePixelSnapping
;
14 void AppendRectToPath(PathBuilder
* aPathBuilder
, const Rect
& aRect
,
15 bool aDrawClockwise
) {
17 aPathBuilder
->MoveTo(aRect
.TopLeft());
18 aPathBuilder
->LineTo(aRect
.TopRight());
19 aPathBuilder
->LineTo(aRect
.BottomRight());
20 aPathBuilder
->LineTo(aRect
.BottomLeft());
22 aPathBuilder
->MoveTo(aRect
.TopRight());
23 aPathBuilder
->LineTo(aRect
.TopLeft());
24 aPathBuilder
->LineTo(aRect
.BottomLeft());
25 aPathBuilder
->LineTo(aRect
.BottomRight());
27 aPathBuilder
->Close();
30 void AppendRoundedRectToPath(PathBuilder
* aPathBuilder
, const Rect
& aRect
,
31 const RectCornerRadii
& aRadii
,
32 bool aDrawClockwise
) {
33 // For CW drawing, this looks like:
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.
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
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).
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
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
77 // We follow the approach in section 2 of Goldapp (least-error,
78 // Hermite-type approximation) and make both ratios equal to
80 // 2 2 + n - sqrt(2n + 28)
81 // alpha = - * ---------------------
84 // where n = 3( cbrt(sqrt(2)+1) - cbrt(sqrt(2)-1) ).
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).
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.
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.
102 const Float alpha
= Float(0.55191497064665766025);
108 twoFloats cwCornerMults
[4] = {{-1, 0}, // cc == clockwise
112 twoFloats ccwCornerMults
[4] = {{+1, 0}, // ccw == counter-clockwise
117 twoFloats
* cornerMults
= aDrawClockwise
? cwCornerMults
: ccwCornerMults
;
119 Point cornerCoords
[] = {aRect
.TopLeft(), aRect
.TopRight(),
120 aRect
.BottomRight(), aRect
.BottomLeft()};
122 Point pc
, p0
, p1
, p2
, p3
;
124 if (aDrawClockwise
) {
125 aPathBuilder
->MoveTo(
126 Point(aRect
.X() + aRadii
[eCornerTopLeft
].width
, aRect
.Y()));
128 aPathBuilder
->MoveTo(Point(
129 aRect
.X() + aRect
.Width() - aRadii
[eCornerTopRight
].width
, aRect
.Y()));
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);
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;
142 pc
= cornerCoords
[c
];
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
;
148 p3
.x
= pc
.x
+ cornerMults
[i3
].a
* aRadii
[c
].width
;
149 p3
.y
= pc
.y
+ cornerMults
[i3
].b
* aRadii
[c
].height
;
151 p1
.x
= p0
.x
+ alpha
* cornerMults
[i2
].a
* aRadii
[c
].width
;
152 p1
.y
= p0
.y
+ alpha
* cornerMults
[i2
].b
* aRadii
[c
].height
;
154 p2
.x
= p3
.x
- alpha
* cornerMults
[i3
].a
* aRadii
[c
].width
;
155 p2
.y
= p3
.y
- alpha
* cornerMults
[i3
].b
* aRadii
[c
].height
;
157 aPathBuilder
->LineTo(p0
);
158 aPathBuilder
->BezierTo(p1
, p2
, p3
);
160 aPathBuilder
->LineTo(pc
);
164 aPathBuilder
->Close();
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
);
173 AppendRoundedRectToPath(aPathBuilder
, rect
, radii
);
176 bool SnapLineToDevicePixelsForStroking(Point
& aP1
, Point
& aP2
,
177 const DrawTarget
& aDrawTarget
,
179 Matrix mat
= aDrawTarget
.GetTransform();
180 if (mat
.HasNonTranslation()) {
183 if (aP1
.x
!= aP2
.x
&& aP1
.y
!= aP2
.y
) {
184 return false; // not a horizontal or vertical line
186 Point p1
= aP1
+ mat
.GetTranslation(); // into device space
187 Point p2
= aP2
+ mat
.GetTranslation();
190 p1
-= mat
.GetTranslation(); // back into user space
191 p2
-= mat
.GetTranslation();
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);
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);
211 void StrokeSnappedEdgesOfRect(const Rect
& aRect
, DrawTarget
& aDrawTarget
,
212 const ColorPattern
& aColor
,
213 const StrokeOptions
& aStrokeOptions
) {
214 if (aRect
.IsEmpty()) {
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
);
224 p1
= aRect
.BottomLeft();
225 p2
= aRect
.BottomRight();
226 SnapLineToDevicePixelsForStroking(p1
, p2
, aDrawTarget
,
227 aStrokeOptions
.mLineWidth
);
228 aDrawTarget
.StrokeLine(p1
, p2
, aColor
, aStrokeOptions
);
230 p1
= aRect
.TopLeft();
231 p2
= aRect
.TopRight();
232 SnapLineToDevicePixelsForStroking(p1
, p2
, aDrawTarget
,
233 aStrokeOptions
.mLineWidth
);
234 aDrawTarget
.StrokeLine(p1
, p2
, aColor
, aStrokeOptions
);
236 p1
= aRect
.TopRight();
237 p2
= aRect
.BottomRight();
238 SnapLineToDevicePixelsForStroking(p1
, p2
, aDrawTarget
,
239 aStrokeOptions
.mLineWidth
);
240 aDrawTarget
.StrokeLine(p1
, p2
, aColor
, aStrokeOptions
);
243 // The logic for this comes from _cairo_stroke_style_max_distance_from_path
244 Margin
MaxStrokeExtents(const StrokeOptions
& aStrokeOptions
,
245 const Matrix
& aTransform
) {
246 double styleExpansionFactor
= 0.5f
;
248 if (aStrokeOptions
.mLineCap
== CapStyle::SQUARE
) {
249 styleExpansionFactor
= M_SQRT1_2
;
252 if (aStrokeOptions
.mLineJoin
== JoinStyle::MITER
&&
253 styleExpansionFactor
< M_SQRT2
* aStrokeOptions
.mMiterLimit
) {
254 styleExpansionFactor
= M_SQRT2
* aStrokeOptions
.mMiterLimit
;
257 styleExpansionFactor
*= aStrokeOptions
.mLineWidth
;
259 double dx
= styleExpansionFactor
* hypot(aTransform
._11
, aTransform
._21
);
260 double dy
= styleExpansionFactor
* hypot(aTransform
._22
, aTransform
._12
);
262 // Even if the stroke only partially covers a pixel, it must still render to
263 // full pixels. Round up to compensate for this.
267 return Margin(dy
, dx
, dy
, dx
);
271 } // namespace mozilla