如何在HTML 5画布上绘制沿弧线路径的文本?

5

你有什么问题?你尝试过什么? - j08691
1
你需要学习编程和数学。HTML5画布上下文中没有简单的“沿着这条路径绘制文本”的方法。 - Phrogz
4
我也是一名计算机工程师,我懂数学,兄弟。 - EnzGLKba
@phrogz 我确信 Firefox 曾经支持沿路径的文本。我记得多年前看到过一个演示,使用此功能将文本绕着圆形排列。我还记得看到 W3C 画布参考(可能是未来版本),允许在 fillText() 函数中给出路径 - 所以也许那就是同一件事情。不过这可能只是一种幻想。 - Richard
4个回答

27

我有一个jsFiddle示例,可以将文本应用于任意贝塞尔曲线定义。享受http://jsfiddle.net/Makallus/hyyvpp8g/

var first = true;
startIt();


function startIt() {
  canvasDiv = document.getElementById('canvasDiv');
  canvasDiv.innerHTML = '<canvas id="layer0" width="300" height="300"></canvas>'; //for IE
  canvas = document.getElementById('layer0');
  ctx = canvas.getContext('2d');
  ctx.fillStyle = "black";
  ctx.font = "18px arial black";
  curve = document.getElementById('curve');
  curveText = document.getElementById('text');
  $(curve).keyup(function(e) {
    changeCurve();
  });
  $(curveText).keyup(function(e) {
    changeCurve();
  });




  if (first) {
    changeCurve();
    first = false;
  }

}

function changeCurve() {
  points = curve.value.split(',');
  if (points.length == 8) drawStack();

}

function drawStack() {
  Ribbon = {
    maxChar: 50,
    startX: points[0],
    startY: points[1],
    control1X: points[2],
    control1Y: points[3],
    control2X: points[4],
    control2Y: points[5],
    endX: points[6],
    endY: points[7]
  };

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.save();
  ctx.beginPath();

  ctx.moveTo(Ribbon.startX, Ribbon.startY);
  ctx.bezierCurveTo(Ribbon.control1X, Ribbon.control1Y,
    Ribbon.control2X, Ribbon.control2Y,
    Ribbon.endX, Ribbon.endY);

  ctx.stroke();
  ctx.restore();

  FillRibbon(curveText.value, Ribbon);
}

function FillRibbon(text, Ribbon) {

  var textCurve = [];
  var ribbon = text.substring(0, Ribbon.maxChar);
  var curveSample = 1000;


  xDist = 0;
  var i = 0;
  for (i = 0; i < curveSample; i++) {
    a = new bezier2(i / curveSample, Ribbon.startX, Ribbon.startY, Ribbon.control1X, Ribbon.control1Y, Ribbon.control2X, Ribbon.control2Y, Ribbon.endX, Ribbon.endY);
    b = new bezier2((i + 1) / curveSample, Ribbon.startX, Ribbon.startY, Ribbon.control1X, Ribbon.control1Y, Ribbon.control2X, Ribbon.control2Y, Ribbon.endX, Ribbon.endY);
    c = new bezier(a, b);
    textCurve.push({
      bezier: a,
      curve: c.curve
    });
  }

  letterPadding = ctx.measureText(" ").width / 4;
  w = ribbon.length;
  ww = Math.round(ctx.measureText(ribbon).width);


  totalPadding = (w - 1) * letterPadding;
  totalLength = ww + totalPadding;
  p = 0;

  cDist = textCurve[curveSample - 1].curve.cDist;

  z = (cDist / 2) - (totalLength / 2);

  for (i = 0; i < curveSample; i++) {
    if (textCurve[i].curve.cDist >= z) {
      p = i;
      break;
    }
  }

  for (i = 0; i < w; i++) {
    ctx.save();
    ctx.translate(textCurve[p].bezier.point.x, textCurve[p].bezier.point.y);
    ctx.rotate(textCurve[p].curve.rad);
    ctx.fillText(ribbon[i], 0, 0);
    ctx.restore();

    x1 = ctx.measureText(ribbon[i]).width + letterPadding;
    x2 = 0;
    for (j = p; j < curveSample; j++) {
      x2 = x2 + textCurve[j].curve.dist;
      if (x2 >= x1) {
        p = j;
        break;
      }
    }




  }
} //end FillRibon

function bezier(b1, b2) {
  //Final stage which takes p, p+1 and calculates the rotation, distance on the path and accumulates the total distance
  this.rad = Math.atan(b1.point.mY / b1.point.mX);
  this.b2 = b2;
  this.b1 = b1;
  dx = (b2.x - b1.x);
  dx2 = (b2.x - b1.x) * (b2.x - b1.x);
  this.dist = Math.sqrt(((b2.x - b1.x) * (b2.x - b1.x)) + ((b2.y - b1.y) * (b2.y - b1.y)));
  xDist = xDist + this.dist;
  this.curve = {
    rad: this.rad,
    dist: this.dist,
    cDist: xDist
  };
}

