1 /* 2 Copyright 2008-2024 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 31 32 Metapost/Hobby curves, see e.g. https://bosker.wordpress.com/2013/11/13/beyond-bezier-curves/ 33 34 * Ported to Python for the project PyX. Copyright (C) 2011 Michael Schindler <m-schindler@users.sourceforge.net> 35 * Ported to javascript from the PyX implementation (https://pyx-project.org/) by Vlad-X. 36 * Adapted to JSXGraph and some code changes by Alfred Wassermann 2020. 37 38 This program is distributed in the hope that it will be useful, 39 but WITHOUT ANY WARRANTY; without even the implied warranty of 40 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 41 GNU General Public License for more details. 42 43 You should have received a copy of the GNU General Public License 44 along with this program; if not, write to the Free Software 45 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 46 47 Internal functions of MetaPost 48 This file re-implements some of the functionality of MetaPost 49 (https://tug.org/metapost.html). MetaPost was developed by John D. Hobby and 50 others. The code of Metapost is in the public domain, which we understand as 51 an implicit permission to reuse the code here (see the comment at 52 https://www.gnu.org/licenses/license-list.html) 53 54 This file is based on the MetaPost version distributed by TeXLive: 55 svn://tug.org/texlive/trunk/Build/source/texk/web2c/mplibdir revision 22737 # 56 (2011-05-31) 57 */ 58 59 /*global JXG: true, define: true*/ 60 /*jslint nomen: true, plusplus: true*/ 61 62 /** 63 * @fileoverview In this file the namespace Math.Metapost is defined which holds algorithms translated from Metapost 64 * by D.E. Knuth and J.D. Hobby. 65 */ 66 67 import Type from "../utils/type.js"; 68 import Mat from "./math.js"; 69 70 /** 71 * The JXG.Math.Metapost namespace holds algorithms translated from Metapost 72 * by D.E. Knuth and J.D. Hobby. 73 * 74 * @name JXG.Math.Metapost 75 * @exports Mat.Metapost as JXG.Math.Metapost 76 * @namespace 77 */ 78 Mat.Metapost = { 79 MP_ENDPOINT: 0, 80 MP_EXPLICIT: 1, 81 MP_GIVEN: 2, 82 MP_CURL: 3, 83 MP_OPEN: 4, 84 MP_END_CYCLE: 5, 85 86 UNITY: 1.0, 87 // two: 2, 88 // fraction_half: 0.5, 89 FRACTION_ONE: 1.0, 90 FRACTION_THREE: 3.0, 91 ONE_EIGHTY_DEG: Math.PI, 92 THREE_SIXTY_DEG: 2 * Math.PI, 93 // EPSILON: 1e-5, 94 EPS_SQ: 1e-5 * 1e-5, 95 96 /** 97 * @private 98 */ 99 make_choices: function (knots) { 100 var dely, h, k, delx, n, q, p, s, cosine, t, sine, delta_x, delta_y, delta, psi, 101 endless = true; 102 103 p = knots[0]; 104 do { 105 if (!p) { 106 break; 107 } 108 q = p.next; 109 110 // Join two identical knots by setting the control points to the same 111 // coordinates. 112 // MP 291 113 if ( 114 p.rtype > this.MP_EXPLICIT && 115 (p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y) < this.EPS_SQ 116 ) { 117 p.rtype = this.MP_EXPLICIT; 118 if (p.ltype === this.MP_OPEN) { 119 p.ltype = this.MP_CURL; 120 p.set_left_curl(this.UNITY); 121 } 122 123 q.ltype = this.MP_EXPLICIT; 124 if (q.rtype === this.MP_OPEN) { 125 q.rtype = this.MP_CURL; 126 q.set_right_curl(this.UNITY); 127 } 128 129 p.rx = p.x; 130 q.lx = p.x; 131 p.ry = p.y; 132 q.ly = p.y; 133 } 134 p = q; 135 } while (p !== knots[0]); 136 137 // Find the first breakpoint, h, on the path 138 // MP 292 139 h = knots[0]; 140 while (endless) { 141 if (h.ltype !== this.MP_OPEN || h.rtype !== this.MP_OPEN) { 142 break; 143 } 144 h = h.next; 145 if (h === knots[0]) { 146 h.ltype = this.MP_END_CYCLE; 147 break; 148 } 149 } 150 151 p = h; 152 while (endless) { 153 if (!p) { 154 break; 155 } 156 157 // Fill in the control points between p and the next breakpoint, 158 // then advance p to that breakpoint 159 // MP 299 160 q = p.next; 161 if (p.rtype >= this.MP_GIVEN) { 162 while (q.ltype === this.MP_OPEN && q.rtype === this.MP_OPEN) { 163 q = q.next; 164 } 165 166 // Calculate the turning angles psi_ k and the distances d_{k,k+1}; 167 // set n to the length of the path 168 // MP 302 169 k = 0; 170 s = p; 171 n = knots.length; 172 173 delta_x = []; 174 delta_y = []; 175 delta = []; 176 psi = [null]; 177 178 // tuple([]) = tuple([[], [], [], [null]]); 179 while (endless) { 180 t = s.next; 181 // None; 182 delta_x.push(t.x - s.x); 183 delta_y.push(t.y - s.y); 184 delta.push(this.mp_pyth_add(delta_x[k], delta_y[k])); 185 if (k > 0) { 186 sine = delta_y[k - 1] / delta[k - 1]; 187 cosine = delta_x[k - 1] / delta[k - 1]; 188 psi.push( 189 Math.atan2( 190 delta_y[k] * cosine - delta_x[k] * sine, 191 delta_x[k] * cosine + delta_y[k] * sine 192 ) 193 ); 194 } 195 k++; 196 s = t; 197 if (s === q) { 198 n = k; 199 } 200 if (k >= n && s.ltype !== this.MP_END_CYCLE) { 201 break; 202 } 203 } 204 if (k === n) { 205 psi.push(0); 206 } else { 207 psi.push(psi[1]); 208 } 209 210 // Remove open types at the breakpoints 211 // MP 303 212 if (q.ltype === this.MP_OPEN) { 213 delx = q.rx - q.x; 214 dely = q.ry - q.y; 215 if (delx * delx + dely * dely < this.EPS_SQ) { 216 q.ltype = this.MP_CURL; 217 q.set_left_curl(this.UNITY); 218 } else { 219 q.ltype = this.MP_GIVEN; 220 q.set_left_given(Math.atan2(dely, delx)); 221 } 222 } 223 if (p.rtype === this.MP_OPEN && p.ltype === this.MP_EXPLICIT) { 224 delx = p.x - p.lx; 225 dely = p.y - p.ly; 226 if (delx * delx + dely * dely < this.EPS_SQ) { 227 p.rtype = this.MP_CURL; 228 p.set_right_curl(this.UNITY); 229 } else { 230 p.rtype = this.MP_GIVEN; 231 p.set_right_given(Math.atan2(dely, delx)); 232 } 233 } 234 this.mp_solve_choices(p, q, n, delta_x, delta_y, delta, psi); 235 } else if (p.rtype === this.MP_ENDPOINT) { 236 // MP 294 237 p.rx = p.x; 238 p.ry = p.y; 239 q.lx = q.x; 240 q.ly = q.y; 241 } 242 p = q; 243 244 if (p === h) { 245 break; 246 } 247 } 248 }, 249 250 /** 251 * Implements solve_choices form metapost 252 * MP 305 253 * @private 254 */ 255 mp_solve_choices: function (p, q, n, delta_x, delta_y, delta, psi) { 256 var aa, acc, vv, bb, ldelta, ee, k, 257 s, ww, uu, lt, r, t, ff, 258 theta, rt, dd, cc, ct_st, 259 ct, st, cf_sf, cf, sf, i, k_idx, 260 endless = true; 261 262 ldelta = delta.length + 1; 263 uu = new Array(ldelta); 264 ww = new Array(ldelta); 265 vv = new Array(ldelta); 266 theta = new Array(ldelta); 267 for (i = 0; i < ldelta; i++) { 268 theta[i] = vv[i] = ww[i] = uu[i] = 0; 269 } 270 k = 0; 271 s = p; 272 r = 0; 273 while (endless) { 274 t = s.next; 275 if (k === 0) { 276 // MP 306 277 if (s.rtype === this.MP_GIVEN) { 278 // MP 314 279 if (t.ltype === this.MP_GIVEN) { 280 aa = Math.atan2(delta_y[0], delta_x[0]); 281 ct_st = this.mp_n_sin_cos(p.right_given() - aa); 282 ct = ct_st[0]; 283 st = ct_st[1]; 284 cf_sf = this.mp_n_sin_cos(q.left_given() - aa); 285 cf = cf_sf[0]; 286 sf = cf_sf[1]; 287 this.mp_set_controls(p, q, delta_x[0], delta_y[0], st, ct, -sf, cf); 288 return; 289 } 290 vv[0] = s.right_given() - Math.atan2(delta_y[0], delta_x[0]); 291 vv[0] = this.reduce_angle(vv[0]); 292 uu[0] = 0; 293 ww[0] = 0; 294 } else if (s.rtype === this.MP_CURL) { 295 // MP 315 296 if (t.ltype === this.MP_CURL) { 297 p.rtype = this.MP_EXPLICIT; 298 q.ltype = this.MP_EXPLICIT; 299 lt = Math.abs(q.left_tension()); 300 rt = Math.abs(p.right_tension()); 301 ff = this.UNITY / (3.0 * rt); 302 p.rx = p.x + delta_x[0] * ff; 303 p.ry = p.y + delta_y[0] * ff; 304 ff = this.UNITY / (3.0 * lt); 305 q.lx = q.x - delta_x[0] * ff; 306 q.ly = q.y - delta_y[0] * ff; 307 return; 308 } 309 cc = s.right_curl(); 310 lt = Math.abs(t.left_tension()); 311 rt = Math.abs(s.right_tension()); 312 uu[0] = this.mp_curl_ratio(cc, rt, lt); 313 vv[0] = -psi[1] * uu[0]; 314 ww[0] = 0; 315 } else { 316 if (s.rtype === this.MP_OPEN) { 317 uu[0] = 0; 318 vv[0] = 0; 319 ww[0] = this.FRACTION_ONE; 320 } 321 } 322 } else { 323 if (s.ltype === this.MP_END_CYCLE || s.ltype === this.MP_OPEN) { 324 // MP 308 325 aa = this.UNITY / (3.0 * Math.abs(r.right_tension()) - this.UNITY); 326 dd = 327 delta[k] * 328 (this.FRACTION_THREE - this.UNITY / Math.abs(r.right_tension())); 329 bb = this.UNITY / (3 * Math.abs(t.left_tension()) - this.UNITY); 330 ee = 331 delta[k - 1] * 332 (this.FRACTION_THREE - this.UNITY / Math.abs(t.left_tension())); 333 cc = this.FRACTION_ONE - uu[k - 1] * aa; 334 dd = dd * cc; 335 lt = Math.abs(s.left_tension()); 336 rt = Math.abs(s.right_tension()); 337 if (lt < rt) { 338 dd *= Math.pow(lt / rt, 2); 339 } else { 340 if (lt > rt) { 341 ee *= Math.pow(rt / lt, 2); 342 } 343 } 344 ff = ee / (ee + dd); 345 uu[k] = ff * bb; 346 acc = -psi[k + 1] * uu[k]; 347 if (r.rtype === this.MP_CURL) { 348 ww[k] = 0; 349 vv[k] = acc - psi[1] * (this.FRACTION_ONE - ff); 350 } else { 351 ff = (this.FRACTION_ONE - ff) / cc; 352 acc = acc - psi[k] * ff; 353 ff = ff * aa; 354 vv[k] = acc - vv[k - 1] * ff; 355 ww[k] = -ww[k - 1] * ff; 356 } 357 if (s.ltype === this.MP_END_CYCLE) { 358 aa = 0; 359 bb = this.FRACTION_ONE; 360 while (endless) { 361 k -= 1; 362 if (k === 0) { 363 k = n; 364 } 365 aa = vv[k] - aa * uu[k]; 366 bb = ww[k] - bb * uu[k]; 367 if (k === n) { 368 break; 369 } 370 } 371 aa = aa / (this.FRACTION_ONE - bb); 372 theta[n] = aa; 373 vv[0] = aa; 374 // k_val = range(1, n); 375 // for (k_idx in k_val) { 376 // k = k_val[k_idx]; 377 for (k_idx = 1; k_idx < n; k_idx++) { 378 vv[k_idx] = vv[k_idx] + aa * ww[k_idx]; 379 } 380 break; 381 } 382 } else { 383 if (s.ltype === this.MP_CURL) { 384 cc = s.left_curl(); 385 lt = Math.abs(s.left_tension()); 386 rt = Math.abs(r.right_tension()); 387 ff = this.mp_curl_ratio(cc, lt, rt); 388 theta[n] = -(vv[n - 1] * ff) / (this.FRACTION_ONE - ff * uu[n - 1]); 389 break; 390 } 391 if (s.ltype === this.MP_GIVEN) { 392 theta[n] = s.left_given() - Math.atan2(delta_y[n - 1], delta_x[n - 1]); 393 theta[n] = this.reduce_angle(theta[n]); 394 break; 395 } 396 } 397 } 398 r = s; 399 s = t; 400 k += 1; 401 } 402 403 // MP 318 404 for (k = n - 1; k > -1; k--) { 405 theta[k] = vv[k] - theta[k + 1] * uu[k]; 406 } 407 408 s = p; 409 k = 0; 410 while (endless) { 411 t = s.next; 412 ct_st = this.mp_n_sin_cos(theta[k]); 413 ct = ct_st[0]; 414 st = ct_st[1]; 415 cf_sf = this.mp_n_sin_cos(-psi[k + 1] - theta[k + 1]); 416 cf = cf_sf[0]; 417 sf = cf_sf[1]; 418 this.mp_set_controls(s, t, delta_x[k], delta_y[k], st, ct, sf, cf); 419 k++; 420 s = t; 421 if (k === n) { 422 break; 423 } 424 } 425 }, 426 427 /** 428 * @private 429 */ 430 mp_n_sin_cos: function (z) { 431 return [Math.cos(z), Math.sin(z)]; 432 }, 433 434 /** 435 * @private 436 */ 437 mp_set_controls: function (p, q, delta_x, delta_y, st, ct, sf, cf) { 438 var rt, ss, lt, sine, rr; 439 lt = Math.abs(q.left_tension()); 440 rt = Math.abs(p.right_tension()); 441 rr = this.mp_velocity(st, ct, sf, cf, rt); 442 ss = this.mp_velocity(sf, cf, st, ct, lt); 443 444 // console.log('lt rt rr ss', lt, rt, rr, ss); 445 if (p.right_tension() < 0 || q.left_tension() < 0) { 446 if ((st >= 0 && sf >= 0) || (st <= 0 && sf <= 0)) { 447 sine = Math.abs(st) * cf + Math.abs(sf) * ct; 448 if (sine > 0) { 449 sine *= 1.00024414062; 450 if (p.right_tension() < 0) { 451 if (this.mp_ab_vs_cd(Math.abs(sf), this.FRACTION_ONE, rr, sine) < 0) { 452 rr = Math.abs(sf) / sine; 453 } 454 } 455 if (q.left_tension() < 0) { 456 if (this.mp_ab_vs_cd(Math.abs(st), this.FRACTION_ONE, ss, sine) < 0) { 457 ss = Math.abs(st) / sine; 458 } 459 } 460 } 461 } 462 } 463 p.rx = p.x + (delta_x * ct - delta_y * st) * rr; 464 p.ry = p.y + (delta_y * ct + delta_x * st) * rr; 465 q.lx = q.x - (delta_x * cf + delta_y * sf) * ss; 466 q.ly = q.y - (delta_y * cf - delta_x * sf) * ss; 467 p.rtype = this.MP_EXPLICIT; 468 q.ltype = this.MP_EXPLICIT; 469 }, 470 471 /** 472 * @private 473 */ 474 mp_pyth_add: function (a, b) { 475 return Mat.hypot(a, b); 476 }, 477 478 /** 479 * 480 * @private 481 */ 482 mp_curl_ratio: function (gamma, a_tension, b_tension) { 483 var alpha = 1.0 / a_tension, 484 beta = 1.0 / b_tension; 485 486 return Math.min( 487 4.0, 488 ((3.0 - alpha) * alpha * alpha * gamma + beta * beta * beta) / 489 (alpha * alpha * alpha * gamma + (3.0 - beta) * beta * beta) 490 ); 491 }, 492 493 /** 494 * @private 495 */ 496 mp_ab_vs_cd: function (a, b, c, d) { 497 if (a * b === c * d) { 498 return 0; 499 } 500 if (a * b > c * d) { 501 return 1; 502 } 503 return -1; 504 }, 505 506 /** 507 * @private 508 */ 509 mp_velocity: function (st, ct, sf, cf, t) { 510 return Math.min( 511 4.0, 512 (2.0 + Math.sqrt(2) * (st - sf / 16.0) * (sf - st / 16.0) * (ct - cf)) / 513 (1.5 * t * (2 + (Math.sqrt(5) - 1) * ct + (3 - Math.sqrt(5)) * cf)) 514 ); 515 }, 516 517 /** 518 * @private 519 * @param {Number} A 520 */ 521 reduce_angle: function (A) { 522 if (Math.abs(A) > this.ONE_EIGHTY_DEG) { 523 if (A > 0) { 524 A -= this.THREE_SIXTY_DEG; 525 } else { 526 A += this.THREE_SIXTY_DEG; 527 } 528 } 529 return A; 530 }, 531 532 /** 533 * 534 * @private 535 * @param {Array} p 536 * @param {Number} tension 537 * @param {Boolean} cycle 538 */ 539 makeknots: function (p, tension) { 540 var i, len, 541 knots = []; 542 543 len = p.length; 544 for (i = 0; i < len; i++) { 545 knots.push({ 546 x: p[i][0], 547 y: p[i][1], 548 ltype: this.MP_OPEN, 549 rtype: this.MP_OPEN, 550 lx: false, 551 rx: false, 552 ly: tension, 553 ry: tension, 554 left_curl: function () { 555 return this.lx || 0; 556 }, 557 right_curl: function () { 558 return this.rx || 0; 559 }, 560 left_tension: function () { 561 return this.ly || 1; 562 }, 563 right_tension: function () { 564 return this.ry || 1; 565 }, 566 set_right_curl: function (v) { 567 this.rx = v || 0; 568 }, 569 set_left_curl: function (v) { 570 this.lx = v || 0; 571 } 572 }); 573 } 574 575 len = knots.length; 576 for (i = 0; i < len; i++) { 577 knots[i].next = knots[i + 1] || knots[i]; 578 knots[i].set_right_given = knots[i].set_right_curl; 579 knots[i].set_left_given = knots[i].set_left_curl; 580 knots[i].right_given = knots[i].right_curl; 581 knots[i].left_given = knots[i].left_curl; 582 } 583 knots[len - 1].next = knots[0]; 584 585 return knots; 586 }, 587 588 /** 589 * 590 * @param {Array} point_list 591 * @param {Object} controls 592 * 593 * @returns {Array} 594 */ 595 curve: function (point_list, controls) { 596 var knots, len, i, ii, 597 val, obj, 598 isClosed = false, 599 x = [], 600 y = []; 601 602 controls = controls || { 603 tension: 1, 604 direction: {}, 605 curl: {}, 606 isClosed: false 607 }; 608 609 // Change default tension 610 val = 1; 611 if (controls.hasOwnProperty('tension')) { 612 val = Type.evaluate(controls.tension); 613 } 614 615 knots = this.makeknots(point_list, val); 616 617 len = knots.length; 618 if (Type.exists(controls.isClosed) && Type.evaluate(controls.isClosed)) { 619 isClosed = true; 620 } 621 622 if (!isClosed) { 623 knots[0].ltype = this.MP_ENDPOINT; 624 knots[0].rtype = this.MP_CURL; 625 knots[len - 1].rtype = this.MP_ENDPOINT; 626 knots[len - 1].ltype = this.MP_CURL; 627 } 628 629 // for (i in controls.direction) { 630 // if (controls.direction.hasOwnProperty(i)) { 631 // val = Type.evaluate(controls.direction[i]); 632 // if (Type.isArray(val)) { 633 // if (val[0] !== false) { 634 // knots[i].lx = (val[0] * Math.PI) / 180; 635 // knots[i].ltype = this.MP_GIVEN; 636 // } 637 // if (val[1] !== false) { 638 // knots[i].rx = (val[1] * Math.PI) / 180; 639 // knots[i].rtype = this.MP_GIVEN; 640 // } 641 // } else { 642 // knots[i].lx = (val * Math.PI) / 180; 643 // knots[i].rx = (val * Math.PI) / 180; 644 // knots[i].ltype = knots[i].rtype = this.MP_GIVEN; 645 // } 646 // } 647 // } 648 649 // for (i in controls.curl) { 650 // if (controls.curl.hasOwnProperty(i)) { 651 // val = Type.evaluate(controls.curl[i]); 652 // if (parseInt(i, 10) === 0) { 653 // knots[i].rtype = this.MP_CURL; 654 // knots[i].set_right_curl(val); 655 // } else if (parseInt(i, 10) === len - 1) { 656 // knots[i].ltype = this.MP_CURL; 657 // knots[i].set_left_curl(val); 658 // } 659 // } 660 // } 661 662 // Set individual point control values 663 for (ii in controls) { 664 if (controls.hasOwnProperty(ii)) { 665 i = parseInt(ii, 10); 666 if (isNaN(i) || i < 0 || i >= len) { 667 continue; 668 } 669 670 // Handle individual curl 671 obj = controls[i]; 672 if (Type.exists(obj.type)) { 673 switch (obj.type) { 674 case 'curl': 675 val = Type.evaluate(obj.curl); 676 if (i === 0) { 677 knots[i].rtype = this.MP_CURL; 678 knots[i].set_right_curl(val); 679 } else if (i === len - 1) { 680 knots[i].ltype = this.MP_CURL; 681 knots[i].set_left_curl(val); 682 } else { 683 knots[i].ltype = this.MP_CURL; 684 knots[i].rtype = this.MP_CURL; 685 knots[i].lx = val; 686 knots[i].rx = val; 687 } 688 break; 689 } 690 } 691 692 // Handle individual directions 693 if (Type.exists(obj.direction)) { 694 val = Type.evaluate(obj.direction); 695 if (Type.isArray(val)) { 696 if (val[0] !== false) { 697 knots[i].lx = (val[0] * Math.PI) / 180; 698 knots[i].ltype = this.MP_GIVEN; 699 } 700 if (val[1] !== false) { 701 knots[i].rx = (val[1] * Math.PI) / 180; 702 knots[i].rtype = this.MP_GIVEN; 703 } 704 } else { 705 knots[i].lx = (val * Math.PI) / 180; 706 knots[i].rx = (val * Math.PI) / 180; 707 knots[i].ltype = knots[i].rtype = this.MP_GIVEN; 708 } 709 } 710 711 // Handle individual tension 712 if (Type.exists(obj.tension)) { 713 val = Type.evaluate(obj.tension); 714 if (Type.isArray(val)) { 715 if (val[0] !== false) { 716 knots[i].ly = Type.evaluate(val[0]); 717 } 718 if (val[1] !== false) { 719 knots[i].ry = Type.evaluate(val[1]); 720 } 721 } else { 722 knots[i].ly = val; 723 knots[i].ry = val; 724 } 725 } 726 } 727 } 728 729 // Generate ths Bezier curve 730 this.make_choices(knots); 731 732 // Return the coordinates 733 for (i = 0; i < len - 1; i++) { 734 x.push(knots[i].x); 735 x.push(knots[i].rx); 736 x.push(knots[i + 1].lx); 737 y.push(knots[i].y); 738 y.push(knots[i].ry); 739 y.push(knots[i + 1].ly); 740 } 741 x.push(knots[len - 1].x); 742 y.push(knots[len - 1].y); 743 744 if (isClosed) { 745 x.push(knots[len - 1].rx); 746 y.push(knots[len - 1].ry); 747 x.push(knots[0].lx); 748 y.push(knots[0].ly); 749 x.push(knots[0].x); 750 y.push(knots[0].y); 751 } 752 753 return [x, y]; 754 } 755 }; 756 757 export default Mat.Metapost; 758