import { ImageTracer, type TraceData, type OutlinedArea, type SvgLineAttributes, Options } from "@image-tracer-ts/core"; export interface TracedLayer { id: string; name: string; color: string; paths: PathData[]; } export interface PathData { points: Point[]; isClosed: boolean; } export interface Point { x: number; y: number; } export interface TraceResult { width: number; height: number; layers: TracedLayer[]; } interface SvgPath { color: string; d: string; path: PathData; } /** * 将图像转换为矢量路径 * @param image - 要追踪的图片元素 * @param options - 追踪选项 * @returns 追踪结果 */ export async function traceImage( image: HTMLImageElement, options?: Partial ): Promise { // 创建 canvas 来获取图片数据 const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) { throw new Error("无法创建 canvas 上下文"); } // 设置 canvas 尺寸为图片原始尺寸 canvas.width = image.naturalWidth || image.width; canvas.height = image.naturalHeight || image.height; // 绘制图片到 canvas ctx.drawImage(image, 0, 0); // 获取图片数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 默认配置 - 使用 detailed preset 作为基础 const defaultOptions: Partial = { ...Options.Presets.curvy, numberOfColors: 8, // 限制颜色数量以控制图层数 minColorQuota: 0.01, // 降低最小颜色占比阈值 strokeWidth: 0, // 不需要描边 lineFilter: true, ...options, }; // 创建追踪器 const tracer = new ImageTracer(defaultOptions); // 执行追踪,返回 SVG 字符串 const svgString = tracer.traceImage(imageData); // 解析 SVG 字符串 const paths = parseSVGString(svgString); // 将路径按颜色分组为图层 const colorMap = new Map(); for (const path of paths) { if (!colorMap.has(path.color)) { colorMap.set(path.color, []); } colorMap.get(path.color)!.push(path.path); } const layers: TracedLayer[] = []; let layerIndex = 0; for (const [color, pathDatas] of colorMap.entries()) { layers.push({ id: `layer-${layerIndex}`, name: `颜色层 ${layerIndex + 1}`, color: color, paths: pathDatas, }); layerIndex++; } return { width: canvas.width, height: canvas.height, layers, }; } /** * 解析 SVG 字符串提取路径和颜色信息 */ function parseSVGString(svgString: string): SvgPath[] { const paths: SvgPath[] = []; // 匹配所有 path 元素 const pathRegex = /]*\/?>/g; let match; while ((match = pathRegex.exec(svgString)) !== null) { const pathTag = match[0]; // 提取 d 属性(路径数据) const dMatch = pathTag.match(/d="([^"]+)"/); if (!dMatch) continue; const d = dMatch[1]; // 提取 fill 颜色 const fillMatch = pathTag.match(/fill="([^"]+)"/); let color = '#000000'; if (fillMatch) { color = fillMatch[1]; // 处理 rgb() 格式 if (color.startsWith('rgb(')) { const rgbValues = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (rgbValues) { const r = parseInt(rgbValues[1]); const g = parseInt(rgbValues[2]); const b = parseInt(rgbValues[3]); color = `#${toHex(r)}${toHex(g)}${toHex(b)}`; } } } // 解析路径数据 const pathDatas = parseSVGPathData(d); // 为每个解析出的路径创建一个 SvgPath for (const pathData of pathDatas) { paths.push({ color, d, path: pathData }); } } return paths; } /** * 解析 SVG 路径数据字符串为点序列 */ function parseSVGPathData(d: string): PathData[] { const paths: PathData[] = []; // 分割路径命令 const commands = d.split(/(?=[MZLQCSHVTA])/g); let currentPoints: Point[] = []; let isClosed = false; for (const cmd of commands) { const type = cmd[0]; const values = cmd.slice(1) .trim() .split(/[\s,]+/) .map(Number) .filter(n => !isNaN(n)); if (type === 'M') { // 如果有未完成的路径,保存它 if (currentPoints.length > 0) { paths.push({ points: [...currentPoints], isClosed }); currentPoints = []; isClosed = false; } // 移动到起点 for (let i = 0; i < values.length; i += 2) { if (i + 1 < values.length) { currentPoints.push({ x: values[i], y: values[i + 1] }); } } } else if (type === 'L') { // 直线 for (let i = 0; i < values.length; i += 2) { if (i + 1 < values.length) { currentPoints.push({ x: values[i], y: values[i + 1] }); } } } else if (type === 'Q') { // 二次贝塞尔曲线 - 简化处理 for (let i = 0; i < values.length; i += 4) { if (i + 3 < values.length) { const [cpX, cpY, endX, endY] = [values[i], values[i + 1], values[i + 2], values[i + 3]]; // 使用控制点和终点的中点作为近似 currentPoints.push({ x: (cpX + endX) / 2, y: (cpY + endY) / 2, }); } } } else if (type === 'C') { // 三次贝塞尔曲线 - 简化处理 for (let i = 0; i < values.length; i += 6) { if (i + 5 < values.length) { const [cp1X, cp1Y, cp2X, cp2Y, endX, endY] = values.slice(i, i + 6); // 使用两个控制点的中点作为近似 currentPoints.push({ x: (cp1X + cp2X) / 2, y: (cp1Y + cp2Y) / 2, }); currentPoints.push({ x: endX, y: endY }); } } } else if (type === 'Z' || type === 'z') { isClosed = true; } } // 保存最后一个路径 if (currentPoints.length > 0) { paths.push({ points: currentPoints, isClosed }); } return paths; } /** * 从 OutlinedArea 解析路径数据 */ function parseOutlinedArea(area: OutlinedArea): PathData | null { const points: Point[] = []; let isClosed = false; if (!area.lineAttributes || area.lineAttributes.length === 0) { return null; } // 收集所有唯一点 const pointMap = new Map(); const orderedPoints: Point[] = []; for (const attr of area.lineAttributes) { // 添加起点 const startKey = `${attr.x1},${attr.y1}`; if (!pointMap.has(startKey)) { const startPoint = { x: attr.x1, y: attr.y1 }; pointMap.set(startKey, startPoint); orderedPoints.push(startPoint); } // 添加终点 const endKey = `${attr.x2},${attr.y2}`; if (!pointMap.has(endKey)) { const endPoint = { x: attr.x2, y: attr.y2 }; pointMap.set(endKey, endPoint); orderedPoints.push(endPoint); } // 如果是 Q 类型,添加控制点相关的近似点 if (attr.type === 'Q' && 'x3' in attr) { // 使用控制点和终点的中点作为近似 const midX = (attr.x1 + attr.x2 + (attr as any).x3) / 3; const midY = (attr.y1 + attr.y2 + (attr as any).y3) / 3; const midKey = `${midX},${midY}`; if (!pointMap.has(midKey)) { const midPoint = { x: midX, y: midY }; pointMap.set(midKey, midPoint); orderedPoints.push(midPoint); } } } // 使用有序点 points.push(...orderedPoints); // 检查是否闭合(起点和终点接近) if (points.length >= 2) { const first = points[0]; const last = points[points.length - 1]; const dist = Math.sqrt( Math.pow(last.x - first.x, 2) + Math.pow(last.y - first.y, 2) ); isClosed = dist < 5; // 距离小于 5 像素视为闭合 } if (points.length === 0) { return null; } return { points, isClosed, }; } function toHex(n: number): string { const hex = Math.round(Math.min(255, Math.max(0, n))).toString(16); return hex.length === 1 ? "0" + hex : hex; } /** * RGB 对象转换为十六进制颜色 */ function rgbToHex(rgb: { r: number; g: number; b: number; a?: number }): string { return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`; }