API Documentation for: 1.0.0
Show:

File:FontLoader.js

/*
 * FontLoader
 * Visit http://createjs.com/ for documentation, updates and examples.
 *
 *
 * Copyright (c) 2012 gskinner.com, inc.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * @module PreloadJS
 */

// namespace:
this.createjs = this.createjs || {};

(function () {
	"use strict";

// constructor:
	/**
	 * A loader that handles font files, CSS definitions, and CSS paths. FontLoader doesn't actually preload fonts
	 * themselves, but rather generates CSS definitions, and then tests the size changes on an HTML5 Canvas element.
	 *
	 * Note that FontLoader does not support tag-based loading due to the requirement that CSS be read to determine the
	 * font definitions to test for.
	 * @class FontLoader
	 * @param {LoadItem|object|string} loadItem The item to be loaded.
	 * @extends AbstractLoader
	 * @constructor
	 **/
	function FontLoader(loadItem, preferXHR) {
		this.AbstractLoader_constructor(loadItem, preferXHR, loadItem.type);

		// private properties:
		/**
		 * A lookup of font faces to load.
		 * @property _faces
		 * @protected
		 * @type Object
		 **/
		this._faces = {};

		/**
		 * A list of font faces currently being "watched". Watched fonts will be tested on a regular interval, and be
		 * removed from this list when they are complete.
		 * @oroperty _watched
		 * @type {Array}
		 * @protected
		 */
		this._watched = [];

		/**
		 * A count of the total font faces to load.
		 * @property _count
		 * @type {number}
		 * @protected
		 * @default 0
		 */
		this._count = 0;

		/**
		 * The interval for checking if fonts have been loaded.
		 * @property _watchInterval
		 * @type {Number}
		 * @protected
		 */
		this._watchInterval = null;

		/**
		 * The timeout for determining if a font can't be loaded. Uses the LoadItem {{#crossLink "LoadImte/timeout:property"}}{{/crossLink}}
		 * value.
		 * @property _loadTimeout
		 * @type {Number}
		 * @protected
		 */
		this._loadTimeout = null;
		/**
		 * Determines if generated CSS should be injected into the document.
		 * @property _injectCSS
		 * @type {boolean}
		 * @protected
		 */
		this._injectCSS = (loadItem.injectCSS === undefined) ? true : loadItem.injectCSS;

		this.dispatchEvent("initialize");
	}
	var p = createjs.extend(FontLoader, createjs.AbstractLoader);
    
    /**
     * Determines if the loader can load a specific item. This loader can only load items that are of type
     * {{#crossLink "Types/FONT:property"}}{{/crossLink}}.
     * @method canLoadItem
     * @param {LoadItem|Object} item The LoadItem that a LoadQueue is trying to load.
     * @returns {Boolean} Whether the loader can load the item.
     * @static
     */
    FontLoader.canLoadItem = function (item) {
        return item.type == createjs.Types.FONT || item.type == createjs.Types.FONTCSS;
    };

// static properties:
	/**
	 * Sample text used by the FontLoader to determine if the font has been loaded. The sample text size is compared
	 * to the loaded font size, and a change indicates that the font has completed.
	 * @property sampleText
	 * @type {String}
	 * @default abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ
	 * @static
	 * @private
	 */
	FontLoader.sampleText = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ";

	/**
	 * The canvas context used to test the font size. Note that this currently requires an HTML DOM.
	 * @property _ctx
	 * @type {CanvasRenderingContext2D}
	 * @static
	 * @private
	 */
	FontLoader._ctx = document.createElement("canvas").getContext("2d"); // TODO: Consider a method to do this like EaselJS Stage has.

	/**
	 * A list of reference fonts to test. Multiple faces are tested to address the rare case of a loaded font being the
	 * exact same dimensions as the test font.
	 * @property _referenceFonts
	 * @type {Array}
	 * @default ["serif", "monospace"]
	 * @private
	 */
	FontLoader._referenceFonts = ["serif","monospace"];

	/**
	 * A regular expression that pulls out possible style values from the font name.
	 * <ul>
	 *     <li>This includes font names that include thin, normal, book, regular, medium, black, and heavy (such as
	 *     "Arial Black")</li>
	 *     <li>Weight modifiers including extra, ultra, semi, demi, light, and bold (such as "WorkSans SemiBold")</li>
	 * </ul>
	 *
	 * Weight descriptions map to font weight values by default using the following (from
	 * http://www.w3.org/TR/css3-fonts/#font-weight-numeric-values):
	 * <ul>
	 *     <li>100 - Thin</li>
	 * 	   <li>200 - Extra Light, Ultra Light</li>
	 *     <li>300 - Light, Semi Light, Demi Light</li>
	 *     <li>400 - Normal, Book, Regular</li>
	 *     <li>500 - Medium</li>
	 *     <li>600 - Semi Bold, Demi Bold</li>
	 *     <li>700 - Bold</li>
	 *     <li>800 - Extra Bold, Ultra Bold</li>
	 *     <li>900 - Black, Heavy</li>
	 * </ul>
	 * @property WEIGHT_REGEX
	 * @type {RegExp}
	 * @static
	 */
	FontLoader.WEIGHT_REGEX = /[- ._]*(thin|normal|book|regular|medium|black|heavy|[1-9]00|(?:extra|ultra|semi|demi)?[- ._]*(?:light|bold))[- ._]*/ig;

	/**
	 * A regular expression that pulls out possible style values from the font name. These include "italic"
	 * and "oblique".
	 * @property STYLE_REGEX
	 * @type {RegExp}
	 * @static
	 */
	FontLoader.STYLE_REGEX = /[- ._]*(italic|oblique)[- ._]*/ig;

	/**
	 * A lookup of font types for generating a CSS definition. For example, TTF fonts requires a "truetype" type.
	 * @property FONT_FORMAT
	 * @type {Object}
	 * @static
	 */
	FontLoader.FONT_FORMAT = {woff2:"woff2", woff:"woff", ttf:"truetype", otf:"truetype"};

	/**
	 * A lookup of font weights based on a name. These values are from http://www.w3.org/TR/css3-fonts/#font-weight-numeric-values.
	 * @property FONT_WEIGHT
	 * @type {Object}
	 * @static
	 */
	FontLoader.FONT_WEIGHT = {thin:100, extralight:200, ultralight:200, light:300, semilight:300, demilight:300, book:"normal", regular:"normal", semibold:600, demibold:600, extrabold:800, ultrabold:800, black:900, heavy:900};

	/**
	 * The frequency in milliseconds to check for loaded fonts.
	 * @property WATCH_DURATION
	 * @type {number}
	 * @default 10
	 * @static
	 */
	FontLoader.WATCH_DURATION = 10;
// public methods:
	p.load = function() {
		if (this.type == createjs.Types.FONTCSS) {
			var loaded = this._watchCSS();

			// If the CSS is not ready, it will create a request, which AbstractLoader can handle.
			if (!loaded) {
				this.AbstractLoader_load();
				return;
			}

		} else if (this._item.src instanceof Array) {
			this._watchFontArray();
		} else {
			var def = this._defFromSrc(this._item.src);
			this._watchFont(def);
			this._injectStyleTag(this._cssFromDef(def));
		}

		this._loadTimeout = setTimeout(createjs.proxy(this._handleTimeout, this), this._item.loadTimeout);

		this.dispatchEvent("loadstart");
	};

	/**
	 * The font load has timed out. This is called via a <code>setTimeout</code>.
	 * callback.
	 * @method _handleTimeout
	 * @protected
	 */
	p._handleTimeout = function () {
		this._stopWatching();
		this.dispatchEvent(new createjs.ErrorEvent("PRELOAD_TIMEOUT"));
	};

	// WatchCSS does the work for us, and provides a modified src.
	p._createRequest = function() {
		return this._request;
	};

	// Events come from the internal XHR loader.
	p.handleEvent = function (event) {
		switch (event.type) {
			case "complete":
				this._rawResult = event.target._response;
				this._result = true;
				this._parseCSS(this._rawResult);
				break;

			case "error":
				this._stopWatching();
				this.AbstractLoader_handleEvent(event);
				break;
		}
	};

// private methods:
	/**
	 * Determine if the provided CSS is a string definition, CSS HTML element, or a CSS file URI. Depending on the
	 * format, the CSS will be parsed, or loaded.
	 * @method _watchCSS
	 * @returns {boolean} Whether or not the CSS is ready
	 * @protected
	 */
	p._watchCSS = function() {
		var src = this._item.src;

		// An HTMLElement was passed in. Just use it.
		if (src instanceof HTMLStyleElement) {
			if (this._injectCSS && !src.parentNode) { (document.head || document.getElementsByTagName('head')[0]).appendChild(src); }
			this._injectCSS = false;
			src = "\n"+src.textContent;
		}

		// A CSS string was passed in. Parse and use it
		if (src.search(/\n|\r|@font-face/i) !== -1) { // css string.
			this._parseCSS(src);
			return true;
		}

		// Load a CSS Path. Note that we CAN NOT load it without XHR because we need to read the CSS definition
		this._request = new createjs.XHRRequest(this._item);
		return false;
	};

	/**
	 * Parse a CSS string to determine the fonts to load.
	 * @method _parseCSS
	 * @param {String} css The CSS string to parse
	 * @protected
	 */
	p._parseCSS = function(css) {
		var regex = /@font-face\s*\{([^}]+)}/g
		while (true) {
			var result = regex.exec(css);
			if (!result) { break; }
			this._watchFont(this._parseFontFace(result[1]));
		}
		this._injectStyleTag(css);
	};

	/**
	 * The provided fonts were an array of object or string definitions. Parse them, and inject any that are ready.
	 * @method _watchFontArray
	 * @protected
	 */
	p._watchFontArray = function() {
		var arr = this._item.src, css = "", def;
		for (var i=arr.length-1; i>=0; i--) {
			var o = arr[i];
			if (typeof o === "string") { def = this._defFromSrc(o) }
			else { def = this._defFromObj(o); }
			this._watchFont(def);
			css += this._cssFromDef(def)+"\n";
		}
		this._injectStyleTag(css);
	};

	/**
	 * Inject any style definitions into the document head. This is necessary when the definition is just a string or
	 * object definition in order for the styles to be applied to the document. If the loaded fonts are already HTML CSS
	 * elements, they don't need to be appended again.
	 * @method _injectStyleTag
	 * @param {String} css The CSS string content to be appended to the
	 * @protected
	 */
	p._injectStyleTag = function(css) {
		if (!this._injectCSS) { return; }
		var head = document.head || document.getElementsByTagName('head')[0];
		var styleTag = document.createElement("style");
		styleTag.type = "text/css";
		if (styleTag.styleSheet){
			styleTag.styleSheet.cssText = css;
		} else {
			styleTag.appendChild(document.createTextNode(css));
		}
		head.appendChild(styleTag);
	};

	/**
	 * Determine the font face from a CSS definition.
	 * @method _parseFontFace
	 * @param {String} str The CSS string definition
	 * @protected
	 * @return {String} A modified CSS object containing family name, src, style, and weight
	 */
	p._parseFontFace = function(str) {
		var family = this._getCSSValue(str, "font-family"), src = this._getCSSValue(str, "src");
		if (!family || !src) { return null; }
		return this._defFromObj({
			family: family,
			src: src,
			style: this._getCSSValue(str, "font-style"),
			weight: this._getCSSValue(str, "font-weight")
		});
	};

	/**
	 * Add a font to the list of fonts currently being watched. If the font is already watched or loaded, it won't be
	 * added again.
	 * @method _watchFont
	 * @param {Object} def The font definition
	 * @protected
	 */
	p._watchFont = function(def) {
		if (!def || this._faces[def.id]) { return; }
		this._faces[def.id] = def;
		this._watched.push(def);
		this._count++;

		this._calculateReferenceSizes(def);
		this._startWatching();
	};

	/**
	 * Create a interval to check for loaded fonts. Only one interval is used for all fonts. The fonts are checked based
	 * on the {{#crossLink "FontLoader/WATCH_DURATION:property"}}{{/crossLink}}.
	 * @method _startWatching
	 * @protected
	 */
	p._startWatching = function() {
		if (this._watchInterval != null) { return; }
		this._watchInterval = setInterval(createjs.proxy(this._watch, this), FontLoader.WATCH_DURATION);
	};

	/**
	 * Clear the interval used to check fonts. This happens when all fonts are loaded, or an error occurs, such as a
	 * CSS file error, or a load timeout.
	 * @method _stopWatching
	 * @protected
	 */
	p._stopWatching = function() {
		clearInterval(this._watchInterval);
		clearTimeout(this._loadTimeout);
		this._watchInterval = null;
	};

	/**
	 * Check all the fonts that have not been loaded. The fonts are drawn to a canvas in memory, and if their font size
	 * varies from the default text size, then the font is considered loaded.
	 *
	 * A {{#crossLink "AbstractLoader/fileload"}}{{/crossLink}} event will be dispatched when each file is loaded, along
	 * with the font family name as the `item` value. A {{#crossLink "ProgressEvent"}}{{/crossLink}} is dispatched a
	 * maximum of one time per check when any fonts are loaded, with the {{#crossLink "ProgressEvent/progress:property"}}{{/crossLink}}
	 * value showing the percentage of fonts that have loaded.
	 * @method _watch
	 * @protected
	 */
	p._watch = function() {
		var defs = this._watched, refFonts = FontLoader._referenceFonts, l = defs.length;
		for (var i = l - 1; i >= 0; i--) {
			var def = defs[i], refs = def.refs;
			for (var j = refs.length - 1; j >= 0; j--) {
				var w = this._getTextWidth(def.family + "," + refFonts[j], def.weight, def.style);
				if (w != refs[j]) {
					var event = new createjs.Event("fileload");
					def.type = "font-family";
					event.item = def;
					this.dispatchEvent(event);
					defs.splice(i, 1);
					break;
				}
			}
		}
		if (l !== defs.length) {
			var event = new createjs.ProgressEvent(this._count-defs.length, this._count);
			this.dispatchEvent(event);
		}
		if (l === 0) {
			this._stopWatching();
			this._sendComplete();
		}
	};

	/**
	 * Determine the default size of the reference fonts used to compare against loaded fonts.
	 * @method _calculateReferenceSizes
	 * @param {Object} def The font definition to get the size of.
	 * @protected
	 */
	p._calculateReferenceSizes = function(def) {
		var refFonts = FontLoader._referenceFonts;
		var refs = def.refs = [];
		for (var i=0; i<refFonts.length; i++) {
			refs[i] = this._getTextWidth(refFonts[i], def.weight, def.style);
		}
	};

	/**
	 * Get a CSS definition from a font source and name.
	 * @method _defFromSrc
	 * @param {String} src The font source
	 * @protected
	 */
	p._defFromSrc = function(src) {
		var re = /[- ._]+/g, name = src, ext = null, index;
		
		index = name.search(/[?#]/);
		if (index !== -1) {
			name = name.substr(0,index);
		}
		index = name.lastIndexOf(".");
		if (index !== -1) {
			ext = name.substr(index+1);
			name = name.substr(0,index);
		}
		index = name.lastIndexOf("/");
		if (index !== -1) {
			name = name.substr(index+1);
		}
		
		var family = name,
				weight = family.match(FontLoader.WEIGHT_REGEX);
		if (weight) {
			weight = weight[0];
			family = family.replace(weight, "");
			weight = weight.replace(re, "").toLowerCase();
		}
		var style = name.match(FontLoader.STYLE_REGEX);
		if (style) {
			family = family.replace(style[0], "");
			style = "italic";
		}
		family = family.replace(re, "");
		
		var cssSrc = "local('"+name.replace(re," ")+"'), url('"+src+"')";
		var format = FontLoader.FONT_FORMAT[ext];
		if (format) { cssSrc += " format('"+format+"')"; }
		
		return this._defFromObj({
			family: family,
			weight: FontLoader.FONT_WEIGHT[weight]||weight,
			style: style,
			src: cssSrc
		});
	};

	/**
	 * Get a font definition from a raw font object.
	 * @method _defFromObj
	 * @param {Object} o A raw object provided to the FontLoader
	 * @returns {Object} A standard font object that the FontLoader understands
	 * @protected
	 */
	p._defFromObj = function(o) {
		var def = {
			family: o.family,
			src: o.src,
			style: o.style || "normal",
			weight: o.weight || "normal"
		};
		def.id = def.family + ";" + def.style + ";" + def.weight;
		return def;
	};

	/**
	 * Get CSS from a font definition.
	 * @method _cssFromDef
	 * @param {Object} def A font definition
	 * @returns {string} A CSS string representing the object
	 * @protected
	 */
	p._cssFromDef = function(def) {
		return "@font-face {\n" +
			"\tfont-family: '"+def.family+"';\n" +
			"\tfont-style: "+def.style+";\n" +
			"\tfont-weight: "+def.weight+";\n" +
			"\tsrc: "+def.src+";\n" +
			"}";
	};

	/**
	 * Get the text width of text using the family, weight, and style
	 * @method _getTextWidth
	 * @param {String} family The font family
	 * @param {String} weight The font weight
	 * @param {String} style The font style
	 * @returns {Number} The pixel measurement of the font.
	 * @protected
	 */
	p._getTextWidth = function(family, weight, style) {
		var ctx = FontLoader._ctx;
		ctx.font = style+" "+weight+" 72px "+family;
		return ctx.measureText(FontLoader.sampleText).width;
	};

	/**
	 * Get the value of a property from a CSS string. For example, searches a CSS string for the value of the
	 * "font-family" property.
	 * @method _getCSSValue
	 * @param {String} str The CSS string to search
	 * @param {String} propName The property name to get the value for
	 * @returns {String} The value in the CSS for the provided property name
	 * @protected
	 */
	p._getCSSValue = function(str, propName) {
		var regex = new RegExp(propName+":\s*([^;}]+?)\s*[;}]");
		var result = regex.exec(str);
		if (!result || !result[1]) { return null; }
		return result[1];
	};

	createjs.FontLoader = createjs.promote(FontLoader, "AbstractLoader");

})();