michael@0: /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "PathHelpers.h" michael@0: michael@0: namespace mozilla { michael@0: namespace gfx { michael@0: michael@0: void michael@0: AppendRoundedRectToPath(PathBuilder* aPathBuilder, michael@0: const Rect& aRect, michael@0: // paren's needed due to operator precedence: michael@0: const Size(& aCornerRadii)[4], michael@0: bool aDrawClockwise) michael@0: { michael@0: // For CW drawing, this looks like: michael@0: // michael@0: // ...******0** 1 C michael@0: // **** michael@0: // *** 2 michael@0: // ** michael@0: // * michael@0: // * michael@0: // 3 michael@0: // * michael@0: // * michael@0: // michael@0: // Where 0, 1, 2, 3 are the control points of the Bezier curve for michael@0: // the corner, and C is the actual corner point. michael@0: // michael@0: // At the start of the loop, the current point is assumed to be michael@0: // the point adjacent to the top left corner on the top michael@0: // horizontal. Note that corner indices start at the top left and michael@0: // continue clockwise, whereas in our loop i = 0 refers to the top michael@0: // right corner. michael@0: // michael@0: // When going CCW, the control points are swapped, and the first michael@0: // corner that's drawn is the top left (along with the top segment). michael@0: // michael@0: // There is considerable latitude in how one chooses the four michael@0: // control points for a Bezier curve approximation to an ellipse. michael@0: // For the overall path to be continuous and show no corner at the michael@0: // endpoints of the arc, points 0 and 3 must be at the ends of the michael@0: // straight segments of the rectangle; points 0, 1, and C must be michael@0: // collinear; and points 3, 2, and C must also be collinear. This michael@0: // leaves only two free parameters: the ratio of the line segments michael@0: // 01 and 0C, and the ratio of the line segments 32 and 3C. See michael@0: // the following papers for extensive discussion of how to choose michael@0: // these ratios: michael@0: // michael@0: // Dokken, Tor, et al. "Good approximation of circles by michael@0: // curvature-continuous Bezier curves." Computer-Aided michael@0: // Geometric Design 7(1990) 33--41. michael@0: // Goldapp, Michael. "Approximation of circular arcs by cubic michael@0: // polynomials." Computer-Aided Geometric Design 8(1991) 227--238. michael@0: // Maisonobe, Luc. "Drawing an elliptical arc using polylines, michael@0: // quadratic, or cubic Bezier curves." michael@0: // http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf michael@0: // michael@0: // We follow the approach in section 2 of Goldapp (least-error, michael@0: // Hermite-type approximation) and make both ratios equal to michael@0: // michael@0: // 2 2 + n - sqrt(2n + 28) michael@0: // alpha = - * --------------------- michael@0: // 3 n - 4 michael@0: // michael@0: // where n = 3( cbrt(sqrt(2)+1) - cbrt(sqrt(2)-1) ). michael@0: // michael@0: // This is the result of Goldapp's equation (10b) when the angle michael@0: // swept out by the arc is pi/2, and the parameter "a-bar" is the michael@0: // expression given immediately below equation (21). michael@0: // michael@0: // Using this value, the maximum radial error for a circle, as a michael@0: // fraction of the radius, is on the order of 0.2 x 10^-3. michael@0: // Neither Dokken nor Goldapp discusses error for a general michael@0: // ellipse; Maisonobe does, but his choice of control points michael@0: // follows different constraints, and Goldapp's expression for michael@0: // 'alpha' gives much smaller radial error, even for very flat michael@0: // ellipses, than Maisonobe's equivalent. michael@0: // michael@0: // For the various corners and for each axis, the sign of this michael@0: // constant changes, or it might be 0 -- it's multiplied by the michael@0: // appropriate multiplier from the list before using. michael@0: michael@0: const Float alpha = Float(0.55191497064665766025); michael@0: michael@0: typedef struct { Float a, b; } twoFloats; michael@0: michael@0: twoFloats cwCornerMults[4] = { { -1, 0 }, // cc == clockwise michael@0: { 0, -1 }, michael@0: { +1, 0 }, michael@0: { 0, +1 } }; michael@0: twoFloats ccwCornerMults[4] = { { +1, 0 }, // ccw == counter-clockwise michael@0: { 0, -1 }, michael@0: { -1, 0 }, michael@0: { 0, +1 } }; michael@0: michael@0: twoFloats *cornerMults = aDrawClockwise ? cwCornerMults : ccwCornerMults; michael@0: michael@0: Point cornerCoords[] = { aRect.TopLeft(), aRect.TopRight(), michael@0: aRect.BottomRight(), aRect.BottomLeft() }; michael@0: michael@0: Point pc, p0, p1, p2, p3; michael@0: michael@0: // The indexes of the corners: michael@0: const int kTopLeft = 0, kTopRight = 1; michael@0: michael@0: if (aDrawClockwise) { michael@0: aPathBuilder->MoveTo(Point(aRect.X() + aCornerRadii[kTopLeft].width, michael@0: aRect.Y())); michael@0: } else { michael@0: aPathBuilder->MoveTo(Point(aRect.X() + aRect.Width() - aCornerRadii[kTopRight].width, michael@0: aRect.Y())); michael@0: } michael@0: michael@0: for (int i = 0; i < 4; ++i) { michael@0: // the corner index -- either 1 2 3 0 (cw) or 0 3 2 1 (ccw) michael@0: int c = aDrawClockwise ? ((i+1) % 4) : ((4-i) % 4); michael@0: michael@0: // i+2 and i+3 respectively. These are used to index into the corner michael@0: // multiplier table, and were deduced by calculating out the long form michael@0: // of each corner and finding a pattern in the signs and values. michael@0: int i2 = (i+2) % 4; michael@0: int i3 = (i+3) % 4; michael@0: michael@0: pc = cornerCoords[c]; michael@0: michael@0: if (aCornerRadii[c].width > 0.0 && aCornerRadii[c].height > 0.0) { michael@0: p0.x = pc.x + cornerMults[i].a * aCornerRadii[c].width; michael@0: p0.y = pc.y + cornerMults[i].b * aCornerRadii[c].height; michael@0: michael@0: p3.x = pc.x + cornerMults[i3].a * aCornerRadii[c].width; michael@0: p3.y = pc.y + cornerMults[i3].b * aCornerRadii[c].height; michael@0: michael@0: p1.x = p0.x + alpha * cornerMults[i2].a * aCornerRadii[c].width; michael@0: p1.y = p0.y + alpha * cornerMults[i2].b * aCornerRadii[c].height; michael@0: michael@0: p2.x = p3.x - alpha * cornerMults[i3].a * aCornerRadii[c].width; michael@0: p2.y = p3.y - alpha * cornerMults[i3].b * aCornerRadii[c].height; michael@0: michael@0: aPathBuilder->LineTo(p0); michael@0: aPathBuilder->BezierTo(p1, p2, p3); michael@0: } else { michael@0: aPathBuilder->LineTo(pc); michael@0: } michael@0: } michael@0: michael@0: aPathBuilder->Close(); michael@0: } michael@0: michael@0: void michael@0: AppendEllipseToPath(PathBuilder* aPathBuilder, michael@0: const Point& aCenter, michael@0: const Size& aDimensions) michael@0: { michael@0: Size halfDim = aDimensions / 2.0; michael@0: Rect rect(aCenter - Point(halfDim.width, halfDim.height), aDimensions); michael@0: Size radii[] = { halfDim, halfDim, halfDim, halfDim }; michael@0: michael@0: AppendRoundedRectToPath(aPathBuilder, rect, radii); michael@0: } michael@0: michael@0: } // namespace gfx michael@0: } // namespace mozilla michael@0: