convertPathData.js 30 KB

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