HTML5画布ctx.fillText为什么不换行?

139

如果文本中包含 "\n",我似乎无法将文本添加到画布上。我的意思是,换行符不会显示/起作用。

ctxPaint.fillText("s  ome \n \\n <br/> thing", x, y);

以上代码将在一行上绘制"s ome \n <br/> thing"

这是fillText的限制还是我的问题?"\n"存在但没有被打印出来,也不起作用。


1
你想在到达末尾时自动换行吗?还是只考虑文本中存在的换行符? - Gabriele Petrioli
将文本换行为多行。 - Tower
嗨,twodordan,这个限制在Chrome和Mozilla上都存在吗? 人们经常使用简单的HTML文本,并将其放置在画布上,例如position:absolute。 此外,您可以使用两个fillText并移动文本的Y原点以获取第二行。 - Tim
简而言之:要么多次调用 fillText() 并使用字体高度进行分隔,要么使用 https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/measureText - 或者使用下面非常复杂的“解决方案”之一,但不使用 TextMetrics... - Andrew
20个回答

114

如果您只想处理文本中的换行符,可以通过在换行符处拆分文本并多次调用fillText()来模拟它。

类似于http://jsfiddle.net/BaG4J/1/

var c = document.getElementById('c').getContext('2d');
c.font = '11px Courier';
    console.log(c);
var txt = 'line 1\nline 2\nthird line..';
var x = 30;
var y = 30;
var lineheight = 15;
var lines = txt.split('\n');

for (var i = 0; i<lines.length; i++)
    c.fillText(lines[i], x, y + (i*lineheight) );
