Text Rendering using canvas

Text Rendering using canvas
Nikhil
Published on 2021-07-04 17:09:16

Justified text using Canvas


This example renders justified text. It adds extra functionality to the CanvasRenderingContext2D by extending its prototype or as a global object justifiedText.

The Example

The function as an anonymous immediately invoked function.

(function()
{
  const FILL = 0; // const to indicate filltext render
  const STROKE = 1;
  const MEASURE = 2;
  var renderType = FILL; // used internal to set fill or stroke text
  var maxSpaceSize = 3; // Multiplier for max space size. If greater then no justificatoin applied
  var minSpaceSize = 0.5; // Multiplier for minimum space size
  var renderTextJustified = function(ctx, text, x, y, width)
  {
    var words, wordsWidth, count, spaces, spaceWidth, adjSpace, renderer, i, textAlign,
      useSize, totalWidth;
    textAlign = ctx.textAlign; // get current align settings
    ctx.textAlign = "left";
    wordsWidth = 0;
    words = text.split(" ").map(word =>
    {
      var w = ctx.measureText(word).width;
      wordsWidth += w;
      return {
        width: w,
        word: word,
      };
    });
    // count = num words, spaces = number spaces, spaceWidth normal space size
    // adjSpace new space size >= min size. useSize Resulting space size used to render
    count = words.length;
    spaces = count - 1;
    spaceWidth = ctx.measureText(" ").width;
    adjSpace = Math.max(spaceWidth * minSpaceSize, (width - wordsWidth) / spaces);
    useSize = adjSpace > spaceWidth * maxSpaceSize ? spaceWidth : adjSpace;
    totalWidth = wordsWidth + useSize * spaces
    if (renderType === MEASURE)
    { // if measuring return size
      ctx.textAlign = textAlign;
      return totalWidth;
    }
    renderer = renderType === FILL ? ctx.fillText.bind(ctx) : ctx.strokeText.bind(ctx); // fill
    or stroke
    switch (textAlign)
    {
      case "right":
        x -= totalWidth;
        break;
      case "end":
        x += width - totalWidth;
        break;
      case "center": // intentional fall through to default
        x -= totalWidth / 2;
      default:
    }
    if (useSize === spaceWidth)
    { // if space size unchanged
      renderer(text, x, y);
    }
    else
    {
      for (i = 0; i < count; i += 1)
      {
        renderer(words[i].word, x, y);
        x += words[i].width;
        x += useSize;
      }
    }
    ctx.textAlign = textAlign;
  }
  // Parse vet and set settings object.
  var justifiedTextSettings = function(settings)
  {
    var min, max;
    var vetNumber = (num, defaultNum) =>
    {
      num = num !== null && num !== null && !isNaN(num) ? num : defaultNum;
      if (num < 0)
      {
        num = defaultNum;
      }
      return num;
    }
    if (settings === undefined || settings === null)
    {
      return;
    }
    max = vetNumber(settings.maxSpaceSize, maxSpaceSize);
    min = vetNumber(settings.minSpaceSize, minSpaceSize);
    if (min > max)
    {
      return;
    }
    minSpaceSize = min;
    maxSpaceSize = max;
  }
  // define fill text
  var fillJustifyText = function(text, x, y, width, settings)
  {
    justifiedTextSettings(settings);
    renderType = FILL;
    renderTextJustified(this, text, x, y, width);
  }
  // define stroke text
  var strokeJustifyText = function(text, x, y, width, settings)
  {
    justifiedTextSettings(settings);
    renderType = STROKE;
    renderTextJustified(this, text, x, y, width);
  }
  // define measure text
  var measureJustifiedText = function(text, width, settings)
  {
    justifiedTextSettings(settings);
    renderType = MEASURE;
    return renderTextJustified(this, text, 0, 0, width);
  }
  // code point A
  // set the prototypes
  CanvasRenderingContext2D.prototype.fillJustifyText = fillJustifyText;
  CanvasRenderingContext2D.prototype.strokeJustifyText = strokeJustifyText;
  CanvasRenderingContext2D.prototype.measureJustifiedText = measureJustifiedText;
  // code point B
  // optional code if you do not wish to extend the CanvasRenderingContext2D prototype
  /* Uncomment from here to the closing comment
  window.justifiedText = {
  fill : function(ctx, text, x, y, width, settings){
  justifiedTextSettings(settings);
  renderType = FILL;
  renderTextJustified(ctx, text, x, y, width);
  },
  stroke : function(ctx, text, x, y, width, settings){
  justifiedTextSettings(settings);
  renderType = STROKE;
  renderTextJustified(ctx, text, x, y, width);
  },
  measure : function(ctx, text, width, settings){
  justifiedTextSettings(settings);
  renderType = MEASURE;
  return renderTextJustified(ctx, text, 0, 0, width);
  }
  }
  to here*/
})();

 

