如何用SVG与JavaScript实现类似Illustrator的闭合区域识别功能
实现Kufi排版SVG网格的闭合区域检测与着色(类似Illustrator油漆桶工具)
针对你开发Kufi排版辅助网站时遇到的闭合区域检测与着色需求,以下是分步骤的解决方案:
步骤1:将基础SVG元素转换为路径(Path)
SVG的<circle>、<rect>等基础元素无法直接进行精确的相交计算,需先转换为<path>元素。可以通过手动生成路径数据实现转换:
// 转换圆形为路径 function circleToPath(circle) { const cx = circle.cx.baseVal.value; const cy = circle.cy.baseVal.value; const r = circle.r.baseVal.value; const d = `M ${cx - r},${cy} A ${r},${r} 0 1,1 ${cx + r},${cy} A ${r},${r} 0 1,1 ${cx - r},${cy} Z`; const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", d); path.setAttribute("fill", "none"); path.setAttribute("stroke", "black"); path.setAttribute("stroke-width", circle.getAttribute("stroke-width")); circle.parentNode.replaceChild(path, circle); return path; } // 转换旋转矩形为路径 function rectToPath(rect) { const transform = rect.transform.baseVal.consolidate(); const matrix = transform ? transform.matrix : null; const x = rect.x.baseVal.value; const y = rect.y.baseVal.value; const width = rect.width.baseVal.value; const height = rect.height.baseVal.value; // 生成矩形四个顶点并应用变换 const points = [ {x, y}, {x: x + width, y}, {x: x + width, y: y + height}, {x, y: y + height} ]; if (matrix) { points.forEach(p => { const newX = p.x * matrix.a + p.y * matrix.c + matrix.e; const newY = p.x * matrix.b + p.y * matrix.d + matrix.f; p.x = newX; p.y = newY; }); } const d = `M ${points[0].x},${points[0].y} L ${points[1].x},${points[1].y} L ${points[2].x},${points[2].y} L ${points[3].x},${points[3].y} Z`; const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", d); path.setAttribute("fill", "none"); path.setAttribute("stroke", "black"); path.setAttribute("stroke-width", rect.getAttribute("stroke-width")); rect.parentNode.replaceChild(path, rect); return path; } // 批量转换SVG中的图形 const svg = document.querySelector("svg"); svg.querySelectorAll("circle").forEach(circleToPath); svg.querySelectorAll("rect").forEach(rectToPath);
步骤2:利用网格对称性直接生成闭合区域
你的Kufi网格具有明显的极坐标对称性(环形+旋转等分线),无需复杂的相交检测,可直接预计算所有扇形闭合区域:
const centerX = 500; const centerY = 500; // 从你的SVG中提取的环形半径列表 const radii = [20,30,50,60,80,90,110,120,140,150,170,180,200,210,230,240,260,270,290,300]; // 10等分的角度(0°、18°...162°) const angles = Array.from({length:10}, (_,i) => i*18); const regions = []; // 生成每个环形扇形区域 for (let rIdx = 0; rIdx < radii.length -1; rIdx++) { const innerR = radii[rIdx]; const outerR = radii[rIdx+1]; for (let aIdx = 0; aIdx < angles.length; aIdx++) { const startAngle = angles[aIdx] * Math.PI / 180; const endAngle = angles[(aIdx+1)%angles.length] * Math.PI / 180; // 构建扇形路径数据 const d = ` M ${centerX + innerR * Math.cos(startAngle)}, ${centerY + innerR * Math.sin(startAngle)} A ${innerR},${innerR} 0 0,1 ${centerX + innerR * Math.cos(endAngle)}, ${centerY + innerR * Math.sin(endAngle)} L ${centerX + outerR * Math.cos(endAngle)}, ${centerY + outerR * Math.sin(endAngle)} A ${outerR},${outerR} 0 0,0 ${centerX + outerR * Math.cos(startAngle)}, ${centerY + outerR * Math.sin(startAngle)} Z `; // 创建区域路径元素(透明填充,用于检测) const regionPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); regionPath.setAttribute("d", d); regionPath.setAttribute("fill", "transparent"); regionPath.setAttribute("stroke", "none"); regionPath.setAttribute("class", "kufi-region"); svg.appendChild(regionPath); regions.push({ path: new Path2D(d), element: regionPath }); } }
步骤3:实现鼠标悬停检测与着色功能
使用SVG的isPointInPath方法检测鼠标位置,实现实时高亮和点击着色:
// 鼠标悬停高亮 svg.addEventListener("mousemove", (e) => { const point = svg.createSVGPoint(); point.x = e.clientX; point.y = e.clientY; const svgPoint = point.matrixTransform(svg.getScreenCTM().inverse()); // 重置所有区域高亮 regions.forEach(reg => reg.element.setAttribute("fill", "transparent")); // 检测并高亮当前区域 regions.forEach(reg => { if (svg.isPointInPath(reg.path, svgPoint.x, svgPoint.y)) { reg.element.setAttribute("fill", "rgba(255, 165, 0, 0.3)"); } }); }); // 点击着色 svg.addEventListener("click", (e) => { const point = svg.createSVGPoint(); point.x = e.clientX; point.y = e.clientY; const svgPoint = point.matrixTransform(svg.getScreenCTM().inverse()); regions.forEach(reg => { if (svg.isPointInPath(reg.path, svgPoint.x, svgPoint.y)) { reg.element.setAttribute("fill", "#ff6b6b"); // 可替换为自定义颜色 } }); });
关键优化点
- 性能:利用网格对称性预生成区域,避免实时相交计算的性能损耗
- 精度:极坐标生成路径时使用弧度转换,保证区域边界与原网格完全对齐
- 交互体验:透明填充的区域元素不会遮挡原网格,同时能准确捕获鼠标事件
内容的提问来源于stack exchange,提问作者ibrahim Abdelmonem
相关产品推荐
相关产品推荐

