Tuesday, 18 December 2012

Text Wrapping in HTML Canvas.

Canvas has taken the internet by storm, and with proper IE support we can now look forward to developing more canvas based web applications. Anyone who has attempted to write text to the canvas using a user defined input will know it's not as easy as it should be.
Since there is not a proper text-overflow mechanism for Canvas we need to create our own. In this post I will show you my implementation of Canvas text-overflow and how to handle it.

First, here is my complete function to handle text-overflow and the Canvas element:


    function fragmentText(textmaxWidth{
        var words text.split(' '),
            lines [],
            line "";
        if (ctx.measureText(text).width maxWidth{
            return [text];
        }
        while (words.length 0{
            while (ctx.measureText(words[0]).width >= maxWidth{
                var tmp words[0];
                words[0tmp.slice(0-1);
                if (words.length 1{
                    words[1tmp.slice(-1words[1];
                else {
                    words.push(tmp.slice(-1));
                }
            }
            if (ctx.measureText(line words[0]).width maxWidth{
                line += words.shift(" ";
            else {
                lines.push(line);
                line "";
            }
            if (words.length === 0{
                lines.push(line);
            }
        }
        return lines;
    }

Ok, that is probably confusing to you (if it is not you can stop reading now), I will try to explain what the method is doing.

I choose to split the sentence up into individual words, then act on those words and rebuild the sentence back up based on the maxWidth parameter you pass in.

The first thing is to check if the sentence even needs to be split up. If it does not then return the sentence as a single line.

    if (ctx.measureText(text).width maxWidth{
        return [text];
    }

If you are a stickler for performance you should write the check before you define any local variables in the function scope, however in most cases this does not make any measurable difference.

The next bit of code does the actual text splitting. The outer while (){} is looping over all words, checking to see if adding the next word to the line will make the line wider than the maxWidth, and if so creating a new line from it. The inner while (){} is checking each word to make sure it in itself is not longer than the maxWidth, and if it is breaking the word up into multiple words.

    while (words.length 0{
        while (ctx.measureText(words[0]).width >= maxWidth{
            var tmp words[0];
            words[0tmp.slice(0-1);
            if (words.length 1{
                words[1tmp.slice(-1words[1];
            else {
                words.push(tmp.slice(-1));
            }
        }
        if (ctx.measureText(line words[0]).width maxWidth{
            line += words.shift(" ";
        else {
            lines.push(line);
            line "";
        }
        if (words.length === 0{
            lines.push(line);
        }
    }

Note: In each iteration of the outer loop, the inner check is done first prior to the words being added to the current line.

After the function has processed the sentence into lines, the lines are returned in an array (each row is a new line). Here is a fiddle showing you how to use this code:



Awesome! We're now on our way to getting this on an actual Canvas element.

If you have ever used the Canvas element before you are familiar with having to draw frames, well in this example we will only need to draw on keyup for the inputs.

Here is some basic HTML for the Canvas element and supporting inputs:

​    <canvas id="canvas" height="200" width="200"></canvas>
    <input type="text" id="sentence" value="" placeholder="your text here" />​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​

onkeyup for the input you will want to trigger the draw method.

    document.getElementById('sentence').onkeyup draw;

You may do some other processing in the draw method, however for this demo we will only dump the text to the canvas element (top down).

    function draw({
        var lines fragmentText(this.valuecanvas.width 0.9), // 10% padding
            font_size 22// px
        ctx.font "Bold " font_size "px Arial";
        ctx.save();
        ctx.clearRect(00canvas.widthcanvas.height);
        lines.forEach(function(linei{
            ctx.fillText(linecanvas.width 2(1font_size)// assume font height.
        });
        ctx.restore();
    }

And there you go! Wrapping text on a Canvas element made easy. Here is a complete demo:



Further Readings:

  1. MDN - Drawing text using a canvas (measureText)
  2. MDN - Drawing text using a canvas (fillText)
Real World Example:

2 comments:

  1. Hello Robert, I found you at stackoverflow with the same topic question. I commented on you there but haven't replied yet. So I will repeat same question here. :)

    "the code doesn't recognizes 'ENTER' value. And if user enters a long word/characters that beyond the maxWidth allowed, it doesn't produce space for the following word. Any suggestion? BTW, I converted the text input into a textarea."

    I'm stuck with this problem for a week so I guess I need a help from you.

    Thanks in advance

    ReplyDelete
    Replies
    1. It shouldn't recognize enter value, it's not built that way.
      However if you listened for it you could force a new line I suppose. And you are right! Thanks, I have never noticed this bug before.
      I will look into it and update my post,
      Thanks for the reply!

      Delete