Although adding text to HTML canvas is very common, there is no built in line break functionality. That means if our text is too long, the text will run off the end. Take the example below, where the text is supposed to be "Hello, this text line is very long. It will overflow". Since it's too long to fit in the canvas, it just overflows with no line breaks:
Code for this example:
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let grd = ctx.createLinearGradient(0, 853, 1352, 0);
grd.addColorStop(0, '#00a0ff');
grd.addColorStop(1, '#12cba6');
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 1342, 853);
// More text
ctx.font = '700 95px Helvetica';
ctx.fillStyle = 'white';
ctx.fillText("Hello, this text line is very long. It will overflow.", 85, 200);
Our text above starts at (85, 200)
px, and continues with no line breaks. As odd as it is, we need to calculate ourselves where the line breaks should be in HTML Canvas. To do that, we can use a custom function, and use the data from that function to put line breaks in place.
How to wrap text in HTML Canvas
When we build our custom function to wrap text in HTML, we need to consider when a line break occurs. A line break typically occurs when the next word is going to overflow the width of the parent element - in this case, our canvas. When we build our function to wrap the text, we need to check if the next word in the sentence will cause an overflow.
As such, we'll build a function which accepts a few different variables:
ctx
- the context for the canvas we want to wrap text on. text - the text we want to wrap.x
- the X starting point of the text on the canvas.y
- the Y starting point of the text on the canvas.maxWidth
- the width at which we want line breaks to begin - i.e. the maximum width of the canvas.lineHeight
- the height of each line, so we can space them below each other. Let's take a look at the function I've built for this:
// @description: wrapText wraps HTML canvas text onto a canvas of fixed width
// @param ctx - the context for the canvas we want to wrap text on
// @param text - the text we want to wrap.
// @param x - the X starting point of the text on the canvas.
// @param y - the Y starting point of the text on the canvas.
// @param maxWidth - the width at which we want line breaks to begin - i.e. the maximum width of the canvas.
// @param lineHeight - the height of each line, so we can space them below each other.
// @returns an array of [ lineText, x, y ] for all lines
const wrapText = function(ctx, text, x, y, maxWidth, lineHeight) {
// First, start by splitting all of our text into words, but splitting it into an array split by spaces
let words = text.split(' ');
let line = ''; // This will store the text of the current line
let testLine = ''; // This will store the text when we add a word, to test if it's too long
let lineArray = []; // This is an array of lines, which the function will return
// Lets iterate over each word
for(var n = 0; n < words.length; n++) {
// Create a test line, and measure it..
testLine += `${words[n]} `;
let metrics = ctx.measureText(testLine);
let testWidth = metrics.width;
// If the width of this test line is more than the max width
if (testWidth > maxWidth && n > 0) {
// Then the line is finished, push the current line into "lineArray"
lineArray.push([line, x, y]);
// Increase the line height, so a new line is started
y += lineHeight;
// Update line and test line to use this word as the first word on the next line
line = `${words[n]} `;
testLine = `${words[n]} `;
}
else {
// If the test line is still less than the max width, then add the word to the current line
line += `${words[n]} `;
}
// If we never reach the full max width, then there is only one line.. so push it into the lineArray so we return something
if(n === words.length - 1) {
lineArray.push([line, x, y]);
}
}
// Return the line array
return lineArray;
}
This function works on a few premises:
- We test a new line by using
measureText()
. If it's too long for the container, then we start a new line. Otherwise we stay on the current one. - We use a predefined line height, so that we can have consistent line heights.
- We return an array of
[ lineText, x, y ]
for each line - wherelineText
is the text for that line, andx
/y
is the starting position of that particular line. - If there is only one line, we just return that line in
lineArray
. - To apply it to our canvas, we have to iterate over each element from the array. Then we use
ctx.fillText
to draw each line at the coordinates calculated by ourwrapText()
function - which will ultimately create line breaks for us:
// Set up our font and fill style
ctx.font = '700 95px Helvetica';
ctx.fillStyle = 'white';
// we pass in ctx, text, x, y, maxWidth, lineHeight to wrapText()
// I am using a slightly smaller maxWidth than the canvas width, since I wanted to add padding to either side of the canvas.
let wrappedText = wrapText(ctx, "This line is way too long. It's going to overflow - but it should line break.", 85, 200, 1050, 140);
// wrappedTe
wrappedText.forEach(function(item) {
// item[0] is the text
// item[1] is the x coordinate to fill the text at
// item[2] is the y coordinate to fill the text at
ctx.fillText(item[0], item[1], item[2]);
})
And we end up with wrapped text:
Now we can wrap text in canvas. The final code for the example above is shown below:
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
canvas.width = 1200;
canvas.height = 800;
// @description: wrapText wraps HTML canvas text onto a canvas of fixed width
// @param ctx - the context for the canvas we want to wrap text on
// @param text - the text we want to wrap.
// @param x - the X starting point of the text on the canvas.
// @param y - the Y starting point of the text on the canvas.
// @param maxWidth - the width at which we want line breaks to begin - i.e. the maximum width of the canvas.
// @param lineHeight - the height of each line, so we can space them below each other.
// @returns an array of [ lineText, x, y ] for all lines
const wrapText = function(ctx, text, x, y, maxWidth, lineHeight) {
// First, start by splitting all of our text into words, but splitting it into an array split by spaces
let words = text.split(' ');
let line = ''; // This will store the text of the current line
let testLine = ''; // This will store the text when we add a word, to test if it's too long
let lineArray = []; // This is an array of lines, which the function will return
// Lets iterate over each word
for(var n = 0; n < words.length; n++) {
// Create a test line, and measure it..
testLine += `${words[n]} `;
let metrics = ctx.measureText(testLine);
let testWidth = metrics.width;
// If the width of this test line is more than the max width
if (testWidth > maxWidth && n > 0) {
// Then the line is finished, push the current line into "lineArray"
lineArray.push([line, x, y]);
// Increase the line height, so a new line is started
y += lineHeight;
// Update line and test line to use this word as the first word on the next line
line = `${words[n]} `;
testLine = `${words[n]} `;
}
else {
// If the test line is still less than the max width, then add the word to the current line
line += `${words[n]} `;
}
// If we never reach the full max width, then there is only one line.. so push it into the lineArray so we return something
if(n === words.length - 1) {
lineArray.push([line, x, y]);
}
}
// Return the line array
return lineArray;
}
// Add gradient
let grd = ctx.createLinearGradient(0, 1200, 800, 0);
grd.addColorStop(0, '#00a0ff');
grd.addColorStop(1, '#12cba6');
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 1200, 800);
// More text
ctx.font = '700 95px Helvetica';
ctx.fillStyle = 'white';
let wrappedText = wrapText(ctx, "This line is way too long. It's going to overflow - but it should line break.", 85, 200, 1050, 140);
wrappedText.forEach(function(item) {
ctx.fillText(item[0], item[1], item[2]);
})
Conclusion
Although we have to write a custom function to wrap text in HTML canvas, it's not too hard when you understand how it works. I hope you've enjoyed this guide on how to wrap text with HTML canvas. For more on HTML canvas, check out my full length guide here.