/* $Id$ */ /** * @projectDescription An cross-browser implementation of the HTML5 text methods * @author Fabien M�nager * @version $Revision$ * @license MIT License */ /** * Known issues: * - The 'light' font weight is not supported, neither is the 'oblique' font style. * - Optimize the different hacks (for Opera9) */ window.Canvas = window.Canvas || {}; window.Canvas.Text = { // http://mondaybynoon.com/2007/04/02/linux-font-equivalents-to-popular-web-typefaces/ equivalentFaces: { // Web popular fonts 'arial': ['liberation sans', 'nimbus sans l', 'freesans', 'optimer', 'dejavu sans'], 'times new roman': ['liberation serif', 'helvetiker', 'linux libertine', 'freeserif'], 'courier new': ['dejavu sans mono', 'liberation mono', 'nimbus mono l', 'freemono'], 'georgia': ['nimbus roman no9 l', 'helvetiker'], 'helvetica': ['nimbus sans l', 'helvetiker', 'freesans'], 'tahoma': ['dejavu sans', 'optimer', 'bitstream vera sans'], 'verdana': ['dejavu sans', 'optimer', 'bitstream vera sans'] }, genericFaces: { 'serif': ['times new roman', 'georgia', 'garamond', 'bodoni', 'minion web', 'itc stone serif', 'bitstream cyberbit'], 'sans-serif': ['arial', 'verdana', 'trebuchet', 'tahoma', 'helvetica', 'itc avant garde gothic', 'univers', 'futura', 'gill sans', 'akzidenz grotesk', 'attika', 'typiko new era', 'itc stone sans', 'monotype gill sans 571'], 'monospace': ['courier', 'courier new', 'prestige', 'everson mono'], 'cursive': ['caflisch script', 'adobe poetica', 'sanvito', 'ex ponto', 'snell roundhand', 'zapf-chancery'], 'fantasy': ['alpha geometrique', 'critter', 'cottonwood', 'fb reactor', 'studz'] }, faces: {}, scaling: 0.962, _styleCache: {} }; /** The implementation of the text functions */ (function(){ var isOpera9 = (window.opera && /Opera\/9/.test(navigator.userAgent)), // It seems to be faster when the hacked methods are used. But there are artifacts with Opera 10. proto = window.CanvasRenderingContext2D ? window.CanvasRenderingContext2D.prototype : document.createElement('canvas').getContext('2d').__proto__, ctxt = window.Canvas.Text; // Global options ctxt.options = { fallbackCharacter: ' ', // The character that will be drawn when not present in the font face file dontUseMoz: false, // Don't use the builtin Firefox 3.0 functions (mozDrawText, mozPathText and mozMeasureText) reimplement: false, // Don't use the builtin official functions present in Chrome 2, Safari 4, and Firefox 3.1+ debug: false, // Debug mode, not used yet autoload: false // Specify the directory containing the face files or false }; var scripts = document.getElementsByTagName("script"), parts = scripts[scripts.length-1].src.split('?'); ctxt.basePath = parts[0].substr(0, parts[0].lastIndexOf("/")+1); if (parts[1]) { var options = parts[1].split('&'); for (var j = options.length-1; j >= 0; --j) { var pair = options[j].split('='); ctxt.options[pair[0]] = pair[1]; } } // What is the browser's implementation ? var moz = !ctxt.options.dontUseMoz && proto.mozDrawText && !proto.fillText; // If the text functions are already here or if on the iPhone (fillText exists) : nothing to do ! if (proto.fillText && !ctxt.options.reimplement && !/iphone/i.test(navigator.userAgent)) { // This property is needed, when including the font face files return window._typeface_js = {loadFace: function(){}}; } function getCSSWeightEquivalent(weight){ switch(String(weight)) { case 'bolder': case 'bold': case '900': case '800': case '700': return 'bold'; case '600': case '500': case '400': default: case 'normal': return 'normal'; //default: return 'light'; } } function getElementStyle(e){ if (document.defaultView && document.defaultView.getComputedStyle) { return document.defaultView.getComputedStyle(e, null); } return e.currentStyle || e.style; } function getXHR(){ if (!ctxt.xhr) { var methods = [ function(){return new XMLHttpRequest()}, function(){return new ActiveXObject('Msxml2.XMLHTTP')}, function(){return new ActiveXObject('Microsoft.XMLHTTP')} ]; for (var i = 0; i < methods.length; i++) { try { ctxt.xhr = methods[i](); break; } catch (e) {} } } return ctxt.xhr; } function arrayContains(a, v){ var i, l = a.length; for (i = l-1; i >= 0; --i) if (a[i] === v) return true; return false; } ctxt.lookupFamily = function(family){ var faces = this.faces, face, i, f, list, equiv = this.equivalentFaces, generic = this.genericFaces; if (faces[family]) return faces[family]; if (generic[family]) { for (i = 0; i < generic[family].length; i++) { if (f = this.lookupFamily(generic[family][i])) return f; } } if (!(list = equiv[family])) return false; for (i = 0; i < list.length; i++) if (face = faces[list[i]]) return face; return false; } ctxt.getFace = function(family, weight, style){ var face = this.lookupFamily(family); if (!face) return false; if (face && face[weight] && face[weight][style]) return face[weight][style]; if (!this.options.autoload) return false; var faceName = (family.replace(/[ -]/g, '_')+'-'+weight+'-'+style), xhr = this.xhr, url = this.basePath+this.options.autoload+'/'+faceName+'.js'; xhr = getXHR(); xhr.open("get", url, false); xhr.send(null); if(xhr.status == 200) { eval(xhr.responseText); return this.faces[family][weight][style]; } else throw 'Unable to load the font ['+family+' '+weight+' '+style+']'; return false; }; ctxt.loadFace = function(data){ var family = data.familyName.toLowerCase(); this.faces[family] = this.faces[family] || {}; if (data.strokeFont) { this.faces[family].normal = this.faces[family].normal || {}; this.faces[family].normal.normal = data; this.faces[family].normal.italic = data; this.faces[family].bold = this.faces[family].normal || {}; this.faces[family].bold.normal = data; this.faces[family].bold.italic = data; } else { this.faces[family][data.cssFontWeight] = this.faces[family][data.cssFontWeight] || {}; this.faces[family][data.cssFontWeight][data.cssFontStyle] = data; } return data; }; // To use the typeface.js face files window._typeface_js = {faces: ctxt.faces, loadFace: ctxt.loadFace}; ctxt.getFaceFromStyle = function(style){ var weight = getCSSWeightEquivalent(style.weight), families = style.family, i, face; for (i = 0; i < families.length; i++) { // The iPhone adds "-webkit-" at the beginning if (face = this.getFace(families[i].toLowerCase().replace(/^-webkit-/, ""), weight, style.style)) { return face; } } try { return window._typeface_js.faces.optimer.bold.normal; } catch () { return false; } }; // Default values // Firefox 3.5 throws an error when redefining these properties try { proto.font = "10px sans-serif"; proto.textAlign = "start"; proto.textBaseline = "alphabetic"; } catch(e){} proto.parseStyle = function(styleText){ if (ctxt._styleCache[styleText]) return this.getComputedStyle(ctxt._styleCache[styleText]); var style = {}, computedStyle, families; if (!this._elt) { this._elt = document.createElement('span'); this.canvas.appendChild(this._elt); } // Default style this.canvas.font = '10px sans-serif'; this._elt.style.font = styleText; computedStyle = getElementStyle(this._elt); style.size = computedStyle.fontSize; style.weight = getCSSWeightEquivalent(computedStyle.fontWeight); style.style = computedStyle.fontStyle; families = computedStyle.fontFamily.split(','); for(i = 0; i < families.length; i++) { families[i] = families[i].replace(/^["'\s]*/, '').replace(/["'\s]*$/, ''); } style.family = families; return this.getComputedStyle(ctxt._styleCache[styleText] = style); }; proto.buildStyle = function (style){ return style.style+' '+style.weight+' '+style.size+'px "'+style.family+'"'; }; proto.renderText = function(text, style){ var face = ctxt.getFaceFromStyle(style), scale = (style.size / face.resolution) * 0.75, offset = 0, i, chars = String(text).split(''), length = chars.length; if (!isOpera9) { this.scale(scale, -scale); this.lineWidth /= scale; } for (i = 0; i < length; i++) { offset += this.renderGlyph(chars[i], face, scale, offset); } }; if (isOpera9) { proto.renderGlyph = function(c, face, scale, offset){ var i, cpx, cpy, outline, action, length, glyph = face.glyphs[c] || face.glyphs[ctxt.options.fallbackCharacter]; if (!glyph) return; if (glyph.o) { outline = glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(' ')); length = outline.length; for (i = 0; i < length; ) { action = outline[i++]; switch(action) { case 'm': this.moveTo(outline[i++]*scale+offset, outline[i++]*-scale); break; case 'l': this.lineTo(outline[i++]*scale+offset, outline[i++]*-scale); break; case 'q': cpx = outline[i++]*scale+offset; cpy = outline[i++]*-scale; this.quadraticCurveTo(outline[i++]*scale+offset, outline[i++]*-scale, cpx, cpy); break; case 'b': cpx = outline[i++]*scale+offset; cpy = outline[i++]*-scale; this.bezierCurveTo(outline[i++]*scale+offset, outline[i++]*-scale, outline[i++]*scale+offset, outline[i++]*-scale, cpx, cpy); break; } } } return glyph.ha*scale; }; } else { proto.renderGlyph = function(c, face){ var i, cpx, cpy, outline, action, length, glyph = face.glyphs[c] || face.glyphs[ctxt.options.fallbackCharacter]; if (!glyph) return; if (glyph.o) { outline = glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(' ')); length = outline.length; for (i = 0; i < length; ) { action = outline[i++]; switch(action) { case 'm': this.moveTo(outline[i++], outline[i++]); break; case 'l': this.lineTo(outline[i++], outline[i++]); break; case 'q': cpx = outline[i++]; cpy = outline[i++]; this.quadraticCurveTo(outline[i++], outline[i++], cpx, cpy); break; case 'b': cpx = outline[i++]; cpy = outline[i++]; this.bezierCurveTo(outline[i++], outline[i++], outline[i++], outline[i++], cpx, cpy); break; } } } if (glyph.ha) this.translate(glyph.ha, 0); }; } proto.getTextExtents = function(text, style){ var width = 0, height = 0, ha = 0, face = ctxt.getFaceFromStyle(style), i, length = text.length, glyph; for (i = 0; i < length; i++) { glyph = face.glyphs[text.charAt(i)] || face.glyphs[ctxt.options.fallbackCharacter]; width += Math.max(glyph.ha, glyph.x_max); ha += glyph.ha; } return { width: width, height: face.lineHeight, ha: ha }; }; proto.getComputedStyle = function(style){ var p, canvasStyle = getElementStyle(this.canvas), computedStyle = {}, s = style.size, canvasFontSize = parseFloat(canvasStyle.fontSize), fontSize = parseFloat(s); for (p in style) { computedStyle[p] = style[p]; } // Compute the size if (typeof s === 'number' || s.indexOf('px') != -1) computedStyle.size = fontSize; else if (s.indexOf('em') != -1) computedStyle.size = canvasFontSize * fontSize; else if (s.indexOf('%') != -1) computedStyle.size = (canvasFontSize / 100) * fontSize; else if (s.indexOf('pt') != -1) computedStyle.size = fontSize / 0.75; else computedStyle.size = canvasFontSize; return computedStyle; }; proto.getTextOffset = function(text, style, face){ var canvasStyle = getElementStyle(this.canvas), metrics = this.measureText(text), scale = (style.size / face.resolution) * 0.75, offset = {x: 0, y: 0, metrics: metrics, scale: scale}; switch (this.textAlign) { default: case null: case 'left': break; case 'center': offset.x = -metrics.width/2; break; case 'right': offset.x = -metrics.width; break; case 'start': offset.x = (canvasStyle.direction == 'rtl') ? -metrics.width : 0; break; case 'end': offset.x = (canvasStyle.direction == 'ltr') ? -metrics.width : 0; break; } switch (this.textBaseline) { case 'alphabetic': break; default: case null: case 'ideographic': case 'bottom': offset.y = face.descender; break; case 'hanging': case 'top': offset.y = face.ascender; break; case 'middle': offset.y = (face.ascender + face.descender) / 2; break; } offset.y *= scale; return offset; }; proto.drawText = function(text, x, y, maxWidth, stroke){ var style = this.parseStyle(this.font), face = ctxt.getFaceFromStyle(style), offset = this.getTextOffset(text, style, face); this.save(); this.translate(x + offset.x, y + offset.y); if (face.strokeFont && !stroke) { this.strokeStyle = this.fillStyle; } this.lineCap = "round"; this.beginPath(); if (moz) { this.mozTextStyle = this.buildStyle(style); this[stroke ? 'mozPathText' : 'mozDrawText'](text); } else { this.scale(ctxt.scaling, ctxt.scaling); this.renderText(text, style); if (face.strokeFont) { this.lineWidth = 2 + style.size * (style.weight == 'bold' ? 0.08 : 0.015) / 2; } } this[(stroke || (face.strokeFont && !moz)) ? 'stroke' : 'fill'](); this.closePath(); this.restore(); if (ctxt.options.debug) { var left = Math.floor(offset.x + x) + 0.5, top = Math.floor(y)+0.5; this.save(); this.strokeStyle = '#F00'; this.lineWidth = 0.5; this.beginPath(); // Text baseline this.moveTo(left + offset.metrics.width, top); this.lineTo(left, top); // Text align this.moveTo(left - offset.x, top + offset.y); this.lineTo(left - offset.x, top + offset.y - style.size); this.stroke(); this.closePath(); this.restore(); } }; proto.fillText = function(text, x, y, maxWidth){ this.drawText(text, x, y, maxWidth, false); }; proto.strokeText = function(text, x, y, maxWidth){ this.drawText(text, x, y, maxWidth, true); }; proto.measureText = function(text){ var style = this.parseStyle(this.font), dim = {width: 0}; if (moz) { this.mozTextStyle = this.buildStyle(style); dim.width = this.mozMeasureText(text); } else { var face = ctxt.getFaceFromStyle(style), scale = (style.size / face.resolution) * 0.75; dim.width = this.getTextExtents(text, style).ha * scale * ctxt.scaling; } return dim; }; })();