convertPathData.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. 'use strict';
  2. const { collectStylesheet, computeStyle } = require('../lib/style.js');
  3. const { pathElems } = require('./_collections.js');
  4. const { path2js, js2path } = require('./_path.js');
  5. const { applyTransforms } = require('./_applyTransforms.js');
  6. const { cleanupOutData } = require('../lib/svgo/tools');
  7. exports.name = 'convertPathData';
  8. exports.type = 'visitor';
  9. exports.active = true;
  10. exports.description =
  11. 'optimizes path data: writes in shorter form, applies transformations';
  12. exports.params = {
  13. applyTransforms: true,
  14. applyTransformsStroked: true,
  15. makeArcs: {
  16. threshold: 2.5, // coefficient of rounding error
  17. tolerance: 0.5, // percentage of radius
  18. },
  19. straightCurves: true,
  20. lineShorthands: true,
  21. curveSmoothShorthands: true,
  22. floatPrecision: 3,
  23. transformPrecision: 5,
  24. removeUseless: true,
  25. collapseRepeated: true,
  26. utilizeAbsolute: true,
  27. leadingZero: true,
  28. negativeExtraSpace: true,
  29. noSpaceAfterFlags: false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
  30. forceAbsolutePath: false,
  31. };
  32. let roundData;
  33. let precision;
  34. let error;
  35. let arcThreshold;
  36. let arcTolerance;
  37. /**
  38. * Convert absolute Path to relative,
  39. * collapse repeated instructions,
  40. * detect and convert Lineto shorthands,
  41. * remove useless instructions like "l0,0",
  42. * trim useless delimiters and leading zeros,
  43. * decrease accuracy of floating-point numbers.
  44. *
  45. * @see https://www.w3.org/TR/SVG11/paths.html#PathData
  46. *
  47. * @param {Object} item current iteration item
  48. * @param {Object} params plugin params
  49. * @return {Boolean} if false, item will be filtered out
  50. *
  51. * @author Kir Belevich
  52. */
  53. exports.fn = (root, params) => {
  54. const stylesheet = collectStylesheet(root);
  55. return {
  56. element: {
  57. enter: (node) => {
  58. if (pathElems.includes(node.name) && node.attributes.d != null) {
  59. const computedStyle = computeStyle(stylesheet, node);
  60. precision = params.floatPrecision;
  61. error =
  62. precision !== false
  63. ? +Math.pow(0.1, precision).toFixed(precision)
  64. : 1e-2;
  65. roundData = precision > 0 && precision < 20 ? strongRound : round;
  66. if (params.makeArcs) {
  67. arcThreshold = params.makeArcs.threshold;
  68. arcTolerance = params.makeArcs.tolerance;
  69. }
  70. const hasMarkerMid = computedStyle['marker-mid'] != null;
  71. const maybeHasStroke =
  72. computedStyle.stroke &&
  73. (computedStyle.stroke.type === 'dynamic' ||
  74. computedStyle.stroke.value !== 'none');
  75. const maybeHasLinecap =
  76. computedStyle['stroke-linecap'] &&
  77. (computedStyle['stroke-linecap'].type === 'dynamic' ||
  78. computedStyle['stroke-linecap'].value !== 'butt');
  79. const maybeHasStrokeAndLinecap = maybeHasStroke && maybeHasLinecap;
  80. var data = path2js(node);
  81. // TODO: get rid of functions returns
  82. if (data.length) {
  83. if (params.applyTransforms) {
  84. applyTransforms(node, data, params);
  85. }
  86. convertToRelative(data);
  87. data = filters(data, params, {
  88. maybeHasStrokeAndLinecap,
  89. hasMarkerMid,
  90. });
  91. if (params.utilizeAbsolute) {
  92. data = convertToMixed(data, params);
  93. }
  94. js2path(node, data, params);
  95. }
  96. }
  97. },
  98. },
  99. };
  100. };
  101. /**
  102. * Convert absolute path data coordinates to relative.
  103. *
  104. * @param {Array} path input path data
  105. * @param {Object} params plugin params
  106. * @return {Array} output path data
  107. */
  108. const convertToRelative = (pathData) => {
  109. let start = [0, 0];
  110. let cursor = [0, 0];
  111. let prevCoords = [0, 0];
  112. for (let i = 0; i < pathData.length; i += 1) {
  113. const pathItem = pathData[i];
  114. let { command, args } = pathItem;
  115. // moveto (x y)
  116. if (command === 'm') {
  117. // update start and cursor
  118. cursor[0] += args[0];
  119. cursor[1] += args[1];
  120. start[0] = cursor[0];
  121. start[1] = cursor[1];
  122. }
  123. if (command === 'M') {
  124. // M → m
  125. // skip first moveto
  126. if (i !== 0) {
  127. command = 'm';
  128. }
  129. args[0] -= cursor[0];
  130. args[1] -= cursor[1];
  131. // update start and cursor
  132. cursor[0] += args[0];
  133. cursor[1] += args[1];
  134. start[0] = cursor[0];
  135. start[1] = cursor[1];
  136. }
  137. // lineto (x y)
  138. if (command === 'l') {
  139. cursor[0] += args[0];
  140. cursor[1] += args[1];
  141. }
  142. if (command === 'L') {
  143. // L → l
  144. command = 'l';
  145. args[0] -= cursor[0];
  146. args[1] -= cursor[1];
  147. cursor[0] += args[0];
  148. cursor[1] += args[1];
  149. }
  150. // horizontal lineto (x)
  151. if (command === 'h') {
  152. cursor[0] += args[0];
  153. }
  154. if (command === 'H') {
  155. // H → h
  156. command = 'h';
  157. args[0] -= cursor[0];
  158. cursor[0] += args[0];
  159. }
  160. // vertical lineto (y)
  161. if (command === 'v') {
  162. cursor[1] += args[0];
  163. }
  164. if (command === 'V') {
  165. // V → v
  166. command = 'v';
  167. args[0] -= cursor[1];
  168. cursor[1] += args[0];
  169. }
  170. // curveto (x1 y1 x2 y2 x y)
  171. if (command === 'c') {
  172. cursor[0] += args[4];
  173. cursor[1] += args[5];
  174. }
  175. if (command === 'C') {
  176. // C → c
  177. command = 'c';
  178. args[0] -= cursor[0];
  179. args[1] -= cursor[1];
  180. args[2] -= cursor[0];
  181. args[3] -= cursor[1];
  182. args[4] -= cursor[0];
  183. args[5] -= cursor[1];
  184. cursor[0] += args[4];
  185. cursor[1] += args[5];
  186. }
  187. // smooth curveto (x2 y2 x y)
  188. if (command === 's') {
  189. cursor[0] += args[2];
  190. cursor[1] += args[3];
  191. }
  192. if (command === 'S') {
  193. // S → s
  194. command = 's';
  195. args[0] -= cursor[0];
  196. args[1] -= cursor[1];
  197. args[2] -= cursor[0];
  198. args[3] -= cursor[1];
  199. cursor[0] += args[2];
  200. cursor[1] += args[3];
  201. }
  202. // quadratic Bézier curveto (x1 y1 x y)
  203. if (command === 'q') {
  204. cursor[0] += args[2];
  205. cursor[1] += args[3];
  206. }
  207. if (command === 'Q') {
  208. // Q → q
  209. command = 'q';
  210. args[0] -= cursor[0];
  211. args[1] -= cursor[1];
  212. args[2] -= cursor[0];
  213. args[3] -= cursor[1];
  214. cursor[0] += args[2];
  215. cursor[1] += args[3];
  216. }
  217. // smooth quadratic Bézier curveto (x y)
  218. if (command === 't') {
  219. cursor[0] += args[0];
  220. cursor[1] += args[1];
  221. }
  222. if (command === 'T') {
  223. // T → t
  224. command = 't';
  225. args[0] -= cursor[0];
  226. args[1] -= cursor[1];
  227. cursor[0] += args[0];
  228. cursor[1] += args[1];
  229. }
  230. // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
  231. if (command === 'a') {
  232. cursor[0] += args[5];
  233. cursor[1] += args[6];
  234. }
  235. if (command === 'A') {
  236. // A → a
  237. command = 'a';
  238. args[5] -= cursor[0];
  239. args[6] -= cursor[1];
  240. cursor[0] += args[5];
  241. cursor[1] += args[6];
  242. }
  243. // closepath
  244. if (command === 'Z' || command === 'z') {
  245. // reset cursor
  246. cursor[0] = start[0];
  247. cursor[1] = start[1];
  248. }
  249. pathItem.command = command;
  250. pathItem.args = args;
  251. // store absolute coordinates for later use
  252. // base should preserve reference from other element
  253. pathItem.base = prevCoords;
  254. pathItem.coords = [cursor[0], cursor[1]];
  255. prevCoords = pathItem.coords;
  256. }
  257. return pathData;
  258. };
  259. /**
  260. * Main filters loop.
  261. *
  262. * @param {Array} path input path data
  263. * @param {Object} params plugin params
  264. * @return {Array} output path data
  265. */
  266. function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) {
  267. var stringify = data2Path.bind(null, params),
  268. relSubpoint = [0, 0],
  269. pathBase = [0, 0],
  270. prev = {};
  271. path = path.filter(function (item, index, path) {
  272. let command = item.command;
  273. let data = item.args;
  274. let next = path[index + 1];
  275. if (command !== 'Z' && command !== 'z') {
  276. var sdata = data,
  277. circle;
  278. if (command === 's') {
  279. sdata = [0, 0].concat(data);
  280. if (command === 'c' || command === 's') {
  281. var pdata = prev.args,
  282. n = pdata.length;
  283. // (-x, -y) of the prev tangent point relative to the current point
  284. sdata[0] = pdata[n - 2] - pdata[n - 4];
  285. sdata[1] = pdata[n - 1] - pdata[n - 3];
  286. }
  287. }
  288. // convert curves to arcs if possible
  289. if (
  290. params.makeArcs &&
  291. (command == 'c' || command == 's') &&
  292. isConvex(sdata) &&
  293. (circle = findCircle(sdata))
  294. ) {
  295. var r = roundData([circle.radius])[0],
  296. angle = findArcAngle(sdata, circle),
  297. sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0,
  298. arc = {
  299. command: 'a',
  300. args: [r, r, 0, 0, sweep, sdata[4], sdata[5]],
  301. coords: item.coords.slice(),
  302. base: item.base,
  303. },
  304. output = [arc],
  305. // relative coordinates to adjust the found circle
  306. relCenter = [
  307. circle.center[0] - sdata[4],
  308. circle.center[1] - sdata[5],
  309. ],
  310. relCircle = { center: relCenter, radius: circle.radius },
  311. arcCurves = [item],
  312. hasPrev = 0,
  313. suffix = '',
  314. nextLonghand;
  315. if (
  316. (prev.command == 'c' &&
  317. isConvex(prev.args) &&
  318. isArcPrev(prev.args, circle)) ||
  319. (prev.command == 'a' && prev.sdata && isArcPrev(prev.sdata, circle))
  320. ) {
  321. arcCurves.unshift(prev);
  322. arc.base = prev.base;
  323. arc.args[5] = arc.coords[0] - arc.base[0];
  324. arc.args[6] = arc.coords[1] - arc.base[1];
  325. var prevData = prev.command == 'a' ? prev.sdata : prev.args;
  326. var prevAngle = findArcAngle(prevData, {
  327. center: [
  328. prevData[4] + circle.center[0],
  329. prevData[5] + circle.center[1],
  330. ],
  331. radius: circle.radius,
  332. });
  333. angle += prevAngle;
  334. if (angle > Math.PI) arc.args[3] = 1;
  335. hasPrev = 1;
  336. }
  337. // check if next curves are fitting the arc
  338. for (
  339. var j = index;
  340. (next = path[++j]) && ~'cs'.indexOf(next.command);
  341. ) {
  342. var nextData = next.args;
  343. if (next.command == 's') {
  344. nextLonghand = makeLonghand(
  345. { command: 's', args: next.args.slice() },
  346. path[j - 1].args
  347. );
  348. nextData = nextLonghand.args;
  349. nextLonghand.args = nextData.slice(0, 2);
  350. suffix = stringify([nextLonghand]);
  351. }
  352. if (isConvex(nextData) && isArc(nextData, relCircle)) {
  353. angle += findArcAngle(nextData, relCircle);
  354. if (angle - 2 * Math.PI > 1e-3) break; // more than 360°
  355. if (angle > Math.PI) arc.args[3] = 1;
  356. arcCurves.push(next);
  357. if (2 * Math.PI - angle > 1e-3) {
  358. // less than 360°
  359. arc.coords = next.coords;
  360. arc.args[5] = arc.coords[0] - arc.base[0];
  361. arc.args[6] = arc.coords[1] - arc.base[1];
  362. } else {
  363. // full circle, make a half-circle arc and add a second one
  364. arc.args[5] = 2 * (relCircle.center[0] - nextData[4]);
  365. arc.args[6] = 2 * (relCircle.center[1] - nextData[5]);
  366. arc.coords = [
  367. arc.base[0] + arc.args[5],
  368. arc.base[1] + arc.args[6],
  369. ];
  370. arc = {
  371. command: 'a',
  372. args: [
  373. r,
  374. r,
  375. 0,
  376. 0,
  377. sweep,
  378. next.coords[0] - arc.coords[0],
  379. next.coords[1] - arc.coords[1],
  380. ],
  381. coords: next.coords,
  382. base: arc.coords,
  383. };
  384. output.push(arc);
  385. j++;
  386. break;
  387. }
  388. relCenter[0] -= nextData[4];
  389. relCenter[1] -= nextData[5];
  390. } else break;
  391. }
  392. if ((stringify(output) + suffix).length < stringify(arcCurves).length) {
  393. if (path[j] && path[j].command == 's') {
  394. makeLonghand(path[j], path[j - 1].args);
  395. }
  396. if (hasPrev) {
  397. var prevArc = output.shift();
  398. roundData(prevArc.args);
  399. relSubpoint[0] += prevArc.args[5] - prev.args[prev.args.length - 2];
  400. relSubpoint[1] += prevArc.args[6] - prev.args[prev.args.length - 1];
  401. prev.command = 'a';
  402. prev.args = prevArc.args;
  403. item.base = prev.coords = prevArc.coords;
  404. }
  405. arc = output.shift();
  406. if (arcCurves.length == 1) {
  407. item.sdata = sdata.slice(); // preserve curve data for future checks
  408. } else if (arcCurves.length - 1 - hasPrev > 0) {
  409. // filter out consumed next items
  410. path.splice.apply(
  411. path,
  412. [index + 1, arcCurves.length - 1 - hasPrev].concat(output)
  413. );
  414. }
  415. if (!arc) return false;
  416. command = 'a';
  417. data = arc.args;
  418. item.coords = arc.coords;
  419. }
  420. }
  421. // Rounding relative coordinates, taking in account accummulating error
  422. // to get closer to absolute coordinates. Sum of rounded value remains same:
  423. // l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2
  424. if (precision !== false) {
  425. if (
  426. command === 'm' ||
  427. command === 'l' ||
  428. command === 't' ||
  429. command === 'q' ||
  430. command === 's' ||
  431. command === 'c'
  432. ) {
  433. for (var i = data.length; i--; ) {
  434. data[i] += item.base[i % 2] - relSubpoint[i % 2];
  435. }
  436. } else if (command == 'h') {
  437. data[0] += item.base[0] - relSubpoint[0];
  438. } else if (command == 'v') {
  439. data[0] += item.base[1] - relSubpoint[1];
  440. } else if (command == 'a') {
  441. data[5] += item.base[0] - relSubpoint[0];
  442. data[6] += item.base[1] - relSubpoint[1];
  443. }
  444. roundData(data);
  445. if (command == 'h') relSubpoint[0] += data[0];
  446. else if (command == 'v') relSubpoint[1] += data[0];
  447. else {
  448. relSubpoint[0] += data[data.length - 2];
  449. relSubpoint[1] += data[data.length - 1];
  450. }
  451. roundData(relSubpoint);
  452. if (command === 'M' || command === 'm') {
  453. pathBase[0] = relSubpoint[0];
  454. pathBase[1] = relSubpoint[1];
  455. }
  456. }
  457. // convert straight curves into lines segments
  458. if (params.straightCurves) {
  459. if (
  460. (command === 'c' && isCurveStraightLine(data)) ||
  461. (command === 's' && isCurveStraightLine(sdata))
  462. ) {
  463. if (next && next.command == 's') makeLonghand(next, data); // fix up next curve
  464. command = 'l';
  465. data = data.slice(-2);
  466. } else if (command === 'q' && isCurveStraightLine(data)) {
  467. if (next && next.command == 't') makeLonghand(next, data); // fix up next curve
  468. command = 'l';
  469. data = data.slice(-2);
  470. } else if (
  471. command === 't' &&
  472. prev.command !== 'q' &&
  473. prev.command !== 't'
  474. ) {
  475. command = 'l';
  476. data = data.slice(-2);
  477. } else if (command === 'a' && (data[0] === 0 || data[1] === 0)) {
  478. command = 'l';
  479. data = data.slice(-2);
  480. }
  481. }
  482. // horizontal and vertical line shorthands
  483. // l 50 0 → h 50
  484. // l 0 50 → v 50
  485. if (params.lineShorthands && command === 'l') {
  486. if (data[1] === 0) {
  487. command = 'h';
  488. data.pop();
  489. } else if (data[0] === 0) {
  490. command = 'v';
  491. data.shift();
  492. }
  493. }
  494. // collapse repeated commands
  495. // h 20 h 30 -> h 50
  496. if (
  497. params.collapseRepeated &&
  498. hasMarkerMid === false &&
  499. (command === 'm' || command === 'h' || command === 'v') &&
  500. prev.command &&
  501. command == prev.command.toLowerCase() &&
  502. ((command != 'h' && command != 'v') ||
  503. prev.args[0] >= 0 == data[0] >= 0)
  504. ) {
  505. prev.args[0] += data[0];
  506. if (command != 'h' && command != 'v') {
  507. prev.args[1] += data[1];
  508. }
  509. prev.coords = item.coords;
  510. path[index] = prev;
  511. return false;
  512. }
  513. // convert curves into smooth shorthands
  514. if (params.curveSmoothShorthands && prev.command) {
  515. // curveto
  516. if (command === 'c') {
  517. // c + c → c + s
  518. if (
  519. prev.command === 'c' &&
  520. data[0] === -(prev.args[2] - prev.args[4]) &&
  521. data[1] === -(prev.args[3] - prev.args[5])
  522. ) {
  523. command = 's';
  524. data = data.slice(2);
  525. }
  526. // s + c → s + s
  527. else if (
  528. prev.command === 's' &&
  529. data[0] === -(prev.args[0] - prev.args[2]) &&
  530. data[1] === -(prev.args[1] - prev.args[3])
  531. ) {
  532. command = 's';
  533. data = data.slice(2);
  534. }
  535. // [^cs] + c → [^cs] + s
  536. else if (
  537. prev.command !== 'c' &&
  538. prev.command !== 's' &&
  539. data[0] === 0 &&
  540. data[1] === 0
  541. ) {
  542. command = 's';
  543. data = data.slice(2);
  544. }
  545. }
  546. // quadratic Bézier curveto
  547. else if (command === 'q') {
  548. // q + q → q + t
  549. if (
  550. prev.command === 'q' &&
  551. data[0] === prev.args[2] - prev.args[0] &&
  552. data[1] === prev.args[3] - prev.args[1]
  553. ) {
  554. command = 't';
  555. data = data.slice(2);
  556. }
  557. // t + q → t + t
  558. else if (
  559. prev.command === 't' &&
  560. data[2] === prev.args[0] &&
  561. data[3] === prev.args[1]
  562. ) {
  563. command = 't';
  564. data = data.slice(2);
  565. }
  566. }
  567. }
  568. // remove useless non-first path segments
  569. if (params.removeUseless && !maybeHasStrokeAndLinecap) {
  570. // l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0
  571. if (
  572. (command === 'l' ||
  573. command === 'h' ||
  574. command === 'v' ||
  575. command === 'q' ||
  576. command === 't' ||
  577. command === 'c' ||
  578. command === 's') &&
  579. data.every(function (i) {
  580. return i === 0;
  581. })
  582. ) {
  583. path[index] = prev;
  584. return false;
  585. }
  586. // a 25,25 -30 0,1 0,0
  587. if (command === 'a' && data[5] === 0 && data[6] === 0) {
  588. path[index] = prev;
  589. return false;
  590. }
  591. }
  592. item.command = command;
  593. item.args = data;
  594. prev = item;
  595. } else {
  596. // z resets coordinates
  597. relSubpoint[0] = pathBase[0];
  598. relSubpoint[1] = pathBase[1];
  599. if (prev.command === 'Z' || prev.command === 'z') return false;
  600. prev = item;
  601. }
  602. return true;
  603. });
  604. return path;
  605. }
  606. /**
  607. * Writes data in shortest form using absolute or relative coordinates.
  608. *
  609. * @param {Array} data input path data
  610. * @return {Boolean} output
  611. */
  612. function convertToMixed(path, params) {
  613. var prev = path[0];
  614. path = path.filter(function (item, index) {
  615. if (index == 0) return true;
  616. if (item.command === 'Z' || item.command === 'z') {
  617. prev = item;
  618. return true;
  619. }
  620. var command = item.command,
  621. data = item.args,
  622. adata = data.slice();
  623. if (
  624. command === 'm' ||
  625. command === 'l' ||
  626. command === 't' ||
  627. command === 'q' ||
  628. command === 's' ||
  629. command === 'c'
  630. ) {
  631. for (var i = adata.length; i--; ) {
  632. adata[i] += item.base[i % 2];
  633. }
  634. } else if (command == 'h') {
  635. adata[0] += item.base[0];
  636. } else if (command == 'v') {
  637. adata[0] += item.base[1];
  638. } else if (command == 'a') {
  639. adata[5] += item.base[0];
  640. adata[6] += item.base[1];
  641. }
  642. roundData(adata);
  643. var absoluteDataStr = cleanupOutData(adata, params),
  644. relativeDataStr = cleanupOutData(data, params);
  645. // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true.
  646. // v-20 -> V0
  647. // Don't convert if it fits following previous command.
  648. // l20 30-10-50 instead of l20 30L20 30
  649. if (
  650. params.forceAbsolutePath ||
  651. (absoluteDataStr.length < relativeDataStr.length &&
  652. !(
  653. params.negativeExtraSpace &&
  654. command == prev.command &&
  655. prev.command.charCodeAt(0) > 96 &&
  656. absoluteDataStr.length == relativeDataStr.length - 1 &&
  657. (data[0] < 0 ||
  658. (/^0\./.test(data[0]) && prev.args[prev.args.length - 1] % 1))
  659. ))
  660. ) {
  661. item.command = command.toUpperCase();
  662. item.args = adata;
  663. }
  664. prev = item;
  665. return true;
  666. });
  667. return path;
  668. }
  669. /**
  670. * Checks if curve is convex. Control points of such a curve must form
  671. * a convex quadrilateral with diagonals crosspoint inside of it.
  672. *
  673. * @param {Array} data input path data
  674. * @return {Boolean} output
  675. */
  676. function isConvex(data) {
  677. var center = getIntersection([
  678. 0,
  679. 0,
  680. data[2],
  681. data[3],
  682. data[0],
  683. data[1],
  684. data[4],
  685. data[5],
  686. ]);
  687. return (
  688. center &&
  689. data[2] < center[0] == center[0] < 0 &&
  690. data[3] < center[1] == center[1] < 0 &&
  691. data[4] < center[0] == center[0] < data[0] &&
  692. data[5] < center[1] == center[1] < data[1]
  693. );
  694. }
  695. /**
  696. * Computes lines equations by two points and returns their intersection point.
  697. *
  698. * @param {Array} coords 8 numbers for 4 pairs of coordinates (x,y)
  699. * @return {Array|undefined} output coordinate of lines' crosspoint
  700. */
  701. function getIntersection(coords) {
  702. // Prev line equation parameters.
  703. var a1 = coords[1] - coords[3], // y1 - y2
  704. b1 = coords[2] - coords[0], // x2 - x1
  705. c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1
  706. // Next line equation parameters
  707. a2 = coords[5] - coords[7], // y1 - y2
  708. b2 = coords[6] - coords[4], // x2 - x1
  709. c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1
  710. denom = a1 * b2 - a2 * b1;
  711. if (!denom) return; // parallel lines havn't an intersection
  712. var cross = [(b1 * c2 - b2 * c1) / denom, (a1 * c2 - a2 * c1) / -denom];
  713. if (
  714. !isNaN(cross[0]) &&
  715. !isNaN(cross[1]) &&
  716. isFinite(cross[0]) &&
  717. isFinite(cross[1])
  718. ) {
  719. return cross;
  720. }
  721. }
  722. /**
  723. * Decrease accuracy of floating-point numbers
  724. * in path data keeping a specified number of decimals.
  725. * Smart rounds values like 2.3491 to 2.35 instead of 2.349.
  726. * Doesn't apply "smartness" if the number precision fits already.
  727. *
  728. * @param {Array} data input data array
  729. * @return {Array} output data array
  730. */
  731. function strongRound(data) {
  732. for (var i = data.length; i-- > 0; ) {
  733. if (data[i].toFixed(precision) != data[i]) {
  734. var rounded = +data[i].toFixed(precision - 1);
  735. data[i] =
  736. +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error
  737. ? +data[i].toFixed(precision)
  738. : rounded;
  739. }
  740. }
  741. return data;
  742. }
  743. /**
  744. * Simple rounding function if precision is 0.
  745. *
  746. * @param {Array} data input data array
  747. * @return {Array} output data array
  748. */
  749. function round(data) {
  750. for (var i = data.length; i-- > 0; ) {
  751. data[i] = Math.round(data[i]);
  752. }
  753. return data;
  754. }
  755. /**
  756. * Checks if a curve is a straight line by measuring distance
  757. * from middle points to the line formed by end points.
  758. *
  759. * @param {Array} xs array of curve points x-coordinates
  760. * @param {Array} ys array of curve points y-coordinates
  761. * @return {Boolean}
  762. */
  763. function isCurveStraightLine(data) {
  764. // Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points.
  765. var i = data.length - 2,
  766. a = -data[i + 1], // y1 − y2 (y1 = 0)
  767. b = data[i], // x2 − x1 (x1 = 0)
  768. d = 1 / (a * a + b * b); // same part for all points
  769. if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case
  770. // Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²))
  771. while ((i -= 2) >= 0) {
  772. if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error)
  773. return false;
  774. }
  775. return true;
  776. }
  777. /**
  778. * Converts next curve from shorthand to full form using the current curve data.
  779. *
  780. * @param {Object} item curve to convert
  781. * @param {Array} data current curve data
  782. */
  783. function makeLonghand(item, data) {
  784. switch (item.command) {
  785. case 's':
  786. item.command = 'c';
  787. break;
  788. case 't':
  789. item.command = 'q';
  790. break;
  791. }
  792. item.args.unshift(
  793. data[data.length - 2] - data[data.length - 4],
  794. data[data.length - 1] - data[data.length - 3]
  795. );
  796. return item;
  797. }
  798. /**
  799. * Returns distance between two points
  800. *
  801. * @param {Array} point1 first point coordinates
  802. * @param {Array} point2 second point coordinates
  803. * @return {Number} distance
  804. */
  805. function getDistance(point1, point2) {
  806. return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]);
  807. }
  808. /**
  809. * Returns coordinates of the curve point corresponding to the certain t
  810. * a·(1 - t)³·p1 + b·(1 - t)²·t·p2 + c·(1 - t)·t²·p3 + d·t³·p4,
  811. * where pN are control points and p1 is zero due to relative coordinates.
  812. *
  813. * @param {Array} curve array of curve points coordinates
  814. * @param {Number} t parametric position from 0 to 1
  815. * @return {Array} Point coordinates
  816. */
  817. function getCubicBezierPoint(curve, t) {
  818. var sqrT = t * t,
  819. cubT = sqrT * t,
  820. mt = 1 - t,
  821. sqrMt = mt * mt;
  822. return [
  823. 3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4],
  824. 3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5],
  825. ];
  826. }
  827. /**
  828. * Finds circle by 3 points of the curve and checks if the curve fits the found circle.
  829. *
  830. * @param {Array} curve
  831. * @return {Object|undefined} circle
  832. */
  833. function findCircle(curve) {
  834. var midPoint = getCubicBezierPoint(curve, 1 / 2),
  835. m1 = [midPoint[0] / 2, midPoint[1] / 2],
  836. m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2],
  837. center = getIntersection([
  838. m1[0],
  839. m1[1],
  840. m1[0] + m1[1],
  841. m1[1] - m1[0],
  842. m2[0],
  843. m2[1],
  844. m2[0] + (m2[1] - midPoint[1]),
  845. m2[1] - (m2[0] - midPoint[0]),
  846. ]),
  847. radius = center && getDistance([0, 0], center),
  848. tolerance = Math.min(arcThreshold * error, (arcTolerance * radius) / 100);
  849. if (
  850. center &&
  851. radius < 1e15 &&
  852. [1 / 4, 3 / 4].every(function (point) {
  853. return (
  854. Math.abs(
  855. getDistance(getCubicBezierPoint(curve, point), center) - radius
  856. ) <= tolerance
  857. );
  858. })
  859. )
  860. return { center: center, radius: radius };
  861. }
  862. /**
  863. * Checks if a curve fits the given circle.
  864. *
  865. * @param {Object} circle
  866. * @param {Array} curve
  867. * @return {Boolean}
  868. */
  869. function isArc(curve, circle) {
  870. var tolerance = Math.min(
  871. arcThreshold * error,
  872. (arcTolerance * circle.radius) / 100
  873. );
  874. return [0, 1 / 4, 1 / 2, 3 / 4, 1].every(function (point) {
  875. return (
  876. Math.abs(
  877. getDistance(getCubicBezierPoint(curve, point), circle.center) -
  878. circle.radius
  879. ) <= tolerance
  880. );
  881. });
  882. }
  883. /**
  884. * Checks if a previous curve fits the given circle.
  885. *
  886. * @param {Object} circle
  887. * @param {Array} curve
  888. * @return {Boolean}
  889. */
  890. function isArcPrev(curve, circle) {
  891. return isArc(curve, {
  892. center: [circle.center[0] + curve[4], circle.center[1] + curve[5]],
  893. radius: circle.radius,
  894. });
  895. }
  896. /**
  897. * Finds angle of a curve fitting the given arc.
  898. * @param {Array} curve
  899. * @param {Object} relCircle
  900. * @return {Number} angle
  901. */
  902. function findArcAngle(curve, relCircle) {
  903. var x1 = -relCircle.center[0],
  904. y1 = -relCircle.center[1],
  905. x2 = curve[4] - relCircle.center[0],
  906. y2 = curve[5] - relCircle.center[1];
  907. return Math.acos(
  908. (x1 * x2 + y1 * y2) / Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2))
  909. );
  910. }
  911. /**
  912. * Converts given path data to string.
  913. *
  914. * @param {Object} params
  915. * @param {Array} pathData
  916. * @return {String}
  917. */
  918. function data2Path(params, pathData) {
  919. return pathData.reduce(function (pathString, item) {
  920. var strData = '';
  921. if (item.args) {
  922. strData = cleanupOutData(roundData(item.args.slice()), params);
  923. }
  924. return pathString + item.command + strData;
  925. }, '');
  926. }