path.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import Path, { PathProps } from '../graphic/Path';
  2. import PathProxy from '../core/PathProxy';
  3. import transformPath from './transformPath';
  4. import { VectorArray } from '../core/vector';
  5. import { MatrixArray } from '../core/matrix';
  6. import { extend } from '../core/util';
  7. // command chars
  8. // const cc = [
  9. // 'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z',
  10. // 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'
  11. // ];
  12. const mathSqrt = Math.sqrt;
  13. const mathSin = Math.sin;
  14. const mathCos = Math.cos;
  15. const PI = Math.PI;
  16. function vMag(v: VectorArray): number {
  17. return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
  18. };
  19. function vRatio(u: VectorArray, v: VectorArray): number {
  20. return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v));
  21. };
  22. function vAngle(u: VectorArray, v: VectorArray): number {
  23. return (u[0] * v[1] < u[1] * v[0] ? -1 : 1)
  24. * Math.acos(vRatio(u, v));
  25. };
  26. function processArc(
  27. x1: number, y1: number, x2: number, y2: number, fa: number, fs: number,
  28. rx: number, ry: number, psiDeg: number, cmd: number, path: PathProxy
  29. ) {
  30. // https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
  31. const psi = psiDeg * (PI / 180.0);
  32. const xp = mathCos(psi) * (x1 - x2) / 2.0
  33. + mathSin(psi) * (y1 - y2) / 2.0;
  34. const yp = -1 * mathSin(psi) * (x1 - x2) / 2.0
  35. + mathCos(psi) * (y1 - y2) / 2.0;
  36. const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry);
  37. if (lambda > 1) {
  38. rx *= mathSqrt(lambda);
  39. ry *= mathSqrt(lambda);
  40. }
  41. const f = (fa === fs ? -1 : 1)
  42. * mathSqrt((((rx * rx) * (ry * ry))
  43. - ((rx * rx) * (yp * yp))
  44. - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp)
  45. + (ry * ry) * (xp * xp))
  46. ) || 0;
  47. const cxp = f * rx * yp / ry;
  48. const cyp = f * -ry * xp / rx;
  49. const cx = (x1 + x2) / 2.0
  50. + mathCos(psi) * cxp
  51. - mathSin(psi) * cyp;
  52. const cy = (y1 + y2) / 2.0
  53. + mathSin(psi) * cxp
  54. + mathCos(psi) * cyp;
  55. const theta = vAngle([ 1, 0 ], [ (xp - cxp) / rx, (yp - cyp) / ry ]);
  56. const u = [ (xp - cxp) / rx, (yp - cyp) / ry ];
  57. const v = [ (-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry ];
  58. let dTheta = vAngle(u, v);
  59. if (vRatio(u, v) <= -1) {
  60. dTheta = PI;
  61. }
  62. if (vRatio(u, v) >= 1) {
  63. dTheta = 0;
  64. }
  65. if (dTheta < 0) {
  66. const n = Math.round(dTheta / PI * 1e6) / 1e6;
  67. // Convert to positive
  68. dTheta = PI * 2 + (n % 2) * PI;
  69. }
  70. path.addData(cmd, cx, cy, rx, ry, theta, dTheta, psi, fs);
  71. }
  72. const commandReg = /([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig;
  73. // Consider case:
  74. // (1) delimiter can be comma or space, where continuous commas
  75. // or spaces should be seen as one comma.
  76. // (2) value can be like:
  77. // '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983',
  78. // 'l-.5E1,54', '121-23-44-11' (no delimiter)
  79. const numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;
  80. // const valueSplitReg = /[\s,]+/;
  81. function createPathProxyFromString(data: string) {
  82. const path = new PathProxy();
  83. if (!data) {
  84. return path;
  85. }
  86. // const data = data.replace(/-/g, ' -')
  87. // .replace(/ /g, ' ')
  88. // .replace(/ /g, ',')
  89. // .replace(/,,/g, ',');
  90. // const n;
  91. // create pipes so that we can split the data
  92. // for (n = 0; n < cc.length; n++) {
  93. // cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
  94. // }
  95. // data = data.replace(/-/g, ',-');
  96. // create array
  97. // const arr = cs.split('|');
  98. // init context point
  99. let cpx = 0;
  100. let cpy = 0;
  101. let subpathX = cpx;
  102. let subpathY = cpy;
  103. let prevCmd;
  104. const CMD = PathProxy.CMD;
  105. // commandReg.lastIndex = 0;
  106. // const cmdResult;
  107. // while ((cmdResult = commandReg.exec(data)) != null) {
  108. // const cmdStr = cmdResult[1];
  109. // const cmdContent = cmdResult[2];
  110. const cmdList = data.match(commandReg);
  111. if (!cmdList) {
  112. // Invalid svg path.
  113. return path;
  114. }
  115. for (let l = 0; l < cmdList.length; l++) {
  116. const cmdText = cmdList[l];
  117. let cmdStr = cmdText.charAt(0);
  118. let cmd;
  119. // String#split is faster a little bit than String#replace or RegExp#exec.
  120. // const p = cmdContent.split(valueSplitReg);
  121. // const pLen = 0;
  122. // for (let i = 0; i < p.length; i++) {
  123. // // '' and other invalid str => NaN
  124. // const val = parseFloat(p[i]);
  125. // !isNaN(val) && (p[pLen++] = val);
  126. // }
  127. // Following code will convert string to number. So convert type to number here
  128. const p = cmdText.match(numberReg) as any[] as number[] || [];
  129. const pLen = p.length;
  130. for (let i = 0; i < pLen; i++) {
  131. p[i] = parseFloat(p[i] as any as string);
  132. }
  133. let off = 0;
  134. while (off < pLen) {
  135. let ctlPtx;
  136. let ctlPty;
  137. let rx;
  138. let ry;
  139. let psi;
  140. let fa;
  141. let fs;
  142. let x1 = cpx;
  143. let y1 = cpy;
  144. let len: number;
  145. let pathData: number[] | Float32Array;
  146. // convert l, H, h, V, and v to L
  147. switch (cmdStr) {
  148. case 'l':
  149. cpx += p[off++];
  150. cpy += p[off++];
  151. cmd = CMD.L;
  152. path.addData(cmd, cpx, cpy);
  153. break;
  154. case 'L':
  155. cpx = p[off++];
  156. cpy = p[off++];
  157. cmd = CMD.L;
  158. path.addData(cmd, cpx, cpy);
  159. break;
  160. case 'm':
  161. cpx += p[off++];
  162. cpy += p[off++];
  163. cmd = CMD.M;
  164. path.addData(cmd, cpx, cpy);
  165. subpathX = cpx;
  166. subpathY = cpy;
  167. cmdStr = 'l';
  168. break;
  169. case 'M':
  170. cpx = p[off++];
  171. cpy = p[off++];
  172. cmd = CMD.M;
  173. path.addData(cmd, cpx, cpy);
  174. subpathX = cpx;
  175. subpathY = cpy;
  176. cmdStr = 'L';
  177. break;
  178. case 'h':
  179. cpx += p[off++];
  180. cmd = CMD.L;
  181. path.addData(cmd, cpx, cpy);
  182. break;
  183. case 'H':
  184. cpx = p[off++];
  185. cmd = CMD.L;
  186. path.addData(cmd, cpx, cpy);
  187. break;
  188. case 'v':
  189. cpy += p[off++];
  190. cmd = CMD.L;
  191. path.addData(cmd, cpx, cpy);
  192. break;
  193. case 'V':
  194. cpy = p[off++];
  195. cmd = CMD.L;
  196. path.addData(cmd, cpx, cpy);
  197. break;
  198. case 'C':
  199. cmd = CMD.C;
  200. path.addData(
  201. cmd, p[off++], p[off++], p[off++], p[off++], p[off++], p[off++]
  202. );
  203. cpx = p[off - 2];
  204. cpy = p[off - 1];
  205. break;
  206. case 'c':
  207. cmd = CMD.C;
  208. path.addData(
  209. cmd,
  210. p[off++] + cpx, p[off++] + cpy,
  211. p[off++] + cpx, p[off++] + cpy,
  212. p[off++] + cpx, p[off++] + cpy
  213. );
  214. cpx += p[off - 2];
  215. cpy += p[off - 1];
  216. break;
  217. case 'S':
  218. ctlPtx = cpx;
  219. ctlPty = cpy;
  220. len = path.len();
  221. pathData = path.data;
  222. if (prevCmd === CMD.C) {
  223. ctlPtx += cpx - pathData[len - 4];
  224. ctlPty += cpy - pathData[len - 3];
  225. }
  226. cmd = CMD.C;
  227. x1 = p[off++];
  228. y1 = p[off++];
  229. cpx = p[off++];
  230. cpy = p[off++];
  231. path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy);
  232. break;
  233. case 's':
  234. ctlPtx = cpx;
  235. ctlPty = cpy;
  236. len = path.len();
  237. pathData = path.data;
  238. if (prevCmd === CMD.C) {
  239. ctlPtx += cpx - pathData[len - 4];
  240. ctlPty += cpy - pathData[len - 3];
  241. }
  242. cmd = CMD.C;
  243. x1 = cpx + p[off++];
  244. y1 = cpy + p[off++];
  245. cpx += p[off++];
  246. cpy += p[off++];
  247. path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy);
  248. break;
  249. case 'Q':
  250. x1 = p[off++];
  251. y1 = p[off++];
  252. cpx = p[off++];
  253. cpy = p[off++];
  254. cmd = CMD.Q;
  255. path.addData(cmd, x1, y1, cpx, cpy);
  256. break;
  257. case 'q':
  258. x1 = p[off++] + cpx;
  259. y1 = p[off++] + cpy;
  260. cpx += p[off++];
  261. cpy += p[off++];
  262. cmd = CMD.Q;
  263. path.addData(cmd, x1, y1, cpx, cpy);
  264. break;
  265. case 'T':
  266. ctlPtx = cpx;
  267. ctlPty = cpy;
  268. len = path.len();
  269. pathData = path.data;
  270. if (prevCmd === CMD.Q) {
  271. ctlPtx += cpx - pathData[len - 4];
  272. ctlPty += cpy - pathData[len - 3];
  273. }
  274. cpx = p[off++];
  275. cpy = p[off++];
  276. cmd = CMD.Q;
  277. path.addData(cmd, ctlPtx, ctlPty, cpx, cpy);
  278. break;
  279. case 't':
  280. ctlPtx = cpx;
  281. ctlPty = cpy;
  282. len = path.len();
  283. pathData = path.data;
  284. if (prevCmd === CMD.Q) {
  285. ctlPtx += cpx - pathData[len - 4];
  286. ctlPty += cpy - pathData[len - 3];
  287. }
  288. cpx += p[off++];
  289. cpy += p[off++];
  290. cmd = CMD.Q;
  291. path.addData(cmd, ctlPtx, ctlPty, cpx, cpy);
  292. break;
  293. case 'A':
  294. rx = p[off++];
  295. ry = p[off++];
  296. psi = p[off++];
  297. fa = p[off++];
  298. fs = p[off++];
  299. x1 = cpx, y1 = cpy;
  300. cpx = p[off++];
  301. cpy = p[off++];
  302. cmd = CMD.A;
  303. processArc(
  304. x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path
  305. );
  306. break;
  307. case 'a':
  308. rx = p[off++];
  309. ry = p[off++];
  310. psi = p[off++];
  311. fa = p[off++];
  312. fs = p[off++];
  313. x1 = cpx, y1 = cpy;
  314. cpx += p[off++];
  315. cpy += p[off++];
  316. cmd = CMD.A;
  317. processArc(
  318. x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path
  319. );
  320. break;
  321. }
  322. }
  323. if (cmdStr === 'z' || cmdStr === 'Z') {
  324. cmd = CMD.Z;
  325. path.addData(cmd);
  326. // z may be in the middle of the path.
  327. cpx = subpathX;
  328. cpy = subpathY;
  329. }
  330. prevCmd = cmd;
  331. }
  332. path.toStatic();
  333. return path;
  334. }
  335. type SVGPathOption = Omit<PathProps, 'shape' | 'buildPath'>
  336. interface InnerSVGPathOption extends PathProps {
  337. applyTransform?: (m: MatrixArray) => void
  338. }
  339. class SVGPath extends Path {
  340. applyTransform(m: MatrixArray) {}
  341. }
  342. function isPathProxy(path: PathProxy | CanvasRenderingContext2D): path is PathProxy {
  343. return (path as PathProxy).setData != null;
  344. }
  345. // TODO Optimize double memory cost problem
  346. function createPathOptions(str: string, opts: SVGPathOption): InnerSVGPathOption {
  347. const pathProxy = createPathProxyFromString(str);
  348. const innerOpts: InnerSVGPathOption = extend({}, opts);
  349. innerOpts.buildPath = function (path: PathProxy | CanvasRenderingContext2D) {
  350. if (isPathProxy(path)) {
  351. path.setData(pathProxy.data);
  352. // Svg and vml renderer don't have context
  353. const ctx = path.getContext();
  354. if (ctx) {
  355. path.rebuildPath(ctx, 1);
  356. }
  357. }
  358. else {
  359. const ctx = path;
  360. pathProxy.rebuildPath(ctx, 1);
  361. }
  362. };
  363. innerOpts.applyTransform = function (this: SVGPath, m: MatrixArray) {
  364. transformPath(pathProxy, m);
  365. this.dirtyShape();
  366. };
  367. return innerOpts;
  368. }
  369. /**
  370. * Create a Path object from path string data
  371. * http://www.w3.org/TR/SVG/paths.html#PathData
  372. * @param opts Other options
  373. */
  374. export function createFromString(str: string, opts?: SVGPathOption): SVGPath {
  375. // PENDING
  376. return new SVGPath(createPathOptions(str, opts));
  377. }
  378. /**
  379. * Create a Path class from path string data
  380. * @param str
  381. * @param opts Other options
  382. */
  383. export function extendFromString(str: string, defaultOpts?: SVGPathOption): typeof SVGPath {
  384. const innerOpts = createPathOptions(str, defaultOpts);
  385. class Sub extends SVGPath {
  386. constructor(opts: InnerSVGPathOption) {
  387. super(opts);
  388. this.applyTransform = innerOpts.applyTransform;
  389. this.buildPath = innerOpts.buildPath;
  390. }
  391. }
  392. return Sub;
  393. }
  394. /**
  395. * Merge multiple paths
  396. */
  397. // TODO Apply transform
  398. // TODO stroke dash
  399. // TODO Optimize double memory cost problem
  400. export function mergePath(pathEls: Path[], opts: PathProps) {
  401. const pathList: PathProxy[] = [];
  402. const len = pathEls.length;
  403. for (let i = 0; i < len; i++) {
  404. const pathEl = pathEls[i];
  405. pathList.push(pathEl.getUpdatedPathProxy(true));
  406. }
  407. const pathBundle = new Path(opts);
  408. // Need path proxy.
  409. pathBundle.createPathProxy();
  410. pathBundle.buildPath = function (path: PathProxy | CanvasRenderingContext2D) {
  411. if (isPathProxy(path)) {
  412. path.appendPath(pathList);
  413. // Svg and vml renderer don't have context
  414. const ctx = path.getContext();
  415. if (ctx) {
  416. // Path bundle not support percent draw.
  417. path.rebuildPath(ctx, 1);
  418. }
  419. }
  420. };
  421. return pathBundle;
  422. }
  423. /**
  424. * Clone a path.
  425. */
  426. export function clonePath(sourcePath: Path, opts?: {
  427. /**
  428. * If bake global transform to path.
  429. */
  430. bakeTransform?: boolean
  431. /**
  432. * Convert global transform to local.
  433. */
  434. toLocal?: boolean
  435. }) {
  436. opts = opts || {};
  437. const path = new Path();
  438. if (sourcePath.shape) {
  439. path.setShape(sourcePath.shape);
  440. }
  441. path.setStyle(sourcePath.style);
  442. if (opts.bakeTransform) {
  443. transformPath(path.path, sourcePath.getComputedTransform());
  444. }
  445. else {
  446. // TODO Copy getLocalTransform, updateTransform since they can be changed.
  447. if (opts.toLocal) {
  448. path.setLocalTransform(sourcePath.getComputedTransform());
  449. }
  450. else {
  451. path.copyTransform(sourcePath);
  452. }
  453. }
  454. // These methods may be overridden
  455. path.buildPath = sourcePath.buildPath;
  456. (path as SVGPath).applyTransform = (path as SVGPath).applyTransform;
  457. path.z = sourcePath.z;
  458. path.z2 = sourcePath.z2;
  459. path.zlevel = sourcePath.zlevel;
  460. return path;
  461. }