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

如何在FabricJS中正确对齐反转的弧形文本?

修复fabric.curvedText反转后的文本对齐问题

当使用fabric.curvedText库的reverse选项时,反转后的弧形文本无法保持和原文本一致的对齐效果(比如居中对齐),问题出在原库计算反转文本的起始角度和定位时没有做对齐补偿。以下是修复方案:

修复思路

通过覆盖fabric.CurvedText的核心渲染方法,在反转文本时重新计算起始角度,确保文本始终围绕中心点对齐,同时调整字符的绘制偏移,让反转后的文本位置和原文本对齐逻辑一致。

修改后的完整代码

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.9/fabric.js"></script>
<script src="https://amz-rekognition.s3.amazonaws.com/json/fabric.curvedText.js"></script>

<!-- 修复reverse对齐的补丁代码 -->
<script>
// 覆盖fabric.CurvedText的字符宽度计算
fabric.CurvedText.prototype._getCharWidth = function(char) {
  var ctx = this.canvas.getContext();
  ctx.font = this.fontSize + 'px ' + this.fontFamily;
  return ctx.measureText(char).width;
};

// 覆盖弧形文本渲染逻辑,修复反转对齐
fabric.CurvedText.prototype._renderCurved = function(ctx) {
  var centerX = this.left + this.width / 2;
  var centerY = this.top + this.height / 2;
  var radius = this.radius;
  var anglePerChar = [];
  var totalAngle = 0;
  var chars = this.text.split('');
  var maxCharHeight = this.fontSize; // 简化处理,用字号作为字符高度

  // 计算每个字符对应的弧度
  for (var i = 0; i < chars.length; i++) {
    var charWidth = this._getCharWidth(chars[i]);
    var angle = (charWidth + this.spacing) / radius;
    anglePerChar.push(angle);
    totalAngle += angle;
  }

  // 计算起始弧度,确保文本居中
  var startAngle = -totalAngle / 2;
  if (this.reverse) {
    startAngle = totalAngle / 2;
    anglePerChar = anglePerChar.map(a => -a);
  }

  var currentAngle = startAngle;
  ctx.save();
  ctx.translate(centerX, centerY);
  ctx.rotate(-Math.PI / 2);

  for (var j = 0; j < chars.length; j++) {
    var char = chars[j];
    var angle = anglePerChar[j];
    var charWidth = this._getCharWidth(char);
    
    ctx.save();
    ctx.rotate(currentAngle + angle / 2);
    if (this.reverse) {
      // 反转时调整平移和缩放,保证字符正确显示且对齐
      ctx.translate(radius, -maxCharHeight / 2);
      ctx.scale(-1, 1);
    } else {
      ctx.translate(radius, -maxCharHeight / 2);
    }
    ctx.fillStyle = this.fill;
    ctx.strokeStyle = this.stroke;
    ctx.lineWidth = this.strokeWidth;
    ctx.font = this.fontSize + 'px ' + this.fontFamily;
    ctx.fillText(char, 0, 0);
    ctx.strokeText(char, 0, 0);
    ctx.restore();
    
    currentAngle += angle;
  }

  ctx.restore();
};
</script>

<canvas id="c" width="400" height="300"></canvas><br/>
<input type="text" id="text" value="CurvedText" /><br>
Reverse : <input type="checkbox" name="reverse" id="reverse" /><br>
Radius : <input type="range"  min="0" max="500" value="50" id="radius" /><br>
Spacing : <input type="range"  min="5" max="40" value="20" id="spacing" /><br>
Color : <input type="color"  value="blue" id="fill" /><br>
Effect : <select name="effect" id="effect" >
  <option value="curved">Curved</option>
  <option value="arc">Arc</option>
  <option value="STRAIGHT">STRAIGHT</option>
  <option value="smallToLarge">smallToLarge</option>
  <option value="largeToSmallTop">largeToSmallToped</option>
  <option value="largeToSmallBottom">largeToSmallBottom</option>
  <option value="bulge">bulge</option>
