<canvas>元素中的文本换行

64

我想使用<canvas>元素在图像上添加文字。首先绘制图像,然后在图像上绘制文字。到目前为止都很好。

但是当文本过长时,它会被画布的开头和结尾截断。我不打算调整画布大小,但我想知道如何将长文本换行为多行以便全部显示。有人能指导我吗?


3
值得一提的是,Fabric.js 在画布上拥有广泛的文本支持(多行、文本样式、装饰、描边等)。 - kangax
11个回答

68

更新了@mizar的答案,修复了一个严重错误和一个次要错误。

function getLines(ctx, text, maxWidth) {
    var words = text.split(" ");
    var lines = [];
    var currentLine = words[0];

    for (var i = 1; i < words.length; i++) {
        var word = words[i];
        var width = ctx.measureText(currentLine + " " + word).width;
        if (width < maxWidth) {
            currentLine += " " + word;
        } else {
            lines.push(currentLine);
            currentLine = word;
        }
    }
    lines.push(currentLine);
    return lines;
}

我们使用这段代码已经有一段时间了,但今天我们尝试解决为什么有些文本没有绘制出来的问题时,我们发现了一个bug!

原来,如果你给getLines()函数传递一个单词(没有任何空格),它将返回一个空数组,而不是一个包含单行的数组。

当我们调查这个问题时,我们发现了另一个(更微妙的)bug,在测量行长度时,由于原始代码没有考虑到空格,所以行可能会变得略长于它们应该的长度。

我们更新后的版本可以处理我们测试的所有内容,代码如上。请让我知道您是否发现任何bug!


我不知道你是否有意为之,但它无法处理换行符。 - Jason
3
@Jason: 很容易解决,只需要将其包装在你自己的函数中,例如:function getLinesForParagraphs(ctx, text, maxWidth) { return text.split("\n").map(para => getLines(ctx, para, maxWidth)).reduce([], (a, b) => a.concat(b)) } - crazy2be
这种方法对于使用块字符的语言,例如中文,是不起作用的。 - Junle Li
2
@JunleLi:正确处理所有语言的换行似乎很复杂[1],但是对于使用块状字符的语言,您可以通过执行“var words = text.split('')”而不是在空格上拆分来更接近(请注意,这将在拉丁字母语言中在单词中间拆分)。更好的解决方案是解析单词并根据语言进行拆分(因此块状字符和拉丁语单词可以混合并被正确打断)。真正正确的解决方案需要使用字典。[1] http://w3c.github.io/i18n-drafts/articles/typography/linebreak.zh - crazy2be

23

一种可能的方法(尚未完全测试,但目前为止表现完美)

    /**
     * Divide an entire phrase in an array of phrases, all with the max pixel length given.
     * The words are initially separated by the space char.
     * @param phrase
     * @param length
     * @return
     */
function getLines(ctx,phrase,maxPxLength,textStyle) {
    var wa=phrase.split(" "),
        phraseArray=[],
        lastPhrase=wa[0],
        measure=0,
        splitChar=" ";
    if (wa.length <= 1) {
        return wa
    }
    ctx.font = textStyle;
    for (var i=1;i<wa.length;i++) {
        var w=wa[i];
        measure=ctx.measureText(lastPhrase+splitChar+w).width;
        if (measure<maxPxLength) {
            lastPhrase+=(splitChar+w);
        } else {
            phraseArray.push(lastPhrase);
            lastPhrase=w;
        }
        if (i===wa.length-1) {
            phraseArray.push(lastPhrase);
            break;
        }
    }
    return phraseArray;
}

1
如果短语只包含一个单词,则返回一个空数组。 - Ingvi Gautsson
只是一个建议 - 经过一些性能测试后,我发现逐个测量每个字符并缓存每个字符的结果,然后将单个字符宽度相加,即使看起来很昂贵,实际上比画布的measureText方法更快。 - notrota

8
这是我的改编版本...我阅读了@mizar的答案并进行了一些修改...再加上一点帮助,我就能得到这个。

已删除代码,请参见fiddle。

以下是使用示例。http://jsfiddle.net/9PvMU/1/ - 这个脚本也可以在这里看到,并最终成为我使用的内容...此函数假定ctx在父作用域中可用...如果没有,您总是可以传递它。

编辑

