Drawing wrapped text with the HTML canvas element

The HTML canvas element can draw text but it cannot wrap text. Text wrapping is complicated so in this tutorial, instead of breaking text lines with JavaScript, we’ll lean on native browser tech to draw wrapped text lines.

Let’s try to draw the paragraph below to a canvas element.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Using Canvas fillText

We’ll first give fillText() a go. As the native canvas text drawing method it deserves a fair chance.

// Get the 2d drawing context of our canvas
const ctx = document.getElementById("canvas").getContext("2d");

// Set the font-size and font-family to mimic our paragraph
ctx.font = "20px serif";

// Draw the text
ctx.fillText("Lorem Ipsum is … typesetting industry.", 0, 20);

As we can see below, the text won’t wrap.

The HTML canvas API exposes some other methods to draw basic shapes and paths but those aren’t of use to us when we want to draw text.

We’re left with drawImage(). The method used to draw ImageData to a canvas element.

If we can only use drawImage it means we have to get our text in a format that can be passed to the drawImage method.

The method accepts:

Let’s keep those in mind and go back to drawing wrapped text.

Text wrapping in a graphic format

What other ways can we use to draw wrapped text? We’ve already drawn wrapped text to the screen with our reference paragraph above, but unfortunatley we cannot pass HTML to the drawImage method.

We can however pass an image element, so maybe we can use SVG to draw wrapped text. Let’s try it now.

<svg width="280" height="100" xmlns="http://www.w3.org/2000/svg">
  <text x="0" y="20">Lorem Ipsum is … typesetting industry.</text>
</svg>
Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Nope.

The SVG <text> element seems to be related to the canvas fillText method.

Luckily our friend <foreignObject> enables us to put HTML in SVG.

<svg width="280" height="100" xmlns="http://www.w3.org/2000/svg">
  <foreignObject x="0" y="0" width="280" height="100">
    <style>
      p {
        margin: 0;
        font-weight: normal;
        font-family: serif;
        font-size: 20px;
        line-height: 32px;
      }
    </style>
    <!-- Yo! -->
    <p xmlns="http://www.w3.org/1999/xhtml">
      Lorem Ipsum is … typesetting industry.
    </p>
  </foreignObject>
</svg>

Check it out below and let’s claim this small victory!

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

But just like we can’t pass our HTML paragraph to the drawImage method we can’t pass this SVG code to the drawImage method.

But unlike HTML we can pass SVG to an image element and we can pass an image element to the drawImage method.

To do that we need to convert our SVG to a dataURL.

<img src="data:image/svg+xml,…" />

Loading inline SVG to an image element

We’ll load the SVG from the previous example and convert it to a dataURL which we can assign to our image src attribute.

// Get a reference to the svg element
const svgElement = document.getElementById("svg");

// Get the SVG code in string format <svg>...
const svgCode = svgElement.outerHTML;

// Remove newlines and replace double quotes with single quotes
const svgCodeEncoded = svgCode.replace(/\n/g, "").replace(/"/g, "'");

// Get the image element and use our svg as src
const imgElement = document.getElementById("svgImage");
imgElement.src = `data:image/svg+xml,${svgCodeEncoded}`;

Below we can see that the SVG is loaded in our image element.

Now for our final trick we have to draw the image element containing our inline SVG to our canvas element. We’re now three layers deep, let’s not stay too long as time flows slow down here.

Drawing our example HTML paragraph to a canvas

Let’s combine all steps together.

We’ll load the HTML paragraph and dynamically create an SVG that we’ll then load to a canvas.

// Get a reference to the example paragraph at the start of the article
const exampleParagraph = document.getElementById("exampleParagraph");

// Generate SVG code with JavaScript
// Note that we wrap the paragraph HTML in a <div>
const svgCode = `
    <svg width="280" height="100" xmlns="http://www.w3.org/2000/svg">
        <foreignObject x="0" y="0" width="280" height="100">
            <style>
            p {
                margin:0;
                font-weight: normal;
                font-family: serif;
                font-size: 20px;
                line-height: 32px;
            }
            </style>
            <div xmlns="http://www.w3.org/1999/xhtml">
                ${exampleParagraph.outerHTML}
            </div>
        </foreignObject>
    </svg>`;

// Remove newlines and replace double quotes with single quotes
const svgCodeEncoded = svgCode.replace(/\n/g, "").replace(/"/g, "'");

// Dynamically create an image element
const img = document.createElement("img");

// Wait for the image to load before drawing it to the canvas
img.onload = () => {
  // Get the output canvas context
  const ctx = document.getElementById("canvasDrawImage").getContext("2d");

  // Draw the image to the canvas
  ctx.drawImage(img, 0, 0);
};

// Load our SVG to the image
img.src = `data:image/svg+xml,${svgCodeEncoded}`;

And there we go!

The result might appear a bit blurry on high res displays. To change that we can draw the text to a higher resolution canvas and then scale down the canvas with CSS.

To use this in production you would probably rewrite this logic to a function. There are some additional caveats to keep in mind.

When done correctly this yields a very exact rendering of text. Lines are broken were you expect them to break and the resulting code is relatively small compared to writing custom line break logic that is as precise as the browser (if that’s even possible).

I use Twitter to share new webdevelopment tips and tricks, so Follow me there if you found this interesting and want to learn more.

Rik Schennink

Indie Product Developer

to pqina.nl