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