You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过客户端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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:23:13