我如何在 fabricjs 圆圈周围绘制字母

How can i plot letter around a fabricjs circle

我在 canvas 中添加了一个圆圈,然后我想在圆圈周围添加一些文字。 这是我目前所拥有的

var circle = new fabric.Circle({
  top: 100,
  left: 100,
  radius: 100,
  fill: '',
  stroke: 'green',
});
canvas.add(circle);

var obj = "some text"

for(letter in obj){             
  var newLetter = new fabric.Text(obj[letter], {
    top: 100,
    left: 100
});

canvas.add(newLetter);
canvas.renderAll();
}

我已经尝试了一些在网上发布的其他解决方案,但到目前为止 fabric 都无法正常工作。

循环文本。

我开始这个回答时以为它会很简单,但它变得有点难看。我将其回滚到更简单的版本。

我遇到的问题很基础,但没有简单的解决方案..

  • 环绕圆圈的文字可能会颠倒过来。不利于 阅读
  • 间距。因为 canvas 只给出了一个基本的二维变换,我可以 不单独缩放文本的顶部和底部导致 文本看起来间距太宽或太压扁。

我有一个完全不同的方法,因为它太重了,无法在这里回答。它涉及自定义扫描线渲染(某种 GPU hack),因此如果文本质量至关重要,您可以尝试沿着这些线寻找一些东西。

我遇到的问题是通过忽略它们来解决的,总是一个很好的解决方案。哈哈

如何在 2D 上呈现圆形文本 canvas。

由于无法在一次调用中完成,我编写了一个函数,一次渲染一个字符。我使用 ctx.measureText 获取要绘制的整个字符串的大小,然后将其转换为 angular 像素大小。然后对各种选项、对齐、拉伸和方向(镜像)进行一些调整,我一次一个地检查字符串中的每个字符,使用 ctx.measureText 测量它的大小,然后使用 ctx.setTransform定位旋转和缩放角色,然后调用 ctx.fillText() 渲染那个角色。

它比 ctx.fillText() 方法慢一点,但是填充文本不能画在圆圈上可以吗。

需要一些计算。

针对给定半径锻炼像素的 angular 大小是微不足道的,但我经常看到做得不正确。因为 Javascript 以弧度工作 angular 像素大小只是

var angularPixelSize = 1 / radius; // simple

从而计算一些文本在圆或给定半径上占据的角度。

var textWidth = ctx.measureText("Hello").width;
var textAngularWidth = angularPixelSize * textWidth;

锻炼单个字符的大小。

var text = "This is some text";
var index = 2; // which character
var characterWidth = ctx.measureText(text[index]).width;
var characterAngularWidth = angularPixelSize * textWidth;

因此您拥有 angular 大小,您现在可以将圆上的文本居中、向右或向左对齐。有关详细信息,请参阅代码段。

然后您需要逐个遍历每个字符,一次计算转换、渲染文本、为下一个字符移动正确的 angular 距离,直到完成。

var angle = ?? // the start angle
for(var i = 0; i < text.length; i += 1){ // for each character in the string
    var c = text[i]; // get character
    // get character angular width
    var w = ctx.measureText(c).width * angularPixelSize;
    // set the matrix  to align the text. See code under next paragraph 
    ...
    ...
    // matrix set
    ctx.fillText(c,0,0); // as the matrix set the origin just render at 0,0
    angle += w;
}

繁琐的数学部分是设置变换。我发现直接使用变换矩阵更容易,这样我就可以处理缩放等问题,而不必使用太多变换调用。

Set transform取6个数,前两个是x轴方向,后两个是y轴方向,最后两个是从canvas原点的平移。

所以得到Y轴。对于每个字符,从圆心向外移动的线我们需要绘制字符的角度并减少错位(注意减少而不是消除)angular 宽度,以便我们可以使用字符的中心来对齐它。

// assume angle is position and w is character angular width from above code
var xDx = Math.cos(angle + w / 2); // get x part of X axis direction
var xDy = Math.sin(angle + w / 2); // get y part of X axis direction

现在我们有了将作为 x 轴的归一化向量。角色是沿着这个轴从左到右绘制的。我一次性构建了矩阵,但我将在下面对其进行分解。请注意,我在我的代码片段中用角度做了一个 boo boo,所以代码回到前面(X 是 Y,Y 是 X) 请注意,该代码段能够在两个角度之间调整文本,因此我缩放 x 轴以允许这样做。

// assume scale is how much the text is squashed along its length. 
ctx.setTransform(
    xDx * scale, xDy * scale, // set the direction and size of a pixel for the X axis
    -xDy, xDx,                // the direction ot the Y axis is perpendicular so switch x and y 
    -xDy * radius + x, xdx * radius + y  // now set the origin by scaling by radius and translating by the circle center
);

这就是绘制圆形字符串的数学和逻辑。对不起,但我不使用 fabric.js 所以它可能有也可能没有选项。但是您可以创建自己的函数并直接渲染到与 fabric.js 相同的 canvas,因为它不排除访问。虽然保存和恢复 canvas 状态是值得的,因为 fabric.js 不知道状态变化。

下面是一个片段,展示了上面的实践。它远非理想,但大约是使用现有 canvas 2D API 可以快速完成的最佳效果。 Snippet有测量和绘图两个功能加上一些基本的使用例子。

