fabricJSTextWrap

Fabric.js is an otherwise awesome library, but doesn’t have text wrapping functionality that of standard DOM. This little function takes a fabric.IText and returns a formatted copy with width and height enforced through line breaks and text trim. Long words get hyphenated, but without syllable regard.

Charalampos (in the comments below) has posted an update that also accepts an additional parameter and justifies the text alignment. I am making his version the featured version. Thank you sir!

function wrapCanvasText(t, canvas, maxW, maxH, justify) {
 
  if (typeof maxH === "undefined") { maxH = 0; }
  var words = t.text.split(" ");
  var formatted = '';
 
  // This works only with monospace fonts
  justify = justify || 'left';
 
  // clear newlines
  var sansBreaks = t.text.replace(/(\r\n|\n|\r)/gm, "");  
  // calc line height
  var lineHeight = new fabric.Text(sansBreaks, {        
    fontFamily: t.fontFamily,
    fontSize: t.fontSize
  }).height;
 
  // adjust for vertical offset
  var maxHAdjusted = maxH > 0 ? maxH - lineHeight : 0;                  
  var context = canvas.getContext("2d");
 
 
  context.font = t.fontSize + "px " + t.fontFamily;
  var currentLine = '';
  var breakLineCount = 0;
 
  n = 0;
  while(n < words.length) {
    var isNewLine = currentLine == "";
    var testOverlap = currentLine + ' ' + words[n];
   
    // are we over width?
    var w = context.measureText(testOverlap).width;
   
    if(w < maxW) {  // if not, keep adding words
      if (currentLine != '')
        currentLine += ' ';
      currentLine += words[n];
      // formatted += words[n] + ' ';
    } else {
     
      // if this hits, we got a word that need to be hypenated
      if(isNewLine) {
        var wordOverlap = "";
       
        // test word length until its over maxW
        for(var i = 0; i < words[n].length; ++i) {
         
          wordOverlap += words[n].charAt(i);
          var withHypeh = wordOverlap + "-";
         
          if(context.measureText(withHypeh).width >= maxW) {
            // add hyphen when splitting a word
            withHypeh = wordOverlap.substr(0, wordOverlap.length - 2) + "-";
            // update current word with remainder
            words[n] = words[n].substr(wordOverlap.length - 1, words[n].length);
            formatted += withHypeh; // add hypenated word
                                  break;
          }
        }
      }
      while(justify == 'right' && context.measureText(' ' + currentLine).width < maxW)
        currentLine = ' ' + currentLine;
     
      while(justify == 'center' && context.measureText(' ' + currentLine + ' ').width < maxW)
        currentLine = ' ' + currentLine + ' ';
     
      formatted += currentLine + '\n';
      breakLineCount++;
      currentLine = "";
     
      continue; // restart cycle
    }
    if(maxHAdjusted > 0 && (breakLineCount * lineHeight) > maxHAdjusted) {
      // add ... at the end indicating text was cutoff
      formatted = formatted.substr(0, formatted.length - 3) + "...\n";
      currentLine = "";
      break;
    }
    n++;
  }
 
  if (currentLine != '') {
    while(justify == 'right' && context.measureText(' ' + currentLine).width < maxW)
      currentLine = ' ' + currentLine;
   
    while(justify == 'center' && context.measureText(' ' + currentLine + ' ').width < maxW)
      currentLine = ' ' + currentLine + ' ';
   
    formatted += currentLine + '\n';
    breakLineCount++;
    currentLine = "";
  }
 
  // get rid of empy newline at the end
  formatted = formatted.substr(0, formatted.length - 1);
                                 
                                  var ret = new fabric.Text(formatted, { // return new text-wrapped text obj
                                    left: t.left,
                                  top: t.top,
                                  fill: t.fill,
                                  fontFamily: t.fontFamily,
                                  fontSize: t.fontSize,
                                  originX: t.originX,
                                  originY: t.originY,
                                  angle: t.angle,
                                  });
  return ret;
}