这篇文章很旧,其中包含我仍在调试的函数版本。这个版本迄今为止似乎满足了我的需求,我希望它能帮助别人。


编辑

有人指出这段代码存在一个小错误。花了我一些时间来解决它,现在更新了。我自己测试过,现在似乎按预期工作。

function fragmentText(text, maxWidth) {
    var words = text.split(' '),
        lines = [],
        line = "";
    if (ctx.measureText(text).width < maxWidth) {
        return [text];
    }
    while (words.length > 0) {
        var split = false;
        while (ctx.measureText(words[0]).width >= maxWidth) {
            var tmp = words[0];
            words[0] = tmp.slice(0, -1);
            if (!split) {
                split = true;
                words.splice(1, 0, tmp.slice(-1));
            } else {
                words[1] = tmp.slice(-1) + words[1];
            }
        }
        if (ctx.measureText(line + words[0]).width < maxWidth) {
            line += words.shift() + " ";
        } else {
            lines.push(line);
            line = "";
        }
        if (words.length === 0) {
            lines.push(line);
        }
    }
    return lines;
}

1
它无法识别“ENTER”值。如果用户输入的单词/字符超出了允许的最大宽度,它不会为下一个单词产生空格。有什么建议吗? - Ivo San
它不会识别回车值,因为它没有被构建成那样。但是如果你监听它,你可以强制换行。而且你说得对,我以前从未注意到这个错误。我会调查一下并更新我的答案。 - rlemon
@ivory-santos,现在已经修复了maxWidth修剪空格的问题。 - rlemon

6

context.measureText(text).width 是你要查找的内容...


该代码用于测量给定文本在当前字体和大小下的宽度。

4
尝试使用此脚本在画布上换行文本。
 <script>
  function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
    var words = text.split(' ');
    var line = '';

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

  var canvas = document.getElementById('Canvas01');
  var ctx = canvas.getContext('2d');
  var maxWidth = 400;
  var lineHeight = 24;
  var x = (canvas.width - maxWidth) / 2;
  var y = 70;
  var text = 'HTML is the language for describing the structure of Web pages. HTML stands for HyperText Markup Language. Web pages consist of markup tags and plain text. HTML is written in the form of HTML elements consisting of tags enclosed in angle brackets (like <html>). HTML tags most commonly come in pairs like <h1> and </h1>, although some tags represent empty elements and so are unpaired, for example <img>..';

  ctx.font = '15pt Calibri';
  ctx.fillStyle = '#555555';

  wrapText(ctx, text, x, y, maxWidth, lineHeight);
  </script>
</body>

在这里查看演示:http://codetutorial.com/examples-canvas/canvas-examples-text-wrap

3

我正在发布我自己的版本,使用这里,因为这里的答案对我来说不够充分。在我的情况下,需要测量第一个单词,以便能够拒绝过长的单词从小画布区域中出现。而且我需要支持“断+空格”、“空格+断”或双断/段落断组合。

wrapLines: function(ctx, text, maxWidth) {
    var lines = [],
        words = text.replace(/\n\n/g,' ` ').replace(/(\n\s|\s\n)/g,'\r')
        .replace(/\s\s/g,' ').replace('`',' ').replace(/(\r|\n)/g,' '+' ').split(' '),
        space = ctx.measureText(' ').width,
        width = 0,
        line = '',
        word = '',
        len = words.length,
        w = 0,
        i;
    for (i = 0; i < len; i++) {
        word = words[i];
        w = word ? ctx.measureText(word).width : 0;
        if (w) {
            width = width + space + w;
        }
        if (w > maxWidth) {
            return [];
        } else if (w && width < maxWidth) {
            line += (i ? ' ' : '') + word;
        } else {
            !i || lines.push(line !== '' ? line.trim() : '');
            line = word;
            width = w;
        }
    }
    if (len !== i || line !== '') {
        lines.push(line);
    }
    return lines;
}

它支持任何换行符或段落分隔符,删除双空格以及前导或尾随段落分隔符。如果文本不合适,它将返回一个空数组。否则将返回一组可供绘制的行。