Note A: If you do not wish to extend the CanvasRenderingContext2D prototype Remove from the example all code between // code point A and // code point B and uncomment the code marked /* Uncomment from here to the closing comment

How to use


Three functions are added to the CanvasRenderingContext2D and are available to all 2D context objects created.

  • ctx.fillJustifyText( text, x, y, width, [settings]);
  • ctx.strokeJustifyText( text, x, y, width, [settings]);
  • ctx.measureJustifiedText( text, width, [settings]);

Fill and stroke text function fill or stroke text and use the same arguments. measureJustifiedText will return the actual width that text would be rendered at. This may be equal, less, or greater than the argument width depending on current settings.

 

Function arguments


  • text: String containing the text to be rendered.
  • x, y: Coordinates to render the text at.
  • width: Width of the justified text. Text will increase/decrease spaces between words to fit the width. If the space between words is greater than maxSpaceSize (default = 6) times normal spacing will be used and the text will not fill the required width. If the spacing is less than minSpaceSize (default = 0.5) time normal spacing then the min space size is used and the text will overrun the width requested
  • settings: Optional. An object containing min and max space sizes.

The settings argument is optional and if not included text rendering will use the last setting defined or the default (shown below).

Both min and max are the min and max sizes for the [space] character separating words. The default maxSpaceSize = 6 means that when the space between characters is > 63 * ctx.measureText(" ").width text will not be justified. If text to be justified has spaces less than minSpaceSize = 0.5 (default value 0.5) * ctx.measureText(" ").width the spacing will be set to minSpaceSize * ctx.measureText(" ").width and the resulting text will overrun the justifying width.

The following rules are applied, min and max must be numbered. If not then the associate values will not be changed. If minSpaceSize is larger than maxSpaceSize both input settings are invalid and min-max will not be changed.

Example setting object with defaults

settings = {
maxSpaceSize : 6; // Multiplier for max space size.
minSpaceSize : 0.5; // Multiplier for minimum space size
};

NOTE: These text functions introduce a subtle behaviour change for the textAlign property of the 2D context. 'Left', 'right', 'center' and 'start' behave as is expected but 'end' will not align from the right of the function argument x but rather from the right of x + width

Note: settings (min and max space size) are global to all 2D context objects.

 

Text Rendering USAGE Examples


var i = 0;
text[i++] = "This text is aligned from the left of the canvas.";
text[i++] = "This text is near the max spacing size";
text[i++] = "This text is way too short.";
text[i++] = "This text is too long for the space provied and will overflow#";

