Skip to content

Commit 49ffb16

Browse files
committed
Implemented elliptical arcs for PShapeSVG
Feature request #169
1 parent 8daa225 commit 49ffb16

1 file changed

Lines changed: 122 additions & 1 deletion

File tree

core/src/processing/core/PShapeSVG.java

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ protected void parsePath() {
518518
c == 'S' || c == 's' ||
519519
c == 'Q' || c == 'q' || // quadratic beziers
520520
c == 'T' || c == 't' ||
521-
// c == 'A' || c == 'a' || // elliptical arc
521+
c == 'A' || c == 'a' || // elliptical arc
522522
c == 'Z' || c == 'z' || // closepath
523523
c == ',') {
524524
separate = true;
@@ -816,6 +816,40 @@ protected void parsePath() {
816816
}
817817
break;
818818

819+
// A - elliptical arc to (absolute)
820+
case 'A': {
821+
float rx = PApplet.parseFloat(pathTokens[i + 1]);
822+
float ry = PApplet.parseFloat(pathTokens[i + 2]);
823+
float angle = PApplet.parseFloat(pathTokens[i + 3]);
824+
boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0;
825+
boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0;
826+
float endX = PApplet.parseFloat(pathTokens[i + 6]);
827+
float endY = PApplet.parseFloat(pathTokens[i + 7]);
828+
parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY);
829+
cx = endX;
830+
cy = endY;
831+
i += 8;
832+
prevCurve = true;
833+
}
834+
break;
835+
836+
// a - elliptical arc to (relative)
837+
case 'a': {
838+
float rx = PApplet.parseFloat(pathTokens[i + 1]);
839+
float ry = PApplet.parseFloat(pathTokens[i + 2]);
840+
float angle = PApplet.parseFloat(pathTokens[i + 3]);
841+
boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0;
842+
boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0;
843+
float endX = cx + PApplet.parseFloat(pathTokens[i + 6]);
844+
float endY = cy + PApplet.parseFloat(pathTokens[i + 7]);
845+
parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY);
846+
cx = endX;
847+
cy = endY;
848+
i += 8;
849+
prevCurve = true;
850+
}
851+
break;
852+
819853
case 'Z':
820854
case 'z':
821855
// since closing the path, the 'current' point needs
@@ -924,6 +958,93 @@ private void parsePathQuadto(float cx, float cy,
924958
}
925959

926960

961+
// Approximates elliptical arc by several bezier segments.
962+
// Meets SVG standard requirements from:
963+
// http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
964+
// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
965+
// Based on arc to bezier curve equations from:
966+
// http://www.spaceroots.org/documents/ellipse/node22.html
967+
private void parsePathArcto(float x1, float y1,
968+
float rx, float ry,
969+
float angle,
970+
boolean fa, boolean fs,
971+
float x2, float y2) {
972+
if (x1 == x2 && y1 == y2) return;
973+
if (rx == 0 || ry == 0) { parsePathLineto(x2, y2); return; }
974+
975+
rx = PApplet.abs(rx); ry = PApplet.abs(ry);
976+
977+
float phi = PApplet.radians(((angle % 360) + 360) % 360);
978+
float cosPhi = PApplet.cos(phi), sinPhi = PApplet.sin(phi);
979+
980+
float x1r = ( cosPhi * (x1 - x2) + sinPhi * (y1 - y2)) / 2;
981+
float y1r = (-sinPhi * (x1 - x2) + cosPhi * (y1 - y2)) / 2;
982+
983+
float cxr, cyr;
984+
{
985+
float A = (x1r*x1r) / (rx*rx) + (y1r*y1r) / (ry*ry);
986+
if (A > 1) {
987+
// No solution, scale ellipse up according to SVG standard
988+
float sqrtA = PApplet.sqrt(A);
989+
rx *= sqrtA; cxr = 0;
990+
ry *= sqrtA; cyr = 0;
991+
} else {
992+
float k = ((fa == fs) ? -1f : 1f) *
993+
PApplet.sqrt((rx*rx * ry*ry) / ((rx*rx * y1r*y1r) + (ry*ry * x1r*x1r)) - 1f);
994+
cxr = k * rx * y1r / ry;
995+
cyr = -k * ry * x1r / rx;
996+
}
997+
}
998+
999+
float cx = cosPhi * cxr - sinPhi * cyr + (x1 + x2) / 2;
1000+
float cy = sinPhi * cxr + cosPhi * cyr + (y1 + y2) / 2;
1001+
1002+
float phi1, phiDelta;
1003+
{
1004+
float sx = ( x1r - cxr) / rx, sy = ( y1r - cyr) / ry;
1005+
float tx = (-x1r - cxr) / rx, ty = (-y1r - cyr) / ry;
1006+
phi1 = PApplet.atan2(sy, sx);
1007+
phiDelta = (((PApplet.atan2(ty, tx) - phi1) % TWO_PI) + TWO_PI) % TWO_PI;
1008+
if (!fs) phiDelta -= TWO_PI;
1009+
}
1010+
1011+
// One segment can not cover more that PI, less than PI/2 is
1012+
// recommended to avoid visible inaccuracies caused by rounding errors
1013+
int segmentCount = PApplet.ceil(PApplet.abs(phiDelta) / TWO_PI * 4);
1014+
1015+
float inc = phiDelta / segmentCount;
1016+
float a = PApplet.sin(inc) *
1017+
(PApplet.sqrt(4 + 3 * PApplet.sq(PApplet.tan(inc / 2))) - 1) / 3;
1018+
1019+
float sinPhi1 = PApplet.sin(phi1), cosPhi1 = PApplet.cos(phi1);
1020+
1021+
float p1x = x1;
1022+
float p1y = y1;
1023+
float relq1x = a * (-rx * cosPhi * sinPhi1 - ry * sinPhi * cosPhi1);
1024+
float relq1y = a * (-rx * sinPhi * sinPhi1 + ry * cosPhi * cosPhi1);
1025+
1026+
for (int i = 0; i < segmentCount; i++) {
1027+
float eta = phi1 + (i + 1) * inc;
1028+
float sinEta = PApplet.sin(eta), cosEta = PApplet.cos(eta);
1029+
1030+
float p2x = cx + rx * cosPhi * cosEta - ry * sinPhi * sinEta;
1031+
float p2y = cy + rx * sinPhi * cosEta + ry * cosPhi * sinEta;
1032+
float relq2x = a * (-rx * cosPhi * sinEta - ry * sinPhi * cosEta);
1033+
float relq2y = a * (-rx * sinPhi * sinEta + ry * cosPhi * cosEta);
1034+
1035+
if (i == segmentCount - 1) { p2x = x2; p2y = y2; }
1036+
1037+
parsePathCode(BEZIER_VERTEX);
1038+
parsePathVertex(p1x + relq1x, p1y + relq1y);
1039+
parsePathVertex(p2x - relq2x, p2y - relq2y);
1040+
parsePathVertex(p2x, p2y);
1041+
1042+
p1x = p2x; relq1x = relq2x;
1043+
p1y = p2y; relq1y = relq2y;
1044+
}
1045+
}
1046+
1047+
9271048
/**
9281049
* Parse the specified SVG matrix into a PMatrix2D. Note that PMatrix2D
9291050
* is rotated relative to the SVG definition, so parameters are rearranged

0 commit comments

Comments
 (0)