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?"