b8e08069422cb3af279c0f55774458f35cb20abe40f14e5f798c3e56e2bf6ce9a21d4268cce52dd382fc19ae9d54c8e8be7888f318f715747d1c8404220137 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import { cubicSubdivide } from '../core/curve';
  2. import PathProxy from '../core/PathProxy';
  3. const CMD = PathProxy.CMD;
  4. function aroundEqual(a: number, b: number) {
  5. return Math.abs(a - b) < 1e-5;
  6. }
  7. export function pathToBezierCurves(path: PathProxy) {
  8. const data = path.data;
  9. const len = path.len();
  10. const bezierArrayGroups: number[][] = [];
  11. let currentSubpath: number[];
  12. let xi = 0;
  13. let yi = 0;
  14. let x0 = 0;
  15. let y0 = 0;
  16. function createNewSubpath(x: number, y: number) {
  17. // More than one M command
  18. if (currentSubpath && currentSubpath.length > 2) {
  19. bezierArrayGroups.push(currentSubpath);
  20. }
  21. currentSubpath = [x, y];
  22. }
  23. function addLine(x0: number, y0: number, x1: number, y1: number) {
  24. if (!(aroundEqual(x0, x1) && aroundEqual(y0, y1))) {
  25. currentSubpath.push(x0, y0, x1, y1, x1, y1);
  26. }
  27. }
  28. function addArc(startAngle: number, endAngle: number, cx: number, cy: number, rx: number, ry: number) {
  29. // https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
  30. const delta = Math.abs(endAngle - startAngle);
  31. const len = Math.tan(delta / 4) * 4 / 3;
  32. const dir = endAngle < startAngle ? -1 : 1;
  33. const c1 = Math.cos(startAngle);
  34. const s1 = Math.sin(startAngle);
  35. const c2 = Math.cos(endAngle);
  36. const s2 = Math.sin(endAngle);
  37. const x1 = c1 * rx + cx;
  38. const y1 = s1 * ry + cy;
  39. const x4 = c2 * rx + cx;
  40. const y4 = s2 * ry + cy;
  41. const hx = rx * len * dir;
  42. const hy = ry * len * dir;
  43. currentSubpath.push(
  44. // Move control points on tangent.
  45. x1 - hx * s1, y1 + hy * c1,
  46. x4 + hx * s2, y4 - hy * c2,
  47. x4, y4
  48. );
  49. }
  50. let x1;
  51. let y1;
  52. let x2;
  53. let y2;
  54. for (let i = 0; i < len;) {
  55. const cmd = data[i++];
  56. const isFirst = i === 1;
  57. if (isFirst) {
  58. // 如果第一个命令是 L, C, Q
  59. // 则 previous point 同绘制命令的第一个 point
  60. // 第一个命令为 Arc 的情况下会在后面特殊处理
  61. xi = data[i];
  62. yi = data[i + 1];
  63. x0 = xi;
  64. y0 = yi;
  65. if (cmd === CMD.L || cmd === CMD.C || cmd === CMD.Q) {
  66. // Start point
  67. currentSubpath = [x0, y0];
  68. }
  69. }
  70. switch (cmd) {
  71. case CMD.M:
  72. // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
  73. // 在 closePath 的时候使用
  74. xi = x0 = data[i++];
  75. yi = y0 = data[i++];
  76. createNewSubpath(x0, y0);
  77. break;
  78. case CMD.L:
  79. x1 = data[i++];
  80. y1 = data[i++];
  81. addLine(xi, yi, x1, y1);
  82. xi = x1;
  83. yi = y1;
  84. break;
  85. case CMD.C:
  86. currentSubpath.push(
  87. data[i++], data[i++], data[i++], data[i++],
  88. xi = data[i++], yi = data[i++]
  89. );
  90. break;
  91. case CMD.Q:
  92. x1 = data[i++];
  93. y1 = data[i++];
  94. x2 = data[i++];
  95. y2 = data[i++];
  96. currentSubpath.push(
  97. // Convert quadratic to cubic
  98. xi + 2 / 3 * (x1 - xi), yi + 2 / 3 * (y1 - yi),
  99. x2 + 2 / 3 * (x1 - x2), y2 + 2 / 3 * (y1 - y2),
  100. x2, y2
  101. );
  102. xi = x2;
  103. yi = y2;
  104. break;
  105. case CMD.A:
  106. const cx = data[i++];
  107. const cy = data[i++];
  108. const rx = data[i++];
  109. const ry = data[i++];
  110. const startAngle = data[i++];
  111. const endAngle = data[i++] + startAngle;
  112. // TODO Arc rotation
  113. i += 1;
  114. const anticlockwise = !data[i++];
  115. x1 = Math.cos(startAngle) * rx + cx;
  116. y1 = Math.sin(startAngle) * ry + cy;
  117. if (isFirst) {
  118. // 直接使用 arc 命令
  119. // 第一个命令起点还未定义
  120. x0 = x1;
  121. y0 = y1;
  122. createNewSubpath(x0, y0);
  123. }
  124. else {
  125. // Connect a line between current point to arc start point.
  126. addLine(xi, yi, x1, y1);
  127. }
  128. xi = Math.cos(endAngle) * rx + cx;
  129. yi = Math.sin(endAngle) * ry + cy;
  130. const step = (anticlockwise ? -1 : 1) * Math.PI / 2;
  131. for (let angle = startAngle; anticlockwise ? angle > endAngle : angle < endAngle; angle += step) {
  132. const nextAngle = anticlockwise ? Math.max(angle + step, endAngle)
  133. : Math.min(angle + step, endAngle);
  134. addArc(angle, nextAngle, cx, cy, rx, ry);
  135. }
  136. break;
  137. case CMD.R:
  138. x0 = xi = data[i++];
  139. y0 = yi = data[i++];
  140. x1 = x0 + data[i++];
  141. y1 = y0 + data[i++];
  142. // rect is an individual path.
  143. createNewSubpath(x1, y0);
  144. addLine(x1, y0, x1, y1);
  145. addLine(x1, y1, x0, y1);
  146. addLine(x0, y1, x0, y0);
  147. addLine(x0, y0, x1, y0);
  148. break;
  149. case CMD.Z:
  150. currentSubpath && addLine(xi, yi, x0, y0);
  151. xi = x0;
  152. yi = y0;
  153. break;
  154. }
  155. }
  156. if (currentSubpath && currentSubpath.length > 2) {
  157. bezierArrayGroups.push(currentSubpath);
  158. }
  159. return bezierArrayGroups;
  160. }
  161. function adpativeBezier(
  162. x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number,
  163. out: number[], scale: number
  164. ) {
  165. // This bezier is used to simulates a line when converting path to beziers.
  166. if (aroundEqual(x0, x1) && aroundEqual(y0, y1) && aroundEqual(x2, x3) && aroundEqual(y2, y3)) {
  167. out.push(x3, y3);
  168. return;
  169. }
  170. const PIXEL_DISTANCE = 2 / scale;
  171. const PIXEL_DISTANCE_SQR = PIXEL_DISTANCE * PIXEL_DISTANCE;
  172. // Determine if curve is straight enough
  173. let dx = x3 - x0;
  174. let dy = y3 - y0;
  175. const d = Math.sqrt(dx * dx + dy * dy);
  176. dx /= d;
  177. dy /= d;
  178. const dx1 = x1 - x0;
  179. const dy1 = y1 - y0;
  180. const dx2 = x2 - x3;
  181. const dy2 = y2 - y3;
  182. const cp1LenSqr = dx1 * dx1 + dy1 * dy1;
  183. const cp2LenSqr = dx2 * dx2 + dy2 * dy2;
  184. if (cp1LenSqr < PIXEL_DISTANCE_SQR && cp2LenSqr < PIXEL_DISTANCE_SQR) {
  185. // Add small segment
  186. out.push(x3, y3);
  187. return;
  188. }
  189. // Project length of cp1
  190. const projLen1 = dx * dx1 + dy * dy1;
  191. // Project length of cp2
  192. const projLen2 = -dx * dx2 - dy * dy2;
  193. // Distance from cp1 to start-end line.
  194. const d1Sqr = cp1LenSqr - projLen1 * projLen1;
  195. // Distance from cp2 to start-end line.
  196. const d2Sqr = cp2LenSqr - projLen2 * projLen2;
  197. // IF the cp1 and cp2 is near to the start-line enough
  198. // We treat it straight enough
  199. if (d1Sqr < PIXEL_DISTANCE_SQR && projLen1 >= 0
  200. && d2Sqr < PIXEL_DISTANCE_SQR && projLen2 >= 0
  201. ) {
  202. out.push(x3, y3);
  203. return;
  204. }
  205. const tmpSegX: number[] = [];
  206. const tmpSegY: number[] = [];
  207. // Subdivide
  208. cubicSubdivide(x0, x1, x2, x3, 0.5, tmpSegX);
  209. cubicSubdivide(y0, y1, y2, y3, 0.5, tmpSegY);
  210. adpativeBezier(
  211. tmpSegX[0], tmpSegY[0], tmpSegX[1], tmpSegY[1], tmpSegX[2], tmpSegY[2], tmpSegX[3], tmpSegY[3],
  212. out, scale
  213. );
  214. adpativeBezier(
  215. tmpSegX[4], tmpSegY[4], tmpSegX[5], tmpSegY[5], tmpSegX[6], tmpSegY[6], tmpSegX[7], tmpSegY[7],
  216. out, scale
  217. );
  218. }
  219. export function pathToPolygons(path: PathProxy, scale?: number) {
  220. // TODO Optimize simple case like path is polygon and rect?
  221. const bezierArrayGroups = pathToBezierCurves(path);
  222. const polygons: number[][] = [];
  223. scale = scale || 1;
  224. for (let i = 0; i < bezierArrayGroups.length; i++) {
  225. const beziers = bezierArrayGroups[i];
  226. const polygon: number[] = [];
  227. let x0 = beziers[0];
  228. let y0 = beziers[1];
  229. polygon.push(x0, y0);
  230. for (let k = 2; k < beziers.length;) {
  231. const x1 = beziers[k++];
  232. const y1 = beziers[k++];
  233. const x2 = beziers[k++];
  234. const y2 = beziers[k++];
  235. const x3 = beziers[k++];
  236. const y3 = beziers[k++];
  237. adpativeBezier(x0, y0, x1, y1, x2, y2, x3, y3, polygon, scale);
  238. x0 = x3;
  239. y0 = y3;
  240. }
  241. polygons.push(polygon);
  242. }
  243. return polygons;
  244. }