normalize.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import { GeometryUtil } from '../util';
  2. function rotate(x, y, rad) {
  3. return {
  4. x: x * Math.cos(rad) - y * Math.sin(rad),
  5. y: x * Math.sin(rad) + y * Math.cos(rad),
  6. };
  7. }
  8. function q2c(x1, y1, ax, ay, x2, y2) {
  9. const v13 = 1 / 3;
  10. const v23 = 2 / 3;
  11. return [
  12. v13 * x1 + v23 * ax,
  13. v13 * y1 + v23 * ay,
  14. v13 * x2 + v23 * ax,
  15. v13 * y2 + v23 * ay,
  16. x2,
  17. y2,
  18. ];
  19. }
  20. function a2c(x1, y1, rx, ry, angle, largeArcFlag, sweepFlag, x2, y2, recursive) {
  21. // for more information of where this math came from visit:
  22. // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
  23. const v120 = (Math.PI * 120) / 180;
  24. const rad = (Math.PI / 180) * (+angle || 0);
  25. let res = [];
  26. let xy;
  27. let f1;
  28. let f2;
  29. let cx;
  30. let cy;
  31. if (!recursive) {
  32. xy = rotate(x1, y1, -rad);
  33. x1 = xy.x; // eslint-disable-line
  34. y1 = xy.y; // eslint-disable-line
  35. xy = rotate(x2, y2, -rad);
  36. x2 = xy.x; // eslint-disable-line
  37. y2 = xy.y; // eslint-disable-line
  38. const x = (x1 - x2) / 2;
  39. const y = (y1 - y2) / 2;
  40. let h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
  41. if (h > 1) {
  42. h = Math.sqrt(h);
  43. rx = h * rx; // eslint-disable-line
  44. ry = h * ry; // eslint-disable-line
  45. }
  46. const rx2 = rx * rx;
  47. const ry2 = ry * ry;
  48. const k = (largeArcFlag === sweepFlag ? -1 : 1) *
  49. Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
  50. cx = (k * rx * y) / ry + (x1 + x2) / 2;
  51. cy = (k * -ry * x) / rx + (y1 + y2) / 2;
  52. f1 = Math.asin((y1 - cy) / ry);
  53. f2 = Math.asin((y2 - cy) / ry);
  54. f1 = x1 < cx ? Math.PI - f1 : f1;
  55. f2 = x2 < cx ? Math.PI - f2 : f2;
  56. if (f1 < 0) {
  57. f1 = Math.PI * 2 + f1;
  58. }
  59. if (f2 < 0) {
  60. f2 = Math.PI * 2 + f2;
  61. }
  62. if (sweepFlag && f1 > f2) {
  63. f1 -= Math.PI * 2;
  64. }
  65. if (!sweepFlag && f2 > f1) {
  66. f2 -= Math.PI * 2;
  67. }
  68. }
  69. else {
  70. f1 = recursive[0];
  71. f2 = recursive[1];
  72. cx = recursive[2];
  73. cy = recursive[3];
  74. }
  75. let df = f2 - f1;
  76. if (Math.abs(df) > v120) {
  77. const f2old = f2;
  78. const x2old = x2;
  79. const y2old = y2;
  80. f2 = f1 + v120 * (sweepFlag && f2 > f1 ? 1 : -1);
  81. x2 = cx + rx * Math.cos(f2); // eslint-disable-line
  82. y2 = cy + ry * Math.sin(f2); // eslint-disable-line
  83. res = a2c(x2, y2, rx, ry, angle, 0, sweepFlag, x2old, y2old, [
  84. f2,
  85. f2old,
  86. cx,
  87. cy,
  88. ]);
  89. }
  90. df = f2 - f1;
  91. const c1 = Math.cos(f1);
  92. const s1 = Math.sin(f1);
  93. const c2 = Math.cos(f2);
  94. const s2 = Math.sin(f2);
  95. const t = Math.tan(df / 4);
  96. const hx = (4 / 3) * (rx * t);
  97. const hy = (4 / 3) * (ry * t);
  98. const m1 = [x1, y1];
  99. const m2 = [x1 + hx * s1, y1 - hy * c1];
  100. const m3 = [x2 + hx * s2, y2 - hy * c2];
  101. const m4 = [x2, y2];
  102. m2[0] = 2 * m1[0] - m2[0];
  103. m2[1] = 2 * m1[1] - m2[1];
  104. if (recursive) {
  105. return [m2, m3, m4].concat(res);
  106. }
  107. {
  108. res = [m2, m3, m4].concat(res).join().split(',');
  109. const newres = [];
  110. const ii = res.length;
  111. for (let i = 0; i < ii; i += 1) {
  112. newres[i] =
  113. i % 2
  114. ? rotate(+res[i - 1], +res[i], rad).y
  115. : rotate(+res[i], +res[i + 1], rad).x;
  116. }
  117. return newres;
  118. }
  119. }
  120. function parse(pathData) {
  121. if (!pathData) {
  122. return null;
  123. }
  124. const spaces = '\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029';
  125. // https://regexper.com/#%28%5Ba-z%5D%29%5B%5Cs%2C%5D*%28%28-%3F%5Cd*%5C.%3F%5C%5Cd*%28%3F%3Ae%5B%5C-%2B%5D%3F%5Cd%2B%29%3F%5B%5Cs%5D*%2C%3F%5B%5Cs%5D*%29%2B%29
  126. const segmentReg = new RegExp(`([a-z])[${spaces},]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?[${spaces}]*,?[${spaces}]*)+)`, // eslint-disable-line
  127. 'ig');
  128. // https://regexper.com/#%28-%3F%5Cd*%5C.%3F%5Cd*%28%3F%3Ae%5B%5C-%2B%5D%3F%5Cd%2B%29%3F%29%5B%5Cs%5D*%2C%3F%5B%5Cs%5D*
  129. const commandParamReg = new RegExp(
  130. // eslint-disable-next-line
  131. `(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)[${spaces}]*,?[${spaces}]*`, 'ig');
  132. const paramsCount = {
  133. a: 7,
  134. c: 6,
  135. h: 1,
  136. l: 2,
  137. m: 2,
  138. q: 4,
  139. s: 4,
  140. t: 2,
  141. v: 1,
  142. z: 0,
  143. };
  144. const segmetns = [];
  145. pathData.replace(segmentReg, (input, cmd, args) => {
  146. const params = [];
  147. let command = cmd.toLowerCase();
  148. args.replace(commandParamReg, (a, b) => {
  149. if (b) {
  150. params.push(+b);
  151. }
  152. return a;
  153. });
  154. if (command === 'm' && params.length > 2) {
  155. segmetns.push([cmd, ...params.splice(0, 2)]);
  156. command = 'l';
  157. cmd = cmd === 'm' ? 'l' : 'L'; // eslint-disable-line
  158. }
  159. const count = paramsCount[command];
  160. while (params.length >= count) {
  161. segmetns.push([cmd, ...params.splice(0, count)]);
  162. if (!count) {
  163. break;
  164. }
  165. }
  166. return input;
  167. });
  168. return segmetns;
  169. }
  170. function abs(pathString) {
  171. const pathArray = parse(pathString);
  172. // if invalid string, return 'M 0 0'
  173. if (!pathArray || !pathArray.length) {
  174. return [['M', 0, 0]];
  175. }
  176. let x = 0;
  177. let y = 0;
  178. let mx = 0;
  179. let my = 0;
  180. const segments = [];
  181. for (let i = 0, ii = pathArray.length; i < ii; i += 1) {
  182. const r = [];
  183. segments.push(r);
  184. const segment = pathArray[i];
  185. const command = segment[0];
  186. if (command !== command.toUpperCase()) {
  187. r[0] = command.toUpperCase();
  188. switch (r[0]) {
  189. case 'A':
  190. r[1] = segment[1];
  191. r[2] = segment[2];
  192. r[3] = segment[3];
  193. r[4] = segment[4];
  194. r[5] = segment[5];
  195. r[6] = +segment[6] + x;
  196. r[7] = +segment[7] + y;
  197. break;
  198. case 'V':
  199. r[1] = +segment[1] + y;
  200. break;
  201. case 'H':
  202. r[1] = +segment[1] + x;
  203. break;
  204. case 'M':
  205. mx = +segment[1] + x;
  206. my = +segment[2] + y;
  207. for (let j = 1, jj = segment.length; j < jj; j += 1) {
  208. r[j] = +segment[j] + (j % 2 ? x : y);
  209. }
  210. break;
  211. default:
  212. for (let j = 1, jj = segment.length; j < jj; j += 1) {
  213. r[j] = +segment[j] + (j % 2 ? x : y);
  214. }
  215. break;
  216. }
  217. }
  218. else {
  219. for (let j = 0, jj = segment.length; j < jj; j += 1) {
  220. r[j] = segment[j];
  221. }
  222. }
  223. switch (r[0]) {
  224. case 'Z':
  225. x = +mx;
  226. y = +my;
  227. break;
  228. case 'H':
  229. x = r[1];
  230. break;
  231. case 'V':
  232. y = r[1];
  233. break;
  234. case 'M':
  235. mx = r[r.length - 2];
  236. my = r[r.length - 1];
  237. x = r[r.length - 2];
  238. y = r[r.length - 1];
  239. break;
  240. default:
  241. x = r[r.length - 2];
  242. y = r[r.length - 1];
  243. break;
  244. }
  245. }
  246. return segments;
  247. }
  248. function normalize(path) {
  249. const pathArray = abs(path);
  250. const attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null };
  251. function processPath(path, d, pcom) {
  252. let nx;
  253. let ny;
  254. if (!path) {
  255. return ['C', d.x, d.y, d.x, d.y, d.x, d.y];
  256. }
  257. if (!(path[0] in { T: 1, Q: 1 })) {
  258. d.qx = null;
  259. d.qy = null;
  260. }
  261. switch (path[0]) {
  262. case 'M':
  263. d.X = path[1];
  264. d.Y = path[2];
  265. break;
  266. case 'A':
  267. if (parseFloat(path[1]) === 0 || parseFloat(path[2]) === 0) {
  268. // https://www.w3.org/TR/SVG/paths.html#ArcOutOfRangeParameters
  269. // "If either rx or ry is 0, then this arc is treated as a
  270. // straight line segment (a "lineto") joining the endpoints."
  271. return ['L', path[6], path[7]];
  272. }
  273. return ['C'].concat(a2c.apply(0, [d.x, d.y].concat(path.slice(1))));
  274. case 'S':
  275. if (pcom === 'C' || pcom === 'S') {
  276. // In 'S' case we have to take into account, if the previous command is C/S.
  277. nx = d.x * 2 - d.bx; // And reflect the previous
  278. ny = d.y * 2 - d.by; // command's control point relative to the current point.
  279. }
  280. else {
  281. // or some else or nothing
  282. nx = d.x;
  283. ny = d.y;
  284. }
  285. return ['C', nx, ny].concat(path.slice(1));
  286. case 'T':
  287. if (pcom === 'Q' || pcom === 'T') {
  288. // In 'T' case we have to take into account, if the previous command is Q/T.
  289. d.qx = d.x * 2 - d.qx; // And make a reflection similar
  290. d.qy = d.y * 2 - d.qy; // to case 'S'.
  291. }
  292. else {
  293. // or something else or nothing
  294. d.qx = d.x;
  295. d.qy = d.y;
  296. }
  297. return ['C'].concat(q2c(d.x, d.y, d.qx, d.qy, path[1], path[2]));
  298. case 'Q':
  299. d.qx = path[1];
  300. d.qy = path[2];
  301. return ['C'].concat(q2c(d.x, d.y, path[1], path[2], path[3], path[4]));
  302. case 'H':
  303. return ['L'].concat(path[1], d.y);
  304. case 'V':
  305. return ['L'].concat(d.x, path[1]);
  306. case 'L':
  307. break;
  308. case 'Z':
  309. break;
  310. default:
  311. break;
  312. }
  313. return path;
  314. }
  315. function fixArc(pp, i) {
  316. if (pp[i].length > 7) {
  317. pp[i].shift();
  318. const pi = pp[i];
  319. while (pi.length) {
  320. // if created multiple 'C's, their original seg is saved
  321. commands[i] = 'A';
  322. i += 1; // eslint-disable-line
  323. pp.splice(i, 0, ['C'].concat(pi.splice(0, 6)));
  324. }
  325. pp.splice(i, 1);
  326. ii = pathArray.length;
  327. }
  328. }
  329. const commands = []; // path commands of original path p
  330. let prevCommand = ''; // holder for previous path command of original path
  331. let ii = pathArray.length;
  332. for (let i = 0; i < ii; i += 1) {
  333. let command = ''; // temporary holder for original path command
  334. if (pathArray[i]) {
  335. command = pathArray[i][0]; // save current path command
  336. }
  337. if (command !== 'C') {
  338. // C is not saved yet, because it may be result of conversion
  339. commands[i] = command; // Save current path command
  340. if (i > 0) {
  341. prevCommand = commands[i - 1]; // Get previous path command pcom
  342. }
  343. }
  344. // Previous path command is inputted to processPath
  345. pathArray[i] = processPath(pathArray[i], attrs, prevCommand);
  346. if (commands[i] !== 'A' && command === 'C') {
  347. commands[i] = 'C'; // 'A' is the only command
  348. }
  349. // which may produce multiple 'C's
  350. // so we have to make sure that 'C' is also 'C' in original path
  351. fixArc(pathArray, i); // fixArc adds also the right amount of 'A's to pcoms
  352. const seg = pathArray[i];
  353. const seglen = seg.length;
  354. attrs.x = seg[seglen - 2];
  355. attrs.y = seg[seglen - 1];
  356. attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x;
  357. attrs.by = parseFloat(seg[seglen - 3]) || attrs.y;
  358. }
  359. // make sure normalized path data string starts with an M segment
  360. if (!pathArray[0][0] || pathArray[0][0] !== 'M') {
  361. pathArray.unshift(['M', 0, 0]);
  362. }
  363. return pathArray;
  364. }
  365. /**
  366. * Converts provided SVG path data string into a normalized path data string.
  367. *
  368. * The normalization uses a restricted subset of path commands; all segments
  369. * are translated into lineto, curveto, moveto, and closepath segments.
  370. *
  371. * Relative path commands are changed into their absolute counterparts,
  372. * and chaining of coordinates is disallowed.
  373. *
  374. * The function will always return a valid path data string; if an input
  375. * string cannot be normalized, 'M 0 0' is returned.
  376. */
  377. export function normalizePathData(pathData) {
  378. return normalize(pathData)
  379. .map((segment) => segment.map((item) => typeof item === 'string' ? item : GeometryUtil.round(item, 2)))
  380. .join(',')
  381. .split(',')
  382. .join(' ');
  383. }
  384. //# sourceMappingURL=normalize.js.map