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