function bezierT(t, startX, startY, control1X, control1Y, control2X, control2Y, endX, endY) {
  //calculates the tangent line to a point in the curve; later used to calculate the degrees of rotation at this point.
  this.mx = (3 * (1 - t) * (1 - t) * (control1X - startX)) + ((6 * (1 - t) * t) * (control2X - control1X)) + (3 * t * t * (endX - control2X));
  this.my = (3 * (1 - t) * (1 - t) * (control1Y - startY)) + ((6 * (1 - t) * t) * (control2Y - control1Y)) + (3 * t * t * (endY - control2Y));
}

function bezier2(t, startX, startY, control1X, control1Y, control2X, control2Y, endX, endY) {
  //Quadratic bezier curve plotter
  this.Bezier1 = new bezier1(t, startX, startY, control1X, control1Y, control2X, control2Y);
  this.Bezier2 = new bezier1(t, control1X, control1Y, control2X, control2Y, endX, endY);
  this.x = ((1 - t) * this.Bezier1.x) + (t * this.Bezier2.x);
  this.y = ((1 - t) * this.Bezier1.y) + (t * this.Bezier2.y);
  this.slope = new bezierT(t, startX, startY, control1X, control1Y, control2X, control2Y, endX, endY);

  this.point = {
    t: t,
    x: this.x,
    y: this.y,
    mX: this.slope.mx,
    mY: this.slope.my
  };
}

