1 /* $Id$ */ 2 3 /** 4 * @projectDescription An cross-browser implementation of the HTML5 <canvas> text methods 5 * @author Fabien M�nager 6 * @version $Revision$ 7 * @license MIT License <http://www.opensource.org/licenses/mit-license.php> 8 */ 9 10 /** 11 * Known issues: 12 * - The 'light' font weight is not supported, neither is the 'oblique' font style. 13 * - Optimize the different hacks (for Opera9) 14 */ 15 16 window.Canvas = window.Canvas || {}; 17 window.Canvas.Text = { 18 // http://mondaybynoon.com/2007/04/02/linux-font-equivalents-to-popular-web-typefaces/ 19 equivalentFaces: { 20 // Web popular fonts 21 'arial': ['liberation sans', 'nimbus sans l', 'freesans'], 22 'times new roman': ['liberation serif', 'linux libertine', 'freeserif'], 23 'courier new': ['dejavu sans mono', 'liberation mono', 'nimbus mono l', 'freemono'], 24 'georgia': ['nimbus roman no9 l'], 25 'helvetica': ['nimbus sans l', 'freesans'], 26 'tahoma': ['dejavu sans', 'bitstream vera sans'], 27 'verdana': ['dejavu sans', 'bitstream vera sans'] 28 }, 29 genericFaces: { 30 'serif': ['times new roman', 'georgia', 'garamond', 'bodoni', 'minion web', 'itc stone serif', 'bitstream cyberbit'], 31 'sans-serif': ['arial', 'verdana', 'trebuchet', 'tahoma', 'helvetica', 'itc avant garde gothic', 'univers', 'futura', 32 'gill sans', 'akzidenz grotesk', 'attika', 'typiko new era', 'itc stone sans', 'monotype gill sans 571'], 33 'monospace': ['courier', 'courier new', 'prestige', 'everson mono'], 34 'cursive': ['caflisch script', 'adobe poetica', 'sanvito', 'ex ponto', 'snell roundhand', 'zapf-chancery'], 35 'fantasy': ['alpha geometrique', 'critter', 'cottonwood', 'fb reactor', 'studz'] 36 }, 37 faces: {}, 38 scaling: 0.962, 39 _styleCache: {} 40 }; 41 42 /** The implementation of the text functions */ 43 (function(){ 44 var isOpera9 = (window.opera && navigator.userAgent.match(/Opera\/9/)), // It seems to be faster when the hacked methods are used. But there are artifacts with Opera 10. 45 proto = window.CanvasRenderingContext2D ? window.CanvasRenderingContext2D.prototype : document.createElement('canvas').getContext('2d').__proto__, 46 ctxt = window.Canvas.Text; 47 48 // Global options 49 ctxt.options = { 50 fallbackCharacter: ' ', // The character that will be drawn when not present in the font face file 51 dontUseMoz: false, // Don't use the builtin Firefox 3.0 functions (mozDrawText, mozPathText and mozMeasureText) 52 reimplement: false, // Don't use the builtin official functions present in Chrome 2, Safari 4, and Firefox 3.1+ 53 debug: false, // Debug mode, not used yet 54 autoload: 'faces' 55 }; 56 57 function initialize(){ 58 var libFileName = 'canvas.text.js', 59 scripts = document.getElementsByTagName("script"), i, j; 60 61 for (i = 0; i < scripts.length; i++) { 62 var src = scripts[i].src; 63 if (src.indexOf(libFileName) != -1) { 64 var parts = src.split('?'); 65 ctxt.basePath = parts[0].replace(libFileName, ''); 66 if (parts[1]) { 67 var options = parts[1].split('&'); 68 for (j = options.length-1; j >= 0; --j) { 69 var pair = options[j].split('='); 70 ctxt.options[pair[0]] = pair[1]; 71 } 72 } 73 break; 74 } 75 } 76 } 77 initialize(); 78 79 // What is the browser's implementation ? 80 var moz = !ctxt.options.dontUseMoz && proto.mozDrawText && !proto.strokeText; 81 82 // If the text functions are already here : nothing to do ! 83 if (proto.strokeText && !ctxt.options.reimplement) { 84 // This property is needed, when including the font face files 85 return window._typeface_js = {loadFace: function(){}}; 86 } 87 88 function getCSSWeightEquivalent(weight){ 89 switch(weight) { 90 case 'bolder': 91 case 'bold': 92 case '900': 93 case '800': 94 case '700': return 'bold'; 95 case '600': 96 case '500': 97 case '400': 98 default: 99 case 'normal': return 'normal'; 100 //default: return 'light'; 101 } 102 } 103 104 function getElementStyle(e){ 105 if (document.defaultView && document.defaultView.getComputedStyle) { 106 return document.defaultView.getComputedStyle(e, null); 107 } else if (e.currentStyle) { 108 return e.currentStyle; 109 } else { 110 return e.style; 111 } 112 } 113 114 function getXHR(){ 115 if (!ctxt.xhr) { 116 var methods = [ 117 function(){return new XMLHttpRequest()}, 118 function(){return new ActiveXObject('Msxml2.XMLHTTP')}, 119 function(){return new ActiveXObject('Microsoft.XMLHTTP')} 120 ]; 121 for (i = 0; i < methods.length; i++) { 122 try { 123 ctxt.xhr = methods[i](); 124 break; 125 } 126 catch (e) {} 127 } 128 } 129 return ctxt.xhr; 130 } 131 132 function arrayContains(a, v){ 133 var i, l = a.length; 134 for (i = l-1; i >= 0; --i) if (a[i] === v) return true; 135 return false; 136 } 137 138 ctxt.lookupFamily = function(family){ 139 var faces = this.faces, face, i, f, list, 140 equiv = this.equivalentFaces, 141 generic = this.genericFaces; 142 143 if (faces[family]) return faces[family]; 144 145 if (generic[family]) { 146 for (i = 0; i < generic[family].length; i++) { 147 if (f = this.lookupFamily(generic[family][i])) return f; 148 } 149 } 150 151 if (!(list = equiv[family])) return false; 152 153 for (i = 0; i < list.length; i++) 154 if (face = faces[list[i]]) return face; 155 return false; 156 } 157 158 ctxt.getFace = function(family, weight, style){ 159 var face = this.lookupFamily(family); 160 if (!face) return false; 161 162 if (face && 163 face[weight] && 164 face[weight][style]) return face[weight][style]; 165 166 if (!this.options.autoload) return false; 167 168 var faceName = (family.replace(/[ -]/g, '_')+'-'+weight+'-'+style), 169 xhr = this.xhr, 170 url = this.basePath+this.options.autoload+'/'+faceName+'.js'; 171 172 xhr = getXHR(); 173 xhr.open("get", url, false); 174 xhr.send(null); 175 if(xhr.status == 200) { 176 eval(xhr.responseText); 177 return this.faces[family][weight][style]; 178 } 179 else throw 'Unable to load the font ['+family+' '+weight+' '+style+']'; 180 return false; 181 }; 182 183 ctxt.loadFace = function(data){ 184 var family = data.familyName.toLowerCase(); 185 this.faces[family] = this.faces[family] || {}; 186 this.faces[family][data.cssFontWeight] = this.faces[family][data.cssFontWeight] || {}; 187 this.faces[family][data.cssFontWeight][data.cssFontStyle] = data; 188 return data; 189 }; 190 191 // To use the typeface.js face files 192 window._typeface_js = {faces: ctxt.faces, loadFace: ctxt.loadFace}; 193 194 ctxt.getFaceFromStyle = function(style){ 195 var weight = getCSSWeightEquivalent(style.weight), 196 families = style.family, i, face; 197 198 for (i = 0; i < families.length; i++) { 199 if (face = this.getFace(families[i].toLowerCase(), weight, style.style)) { 200 return face; 201 } 202 } 203 return false; 204 }; 205 206 // Default values 207 // Firefox 3.5 throws an error when redefining these properties 208 try { 209 proto.font = "10px sans-serif"; 210 proto.textAlign = "start"; 211 proto.textBaseline = "alphabetic"; 212 } 213 catch(e){} 214 215 proto.parseStyle = function(styleText){ 216 if (ctxt._styleCache[styleText]) return this.getComputedStyle(ctxt._styleCache[styleText]); 217 218 var style = {}, computedStyle, families; 219 220 if (!this._elt) { 221 this._elt = document.createElement('span'); 222 this.canvas.appendChild(this._elt); 223 } 224 225 // Default style 226 this.canvas.font = '10px sans-serif'; 227 this._elt.style.font = styleText; 228 229 computedStyle = getElementStyle(this._elt); 230 style.size = computedStyle.fontSize; 231 style.weight = computedStyle.fontWeight; 232 style.style = computedStyle.fontStyle; 233 234 families = computedStyle.fontFamily.split(','); 235 for(i = 0; i < families.length; i++) { 236 families[i] = families[i].replace(/^["'\s]*/, '').replace(/["'\s]*$/, ''); 237 } 238 style.family = families; 239 240 return this.getComputedStyle(ctxt._styleCache[styleText] = style); 241 }; 242 243 proto.buildStyle = function (style){ 244 return style.style+' '+style.weight+' '+style.size+'px "'+style.family+'"'; 245 }; 246 247 proto.renderText = function(text, style){ 248 var face = ctxt.getFaceFromStyle(style), 249 scale = (style.size / face.resolution) * (3/4), 250 offset = 0, i, 251 chars = text.split(''), 252 length = chars.length; 253 254 if (!isOpera9) { 255 this.scale(scale, -scale); 256 this.lineWidth /= scale; 257 } 258 259 for (i = 0; i < length; i++) { 260 offset += this.renderGlyph(chars[i], face, scale, offset); 261 } 262 }; 263 264 if (isOpera9) { 265 proto.renderGlyph = function(c, face, scale, offset){ 266 var i, cpx, cpy, outline, action, glyph = face.glyphs[c], length; 267 268 if (!glyph) return; 269 270 if (glyph.o) { 271 outline = glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(' ')); 272 length = outline.length; 273 for (i = 0; i < length; ) { 274 action = outline[i++]; 275 276 switch(action) { 277 case 'm': 278 this.moveTo(outline[i++]*scale+offset, outline[i++]*-scale); 279 break; 280 case 'l': 281 this.lineTo(outline[i++]*scale+offset, outline[i++]*-scale); 282 break; 283 case 'q': 284 cpx = outline[i++]*scale+offset; 285 cpy = outline[i++]*-scale; 286 this.quadraticCurveTo(outline[i++]*scale+offset, outline[i++]*-scale, cpx, cpy); 287 break; 288 } 289 } 290 } 291 return glyph.ha * scale; 292 }; 293 } 294 else { 295 proto.renderGlyph = function(c, face){ 296 var i, cpx, cpy, outline, action, glyph = face.glyphs[c], length; 297 298 if (!glyph) return; 299 300 if (glyph.o) { 301 outline = glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(' ')); 302 length = outline.length; 303 for (i = 0; i < length; ) { 304 action = outline[i++]; 305 306 switch(action) { 307 case 'm': 308 this.moveTo(outline[i++], outline[i++]); 309 break; 310 case 'l': 311 this.lineTo(outline[i++], outline[i++]); 312 break; 313 case 'q': 314 cpx = outline[i++]; 315 cpy = outline[i++]; 316 this.quadraticCurveTo(outline[i++], outline[i++], cpx, cpy); 317 break; 318 } 319 } 320 } 321 if (glyph.ha) this.translate(glyph.ha, 0); 322 }; 323 } 324 325 proto.getTextExtents = function(text, style){ 326 var width = 0, height = 0, ha = 0, 327 face = ctxt.getFaceFromStyle(style), 328 i, glyph; 329 330 for (i = 0; i < text.length; i++) { 331 glyph = face.glyphs[text.charAt(i)] || face.glyphs[ctxt.options.fallbackCharacter]; 332 width += Math.max(glyph.ha, glyph.x_max); 333 ha += glyph.ha; 334 } 335 336 return { 337 width: width, 338 height: face.lineHeight, 339 ha: ha 340 }; 341 }; 342 343 proto.getComputedStyle = function(style){ 344 var p, canvasStyle = getElementStyle(this.canvas), 345 computedStyle = {}, 346 s = style.size, 347 canvasFontSize = parseFloat(canvasStyle.fontSize), 348 fontSize = parseFloat(s); 349 350 for (p in style) { 351 computedStyle[p] = style[p]; 352 } 353 354 // Compute the size 355 if (typeof s == 'number' || s.indexOf('px') != -1) 356 computedStyle.size = fontSize; 357 else if (s.indexOf('em') != -1) 358 computedStyle.size = canvasFontSize * fontSize; 359 else if (s.indexOf('%') != -1) 360 computedStyle.size = (canvasFontSize / 100) * fontSize; 361 else if (s.indexOf('pt') != -1) 362 computedStyle.size = canvasFontSize * (4/3) * fontSize; 363 else 364 computedStyle.size = canvasFontSize; 365 366 return computedStyle; 367 }; 368 369 proto.getTextOffset = function(text, style, face){ 370 var canvasStyle = getElementStyle(this.canvas), 371 metrics = this.measureText(text), 372 scale = (style.size / face.resolution) * (3/4), 373 offset = {x: 0, y: 0, metrics: metrics, scale: scale}; 374 375 switch (this.textAlign) { 376 default: 377 case null: 378 case 'left': break; 379 case 'center': offset.x = -metrics.width/2; break; 380 case 'right': offset.x = -metrics.width; break; 381 case 'start': offset.x = (canvasStyle.direction == 'rtl') ? -metrics.width : 0; break; 382 case 'end': offset.x = (canvasStyle.direction == 'ltr') ? -metrics.width : 0; break; 383 } 384 385 switch (this.textBaseline) { 386 case 'alphabetic': break; 387 default: 388 case null: 389 case 'ideographic': 390 case 'bottom': offset.y = face.descender; break; 391 case 'hanging': 392 case 'top': offset.y = face.ascender; break; 393 case 'middle': offset.y = (face.ascender + face.descender) / 2; break; 394 } 395 offset.y *= scale; 396 return offset; 397 }; 398 399 proto.drawText = function(text, x, y, maxWidth, stroke){ 400 var style = this.parseStyle(this.font), 401 face = ctxt.getFaceFromStyle(style), 402 offset = this.getTextOffset(text, style, face); 403 404 this.save(); 405 this.translate(x + offset.x, y + offset.y); 406 if (face.strokeFont && !stroke) { 407 this.strokeStyle = this.fillStyle; 408 } 409 this.beginPath(); 410 411 if (moz) { 412 this.mozTextStyle = this.buildStyle(style); 413 this[stroke ? 'mozPathText' : 'mozDrawText'](text); 414 } 415 else { 416 this.scale(ctxt.scaling, ctxt.scaling); 417 this.renderText(text, style); 418 if (face.strokeFont) { 419 this.lineWidth = style.size * (style.weight == 'bold' ? 0.5 : 0.3); 420 } 421 } 422 423 this[(stroke || (face.strokeFont && !moz)) ? 'stroke' : 'fill'](); 424 425 this.closePath(); 426 this.restore(); 427 428 if (ctxt.options.debug) { 429 var left = Math.floor(offset.x + x) + 0.5, 430 top = Math.floor(y)+0.5; 431 432 this.save(); 433 this.strokeStyle = '#F00'; 434 this.lineWidth = 0.5; 435 this.beginPath(); 436 437 // Text baseline 438 this.moveTo(left + offset.metrics.width, top); 439 this.lineTo(left, top); 440 441 // Text align 442 this.moveTo(left - offset.x, top + offset.y); 443 this.lineTo(left - offset.x, top + offset.y - style.size); 444 445 this.stroke(); 446 this.closePath(); 447 this.restore(); 448 } 449 }; 450 451 proto.fillText = function(text, x, y, maxWidth){ 452 this.drawText(text, x, y, maxWidth, false); 453 }; 454 455 proto.strokeText = function(text, x, y, maxWidth){ 456 this.drawText(text, x, y, maxWidth, true); 457 }; 458 459 proto.measureText = function(text){ 460 var style = this.parseStyle(this.font), 461 dim = {width: 0}; 462 463 if (moz) { 464 this.mozTextStyle = this.buildStyle(style); 465 dim.width = this.mozMeasureText(text); 466 } 467 else { 468 var face = ctxt.getFaceFromStyle(style), 469 scale = (style.size / face.resolution) * (3/4); 470 471 dim.width = this.getTextExtents(text, style).ha * scale * ctxt.scaling; 472 } 473 474 return dim; 475 }; 476 })();