Source: tiny/core/text/TextMetrics.js

/**
 * The TextMetrics object represents the measurement of a block of text with a specified style.
 *
 * @class
 * @memberOf Tiny
 */
export default class TextMetrics {
  /**
   * @param {string} text - the text that was measured
   * @param {Tiny.TextStyle} style - the style that was measured
   * @param {number} width - the measured width of the text
   * @param {number} height - the measured height of the text
   * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style
   * @param {array} lineWidths - an array of the line widths for each line matched to `lines`
   * @param {number} lineHeight - the measured line height for this style
   * @param {number} maxLineWidth - the maximum line width for all measured lines
   * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont
   */
  constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) {
    this.text = text;
    this.style = style;
    this.width = width;
    this.height = height;
    this.lines = lines;
    this.lineWidths = lineWidths;
    this.lineHeight = lineHeight;
    this.maxLineWidth = maxLineWidth;
    this.fontProperties = fontProperties;
  }

  /**
   * Measures the supplied string of text and returns a Rectangle.
   *
   * @param {string} text - the text to measure.
   * @param {Tiny.TextStyle} style - the text style to use for measuring
   * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text.
   * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring.
   * @return {Tiny.TextMetrics} measured width and height of the text.
   */
  static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) {
    wordWrap = wordWrap || style.wordWrap;
    const font = style.toFontString();
    const fontProperties = TextMetrics.measureFont(font);
    const context = canvas.getContext('2d');

    context.font = font;

    const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text;
    const lines = outputText.split(/(?:\r\n|\r|\n)/);
    const lineWidths = new Array(lines.length);
    let maxLineWidth = 0;

    for (let i = 0; i < lines.length; i++) {
      const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing);

      lineWidths[i] = lineWidth;
      maxLineWidth = Math.max(maxLineWidth, lineWidth);
    }
    let width = maxLineWidth + style.strokeThickness;

    if (style.dropShadow) {
      width += style.dropShadowDistance;
    }

    const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness;
    let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + ((lines.length - 1) * lineHeight);

    if (style.dropShadow) {
      height += style.dropShadowDistance;
    }

    return new TextMetrics(
      text,
      style,
      width,
      height,
      lines,
      lineWidths,
      lineHeight,
      maxLineWidth,
      fontProperties
    );
  }

  /**
   * Applies newlines to a string to have it optimally fit into the horizontal
   * bounds set by the Text object's wordWrapWidth property.
   *
   * @private
   * @param {string} text - String to apply word wrapping to
   * @param {Tiny.TextStyle} style - the style to use when wrapping
   * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring.
   * @return {string} New string with new lines applied where required
   */
  static wordWrap(text, style, canvas = TextMetrics._canvas) {
    const context = canvas.getContext('2d');

    // Greedy wrapping algorithm that will wrap words as the line grows longer
    // than its horizontal bounds.
    let result = '';
    const lines = text.split('\n');
    const wordWrapWidth = style.wordWrapWidth;
    const characterCache = {};

    for (let i = 0; i < lines.length; i++) {
      let spaceLeft = wordWrapWidth;
      const words = lines[i].split(' ');

      for (let j = 0; j < words.length; j++) {
        const wordWidth = context.measureText(words[j]).width;

        if (style.breakWords && wordWidth > wordWrapWidth) {
          // Word should be split in the middle
          const characters = words[j].split('');

          for (let c = 0; c < characters.length; c++) {
            const character = characters[c];
            let characterWidth = characterCache[character];

            if (characterWidth === undefined) {
              characterWidth = context.measureText(character).width;
              characterCache[character] = characterWidth;
            }

            if (characterWidth > spaceLeft) {
              result += `\n${character}`;
              spaceLeft = wordWrapWidth - characterWidth;
            } else {
              if (c === 0) {
                result += ' ';
              }

              result += character;
              spaceLeft -= characterWidth;
            }
          }
        } else {
          const wordWidthWithSpace = wordWidth + context.measureText(' ').width;

          if (j === 0 || wordWidthWithSpace > spaceLeft) {
            // Skip printing the newline if it's the first word of the line that is
            // greater than the word wrap width.
            if (j > 0) {
              result += '\n';
            }
            result += words[j];
            spaceLeft = wordWrapWidth - wordWidth;
          } else {
            spaceLeft -= wordWidthWithSpace;
            result += ` ${words[j]}`;
          }
        }
      }

      if (i < lines.length - 1) {
        result += '\n';
      }
    }

    return result;
  }

  /**
   * Calculates the ascent, descent and fontSize of a given font-style
   *
   * @static
   * @param {string} font - String representing the style of the font
   * @return {Tiny.TextMetrics~FontMetrics} Font properties object
   */
  static measureFont(font) {
    // as this method is used for preparing assets, don't recalculate things if we don't need to
    if (TextMetrics._fonts[font]) {
      return TextMetrics._fonts[font];
    }

    const properties = {};

    const canvas = TextMetrics._canvas;
    const context = TextMetrics._context;

    context.font = font;

    const width = Math.ceil(context.measureText('|MÉq').width);
    let baseline = Math.ceil(context.measureText('M').width);
    const height = 2 * baseline;

    baseline = baseline * 1.4 | 0;

    canvas.width = width;
    canvas.height = height;

    context.fillStyle = '#f00';
    context.fillRect(0, 0, width, height);

    context.font = font;

    context.textBaseline = 'alphabetic';
    context.fillStyle = '#000';
    context.fillText('|MÉq', 0, baseline);

    const imagedata = context.getImageData(0, 0, width, height).data;
    const pixels = imagedata.length;
    const line = width * 4;

    let i = 0;
    let idx = 0;
    let stop = false;

    // ascent. scan from top to bottom until we find a non red pixel
    for (i = 0; i < baseline; ++i) {
      for (let j = 0; j < line; j += 4) {
        if (imagedata[idx + j] !== 255) {
          stop = true;
          break;
        }
      }
      if (!stop) {
        idx += line;
      } else {
        break;
      }
    }

    properties.ascent = baseline - i;

    idx = pixels - line;
    stop = false;

    // descent. scan from bottom to top until we find a non red pixel
    for (i = height; i > baseline; --i) {
      for (let j = 0; j < line; j += 4) {
        if (imagedata[idx + j] !== 255) {
          stop = true;
          break;
        }
      }

      if (!stop) {
        idx -= line;
      } else {
        break;
      }
    }

    properties.descent = i - baseline;
    properties.fontSize = properties.ascent + properties.descent;

    TextMetrics._fonts[font] = properties;

    return properties;
  }
}

/**
 * Internal return object for {@link Tiny.TextMetrics.measureFont `TextMetrics.measureFont`}.
 * @class FontMetrics
 * @memberof Tiny.TextMetrics~
 * @property {number} ascent - The ascent distance
 * @property {number} descent - The descent distance
 * @property {number} fontSize - Font size from ascent to descent
 */

const canvas = document.createElement('canvas');

canvas.width = canvas.height = 10;

/**
 * Cached canvas element for measuring text
 * @memberof Tiny.TextMetrics
 * @type {HTMLCanvasElement}
 * @private
 */
TextMetrics._canvas = canvas;

/**
 * Cache for context to use.
 * @memberof Tiny.TextMetrics
 * @type {CanvasRenderingContext2D}
 * @private
 */
TextMetrics._context = canvas.getContext('2d');

/**
 * Cache of Tiny.TextMetrics~FontMetrics objects.
 * @memberof Tiny.TextMetrics
 * @type {Object}
 * @private
 */
TextMetrics._fonts = {};
Documentation generated by JSDoc 3.4.3 on Thu May 31 2018 14:40:21 GMT+0800 (CST)