Typescript Canvas Multiline Text

So I've been working on some games, and I realized I was going to need a multi-line text field on my HTML5 Canvas. The problem is there doesn't really seem to be a built in way to do that on a JavaScript / TypeScript canvas. This creates a great opportunity to make one. I've put together a TypeScript class that allows you to create a multi-line text field. I've also added some additional functionality to the version I'm going to show you here. I've got some code that will allow you to move and resize the textfield. I've also added some code to change the cursor for an additional affordance letting you know where you can resize the field or where you can grap it to move it.

Properties of the Multiline Textfield class

The first thing we need to figure out is what kind of properties we're going to need for this class. We need all the basic textfield stuff, like the text we're going to display, x and y coordinates, font size and color. For our older textfield we also had the font (e.g. Verdana), but I'm going to have a global variable for that in this app.

class MultiLineText {
   private _text: string = "";
   public x: number = 0;
   public y: number = 0;
   public fontSize: number = 16;
   public color: string = "black";
}

You'll notice that _text is private. Unlike in our single line textfield, we can't just change text without doing a lot of other things. We are going to need to add some parsing to figure out where we can break up our text by spaces into seperate lines. We are going to need to know what the maximum width and height of our textfield will be, to help us know how to display it. We will also need to keep an array of all the words in our text as well as an array of all the lines. Here is what our class looks like after we add the additional variables we need. I've also added a lineHeight variable that will be a multiple (1.25x) of the font size. That will allow us to space out the lines based on what we set the font size to in the constructor.

The Constructor And Setting The Lines

The next thing we need is a constructor, and a method (setText) for breaking apart the textfield into the lines we need to display. In our setText method, we're going to need to split our text string into words by splitting on spaces. After that we will loop over all the words attempting to put together the maximum number of words that will fit on each line. We will have a temporary string that will have one more word than the last string. We will check ctx.measureText against our maximum width variable, if this fits, we continue trying to add words. If it doesn't fit, we take the string before we added this last word, and add it to our list of lines. We have a maximum height as well, but we will simply not display that in our draw method when we get to that.

constructor(max_width: number, max_height: number, text: string,
   font_size: number = 16, color: string = "black",
   x: number = 0, y: number = 0) {

   this.fontSize = font_size;
   this.lineHeight = Math.ceil(this.fontSize * 1.25);
   this.color = color;
   this.x = x;
   this.y = y;

   this.maxWidth = max_width;
   this.maxHeight = max_height;

   this.setText(text);
}

public setText = (text: string = null): void => {
   if (text != null) {
      this._wordArray = text.split(" ");
   }

   var temp_line: string = "";
   var temp_line_ext: string = "";
   var temp_word: string = "";
   var temp_word_array: Array<string> = this._wordArray.slice(0);

   this._lineList = new Array<string>();

   ctx.font = this.fontSize + "px " + font;
   outerloop:
   while (temp_word_array.length > 0) {
      var add_count: number = 0;
      do {
         if (temp_word_array.length == 0) {
            this._lineList.push(temp_line_ext);
            break outerloop;
         }
         temp_line = temp_line_ext;
         temp_word = temp_word_array.shift();
         temp_line_ext = temp_line_ext + " " + temp_word;
         add_count++;
      } while (ctx.measureText(temp_line_ext).width < this.maxWidth);

      if (add_count > 1) {
         temp_word_array.unshift(temp_word);
      }
      else {
         temp_line = temp_line_ext;
      }

      this._lineList.push(temp_line);
      temp_line = "";
      temp_line_ext = "";
   }

}

Drawing our MultiLine Textfield

I'm going to start out by drawing a box around the textfield to show where the text will wrap. this is a simple ctx.rect call. After that I'm going to loop through each line I set up early and do a ctx.fill text until the height of my lines exceeds the maximum height.

public draw = (): void => {
   ctx.save();
   ctx.beginPath();
   ctx.strokeStyle = this.color;
   ctx.lineWidth = 1;
   ctx.rect(this.x, this.y, this.maxWidth, this.maxHeight);
   ctx.stroke();
   ctx.restore();


   ctx.save();
   ctx.textAlign = "left";
   ctx.textBaseline = "top";
   ctx.fillStyle = this.color;
   ctx.font = this.fontSize + "px " + font;

   for (var i: number = 0; i < this._lineList.length; i++) {
      ctx.beginPath();
      ctx.fillText(this._lineList[i], this.x, this.y + i * this.lineHeight);
      ctx.stroke();

      if ((i + 2) * this.lineHeight > this.maxHeight) {
         break;
      }
   }
   ctx.restore();
}

This ends up drawing a multi-line textbox that looks like this:

In the final version of the code, I've added some cursor changes as well as the ability to move the box and drag the edges to increase or decrease the size of the textbox. You can check out the full code here.

I've got the app below. Play around with it by dragging the box, or grabbing the edge of the box and dragging it to make the box wider, narrower, shorter or taller.