PathParser.java revision 5e7a29f6774f0672a51761297e5c6dbdbc8f794d
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
15package android.util;
16
17import android.graphics.Path;
18import android.util.Log;
19
20import java.util.ArrayList;
21import java.util.Arrays;
22
23/**
24 * @hide
25 */
26public 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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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
266        private PathDataNode(char type, float[] params) {
267            mType = type;
268            mParams = params;
269        }
270
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         */
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         */
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
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
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         */
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