3
从这里的脚本开始:http://www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/ 我已经扩展了支持段落。使用\n表示新行。
function wrapText(context, text, x, y, line_width, line_height)
{
    var line = '';
    var paragraphs = text.split('\n');
    for (var i = 0; i < paragraphs.length; i++)
    {
        var words = paragraphs[i].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 > line_width && n > 0)
            {
                context.fillText(line, x, y);
                line = words[n] + ' ';
                y += line_height;
            }
            else
            {
                line = testLine;
            }
        }
        context.fillText(line, x, y);
        y += line_height;
        line = '';
    }
}

文本可以像这样格式化:

var text = 
[
    "Paragraph 1.",
    "\n\n",
    "Paragraph 2."
].join("");

使用:

wrapText(context, text, x, y, line_width, line_height);

代替
context.fillText(text, x, y);

1

看一下https://developer.mozilla.org/en/Drawing_text_using_a_canvas#measureText%28%29

如果你能看到选中的文本,并且发现它比你的画布宽,你可以删除一些单词,直到文本足够短。使用被删除的单词,你可以从第二行开始做同样的事情。

当然,这样做效率不高,所以你可以通过删除多个单词来改进它,如果你发现文本比画布宽得多。

我没有进行研究,但也许有JavaScript库可以为你完成这项工作。


1
我使用了这里的代码进行了修改 http://miteshmaheta.blogspot.sg/2012/07/html5-wrap-text-in-canvas.html

var canvas = document.getElementById('cvs'),
  ctx = canvas.getContext('2d'),
  input = document.getElementById('input'),
  width = +(canvas.width = 400),
  height = +(canvas.height = 250),
  fontFamily = "Arial",
  fontSize = "24px",
  fontColour = "blue";

function wrapText(context, text, x, y, maxWidth, lineHeight) {
  var words = text.split(" ");
  var line = "";
  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);
}

function draw() {
  ctx.save();
  ctx.clearRect(0, 0, width, height);
  //var lines = makeLines(input.value, width - parseInt(fontSize,0));
  //lines.forEach(function(line, i) {
  //    ctx.fillText(line, width / 2, height - ((i + 1) * parseInt(fontSize,0)));
  //});
  wrapText(ctx, input.value, x, y, (canvas.width - 8), 25)
  ctx.restore();
}

input.onkeyup = function(e) { // keyup because we need to know what the entered text is.
  draw();
};

var text = "HTML5 Wrap Text functionsdfasdfasdfsadfasdfasdfasdfasdfasdf in Javascript written here is helped me a lot.";
var x = 20;
var y = 20;
wrapText(ctx, text, x, y, (canvas.width - 8), 25)
body {
  background-color: #efefef;
}

canvas {
  outline: 1px solid #000;
  background-color: white;
}
<canvas id="cvs"></canvas><br />
<input type="text" id="input" />


0

这是@JBelfort答案的TypeScript版本。 (顺便说一下,感谢他提供了这个精彩的代码)

正如他在回答中提到的那样,此代码可以模拟HTML元素,例如文本区域,并且还可以模拟CSS属性。

word-break: break-all

我添加了画布位置参数(x、y和行高)

function wrapText(
  ctx: CanvasRenderingContext2D,
  text: string,
  maxWidth: number,
  x: number,
  y: number,
  lineHeight: number
) {
  const xOffset = x;
  let yOffset = y;
  const lines = text.split('\n');
  const fittingLines: [string, number, number][] = [];
  for (let i = 0; i < lines.length; i++) {
    if (ctx.measureText(lines[i]).width <= maxWidth) {
      fittingLines.push([lines[i], xOffset, yOffset]);
      yOffset += lineHeight;
    } else {
      let tmp = lines[i];
      while (ctx.measureText(tmp).width > maxWidth) {
        tmp = tmp.slice(0, tmp.length - 1);
      }
      if (tmp.length >= 1) {
        const regex = new RegExp(`.{1,${tmp.length}}`, 'g');
        const thisLineSplitted = lines[i].match(regex);
        for (let j = 0; j < thisLineSplitted!.length; j++) {
          fittingLines.push([thisLineSplitted![j], xOffset, yOffset]);
          yOffset += lineHeight;
        }
      }
    }
  }
  return fittingLines;
}

你可以像这样使用它

const wrappedText = wrapText(ctx, dialog, 200, 100, 200, 50);
wrappedText.forEach(function (text) {
  ctx.fillText(...text);
});
}

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