【问题标题】:How can I implement word wrap and carriage returns in canvas fillText?如何在画布填充文本中实现自动换行和回车?
【发布时间】:2019-12-15 15:20:17
【问题描述】:

我正在尝试显示已存储在 MariaDB 中的文本区域信息。我存储文本信息没有问题。我遇到的问题是将格式从文本区域转换到我希望在其中显示的画布。

目标是让用户在文本区域中填写注释,然后将这些注释显示在单独的画布报告中。

现在,我可以使用我存储在 wordWrap.js 文件中的代码成功地使自动换行:

function wrapText (c, text, x, y, maxWidth, lineHeight) {

    var words = text.split(' ');
    var line = '';
    var lineCount = 0;
    var test;
    var metrics;

    for (var i = 0; i < words.length; i++) {
        test = words[i];
// add test for length of text
        metrics = c.measureText(test);
        while (metrics.width > maxWidth) {
            test = test.substring(0, test.length - 1);
            metrics = c.measureText(test);
        }

        if (words[i] != test) {
            words.splice(i + 1, 0,  words[i].substr(test.length))
            words[i] = test;
        }  

        test = line + words[i] + ' ';  
        metrics = c.measureText(test);

        if (metrics.width > maxWidth && i > 0) {
            c.fillText(line, x, y);
            line = words[i] + ' ';
            y += lineHeight;
            lineCount++;
        }
        else {
            line = test;
        }
    }

    c.fillText(line, x, y);
}

我可以添加文本,根据 fillText 区域的大小和单词的长度来换行。我需要补充的是支持回车的能力。用户使用 \n 支持回车不会有问题,所以我只需要让它工作。

我见过其他支持回车的代码。我在下面玩过的例子。

ctx.font = '12px Courier';
var text = <?php echo json_encode($row['notes']);?>;
var x = 30;
var y = 30;
var lineheight = 15;
var lines = text.split('\n');

for (var i = 0; i<lines.length; i++) {
    ctx.fillText(lines[i], x, y + (i*lineheight) );
}

这些方法具有相似的属性,我相信它们可以对齐,但我无法弄清楚如何实现驱动文本拆分的两个脚本的关键部分......

text.split('\n')

text.split(' ')

在我看来,这就像 word wrap 使用的 for 和 while 循环的组合,但我需要一些帮助来确定在哪里。

