0

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 the formatting from the text area to the canvas I want it displayed in.

The goal is to have a user fill in the notes in a textarea and then have those displayed in the separate canvas report.

Right now, I can get the wordwrap working successfully using this code I have stored in a wordWrap.js file:

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);
}

I can add the text, which word wraps based on the size of the fillText area and the length of the words. What I need to add to this is the ability to support carriage returns. The users won't have a problem using \n to support carriage returns so I just need to get it to work.

I've seen other code out there that supports carriage returns. Example I've played with below.

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) );
}

These methods have similar attributes and I believe they can be aligned but I'm having trouble figuring out how to implement the key piece of both scripts which is what drives text split ...

text.split('\n')

text.split(' ')

This looks to me like a combination of for and while loops like the word wrap uses, but I need some help figuring out where.

airider74
  • 390
  • 3
  • 14

1 Answers1

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>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thanks. I'll play with this a bit and let you know how it goes. – airider74 Aug 09 '19 at 02:25
  • Okay, I get how you're getting after this (I think). If I have the information stored in a variable though ... var text = ;. I'm having a little trouble figuring out how to implement this with your design. – airider74 Aug 09 '19 at 03:02
  • 1
    Look at the second snippet, it would be `renderText(text)` at the top instead of my `renderText(\`This is an example text ...` – Kaiido Aug 09 '19 at 03:05
  • I've got the second snippet loaded exactly. The green box is drawn, but no text shows up. The first snippet works. I see where you're going with this but I'm missing something – airider74 Aug 09 '19 at 03:23
  • Found a bug. The snippet is working now. Going to put the function code in a .js file and play with it a bit more in the current canvas I've been working on. – airider74 Aug 09 '19 at 03:28
  • Running into a problem where when I change the size of he canvas, the text inside gets stretched out, and none of the other formatting works. – airider74 Aug 11 '19 at 18:34
  • also, if I change the id of the canvas to "layer 4" in the html tag, and update the canvas tags in the CSS and the bottom javascript, it gives me a blank page. Changing everything back but the html tag returns it. Basically, I'm stuck with a 300 x 150 box. I need to be able to change that, and it doesn't look like I can in the CSS without screwing up something else. – airider74 Aug 11 '19 at 18:54
  • Gone through the code several times now and the big thing that has me scratching my head is where we declare the canvas name. We give it an ID in the html tag, and assume it in the code, but don't declare it as a variable first. ... var c1 = document.getElementById("canvas"); The reason I need this is because this box will be a layer (at a different Z height) in an existing planner – airider74 Aug 11 '19 at 19:13
  • 1
    @airider74 for [`canvas` variable](https://stackoverflow.com/questions/3434278/do-dom-tree-elements-with-ids-become-global-variables). You can make your variable point to anything you want in your own code. The important things are the functions `getLineBreaks` and `getRenderedTextLinesFromElement`. The size of your canvas doesn't have to be the same as the one of the text-renderer. https://jsfiddle.net/pv7x19ym/ – Kaiido Aug 12 '19 at 01:58
  • Thanks. I've played with it quite a bit but kept running into an issue where the text didn't scale as I expected. I'll play with the jsFiddle you provided and see what I need to change in my implementation. – airider74 Aug 16 '19 at 01:34
  • 1
    What do you mean by "scale"? If you wish, there is [this previous answer](https://stackoverflow.com/questions/54203800/javascript-jquery-making-text-to-fit-perfectly-on-div-given-dimensions-with-line/54472418#54472418) from which you could plug the best font-size detector part in the current code. – Kaiido Aug 16 '19 at 01:37
  • Figured it out. Also, I ended up using two spaces instead of \n since I do a check of what gets submitted to my database and use php stripslashes which obviously would cause problems. https://jsfiddle.net/airider/o64dvz0b/3/ – airider74 Aug 16 '19 at 19:35