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