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