processing-js
Version:
For the project website, visit http://processingjs.org
374 lines (341 loc) • 13.3 kB
JavaScript
// module export
module.exports = function(options,undef) {
var window = options.Browser.window,
document = options.Browser.document,
noop = options.noop;
/**
* [internal function] computeFontMetrics() calculates various metrics for text
* placement. Currently this function computes the ascent, descent and leading
* (from "lead", used for vertical space) values for the currently active font.
*/
function computeFontMetrics(pfont) {
var emQuad = 250,
correctionFactor = pfont.size / emQuad,
canvas = document.createElement("canvas");
canvas.width = 2*emQuad;
canvas.height = 2*emQuad;
canvas.style.opacity = 0;
var cfmFont = pfont.getCSSDefinition(emQuad+"px", "normal"),
ctx = canvas.getContext("2d");
ctx.font = cfmFont;
// Size the canvas using a string with common max-ascent and max-descent letters.
// Changing the canvas dimensions resets the context, so we must reset the font.
var protrusions = "dbflkhyjqpg";
canvas.width = ctx.measureText(protrusions).width;
ctx.font = cfmFont;
// for text lead values, we meaure a multiline text container.
var leadDiv = document.createElement("div");
leadDiv.style.position = "absolute";
leadDiv.style.opacity = 0;
leadDiv.style.fontFamily = '"' + pfont.name + '"';
leadDiv.style.fontSize = emQuad + "px";
leadDiv.innerHTML = protrusions + "<br/>" + protrusions;
document.body.appendChild(leadDiv);
var w = canvas.width,
h = canvas.height,
baseline = h/2;
// Set all canvas pixeldata values to 255, with all the content
// data being 0. This lets us scan for data[i] != 255.
ctx.fillStyle = "white";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "black";
ctx.fillText(protrusions, 0, baseline);
var pixelData = ctx.getImageData(0, 0, w, h).data;
// canvas pixel data is w*4 by h*4, because R, G, B and A are separate,
// consecutive values in the array, rather than stored as 32 bit ints.
var i = 0,
w4 = w * 4,
len = pixelData.length;
// Finding the ascent uses a normal, forward scanline
while (++i < len && pixelData[i] === 255) {
noop();
}
var ascent = Math.round(i / w4);
// Finding the descent uses a reverse scanline
i = len - 1;
while (--i > 0 && pixelData[i] === 255) {
noop();
}
var descent = Math.round(i / w4);
// set font metrics
pfont.ascent = correctionFactor * (baseline - ascent);
pfont.descent = correctionFactor * (descent - baseline);
// Then we try to get the real value from the browser
if (document.defaultView.getComputedStyle) {
var leadDivHeight = document.defaultView.getComputedStyle(leadDiv,null).getPropertyValue("height");
leadDivHeight = correctionFactor * leadDivHeight.replace("px","");
if (leadDivHeight >= pfont.size * 2) {
pfont.leading = Math.round(leadDivHeight/2);
}
}
document.body.removeChild(leadDiv);
// if we're caching, cache the context used for this pfont
if (pfont.caching) {
return ctx;
}
}
/**
* Constructor for a system or from-file (non-SVG) font.
*/
function PFont(name, size) {
// according to the P5 API, new PFont() is legal (albeit completely useless)
if (name === undef) {
name = "";
}
this.name = name;
if (size === undef) {
size = 0;
}
this.size = size;
this.glyph = false;
this.ascent = 0;
this.descent = 0;
// For leading, the "safe" value uses the standard TEX ratio
this.leading = 1.2 * size;
// Note that an italic, bold font must used "... Bold Italic"
// in P5. "... Italic Bold" is treated as normal/normal.
var illegalIndicator = name.indexOf(" Italic Bold");
if (illegalIndicator !== -1) {
name = name.substring(0, illegalIndicator);
}
// determine font style
this.style = "normal";
var italicsIndicator = name.indexOf(" Italic");
if (italicsIndicator !== -1) {
name = name.substring(0, italicsIndicator);
this.style = "italic";
}
// determine font weight
this.weight = "normal";
var boldIndicator = name.indexOf(" Bold");
if (boldIndicator !== -1) {
name = name.substring(0, boldIndicator);
this.weight = "bold";
}
// determine font-family name
this.family = "sans-serif";
if (name !== undef) {
switch(name) {
case "sans-serif":
case "serif":
case "monospace":
case "fantasy":
case "cursive":
this.family = name;
break;
default:
this.family = '"' + name + '", sans-serif';
break;
}
}
// Calculate the ascent/descent/leading value based on
// how the browser renders this font.
this.context2d = computeFontMetrics(this);
this.css = this.getCSSDefinition();
if (this.context2d) {
this.context2d.font = this.css;
}
}
/**
* regulates whether or not we're caching the canvas
* 2d context for quick text width computation.
*/
PFont.prototype.caching = true;
/**
* This function generates the CSS "font" string for this PFont
*/
PFont.prototype.getCSSDefinition = function(fontSize, lineHeight) {
if(fontSize===undef) {
fontSize = this.size + "px";
}
if(lineHeight===undef) {
lineHeight = this.leading + "px";
}
// CSS "font" definition: font-style font-variant font-weight font-size/line-height font-family
var components = [this.style, "normal", this.weight, fontSize + "/" + lineHeight, this.family];
return components.join(" ");
};
/**
* Rely on the cached context2d measureText function.
*/
PFont.prototype.measureTextWidth = function(string) {
return this.context2d.measureText(string).width;
};
/**
* FALLBACK FUNCTION -- replaces Pfont.prototype.measureTextWidth
* when the font cache becomes too large. This contructs a new
* canvas 2d context object for calling measureText on.
*/
PFont.prototype.measureTextWidthFallback = function(string) {
var canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d");
ctx.font = this.css;
return ctx.measureText(string).width;
};
/**
* Global "loaded fonts" list, internal to PFont
*/
PFont.PFontCache = { length: 0 };
/**
* This function acts as single access point for getting and caching
* fonts across all sketches handled by an instance of Processing.js
*/
PFont.get = function(fontName, fontSize) {
// round fontSize to one decimal point
fontSize = ((fontSize*10)+0.5|0)/10;
var cache = PFont.PFontCache,
idx = fontName+"/"+fontSize;
if (!cache[idx]) {
cache[idx] = new PFont(fontName, fontSize);
cache.length++;
// FALLBACK FUNCTIONALITY 1:
// If the cache has become large, switch over from full caching
// to caching only the static metrics for each new font request.
if (cache.length === 50) {
PFont.prototype.measureTextWidth = PFont.prototype.measureTextWidthFallback;
PFont.prototype.caching = false;
// clear contexts stored for each cached font
var entry;
for (entry in cache) {
if (entry !== "length") {
cache[entry].context2d = null;
}
}
return new PFont(fontName, fontSize);
}
// FALLBACK FUNCTIONALITY 2:
// If the cache has become too large, switch off font caching entirely.
if (cache.length === 400) {
PFont.PFontCache = {};
PFont.get = PFont.getFallback;
return new PFont(fontName, fontSize);
}
}
return cache[idx];
};
/**
* FALLBACK FUNCTION -- replaces PFont.get when the font cache
* becomes too large. This function bypasses font caching entirely.
*/
PFont.getFallback = function(fontName, fontSize) {
return new PFont(fontName, fontSize);
};
/**
* Lists all standard fonts. Due to browser limitations, this list is
* not the system font list, like in P5, but the CSS "genre" list.
*/
PFont.list = function() {
return ["sans-serif", "serif", "monospace", "fantasy", "cursive"];
};
/**
* Loading external fonts through @font-face rules is handled by PFont,
* to ensure fonts loaded in this way are globally available.
*/
PFont.preloading = {
// template element used to compare font sizes
template: {},
// indicates whether or not the reference tiny font has been loaded
initialized: false,
// load the reference tiny font via a css @font-face rule
initialize: function() {
var generateTinyFont = function() {
var encoded = "#E3KAI2wAgT1MvMg7Eo3VmNtYX7ABi3CxnbHlm" +
"7Abw3kaGVhZ7ACs3OGhoZWE7A53CRobXR47AY3" +
"AGbG9jYQ7G03Bm1heH7ABC3CBuYW1l7Ae3AgcG" +
"9zd7AI3AE#B3AQ2kgTY18PPPUACwAg3ALSRoo3" +
"#yld0xg32QAB77#E777773B#E3C#I#Q77773E#" +
"Q7777777772CMAIw7AB77732B#M#Q3wAB#g3B#" +
"E#E2BB//82BB////w#B7#gAEg3E77x2B32B#E#" +
"Q#MTcBAQ32gAe#M#QQJ#E32M#QQJ#I#g32Q77#";
var expand = function(input) {
return "AAAAAAAA".substr(~~input ? 7-input : 6);
};
return encoded.replace(/[#237]/g, expand);
};
var fontface = document.createElement("style");
fontface.setAttribute("type","text/css");
fontface.innerHTML = "@font-face {\n" +
' font-family: "PjsEmptyFont";' + "\n" +
" src: url('data:application/x-font-ttf;base64,"+generateTinyFont()+"')\n" +
" format('truetype');\n" +
"}";
document.head.appendChild(fontface);
// set up the template element
var element = document.createElement("span");
element.style.cssText = 'position: absolute; top: -1000; left: 0; opacity: 0; font-family: "PjsEmptyFont", fantasy;';
element.innerHTML = "AAAAAAAA";
document.body.appendChild(element);
this.template = element;
this.initialized = true;
},
// Shorthand function to get the computed width for an element.
getElementWidth: function(element) {
return document.defaultView.getComputedStyle(element,"").getPropertyValue("width");
},
// time taken so far in attempting to load a font
timeAttempted: 0,
// returns false if no fonts are pending load, or true otherwise.
pending: function(intervallength) {
if (!this.initialized) {
this.initialize();
}
var element,
computedWidthFont,
computedWidthRef = this.getElementWidth(this.template);
for (var i = 0; i < this.fontList.length; i++) {
// compares size of text in pixels. if equal, custom font is not yet loaded
element = this.fontList[i];
computedWidthFont = this.getElementWidth(element);
if (this.timeAttempted < 4000 && computedWidthFont === computedWidthRef) {
this.timeAttempted += intervallength;
return true;
} else {
document.body.removeChild(element);
this.fontList.splice(i--, 1);
this.timeAttempted = 0;
}
}
// if there are no more fonts to load, pending is false
if (this.fontList.length === 0) {
return false;
}
// We should have already returned before getting here.
// But, if we do get here, length!=0 so fonts are pending.
return true;
},
// fontList contains elements to compare font sizes against a template
fontList: [],
// addedList contains the fontnames of all the fonts loaded via @font-face
addedList: {},
// adds a font to the font cache
// creates an element using the font, to start loading the font,
// and compare against a default font to see if the custom font is loaded
add: function(fontSrc) {
if (!this.initialized) {
this.initialize();
}
// fontSrc can be a string or a javascript object
// acceptable fonts are .ttf, .otf, and data uri
var fontName = (typeof fontSrc === 'object' ? fontSrc.fontFace : fontSrc),
fontUrl = (typeof fontSrc === 'object' ? fontSrc.url : fontSrc);
// check whether we already created the @font-face rule for this font
if (this.addedList[fontName]) {
return;
}
// if we didn't, create the @font-face rule
var style = document.createElement("style");
style.setAttribute("type","text/css");
style.innerHTML = "@font-face{\n font-family: '" + fontName + "';\n src: url('" + fontUrl + "');\n}\n";
document.head.appendChild(style);
this.addedList[fontName] = true;
// also create the element to load and compare the new font
var element = document.createElement("span");
element.style.cssText = "position: absolute; top: 0; left: 0; opacity: 0;";
element.style.fontFamily = '"' + fontName + '", "PjsEmptyFont", fantasy';
element.innerHTML = "AAAAAAAA";
document.body.appendChild(element);
this.fontList.push(element);
}
};
return PFont;
};