text[i++] = "This text is aligned using 'end' and starts at x + width";
text[i++] = "This text is near the max spacing size";
text[i++] = "This text is way too short.";
text[i++] = "#This text is too long for the space provied and will overflow";
text[i++] = "This is aligned with 'center' and is placed from the center";
text[i++] = "This text is near the max spacing size";
text[i++] = "This text is way too short.";
text[i++] = "This text is just too long for the space provied and will overflow";
// ctx is the 2d context
// canvas is the canvas
ctx.clearRect(0, 0, w, h);
ctx.font = "25px arial";
ctx.textAlign = "center"
var left = 20;
var center = canvas.width / 2;
var width = canvas.width - left * 2;
var y = 40;
var size = 16;
var i = 0;
ctx.fillText("Justified text examples.", center, y);
y += 40;
ctx.font = "14px arial";
ctx.textAlign = "left"
var ww = ctx.measureJustifiedText(text[0], width);
var setting = {
  maxSpaceSize: 6,
  minSpaceSize: 0.5
}
ctx.strokeStyle = "red"
ctx.beginPath();
ctx.moveTo(left, y - size * 2);
ctx.lineTo(left, y + size * 15);
ctx.moveTo(canvas.width - left, y - size * 2);
ctx.lineTo(canvas.width - left, y + size * 15);
ctx.stroke();
ctx.textAlign = "left";
ctx.fillStyle = "red";
ctx.fillText("< 'left' aligned", left, y - size)
ctx.fillStyle = "black";
ctx.fillJustifyText(text[i++], left, y, width, setting); // settings is remembered
ctx.fillJustifyText(text[i++], left, y += size, width);
ctx.fillJustifyText(text[i++], left, y += size, width);
ctx.fillJustifyText(text[i++], left, y += size, width);
y += 2.3 * size;
ctx.fillStyle = "red";
ctx.fillText("< 'end' aligned from x plus the width -------------------->", left, y - size)
ctx.fillStyle = "black";
ctx.textAlign = "end";
ctx.fillJustifyText(text[i++], left, y, width);
ctx.fillJustifyText(text[i++], left, y += size, width);
ctx.fillJustifyText(text[i++], left, y += size, width);
ctx.fillJustifyText(text[i++], left, y += size, width);
y += 40;
ctx.strokeStyle = "red"
ctx.beginPath();
ctx.moveTo(center, y - size * 2);
ctx.lineTo(center, y + size * 5);
ctx.stroke();
ctx.textAlign = "center";

ctx.fillStyle = "red";
ctx.fillText("'center' aligned", center, y - size)
ctx.fillStyle = "black";
ctx.fillJustifyText(text[i++], center, y, width);
ctx.fillJustifyText(text[i++], center, y += size, width);
ctx.fillJustifyText(text[i++], center, y += size, width);
ctx.fillJustifyText(text[i++], center, y += size, width);

 

Text Rendering Justified paragraphs


Renders text as justified paragraphs. REQUIRES the example Justified text

The top paragraph has setting.compact = true and bottom false and line spacing is 1.2 rather than the default 1.5. Rendered by code usage example bottom of this example.

Example code

 

