Source: Font.js

//////////////////////////////////////////////////////////////////////////////
// Font
//////////////////////////////////////////////////////////////////////////////

import Events from './Events';

/**
 * The *Font* class stores the font internally as a CSS font string but makes it
 * easy to change individual components of the font. For example, the size can be
 * changed using the [size]{@link Font#size} method. A font consists of 3 components:
 *
 *   Component   | Description
 *   ------------|---------------
 *   *family*    | This can be a generic family (e.g. serif, sans-serif, monospace) or a specific font family (e.g. Times New Roman, Arial, or Courier)
 *   *style*     | One of *plain*, *bold*, *italic*, or *bold-italic*
 *   *size*      | The size of the font in pixels. The size will be adjusted for retina displays.
 *
 */
// See _generateFont() below for where Events is used
class Font extends Events  {
// class Font {

  /**
   * Create a new *Font*. The *Font* can be created using a string or an object representing the font.
   *
   * @param {(String|Object)} font - If a string is provided, it must have the following format:
   *   family,style,size (e.g. 'serif,plain,12'). If an object is provided, it must have a *family*,
   *   *style* and *size* property (e.g. { family: 'serif', style: 'plain', size: 12 })
   */
  constructor(font) {
    super();
    this._rawFont = font;
  }

  /**
   * Return the class name as a string.
   * @return {String} - 'Font'
   */
  toString() {
    return 'Font';
  }

  set _rawFont(font) {
    if (typeof font === 'string' || font instanceof String) {
      this.string = font;
    } else {
      const keys = Object.keys(font);
      if (keys.includes('family') && keys.includes('style') && keys.includes('size')) {
        this._family = font.family;
        this._style = font.style;
        this._size = Number(font.size);
        this._generateFont();
      } else {
        console.log('Font objects require the following keys: family, style, and size');
      }
    }
  }

  /**
   * @member {String} - Get or set the font using a simple string format: family,style,size (e.g. 'serif,plain,12').
   */
  get string() {
    return `${this.family},${this.style},${this.size}`;
  }

  set string(value) {
    value = value.replace(/ +/g, '');
    const parts = value.split(',');
    if (parts.length === 3) {
      this._family = parts[0];
      this._style = parts[1];
      this._size = Number(parts[2]);
    } else {
      console.log('Font must have 3 parts');
    }
    this._generateFont();
  }

  /**
   * @member {String} - Return the font as CSS usable string. This is also how the font is stored internally for quick access.
   */
  get css() {
    return this._font;
  }

  /**
   * Return the font as a CSS string with the size first scaled by multiplying by the *scale* factor.
   * @param {Number} scale - Scale factor.
   * @return {String} - Return the font as CSS usable string.
   * @private
   */
  cssScaled(scale) {
    if (scale && scale !== 1) {
      return `${this._styleAsCss()} ${this.size * scale}px ${this.family}`;
    } else {
      return this.css;
    }
  }


  /**
   * @member {String} - Get or set the font family. Defaults to *sans-serif*.
   */
  get family() {
    return this._family || 'sans-serif';
  }

  set family(value) {
    this._family = value;
    this._generateFont();
  }

  /**
   * @member {Number} - Get or set the font size. The size is stored as a number and is in pixels.
   * The actual value may be altered when setting it to take into account the pixel
   * ratio of the screen. Defaults to *12*.
   */
  get size() {
    // return this._size || CGV.pixel(12)
    return this._size || 12;
  }

  set size(value) {
    // this._size = CGV.pixel(Number(value));
    this._size = Number(value);
    this._generateFont();
  }

  /**
   * @member {String} - Get or set the font style. The possible values are *plain*, *bold*, *italic* and
   * *bold-italic*. Defaults to *plain*.
   */
  get style() {
    return this._style || 'plain';
  }

  set style(value) {
    this._style = value;
    this._generateFont();
  }

  /**
   * @member {Boolean} - Get or set the font boldness.
   */
  get bold() {
    return ( this.style === 'bold' || this.style === 'bold-italic');
  }

  set bold(value) {
    if (value) {
      if (this.style === 'plain') {
        this.style = 'bold';
      } else if (this.style === 'italic') {
        this.style = 'bold-italic';
      }
    } else {
      if (this.style === 'bold') {
        this.style = 'plain';
      } else if (this.style === 'bold-italic') {
        this.style = 'italic';
      }
    }
  }

  /**
   * @member {Boolean} - Get or set the font italics.
   */
  get italic() {
    return ( this.style === 'italic' || this.style === 'bold-italic');
  }

  set italic(value) {
    if (value) {
      if (this.style === 'plain') {
        this.style = 'italic';
      } else if (this.style === 'bold') {
        this.style = 'bold-italic';
      }
    } else {
      if (this.style === 'italic') {
        this.style = 'plain';
      } else if (this.style === 'bold-italic') {
        this.style = 'bold';
      }
    }
  }

  /**
   * @member {Number} - Get the font height. This will be the same as the font [size]{@link Font#size}.
   */
  get height() {
    return this.size;
  }


  /**
   * Measure the width of the supplied *text* using the *context* and the *Font* settings.
   *
   * @param {Context} context - The canvas context to use to measure the width.
   * @param {String} text - The text to measure.
   * @return {Number} - The width of the *text* in pixels.
   */
  width(ctx, text) {
    ctx.font = this.css;
    return ctx.measureText(text).width;
  }

  copy() {
    return new Font(this.string);
  }

  _styleAsCss() {
    if (this.style === 'plain') {
      return 'normal';
    } else if (this.style === 'bold') {
      return 'bold';
    } else if (this.style === 'italic') {
      return 'italic';
    } else if (this.style === 'bold-italic') {
      return 'italic bold';
    } else {
      return '';
    }
  }

  _generateFont() {
    this._font = `${this._styleAsCss()} ${this.size}px ${this.family}`;
    // Is this needed OR can we use the various update events...
    // Currently used by Annotation to update the font widths if any aspect of the font changes
    this.trigger('change', this);
  }

}

/**
 * Calculate the width of multiple *strings* using the supplied *fonts* and *context*.
 * This method minimizes the number of times the context font is changed to speed up
 * the calculations
 * @function calculateWidths
 * @memberof Font
 * @static
 * @param {Context} ctx - The context to use for measurements.
 * @param {Font[]} fonts - An array of fonts. Must be the same length as *strings*.
 * @param {String[]} strings - An array of strings. Must be the same length as *fonts*.
 * @return {Number[]} - An array of widths.
 * @private
 */
Font.calculateWidths = function(ctx, fonts, strings) {
  ctx.save();
  const widths = [];
  const map = [];

  for (let i = 0, len = fonts.length; i < len; i++) {
    map.push({
      index: i,
      font: fonts[i],
      text: strings[i]
    });
  }

  map.sort( (a, b) => {
    return a.font > b.font ? 1 : -1;
  });

  let currentFont = '';
  let font, text;
  for (let i = 0, len = map.length; i < len; i++) {
    font = map[i].font;
    text = map[i].text;
    if (font !== currentFont) {
      ctx.font = font;
      currentFont = font;
    }
    // widths[i] = ctx.measureText(text).width;
    widths[map[i].index] = ctx.measureText(text).width;
  }
  ctx.restore();
  return widths;
};

export default Font;