</select><br>
<br>
<button id="convert">Convert Text/Curved</button>

CSS

canvas {
    border:1px solid #000;
}

JavaScript

$(function () {
    canvas = new fabric.Canvas('c');
    canvas.on('selection:cleared', onDeSelected);
    canvas.on('object:selected', onSelected);
    canvas.on('selection:created', onSelected);
    var CurvedText = new fabric.CurvedText('CurvedText',{
        left: 100,
        top: 5,
        textAlign: 'center',
        fill: '#0000FF',
        radius: 100,
        fontSize: 20,
        spacing: 20,
        stroke:"#FFFF00",
        strokeWidth:1
    });
    
    var CurvedText1 = new fabric.CurvedText('CurvedText',{
        left: 100,
        top: 100,
        textAlign: 'center',
        fill: '#0000FF',
        radius: 100,
        fontSize: 20,
        spacing: 20,
        stroke:"#FFFF00",
        strokeWidth:1,
        reverse:true
    });
    
    
    canvas.add(CurvedText).renderAll();
    canvas.add(CurvedText1).renderAll();
    $('#text').keyup(function(){
        var obj = canvas.getActiveObject();
        if(obj){
            obj.setText(this.value);
            canvas.renderAll();
        }
    });
    $('#reverse').click(function(){
        var obj = canvas.getActiveObject();
        if(obj){
            obj.set('reverse',$(this).is(':checked')); 
            canvas.renderAll();
        }
    });
    $('#radius, #spacing').change(function(){
        var obj = canvas.getActiveObject();
        if(obj){
            obj.set($(this).attr('id'),$(this).val()); 
        }
        canvas.renderAll();
    });
    $('#effect').change(function(){
        var obj = canvas.getActiveObject();
        if(obj){
            obj.set($(this).attr('id'),$(this).val()); 
        }
        canvas.renderAll();
    });
    $('#fill').change(function(){
        var obj = canvas.getActiveObject();
        if(obj){
            obj.setFill($(this).val()); 
        }
        canvas.renderAll();
    });
    $('#convert').click(function(){
        var props = {};
        var obj = canvas.getActiveObject();
        if(obj){
            if(/curvedText/.test(obj.type)) {
                default_text = obj.getText();
                props = obj.toObject();
                delete props['type'];
                var textSample = new fabric.Text(default_text, props);
            }else if(/text/.test(obj.type)) {
                default_text = obj.getText();
                props = obj.toObject();
                delete props['type'];
                props['textAlign'] = 'center';
                props['radius'] = 50;
                props['spacing'] = 20;
                var textSample = new fabric.CurvedText(default_text, props);
            }
            canvas.remove(obj);
            canvas.add(textSample).renderAll();
            canvas.setActiveObject(canvas.item(canvas.getObjects().length-1));
        }
    });
});
function onSelected(){
    var obj = canvas.getActiveObject();
    $('#text').val(obj.getText());
    $('#reverse').prop('checked', obj.get('reverse'));
    $('#radius').val(obj.get('radius'));
    $('#spacing').val(obj.get('spacing'));
    $('#fill').val(obj.getFill());
    $('#effect').val(obj.getEffect());
}
function onDeSelected(){
    $('#text').val('');
    $('#reverse').prop('checked', false);
    $('#radius').val(50);
    $('#spacing').val(20);
    $('#fill').val('#0000FF');
    $('#effect').val('curved');
}

说明

  • 补丁代码通过覆盖fabric.CurvedText的_renderCurved方法,重新计算反转文本的起始弧度,确保文本始终围绕中心点居中对齐。
  • 调整了反转时字符的绘制偏移和缩放,保证字符显示方向正确的同时,对齐效果和非反转文本一致。
  • 无需修改原库文件,只需在引入原库后添加补丁代码即可生效。

内容的提问来源于stack exchange,提问作者NekoLopez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 12:47:03