function showTextDemo(){
/** Include fullScreenCanvas.js begin **/
var canvas = document.getElementById("canv");
if(canvas !== null){
    document.body.removeChild(canvas);
}
canvas = (function () {
    // creates a blank image with 2d context
    canvas = document.createElement("canvas");  
    canvas.id = "canv";
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight; 
    canvas.style.position = "absolute";
    canvas.style.top = "0px";
    canvas.style.left = "0px";
    canvas.ctx = canvas.getContext("2d"); 
    document.body.appendChild(canvas);
    return canvas;
} ) ();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/





// measure circle text
// ctx: canvas context
// text: string of text to measure
// x,y: position of center
// r: radius in pixels
//
// returns the size metrics of the text
//
// width: Pixel width of text
// angularWidth : angular width of text in radians
// pixelAngularSize : angular width of a pixel in radians
var measureCircleText = function(ctx, text, x, y, radius){
    var textWidth;
    // get the width of all the text
    textWidth = ctx.measureText(text).width;
    return {
        width               :textWidth,
        angularWidth        : (1 / radius) * textWidth,
        pixelAngularSize    : 1 / radius
    }
}

// displays text alon a circle
// ctx: canvas context
// text: string of text to measure
// x,y: position of center
// r: radius in pixels
// start: angle in radians to start. 
// [end]: optional. If included text align is ignored and the text is 
//        scalled to fit between start and end;
// direction 
var circleText = function(ctx,text,x,y,radius,start,end,direction){
    var i, textWidth, pA, pAS, a, aw, wScale, aligned, dir;
    // save the current textAlign so that it can be restored at end
    aligned = ctx.textAlign;
    
    dir = direction ? 1 : -1;
    // get the angular size of a pixel in radians
    pAS = 1 / radius;
    
    // get the width of all the text
    textWidth = ctx.measureText(text).width;
    
    // if end is supplied then fit text between start and end
    if(end !== undefined){
        pA = ((end - start) / textWidth) * dir;
        wScale = (pA / pAS) * dir;
    }else{ // if no end is supplied corret start and end for alignment
        pA = -pAS * dir;
        wScale = -1 * dir;
        switch(aligned){
            case "center": // if centered move around half width
                start -= pA * (textWidth / 2);
                end = start + pA * textWidth;
                break;
            case "right":
                end = start;
                start -= pA * textWidth;
                break;
            case "left":
                end = start + pA * textWidth;
        }
    }

    // some code to help me test. Left it here incase someone wants to underline
    // rmove the following 3 lines if you dont need underline
    ctx.beginPath();
    ctx.arc(x,y,radius,end,start,end>start?true:false);
    ctx.stroke();

    ctx.textAlign = "center"; // align for rendering

    a = start;  // set the start angle
    for (var i = 0; i < text.length; i += 1) {  // for each character
        // get the angular width of the text
        aw = ctx.measureText(text[i]).width * pA;
        var xDx = Math.cos(a + aw / 2); // get the yAxies vector from the center x,y out
        var xDy = Math.sin(a + aw / 2);
        if (xDy < 0) {  // is the text upside down. If it is flip it
            // sets the transform for each character scaling width if needed
            ctx.setTransform(-xDy * wScale, xDx * wScale,-xDx,-xDy, xDx * radius + x,xDy * radius + y);
        }else{
            ctx.setTransform(-xDy * wScale, xDx * wScale, xDx, xDy, xDx * radius + x, xDy * radius + y);
        }
        // render the character
        ctx.fillText(text[i],0,0);

        a += aw;

    }
    ctx.setTransform(1,0,0,1,0,0);
    ctx.textAlign = aligned;
}


// set up canvas
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;   // centers
var ch = h / 2;
var rad = (h / 2) * 0.9;  // radius
// clear
ctx.clearRect(0, 0, w, h)
// the font
var fontSize = Math.floor(h/20);
if(h < 400){
   var fontSize = 10;
}
ctx.font = fontSize + "px verdana";
// base settings
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillStyle = "#666";
ctx.strokeStyle = "#666";

// Text under stretched
circleText(ctx, "Test of circular text rendering", cw, ch, rad, Math.PI, 0, true);
// Text over stretchered
ctx.fillStyle = "Black";
circleText(ctx, "This text is over the top", cw, ch, rad, Math.PI, Math.PI * 2, true);
 
// Show centered text
rad -= fontSize + 4;
ctx.fillStyle = "Red";
// Use measureCircleText to get angular size
var tw = measureCircleText(ctx, "Centered", cw, ch, rad).angularWidth;
// centered bottom and top
circleText(ctx, "Centered", cw, ch, rad, Math.PI / 2, undefined, true);
circleText(ctx, "Centered", cw, ch, rad, -Math.PI * 0.5, undefined, false);
// left align bottom and top
ctx.textAlign = "left";
circleText(ctx, "Left Align", cw, ch, rad, Math.PI / 2 - tw * 0.6, undefined, true);
circleText(ctx, "Left Align Top", cw, ch, rad, -Math.PI / 2 + tw * 0.6, undefined, false);
// right align bottom and top
ctx.textAlign = "right";
circleText(ctx, "Right Align", cw, ch, rad, Math.PI / 2 + tw * 0.6, undefined, true);
circleText(ctx, "Right Align Top", cw, ch, rad, -Math.PI / 2 - tw * 0.6, undefined, false);

// Show base line at middle
ctx.fillStyle = "blue";
rad -= fontSize + fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
circleText(ctx, "Baseline Middle", cw, ch, rad, Math.PI / 2, undefined, true);
circleText(ctx, "Baseline Middle", cw, ch, rad, -Math.PI / 2, undefined, false);

// show baseline at top
ctx.fillStyle = "Green";
rad -= fontSize + fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "top";
circleText(ctx, "Baseline top", cw, ch, rad, Math.PI / 2, undefined, true);
circleText(ctx, "Baseline top", cw, ch, rad, -Math.PI / 2, undefined, false);
}

showTextDemo();
window.addEventListener("resize",showTextDemo);