11 comments

  • Ahmed

    Hi, I was wondering what is “fabric.IText” .. there seems to be no object of type IText in fabric js..

    Reply
    • enko

      Ahmed, sorry its confusing – just pass it a regular fabric Text object. I was trying to imply that it needs to be fabric.Text or have the interface that matches, hence IText.

      Reply
  • Charalampos

    Nice!
    Just posting some changes, in order to justify right/center (only with monospace fonts).

    function wrapCanvasText(t, canvas, maxW, maxH, justify) {
     
      if (typeof maxH === "undefined") { maxH = 0; }
      var words = t.text.split(" ");
      var formatted = '';
     
      // This works only with monospace fonts
      justify = justify || 'left';
     
      // clear newlines
      var sansBreaks = t.text.replace(/(\r\n|\n|\r)/gm, "");  
      // calc line height
      var lineHeight = new fabric.Text(sansBreaks, {        
        fontFamily: t.fontFamily,
        fontSize: t.fontSize
      }).height;
     
      // adjust for vertical offset
      var maxHAdjusted = maxH > 0 ? maxH - lineHeight : 0;                  
      var context = canvas.getContext("2d");
     
     
      context.font = t.fontSize + "px " + t.fontFamily;
      var currentLine = '';
      var breakLineCount = 0;
     
      n = 0;
      while(n < words.length) {
        var isNewLine = currentLine == "";
        var testOverlap = currentLine + ' ' + words[n];
       
        // are we over width?
        var w = context.measureText(testOverlap).width;
       
        if(w < maxW) {  // if not, keep adding words
          if (currentLine != '')
            currentLine += ' ';
          currentLine += words[n];
          // formatted += words[n] + ' ';
        } else {
         
          // if this hits, we got a word that need to be hypenated
          if(isNewLine) {
            var wordOverlap = "";
           
            // test word length until its over maxW
            for(var i = 0; i < words[n].length; ++i) {
             
              wordOverlap += words[n].charAt(i);
              var withHypeh = wordOverlap + "-";
             
              if(context.measureText(withHypeh).width >= maxW) {
                // add hyphen when splitting a word
                withHypeh = wordOverlap.substr(0, wordOverlap.length - 2) + "-";
                // update current word with remainder
                words[n] = words[n].substr(wordOverlap.length - 1, words[n].length);
                formatted += withHypeh; // add hypenated word
                                      break;
              }
            }
          }
          while(justify == 'right' && context.measureText(' ' + currentLine).width < maxW)
            currentLine = ' ' + currentLine;
         
          while(justify == 'center' && context.measureText(' ' + currentLine + ' ').width < maxW)
            currentLine = ' ' + currentLine + ' ';
         
          formatted += currentLine + '\n';
          breakLineCount++;
          currentLine = "";
         
          continue; // restart cycle
        }
        if(maxHAdjusted > 0 && (breakLineCount * lineHeight) > maxHAdjusted) {
          // add ... at the end indicating text was cutoff
          formatted = formatted.substr(0, formatted.length - 3) + "...\n";
          currentLine = "";
          break;
        }
        n++;
      }
     
      if (currentLine != '') {
        while(justify == 'right' && context.measureText(' ' + currentLine).width < maxW)
          currentLine = ' ' + currentLine;
       
        while(justify == 'center' && context.measureText(' ' + currentLine + ' ').width < maxW)
          currentLine = ' ' + currentLine + ' ';
       
        formatted += currentLine + '\n';
        breakLineCount++;
        currentLine = "";
      }
     
      // get rid of empy newline at the end
      formatted = formatted.substr(0, formatted.length - 1);
                                     
                                      var ret = new fabric.Text(formatted, { // return new text-wrapped text obj
                                        left: t.left,
                                      top: t.top,
                                      fill: t.fill,
                                      fontFamily: t.fontFamily,
                                      fontSize: t.fontSize,
                                      originX: t.originX,
                                      originY: t.originY,
                                      angle: t.angle,
                                      });
      return ret;
    }
    Reply
    • enko

      Awesome! Thank you! I was actually putting off this feature in my app, but will use your version.

      Reply
      • Anonymous

        how to include this in my project?

        Reply
  • Neil

    Hi there,
    Do you have a working example/demo of this? :)

    Thanks,
    Neil

    Reply
  • Ali

    Hi,

    I am new to Javascript but I would really like to utilize function in my Fabric application. I tried saving the function as a separate .js file but I can’t seem to get it to work. Could you explain where in fabric.js I should add this? and how do I call this function?

    I’m thinking: wrapcanvastext(“text”, “canvas”, maxW: 200 , maxH: 200 , ‘right’);

    I would really appreciate your feedback!

    Reply
  • Dennis

    Thanks for sharing your code…

    If you use enters / new lines in your text, the height of the returned Text object will be higher than the supplied maxH.
    To solve this, add the next 2 lines:

    // calculate new lines
    var newLines = textObject.text.split(/\r\n|\r|\n/).length;
    newLines = (newLines > 0 ? newLines : 1);

    And replace:

    // adjust for vertical offset
    var maxHAdjusted = maxHeight > 0 ? maxHeight – lineHeight : 0;

    With this:

    // adjust for vertical offset
    var maxHAdjusted = maxHeight > 0 ? maxHeight – (lineHeight * newLines) : 0;

    Now you can use enters / new lines (\r \n \r\n) in your text and the maxH will be retained.

    Reply
    • enko

      I’ll add this with next post update, thank you!

      Reply
      • KevinP

        Can you provide a fiddle for this to try more??

        Reply

Leave a comment