From 7c5b6b0e23a6f26f512248f4a1b60c5a179bbf00 Mon Sep 17 00:00:00 2001 From: Jakub Valtar Date: Mon, 28 Jul 2014 02:30:57 +0200 Subject: [PATCH] Implemented elliptical arcs for PShapeSVG Feature request #169 --- core/src/processing/core/PShapeSVG.java | 123 +++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/core/src/processing/core/PShapeSVG.java b/core/src/processing/core/PShapeSVG.java index 7bac4fb62..aa18b6ed0 100644 --- a/core/src/processing/core/PShapeSVG.java +++ b/core/src/processing/core/PShapeSVG.java @@ -518,7 +518,7 @@ public class PShapeSVG extends PShape { c == 'S' || c == 's' || c == 'Q' || c == 'q' || // quadratic beziers c == 'T' || c == 't' || -// c == 'A' || c == 'a' || // elliptical arc + c == 'A' || c == 'a' || // elliptical arc c == 'Z' || c == 'z' || // closepath c == ',') { separate = true; @@ -816,6 +816,40 @@ public class PShapeSVG extends PShape { } break; + // A - elliptical arc to (absolute) + case 'A': { + float rx = PApplet.parseFloat(pathTokens[i + 1]); + float ry = PApplet.parseFloat(pathTokens[i + 2]); + float angle = PApplet.parseFloat(pathTokens[i + 3]); + boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0; + boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; + float endX = PApplet.parseFloat(pathTokens[i + 6]); + float endY = PApplet.parseFloat(pathTokens[i + 7]); + parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY); + cx = endX; + cy = endY; + i += 8; + prevCurve = true; + } + break; + + // a - elliptical arc to (relative) + case 'a': { + float rx = PApplet.parseFloat(pathTokens[i + 1]); + float ry = PApplet.parseFloat(pathTokens[i + 2]); + float angle = PApplet.parseFloat(pathTokens[i + 3]); + boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0; + boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; + float endX = cx + PApplet.parseFloat(pathTokens[i + 6]); + float endY = cy + PApplet.parseFloat(pathTokens[i + 7]); + parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY); + cx = endX; + cy = endY; + i += 8; + prevCurve = true; + } + break; + case 'Z': case 'z': // since closing the path, the 'current' point needs @@ -924,6 +958,93 @@ public class PShapeSVG extends PShape { } + // Approximates elliptical arc by several bezier segments. + // Meets SVG standard requirements from: + // http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands + // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + // Based on arc to bezier curve equations from: + // http://www.spaceroots.org/documents/ellipse/node22.html + private void parsePathArcto(float x1, float y1, + float rx, float ry, + float angle, + boolean fa, boolean fs, + float x2, float y2) { + if (x1 == x2 && y1 == y2) return; + if (rx == 0 || ry == 0) { parsePathLineto(x2, y2); return; } + + rx = PApplet.abs(rx); ry = PApplet.abs(ry); + + float phi = PApplet.radians(((angle % 360) + 360) % 360); + float cosPhi = PApplet.cos(phi), sinPhi = PApplet.sin(phi); + + float x1r = ( cosPhi * (x1 - x2) + sinPhi * (y1 - y2)) / 2; + float y1r = (-sinPhi * (x1 - x2) + cosPhi * (y1 - y2)) / 2; + + float cxr, cyr; + { + float A = (x1r*x1r) / (rx*rx) + (y1r*y1r) / (ry*ry); + if (A > 1) { + // No solution, scale ellipse up according to SVG standard + float sqrtA = PApplet.sqrt(A); + rx *= sqrtA; cxr = 0; + ry *= sqrtA; cyr = 0; + } else { + float k = ((fa == fs) ? -1f : 1f) * + PApplet.sqrt((rx*rx * ry*ry) / ((rx*rx * y1r*y1r) + (ry*ry * x1r*x1r)) - 1f); + cxr = k * rx * y1r / ry; + cyr = -k * ry * x1r / rx; + } + } + + float cx = cosPhi * cxr - sinPhi * cyr + (x1 + x2) / 2; + float cy = sinPhi * cxr + cosPhi * cyr + (y1 + y2) / 2; + + float phi1, phiDelta; + { + float sx = ( x1r - cxr) / rx, sy = ( y1r - cyr) / ry; + float tx = (-x1r - cxr) / rx, ty = (-y1r - cyr) / ry; + phi1 = PApplet.atan2(sy, sx); + phiDelta = (((PApplet.atan2(ty, tx) - phi1) % TWO_PI) + TWO_PI) % TWO_PI; + if (!fs) phiDelta -= TWO_PI; + } + + // One segment can not cover more that PI, less than PI/2 is + // recommended to avoid visible inaccuracies caused by rounding errors + int segmentCount = PApplet.ceil(PApplet.abs(phiDelta) / TWO_PI * 4); + + float inc = phiDelta / segmentCount; + float a = PApplet.sin(inc) * + (PApplet.sqrt(4 + 3 * PApplet.sq(PApplet.tan(inc / 2))) - 1) / 3; + + float sinPhi1 = PApplet.sin(phi1), cosPhi1 = PApplet.cos(phi1); + + float p1x = x1; + float p1y = y1; + float relq1x = a * (-rx * cosPhi * sinPhi1 - ry * sinPhi * cosPhi1); + float relq1y = a * (-rx * sinPhi * sinPhi1 + ry * cosPhi * cosPhi1); + + for (int i = 0; i < segmentCount; i++) { + float eta = phi1 + (i + 1) * inc; + float sinEta = PApplet.sin(eta), cosEta = PApplet.cos(eta); + + float p2x = cx + rx * cosPhi * cosEta - ry * sinPhi * sinEta; + float p2y = cy + rx * sinPhi * cosEta + ry * cosPhi * sinEta; + float relq2x = a * (-rx * cosPhi * sinEta - ry * sinPhi * cosEta); + float relq2y = a * (-rx * sinPhi * sinEta + ry * cosPhi * cosEta); + + if (i == segmentCount - 1) { p2x = x2; p2y = y2; } + + parsePathCode(BEZIER_VERTEX); + parsePathVertex(p1x + relq1x, p1y + relq1y); + parsePathVertex(p2x - relq2x, p2y - relq2y); + parsePathVertex(p2x, p2y); + + p1x = p2x; relq1x = relq2x; + p1y = p2y; relq1y = relq2y; + } + } + + /** * Parse the specified SVG matrix into a PMatrix2D. Note that PMatrix2D * is rotated relative to the SVG definition, so parameters are rearranged