// Requires justified text extensions
(function()
  {
    // code point A
    if (typeof CanvasRenderingContext2D.prototype.fillJustifyText !== "function")
    {
      throw new ReferenceError("Justified Paragraph extension missing requiered
        CanvasRenderingContext2D justified text extension ");
      }
      var maxSpaceSize = 3; // Multiplier for max space size. If greater then no justificatoin applied
      var minSpaceSize = 0.5; // Multiplier for minimum space size
      var compact = true; // if true then try and fit as many words as possible. If false then try to
      get the spacing as close as possible to normal
      var lineSpacing = 1.5; // space between lines
      const noJustifySetting = { // This setting forces justified text off. Used to render last line
        of paragraph.
        minSpaceSize: 1,
        maxSpaceSize: 1,
      }
      // Parse vet and set settings object.
      var justifiedTextSettings = function(settings)
      {
        var min, max;
        var vetNumber = (num, defaultNum) =>
        {
          num = num !== null && num !== null && !isNaN(num) ? num : defaultNum;
          return num < 0 ? defaultNum : num;
        }
        if (settings === undefined || settings === null)
        {
          return;
        }
        compact = settings.compact === true ? true : settings.compact === false ? false : compact;
        max = vetNumber(settings.maxSpaceSize, maxSpaceSize);
        min = vetNumber(settings.minSpaceSize, minSpaceSize);
        lineSpacing = vetNumber(settings.lineSpacing, lineSpacing);
        if (min > max)
        {
          return;
        }
        minSpaceSize = min;
        maxSpaceSize = max;
      }
      var getFontSize = function(font)
      { // get the font size.
        var numFind = /[0-9]+/;
        var number = numFind.exec(font)[0];
        if (isNaN(number))
        {
          throw new ReferenceError("justifiedPar Cant find font size");
        }
        return Number(number);
      }

      function justifiedPar(ctx, text, x, y, width, settings, stroke)
      {
        var spaceWidth, minS, maxS, words, count, lines, lineWidth, lastLineWidth, lastSize, i,
          renderer, fontSize, adjSpace, spaces, word, lineWords, lineFound;
        spaceWidth = ctx.measureText(" ").width;
        minS = spaceWidth * minSpaceSize;
        maxS = spaceWidth * maxSpaceSize;
        words = text.split(" ").map(word =>
        { // measure all words.
          var w = ctx.measureText(word).width;
          return {
            width: w,
            word: word,
          };
        });
        // count = num words, spaces = number spaces, spaceWidth normal space size
        // adjSpace new space size >= min size. useSize Resulting space size used to render
        count = 0;
        lines = [];
        // create lines by shifting words from the words array until the spacing is optimal. If
        compact
        // true then will true and fit as many words as possible. Else it will try and get the
        spacing as
        // close as possible to the normal spacing
        while (words.length > 0)
        {
          lastLineWidth = 0;
          lastSize = -1;
          lineFound = false;
          // each line must have at least one word.
          word = words.shift();
          lineWidth = word.width;
          lineWords = [word.word];
          count = 0;
          while (lineWidth < width && words.length > 0)
          { // Add words to line
            word = words.shift();
            lineWidth += word.width;
            lineWords.push(word.word);
            count += 1;
            spaces = count - 1;
            adjSpace = (width - lineWidth) / spaces;
            if (minS > adjSpace)
            { // if spacing less than min remove last word and finish line
              lineFound = true;
              words.unshift(word);
              lineWords.pop();
            }
            else
            {
              if (!compact)
              { // if compact mode
                if (adjSpace < spaceWidth)
                { // if less than normal space width
                  if (lastSize === -1)
                  {
                    lastSize = adjSpace;
                  }
                  // check if with last word on if its closer to space width
                  if (Math.abs(spaceWidth - adjSpace) < Math.abs(spaceWidth - lastSize))
                  {
                    lineFound = true; // yes keep it
                  }
                  else
                  {
                    words.unshift(word); // no better fit if last word removes
                    lineWords.pop();
                    lineFound = true;
                  }
                }
              }
            }
            lastSize = adjSpace; // remember spacing
          }
          lines.push(lineWords.join(" ")); // and the line
        }
        // lines have been worked out get font size, render, and render all the lines. last
        // line may need to be rendered as normal so it is outside the loop.
        fontSize = getFontSize(ctx.font);
        renderer = stroke === true ? ctx.strokeJustifyText.bind(ctx) :
          ctx.fillJustifyText.bind(ctx);
        for (i = 0; i < lines.length - 1; i++)
        {
          renderer(lines[i], x, y, width, settings);
          y += lineSpacing * fontSize;
        }
        if (lines.length > 0)
        { // last line if left or start aligned for no justify
          if (ctx.textAlign === "left" || ctx.textAlign === "start")
          {
            renderer(lines[lines.length - 1], x, y, width, noJustifySetting);
            ctx.measureJustifiedText("", width, settings);
          }
          else
          {
            renderer(lines[lines.length - 1], x, y, width);
          }
        }
        // return details about the paragraph.
        y += lineSpacing * fontSize;
        return {
          nextLine: y,
          fontSize: fontSize,
          lineHeight: lineSpacing * fontSize,
        };
      }
      // define fill
      var fillParagraphText = function(text, x, y, width, settings)
      {
        justifiedTextSettings(settings);
        settings = {
          minSpaceSize: minSpaceSize,
          maxSpaceSize: maxSpaceSize,
        };
        return justifiedPar(this, text, x, y, width, settings);
      }
      // define stroke
      var strokeParagraphText = function(text, x, y, width, settings)
      {
        justifiedTextSettings(settings);
        settings = {
          minSpaceSize: minSpaceSize,
          maxSpaceSize: maxSpaceSize,
        };
        return justifiedPar(this, text, x, y, width, settings, true);
      }
      CanvasRenderingContext2D.prototype.fillParaText = fillParagraphText;
      CanvasRenderingContext2D.prototype.strokeParaText = strokeParagraphText;
    })();

NOTE this extends the CanvasRenderingContext2D prototype. If you do not wish this to happen use the example Justified text to work out how to change this example to be part of the global namespace.
NOTE Will throw a ReferenceError if this example can not find the function CanvasRenderingContext2D.prototype.fillJustifyText

 

How to use


ctx.fillParaText(text, x, y, width, [settings]);
ctx.strokeParaText(text, x, y, width, [settings]);

See the Justified text for details on arguments. Arguments between [ and ] are optional.

The settings argument has two additional properties.

  • compact: Default true. If true tries to pack as many words as possible per line. If false the tries to get the word spacing as close as possible to normal spacing.
  • line spacing Default 1.5. Space per line default 1.5 the distance from one line to the next in terms of font size

Properties missing from the settings object will default to their default values or to the last valid values. The properties will only be changed if the new values are valid. For compact valid values are only booleans true or false Truthy values are not considered valid.

Return object

The two functions return an object containing information to help you place the next paragraph. The object contains the following properties.

  • next line Position of the next line after the paragraph pixels.
  • font size Size of the font. (please note only use fonts defined in pixels eg 14px arial)
  • line-height Distance in pixels from one line to the next

This example uses a simple algorithm that works one line at to time to find the best fit for a paragraph. This does not mean that it the best fit (rather the algorithm's best) You may wish to improve the algorithm by creating a multi-pass line algorithm over the generated lines. Moving words from the end of one line to the start of the next, or from the start back to the end. The best look is achieved when the spacing over the entire paragraph has the smallest variation and is the closest to the normal text spacing.

As this example is dependent on the Justified text example the code is very similar. You may wish to move the two into one function. Replace the function justifiedTextSettings in the other example with the one used in this example. Then copy all the rest of the code from this example into the anonymous function body of the Justified text example. You will no longer need to test for dependencies found at // Code point A It can be removed.

 

Usage example


ctx.font = "25px arial";
ctx.textAlign = "center"
var left = 10;
var center = canvas.width / 2;
var width = canvas.width - left * 2;
var y = 20;
var size = 16;
var i = 0;
ctx.fillText("Justified paragraph examples.", center, y);
y += 30;
ctx.font = "14px arial";
ctx.textAlign = "left"
// set para settings
var setting = {
  maxSpaceSize: 6,
  minSpaceSize: 0.5,
  lineSpacing: 1.2,
  compact: true,
}
// Show the left and right bounds.
ctx.strokeStyle = "red"
ctx.beginPath();
ctx.moveTo(left, y - size * 2);
ctx.lineTo(left, y + size * 15);
ctx.moveTo(canvas.width - left, y - size * 2);
ctx.lineTo(canvas.width - left, y + size * 15);
ctx.stroke();
ctx.textAlign = "left";
ctx.fillStyle = "black";
// Draw paragraph
var line = ctx.fillParaText(para, left, y, width, setting); // settings is remembered
// Next paragraph
y = line.nextLine + line.lineHeight;
setting.compact = false;
ctx.fillParaText(para, left, y, width, setting);

Note: For text aligned left or start the last line of tha paragraph will always have normal spacing. For all other alignments, the last line is treated like all others.

Note: You can inset the start of the paragraph with spaces. Though this may not be consistent from paragraph to paragraph. It is always a good thing to learn what a function is doing and modifying it. An exercise would be to add a setting to the settings that indent the first line by a fixed amount. Hint the while loop will need to temporarily make the first word appear larger (+ indent) words[0]. width += ? and then when rendering lines indent the first line.

 

Rendering text along an arc


This example shows how to render text along an arc. It includes how you can add functionality to the CanvasRenderingContext2D by extending its prototype.

Example code

The example adds 3 new text rendering functions to the 2D context prototype.

  • ctx.fillCircleText(text, x, y, radius, start, end, forward);
  • ctx.strokeCircleText(text, x, y, radius, start, end, forward);
  • ctx.measureCircleText(text, radius);

(function()
  {
    const FILL = 0; // const to indicate filltext render
    const STROKE = 1;
    var renderType = FILL; // used internal to set fill or stroke text
    const multiplyCurrentTransform = true; // if true Use current transform when rendering
    // if false use absolute coordinates which is a little
    quicker
    // after render the currentTransform is restored to
    default transform
    // measure circle text
    // ctx: canvas context
    // text: string of text to measure
    // r: radius in pixels
    //
    // returns the size metrics of the text
    //
    // width: Pixel width of text
    // angularWidth : angular width of text in radians
    // pixelAngularSize : angular width of a pixel in radians
    var measure = function(ctx, text, radius)
    {
      var textWidth = ctx.measureText(text).width; // get the width of all the text
      return {
        width: textWidth,
        angularWidth: (1 / radius) * textWidth,
        pixelAngularSize: 1 / radius
      };
    }
    // displays text along a circle
    // ctx: canvas context
    // text: string of text to measure
    // x,y: position of circle center
    // r: radius of circle in pixels
    // start: angle in radians to start.
    // [end]: optional. If included text align is ignored and the text is
    // scaled to fit between start and end;
    // [forward]: optional default true. if true text direction is forwards, if false direction is
    backward
    var circleText = function(ctx, text, x, y, radius, start, end, forward)
    {
      var i, textWidth, pA, pAS, a, aw, wScale, aligned, dir, fontSize;
      if (text.trim() === "" || ctx.globalAlpha === 0)
      { // don't render empty string or
        transparent
        return;
      }
      if (isNaN(x) || isNaN(y) || isNaN(radius) || isNaN(start) || (end !== undefined && end !==
          null && isNaN(end)))
      { //
        throw TypeError("circle text arguments requires a number for x,y, radius, start, and
          end.
          ")
        }
        aligned = ctx.textAlign; // save the current textAlign so that it can be restored at
        end
        dir = forward ? 1 : forward === false ? -1 : 1; // set dir if not true or false set
        forward as true
        pAS = 1 / radius; // get the angular size of a pixel in radians
        textWidth = ctx.measureText(text).width; // get the width of all the text
        if (end !== undefined && end !== null)
        { // if end is supplied then fit text between start
          and end
          pA = ((end - start) / textWidth) * dir;
          wScale = (pA / pAS) * dir;
        }
        else
        { // if no end is supplied correct start and end for alignment
          // if forward is not given then swap top of circle text to read the correct direction
          if (forward === null || forward === undefined)
          {
            if (((start % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2) > Math.PI)
            {
              dir = -1;
            }
          }
          pA = -pAS * dir;
          wScale = -1 * dir;
          switch (aligned)
          {
            case "center": // if centered move around half width
              start -= (pA * textWidth) / 2;
              end = start + pA * textWidth;
              break;
            case "right": // intentionally falls through to case "end"
            case "end":
              end = start;
              start -= pA * textWidth;
              break;
            case "left": // intentionally falls through to case "start"
            case "start":
              end = start + pA * textWidth;
          }
        }
        ctx.textAlign = "center"; // align for rendering
        a = start; // set the start angle
        for (var i = 0; i < text.length; i += 1)
        { // for each character
          aw = ctx.measureText(text[i]).width * pA; // get the angular width of the text
          var xDx = Math.cos(a + aw / 2); // get the yAxies vector from the center x,y
          out
          var xDy = Math.sin(a + aw / 2);
          if (multiplyCurrentTransform)
          { // transform multiplying current transform
            ctx.save();
            if (xDy < 0)
            { // is the text upside down. If it is flip it
              ctx.transform(-xDy * wScale, xDx * wScale, -xDx, -xDy, xDx * radius + x, xDy *
                radius + y);
            }
            else
            {
              ctx.transform(-xDy * wScale, xDx * wScale, xDx, xDy, xDx * radius + x, xDy *
                radius + y);
            }
          }
          else
          {
            if (xDy < 0)
            { // is the text upside down. If it is flip it
              ctx.setTransform(-xDy * wScale, xDx * wScale, -xDx, -xDy, xDx * radius + x, xDy *
                radius + y);
            }
            else
            {
              ctx.setTransform(-xDy * wScale, xDx * wScale, xDx, xDy, xDx * radius + x, xDy *
                radius + y);
            }
          }
          if (renderType === FILL)
          {
            ctx.fillText(text[i], 0, 0); // render the character
          }
          else
          {
            ctx.strokeText(text[i], 0, 0); // render the character
          }
          if (multiplyCurrentTransform)
          { // restore current transform
            ctx.restore();
          }
          a += aw; // step to the next angle
        }
        // all done clean up.
        if (!multiplyCurrentTransform)
        {
          ctx.setTransform(1, 0, 0, 1, 0, 0); // restore the transform
        }
        ctx.textAlign = aligned; // restore the text alignment
      }
      // define fill text
      var fillCircleText = function(text, x, y, radius, start, end, forward)
      {
        renderType = FILL;
        circleText(this, text, x, y, radius, start, end, forward);
      }
      // define stroke text
      var strokeCircleText = function(text, x, y, radius, start, end, forward)
      {
        renderType = STROKE;
        circleText(this, text, x, y, radius, start, end, forward);
      }
      // define measure text
      var measureCircleTextExt = function(text, radius)
      {
        return measure(this, text, radius);
      }
      // set the prototypes
      CanvasRenderingContext2D.prototype.fillCircleText = fillCircleText;
      CanvasRenderingContext2D.prototype.strokeCircleText = strokeCircleText;
      CanvasRenderingContext2D.prototype.measureCircleText = measureCircleTextExt;
    })();

 

Function descriptions


This example adds 3 functions to the CanvasRenderingContext2D prototype. fillCircleText, strokeCircleText, and measureCircleText

CanvasRenderingContext2D.fillCircleText(text, x, y, radius, start, [end, [forward]]);

CanvasRenderingContext2D.strokeCircleText(text, x, y, radius, start, [end, [forward]]);

  • text: Text to render as String.
  • x,y: Position of circle center as Numbers.
  • radius: radius of circle in pixels
  • start: angle in radians to start.
  • [end]: optional. If included ctx.textAlign is ignored and the text is scaled to fit between start and end.
  • [forward]: optional default 'true'. if true text direction is forwards, if 'false' direction is backward.

Both functions use the text baseline to position the text vertically around the radius. For the best results use ctx.TextBaseline. Functions will throw a TypeError is any of the numerical arguments as NaN.

If the text argument trims to an empty string or ctx.globalAlpha = 0 the function just drops through and does
nothing.

CanvasRenderingContext2D.measureCircleText(text, radius);

  • - **text:** String of text to measure.
  • - **radius:** radius of circle in pixels.

Returns a Object containing various size metrics for rendering circular text

  • - **width:** Pixel width of text as it would normaly be rendered
  • - **angularWidth:** angular width of text in radians.
  • - **pixelAngularSize:** angular width of a pixel in radians.

 

Usage examples


const rad = canvas.height * 0.4;
const text = "Hello circle TEXT!";
const fontSize = 40;
const centX = canvas.width / 2;
const centY = canvas.height / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.font = fontSize + "px verdana";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillStyle = "#000";
ctx.strokeStyle = "#666";
// Text under stretched from Math.PI to 0 (180 - 0 deg)
ctx.fillCircleText(text, centX, centY, rad, Math.PI, 0);
// text over top centered at Math.PI * 1.5 ( 270 deg)
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5);
// text under top centered at Math.PI * 1.5 ( 270 deg)
ctx.textBaseline = "top";
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5); // text over top centered at Math.PI * 1.5 ( 270 deg)
ctx.textBaseline = "middle";
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5);
// Use measureCircleText to get angular size
var circleTextMetric = ctx.measureCircleText("Text to measure", rad);
console.log(circleTextMetric.width); // width of text if rendered normally
console.log(circleTextMetric.angularWidth); // angular width of text
console.log(circleTextMetric.pixelAngularSize); // angular size of a pixel
// Use measure text to draw a arc around the text
ctx.textBaseline = "middle";
var width = ctx.measureCircleText(text, rad).angularWidth;
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5);
// render the arc around the text
ctx.strokeStyle = "red";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(centX, centY, rad + fontSize / 2, Math.PI * 1.5 - width / 2, Math.PI * 1.5 + width / 2);
ctx.arc(centX, centY, rad - fontSize / 2, Math.PI * 1.5 + width / 2, Math.PI * 1.5 - width / 2, true);
ctx.closePath();
ctx.stroke();

