Skip to content

Commit

Permalink
Add some methods to Path to support constructing Path from SVG (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
YGaurora authored Oct 30, 2024
1 parent 8cadf7d commit b4a1231
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 4 deletions.
27 changes: 27 additions & 0 deletions include/tgfx/core/Path.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,28 @@ class Path {
*/
void cubicTo(const Point& control1, const Point& control2, const Point& point);

/**
* Appends an arc to the Path. The arc is represented by one or more conic sections that describe
* part of an oval with radii (rx, ry) rotated by xAxisRotate degrees. The arc curves from the
* last point in the Path to (x, y), choosing one of four possible routes: clockwise or
* counterclockwise, and smaller or larger.
* The arc sweep is always less than 360 degrees. If either radius is zero, or if the last point
* in the Path equals (x, y), a line to (x, y) is appended instead. If both radii are greater
* than zero but too small to fit the arc, they are scaled to fit.
* This method appends up to four conic curves to represent the arc.
* It implements the functionality of the SVG arc, although the SVG sweep-flag value is the
* opposite of the integer value of the sweep parameter; SVG uses 1 for clockwise, while
* counterclockwise is represented by zero.
*
* @param rx x radius before x-axis rotation
* @param ry y radius before x-axis rotation
* @param xAxisRotate x-axis rotation in degrees; positive values are clockwise
* @param largeArc chooses the larger or smaller arc
* @param reversed chooses the rotation direction; false for clockwise
* @param endPoint end point of the arc
*/
void arcTo(float rx, float ry, float xAxisRotate, PathArcSize largeArc, bool reversed,
Point endPoint);
/**
* Closes the current contour of Path. A closed contour connects the first and last Point with
* line, forming a continuous loop.
Expand Down Expand Up @@ -287,6 +309,11 @@ class Path {
*/
int countVerbs() const;

/**
* Returns last point on Path in lastPoint. Returns false if point array is empty.
*/
bool getLastPoint(Point* lastPoint) const;

private:
std::shared_ptr<PathRef> pathRef = nullptr;

Expand Down
14 changes: 14 additions & 0 deletions include/tgfx/core/PathTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ enum class PathVerb {
Close
};

/**
* Specify whether the arc is greater than 180 degrees pair or less than 180 degrees pair.
*/
enum class PathArcSize : uint8_t {
/**
* smaller of arc pair
*/
Small,
/**
* larger of arc pair
*/
Large,
};

/**
* Zero to four Point are stored in points, depending on the returned PathVerb
*/
Expand Down
40 changes: 36 additions & 4 deletions include/tgfx/core/Point.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ struct Point {
}

/**
* Returns true if fX and fY are both zero.
* Returns true if x and y are both zero.
*/
bool isZero() const {
return (0 == x) && (0 == y);
}

/**
* Sets fX to x and fY to y.
* Sets x to xValue and y to yValue.
*/
void set(float xValue, float yValue) {
x = xValue;
Expand Down Expand Up @@ -104,20 +104,52 @@ struct Point {
}

/**
* Returns a Point from b to a; computed as (a.fX - b.fX, a.fY - b.fY).
* Returns a Point from b to a; computed as (a.x - b.x, a.y - b.y).
*/
friend Point operator-(const Point& a, const Point& b) {
return {a.x - b.x, a.y - b.y};
}

/**
* Subtracts vector Point v from Point. Sets Point to: (x - v.x, y - v.y).
*/
void operator-=(const Point& v) {
x -= v.x;
y -= v.y;
}

/**
* Returns Point resulting from Point a offset by Point b, computed as:
* (a.fX + b.fX, a.fY + b.fY).
* (a.x + b.x, a.y + b.y).
*/
friend Point operator+(const Point& a, const Point& b) {
return {a.x + b.x, a.y + b.y};
}

/**
* offset vector point v from Point. Sets Point to: (x + v.x, y + v.y).
*/
void operator+=(const Point& v) {
x += v.x;
y += v.y;
}

/**
* Returns Point multiplied by scale.
* (x * scale, y * scale)
*/
friend Point operator*(const Point& p, float scale) {
return {p.x * scale, p.y * scale};
}

/**
* Multiplies Point by scale. Sets Point to: (x * scale, y * scale).
*/
void operator*=(float scale) {
x *= scale;
y *= scale;
}

/**
* Returns the Euclidean distance from origin.
*/
Expand Down
140 changes: 140 additions & 0 deletions src/core/Path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,133 @@ void Path::cubicTo(const Point& control1, const Point& control2, const Point& po
cubicTo(control1.x, control1.y, control2.x, control2.y, point.x, point.y);
}

// This converts the SVG arc to conics based on the SVG standard.
// Code source:
// 1. kdelibs/kdecore/svgicons Niko's code
// 2. webkit/chrome SVGPathNormalizer::decomposeArcToCubic()
// See also SVG implementation notes:
// http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
// Note that arcSweep bool value is flipped from the original implementation.
void Path::arcTo(float rx, float ry, float xAxisRotate, PathArcSize largeArc, bool reversed,
Point endPoint) {
std::array<Point, 2> srcPoints;
this->getLastPoint(&srcPoints[0]);
// If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto")
// joining the endpoints.
// http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
if (FloatNearlyZero(rx) && FloatNearlyZero(ry)) {
return this->lineTo(endPoint);
}
// If the current point and target point for the arc are identical, it should be treated as a
// zero length path. This ensures continuity in animations.
srcPoints[1] = endPoint;
if (srcPoints[0] == srcPoints[1]) {
return this->lineTo(endPoint);
}
rx = std::abs(rx);
ry = std::abs(ry);
auto midPointDistance = (srcPoints[0] - srcPoints[1]) * 0.5f;

auto pointTransform = Matrix::MakeRotate(-xAxisRotate);
auto transformedMidPoint = Point::Zero();
pointTransform.mapPoints(&transformedMidPoint, &midPointDistance, 1);
auto squareRx = rx * rx;
auto squareRy = ry * ry;
auto squareX = transformedMidPoint.x * transformedMidPoint.x;
auto squareY = transformedMidPoint.y * transformedMidPoint.y;

// Check if the radii are big enough to draw the arc, scale radii if not.
// http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
auto radiiScale = squareX / squareRx + squareY / squareRy;
if (radiiScale > 1) {
radiiScale = std::sqrt(radiiScale);
rx *= radiiScale;
ry *= radiiScale;
}

pointTransform.setScale(1.0f / rx, 1.0f / ry);
pointTransform.preRotate(-xAxisRotate);

std::array<Point, 2> unitPoints;
pointTransform.mapPoints(unitPoints.data(), srcPoints.data(), unitPoints.size());
auto delta = unitPoints[1] - unitPoints[0];

auto d = delta.x * delta.x + delta.y * delta.y;
auto scaleFactorSquared = std::max(1 / d - 0.25f, 0.f);

auto scaleFactor = std::sqrt(scaleFactorSquared);
if (reversed != static_cast<bool>(largeArc)) { // flipped from the original implementation
scaleFactor = -scaleFactor;
}
delta *= scaleFactor;
auto centerPoint = unitPoints[0] + unitPoints[1];
centerPoint *= 0.5f;
centerPoint.offset(-delta.y, delta.x);
unitPoints[0] -= centerPoint;
unitPoints[1] -= centerPoint;
auto theta1 = std::atan2(unitPoints[0].y, unitPoints[0].x);
auto theta2 = std::atan2(unitPoints[1].y, unitPoints[1].x);
auto thetaArc = theta2 - theta1;
if (thetaArc < 0 && !reversed) { // arcSweep flipped from the original implementation
thetaArc += M_PI_F * 2.0f;
} else if (thetaArc > 0 && reversed) { // arcSweep flipped from the original implementation
thetaArc -= M_PI_F * 2.0f;
}

// Very tiny angles cause our subsequent math to go wonky
// but a larger value is probably ok too.
if (std::abs(thetaArc) < (M_PI_F / (1000 * 1000))) {
return this->lineTo(endPoint);
}

pointTransform.setRotate(xAxisRotate);
pointTransform.preScale(rx, ry);

// the arc may be slightly bigger than 1/4 circle, so allow up to 1/3rd
auto segments = std::ceil(std::abs(thetaArc / (2 * M_PI_F / 3)));
auto thetaWidth = thetaArc / segments;
auto t = std::tan(0.5f * thetaWidth);
if (!FloatsAreFinite(&t, 1)) {
return;
}
auto startTheta = theta1;
auto conicW = std::sqrt(0.5f + std::cos(thetaWidth) * 0.5f);
auto float_is_integer = [](float scalar) -> bool { return scalar == std::floor(scalar); };
auto expectIntegers = FloatNearlyZero(M_PI_F * 0.5f - std::abs(thetaWidth)) &&
float_is_integer(rx) && float_is_integer(ry) &&
float_is_integer(endPoint.x) && float_is_integer(endPoint.y);

auto* path = &(writableRef()->path);
for (int i = 0; i < static_cast<int>(segments); ++i) {
auto endTheta = startTheta + thetaWidth;
auto sinEndTheta = SinSnapToZero(endTheta);
auto cosEndTheta = CosSnapToZero(endTheta);

unitPoints[1].set(cosEndTheta, sinEndTheta);
unitPoints[1] += centerPoint;
unitPoints[0] = unitPoints[1];
unitPoints[0].offset(t * sinEndTheta, -t * cosEndTheta);
std::array<Point, 2> mapped;
pointTransform.mapPoints(mapped.data(), unitPoints.data(), unitPoints.size());

// Computing the arc width introduces rounding errors that cause arcs to start outside their
// marks.A round rect may lose convexity as a result.If the input values are on integers,
// place the conic on integers as well.
if (expectIntegers) {
for (auto& point : mapped) {
point.x = std::round(point.x);
point.y = std::round(point.y);
}
}
path->conicTo(mapped[0].x, mapped[0].y, mapped[1].x, mapped[1].y, conicW);
startTheta = endTheta;
}

// The final point should match the input point (by definition); replace it to
// ensure that rounding errors in the above math don't cause any problems.
path->setLastPt(endPoint.x, endPoint.y);
}

void Path::close() {
writableRef()->path.close();
}
Expand Down Expand Up @@ -415,4 +542,17 @@ int Path::countPoints() const {
int Path::countVerbs() const {
return pathRef->path.countVerbs();
}

bool Path::getLastPoint(Point* lastPoint) const {
if (!lastPoint) {
return false;
}
auto skPoint = SkPoint::Make(0, 0);
if (pathRef->path.getLastPt(&skPoint)) {
lastPoint->set(skPoint.fX, skPoint.fY);
return true;
}
return false;
};

} // namespace tgfx

0 comments on commit b4a1231

Please sign in to comment.