Skip to content Skip to sidebar Skip to footer

How Can I Implement Word Wrap And Carriage Returns In Canvas FillText?

I'm trying to display textarea information that has been stored in a MariaDB. I don't have a problem storing the text information. What I'm having a problem with is transition th

Solution 1:

The best at rendering text in a browser are definitively HTML and CSS.
Canvas 2D API is still far below, so when you need to render complex text on a canvas, the best is to use the power of HTML and CSS to take all the measures needed for your canvas.

I already made a few answers that deal with similar issues, so this one is just an adaptation of these previous codes to your needs:

// 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>

In your case, you will probably want to make this div hidden, and to remove it afterward:

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;
}
<canvas id="canvas"></canvas>

Post a Comment for "How Can I Implement Word Wrap And Carriage Returns In Canvas FillText?"