canvas{background-color:#ccc;}
<canvas id="c" width="150" height="150"></canvas>


我刚刚做了一个包裹的概念验证(在指定宽度上进行绝对换行,但尚未处理单词中断
示例位于http://jsfiddle.net/BaG4J/2/

var c = document.getElementById('c').getContext('2d');
c.font = '11px Courier';

var txt = 'this is a very long text to print';

printAt(c, txt, 10, 20, 15, 90 );


function printAt( context , text, x, y, lineHeight, fitWidth)
{
    fitWidth = fitWidth || 0;
    
    if (fitWidth <= 0)
    {
         context.fillText( text, x, y );
        return;
    }
    
    for (var idx = 1; idx <= text.length; idx++)
    {
        var str = text.substr(0, idx);
        console.log(str, context.measureText(str).width, fitWidth);
        if (context.measureText(str).width > fitWidth)
        {
            context.fillText( text.substr(0, idx-1), x, y );
            printAt(context, text.substr(idx-1), x, y + lineHeight, lineHeight,  fitWidth);
            return;
        }
    }
    context.fillText( text, x, y );
}
canvas{background-color:#ccc;}
<canvas id="c" width="150" height="150"></canvas>


这是一个单词换行(在空格处断开)的概念验证。
示例位于http://jsfiddle.net/BaG4J/5/


var c = document.getElementById('c').getContext('2d');
c.font = '11px Courier';

var txt = 'this is a very long text. Some more to print!';

printAtWordWrap(c, txt, 10, 20, 15, 90 );


function printAtWordWrap( context , text, x, y, lineHeight, fitWidth)
{
    fitWidth = fitWidth || 0;
    
    if (fitWidth <= 0)
    {
        context.fillText( text, x, y );
        return;
    }
    var words = text.split(' ');
    var currentLine = 0;
    var idx = 1;
    while (words.length > 0 && idx <= words.length)
    {
        var str = words.slice(0,idx).join(' ');
        var w = context.measureText(str).width;
        if ( w > fitWidth )
        {
            if (idx==1)
            {
                idx=2;
            }
            context.fillText( words.slice(0,idx-1).join(' '), x, y + (lineHeight*currentLine) );
            currentLine++;
            words = words.splice(idx-1);
            idx = 1;
        }
        else
        {idx++;}
    }
    if  (idx > 0)
        context.fillText( words.join(' '), x, y + (lineHeight*currentLine) );
}
canvas{background-color:#ccc;}
<canvas id="c" width="150" height="150"></canvas>

在第二个和第三个示例中,我使用了measureText() 方法,该方法显示在打印时字符串的长度(以像素为单位)。

如何对齐整个长文本? - amir22
如果您需要一个长的、对齐的文本,为什么要使用画布? - Mike 'Pomax' Kamermans
我喜欢第三种方法。 - Samnad Sainulabdeen
第三个方法似乎没有任何对if条件if(idx > 0)的要求。idx的初始值为1,在if条件之前没有任何地方对其进行减少。因此,if条件将始终为true。或者我有什么遗漏吗? - Nikunj Bhatt

70

很遗憾,Canvas的fillText存在一些限制,它不支持多行文本。更糟糕的是,没有内置的方法来测量行高,只能测量宽度,这使得自己实现变得更加困难!

许多人都编写了自己的多行文本支持,其中最著名的项目可能是Mozilla Skywriter

你需要做的主要是多次进行fillText调用,并在每次添加文本高度到y值中。 (我相信Skywriter中的人们测量M的宽度来近似文本。)


1
谢谢!我就觉得这会很麻烦...知道了SKYWRITER,但我会“等待”,直到fillText()得到改进。在我的情况下,这不是非常重要的事情。哈哈,没有行高,就像有人故意这样做一样。:D - Spectraljump
19
说实话,我认为不要抱太大希望fillText()被"改进"来支持这一点,因为我感觉它的使用方式就是这样(多次调用并自己计算yOffset)。我认为canvas API的很多优势在于将低级别的绘图功能与您已经可以完成的任务(执行必要的测量)分离开来。此外,您可以通过提供文本大小(以像素为单位)来知道文本高度;换句话说:context.font = "16px Arial"; - 您就有了高度;只有宽度是动态的。 - SikoSoft
1
已经添加了一些额外的属性(https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics)用于`measureText()`,我认为这可以解决问题。Chrome有一个标志来启用它们,但其他浏览器还没有... - SWdV
@SWdV 只是为了明确,这些规范已经存在多年了,可能还需要多年才能广泛采用 :( - Simon Sarris
2
也许可以使用 actualBoundingBoxAscent 来测量行高。 - Summer Sun

40

可能我有些晚到这场派对,但我发现下面的教程非常适合在画布上换行文本。

http://www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/

通过这个教程,我成功地让多行文本工作了(很抱歉 Ramirez,你的没能为我起作用!)。我的完整代码如下:

<script type="text/javascript">

     // http: //www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/
     function wrapText(context, text, x, y, maxWidth, lineHeight) {
        var cars = text.split("\n");

        for (var ii = 0; ii < cars.length; ii++) {

            var line = "";
            var words = cars[ii].split(" ");

            for (var n = 0; n < words.length; n++) {
                var testLine = line + words[n] + " ";
                var metrics = context.measureText(testLine);
                var testWidth = metrics.width;

                if (testWidth > maxWidth) {
                    context.fillText(line, x, y);
                    line = words[n] + " ";
                    y += lineHeight;
                }
                else {
                    line = testLine;
                }
            }

            context.fillText(line, x, y);
            y += lineHeight;
        }
     }

     function DrawText() {

         var canvas = document.getElementById("c");
         var context = canvas.getContext("2d");

         context.clearRect(0, 0, 500, 600);

         var maxWidth = 400;
         var lineHeight = 60;
         var x = 20; // (canvas.width - maxWidth) / 2;
         var y = 58;


         var text = document.getElementById("text").value.toUpperCase();                

         context.fillStyle = "rgba(255, 0, 0, 1)";
         context.fillRect(0, 0, 600, 500);

         context.font = "51px 'LeagueGothicRegular'";
         context.fillStyle = "#333";

         wrapText(context, text, x, y, maxWidth, lineHeight);
     }

     $(document).ready(function () {

         $("#text").keyup(function () {
             DrawText();
         });

     });

    </script>

其中c是我的canvas的ID,text是我的文本框的ID。

你可能已经看到我在使用非标准字体。你可以使用@font-face,只要你在操作canvas之前在某些文本上使用了该字体 - 否则canvas不会捕获该字体。

希望这能帮助到某些人。


30

将文本分成行,并单独绘制每一行:

function fillTextMultiLine(ctx, text, x, y) {
  var lineHeight = ctx.measureText("M").width * 1.2;
  var lines = text.split("\n");
  for (var i = 0; i < lines.length; ++i) {
    ctx.fillText(lines[i], x, y);
    y += lineHeight;
  }
}

20

这是我的解决方案,修改了已经在此处提供的流行的wrapText()函数。我使用JavaScript的原型功能,这样您就可以从画布上下文中调用该函数。

CanvasRenderingContext2D.prototype.wrapText = function (text, x, y, maxWidth, lineHeight) {

    var lines = text.split("\n");

    for (var i = 0; i < lines.length; i++) {

        var words = lines[i].split(' ');
        var line = '';

        for (var n = 0; n < words.length; n++) {
            var testLine = line + words[n] + ' ';
            var metrics = this.measureText(testLine);
            var testWidth = metrics.width;
            if (testWidth > maxWidth && n > 0) {
                this.fillText(line, x, y);
                line = words[n] + ' ';
                y += lineHeight;
            }
            else {
                line = testLine;
            }
        }

        this.fillText(line, x, y);
        y += lineHeight;
    }
}

基本用法:

var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
ctx.fillStyle = "black";
ctx.font = "12px sans-serif";
ctx.textBaseline = "top";
ctx.wrapText("Hello\nWorld!",20,20,160,16);

这是我制作的一个演示:http://jsfiddle.net/7RdbL/


1
非常顺利地完成了工作。谢谢。 - couzzi

16
我刚刚扩展了CanvasRenderingContext2D并添加了两个功能:mlFillText和mlStrokeText。
您可以在GitHub上找到最新版本:
使用这些函数,您可以在一个框中填充/描边多行文本。您可以垂直和水平地对齐文本。(它考虑到了\n并且还可以调整文字的对齐方式).
原型如下:
function mlFillText(text,x,y,w,h,vAlign,hAlign,lineheight);
function mlStrokeText(text,x,y,w,h,vAlign,hAlign,lineheight);
< p >其中vAlign可以是:topcenterbutton

hAlign可以是:leftcenterrightjustify

您可以在这里测试该库:http://jsfiddle.net/4WRZj/1/

enter image description here

这是该库的代码:

// Library: mltext.js
// Desciption: Extends the CanvasRenderingContext2D that adds two functions: mlFillText and mlStrokeText.
//
// The prototypes are: 
//
// function mlFillText(text,x,y,w,h,vAlign,hAlign,lineheight);
// function mlStrokeText(text,x,y,w,h,vAlign,hAlign,lineheight);
// 
// Where vAlign can be: "top", "center" or "button"
// And hAlign can be: "left", "center", "right" or "justify"
// Author: Jordi Baylina. (baylina at uniclau.com)
// License: GPL
// Date: 2013-02-21

function mlFunction(text, x, y, w, h, hAlign, vAlign, lineheight, fn) {
    text = text.replace(/[\n]/g, " \n ");
    text = text.replace(/\r/g, "");
    var words = text.split(/[ ]+/);
    var sp = this.measureText(' ').width;
    var lines = [];
    var actualline = 0;
    var actualsize = 0;
    var wo;
    lines[actualline] = {};
    lines[actualline].Words = [];
    i = 0;
    while (i < words.length) {
        var word = words[i];
        if (word == "\n") {
            lines[actualline].EndParagraph = true;
            actualline++;
            actualsize = 0;
            lines[actualline] = {};
            lines[actualline].Words = [];
            i++;
        } else {
            wo = {};
            wo.l = this.measureText(word).width;
            if (actualsize === 0) {
                while (wo.l > w) {
                    word = word.slice(0, word.length - 1);
                    wo.l = this.measureText(word).width;
                }
                if (word === "") return; // I can't fill a single character
                wo.word = word;
                lines[actualline].Words.push(wo);
                actualsize = wo.l;
                if (word != words[i]) {
                    words[i] = words[i].slice(word.length, words[i].length);
                } else {
                    i++;
                }
            } else {
                if (actualsize + sp + wo.l > w) {
                    lines[actualline].EndParagraph = false;
                    actualline++;
                    actualsize = 0;
                    lines[actualline] = {};
                    lines[actualline].Words = [];
                } else {
                    wo.word = word;
                    lines[actualline].Words.push(wo);
                    actualsize += sp + wo.l;
                    i++;
                }
            }
        }
    }
    if (actualsize === 0) lines[actualline].pop();
    lines[actualline].EndParagraph = true;

    var totalH = lineheight * lines.length;
    while (totalH > h) {
        lines.pop();
        totalH = lineheight * lines.length;
    }

    var yy;
    if (vAlign == "bottom") {
        yy = y + h - totalH + lineheight;
    } else if (vAlign == "center") {
        yy = y + h / 2 - totalH / 2 + lineheight;
    } else {
        yy = y + lineheight;
    }

    var oldTextAlign = this.textAlign;
    this.textAlign = "left";

    for (var li in lines) {
        var totallen = 0;
        var xx, usp;
        for (wo in lines[li].Words) totallen += lines[li].Words[wo].l;
        if (hAlign == "center") {
            usp = sp;
            xx = x + w / 2 - (totallen + sp * (lines[li].Words.length - 1)) / 2;
        } else if ((hAlign == "justify") && (!lines[li].EndParagraph)) {
            xx = x;
            usp = (w - totallen) / (lines[li].Words.length - 1);
        } else if (hAlign == "right") {
            xx = x + w - (totallen + sp * (lines[li].Words.length - 1));
            usp = sp;
        } else { // left
            xx = x;
            usp = sp;
        }
        for (wo in lines[li].Words) {
            if (fn == "fillText") {
                this.fillText(lines[li].Words[wo].word, xx, yy);
            } else if (fn == "strokeText") {
                this.strokeText(lines[li].Words[wo].word, xx, yy);
            }
            xx += lines[li].Words[wo].l + usp;
        }
        yy += lineheight;
    }
    this.textAlign = oldTextAlign;
}

(function mlInit() {
    CanvasRenderingContext2D.prototype.mlFunction = mlFunction;

    CanvasRenderingContext2D.prototype.mlFillText = function (text, x, y, w, h, vAlign, hAlign, lineheight) {
        this.mlFunction(text, x, y, w, h, hAlign, vAlign, lineheight, "fillText");
    };

    CanvasRenderingContext2D.prototype.mlStrokeText = function (text, x, y, w, h, vAlign, hAlign, lineheight) {
        this.mlFunction(text, x, y, w, h, hAlign, vAlign, lineheight, "strokeText");
    };
})();

以下是使用示例:

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

var T = "This is a very long line line with a CR at the end.\n This is the second line.\nAnd this is the last line.";
var lh = 12;

ctx.lineWidth = 1;

ctx.mlFillText(T, 10, 10, 100, 100, 'top', 'left', lh);
ctx.strokeRect(10, 10, 100, 100);

ctx.mlFillText(T, 110, 10, 100, 100, 'top', 'center', lh);
ctx.strokeRect(110, 10, 100, 100);

ctx.mlFillText(T, 210, 10, 100, 100, 'top', 'right', lh);
ctx.strokeRect(210, 10, 100, 100);

ctx.mlFillText(T, 310, 10, 100, 100, 'top', 'justify', lh);
ctx.strokeRect(310, 10, 100, 100);

ctx.mlFillText(T, 10, 110, 100, 100, 'center', 'left', lh);
ctx.strokeRect(10, 110, 100, 100);

ctx.mlFillText(T, 110, 110, 100, 100, 'center', 'center', lh);
ctx.strokeRect(110, 110, 100, 100);

ctx.mlFillText(T, 210, 110, 100, 100, 'center', 'right', lh);
ctx.strokeRect(210, 110, 100, 100);

ctx.mlFillText(T, 310, 110, 100, 100, 'center', 'justify', lh);
ctx.strokeRect(310, 110, 100, 100);

ctx.mlFillText(T, 10, 210, 100, 100, 'bottom', 'left', lh);
ctx.strokeRect(10, 210, 100, 100);

ctx.mlFillText(T, 110, 210, 100, 100, 'bottom', 'center', lh);
ctx.strokeRect(110, 210, 100, 100);

ctx.mlFillText(T, 210, 210, 100, 100, 'bottom', 'right', lh);
ctx.strokeRect(210, 210, 100, 100);

ctx.mlFillText(T, 310, 210, 100, 100, 'bottom', 'justify', lh);
ctx.strokeRect(310, 210, 100, 100);

ctx.mlStrokeText("Yo can also use mlStrokeText!", 0 , 310 , 420, 30, 'center', 'center', lh);

如果我尝试更改字体,就会出现“未捕获的ReferenceError:Words未定义”。例如:ctx.font ='40px Arial';- 尝试将其放入您的fiddle中。 - psycho brm
顺便问一下,Words(区分大小写)变量从哪里来的?它在任何地方都没有定义。只有当您更改字体时,代码的这部分才会被执行。 - psycho brm
1
@psychobrm 你说得完全正确。这是一个错误(我已经修复了)。这段代码只有在你需要将一个单词分成两行时才会执行。谢谢! - jbaylina
我进行了一些必要的升级:渲染空格,渲染前导/尾随换行符,使用一次调用渲染描边和填充(不测量两次文本),我还不得不更改迭代方式,因为“for in”与扩展的“Array.prototype”不兼容。你能把它放在Github上,这样我们就可以对它进行迭代吗? - psycho brm
@psychobrm,我已经合并了你的更改。谢谢! - jbaylina
显示剩余2条评论

15

我在这种情况下创建了一个小型的库,链接在此处:Canvas-Txt

它可以在多行中呈现文本,并提供了不错的对齐模式。

如果你想使用它,你需要安装它或使用CDN。

安装

npm install canvas-txt --save

JavaScript

import canvasTxt from 'canvas-txt'

var c = document.getElementById('myCanvas')
var ctx = c.getContext('2d')

var txt = 'Lorem ipsum dolor sit amet'

canvasTxt.fontSize = 24

canvasTxt.drawText(ctx, txt, 100, 200, 200, 200)

这将在一个不可见的框中渲染文本,该框具有以下位置/尺寸:

{ x: 100, y: 200, height: 200, width: 200 }

示例演示


/* https://github.com/geongeorge/Canvas-Txt  */

const canvasTxt = window.canvasTxt.default;
const ctx = document.getElementById('myCanvas').getContext('2d');

const txt = "Lorem ipsum dolor sit amet";
const bounds = { width: 240, height: 80 };

let origin = { x: ctx.canvas.width / 2, y: ctx.canvas.height / 2, };
let offset = { x: origin.x - (bounds.width / 2), y: origin.y - (bounds.height / 2) };

canvasTxt.fontSize = 20;

ctx.fillStyle = '#C1A700';
ctx.fillRect(offset.x, offset.y, bounds.width, bounds.height);

ctx.fillStyle = '#FFFFFF';
canvasTxt.drawText(ctx, txt, offset.x, offset.y, bounds.width, bounds.height);
body {
  background: #111;
}

canvas {
  border: 1px solid #333;
  background: #222; /* Could alternatively be painted on the canvas */
}
<script src="https://unpkg.com/canvas-txt@2.0.6/build/index.js"></script>

<canvas id="myCanvas" width="300" height="160"></canvas>


我已经定义了一些变量来帮助“自文档化”示例。它还处理在画布内居中边界框。我还添加了一个矩形,这样你就可以看到它相对于中心的位置。干得好!+1 我注意到的一个小问题是换行的行不会抑制其前导空格。您可能需要修剪每行,例如 ctx.fillText(txtline.trim(), textanchor, txtY)。我只在您网站上的交互式演示中注意到了这一点。 - Mr. Polywhirl
@Mr.Polywhirl感谢您澄清答案。 我已经解决了修剪问题并发布了“2.0.9”版本。 通过更新软件包版本,演示站点已得到修复。存在多个空格的问题。我不知道选择一个有偏见的软件包还是忽略这个问题更好。来自多个地方的请求一直在增加。我继续添加修剪。 “Lorem ipsum dolor,sit <many spaces> amet”这就是我一开始没有这样做的原因。 您认为我应该考虑多个空格,并仅在只有一个空格时删除吗? - Geon George
编辑:看起来 StackOverflow 代码块也忽略多个空格 - Geon George

8

我用JavaScript开发了一个解决方案,虽然不是很美观但对我很有效:


function drawMultilineText(){

    // set context and formatting   
    var context = document.getElementById("canvas").getContext('2d');
    context.font = fontStyleStr;
    context.textAlign = "center";
    context.textBaseline = "top";
    context.fillStyle = "#000000";

    // prepare textarea value to be drawn as multiline text.
    var textval = document.getElementByID("textarea").value;
    var textvalArr = toMultiLine(textval);
    var linespacing = 25;
    var startX = 0;
    var startY = 0;

    // draw each line on canvas. 
    for(var i = 0; i < textvalArr.length; i++){
        context.fillText(textvalArr[i], x, y);
        y += linespacing;
    }
}

// Creates an array where the <br/> tag splits the values.
function toMultiLine(text){
   var textArr = new Array();
   text = text.replace(/\n\r?/g, '<br/>');
   textArr = text.split("<br/>");
   return textArr;
}

希望能够帮到您!

1
你好,假设我的文本是这样的 var text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 那么在画布中会发生什么? - Amol Navsupe
它将超出画布,因为@Ramirez没有将maxWidth参数放入fillText中 :) - KaHa6uc

7
如果你只需要两行文本,你可以将它们分成两个不同的fillText调用,并给每个调用指定不同的基线。
ctx.textBaseline="bottom";
ctx.fillText("First line", x-position, y-position);
ctx.textBaseline="top";
ctx.fillText("Second line", x-position, y-position);

这是一个很好的解决方案,谢谢! - undefined

6
这是 Colin 的 wrapText() 的一个版本,还支持使用 context.textBaseline = 'middle' 实现垂直居中的文本:
var wrapText = function (context, text, x, y, maxWidth, lineHeight) {
    var paragraphs = text.split("\n");
    var textLines = [];

    // Loop through paragraphs
    for (var p = 0; p < paragraphs.length; p++) {
        var line = "";
        var words = paragraphs[p].split(" ");
        // Loop through words
        for (var w = 0; w < words.length; w++) {
            var testLine = line + words[w] + " ";
            var metrics = context.measureText(testLine);
            var testWidth = metrics.width;
            // Make a line break if line is too long
            if (testWidth > maxWidth) {
                textLines.push(line.trim());
                line = words[w] + " ";
            }
            else {
                line = testLine;
            }
        }
        textLines.push(line.trim());
    }

    // Move text up if centered vertically
    if (context.textBaseline === 'middle')
        y = y - ((textLines.length-1) * lineHeight) / 2;

    // Render text on canvas
    for (var tl = 0; tl < textLines.length; tl++) {
        context.fillText(textLines[tl], x, y);
        y += lineHeight;
    }
};

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