svgPathParser.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { SVGPathData } from 'svg-pathdata'
  2. import arcToBezier from 'svg-arc-to-cubic-bezier'
  3. const typeMap = {
  4. 1: 'Z',
  5. 2: 'M',
  6. 4: 'H',
  7. 8: 'V',
  8. 16: 'L',
  9. 32: 'C',
  10. 64: 'S',
  11. 128: 'Q',
  12. 256: 'T',
  13. 512: 'A',
  14. }
  15. /**
  16. * 简单解析SVG路径
  17. * @param d SVG path d属性
  18. */
  19. export const parseSvgPath = (d: string) => {
  20. const pathData = new SVGPathData(d)
  21. const ret = pathData.commands.map(item => {
  22. return { ...item, type: typeMap[item.type] }
  23. })
  24. return ret
  25. }
  26. export type SvgPath = ReturnType<typeof parseSvgPath>
  27. /**
  28. * 解析SVG路径,并将圆弧(A)类型的路径转为三次贝塞尔(C)类型的路径
  29. * @param d SVG path d属性
  30. */
  31. export const toPoints = (d: string) => {
  32. const pathData = new SVGPathData(d)
  33. const points = []
  34. for (const item of pathData.commands) {
  35. const type = typeMap[item.type]
  36. if (item.type === 2 || item.type === 16) {
  37. points.push({
  38. x: item.x,
  39. y: item.y,
  40. relative: item.relative,
  41. type,
  42. })
  43. }
  44. if (item.type === 32) {
  45. points.push({
  46. x: item.x,
  47. y: item.y,
  48. curve: {
  49. type: 'cubic',
  50. x1: item.x1,
  51. y1: item.y1,
  52. x2: item.x2,
  53. y2: item.y2,
  54. },
  55. relative: item.relative,
  56. type,
  57. })
  58. }
  59. else if (item.type === 128) {
  60. points.push({
  61. x: item.x,
  62. y: item.y,
  63. curve: {
  64. type: 'quadratic',
  65. x1: item.x1,
  66. y1: item.y1,
  67. },
  68. relative: item.relative,
  69. type,
  70. })
  71. }
  72. else if (item.type === 512) {
  73. const lastPoint = points[points.length - 1]
  74. if (!['M', 'L', 'Q', 'C'].includes(lastPoint.type)) continue
  75. const cubicBezierPoints = arcToBezier({
  76. px: lastPoint.x as number,
  77. py: lastPoint.y as number,
  78. cx: item.x,
  79. cy: item.y,
  80. rx: item.rX,
  81. ry: item.rY,
  82. xAxisRotation: item.xRot,
  83. largeArcFlag: item.lArcFlag,
  84. sweepFlag: item.sweepFlag,
  85. })
  86. for (const cbPoint of cubicBezierPoints) {
  87. points.push({
  88. x: cbPoint.x,
  89. y: cbPoint.y,
  90. curve: {
  91. type: 'cubic',
  92. x1: cbPoint.x1,
  93. y1: cbPoint.y1,
  94. x2: cbPoint.x2,
  95. y2: cbPoint.y2,
  96. },
  97. relative: false,
  98. type: 'C',
  99. })
  100. }
  101. }
  102. else if (item.type === 1) {
  103. points.push({ close: true, type })
  104. }
  105. else continue
  106. }
  107. return points
  108. }
  109. /*
  110. export const getSvgPathRange = (path: string) => {
  111. try {
  112. const pathData = new SVGPathData(path)
  113. const xList = []
  114. const yList = []
  115. for (const item of pathData.commands) {
  116. const x = ('x' in item) ? item.x : 0
  117. const y = ('y' in item) ? item.y : 0
  118. xList.push(x)
  119. yList.push(y)
  120. }
  121. return {
  122. minX: Math.min(...xList),
  123. minY: Math.min(...yList),
  124. maxX: Math.max(...xList),
  125. maxY: Math.max(...yList),
  126. }
  127. }
  128. catch {
  129. return {
  130. minX: 0,
  131. minY: 0,
  132. maxX: 0,
  133. maxY: 0,
  134. }
  135. }
  136. }
  137. */
  138. // 辅助:采样三次贝塞尔曲线
  139. function sampleCubicBezier(
  140. x0: number, y0: number, x1: number, y1: number,
  141. x2: number, y2: number, x3: number, y3: number,
  142. samples = 20
  143. ): { x: number; y: number }[] {
  144. const points = [];
  145. for (let i = 0; i <= samples; i++) {
  146. const t = i / samples;
  147. const mt = 1 - t;
  148. const x = mt ** 3 * x0 + 3 * mt ** 2 * t * x1 + 3 * mt * t ** 2 * x2 + t ** 3 * x3;
  149. const y = mt ** 3 * y0 + 3 * mt ** 2 * t * y1 + 3 * mt * t ** 2 * y2 + t ** 3 * y3;
  150. points.push({ x, y });
  151. }
  152. return points;
  153. }
  154. // 辅助:采样二次贝塞尔曲线
  155. function sampleQuadraticBezier(
  156. x0: number, y0: number, x1: number, y1: number,
  157. x2: number, y2: number, samples = 20
  158. ): { x: number; y: number }[] {
  159. const points = [];
  160. for (let i = 0; i <= samples; i++) {
  161. const t = i / samples;
  162. const mt = 1 - t;
  163. const x = mt ** 2 * x0 + 2 * mt * t * x1 + t ** 2 * x2;
  164. const y = mt ** 2 * y0 + 2 * mt * t * y1 + t ** 2 * y2;
  165. points.push({ x, y });
  166. }
  167. return points;
  168. }
  169. // 辅助:采样椭圆弧(基于 SVG 规范参数方程)
  170. function sampleArc(
  171. x0: number, y0: number, rx: number, ry: number,
  172. xAxisRotation: number, largeArcFlag: boolean, sweepFlag: boolean,
  173. x: number, y: number, samples = 30
  174. ): { x: number; y: number }[] {
  175. const phi = (xAxisRotation * Math.PI) / 180;
  176. const cosPhi = Math.cos(phi);
  177. const sinPhi = Math.sin(phi);
  178. const dx = (x0 - x) / 2;
  179. const dy = (y0 - y) / 2;
  180. const x1p = cosPhi * dx + sinPhi * dy;
  181. const y1p = -sinPhi * dx + cosPhi * dy;
  182. let rxx = Math.abs(rx);
  183. let ryy = Math.abs(ry);
  184. const lambda = (x1p * x1p) / (rxx * rxx) + (y1p * y1p) / (ryy * ryy);
  185. if (lambda > 1) {
  186. rxx *= Math.sqrt(lambda);
  187. ryy *= Math.sqrt(lambda);
  188. }
  189. const sq = Math.sqrt(
  190. (rxx * rxx * (ryy * ryy) - rxx * rxx * (y1p * y1p) - ryy * ryy * (x1p * x1p)) /
  191. (rxx * rxx * (y1p * y1p) + ryy * ryy * (x1p * x1p))
  192. );
  193. const sign = largeArcFlag === sweepFlag ? -1 : 1;
  194. const cxp = sign * sq * ((rxx * y1p) / ryy);
  195. const cyp = sign * sq * ((-ryy * x1p) / rxx);
  196. const cx = cosPhi * cxp - sinPhi * cyp + (x0 + x) / 2;
  197. const cy = sinPhi * cxp + cosPhi * cyp + (y0 + y) / 2;
  198. const vectorAngle = (ux: number, uy: number, vx: number, vy: number) => {
  199. const dot = ux * vx + uy * vy;
  200. const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
  201. let ang = Math.acos(Math.max(-1, Math.min(1, dot / len)));
  202. const cross = ux * vy - uy * vx;
  203. return cross < 0 ? -ang : ang;
  204. };
  205. const ux = (x1p - cxp) / rxx;
  206. const uy = (y1p - cyp) / ryy;
  207. const vx = (-x1p - cxp) / rxx;
  208. const vy = (-y1p - cyp) / ryy;
  209. let startAngle = vectorAngle(1, 0, ux, uy);
  210. let deltaAngle = vectorAngle(ux, uy, vx, vy);
  211. if (!sweepFlag && deltaAngle > 0) {
  212. deltaAngle -= 2 * Math.PI;
  213. } else if (sweepFlag && deltaAngle < 0) {
  214. deltaAngle += 2 * Math.PI;
  215. }
  216. const points: { x: number; y: number }[] = [];
  217. for (let i = 0; i <= samples; i++) {
  218. const t = i / samples;
  219. const angle = startAngle + t * deltaAngle;
  220. const xp = rxx * Math.cos(angle);
  221. const yp = ryy * Math.sin(angle);
  222. const px = cosPhi * xp - sinPhi * yp + cx;
  223. const py = sinPhi * xp + cosPhi * yp + cy;
  224. points.push({ x: px, y: py });
  225. }
  226. return points;
  227. }
  228. // 主函数
  229. export const getSvgPathRange = (path: string) => {
  230. try {
  231. const pathData = new SVGPathData(path);
  232. let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  233. let curX = 0, curY = 0; // 当前点(上一命令终点)
  234. let startX = 0, startY = 0; // 子路径起点(用于 Z)
  235. const updateBounds = (x: number, y: number) => {
  236. minX = Math.min(minX, x);
  237. minY = Math.min(minY, y);
  238. maxX = Math.max(maxX, x);
  239. maxY = Math.max(maxY, y);
  240. };
  241. const processPoints = (points: { x: number; y: number }[]) => {
  242. points.forEach(p => updateBounds(p.x, p.y));
  243. };
  244. for (const cmd of pathData.commands) {
  245. switch (cmd.type) {
  246. case SVGPathData.MOVE_TO:
  247. curX = cmd.x;
  248. curY = cmd.y;
  249. startX = curX;
  250. startY = curY;
  251. updateBounds(curX, curY);
  252. break;
  253. case SVGPathData.LINE_TO:
  254. curX = cmd.x;
  255. curY = cmd.y;
  256. updateBounds(curX, curY);
  257. break;
  258. case SVGPathData.HORIZ_LINE_TO:
  259. curX = cmd.x;
  260. updateBounds(curX, curY);
  261. break;
  262. case SVGPathData.VERT_LINE_TO:
  263. curY = cmd.y;
  264. updateBounds(curX, curY);
  265. break;
  266. case SVGPathData.CURVE_TO:
  267. {
  268. const points = sampleCubicBezier(
  269. curX, curY,
  270. cmd.x1, cmd.y1,
  271. cmd.x2, cmd.y2,
  272. cmd.x, cmd.y
  273. );
  274. processPoints(points);
  275. curX = cmd.x;
  276. curY = cmd.y;
  277. }
  278. break;
  279. case SVGPathData.SMOOTH_CURVE_TO:
  280. // 为简化,此处省略反射控制点的精确计算,可根据需要补充
  281. // 可直接使用采样近似或添加反射逻辑
  282. break;
  283. case SVGPathData.QUAD_TO:
  284. {
  285. const points = sampleQuadraticBezier(
  286. curX, curY,
  287. cmd.x1, cmd.y1,
  288. cmd.x, cmd.y
  289. );
  290. processPoints(points);
  291. curX = cmd.x;
  292. curY = cmd.y;
  293. }
  294. break;
  295. case SVGPathData.SMOOTH_QUAD_TO:
  296. // 省略
  297. break;
  298. case SVGPathData.ARC:
  299. {
  300. const points = sampleArc(
  301. curX, curY,
  302. cmd.rX, cmd.rY,
  303. cmd.xRot,
  304. cmd.lArcFlag, cmd.sweepFlag,
  305. cmd.x, cmd.y
  306. );
  307. processPoints(points);
  308. curX = cmd.x;
  309. curY = cmd.y;
  310. }
  311. break;
  312. case SVGPathData.CLOSE_PATH:
  313. curX = startX;
  314. curY = startY;
  315. updateBounds(curX, curY);
  316. break;
  317. }
  318. }
  319. if (minX === Infinity) {
  320. return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
  321. }
  322. return { minX, minY, maxX, maxY };
  323. } catch {
  324. return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
  325. }
  326. };
  327. export type SvgPoints = ReturnType<typeof toPoints>