function bezier1(t, startX, startY, control1X, control1Y, control2X, control2Y) {
  //linear bezier curve plotter; used recursivly in the quadratic bezier curve calculation
  this.x = ((1 - t) * (1 - t) * startX) + (2 * (1 - t) * t * control1X) + (t * t * control2X);
  this.y = ((1 - t) * (1 - t) * startY) + (2 * (1 - t) * t * control1Y) + (t * t * control2Y);

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table>
  <TR>
    <TH>Bezier Curve</TH>
    <TD>
      <input size="80" type="text" id="curve" name="curve" value="99.2,177.2,130.02,60.0,300.5,276.2,300.7,176.2">
    </TD>
  </TR>
  <TR>
    <TH>Text</TH>
    <TD>
      <input size="80" type="text" id="text" name="text" value="testing 1234567890">
    </TD>
  </TR>
  <TR>
    <TD colspan=2>
      <div id="canvasDiv"></div>
    </TD>
  </TR>
</table>


1
你是个天才!厉害的代码,对我非常有用。 - santillanix
只是为了明确一下,@Makallus,这段代码只适用于具有2个锚点和2个控制点的贝塞尔曲线吗? - TKoL
如何根据文本长度将文本适应路径。在这张图片中,我尝试通过更改curveSample数字为example。https://3.pik.vn/2020cd3be257-91b2-4235-8ebf-f877f43a2417.png - hyphens2
这对于像阿拉伯语这样的复杂文本不起作用。字母无法粘在一起。有什么解决办法吗? - Omid Sadeghi

14

一个古老的问题...然而,在我的博客上,我非常详细地探讨了使用HTML5 Canvas创建圆形文本:

html5graphics.blogspot.com

示例中的选项包括从给定角度实现的圆形文本对齐(左、中和右),内向和外向文本,字距(可调整字符之间的间隔)以及半径内或半径外的文本。

还有一个带有工作示例的 jsfiddle

代码如下:

document.body.appendChild(getCircularText("ROUNDED TEXT LOOKS BEST IN CAPS!", 250, 0, "center", true, true, "Arial", "18pt", 0));

function getCircularText(text, diameter, startAngle, align, textInside, inwardFacing, fName, fSize, kerning) {
    // text:         The text to be displayed in circular fashion
    // diameter:     The diameter of the circle around which the text will
    //               be displayed (inside or outside)
    // startAngle:   In degrees, Where the text will be shown. 0 degrees
    //               if the top of the circle
    // align:        Positions text to left right or center of startAngle
    // textInside:   true to show inside the diameter. False draws outside
    // inwardFacing: true for base of text facing inward. false for outward
    // fName:        name of font family. Make sure it is loaded
    // fSize:        size of font family. Don't forget to include units
    // kearning:     0 for normal gap between letters. positive or
    //               negative number to expand/compact gap in pixels
 //------------------------------------------------------------------------

    // declare and intialize canvas, reference, and useful variables
    align = align.toLowerCase();
    var mainCanvas = document.createElement('canvas');
    var ctxRef = mainCanvas.getContext('2d');
    var clockwise = align == "right" ? 1 : -1; // draw clockwise for aligned right. Else Anticlockwise
    startAngle = startAngle * (Math.PI / 180); // convert to radians

    // calculate height of the font. Many ways to do this
    // you can replace with your own!
    var div = document.createElement("div");
    div.innerHTML = text;
    div.style.position = 'absolute';
    div.style.top = '-10000px';
    div.style.left = '-10000px';
    div.style.fontFamily = fName;
    div.style.fontSize = fSize;
    document.body.appendChild(div);
    var textHeight = div.offsetHeight;
    document.body.removeChild(div);

    // in cases where we are drawing outside diameter,
    // expand diameter to handle it
    if (!textInside) diameter += textHeight * 2;

    mainCanvas.width = diameter;
    mainCanvas.height = diameter;
    // omit next line for transparent background
    mainCanvas.style.backgroundColor = 'lightgray'; 
    ctxRef.font = fSize + ' ' + fName;

    // Reverse letter order for align Left inward, align right outward 
    // and align center inward.
    if (((["left", "center"].indexOf(align) > -1) && inwardFacing) || (align == "right" && !inwardFacing)) text = text.split("").reverse().join(""); 

    // Setup letters and positioning
    ctxRef.translate(diameter / 2, diameter / 2); // Move to center
    startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward
    ctxRef.textBaseline = 'middle'; // Ensure we draw in exact center
    ctxRef.textAlign = 'center'; // Ensure we draw in exact center

    // rotate 50% of total angle for center alignment
    if (align == "center") {
        for (var j = 0; j < text.length; j++) {
            var charWid = ctxRef.measureText(text[j]).width;
            startAngle += ((charWid + (j == text.length-1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise;
        }
    }

    // Phew... now rotate into final start position
    ctxRef.rotate(startAngle);

    // Now for the fun bit: draw, rotate, and repeat
    for (var j = 0; j < text.length; j++) {
        var charWid = ctxRef.measureText(text[j]).width; // half letter

        ctxRef.rotate((charWid/2) / (diameter / 2 - textHeight) * clockwise);  // rotate half letter

        // draw char at "top" if inward facing or "bottom" if outward
        ctxRef.fillText(text[j], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2));

        ctxRef.rotate((charWid/2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter
    }

    // Return it
    return (mainCanvas);
}

有什么想法为什么在文本输入开头的 < 会打破定位,直到提供了 > 和一些文本为止?测试:
  • 将文本设置为“<a”(打破)
  • 然后将文本设置为“<a>”(打破)
  • 然后将文本设置为“<a>a”(正常工作)
是画布元素内部认为这是一个标签吗?
- nokturnal
2
啊啊啊,它在“文本高度”计算时出现了错误,因为它检测到了“HTML”。将 div.innerHTML = text; 更新为 div.innerHTML = $('<div>').text(text).html();(假设您正在使用jQuery)。 - nokturnal

12
你可以尝试以下代码,使用HTML5 Canvas编写沿着弧形路径的文字。
function drawTextAlongArc(context, str, centerX, centerY, radius, angle) {
  var len = str.length,
    s;
  context.save();
  context.translate(centerX, centerY);
  context.rotate(-1 * angle / 2);
  context.rotate(-1 * (angle / len) / 2);
  for (var n = 0; n < len; n++) {
    context.rotate(angle / len);
    context.save();
    context.translate(0, -1 * radius);
    s = str[n];
    context.fillText(s, 0, 0);
    context.restore();
  }
  context.restore();
}
var canvas = document.getElementById('myCanvas'),
  context = canvas.getContext('2d'),
  centerX = canvas.width / 2,
  centerY = canvas.height - 30,
  angle = Math.PI * 0.8,
  radius = 150;

context.font = '30pt Calibri';
context.textAlign = 'center';
context.fillStyle = 'blue';
context.strokeStyle = 'blue';
context.lineWidth = 4;
drawTextAlongArc(context, 'Text along arc path', centerX, centerY, radius, angle);

// draw circle underneath text
context.arc(centerX, centerY, radius - 10, 0, 2 * Math.PI, false);
context.stroke();
<!DOCTYPE HTML>
<html>

<head>
  <style>
    body {
      margin: 0px;
      padding: 0px;
    }
  </style>
</head>

<body>
  <canvas id="myCanvas" width="578" height="250"></canvas>
</body>

</html>


4

无法使用任何内置方法。请注意,SVG本身支持沿路径的文本,因此您可能希望考虑使用SVG!

但是,您可以编写自定义代码以实现相同的效果,就像我们为这个问题做的一样:HTML5 Canvas Circle Text


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接