NOTE: The text rendered is only an approximation of circular text. For example, if two l's are rendered the two lines will not be parallel, but if you render a "H" the two edges will be parallel. This is because each character is rendered as close as possible to the required direction, rather than each pixel being correctly transformed to create circular text.

NOTE: const multiplyCurrentTransform = true; defined in this example is used to set the transformation method used. If false the transformation for circular text rendering is absolute and does not depend on the current transformation state. The text will not be affected by any previous scale, rotate, or translate transforms.

This will increase the performance of the render function after the function is called the transform will be set to the default setTransform(1,0,0,1,0,0) If multiplyCurrentTransform = true (set as default in this example) the text will use the current transform so that the text can be scaled translated, skewed, rotated, etc but modifying the current transform before calling the fillCircleText and strokeCircleText functions. Depending on the current state of the 2D context this may be somewhat slower than multiplyCurrentTransform = false

ATutorialHub Related Guide

Comments (8)

Leave a Comment

Your email address will not be published. Required fields are marked*

User Comments

html tutorial comments

panduranga gupta

2021-07-05 07:03:13

good website for learning and help me a lot

html tutorial comments

raju

2021-09-25 14:58:47

The awsome website i am looking like for a long time, good work atutorialhub team keep doing

html tutorial comments

Shivani

2021-09-01 15:03:56

Learning a lot from the courses present on atutorialhub. The courses are very well explained. Great experience

html tutorial comments

Harshitha

2021-09-10 15:05:45

It is very helpful to students and easy to learn the concepts

html tutorial comments

Sowmya

2021-09-14 15:06:41

Great job Tutorials are easy to understand Please make use of it

html tutorial comments

Zain Khan

2021-09-18 15:07:23

Great content and customized courses.

html tutorial comments

Rudrakshi Bhatt

2021-09-09 15:08:10

Well structured coursed and explained really well!

html tutorial comments

Pavana Somashekar

2021-09-11 15:09:08

Good platform for beginners and learn a lot on this website