PathProxy.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  1. /**
  2. * Path 代理,可以在`buildPath`中用于替代`ctx`, 会保存每个path操作的命令到pathCommands属性中
  3. * 可以用于 isInsidePath 判断以及获取boundingRect
  4. */
  5. // TODO getTotalLength, getPointAtLength, arcTo
  6. /* global Float32Array */
  7. import * as vec2 from './vector';
  8. import BoundingRect from './BoundingRect';
  9. import {devicePixelRatio as dpr} from '../config';
  10. import { fromLine, fromCubic, fromQuadratic, fromArc } from './bbox';
  11. import { cubicLength, cubicSubdivide, quadraticLength, quadraticSubdivide } from './curve';
  12. const CMD = {
  13. M: 1,
  14. L: 2,
  15. C: 3,
  16. Q: 4,
  17. A: 5,
  18. Z: 6,
  19. // Rect
  20. R: 7
  21. };
  22. // const CMD_MEM_SIZE = {
  23. // M: 3,
  24. // L: 3,
  25. // C: 7,
  26. // Q: 5,
  27. // A: 9,
  28. // R: 5,
  29. // Z: 1
  30. // };
  31. interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D {
  32. dpr?: number
  33. }
  34. const tmpOutX: number[] = [];
  35. const tmpOutY: number[] = [];
  36. const min: number[] = [];
  37. const max: number[] = [];
  38. const min2: number[] = [];
  39. const max2: number[] = [];
  40. const mathMin = Math.min;
  41. const mathMax = Math.max;
  42. const mathCos = Math.cos;
  43. const mathSin = Math.sin;
  44. const mathAbs = Math.abs;
  45. const PI = Math.PI;
  46. const PI2 = PI * 2;
  47. const hasTypedArray = typeof Float32Array !== 'undefined';
  48. const tmpAngles: number[] = [];
  49. function modPI2(radian: number) {
  50. // It's much more stable to mod N instedof PI
  51. const n = Math.round(radian / PI * 1e8) / 1e8;
  52. return (n % 2) * PI;
  53. }
  54. /**
  55. * Normalize start and end angles.
  56. * startAngle will be normalized to 0 ~ PI*2
  57. * sweepAngle(endAngle - startAngle) will be normalized to 0 ~ PI*2 if clockwise.
  58. * -PI*2 ~ 0 if anticlockwise.
  59. */
  60. export function normalizeArcAngles(angles: number[], anticlockwise: boolean): void {
  61. let newStartAngle = modPI2(angles[0]);
  62. if (newStartAngle < 0) {
  63. // Normlize to 0 - PI2
  64. newStartAngle += PI2;
  65. }
  66. let delta = newStartAngle - angles[0];
  67. let newEndAngle = angles[1];
  68. newEndAngle += delta;
  69. // https://github.com/chromium/chromium/blob/c20d681c9c067c4e15bb1408f17114b9e8cba294/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc#L184
  70. // Is circle
  71. if (!anticlockwise && newEndAngle - newStartAngle >= PI2) {
  72. newEndAngle = newStartAngle + PI2;
  73. }
  74. else if (anticlockwise && newStartAngle - newEndAngle >= PI2) {
  75. newEndAngle = newStartAngle - PI2;
  76. }
  77. // Make startAngle < endAngle when clockwise, otherwise endAngle < startAngle.
  78. // The sweep angle can never been larger than P2.
  79. else if (!anticlockwise && newStartAngle > newEndAngle) {
  80. newEndAngle = newStartAngle + (PI2 - modPI2(newStartAngle - newEndAngle));
  81. }
  82. else if (anticlockwise && newStartAngle < newEndAngle) {
  83. newEndAngle = newStartAngle - (PI2 - modPI2(newEndAngle - newStartAngle));
  84. }
  85. angles[0] = newStartAngle;
  86. angles[1] = newEndAngle;
  87. }
  88. export default class PathProxy {
  89. dpr = 1
  90. data: number[] | Float32Array
  91. /**
  92. * Version is for tracking if the path has been changed.
  93. */
  94. private _version: number
  95. /**
  96. * If save path data.
  97. */
  98. private _saveData: boolean
  99. /**
  100. * If the line segment is too small to draw. It will be added to the pending pt.
  101. * It will be added if the subpath needs to be finished before stroke, fill, or starting a new subpath.
  102. */
  103. private _pendingPtX: number;
  104. private _pendingPtY: number;
  105. // Distance of pending pt to previous point.
  106. // 0 if there is no pending point.
  107. // Only update the pending pt when distance is larger.
  108. private _pendingPtDist: number;
  109. private _ctx: ExtendedCanvasRenderingContext2D
  110. private _xi = 0
  111. private _yi = 0
  112. private _x0 = 0
  113. private _y0 = 0
  114. private _len = 0
  115. // Calculating path len and seg len.
  116. private _pathSegLen: number[]
  117. private _pathLen: number
  118. // Unit x, Unit y. Provide for avoiding drawing that too short line segment
  119. private _ux: number
  120. private _uy: number
  121. static CMD = CMD
  122. constructor(notSaveData?: boolean) {
  123. if (notSaveData) {
  124. this._saveData = false;
  125. }
  126. if (this._saveData) {
  127. this.data = [];
  128. }
  129. }
  130. increaseVersion() {
  131. this._version++;
  132. }
  133. /**
  134. * Version can be used outside for compare if the path is changed.
  135. * For example to determine if need to update svg d str in svg renderer.
  136. */
  137. getVersion() {
  138. return this._version;
  139. }
  140. /**
  141. * @readOnly
  142. */
  143. setScale(sx: number, sy: number, segmentIgnoreThreshold?: number) {
  144. // Compat. Previously there is no segmentIgnoreThreshold.
  145. segmentIgnoreThreshold = segmentIgnoreThreshold || 0;
  146. if (segmentIgnoreThreshold > 0) {
  147. this._ux = mathAbs(segmentIgnoreThreshold / dpr / sx) || 0;
  148. this._uy = mathAbs(segmentIgnoreThreshold / dpr / sy) || 0;
  149. }
  150. }
  151. setDPR(dpr: number) {
  152. this.dpr = dpr;
  153. }
  154. setContext(ctx: ExtendedCanvasRenderingContext2D) {
  155. this._ctx = ctx;
  156. }
  157. getContext(): ExtendedCanvasRenderingContext2D {
  158. return this._ctx;
  159. }
  160. beginPath() {
  161. this._ctx && this._ctx.beginPath();
  162. this.reset();
  163. return this;
  164. }
  165. /**
  166. * Reset path data.
  167. */
  168. reset() {
  169. // Reset
  170. if (this._saveData) {
  171. this._len = 0;
  172. }
  173. if (this._pathSegLen) {
  174. this._pathSegLen = null;
  175. this._pathLen = 0;
  176. }
  177. // Update version
  178. this._version++;
  179. }
  180. moveTo(x: number, y: number) {
  181. // Add pending point for previous path.
  182. this._drawPendingPt();
  183. this.addData(CMD.M, x, y);
  184. this._ctx && this._ctx.moveTo(x, y);
  185. // x0, y0, xi, yi 是记录在 _dashedXXXXTo 方法中使用
  186. // xi, yi 记录当前点, x0, y0 在 closePath 的时候回到起始点。
  187. // 有可能在 beginPath 之后直接调用 lineTo,这时候 x0, y0 需要
  188. // 在 lineTo 方法中记录,这里先不考虑这种情况,dashed line 也只在 IE10- 中不支持
  189. this._x0 = x;
  190. this._y0 = y;
  191. this._xi = x;
  192. this._yi = y;
  193. return this;
  194. }
  195. lineTo(x: number, y: number) {
  196. const dx = mathAbs(x - this._xi);
  197. const dy = mathAbs(y - this._yi);
  198. const exceedUnit = dx > this._ux || dy > this._uy;
  199. this.addData(CMD.L, x, y);
  200. if (this._ctx && exceedUnit) {
  201. this._ctx.lineTo(x, y);
  202. }
  203. if (exceedUnit) {
  204. this._xi = x;
  205. this._yi = y;
  206. this._pendingPtDist = 0;
  207. }
  208. else {
  209. const d2 = dx * dx + dy * dy;
  210. // Only use the farthest pending point.
  211. if (d2 > this._pendingPtDist) {
  212. this._pendingPtX = x;
  213. this._pendingPtY = y;
  214. this._pendingPtDist = d2;
  215. }
  216. }
  217. return this;
  218. }
  219. bezierCurveTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) {
  220. this._drawPendingPt();
  221. this.addData(CMD.C, x1, y1, x2, y2, x3, y3);
  222. if (this._ctx) {
  223. this._ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
  224. }
  225. this._xi = x3;
  226. this._yi = y3;
  227. return this;
  228. }
  229. quadraticCurveTo(x1: number, y1: number, x2: number, y2: number) {
  230. this._drawPendingPt();
  231. this.addData(CMD.Q, x1, y1, x2, y2);
  232. if (this._ctx) {
  233. this._ctx.quadraticCurveTo(x1, y1, x2, y2);
  234. }
  235. this._xi = x2;
  236. this._yi = y2;
  237. return this;
  238. }
  239. arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise?: boolean) {
  240. this._drawPendingPt();
  241. tmpAngles[0] = startAngle;
  242. tmpAngles[1] = endAngle;
  243. normalizeArcAngles(tmpAngles, anticlockwise);
  244. startAngle = tmpAngles[0];
  245. endAngle = tmpAngles[1];
  246. let delta = endAngle - startAngle;
  247. this.addData(
  248. CMD.A, cx, cy, r, r, startAngle, delta, 0, anticlockwise ? 0 : 1
  249. );
  250. this._ctx && this._ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);
  251. this._xi = mathCos(endAngle) * r + cx;
  252. this._yi = mathSin(endAngle) * r + cy;
  253. return this;
  254. }
  255. // TODO
  256. arcTo(x1: number, y1: number, x2: number, y2: number, radius: number) {
  257. this._drawPendingPt();
  258. if (this._ctx) {
  259. this._ctx.arcTo(x1, y1, x2, y2, radius);
  260. }
  261. return this;
  262. }
  263. // TODO
  264. rect(x: number, y: number, w: number, h: number) {
  265. this._drawPendingPt();
  266. this._ctx && this._ctx.rect(x, y, w, h);
  267. this.addData(CMD.R, x, y, w, h);
  268. return this;
  269. }
  270. closePath() {
  271. // Add pending point for previous path.
  272. this._drawPendingPt();
  273. this.addData(CMD.Z);
  274. const ctx = this._ctx;
  275. const x0 = this._x0;
  276. const y0 = this._y0;
  277. if (ctx) {
  278. ctx.closePath();
  279. }
  280. this._xi = x0;
  281. this._yi = y0;
  282. return this;
  283. }
  284. fill(ctx: CanvasRenderingContext2D) {
  285. ctx && ctx.fill();
  286. this.toStatic();
  287. }
  288. stroke(ctx: CanvasRenderingContext2D) {
  289. ctx && ctx.stroke();
  290. this.toStatic();
  291. }
  292. len() {
  293. return this._len;
  294. }
  295. setData(data: Float32Array | number[]) {
  296. const len = data.length;
  297. if (!(this.data && this.data.length === len) && hasTypedArray) {
  298. this.data = new Float32Array(len);
  299. }
  300. for (let i = 0; i < len; i++) {
  301. this.data[i] = data[i];
  302. }
  303. this._len = len;
  304. }
  305. appendPath(path: PathProxy | PathProxy[]) {
  306. if (!(path instanceof Array)) {
  307. path = [path];
  308. }
  309. const len = path.length;
  310. let appendSize = 0;
  311. let offset = this._len;
  312. for (let i = 0; i < len; i++) {
  313. appendSize += path[i].len();
  314. }
  315. if (hasTypedArray && (this.data instanceof Float32Array)) {
  316. this.data = new Float32Array(offset + appendSize);
  317. }
  318. for (let i = 0; i < len; i++) {
  319. const appendPathData = path[i].data;
  320. for (let k = 0; k < appendPathData.length; k++) {
  321. this.data[offset++] = appendPathData[k];
  322. }
  323. }
  324. this._len = offset;
  325. }
  326. /**
  327. * 填充 Path 数据。
  328. * 尽量复用而不申明新的数组。大部分图形重绘的指令数据长度都是不变的。
  329. */
  330. addData(
  331. cmd: number,
  332. a?: number,
  333. b?: number,
  334. c?: number,
  335. d?: number,
  336. e?: number,
  337. f?: number,
  338. g?: number,
  339. h?: number
  340. ) {
  341. if (!this._saveData) {
  342. return;
  343. }
  344. let data = this.data;
  345. if (this._len + arguments.length > data.length) {
  346. // 因为之前的数组已经转换成静态的 Float32Array
  347. // 所以不够用时需要扩展一个新的动态数组
  348. this._expandData();
  349. data = this.data;
  350. }
  351. for (let i = 0; i < arguments.length; i++) {
  352. data[this._len++] = arguments[i];
  353. }
  354. }
  355. private _drawPendingPt() {
  356. if (this._pendingPtDist > 0) {
  357. this._ctx && this._ctx.lineTo(this._pendingPtX, this._pendingPtY);
  358. this._pendingPtDist = 0;
  359. }
  360. }
  361. private _expandData() {
  362. // Only if data is Float32Array
  363. if (!(this.data instanceof Array)) {
  364. const newData = [];
  365. for (let i = 0; i < this._len; i++) {
  366. newData[i] = this.data[i];
  367. }
  368. this.data = newData;
  369. }
  370. }
  371. /**
  372. * Convert dynamic array to static Float32Array
  373. *
  374. * It will still use a normal array if command buffer length is less than 10
  375. * Because Float32Array itself may take more memory than a normal array.
  376. *
  377. * 10 length will make sure at least one M command and one A(arc) command.
  378. */
  379. toStatic() {
  380. if (!this._saveData) {
  381. return;
  382. }
  383. this._drawPendingPt();
  384. const data = this.data;
  385. if (data instanceof Array) {
  386. data.length = this._len;
  387. if (hasTypedArray && this._len > 11) {
  388. this.data = new Float32Array(data);
  389. }
  390. }
  391. }
  392. getBoundingRect() {
  393. min[0] = min[1] = min2[0] = min2[1] = Number.MAX_VALUE;
  394. max[0] = max[1] = max2[0] = max2[1] = -Number.MAX_VALUE;
  395. const data = this.data;
  396. let xi = 0;
  397. let yi = 0;
  398. let x0 = 0;
  399. let y0 = 0;
  400. let i;
  401. for (i = 0; i < this._len;) {
  402. const cmd = data[i++] as number;
  403. const isFirst = i === 1;
  404. if (isFirst) {
  405. // 如果第一个命令是 L, C, Q
  406. // 则 previous point 同绘制命令的第一个 point
  407. // 第一个命令为 Arc 的情况下会在后面特殊处理
  408. xi = data[i];
  409. yi = data[i + 1];
  410. x0 = xi;
  411. y0 = yi;
  412. }
  413. switch (cmd) {
  414. case CMD.M:
  415. // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
  416. // 在 closePath 的时候使用
  417. xi = x0 = data[i++];
  418. yi = y0 = data[i++];
  419. min2[0] = x0;
  420. min2[1] = y0;
  421. max2[0] = x0;
  422. max2[1] = y0;
  423. break;
  424. case CMD.L:
  425. fromLine(xi, yi, data[i], data[i + 1], min2, max2);
  426. xi = data[i++];
  427. yi = data[i++];
  428. break;
  429. case CMD.C:
  430. fromCubic(
  431. xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1],
  432. min2, max2
  433. );
  434. xi = data[i++];
  435. yi = data[i++];
  436. break;
  437. case CMD.Q:
  438. fromQuadratic(
  439. xi, yi, data[i++], data[i++], data[i], data[i + 1],
  440. min2, max2
  441. );
  442. xi = data[i++];
  443. yi = data[i++];
  444. break;
  445. case CMD.A:
  446. const cx = data[i++];
  447. const cy = data[i++];
  448. const rx = data[i++];
  449. const ry = data[i++];
  450. const startAngle = data[i++];
  451. const endAngle = data[i++] + startAngle;
  452. // TODO Arc 旋转
  453. i += 1;
  454. const anticlockwise = !data[i++];
  455. if (isFirst) {
  456. // 直接使用 arc 命令
  457. // 第一个命令起点还未定义
  458. x0 = mathCos(startAngle) * rx + cx;
  459. y0 = mathSin(startAngle) * ry + cy;
  460. }
  461. fromArc(
  462. cx, cy, rx, ry, startAngle, endAngle,
  463. anticlockwise, min2, max2
  464. );
  465. xi = mathCos(endAngle) * rx + cx;
  466. yi = mathSin(endAngle) * ry + cy;
  467. break;
  468. case CMD.R:
  469. x0 = xi = data[i++];
  470. y0 = yi = data[i++];
  471. const width = data[i++];
  472. const height = data[i++];
  473. // Use fromLine
  474. fromLine(x0, y0, x0 + width, y0 + height, min2, max2);
  475. break;
  476. case CMD.Z:
  477. xi = x0;
  478. yi = y0;
  479. break;
  480. }
  481. // Union
  482. vec2.min(min, min, min2);
  483. vec2.max(max, max, max2);
  484. }
  485. // No data
  486. if (i === 0) {
  487. min[0] = min[1] = max[0] = max[1] = 0;
  488. }
  489. return new BoundingRect(
  490. min[0], min[1], max[0] - min[0], max[1] - min[1]
  491. );
  492. }
  493. private _calculateLength(): number {
  494. const data = this.data;
  495. const len = this._len;
  496. const ux = this._ux;
  497. const uy = this._uy;
  498. let xi = 0;
  499. let yi = 0;
  500. let x0 = 0;
  501. let y0 = 0;
  502. if (!this._pathSegLen) {
  503. this._pathSegLen = [];
  504. }
  505. const pathSegLen = this._pathSegLen;
  506. let pathTotalLen = 0;
  507. let segCount = 0;
  508. for (let i = 0; i < len;) {
  509. const cmd = data[i++] as number;
  510. const isFirst = i === 1;
  511. if (isFirst) {
  512. // 如果第一个命令是 L, C, Q
  513. // 则 previous point 同绘制命令的第一个 point
  514. // 第一个命令为 Arc 的情况下会在后面特殊处理
  515. xi = data[i];
  516. yi = data[i + 1];
  517. x0 = xi;
  518. y0 = yi;
  519. }
  520. let l = -1;
  521. switch (cmd) {
  522. case CMD.M:
  523. // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
  524. // 在 closePath 的时候使用
  525. xi = x0 = data[i++];
  526. yi = y0 = data[i++];
  527. break;
  528. case CMD.L: {
  529. const x2 = data[i++];
  530. const y2 = data[i++];
  531. const dx = x2 - xi;
  532. const dy = y2 - yi;
  533. if (mathAbs(dx) > ux || mathAbs(dy) > uy || i === len - 1) {
  534. l = Math.sqrt(dx * dx + dy * dy);
  535. xi = x2;
  536. yi = y2;
  537. }
  538. break;
  539. }
  540. case CMD.C: {
  541. const x1 = data[i++];
  542. const y1 = data[i++];
  543. const x2 = data[i++];
  544. const y2 = data[i++];
  545. const x3 = data[i++];
  546. const y3 = data[i++];
  547. // TODO adaptive iteration
  548. l = cubicLength(xi, yi, x1, y1, x2, y2, x3, y3, 10);
  549. xi = x3;
  550. yi = y3;
  551. break;
  552. }
  553. case CMD.Q: {
  554. const x1 = data[i++];
  555. const y1 = data[i++];
  556. const x2 = data[i++];
  557. const y2 = data[i++];
  558. l = quadraticLength(xi, yi, x1, y1, x2, y2, 10);
  559. xi = x2;
  560. yi = y2;
  561. break;
  562. }
  563. case CMD.A:
  564. // TODO Arc 判断的开销比较大
  565. const cx = data[i++];
  566. const cy = data[i++];
  567. const rx = data[i++];
  568. const ry = data[i++];
  569. const startAngle = data[i++];
  570. let delta = data[i++];
  571. const endAngle = delta + startAngle;
  572. // TODO Arc 旋转
  573. i += 1;
  574. if (isFirst) {
  575. // 直接使用 arc 命令
  576. // 第一个命令起点还未定义
  577. x0 = mathCos(startAngle) * rx + cx;
  578. y0 = mathSin(startAngle) * ry + cy;
  579. }
  580. // TODO Ellipse
  581. l = mathMax(rx, ry) * mathMin(PI2, Math.abs(delta));
  582. xi = mathCos(endAngle) * rx + cx;
  583. yi = mathSin(endAngle) * ry + cy;
  584. break;
  585. case CMD.R: {
  586. x0 = xi = data[i++];
  587. y0 = yi = data[i++];
  588. const width = data[i++];
  589. const height = data[i++];
  590. l = width * 2 + height * 2;
  591. break;
  592. }
  593. case CMD.Z: {
  594. const dx = x0 - xi;
  595. const dy = y0 - yi;
  596. l = Math.sqrt(dx * dx + dy * dy);
  597. xi = x0;
  598. yi = y0;
  599. break;
  600. }
  601. }
  602. if (l >= 0) {
  603. pathSegLen[segCount++] = l;
  604. pathTotalLen += l;
  605. }
  606. }
  607. // TODO Optimize memory cost.
  608. this._pathLen = pathTotalLen;
  609. return pathTotalLen;
  610. }
  611. /**
  612. * Rebuild path from current data
  613. * Rebuild path will not consider javascript implemented line dash.
  614. * @param {CanvasRenderingContext2D} ctx
  615. */
  616. rebuildPath(ctx: PathRebuilder, percent: number) {
  617. const d = this.data;
  618. const ux = this._ux;
  619. const uy = this._uy;
  620. const len = this._len;
  621. let x0;
  622. let y0;
  623. let xi;
  624. let yi;
  625. let x;
  626. let y;
  627. const drawPart = percent < 1;
  628. let pathSegLen;
  629. let pathTotalLen;
  630. let accumLength = 0;
  631. let segCount = 0;
  632. let displayedLength;
  633. let pendingPtDist = 0;
  634. let pendingPtX: number;
  635. let pendingPtY: number;
  636. if (drawPart) {
  637. if (!this._pathSegLen) {
  638. this._calculateLength();
  639. }
  640. pathSegLen = this._pathSegLen;
  641. pathTotalLen = this._pathLen;
  642. displayedLength = percent * pathTotalLen;
  643. if (!displayedLength) {
  644. return;
  645. }
  646. }
  647. lo: for (let i = 0; i < len;) {
  648. const cmd = d[i++];
  649. const isFirst = i === 1;
  650. if (isFirst) {
  651. // 如果第一个命令是 L, C, Q
  652. // 则 previous point 同绘制命令的第一个 point
  653. // 第一个命令为 Arc 的情况下会在后面特殊处理
  654. xi = d[i];
  655. yi = d[i + 1];
  656. x0 = xi;
  657. y0 = yi;
  658. }
  659. // Only lineTo support ignoring small segments.
  660. // Otherwise if the pending point should always been flushed.
  661. if (cmd !== CMD.L && pendingPtDist > 0) {
  662. ctx.lineTo(pendingPtX, pendingPtY);
  663. pendingPtDist = 0;
  664. }
  665. switch (cmd) {
  666. case CMD.M:
  667. x0 = xi = d[i++];
  668. y0 = yi = d[i++];
  669. ctx.moveTo(xi, yi);
  670. break;
  671. case CMD.L: {
  672. x = d[i++];
  673. y = d[i++];
  674. const dx = mathAbs(x - xi);
  675. const dy = mathAbs(y - yi);
  676. // Not draw too small seg between
  677. if (dx > ux || dy > uy) {
  678. if (drawPart) {
  679. const l = pathSegLen[segCount++];
  680. if (accumLength + l > displayedLength) {
  681. const t = (displayedLength - accumLength) / l;
  682. ctx.lineTo(xi * (1 - t) + x * t, yi * (1 - t) + y * t);
  683. break lo;
  684. }
  685. accumLength += l;
  686. }
  687. ctx.lineTo(x, y);
  688. xi = x;
  689. yi = y;
  690. pendingPtDist = 0;
  691. }
  692. else {
  693. const d2 = dx * dx + dy * dy;
  694. // Only use the farthest pending point.
  695. if (d2 > pendingPtDist) {
  696. pendingPtX = x;
  697. pendingPtY = y;
  698. pendingPtDist = d2;
  699. }
  700. }
  701. break;
  702. }
  703. case CMD.C: {
  704. const x1 = d[i++];
  705. const y1 = d[i++];
  706. const x2 = d[i++];
  707. const y2 = d[i++];
  708. const x3 = d[i++];
  709. const y3 = d[i++];
  710. if (drawPart) {
  711. const l = pathSegLen[segCount++];
  712. if (accumLength + l > displayedLength) {
  713. const t = (displayedLength - accumLength) / l;
  714. cubicSubdivide(xi, x1, x2, x3, t, tmpOutX);
  715. cubicSubdivide(yi, y1, y2, y3, t, tmpOutY);
  716. ctx.bezierCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2], tmpOutX[3], tmpOutY[3]);
  717. break lo;
  718. }
  719. accumLength += l;
  720. }
  721. ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
  722. xi = x3;
  723. yi = y3;
  724. break;
  725. }
  726. case CMD.Q: {
  727. const x1 = d[i++];
  728. const y1 = d[i++];
  729. const x2 = d[i++];
  730. const y2 = d[i++];
  731. if (drawPart) {
  732. const l = pathSegLen[segCount++];
  733. if (accumLength + l > displayedLength) {
  734. const t = (displayedLength - accumLength) / l;
  735. quadraticSubdivide(xi, x1, x2, t, tmpOutX);
  736. quadraticSubdivide(yi, y1, y2, t, tmpOutY);
  737. ctx.quadraticCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2]);
  738. break lo;
  739. }
  740. accumLength += l;
  741. }
  742. ctx.quadraticCurveTo(x1, y1, x2, y2);
  743. xi = x2;
  744. yi = y2;
  745. break;
  746. }
  747. case CMD.A:
  748. const cx = d[i++];
  749. const cy = d[i++];
  750. const rx = d[i++];
  751. const ry = d[i++];
  752. let startAngle = d[i++];
  753. let delta = d[i++];
  754. const psi = d[i++];
  755. const anticlockwise = !d[i++];
  756. const r = (rx > ry) ? rx : ry;
  757. // const scaleX = (rx > ry) ? 1 : rx / ry;
  758. // const scaleY = (rx > ry) ? ry / rx : 1;
  759. const isEllipse = mathAbs(rx - ry) > 1e-3;
  760. let endAngle = startAngle + delta;
  761. let breakBuild = false;
  762. if (drawPart) {
  763. const l = pathSegLen[segCount++];
  764. if (accumLength + l > displayedLength) {
  765. endAngle = startAngle + delta * (displayedLength - accumLength) / l;
  766. breakBuild = true;
  767. }
  768. accumLength += l;
  769. }
  770. if (isEllipse && ctx.ellipse) {
  771. ctx.ellipse(cx, cy, rx, ry, psi, startAngle, endAngle, anticlockwise);
  772. }
  773. else {
  774. ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);
  775. }
  776. if (breakBuild) {
  777. break lo;
  778. }
  779. if (isFirst) {
  780. // 直接使用 arc 命令
  781. // 第一个命令起点还未定义
  782. x0 = mathCos(startAngle) * rx + cx;
  783. y0 = mathSin(startAngle) * ry + cy;
  784. }
  785. xi = mathCos(endAngle) * rx + cx;
  786. yi = mathSin(endAngle) * ry + cy;
  787. break;
  788. case CMD.R:
  789. x0 = xi = d[i];
  790. y0 = yi = d[i + 1];
  791. x = d[i++];
  792. y = d[i++];
  793. const width = d[i++];
  794. const height = d[i++];
  795. if (drawPart) {
  796. const l = pathSegLen[segCount++];
  797. if (accumLength + l > displayedLength) {
  798. let d = displayedLength - accumLength;
  799. ctx.moveTo(x, y);
  800. ctx.lineTo(x + mathMin(d, width), y);
  801. d -= width;
  802. if (d > 0) {
  803. ctx.lineTo(x + width, y + mathMin(d, height));
  804. }
  805. d -= height;
  806. if (d > 0) {
  807. ctx.lineTo(x + mathMax(width - d, 0), y + height);
  808. }
  809. d -= width;
  810. if (d > 0) {
  811. ctx.lineTo(x, y + mathMax(height - d, 0));
  812. }
  813. break lo;
  814. }
  815. accumLength += l;
  816. }
  817. ctx.rect(x, y, width, height);
  818. break;
  819. case CMD.Z:
  820. if (drawPart) {
  821. const l = pathSegLen[segCount++];
  822. if (accumLength + l > displayedLength) {
  823. const t = (displayedLength - accumLength) / l;
  824. ctx.lineTo(xi * (1 - t) + x0 * t, yi * (1 - t) + y0 * t);
  825. break lo;
  826. }
  827. accumLength += l;
  828. }
  829. ctx.closePath();
  830. xi = x0;
  831. yi = y0;
  832. }
  833. }
  834. }
  835. clone() {
  836. const newProxy = new PathProxy();
  837. const data = this.data;
  838. newProxy.data = data.slice ? data.slice()
  839. : Array.prototype.slice.call(data);
  840. newProxy._len = this._len;
  841. return newProxy;
  842. }
  843. private static initDefaultProps = (function () {
  844. const proto = PathProxy.prototype;
  845. proto._saveData = true;
  846. proto._ux = 0;
  847. proto._uy = 0;
  848. proto._pendingPtDist = 0;
  849. proto._version = 0;
  850. })()
  851. }
  852. export interface PathRebuilder {
  853. moveTo(x: number, y: number): void
  854. lineTo(x: number, y: number): void
  855. bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number): void
  856. quadraticCurveTo(x: number, y: number, x2: number, y2: number): void
  857. arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean): void
  858. // eslint-disable-next-line max-len
  859. ellipse(cx: number, cy: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise: boolean): void
  860. rect(x: number, y: number, width: number, height: number): void
  861. closePath(): void
  862. }