HTML5 canvas ctx.fillText won't do line breaks?

122,935

Solution 1

I'm afraid it is a limitation of Canvas' fillText. There is no multi-line support. Whats worse, there's no built-in way to measure line height, only width, making doing it yourself even harder!

A lot of people have written their own multi-line support, perhaps the most notable project that has is Mozilla Skywriter.

The gist of what you'll need to do is multiple fillText calls while adding the height of the text to the y value each time. (measuring the width of M is what the skywriter people do to approximate text, I believe.)

Solution 2

If you just want to take care of the newline chars in the text you could simulate it by splitting the text at the newlines and calling multiple times the fillText()

Something like http://jsfiddle.net/BaG4J/1/

var c = document.getElementById('c').getContext('2d');
c.font = '11px Courier';
    console.log(c);
var txt = 'line 1\nline 2\nthird line..';
var x = 30;
var y = 30;
var lineheight = 15;
var lines = txt.split('\n');

for (var i = 0; i<lines.length; i++)
    c.fillText(lines[i], x, y + (i*lineheight) );
canvas{background-color:#ccc;}
<canvas id="c" width="150" height="150"></canvas>

I just made a wrapping proof of concept (absolute wrap at specified width. No handling words breaking, yet)
example at http://jsfiddle.net/BaG4J/2/

var c = document.getElementById('c').getContext('2d');
c.font = '11px Courier';

var txt = 'this is a very long text to print';

printAt(c, txt, 10, 20, 15, 90 );


function printAt( context , text, x, y, lineHeight, fitWidth)
{
    fitWidth = fitWidth || 0;
    
    if (fitWidth <= 0)
    {
         context.fillText( text, x, y );
        return;
    }
    
    for (var idx = 1; idx <= text.length; idx++)
    {
        var str = text.substr(0, idx);
        console.log(str, context.measureText(str).width, fitWidth);
        if (context.measureText(str).width > fitWidth)
        {
            context.fillText( text.substr(0, idx-1), x, y );
            printAt(context, text.substr(idx-1), x, y + lineHeight, lineHeight,  fitWidth);
            return;
        }
    }
    context.fillText( text, x, y );
}
canvas{background-color:#ccc;}
<canvas id="c" width="150" height="150"></canvas>

And a word-wrapping (breaking at spaces) proof of concept.
example at http://jsfiddle.net/BaG4J/5/

var c = document.getElementById('c').getContext('2d');
c.font = '11px Courier';

var txt = 'this is a very long text. Some more to print!';

printAtWordWrap(c, txt, 10, 20, 15, 90 );


function printAtWordWrap( context , text, x, y, lineHeight, fitWidth)
{
    fitWidth = fitWidth || 0;
    
    if (fitWidth <= 0)
    {
        context.fillText( text, x, y );
        return;
    }
    var words = text.split(' ');
    var currentLine = 0;
    var idx = 1;
    while (words.length > 0 && idx <= words.length)
    {
        var str = words.slice(0,idx).join(' ');
        var w = context.measureText(str).width;
        if ( w > fitWidth )
        {
            if (idx==1)
            {
                idx=2;
            }
            context.fillText( words.slice(0,idx-1).join(' '), x, y + (lineHeight*currentLine) );
            currentLine++;
            words = words.splice(idx-1);
            idx = 1;
        }
        else
        {idx++;}
    }
    if  (idx > 0)
        context.fillText( words.join(' '), x, y + (lineHeight*currentLine) );
}
canvas{background-color:#ccc;}
<canvas id="c" width="150" height="150"></canvas>

In the second and third examples i am using the measureText() method which shows how long (in pixels) a string will be when printed.

Solution 3

Maybe coming to this party a bit late, but I found the following tutorial for wrapping text on a canvas perfect.

http://www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/

From that I was able to think get multi lines working (sorry Ramirez, yours didn't work for me!). My complete code to wrap text in a canvas is as follows:

<script type="text/javascript">

     // http: //www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/
     function wrapText(context, text, x, y, maxWidth, lineHeight) {
        var cars = text.split("\n");

        for (var ii = 0; ii < cars.length; ii++) {

            var line = "";
            var words = cars[ii].split(" ");

            for (var n = 0; n < words.length; n++) {
                var testLine = line + words[n] + " ";
                var metrics = context.measureText(testLine);
                var testWidth = metrics.width;

                if (testWidth > maxWidth) {
                    context.fillText(line, x, y);
                    line = words[n] + " ";
                    y += lineHeight;
                }
                else {
                    line = testLine;
                }
            }

            context.fillText(line, x, y);
            y += lineHeight;
        }
     }

     function DrawText() {

         var canvas = document.getElementById("c");
         var context = canvas.getContext("2d");

         context.clearRect(0, 0, 500, 600);

         var maxWidth = 400;
         var lineHeight = 60;
         var x = 20; // (canvas.width - maxWidth) / 2;
         var y = 58;


         var text = document.getElementById("text").value.toUpperCase();                

         context.fillStyle = "rgba(255, 0, 0, 1)";
         context.fillRect(0, 0, 600, 500);

         context.font = "51px 'LeagueGothicRegular'";
         context.fillStyle = "#333";

         wrapText(context, text, x, y, maxWidth, lineHeight);
     }

     $(document).ready(function () {

         $("#text").keyup(function () {
             DrawText();
         });

     });

    </script>

Where c is the ID of my canvas and text is the ID of my textbox.

As you can probably see am using a non-standard font. You can use @font-face as long as you have used the font on some text PRIOR to manipulating the canvas - otherwise the canvas won't pick up the font.

Hope this helps someone.

Solution 4

Split the text into lines, and draw each separately:

function fillTextMultiLine(ctx, text, x, y) {
  var lineHeight = ctx.measureText("M").width * 1.2;
  var lines = text.split("\n");
  for (var i = 0; i < lines.length; ++i) {
    ctx.fillText(lines[i], x, y);
    y += lineHeight;
  }
}

Solution 5

Here's my solution, modifying the popular wrapText() function that is already presented here. I'm using the prototyping feature of JavaScript so that you can call the function from the canvas context.

CanvasRenderingContext2D.prototype.wrapText = function (text, x, y, maxWidth, lineHeight) {

    var lines = text.split("\n");

    for (var i = 0; i < lines.length; i++) {

        var words = lines[i].split(' ');
        var line = '';

        for (var n = 0; n < words.length; n++) {
            var testLine = line + words[n] + ' ';
            var metrics = this.measureText(testLine);
            var testWidth = metrics.width;
            if (testWidth > maxWidth && n > 0) {
                this.fillText(line, x, y);
                line = words[n] + ' ';
                y += lineHeight;
            }
            else {
                line = testLine;
            }
        }

        this.fillText(line, x, y);
        y += lineHeight;
    }
}

Basic usage:

var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
ctx.fillStyle = "black";
ctx.font = "12px sans-serif";
ctx.textBaseline = "top";
ctx.wrapText("Hello\nWorld!",20,20,160,16);

Here's a demonstration I put together: http://jsfiddle.net/7RdbL/

Share:
122,935
Spectraljump
Author by

Spectraljump

BY DAY: Alt-Rock Ninja Cowgirl at Veridian Dynamics. BY NIGHT: I write code and code rights for penalcoders.example.org, an awesome non-profit that will totally take your money at that link. My kids are cuter than yours. FOR FUN: C+ Jokes, Segway Roller Derby, NYT Sat. Crosswords (in Sharpie!), Ostrich Grooming. "If you see scary things, look for the helpers-you'll always see people helping." -Fred Rogers

Updated on July 18, 2022

Comments

  • Spectraljump
    Spectraljump almost 2 years

    I can't seem to be able to add text to a canvas if the text includes "\n". I mean, the line breaks do not show/work.

    ctxPaint.fillText("s  ome \n \\n <br/> thing", x, y);
    

    The above code will draw "s ome \n <br/> thing", on one line.

    Is this a limitation of fillText or am I doing it wrong? the "\n"s are there, and aren't printed, but they don't work either.

    • Gabriele Petrioli
      Gabriele Petrioli over 13 years
      do you want to automatically wrap when reaching the end ? or just to take into consideration the newline chars present in the text ?
    • Tower
      Tower over 13 years
      Wrap the text into multiple lines.
    • Tim
      Tim over 13 years
      Hi twodordan, does this limitation exist on both chrome and mozilla ? People often use simple html text that they put over the canvas with a position:absolute for example. Also you can do two fillText and moving the Y origin of your text for your second lines.
    • MvG
      MvG almost 8 years
    • Andrew
      Andrew about 4 years
      TL;DR: Either call fillText() multiple times and use your font height to separate, or use developer.mozilla.org/en-US/docs/Web/API/TextMetrics developer.mozilla.org/en-US/docs/Web/API/… - or, use one of the very complicated "solutions" below that do not use TextMetrics...
  • Tower
    Tower over 13 years
    Yes, that is what I am thinking of doing. It's just that with fillText() and strokeText(), you can do things beyond what CSS can do.
  • Spectraljump
    Spectraljump over 13 years
    Thank you! I had a feeling it would be bothersome... Nice to know about the SKYWRITER, but I'll just "wait" until fillText() is improved. It wasn't a terribly important deal in my case. Hah, no line height, it's like someone did that on purpose. :D
  • SikoSoft
    SikoSoft about 13 years
    Honestly, I wouldn't hold your breath on fillText() being "improved" to support this, since I get the feeling this is how it is intended to be used (multiple calls & calculating the yOffset yourself). I think a lot of the power with the canvas API is that it separates the lower-level drawing functionality from what you can already do (perform the necessary measurements). Also, you can know the text height simply by providing the text size in pixels; in other words: context.font = "16px Arial"; - you have the height there; the width is the only one that is dynamic.
  • Jerry Asher
    Jerry Asher about 11 years
    I haven't tested this, but I think this may be a better solution -- the other solutions here using fillText() make it so the text cannot be selected (or presumably pasted).
  • psycho brm
    psycho brm almost 11 years
    Uncaught ReferenceError: Words is not defined If i try to change font. For example: ctx.font = '40px Arial'; - try putting that in your fiddle
  • psycho brm
    psycho brm almost 11 years
    Btw, where the hell does Words (case-sensitive) variable come from?? It's not defined anywhere. That part of the code only gets executed when you change font..
  • jbaylina
    jbaylina almost 11 years
    @psychobrm You are absolutly right. It is a bug (I already fix it). This part of code is only executed if you have to split a word in two lines. Thank you!
  • psycho brm
    psycho brm over 10 years
    I've made some upgrades I needed: render spaces, render leading/trailing newlines, render stroke and fill with one call (dont measure text twice), I've also had to change iteration, since for in doesn't work well with extended Array.prototype. Could you put it on github so that we can iterate on it?
  • jbaylina
    jbaylina over 10 years
    @psychobrm I merged your changes. Thank you!
  • Amol Navsupe
    Amol Navsupe about 9 years
    hello,suppose my text is like this var text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa‌​aaaaaaaaaaaaaaaaaaaa‌​aaaaaaaaaaaaaaaaaaaa‌​aaaaaaaaaaaaaaaaaaaa‌​aaaaaaaaaaaaaaaaaaaa‌​aaaaaaaaaaaaaaaaaaaa‌​aaaaaaaaaaaaaaaaaaaa‌​aaaaaaaaaaaaaaa"; then what is happend in canvas???
  • couzzi
    couzzi over 8 years
    Worked like a charm. Thank you.
  • KaHa6uc
    KaHa6uc almost 8 years
    It will go out of the canvas, as @Ramirez did not put the maxWidth parameter to fillText :)
  • Eric Hodonsky
    Eric Hodonsky almost 8 years
    That's a good solution if you have the memory to build an element like that every time you need to measure. You can also ctx.save() then, ctx.font = '12pt Arial' then, parseInt( ctx.font, 10 ). Note that I use 'pt' when setting it. It then will translate into PX and be able to turn into a digit for consumption as the height of the font.
  • SWdV
    SWdV over 7 years
    Some additional properties for measureText() have been added which I think could solve the problem. Chrome has a flag to enable them, but other browsers don't... yet!
  • Simon Sarris
    Simon Sarris over 7 years
    @SWdV just to be clear, those have been in the spec for years now, it may be years yet until we have wide enough adoption to use :(
  • amir22
    amir22 almost 5 years
    how to justify the whole long text ?
  • amir22
    amir22 almost 5 years
    how to set direction RTL for justify?
  • Mr. Polywhirl
    Mr. Polywhirl about 4 years
    I went ahead and defined some variables to help "self-document" the example. It also handles centering the bounding box within the canvas. I also added a rectangle behind, so you can actually see it centered in relation. Great work! +1 One little thing I noticed is that the lines that wrap will not have their leading spaces suppressed. You may want to trim each line e.g. ctx.fillText(txtline.trim(), textanchor, txtY) I only noticed this in your interactive demo on your website.
  • Geon George
    Geon George about 4 years
    @Mr.Polywhirl Thank you for clearing up the answer. I have fixed the trim issue and published the 2.0.9 version. The demo site is fixed by updating the package version. There is an issue with multiple spaces. I do not know if it's better to go with an opinionated package or ignore the problem. Been getting requests for this from multiple places. I went ahead and added trim anyway. Lorem ipsum dolor, sit <many spaces> amet this was the reason why I didn't do it in the first place. What do you think should I consider multiple spaces and only remove if there is just one?
  • Geon George
    Geon George about 4 years
    Edit: it seems StackOverflow code block ignores multi spaces as well
  • GlenPeterson
    GlenPeterson about 4 years
    This library works very nicely, is small and simple, and has no dependencies. Thank you!
  • Mike 'Pomax' Kamermans
    Mike 'Pomax' Kamermans almost 4 years
    If you need a long, justified text, why would you be using a canvas?
  • Mike 'Pomax' Kamermans
    Mike 'Pomax' Kamermans almost 4 years
    links expire, please put the code in this answer even if you have a working link. If jsfiddle shuts down, this answer becomes entirely useless as is.
  • Summer Sun
    Summer Sun almost 4 years
    Maybe actualBoundingBoxAscent could be used to measure line height.
  • Samnad Sainulabdeen
    Samnad Sainulabdeen over 2 years
    i like the third method