/*
* 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");
})();