_path.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. 'use strict';
  2. /**
  3. * @typedef {import('../lib/types').XastElement} XastElement
  4. * @typedef {import('../lib/types').PathDataItem} PathDataItem
  5. */
  6. const { parsePathData, stringifyPathData } = require('../lib/path.js');
  7. /**
  8. * @type {[number, number]}
  9. */
  10. var prevCtrlPoint;
  11. /**
  12. * Convert path string to JS representation.
  13. *
  14. * @type {(path: XastElement) => Array<PathDataItem>}
  15. */
  16. const path2js = (path) => {
  17. // @ts-ignore legacy
  18. if (path.pathJS) return path.pathJS;
  19. /**
  20. * @type {Array<PathDataItem>}
  21. */
  22. const pathData = []; // JS representation of the path data
  23. const newPathData = parsePathData(path.attributes.d);
  24. for (const { command, args } of newPathData) {
  25. pathData.push({ command, args });
  26. }
  27. // First moveto is actually absolute. Subsequent coordinates were separated above.
  28. if (pathData.length && pathData[0].command == 'm') {
  29. pathData[0].command = 'M';
  30. }
  31. // @ts-ignore legacy
  32. path.pathJS = pathData;
  33. return pathData;
  34. };
  35. exports.path2js = path2js;
  36. /**
  37. * Convert relative Path data to absolute.
  38. *
  39. * @type {(data: Array<PathDataItem>) => Array<PathDataItem>}
  40. *
  41. */
  42. const convertRelativeToAbsolute = (data) => {
  43. /**
  44. * @type {Array<PathDataItem>}
  45. */
  46. const newData = [];
  47. let start = [0, 0];
  48. let cursor = [0, 0];
  49. for (let { command, args } of data) {
  50. args = args.slice();
  51. // moveto (x y)
  52. if (command === 'm') {
  53. args[0] += cursor[0];
  54. args[1] += cursor[1];
  55. command = 'M';
  56. }
  57. if (command === 'M') {
  58. cursor[0] = args[0];
  59. cursor[1] = args[1];
  60. start[0] = cursor[0];
  61. start[1] = cursor[1];
  62. }
  63. // horizontal lineto (x)
  64. if (command === 'h') {
  65. args[0] += cursor[0];
  66. command = 'H';
  67. }
  68. if (command === 'H') {
  69. cursor[0] = args[0];
  70. }
  71. // vertical lineto (y)
  72. if (command === 'v') {
  73. args[0] += cursor[1];
  74. command = 'V';
  75. }
  76. if (command === 'V') {
  77. cursor[1] = args[0];
  78. }
  79. // lineto (x y)
  80. if (command === 'l') {
  81. args[0] += cursor[0];
  82. args[1] += cursor[1];
  83. command = 'L';
  84. }
  85. if (command === 'L') {
  86. cursor[0] = args[0];
  87. cursor[1] = args[1];
  88. }
  89. // curveto (x1 y1 x2 y2 x y)
  90. if (command === 'c') {
  91. args[0] += cursor[0];
  92. args[1] += cursor[1];
  93. args[2] += cursor[0];
  94. args[3] += cursor[1];
  95. args[4] += cursor[0];
  96. args[5] += cursor[1];
  97. command = 'C';
  98. }
  99. if (command === 'C') {
  100. cursor[0] = args[4];
  101. cursor[1] = args[5];
  102. }
  103. // smooth curveto (x2 y2 x y)
  104. if (command === 's') {
  105. args[0] += cursor[0];
  106. args[1] += cursor[1];
  107. args[2] += cursor[0];
  108. args[3] += cursor[1];
  109. command = 'S';
  110. }
  111. if (command === 'S') {
  112. cursor[0] = args[2];
  113. cursor[1] = args[3];
  114. }
  115. // quadratic Bézier curveto (x1 y1 x y)
  116. if (command === 'q') {
  117. args[0] += cursor[0];
  118. args[1] += cursor[1];
  119. args[2] += cursor[0];
  120. args[3] += cursor[1];
  121. command = 'Q';
  122. }
  123. if (command === 'Q') {
  124. cursor[0] = args[2];
  125. cursor[1] = args[3];
  126. }
  127. // smooth quadratic Bézier curveto (x y)
  128. if (command === 't') {
  129. args[0] += cursor[0];
  130. args[1] += cursor[1];
  131. command = 'T';
  132. }
  133. if (command === 'T') {
  134. cursor[0] = args[0];
  135. cursor[1] = args[1];
  136. }
  137. // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
  138. if (command === 'a') {
  139. args[5] += cursor[0];
  140. args[6] += cursor[1];
  141. command = 'A';
  142. }
  143. if (command === 'A') {
  144. cursor[0] = args[5];
  145. cursor[1] = args[6];
  146. }
  147. // closepath
  148. if (command === 'z' || command === 'Z') {
  149. cursor[0] = start[0];
  150. cursor[1] = start[1];
  151. command = 'z';
  152. }
  153. newData.push({ command, args });
  154. }
  155. return newData;
  156. };
  157. /**
  158. * @typedef {{ floatPrecision?: number, noSpaceAfterFlags?: boolean }} Js2PathParams
  159. */
  160. /**
  161. * Convert path array to string.
  162. *
  163. * @type {(path: XastElement, data: Array<PathDataItem>, params: Js2PathParams) => void}
  164. */
  165. exports.js2path = function (path, data, params) {
  166. // @ts-ignore legacy
  167. path.pathJS = data;
  168. const pathData = [];
  169. for (const item of data) {
  170. // remove moveto commands which are followed by moveto commands
  171. if (
  172. pathData.length !== 0 &&
  173. (item.command === 'M' || item.command === 'm')
  174. ) {
  175. const last = pathData[pathData.length - 1];
  176. if (last.command === 'M' || last.command === 'm') {
  177. pathData.pop();
  178. }
  179. }
  180. pathData.push({
  181. command: item.command,
  182. args: item.args,
  183. });
  184. }
  185. path.attributes.d = stringifyPathData({
  186. pathData,
  187. precision: params.floatPrecision,
  188. disableSpaceAfterFlags: params.noSpaceAfterFlags,
  189. });
  190. };
  191. /**
  192. * @type {(dest: Array<number>, source: Array<number>) => Array<number>}
  193. */
  194. function set(dest, source) {
  195. dest[0] = source[source.length - 2];
  196. dest[1] = source[source.length - 1];
  197. return dest;
  198. }
  199. /**
  200. * Checks if two paths have an intersection by checking convex hulls
  201. * collision using Gilbert-Johnson-Keerthi distance algorithm
  202. * https://web.archive.org/web/20180822200027/http://entropyinteractive.com/2011/04/gjk-algorithm/
  203. *
  204. * @type {(path1: Array<PathDataItem>, path2: Array<PathDataItem>) => boolean}
  205. */
  206. exports.intersects = function (path1, path2) {
  207. // Collect points of every subpath.
  208. const points1 = gatherPoints(convertRelativeToAbsolute(path1));
  209. const points2 = gatherPoints(convertRelativeToAbsolute(path2));
  210. // Axis-aligned bounding box check.
  211. if (
  212. points1.maxX <= points2.minX ||
  213. points2.maxX <= points1.minX ||
  214. points1.maxY <= points2.minY ||
  215. points2.maxY <= points1.minY ||
  216. points1.list.every((set1) => {
  217. return points2.list.every((set2) => {
  218. return (
  219. set1.list[set1.maxX][0] <= set2.list[set2.minX][0] ||
  220. set2.list[set2.maxX][0] <= set1.list[set1.minX][0] ||
  221. set1.list[set1.maxY][1] <= set2.list[set2.minY][1] ||
  222. set2.list[set2.maxY][1] <= set1.list[set1.minY][1]
  223. );
  224. });
  225. })
  226. )
  227. return false;
  228. // Get a convex hull from points of each subpath. Has the most complexity O(n·log n).
  229. const hullNest1 = points1.list.map(convexHull);
  230. const hullNest2 = points2.list.map(convexHull);
  231. // Check intersection of every subpath of the first path with every subpath of the second.
  232. return hullNest1.some(function (hull1) {
  233. if (hull1.list.length < 3) return false;
  234. return hullNest2.some(function (hull2) {
  235. if (hull2.list.length < 3) return false;
  236. var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex
  237. direction = minus(simplex[0]); // set the direction to point towards the origin
  238. var iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough
  239. // eslint-disable-next-line no-constant-condition
  240. while (true) {
  241. // eslint-disable-next-line no-constant-condition
  242. if (iterations-- == 0) {
  243. console.error(
  244. 'Error: infinite loop while processing mergePaths plugin.'
  245. );
  246. return true; // true is the safe value that means “do nothing with paths”
  247. }
  248. // add a new point
  249. simplex.push(getSupport(hull1, hull2, direction));
  250. // see if the new point was on the correct side of the origin
  251. if (dot(direction, simplex[simplex.length - 1]) <= 0) return false;
  252. // process the simplex
  253. if (processSimplex(simplex, direction)) return true;
  254. }
  255. });
  256. });
  257. /**
  258. * @type {(a: Point, b: Point, direction: Array<number>) => Array<number>}
  259. */
  260. function getSupport(a, b, direction) {
  261. return sub(supportPoint(a, direction), supportPoint(b, minus(direction)));
  262. }
  263. // Computes farthest polygon point in particular direction.
  264. // Thanks to knowledge of min/max x and y coordinates we can choose a quadrant to search in.
  265. // Since we're working on convex hull, the dot product is increasing until we find the farthest point.
  266. /**
  267. * @type {(polygon: Point, direction: Array<number>) => Array<number>}
  268. */
  269. function supportPoint(polygon, direction) {
  270. var index =
  271. direction[1] >= 0
  272. ? direction[0] < 0
  273. ? polygon.maxY
  274. : polygon.maxX
  275. : direction[0] < 0
  276. ? polygon.minX
  277. : polygon.minY,
  278. max = -Infinity,
  279. value;
  280. while ((value = dot(polygon.list[index], direction)) > max) {
  281. max = value;
  282. index = ++index % polygon.list.length;
  283. }
  284. return polygon.list[(index || polygon.list.length) - 1];
  285. }
  286. };
  287. /**
  288. * @type {(simplex: Array<Array<number>>, direction: Array<number>) => boolean}
  289. */
  290. function processSimplex(simplex, direction) {
  291. // we only need to handle to 1-simplex and 2-simplex
  292. if (simplex.length == 2) {
  293. // 1-simplex
  294. let a = simplex[1],
  295. b = simplex[0],
  296. AO = minus(simplex[1]),
  297. AB = sub(b, a);
  298. // AO is in the same direction as AB
  299. if (dot(AO, AB) > 0) {
  300. // get the vector perpendicular to AB facing O
  301. set(direction, orth(AB, a));
  302. } else {
  303. set(direction, AO);
  304. // only A remains in the simplex
  305. simplex.shift();
  306. }
  307. } else {
  308. // 2-simplex
  309. let a = simplex[2], // [a, b, c] = simplex
  310. b = simplex[1],
  311. c = simplex[0],
  312. AB = sub(b, a),
  313. AC = sub(c, a),
  314. AO = minus(a),
  315. ACB = orth(AB, AC), // the vector perpendicular to AB facing away from C
  316. ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B
  317. if (dot(ACB, AO) > 0) {
  318. if (dot(AB, AO) > 0) {
  319. // region 4
  320. set(direction, ACB);
  321. simplex.shift(); // simplex = [b, a]
  322. } else {
  323. // region 5
  324. set(direction, AO);
  325. simplex.splice(0, 2); // simplex = [a]
  326. }
  327. } else if (dot(ABC, AO) > 0) {
  328. if (dot(AC, AO) > 0) {
  329. // region 6
  330. set(direction, ABC);
  331. simplex.splice(1, 1); // simplex = [c, a]
  332. } else {
  333. // region 5 (again)
  334. set(direction, AO);
  335. simplex.splice(0, 2); // simplex = [a]
  336. }
  337. } // region 7
  338. else return true;
  339. }
  340. return false;
  341. }
  342. /**
  343. * @type {(v: Array<number>) => Array<number>}
  344. */
  345. function minus(v) {
  346. return [-v[0], -v[1]];
  347. }
  348. /**
  349. * @type {(v1: Array<number>, v2: Array<number>) => Array<number>}
  350. */
  351. function sub(v1, v2) {
  352. return [v1[0] - v2[0], v1[1] - v2[1]];
  353. }
  354. /**
  355. * @type {(v1: Array<number>, v2: Array<number>) => number}
  356. */
  357. function dot(v1, v2) {
  358. return v1[0] * v2[0] + v1[1] * v2[1];
  359. }
  360. /**
  361. * @type {(v1: Array<number>, v2: Array<number>) => Array<number>}
  362. */
  363. function orth(v, from) {
  364. var o = [-v[1], v[0]];
  365. return dot(o, minus(from)) < 0 ? minus(o) : o;
  366. }
  367. /**
  368. * @typedef {{
  369. * list: Array<Array<number>>,
  370. * minX: number,
  371. * minY: number,
  372. * maxX: number,
  373. * maxY: number
  374. * }} Point
  375. */
  376. /**
  377. * @typedef {{
  378. * list: Array<Point>,
  379. * minX: number,
  380. * minY: number,
  381. * maxX: number,
  382. * maxY: number
  383. * }} Points
  384. */
  385. /**
  386. * @type {(pathData: Array<PathDataItem>) => Points}
  387. */
  388. function gatherPoints(pathData) {
  389. /**
  390. * @type {Points}
  391. */
  392. const points = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 };
  393. // Writes data about the extreme points on each axle
  394. /**
  395. * @type {(path: Point, point: Array<number>) => void}
  396. */
  397. const addPoint = (path, point) => {
  398. if (!path.list.length || point[1] > path.list[path.maxY][1]) {
  399. path.maxY = path.list.length;
  400. points.maxY = points.list.length
  401. ? Math.max(point[1], points.maxY)
  402. : point[1];
  403. }
  404. if (!path.list.length || point[0] > path.list[path.maxX][0]) {
  405. path.maxX = path.list.length;
  406. points.maxX = points.list.length
  407. ? Math.max(point[0], points.maxX)
  408. : point[0];
  409. }
  410. if (!path.list.length || point[1] < path.list[path.minY][1]) {
  411. path.minY = path.list.length;
  412. points.minY = points.list.length
  413. ? Math.min(point[1], points.minY)
  414. : point[1];
  415. }
  416. if (!path.list.length || point[0] < path.list[path.minX][0]) {
  417. path.minX = path.list.length;
  418. points.minX = points.list.length
  419. ? Math.min(point[0], points.minX)
  420. : point[0];
  421. }
  422. path.list.push(point);
  423. };
  424. for (let i = 0; i < pathData.length; i += 1) {
  425. const pathDataItem = pathData[i];
  426. let subPath =
  427. points.list.length === 0
  428. ? { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 }
  429. : points.list[points.list.length - 1];
  430. let prev = i === 0 ? null : pathData[i - 1];
  431. let basePoint =
  432. subPath.list.length === 0 ? null : subPath.list[subPath.list.length - 1];
  433. let data = pathDataItem.args;
  434. let ctrlPoint = basePoint;
  435. /**
  436. * @type {(n: number, i: number) => number}
  437. * TODO fix null hack
  438. */
  439. const toAbsolute = (n, i) => n + (basePoint == null ? 0 : basePoint[i % 2]);
  440. switch (pathDataItem.command) {
  441. case 'M':
  442. subPath = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 };
  443. points.list.push(subPath);
  444. break;
  445. case 'H':
  446. if (basePoint != null) {
  447. addPoint(subPath, [data[0], basePoint[1]]);
  448. }
  449. break;
  450. case 'V':
  451. if (basePoint != null) {
  452. addPoint(subPath, [basePoint[0], data[0]]);
  453. }
  454. break;
  455. case 'Q':
  456. addPoint(subPath, data.slice(0, 2));
  457. prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand
  458. break;
  459. case 'T':
  460. if (
  461. basePoint != null &&
  462. prev != null &&
  463. (prev.command == 'Q' || prev.command == 'T')
  464. ) {
  465. ctrlPoint = [
  466. basePoint[0] + prevCtrlPoint[0],
  467. basePoint[1] + prevCtrlPoint[1],
  468. ];
  469. addPoint(subPath, ctrlPoint);
  470. prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]];
  471. }
  472. break;
  473. case 'C':
  474. if (basePoint != null) {
  475. // Approximate quibic Bezier curve with middle points between control points
  476. addPoint(subPath, [
  477. 0.5 * (basePoint[0] + data[0]),
  478. 0.5 * (basePoint[1] + data[1]),
  479. ]);
  480. }
  481. addPoint(subPath, [
  482. 0.5 * (data[0] + data[2]),
  483. 0.5 * (data[1] + data[3]),
  484. ]);
  485. addPoint(subPath, [
  486. 0.5 * (data[2] + data[4]),
  487. 0.5 * (data[3] + data[5]),
  488. ]);
  489. prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand
  490. break;
  491. case 'S':
  492. if (
  493. basePoint != null &&
  494. prev != null &&
  495. (prev.command == 'C' || prev.command == 'S')
  496. ) {
  497. addPoint(subPath, [
  498. basePoint[0] + 0.5 * prevCtrlPoint[0],
  499. basePoint[1] + 0.5 * prevCtrlPoint[1],
  500. ]);
  501. ctrlPoint = [
  502. basePoint[0] + prevCtrlPoint[0],
  503. basePoint[1] + prevCtrlPoint[1],
  504. ];
  505. }
  506. if (ctrlPoint != null) {
  507. addPoint(subPath, [
  508. 0.5 * (ctrlPoint[0] + data[0]),
  509. 0.5 * (ctrlPoint[1] + data[1]),
  510. ]);
  511. }
  512. addPoint(subPath, [
  513. 0.5 * (data[0] + data[2]),
  514. 0.5 * (data[1] + data[3]),
  515. ]);
  516. prevCtrlPoint = [data[2] - data[0], data[3] - data[1]];
  517. break;
  518. case 'A':
  519. if (basePoint != null) {
  520. // Convert the arc to bezier curves and use the same approximation
  521. // @ts-ignore no idea what's going on here
  522. var curves = a2c.apply(0, basePoint.concat(data));
  523. for (
  524. var cData;
  525. (cData = curves.splice(0, 6).map(toAbsolute)).length;
  526. ) {
  527. if (basePoint != null) {
  528. addPoint(subPath, [
  529. 0.5 * (basePoint[0] + cData[0]),
  530. 0.5 * (basePoint[1] + cData[1]),
  531. ]);
  532. }
  533. addPoint(subPath, [
  534. 0.5 * (cData[0] + cData[2]),
  535. 0.5 * (cData[1] + cData[3]),
  536. ]);
  537. addPoint(subPath, [
  538. 0.5 * (cData[2] + cData[4]),
  539. 0.5 * (cData[3] + cData[5]),
  540. ]);
  541. if (curves.length) addPoint(subPath, (basePoint = cData.slice(-2)));
  542. }
  543. }
  544. break;
  545. }
  546. // Save final command coordinates
  547. if (data.length >= 2) addPoint(subPath, data.slice(-2));
  548. }
  549. return points;
  550. }
  551. /**
  552. * Forms a convex hull from set of points of every subpath using monotone chain convex hull algorithm.
  553. * https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
  554. *
  555. * @type {(points: Point) => Point}
  556. */
  557. function convexHull(points) {
  558. points.list.sort(function (a, b) {
  559. return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0];
  560. });
  561. var lower = [],
  562. minY = 0,
  563. bottom = 0;
  564. for (let i = 0; i < points.list.length; i++) {
  565. while (
  566. lower.length >= 2 &&
  567. cross(lower[lower.length - 2], lower[lower.length - 1], points.list[i]) <=
  568. 0
  569. ) {
  570. lower.pop();
  571. }
  572. if (points.list[i][1] < points.list[minY][1]) {
  573. minY = i;
  574. bottom = lower.length;
  575. }
  576. lower.push(points.list[i]);
  577. }
  578. var upper = [],
  579. maxY = points.list.length - 1,
  580. top = 0;
  581. for (let i = points.list.length; i--; ) {
  582. while (
  583. upper.length >= 2 &&
  584. cross(upper[upper.length - 2], upper[upper.length - 1], points.list[i]) <=
  585. 0
  586. ) {
  587. upper.pop();
  588. }
  589. if (points.list[i][1] > points.list[maxY][1]) {
  590. maxY = i;
  591. top = upper.length;
  592. }
  593. upper.push(points.list[i]);
  594. }
  595. // last points are equal to starting points of the other part
  596. upper.pop();
  597. lower.pop();
  598. const hullList = lower.concat(upper);
  599. /**
  600. * @type {Point}
  601. */
  602. const hull = {
  603. list: hullList,
  604. minX: 0, // by sorting
  605. maxX: lower.length,
  606. minY: bottom,
  607. maxY: (lower.length + top) % hullList.length,
  608. };
  609. return hull;
  610. }
  611. /**
  612. * @type {(o: Array<number>, a: Array<number>, b: Array<number>) => number}
  613. */
  614. function cross(o, a, b) {
  615. return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
  616. }
  617. /**
  618. * Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/
  619. * Thanks to Dmitry Baranovskiy for his great work!
  620. *
  621. * @type {(
  622. * x1: number,
  623. * y1: number,
  624. * rx: number,
  625. * ry: number,
  626. * angle: number,
  627. * large_arc_flag: number,
  628. * sweep_flag: number,
  629. * x2: number,
  630. * y2: number,
  631. * recursive: Array<number>
  632. * ) => Array<number>}
  633. */
  634. const a2c = (
  635. x1,
  636. y1,
  637. rx,
  638. ry,
  639. angle,
  640. large_arc_flag,
  641. sweep_flag,
  642. x2,
  643. y2,
  644. recursive
  645. ) => {
  646. // for more information of where this Math came from visit:
  647. // https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
  648. const _120 = (Math.PI * 120) / 180;
  649. const rad = (Math.PI / 180) * (+angle || 0);
  650. /**
  651. * @type {Array<number>}
  652. */
  653. let res = [];
  654. /**
  655. * @type {(x: number, y: number, rad: number) => number}
  656. */
  657. const rotateX = (x, y, rad) => {
  658. return x * Math.cos(rad) - y * Math.sin(rad);
  659. };
  660. /**
  661. * @type {(x: number, y: number, rad: number) => number}
  662. */
  663. const rotateY = (x, y, rad) => {
  664. return x * Math.sin(rad) + y * Math.cos(rad);
  665. };
  666. if (!recursive) {
  667. x1 = rotateX(x1, y1, -rad);
  668. y1 = rotateY(x1, y1, -rad);
  669. x2 = rotateX(x2, y2, -rad);
  670. y2 = rotateY(x2, y2, -rad);
  671. var x = (x1 - x2) / 2,
  672. y = (y1 - y2) / 2;
  673. var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
  674. if (h > 1) {
  675. h = Math.sqrt(h);
  676. rx = h * rx;
  677. ry = h * ry;
  678. }
  679. var rx2 = rx * rx;
  680. var ry2 = ry * ry;
  681. var k =
  682. (large_arc_flag == sweep_flag ? -1 : 1) *
  683. Math.sqrt(
  684. Math.abs(
  685. (rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)
  686. )
  687. );
  688. var cx = (k * rx * y) / ry + (x1 + x2) / 2;
  689. var cy = (k * -ry * x) / rx + (y1 + y2) / 2;
  690. var f1 = Math.asin(Number(((y1 - cy) / ry).toFixed(9)));
  691. var f2 = Math.asin(Number(((y2 - cy) / ry).toFixed(9)));
  692. f1 = x1 < cx ? Math.PI - f1 : f1;
  693. f2 = x2 < cx ? Math.PI - f2 : f2;
  694. f1 < 0 && (f1 = Math.PI * 2 + f1);
  695. f2 < 0 && (f2 = Math.PI * 2 + f2);
  696. if (sweep_flag && f1 > f2) {
  697. f1 = f1 - Math.PI * 2;
  698. }
  699. if (!sweep_flag && f2 > f1) {
  700. f2 = f2 - Math.PI * 2;
  701. }
  702. } else {
  703. f1 = recursive[0];
  704. f2 = recursive[1];
  705. cx = recursive[2];
  706. cy = recursive[3];
  707. }
  708. var df = f2 - f1;
  709. if (Math.abs(df) > _120) {
  710. var f2old = f2,
  711. x2old = x2,
  712. y2old = y2;
  713. f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
  714. x2 = cx + rx * Math.cos(f2);
  715. y2 = cy + ry * Math.sin(f2);
  716. res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [
  717. f2,
  718. f2old,
  719. cx,
  720. cy,
  721. ]);
  722. }
  723. df = f2 - f1;
  724. var c1 = Math.cos(f1),
  725. s1 = Math.sin(f1),
  726. c2 = Math.cos(f2),
  727. s2 = Math.sin(f2),
  728. t = Math.tan(df / 4),
  729. hx = (4 / 3) * rx * t,
  730. hy = (4 / 3) * ry * t,
  731. m = [
  732. -hx * s1,
  733. hy * c1,
  734. x2 + hx * s2 - x1,
  735. y2 - hy * c2 - y1,
  736. x2 - x1,
  737. y2 - y1,
  738. ];
  739. if (recursive) {
  740. return m.concat(res);
  741. } else {
  742. res = m.concat(res);
  743. var newres = [];
  744. for (var i = 0, n = res.length; i < n; i++) {
  745. newres[i] =
  746. i % 2
  747. ? rotateY(res[i - 1], res[i], rad)
  748. : rotateX(res[i], res[i + 1], rad);
  749. }
  750. return newres;
  751. }
  752. };