如何通过客户端JavaScript提取TrueType字体中字符的中心线?
从TTF字体提取字符中心线(基于opentype.js)
核心思路
字符的中心线本质是内外轮廓的等距中线:对于封闭的内外轮廓(如O、B这类带空心的字符),将外轮廓向内偏移一半笔画宽度,内轮廓向外偏移一半笔画宽度,两者重合的部分就是中心线;对于单轮廓字符(如I、L这类实心字符),则将轮廓向自身中心偏移一半笔画宽度得到中心线。
实现步骤
1. 获取字符轮廓与笔画宽度
先通过opentype.js加载字体并提取目标字符的内外轮廓,再计算笔画宽度:
// 假设已通过FileReader或URL加载字体文件为ArrayBuffer const font = opentype.parse(fontArrayBuffer); const glyph = font.getGlyph('O'); // 替换为目标字符 const contours = glyph.contours; // 内外轮廓数组,通常外轮廓在前、内轮廓在后 // 计算内外轮廓的平均距离作为笔画宽度 function getStrokeWidth(outerContour, innerContour) { let totalDist = 0; const pointCount = Math.min(outerContour.points.length, innerContour.points.length); for (let i = 0; i < pointCount; i++) { const p1 = outerContour.points[i]; const p2 = innerContour.points[i]; totalDist += Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); } return totalDist / pointCount; } const strokeWidth = getStrokeWidth(contours[0], contours[1]); const offsetDistance = strokeWidth / 2; // 偏移量为笔画宽度的一半
2. 实现路径偏移逻辑
opentype.js没有内置路径偏移方法,需手动处理不同类型的路径命令(移动、直线、贝塞尔曲线):
// 偏移单个点 function offsetPoint(point, normal, distance) { return { x: point.x + normal.x * distance, y: point.y + normal.y * distance, type: point.type }; } // 获取线段的单位法向量(向内偏移取逆时针方向,向外取顺时针) function getLineNormal(p1, p2, isInward) { const dx = p2.x - p1.x; const dy = p2.y - p1.y; const len = Math.sqrt(dx*dx + dy*dy); return isInward ? { x: -dy/len, y: dx/len } : { x: dy/len, y: -dx/len }; } // 偏移整个轮廓 function offsetContour(contour, distance, isInward) { const offsetPoints = []; const points = contour.points; const pointCount = points.length; for (let i = 0; i < pointCount; i++) { const curr = points[i]; const next = points[(i+1)%pointCount]; if (curr.type === 'M') { // 移动点:基于当前点与下一点的线段法向量偏移 const normal = getLineNormal(curr, next, isInward); offsetPoints.push(offsetPoint(curr, normal, distance)); } else if (curr.type === 'L') { // 直线点:基于当前线段的法向量偏移 const normal = getLineNormal(points[i-1], curr, isInward); offsetPoints.push(offsetPoint(curr, normal, distance)); } else if (curr.type === 'C') { // 贝塞尔曲线点:取相邻线段法向量的平均值作为偏移方向(简化实现) const prev = points[i-1]; const ctrl1 = curr; const ctrl2 = points[(i+1)%pointCount]; const end = points[(i+2)%pointCount]; const normal1 = getLineNormal(prev, ctrl1, isInward); const normal2 = getLineNormal(ctrl2, end, isInward); const avgNormal = { x: (normal1.x + normal2.x)/2, y: (normal1.y + normal2.y)/2 }; offsetPoints.push(offsetPoint(ctrl1, avgNormal, distance)); offsetPoints.push(offsetPoint(ctrl2, avgNormal, distance)); } } return new opentype.Contour(offsetPoints); }
3. 生成并渲染中心线
对内外轮廓分别偏移后,取结果作为中心线并转换为可绘制路径:
// 外轮廓向内偏移,内轮廓向外偏移 const offsetOuter = offsetContour(contours[0], offsetDistance, true); const offsetInner = offsetContour(contours[1], offsetDistance, false); // 对称字符的偏移后轮廓几乎重合,取任意一个作为中心线即可 const centerlineContour = offsetOuter; // 将轮廓转换为Canvas可识别的Path2D function contourToPath(contour) { const path = new Path2D(); contour.commands.forEach(cmd => { if (cmd.type === 'M') path.moveTo(cmd.x, cmd.y); else if (cmd.type === 'L') path.lineTo(cmd.x, cmd.y); else if (cmd.type === 'C') path.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); else if (cmd.type === 'Z') path.closePath(); }); return path; } // 渲染到Canvas const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke(contourToPath(centerlineContour));
注意事项
- 贝塞尔曲线精度:上述贝塞尔曲线偏移是简化实现,对于带尖锐拐点的字符(如W、V),需用更复杂的细分算法避免变形。
- 轮廓方向:TrueType字体的内外轮廓通常方向相反(外顺时针、内逆时针),偏移时需确保方向正确,否则会得到错误的中心线。
- 单轮廓字符:对于只有单个轮廓的字符(如I),可通过计算轮廓的对称中线实现,或向轮廓两侧偏移后取中间线。
内容的提问来源于stack exchange,提问作者samcodee
相关产品推荐
相关产品推荐

