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