【问题讨论】:

    标签: javascript canvas


    【解决方案1】:

    最擅长在浏览器中呈现文本的无疑是 HTML 和 CSS。
    Canvas 2D API 仍然远远低于,所以当你需要在画布上渲染复杂的文本时,最好是利用 HTML 和 CSS 的力量来为你的画布采取所有需要的措施。

    我已经制作了处理类似问题的a few answers,所以这个只是根据您的需要对这些以前的代码进行了改编:

    // see https://stackoverflow.com/questions/55604798
    // added x output
    function getLineBreaks(node, contTop = 0, contLeft = 0) {
      if(!node) return [];
      const range = document.createRange();
      const lines = [];
      range.setStart(node, 0);
      let prevBottom = range.getBoundingClientRect().bottom;
      let str = node.textContent;
      let current = 1;
      let lastFound = 0;
      let bottom = 0;
      let left = range.getBoundingClientRect().left;
      while(current <= str.length) {
        range.setStart(node, current);
        if(current < str.length -1) {
          range.setEnd(node, current + 1);
        }
        const range_rect = range.getBoundingClientRect();
        bottom = range_rect.bottom;
        if(bottom > prevBottom) {
          lines.push({
            x: left - contLeft,
            y: prevBottom - contTop,
            text: str.substr(lastFound , current - lastFound)
          });
          prevBottom = bottom;
          lastFound = current;
          left = range_rect.left;
        }
        current++;
      }
      // push the last line
      lines.push({
        x: left - contLeft,
        y: bottom - contTop,
        text: str.substr(lastFound)
      });
    
      return lines;
    }
    
    function getRenderedTextLinesFromElement(elem) {
      elem.normalize();
      // first grab all TextNodes
      const nodes = [];
      const walker = document.createTreeWalker(
        elem, 
        NodeFilter.SHOW_TEXT
      );
      while(walker.nextNode()) {
        nodes.push(walker.currentNode);
      }
      // now get all their positions, with line breaks
      const elem_rect = elem.getBoundingClientRect();
      const top = elem_rect.top;
      const left = elem_rect.left;
      return nodes.reduce((lines, node) => 
        lines.concat(getLineBreaks(node, top, left)),
      []);
    }
    
    const ctx = canvas.getContext('2d');
    ctx.textBaseline = 'bottom';
    txt_area.oninput = e => {    
      ctx.setTransform(1,0,0,1,0,0);
      ctx.clearRect(0,0,canvas.width,canvas.height);
        
      const lines = getRenderedTextLinesFromElement(txt_area);
      // apply the div's style to our canvas
      const node_style = getComputedStyle(txt_area);
      const nodeFont = (prop) => node_style.getPropertyValue('font-' + prop);
      ctx.font = nodeFont('weight') + ' ' + nodeFont('size') + ' ' + nodeFont('family');
      ctx.textAlign = node_style.getPropertyValue('text-align');
      ctx.textBaseline = "bottom";
      // draw each line of text
      lines.forEach(({text, x, y}) => ctx.fillText(text, x, y));
    };
    txt_area.oninput();
    #txt_area, canvas {
      width: 300px;
      height: 150px;
      resize: none;
      border: 1px solid;
      max-width: 300px;
      max-height: 150px;
      overflow: hidden;
    }
    canvas {
      border-color: green;
    }
    <div contenteditable id="txt_area">This is an example text
    <br>that should get rendered as is in the nearby canvas
    </div>
    <canvas id="canvas"></canvas>

    在您的情况下,您可能希望隐藏此 div,然后将其删除:

    const text = "This is an example text with a few new lines\n" +
      "and some normal text-wrap.\n" +
      "\n" +
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
      "\n" +
      "At tempor commodo ullamcorper a lacus.";
    renderText(text);
    
    function getLineBreaks(node, contTop = 0, contLeft = 0) {
      if(!node) return [];
      const range = document.createRange();
      const lines = [];
      range.setStart(node, 0);
      let prevBottom = range.getBoundingClientRect().bottom;
      let str = node.textContent;
      let current = 1;
      let lastFound = 0;
      let bottom = 0;
      let left = range.getBoundingClientRect().left;
      while(current <= str.length) {
        range.setStart(node, current);
        if(current < str.length -1) {
          range.setEnd(node, current + 1);
        }
        const range_rect = range.getBoundingClientRect();
        bottom = range_rect.bottom;
        if(bottom > prevBottom) {
          lines.push({
            x: left - contLeft,
            y: prevBottom - contTop,
            text: str.substr(lastFound , current - lastFound)
          });
          prevBottom = bottom;
          lastFound = current;
          left = range_rect.left;
        }
        current++;
      }
      // push the last line
      lines.push({
        x: left - contLeft,
        y: bottom - contTop,
        text: str.substr(lastFound)
      });
    
      return lines;
    }
    
    function getRenderedTextLinesFromElement(elem) {
      elem.normalize();
      // first grab all TextNodes
      const nodes = [];
      const walker = document.createTreeWalker(
        elem, 
        NodeFilter.SHOW_TEXT
      );
      while(walker.nextNode()) {
        nodes.push(walker.currentNode);
      }
      // now get all their positions, with line breaks
      const elem_rect = elem.getBoundingClientRect();
      const top = elem_rect.top;
      const left = elem_rect.left;
      return nodes.reduce((lines, node) => 
        lines.concat(getLineBreaks(node, top, left)),
      []);
    }
    
    function renderText(text) {
      // make the div we'll use to take the measures
      const elem = document.createElement('div');
      elem.classList.add('canvas-text-renderer');
      // if you wish to have new lines marked by \n in your input
      elem.innerHTML = text.replace(/\n/g,'<br>');
      document.body.append(elem);
      
      const ctx = canvas.getContext('2d');
      ctx.textBaseline = 'bottom';
      const lines = getRenderedTextLinesFromElement(elem);
      // apply the div's style to our canvas
      const node_style = getComputedStyle(elem);
      const nodeFont = (prop) => node_style.getPropertyValue('font-' + prop);
      ctx.font = nodeFont('weight') + ' ' + nodeFont('size') + ' ' + nodeFont('family');
      ctx.textAlign = node_style.getPropertyValue('text-align');
      ctx.textBaseline = "bottom";
      // draw each line of text
      lines.forEach(({text, x, y}) => ctx.fillText(text, x, y));
    
      // clean up
      elem.remove();
    }
    .canvas-text-renderer, canvas {
      width: 300px;
      height: 150px;
      resize: none;
      border: 1px solid;
      max-width: 300px;
      max-height: 150px;
      overflow: hidden;
    }
    canvas {
      border-color: green;
    }
    .canvas-text-renderer {
      position: absolute;
      z-index: -1;
      opacity: 0;
    }
    &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

    【讨论】:

    • 谢谢。我会玩一下,让你知道它是怎么回事。
    • 好的,我知道你在这之后的感受(我想)。如果我将信息存储在变量中... var text = ;。我在弄清楚如何在您的设计中实现这一点时遇到了一些麻烦。
    • 看第二个sn-p,顶部是renderText(text),而不是我的renderText(`This is an example text ...
    • @airider74 为canvas variable。您可以在自己的代码中使变量指向您想要的任何内容。重要的是函数getLineBreaksgetRenderedTextLinesFromElement。画布的大小不必与文本渲染器的大小相同。 jsfiddle.net/pv7x19ym
    • 你所说的“规模”是什么意思?如果您愿意,可以使用 this previous answer 在当前代码中插入最佳字体大小检测器部分。
    猜你喜欢
    • 2017-01-04
    • 2021-05-19
    • 2011-03-05
    • 1970-01-01
    • 1970-01-01
    • 2018-08-10
    • 2014-06-21
    • 1970-01-01
    • 2014-04-13
    相关资源
    最近更新 更多