1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 
15 package android.util;
16 
17 import android.graphics.Path;
18 import android.util.Log;
19 
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 
23 /**
24  * @hide
25  */
26 public class PathParser {
27     static final String LOGTAG = PathParser.class.getSimpleName();
28 
29     /**
30      * @param pathData The string representing a path, the same as "d" string in svg file.
31      * @return the generated Path object.
32      */
createPathFromPathData(String pathData)33     public static Path createPathFromPathData(String pathData) {
34         Path path = new Path();
35         PathDataNode[] nodes = createNodesFromPathData(pathData);
36         if (nodes != null) {
37             try {
38                 PathDataNode.nodesToPath(nodes, path);
39             } catch (RuntimeException e) {
40                 throw new RuntimeException("Error in parsing " + pathData, e);
41             }
42             return path;
43         }
44         return null;
45     }
46 
47     /**
48      * @param pathData The string representing a path, the same as "d" string in svg file.
49      * @return an array of the PathDataNode.
50      */
createNodesFromPathData(String pathData)51     public static PathDataNode[] createNodesFromPathData(String pathData) {
52         if (pathData == null) {
53             return null;
54         }
55         int start = 0;
56         int end = 1;
57 
58         ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
59         while (end < pathData.length()) {
60             end = nextStart(pathData, end);
61             String s = pathData.substring(start, end).trim();
62             if (s.length() > 0) {
63                 float[] val = getFloats(s);
64                 addNode(list, s.charAt(0), val);
65             }
66 
67             start = end;
68             end++;
69         }
70         if ((end - start) == 1 && start < pathData.length()) {
71             addNode(list, pathData.charAt(start), new float[0]);
72         }
73         return list.toArray(new PathDataNode[list.size()]);
74     }
75 
76     /**
77      * @param source The array of PathDataNode to be duplicated.
78      * @return a deep copy of the <code>source</code>.
79      */
deepCopyNodes(PathDataNode[] source)80     public static PathDataNode[] deepCopyNodes(PathDataNode[] source) {
81         if (source == null) {
82             return null;
83         }
84         PathDataNode[] copy = new PathParser.PathDataNode[source.length];
85         for (int i = 0; i < source.length; i ++) {
86             copy[i] = new PathDataNode(source[i]);
87         }
88         return copy;
89     }
90 
91     /**
92      * @param nodesFrom The source path represented in an array of PathDataNode
93      * @param nodesTo The target path represented in an array of PathDataNode
94      * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
95      */
canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo)96     public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
97         if (nodesFrom == null || nodesTo == null) {
98             return false;
99         }
100 
101         if (nodesFrom.length != nodesTo.length) {
102             return false;
103         }
104 
105         for (int i = 0; i < nodesFrom.length; i ++) {
106             if (nodesFrom[i].mType != nodesTo[i].mType
107                     || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) {
108                 return false;
109             }
110         }
111         return true;
112     }
113 
114     /**
115      * Update the target's data to match the source.
116      * Before calling this, make sure canMorph(target, source) is true.
117      *
118      * @param target The target path represented in an array of PathDataNode
119      * @param source The source path represented in an array of PathDataNode
120      */
updateNodes(PathDataNode[] target, PathDataNode[] source)121     public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
122         for (int i = 0; i < source.length; i ++) {
123             target[i].mType = source[i].mType;
124             for (int j = 0; j < source[i].mParams.length; j ++) {
125                 target[i].mParams[j] = source[i].mParams[j];
126             }
127         }
128     }
129 
nextStart(String s, int end)130     private static int nextStart(String s, int end) {
131         char c;
132 
133         while (end < s.length()) {
134             c = s.charAt(end);
135             // Note that 'e' or 'E' are not valid path commands, but could be
136             // used for floating point numbers' scientific notation.
137             // Therefore, when searching for next command, we should ignore 'e'
138             // and 'E'.
139             if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
140                     && c != 'e' && c != 'E') {
141                 return end;
142             }
143             end++;
144         }
145         return end;
146     }
147 
addNode(ArrayList<PathDataNode> list, char cmd, float[] val)148     private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
149         list.add(new PathDataNode(cmd, val));
150     }
151 
152     private static class ExtractFloatResult {
153         // We need to return the position of the next separator and whether the
154         // next float starts with a '-' or a '.'.
155         int mEndPosition;
156         boolean mEndWithNegOrDot;
157     }
158 
159     /**
160      * Parse the floats in the string.
161      * This is an optimized version of parseFloat(s.split(",|\\s"));
162      *
163      * @param s the string containing a command and list of floats
164      * @return array of floats
165      */
getFloats(String s)166     private static float[] getFloats(String s) {
167         if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
168             return new float[0];
169         }
170         try {
171             float[] results = new float[s.length()];
172             int count = 0;
173             int startPosition = 1;
174             int endPosition = 0;
175 
176             ExtractFloatResult result = new ExtractFloatResult();
177             int totalLength = s.length();
178 
179             // The startPosition should always be the first character of the
180             // current number, and endPosition is the character after the current
181             // number.
182             while (startPosition < totalLength) {
183                 extract(s, startPosition, result);
184                 endPosition = result.mEndPosition;
185 
186                 if (startPosition < endPosition) {
187                     results[count++] = Float.parseFloat(
188                             s.substring(startPosition, endPosition));
189                 }
190 
191                 if (result.mEndWithNegOrDot) {
192                     // Keep the '-' or '.' sign with next number.
193                     startPosition = endPosition;
194                 } else {
195                     startPosition = endPosition + 1;
196                 }
197             }
198             return Arrays.copyOf(results, count);
199         } catch (NumberFormatException e) {
200             throw new RuntimeException("error in parsing \"" + s + "\"", e);
201         }
202     }
203 
204     /**
205      * Calculate the position of the next comma or space or negative sign
206      * @param s the string to search
207      * @param start the position to start searching
208      * @param result the result of the extraction, including the position of the
209      * the starting position of next number, whether it is ending with a '-'.
210      */
extract(String s, int start, ExtractFloatResult result)211     private static void extract(String s, int start, ExtractFloatResult result) {
212         // Now looking for ' ', ',', '.' or '-' from the start.
213         int currentIndex = start;
214         boolean foundSeparator = false;
215         result.mEndWithNegOrDot = false;
216         boolean secondDot = false;
217         boolean isExponential = false;
218         for (; currentIndex < s.length(); currentIndex++) {
219             boolean isPrevExponential = isExponential;
220             isExponential = false;
221             char currentChar = s.charAt(currentIndex);
222             switch (currentChar) {
223                 case ' ':
224                 case ',':
225                     foundSeparator = true;
226                     break;
227                 case '-':
228                     // The negative sign following a 'e' or 'E' is not a separator.
229                     if (currentIndex != start && !isPrevExponential) {
230                         foundSeparator = true;
231                         result.mEndWithNegOrDot = true;
232                     }
233                     break;
234                 case '.':
235                     if (!secondDot) {
236                         secondDot = true;
237                     } else {
238                         // This is the second dot, and it is considered as a separator.
239                         foundSeparator = true;
240                         result.mEndWithNegOrDot = true;
241                     }
242                     break;
243                 case 'e':
244                 case 'E':
245                     isExponential = true;
246                     break;
247             }
248             if (foundSeparator) {
249                 break;
250             }
251         }
252         // When there is nothing found, then we put the end position to the end
253         // of the string.
254         result.mEndPosition = currentIndex;
255     }
256 
257     /**
258      * Each PathDataNode represents one command in the "d" attribute of the svg
259      * file.
260      * An array of PathDataNode can represent the whole "d" attribute.
261      */
262     public static class PathDataNode {
263         private char mType;
264         private float[] mParams;
265 
PathDataNode(char type, float[] params)266         private PathDataNode(char type, float[] params) {
267             mType = type;
268             mParams = params;
269         }
270 
PathDataNode(PathDataNode n)271         private PathDataNode(PathDataNode n) {
272             mType = n.mType;
273             mParams = Arrays.copyOf(n.mParams, n.mParams.length);
274         }
275 
276         /**
277          * Convert an array of PathDataNode to Path.
278          *
279          * @param node The source array of PathDataNode.
280          * @param path The target Path object.
281          */
nodesToPath(PathDataNode[] node, Path path)282         public static void nodesToPath(PathDataNode[] node, Path path) {
283             float[] current = new float[6];
284             char previousCommand = 'm';
285             for (int i = 0; i < node.length; i++) {
286                 addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
287                 previousCommand = node[i].mType;
288             }
289         }
290 
291         /**
292          * The current PathDataNode will be interpolated between the
293          * <code>nodeFrom</code> and <code>nodeTo</code> according to the
294          * <code>fraction</code>.
295          *
296          * @param nodeFrom The start value as a PathDataNode.
297          * @param nodeTo The end value as a PathDataNode
298          * @param fraction The fraction to interpolate.
299          */
interpolatePathDataNode(PathDataNode nodeFrom, PathDataNode nodeTo, float fraction)300         public void interpolatePathDataNode(PathDataNode nodeFrom,
301                 PathDataNode nodeTo, float fraction) {
302             for (int i = 0; i < nodeFrom.mParams.length; i++) {
303                 mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
304                         + nodeTo.mParams[i] * fraction;
305             }
306         }
307 
addCommand(Path path, float[] current, char previousCmd, char cmd, float[] val)308         private static void addCommand(Path path, float[] current,
309                 char previousCmd, char cmd, float[] val) {
310 
311             int incr = 2;
312             float currentX = current[0];
313             float currentY = current[1];
314             float ctrlPointX = current[2];
315             float ctrlPointY = current[3];
316             float currentSegmentStartX = current[4];
317             float currentSegmentStartY = current[5];
318             float reflectiveCtrlPointX;
319             float reflectiveCtrlPointY;
320 
321             switch (cmd) {
322                 case 'z':
323                 case 'Z':
324                     path.close();
325                     // Path is closed here, but we need to move the pen to the
326                     // closed position. So we cache the segment's starting position,
327                     // and restore it here.
328                     currentX = currentSegmentStartX;
329                     currentY = currentSegmentStartY;
330                     ctrlPointX = currentSegmentStartX;
331                     ctrlPointY = currentSegmentStartY;
332                     path.moveTo(currentX, currentY);
333                     break;
334                 case 'm':
335                 case 'M':
336                 case 'l':
337                 case 'L':
338                 case 't':
339                 case 'T':
340                     incr = 2;
341                     break;
342                 case 'h':
343                 case 'H':
344                 case 'v':
345                 case 'V':
346                     incr = 1;
347                     break;
348                 case 'c':
349                 case 'C':
350                     incr = 6;
351                     break;
352                 case 's':
353                 case 'S':
354                 case 'q':
355                 case 'Q':
356                     incr = 4;
357                     break;
358                 case 'a':
359                 case 'A':
360                     incr = 7;
361                     break;
362             }
363 
364             for (int k = 0; k < val.length; k += incr) {
365                 switch (cmd) {
366                     case 'm': // moveto - Start a new sub-path (relative)
367                         path.rMoveTo(val[k + 0], val[k + 1]);
368                         currentX += val[k + 0];
369                         currentY += val[k + 1];
370                         currentSegmentStartX = currentX;
371                         currentSegmentStartY = currentY;
372                         break;
373                     case 'M': // moveto - Start a new sub-path
374                         path.moveTo(val[k + 0], val[k + 1]);
375                         currentX = val[k + 0];
376                         currentY = val[k + 1];
377                         currentSegmentStartX = currentX;
378                         currentSegmentStartY = currentY;
379                         break;
380                     case 'l': // lineto - Draw a line from the current point (relative)
381                         path.rLineTo(val[k + 0], val[k + 1]);
382                         currentX += val[k + 0];
383                         currentY += val[k + 1];
384                         break;
385                     case 'L': // lineto - Draw a line from the current point
386                         path.lineTo(val[k + 0], val[k + 1]);
387                         currentX = val[k + 0];
388                         currentY = val[k + 1];
389                         break;
390                     case 'h': // horizontal lineto - Draws a horizontal line (relative)
391                         path.rLineTo(val[k + 0], 0);
392                         currentX += val[k + 0];
393                         break;
394                     case 'H': // horizontal lineto - Draws a horizontal line
395                         path.lineTo(val[k + 0], currentY);
396                         currentX = val[k + 0];
397                         break;
398                     case 'v': // vertical lineto - Draws a vertical line from the current point (r)
399                         path.rLineTo(0, val[k + 0]);
400                         currentY += val[k + 0];
401                         break;
402                     case 'V': // vertical lineto - Draws a vertical line from the current point
403                         path.lineTo(currentX, val[k + 0]);
404                         currentY = val[k + 0];
405                         break;
406                     case 'c': // curveto - Draws a cubic Bézier curve (relative)
407                         path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
408                                 val[k + 4], val[k + 5]);
409 
410                         ctrlPointX = currentX + val[k + 2];
411                         ctrlPointY = currentY + val[k + 3];
412                         currentX += val[k + 4];
413                         currentY += val[k + 5];
414 
415                         break;
416                     case 'C': // curveto - Draws a cubic Bézier curve
417                         path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
418                                 val[k + 4], val[k + 5]);
419                         currentX = val[k + 4];
420                         currentY = val[k + 5];
421                         ctrlPointX = val[k + 2];
422                         ctrlPointY = val[k + 3];
423                         break;
424                     case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
425                         reflectiveCtrlPointX = 0;
426                         reflectiveCtrlPointY = 0;
427                         if (previousCmd == 'c' || previousCmd == 's'
428                                 || previousCmd == 'C' || previousCmd == 'S') {
429                             reflectiveCtrlPointX = currentX - ctrlPointX;
430                             reflectiveCtrlPointY = currentY - ctrlPointY;
431                         }
432                         path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
433                                 val[k + 0], val[k + 1],
434                                 val[k + 2], val[k + 3]);
435 
436                         ctrlPointX = currentX + val[k + 0];
437                         ctrlPointY = currentY + val[k + 1];
438                         currentX += val[k + 2];
439                         currentY += val[k + 3];
440                         break;
441                     case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
442                         reflectiveCtrlPointX = currentX;
443                         reflectiveCtrlPointY = currentY;
444                         if (previousCmd == 'c' || previousCmd == 's'
445                                 || previousCmd == 'C' || previousCmd == 'S') {
446                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
447                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
448                         }
449                         path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
450                                 val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
451                         ctrlPointX = val[k + 0];
452                         ctrlPointY = val[k + 1];
453                         currentX = val[k + 2];
454                         currentY = val[k + 3];
455                         break;
456                     case 'q': // Draws a quadratic Bézier (relative)
457                         path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
458                         ctrlPointX = currentX + val[k + 0];
459                         ctrlPointY = currentY + val[k + 1];
460                         currentX += val[k + 2];
461                         currentY += val[k + 3];
462                         break;
463                     case 'Q': // Draws a quadratic Bézier
464                         path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
465                         ctrlPointX = val[k + 0];
466                         ctrlPointY = val[k + 1];
467                         currentX = val[k + 2];
468                         currentY = val[k + 3];
469                         break;
470                     case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
471                         reflectiveCtrlPointX = 0;
472                         reflectiveCtrlPointY = 0;
473                         if (previousCmd == 'q' || previousCmd == 't'
474                                 || previousCmd == 'Q' || previousCmd == 'T') {
475                             reflectiveCtrlPointX = currentX - ctrlPointX;
476                             reflectiveCtrlPointY = currentY - ctrlPointY;
477                         }
478                         path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
479                                 val[k + 0], val[k + 1]);
480                         ctrlPointX = currentX + reflectiveCtrlPointX;
481                         ctrlPointY = currentY + reflectiveCtrlPointY;
482                         currentX += val[k + 0];
483                         currentY += val[k + 1];
484                         break;
485                     case 'T': // Draws a quadratic Bézier curve (reflective control point)
486                         reflectiveCtrlPointX = currentX;
487                         reflectiveCtrlPointY = currentY;
488                         if (previousCmd == 'q' || previousCmd == 't'
489                                 || previousCmd == 'Q' || previousCmd == 'T') {
490                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
491                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
492                         }
493                         path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
494                                 val[k + 0], val[k + 1]);
495                         ctrlPointX = reflectiveCtrlPointX;
496                         ctrlPointY = reflectiveCtrlPointY;
497                         currentX = val[k + 0];
498                         currentY = val[k + 1];
499                         break;
500                     case 'a': // Draws an elliptical arc
501                         // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
502                         drawArc(path,
503                                 currentX,
504                                 currentY,
505                                 val[k + 5] + currentX,
506                                 val[k + 6] + currentY,
507                                 val[k + 0],
508                                 val[k + 1],
509                                 val[k + 2],
510                                 val[k + 3] != 0,
511                                 val[k + 4] != 0);
512                         currentX += val[k + 5];
513                         currentY += val[k + 6];
514                         ctrlPointX = currentX;
515                         ctrlPointY = currentY;
516                         break;
517                     case 'A': // Draws an elliptical arc
518                         drawArc(path,
519                                 currentX,
520                                 currentY,
521                                 val[k + 5],
522                                 val[k + 6],
523                                 val[k + 0],
524                                 val[k + 1],
525                                 val[k + 2],
526                                 val[k + 3] != 0,
527                                 val[k + 4] != 0);
528                         currentX = val[k + 5];
529                         currentY = val[k + 6];
530                         ctrlPointX = currentX;
531                         ctrlPointY = currentY;
532                         break;
533                 }
534                 previousCmd = cmd;
535             }
536             current[0] = currentX;
537             current[1] = currentY;
538             current[2] = ctrlPointX;
539             current[3] = ctrlPointY;
540             current[4] = currentSegmentStartX;
541             current[5] = currentSegmentStartY;
542         }
543 
drawArc(Path p, float x0, float y0, float x1, float y1, float a, float b, float theta, boolean isMoreThanHalf, boolean isPositiveArc)544         private static void drawArc(Path p,
545                 float x0,
546                 float y0,
547                 float x1,
548                 float y1,
549                 float a,
550                 float b,
551                 float theta,
552                 boolean isMoreThanHalf,
553                 boolean isPositiveArc) {
554 
555             /* Convert rotation angle from degrees to radians */
556             double thetaD = Math.toRadians(theta);
557             /* Pre-compute rotation matrix entries */
558             double cosTheta = Math.cos(thetaD);
559             double sinTheta = Math.sin(thetaD);
560             /* Transform (x0, y0) and (x1, y1) into unit space */
561             /* using (inverse) rotation, followed by (inverse) scale */
562             double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
563             double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
564             double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
565             double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
566 
567             /* Compute differences and averages */
568             double dx = x0p - x1p;
569             double dy = y0p - y1p;
570             double xm = (x0p + x1p) / 2;
571             double ym = (y0p + y1p) / 2;
572             /* Solve for intersecting unit circles */
573             double dsq = dx * dx + dy * dy;
574             if (dsq == 0.0) {
575                 Log.w(LOGTAG, " Points are coincident");
576                 return; /* Points are coincident */
577             }
578             double disc = 1.0 / dsq - 1.0 / 4.0;
579             if (disc < 0.0) {
580                 Log.w(LOGTAG, "Points are too far apart " + dsq);
581                 float adjust = (float) (Math.sqrt(dsq) / 1.99999);
582                 drawArc(p, x0, y0, x1, y1, a * adjust,
583                         b * adjust, theta, isMoreThanHalf, isPositiveArc);
584                 return; /* Points are too far apart */
585             }
586             double s = Math.sqrt(disc);
587             double sdx = s * dx;
588             double sdy = s * dy;
589             double cx;
590             double cy;
591             if (isMoreThanHalf == isPositiveArc) {
592                 cx = xm - sdy;
593                 cy = ym + sdx;
594             } else {
595                 cx = xm + sdy;
596                 cy = ym - sdx;
597             }
598 
599             double eta0 = Math.atan2((y0p - cy), (x0p - cx));
600 
601             double eta1 = Math.atan2((y1p - cy), (x1p - cx));
602 
603             double sweep = (eta1 - eta0);
604             if (isPositiveArc != (sweep >= 0)) {
605                 if (sweep > 0) {
606                     sweep -= 2 * Math.PI;
607                 } else {
608                     sweep += 2 * Math.PI;
609                 }
610             }
611 
612             cx *= a;
613             cy *= b;
614             double tcx = cx;
615             cx = cx * cosTheta - cy * sinTheta;
616             cy = tcx * sinTheta + cy * cosTheta;
617 
618             arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
619         }
620 
621         /**
622          * Converts an arc to cubic Bezier segments and records them in p.
623          *
624          * @param p The target for the cubic Bezier segments
625          * @param cx The x coordinate center of the ellipse
626          * @param cy The y coordinate center of the ellipse
627          * @param a The radius of the ellipse in the horizontal direction
628          * @param b The radius of the ellipse in the vertical direction
629          * @param e1x E(eta1) x coordinate of the starting point of the arc
630          * @param e1y E(eta2) y coordinate of the starting point of the arc
631          * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
632          * @param start The start angle of the arc on the ellipse
633          * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
634          */
arcToBezier(Path p, double cx, double cy, double a, double b, double e1x, double e1y, double theta, double start, double sweep)635         private static void arcToBezier(Path p,
636                 double cx,
637                 double cy,
638                 double a,
639                 double b,
640                 double e1x,
641                 double e1y,
642                 double theta,
643                 double start,
644                 double sweep) {
645             // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
646             // and http://www.spaceroots.org/documents/ellipse/node22.html
647 
648             // Maximum of 45 degrees per cubic Bezier segment
649             int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
650 
651             double eta1 = start;
652             double cosTheta = Math.cos(theta);
653             double sinTheta = Math.sin(theta);
654             double cosEta1 = Math.cos(eta1);
655             double sinEta1 = Math.sin(eta1);
656             double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
657             double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
658 
659             double anglePerSegment = sweep / numSegments;
660             for (int i = 0; i < numSegments; i++) {
661                 double eta2 = eta1 + anglePerSegment;
662                 double sinEta2 = Math.sin(eta2);
663                 double cosEta2 = Math.cos(eta2);
664                 double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
665                 double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
666                 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
667                 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
668                 double tanDiff2 = Math.tan((eta2 - eta1) / 2);
669                 double alpha =
670                         Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
671                 double q1x = e1x + alpha * ep1x;
672                 double q1y = e1y + alpha * ep1y;
673                 double q2x = e2x - alpha * ep2x;
674                 double q2y = e2y - alpha * ep2y;
675 
676                 p.cubicTo((float) q1x,
677                         (float) q1y,
678                         (float) q2x,
679                         (float) q2y,
680                         (float) e2x,
681                         (float) e2y);
682                 eta1 = eta2;
683                 e1x = e2x;
684                 e1y = e2y;
685                 ep1x = ep2x;
686                 ep1y = ep2y;
687             }
688         }
689     }
690 }
691