| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- import { SVGPathData } from 'svg-pathdata'
- import arcToBezier from 'svg-arc-to-cubic-bezier'
- const typeMap = {
- 1: 'Z',
- 2: 'M',
- 4: 'H',
- 8: 'V',
- 16: 'L',
- 32: 'C',
- 64: 'S',
- 128: 'Q',
- 256: 'T',
- 512: 'A',
- }
- /**
- * 简单解析SVG路径
- * @param d SVG path d属性
- */
- export const parseSvgPath = (d: string) => {
- const pathData = new SVGPathData(d)
- const ret = pathData.commands.map(item => {
- return { ...item, type: typeMap[item.type] }
- })
- return ret
- }
- export type SvgPath = ReturnType<typeof parseSvgPath>
- /**
- * 解析SVG路径,并将圆弧(A)类型的路径转为三次贝塞尔(C)类型的路径
- * @param d SVG path d属性
- */
- export const toPoints = (d: string) => {
- const pathData = new SVGPathData(d)
-
- const points = []
- for (const item of pathData.commands) {
- const type = typeMap[item.type]
- if (item.type === 2 || item.type === 16) {
- points.push({
- x: item.x,
- y: item.y,
- relative: item.relative,
- type,
- })
- }
- if (item.type === 32) {
- points.push({
- x: item.x,
- y: item.y,
- curve: {
- type: 'cubic',
- x1: item.x1,
- y1: item.y1,
- x2: item.x2,
- y2: item.y2,
- },
- relative: item.relative,
- type,
- })
- }
- else if (item.type === 128) {
- points.push({
- x: item.x,
- y: item.y,
- curve: {
- type: 'quadratic',
- x1: item.x1,
- y1: item.y1,
- },
- relative: item.relative,
- type,
- })
- }
- else if (item.type === 512) {
- const lastPoint = points[points.length - 1]
- if (!['M', 'L', 'Q', 'C'].includes(lastPoint.type)) continue
- const cubicBezierPoints = arcToBezier({
- px: lastPoint.x as number,
- py: lastPoint.y as number,
- cx: item.x,
- cy: item.y,
- rx: item.rX,
- ry: item.rY,
- xAxisRotation: item.xRot,
- largeArcFlag: item.lArcFlag,
- sweepFlag: item.sweepFlag,
- })
- for (const cbPoint of cubicBezierPoints) {
- points.push({
- x: cbPoint.x,
- y: cbPoint.y,
- curve: {
- type: 'cubic',
- x1: cbPoint.x1,
- y1: cbPoint.y1,
- x2: cbPoint.x2,
- y2: cbPoint.y2,
- },
- relative: false,
- type: 'C',
- })
- }
- }
- else if (item.type === 1) {
- points.push({ close: true, type })
- }
- else continue
- }
- return points
- }
- /*
- export const getSvgPathRange = (path: string) => {
- try {
- const pathData = new SVGPathData(path)
- const xList = []
- const yList = []
- for (const item of pathData.commands) {
- const x = ('x' in item) ? item.x : 0
- const y = ('y' in item) ? item.y : 0
- xList.push(x)
- yList.push(y)
- }
- return {
- minX: Math.min(...xList),
- minY: Math.min(...yList),
- maxX: Math.max(...xList),
- maxY: Math.max(...yList),
- }
- }
- catch {
- return {
- minX: 0,
- minY: 0,
- maxX: 0,
- maxY: 0,
- }
- }
- }
- */
- // 辅助:采样三次贝塞尔曲线
- function sampleCubicBezier(
- x0: number, y0: number, x1: number, y1: number,
- x2: number, y2: number, x3: number, y3: number,
- samples = 20
- ): { x: number; y: number }[] {
- const points = [];
- for (let i = 0; i <= samples; i++) {
- const t = i / samples;
- const mt = 1 - t;
- const x = mt ** 3 * x0 + 3 * mt ** 2 * t * x1 + 3 * mt * t ** 2 * x2 + t ** 3 * x3;
- const y = mt ** 3 * y0 + 3 * mt ** 2 * t * y1 + 3 * mt * t ** 2 * y2 + t ** 3 * y3;
- points.push({ x, y });
- }
- return points;
- }
- // 辅助:采样二次贝塞尔曲线
- function sampleQuadraticBezier(
- x0: number, y0: number, x1: number, y1: number,
- x2: number, y2: number, samples = 20
- ): { x: number; y: number }[] {
- const points = [];
- for (let i = 0; i <= samples; i++) {
- const t = i / samples;
- const mt = 1 - t;
- const x = mt ** 2 * x0 + 2 * mt * t * x1 + t ** 2 * x2;
- const y = mt ** 2 * y0 + 2 * mt * t * y1 + t ** 2 * y2;
- points.push({ x, y });
- }
- return points;
- }
- // 辅助:采样椭圆弧(基于 SVG 规范参数方程)
- function sampleArc(
- x0: number, y0: number, rx: number, ry: number,
- xAxisRotation: number, largeArcFlag: boolean, sweepFlag: boolean,
- x: number, y: number, samples = 30
- ): { x: number; y: number }[] {
- const phi = (xAxisRotation * Math.PI) / 180;
- const cosPhi = Math.cos(phi);
- const sinPhi = Math.sin(phi);
- const dx = (x0 - x) / 2;
- const dy = (y0 - y) / 2;
- const x1p = cosPhi * dx + sinPhi * dy;
- const y1p = -sinPhi * dx + cosPhi * dy;
- let rxx = Math.abs(rx);
- let ryy = Math.abs(ry);
- const lambda = (x1p * x1p) / (rxx * rxx) + (y1p * y1p) / (ryy * ryy);
- if (lambda > 1) {
- rxx *= Math.sqrt(lambda);
- ryy *= Math.sqrt(lambda);
- }
- const sq = Math.sqrt(
- (rxx * rxx * (ryy * ryy) - rxx * rxx * (y1p * y1p) - ryy * ryy * (x1p * x1p)) /
- (rxx * rxx * (y1p * y1p) + ryy * ryy * (x1p * x1p))
- );
- const sign = largeArcFlag === sweepFlag ? -1 : 1;
- const cxp = sign * sq * ((rxx * y1p) / ryy);
- const cyp = sign * sq * ((-ryy * x1p) / rxx);
- const cx = cosPhi * cxp - sinPhi * cyp + (x0 + x) / 2;
- const cy = sinPhi * cxp + cosPhi * cyp + (y0 + y) / 2;
- const vectorAngle = (ux: number, uy: number, vx: number, vy: number) => {
- const dot = ux * vx + uy * vy;
- const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
- let ang = Math.acos(Math.max(-1, Math.min(1, dot / len)));
- const cross = ux * vy - uy * vx;
- return cross < 0 ? -ang : ang;
- };
- const ux = (x1p - cxp) / rxx;
- const uy = (y1p - cyp) / ryy;
- const vx = (-x1p - cxp) / rxx;
- const vy = (-y1p - cyp) / ryy;
- let startAngle = vectorAngle(1, 0, ux, uy);
- let deltaAngle = vectorAngle(ux, uy, vx, vy);
- if (!sweepFlag && deltaAngle > 0) {
- deltaAngle -= 2 * Math.PI;
- } else if (sweepFlag && deltaAngle < 0) {
- deltaAngle += 2 * Math.PI;
- }
- const points: { x: number; y: number }[] = [];
- for (let i = 0; i <= samples; i++) {
- const t = i / samples;
- const angle = startAngle + t * deltaAngle;
- const xp = rxx * Math.cos(angle);
- const yp = ryy * Math.sin(angle);
- const px = cosPhi * xp - sinPhi * yp + cx;
- const py = sinPhi * xp + cosPhi * yp + cy;
- points.push({ x: px, y: py });
- }
- return points;
- }
- // 主函数
- export const getSvgPathRange = (path: string) => {
- try {
- const pathData = new SVGPathData(path);
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- let curX = 0, curY = 0; // 当前点(上一命令终点)
- let startX = 0, startY = 0; // 子路径起点(用于 Z)
- const updateBounds = (x: number, y: number) => {
- minX = Math.min(minX, x);
- minY = Math.min(minY, y);
- maxX = Math.max(maxX, x);
- maxY = Math.max(maxY, y);
- };
- const processPoints = (points: { x: number; y: number }[]) => {
- points.forEach(p => updateBounds(p.x, p.y));
- };
- for (const cmd of pathData.commands) {
- switch (cmd.type) {
- case SVGPathData.MOVE_TO:
- curX = cmd.x;
- curY = cmd.y;
- startX = curX;
- startY = curY;
- updateBounds(curX, curY);
- break;
- case SVGPathData.LINE_TO:
- curX = cmd.x;
- curY = cmd.y;
- updateBounds(curX, curY);
- break;
- case SVGPathData.HORIZ_LINE_TO:
- curX = cmd.x;
- updateBounds(curX, curY);
- break;
- case SVGPathData.VERT_LINE_TO:
- curY = cmd.y;
- updateBounds(curX, curY);
- break;
- case SVGPathData.CURVE_TO:
- {
- const points = sampleCubicBezier(
- curX, curY,
- cmd.x1, cmd.y1,
- cmd.x2, cmd.y2,
- cmd.x, cmd.y
- );
- processPoints(points);
- curX = cmd.x;
- curY = cmd.y;
- }
- break;
- case SVGPathData.SMOOTH_CURVE_TO:
- // 为简化,此处省略反射控制点的精确计算,可根据需要补充
- // 可直接使用采样近似或添加反射逻辑
- break;
- case SVGPathData.QUAD_TO:
- {
- const points = sampleQuadraticBezier(
- curX, curY,
- cmd.x1, cmd.y1,
- cmd.x, cmd.y
- );
- processPoints(points);
- curX = cmd.x;
- curY = cmd.y;
- }
- break;
- case SVGPathData.SMOOTH_QUAD_TO:
- // 省略
- break;
- case SVGPathData.ARC:
- {
- const points = sampleArc(
- curX, curY,
- cmd.rX, cmd.rY,
- cmd.xRot,
- cmd.lArcFlag, cmd.sweepFlag,
- cmd.x, cmd.y
- );
- processPoints(points);
- curX = cmd.x;
- curY = cmd.y;
- }
- break;
- case SVGPathData.CLOSE_PATH:
- curX = startX;
- curY = startY;
- updateBounds(curX, curY);
- break;
- }
- }
- if (minX === Infinity) {
- return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
- }
- return { minX, minY, maxX, maxY };
- } catch {
- return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
- }
- };
- export type SvgPoints = ReturnType<typeof toPoints>
|