Mercurial > hg-website
comparison static/javascript/typeface.js @ 248:53a5e100b497
Convert the frontpage, except for the download button.
author | Steve Losh <steve@stevelosh.com> |
---|---|
date | Wed, 23 Sep 2009 20:34:42 -0400 |
parents | |
children | 84f2a6a16bd2 |
comparison
equal
deleted
inserted
replaced
247:3e6869f76b8d | 248:53a5e100b497 |
---|---|
1 /***************************************************************** | |
2 | |
3 typeface.js, version 0.11 | typefacejs.neocracy.org | |
4 | |
5 Copyright (c) 2008, David Chester davidchester@gmx.net | |
6 | |
7 Permission is hereby granted, free of charge, to any person | |
8 obtaining a copy of this software and associated documentation | |
9 files (the "Software"), to deal in the Software without | |
10 restriction, including without limitation the rights to use, | |
11 copy, modify, merge, publish, distribute, sublicense, and/or sell | |
12 copies of the Software, and to permit persons to whom the | |
13 Software is furnished to do so, subject to the following | |
14 conditions: | |
15 | |
16 The above copyright notice and this permission notice shall be | |
17 included in all copies or substantial portions of the Software. | |
18 | |
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
26 OTHER DEALINGS IN THE SOFTWARE. | |
27 | |
28 *****************************************************************/ | |
29 | |
30 (function() { | |
31 | |
32 var _typeface_js = { | |
33 | |
34 faces: {}, | |
35 | |
36 loadFace: function(typefaceData) { | |
37 | |
38 var familyName = typefaceData.familyName.toLowerCase(); | |
39 | |
40 if (!this.faces[familyName]) { | |
41 this.faces[familyName] = {}; | |
42 } | |
43 if (!this.faces[familyName][typefaceData.cssFontWeight]) { | |
44 this.faces[familyName][typefaceData.cssFontWeight] = {}; | |
45 } | |
46 | |
47 var face = this.faces[familyName][typefaceData.cssFontWeight][typefaceData.cssFontStyle] = typefaceData; | |
48 face.loaded = true; | |
49 }, | |
50 | |
51 log: { | |
52 debug: function(message) { | |
53 var typefaceConsole = document.getElementById('typeface-console'); | |
54 if (typefaceConsole) | |
55 typefaceConsole.innerHTML += 'DEBUG: ' + message + "<br>"; | |
56 }, | |
57 | |
58 error: function(message) { | |
59 var typefaceConsole = document.getElementById('typeface-console'); | |
60 if (typefaceConsole) | |
61 typefaceConsole.innerHTML += 'ERROR: ' + message + "<br>"; | |
62 } | |
63 }, | |
64 | |
65 pixelsFromPoints: function(face, style, points, dimension) { | |
66 var pixels = points * parseInt(style.fontSize) * 72 / (face.resolution * 100); | |
67 if (dimension == 'horizontal' && style.fontStretchPercent) { | |
68 pixels *= style.fontStretchPercent; | |
69 } | |
70 return pixels; | |
71 }, | |
72 | |
73 pointsFromPixels: function(face, style, pixels, dimension) { | |
74 var points = pixels * face.resolution / (parseInt(style.fontSize) * 72 / 100); | |
75 if (dimension == 'horizontal' && style.fontStretchPrecent) { | |
76 points *= style.fontStretchPercent; | |
77 } | |
78 return points; | |
79 }, | |
80 | |
81 cssFontWeightMap: { | |
82 normal: 'normal', | |
83 bold: 'bold', | |
84 400: 'normal', | |
85 700: 'bold' | |
86 }, | |
87 | |
88 cssFontStretchMap: { | |
89 'ultra-condensed': 0.55, | |
90 'extra-condensed': 0.77, | |
91 'condensed': 0.85, | |
92 'semi-condensed': 0.93, | |
93 'normal': 1, | |
94 'semi-expanded': 1.07, | |
95 'expanded': 1.15, | |
96 'extra-expanded': 1.23, | |
97 'ultra-expanded': 1.45, | |
98 'default': 1 | |
99 }, | |
100 | |
101 fallbackCharacter: '.', | |
102 | |
103 getTextExtents: function(face, style, text) { | |
104 var extentX = 0; | |
105 var extentY = 0; | |
106 var horizontalAdvance; | |
107 | |
108 for (var i = 0; i < text.length; i++) { | |
109 var glyph = face.glyphs[text.charAt(i)] ? face.glyphs[text.charAt(i)] : face.glyphs[this.fallbackCharacter]; | |
110 var letterSpacingAdjustment = this.pointsFromPixels(face, style, style.letterSpacing); | |
111 extentX += Math.max(glyph.ha, glyph.x_max) + letterSpacingAdjustment; | |
112 horizontalAdvance += glyph.ha + letterSpacingAdjustment; | |
113 } | |
114 return { | |
115 x: extentX, | |
116 y: extentY, | |
117 ha: horizontalAdvance | |
118 | |
119 }; | |
120 }, | |
121 | |
122 pixelsFromCssAmount: function(cssAmount, defaultValue) { | |
123 | |
124 var matches = undefined; | |
125 | |
126 if (cssAmount == 'normal') { | |
127 return defaultValue; | |
128 | |
129 } else if (matches = cssAmount.match(/([\-\d+\.]+)px/)) { | |
130 return matches[1]; | |
131 | |
132 } else if (matches = cssAmount.match(/([\-\d\.]+)pt/)) { | |
133 return matches[1] * 100 / 75; | |
134 } else { | |
135 return defaultValue; | |
136 } | |
137 }, | |
138 | |
139 getRenderedText: function(e) { | |
140 | |
141 var browserStyle = window.getComputedStyle ? | |
142 document.defaultView.getComputedStyle(e.parentNode, '') : | |
143 e.parentNode.currentStyle ? | |
144 e.parentNode.currentStyle : | |
145 { color: '#ff0000', fontSize: 12, fontFamily: 'arial' }; | |
146 | |
147 var inlineStyleAttribute = e.parentNode.getAttribute('style'); | |
148 if (inlineStyleAttribute && typeof(inlineStyleAttribute) == 'object') { | |
149 inlineStyleAttribute = inlineStyleAttribute.cssText; | |
150 } | |
151 | |
152 if (inlineStyleAttribute) { | |
153 | |
154 var inlineStyleDeclarations = inlineStyleAttribute.split(/\s*\;\s*/); | |
155 | |
156 var inlineStyle = {}; | |
157 for (var i = 0; i < inlineStyleDeclarations.length; i++) { | |
158 var declaration = inlineStyleDeclarations[i]; | |
159 var declarationOperands = declaration.split(/\s*\:\s*/); | |
160 inlineStyle[declarationOperands[0]] = declarationOperands[1]; | |
161 } | |
162 } | |
163 | |
164 var style = { | |
165 color: browserStyle.color, | |
166 fontFamily: browserStyle.fontFamily.split(/\s*,\s*/)[0].replace(/(^"|^'|'$|"$)/g, '').toLowerCase(), | |
167 fontSize: this.pixelsFromCssAmount(browserStyle.fontSize, 12), | |
168 fontWeight: this.cssFontWeightMap[browserStyle.fontWeight], | |
169 fontStyle: browserStyle.fontStyle ? browserStyle.fontStyle : 'normal', | |
170 fontStretchPercent: this.cssFontStretchMap[inlineStyle && inlineStyle['font-stretch'] ? inlineStyle['font-stretch'] : 'default'], | |
171 textDecoration: browserStyle.textDecoration, | |
172 lineHeight: this.pixelsFromCssAmount(browserStyle.lineHeight, 'normal'), | |
173 letterSpacing: this.pixelsFromCssAmount(browserStyle.letterSpacing, 0) | |
174 }; | |
175 | |
176 var face; | |
177 if ( | |
178 this.faces[style.fontFamily] && | |
179 this.faces[style.fontFamily][style.fontWeight] | |
180 ) { | |
181 face = this.faces[style.fontFamily][style.fontWeight][style.fontStyle]; | |
182 } | |
183 | |
184 if (!face) { | |
185 return; | |
186 } | |
187 | |
188 var text = e.nodeValue.replace(/(?:^\s+|\s+$)/g, ''); | |
189 text = text.replace(/\s+/g, ' '); | |
190 var words = text.split(/\s/); | |
191 | |
192 var containerSpan = document.createElement('span'); | |
193 | |
194 for (var i = 0; i < words.length; i++) { | |
195 var word = words[i]; | |
196 var delimiter = i == words.length - 1 ? '' : ' '; | |
197 var vectorElement = this.renderWord(face, style, word + delimiter); | |
198 if (vectorElement) | |
199 containerSpan.appendChild(vectorElement); | |
200 } | |
201 | |
202 return containerSpan; | |
203 }, | |
204 | |
205 renderDocument: function(callback) { // args: onComplete | |
206 | |
207 if (this.renderDocumentLock) | |
208 return; | |
209 | |
210 this.renderDocumentLock = true; | |
211 | |
212 if (!callback) | |
213 callback = function(e) { e.style.visibility = 'visible' }; | |
214 | |
215 var elements = document.getElementsByTagName('*'); | |
216 | |
217 var elementsLength = elements.length; | |
218 for (var i = 0; i < elements.length; i++) { | |
219 if (elements[i].className.match(/(^|\s)typeface-js(\s|$)/) || elements[i].tagName.match(/^(H1|H2|H3|H4|H5|H6)$/)) { | |
220 this.replaceText(elements[i]); | |
221 if (typeof callback == 'function') { | |
222 callback(elements[i]); | |
223 } | |
224 } | |
225 } | |
226 }, | |
227 | |
228 replaceText: function(e) { | |
229 if (e.hasChildNodes()) { | |
230 var childNodes = []; | |
231 for (var i = 0; i < e.childNodes.length; i++) { | |
232 childNodes[i] = e.childNodes[i]; | |
233 } | |
234 for (var i = 0; i < childNodes.length; i++) { | |
235 this.replaceText(childNodes[i]); | |
236 } | |
237 } | |
238 | |
239 if (e.nodeType == 3 && e.nodeValue.match(/\S/)) { | |
240 var parentNode = e.parentNode; | |
241 | |
242 var renderedText = this.getRenderedText(e); | |
243 | |
244 if (renderedText) { | |
245 parentNode.insertBefore(renderedText, e); | |
246 parentNode.removeChild(e); | |
247 } | |
248 } | |
249 }, | |
250 | |
251 applyElementVerticalMetrics: function(face, style, e) { | |
252 | |
253 var boundingBoxAdjustmentTop = this.pixelsFromPoints(face, style, face.ascender - Math.max(face.boundingBox.yMax, face.ascender)); | |
254 var boundingBoxAdjustmentBottom = this.pixelsFromPoints(face, style, Math.min(face.boundingBox.yMin, face.descender) - face.descender); | |
255 | |
256 var cssLineHeightAdjustment = 0; | |
257 if (style.lineHeight != 'normal') { | |
258 cssLineHeightAdjustment = style.lineHeight - this.pixelsFromPoints(face, style, face.lineHeight); | |
259 } | |
260 | |
261 var marginTop = Math.round(boundingBoxAdjustmentTop + cssLineHeightAdjustment / 2); | |
262 var marginBottom = Math.round(boundingBoxAdjustmentBottom + cssLineHeightAdjustment / 2); | |
263 | |
264 e.style.marginTop = marginTop + 'px'; | |
265 e.style.marginBottom = marginBottom + 'px'; | |
266 | |
267 }, | |
268 | |
269 vectorBackends: { | |
270 | |
271 canvas: { | |
272 | |
273 _initializeSurface: function(face, style, text) { | |
274 | |
275 var extents = this.getTextExtents(face, style, text); | |
276 | |
277 var canvas = document.createElement('canvas'); | |
278 canvas.innerHTML = text; | |
279 | |
280 this.applyElementVerticalMetrics(face, style, canvas); | |
281 canvas.height = Math.round(this.pixelsFromPoints(face, style, face.lineHeight)); | |
282 | |
283 canvas.width = Math.round(this.pixelsFromPoints(face, style, extents.x, 'horizontal')); | |
284 | |
285 if (extents.x > extents.ha) | |
286 canvas.style.marginRight = Math.round(this.pixelsFromPoints(face, style, extents.x - extents.ha, 'horizontal')) + 'px'; | |
287 | |
288 var ctx = canvas.getContext('2d'); | |
289 | |
290 var pointScale = this.pixelsFromPoints(face, style, 1); | |
291 ctx.scale(pointScale * style.fontStretchPercent, -1 * pointScale); | |
292 ctx.translate(0, -1 * face.ascender); | |
293 ctx.fillStyle = style.color; | |
294 | |
295 return { context: ctx, canvas: canvas }; | |
296 }, | |
297 | |
298 _renderGlyph: function(ctx, face, char, style) { | |
299 | |
300 var glyph = face.glyphs[char]; | |
301 | |
302 if (!glyph) { | |
303 //this.log.error("glyph not defined: " + char); | |
304 return this.renderGlyph(ctx, face, this.fallbackCharacter, style); | |
305 } | |
306 | |
307 if (glyph.o) { | |
308 | |
309 var outline; | |
310 if (glyph.cached_outline) { | |
311 outline = glyph.cached_outline; | |
312 } else { | |
313 outline = glyph.o.split(' '); | |
314 glyph.cached_outline = outline; | |
315 } | |
316 | |
317 for (var i = 0; i < outline.length; ) { | |
318 | |
319 var action = outline[i++]; | |
320 | |
321 switch(action) { | |
322 case 'm': | |
323 ctx.moveTo(outline[i++], outline[i++]); | |
324 break; | |
325 case 'l': | |
326 ctx.lineTo(outline[i++], outline[i++]); | |
327 break; | |
328 | |
329 case 'q': | |
330 var cpx = outline[i++]; | |
331 var cpy = outline[i++]; | |
332 ctx.quadraticCurveTo(outline[i++], outline[i++], cpx, cpy); | |
333 break; | |
334 } | |
335 } | |
336 } | |
337 if (glyph.ha) { | |
338 var letterSpacingPoints = | |
339 style.letterSpacing && style.letterSpacing != 'normal' ? | |
340 this.pointsFromPixels(face, style, style.letterSpacing) : | |
341 0; | |
342 | |
343 ctx.translate(glyph.ha + letterSpacingPoints, 0); | |
344 } | |
345 }, | |
346 | |
347 _renderWord: function(face, style, text) { | |
348 var surface = this.initializeSurface(face, style, text); | |
349 var ctx = surface.context; | |
350 var canvas = surface.canvas; | |
351 ctx.beginPath(); | |
352 ctx.save(); | |
353 | |
354 var chars = text.split(''); | |
355 for (var i = 0; i < chars.length; i++) { | |
356 var char = chars[i]; | |
357 this.renderGlyph(ctx, face, char, style); | |
358 } | |
359 | |
360 ctx.fill(); | |
361 | |
362 if (style.textDecoration == 'underline') { | |
363 | |
364 ctx.beginPath(); | |
365 ctx.moveTo(0, face.underlinePosition); | |
366 ctx.restore(); | |
367 ctx.lineTo(0, face.underlinePosition); | |
368 ctx.strokeStyle = style.color; | |
369 ctx.lineWidth = face.underlineThickness; | |
370 ctx.stroke(); | |
371 } | |
372 | |
373 return ctx.canvas; | |
374 } | |
375 }, | |
376 | |
377 vml: { | |
378 | |
379 _initializeSurface: function(face, style, text) { | |
380 | |
381 var shape = document.createElement('v:shape'); | |
382 | |
383 var extents = this.getTextExtents(face, style, text); | |
384 | |
385 shape.style.width = style.fontSize + 'px'; | |
386 shape.style.height = style.fontSize + 'px'; | |
387 | |
388 if (extents.x > extents.ha) { | |
389 shape.style.marginRight = this.pixelsFromPoints(face, style, extents.x - extents.ha, 'horizontal') + 'px'; | |
390 } | |
391 | |
392 this.applyElementVerticalMetrics(face, style, shape); | |
393 | |
394 shape.coordsize = (face.resolution * 100 / style.fontStretchPercent / 72 ) + "," + (face.resolution * 100 / 72); | |
395 | |
396 shape.coordorigin = '0,' + face.ascender; | |
397 shape.style.flip = 'y'; | |
398 | |
399 shape.fillColor = style.color; | |
400 shape.stroked = false; | |
401 | |
402 shape.path = 'hh m 0,' + face.ascender + ' l 0,' + face.descender + ' '; | |
403 | |
404 return shape; | |
405 }, | |
406 | |
407 _renderGlyph: function(shape, face, char, offsetX, style) { | |
408 | |
409 var glyph = face.glyphs[char]; | |
410 | |
411 if (!glyph) { | |
412 //this.log.error("glyph not defined: " + char); | |
413 this.renderGlyph(shape, face, this.fallbackCharacter, offsetX, style); | |
414 } | |
415 | |
416 var vmlSegments = []; | |
417 | |
418 if (glyph.o) { | |
419 | |
420 var outline; | |
421 if (glyph.cached_outline) { | |
422 outline = glyph.cached_outline; | |
423 } else { | |
424 outline = glyph.o.split(' '); | |
425 glyph.cached_outline = outline; | |
426 } | |
427 | |
428 var prevAction, prevX, prevY; | |
429 | |
430 var i; | |
431 for (i = 0; i < outline.length;) { | |
432 | |
433 var action = outline[i++]; | |
434 var vmlSegment = ''; | |
435 | |
436 var x = Math.round(outline[i++]) + offsetX; | |
437 var y = Math.round(outline[i++]); | |
438 | |
439 switch(action) { | |
440 case 'm': | |
441 vmlSegment = (vmlSegments.length ? 'x ' : '') + 'm ' + x + ',' + y; | |
442 break; | |
443 | |
444 case 'l': | |
445 vmlSegment = 'l ' + x + ',' + y; | |
446 break; | |
447 | |
448 case 'q': | |
449 var cpx = Math.round(outline[i++]) + offsetX; | |
450 var cpy = Math.round(outline[i++]); | |
451 | |
452 var cp1x = Math.round(prevX + 2.0 / 3.0 * (cpx - prevX)); | |
453 var cp1y = Math.round(prevY + 2.0 / 3.0 * (cpy - prevY)); | |
454 | |
455 var cp2x = Math.round(cp1x + (x - prevX) / 3.0); | |
456 var cp2y = Math.round(cp1y + (y - prevY) / 3.0); | |
457 | |
458 vmlSegment = 'c ' + cp1x + ',' + cp1y + ',' + cp2x + ',' + cp2y + ',' + x + ',' + y; | |
459 break; | |
460 } | |
461 | |
462 prevAction = action; | |
463 prevX = x; | |
464 prevY = y; | |
465 | |
466 if (vmlSegment.length) { | |
467 vmlSegments.push(vmlSegment); | |
468 } | |
469 } | |
470 } | |
471 | |
472 vmlSegments.push('x', 'e'); | |
473 return vmlSegments.join(' '); | |
474 }, | |
475 | |
476 _renderWord: function(face, style, text) { | |
477 var offsetX = 0; | |
478 var shape = this.initializeSurface(face, style, text); | |
479 | |
480 var letterSpacingPoints = | |
481 style.letterSpacing && style.letterSpacing != 'normal' ? | |
482 this.pointsFromPixels(face, style, style.letterSpacing) : | |
483 0; | |
484 | |
485 letterSpacingPoints = Math.round(letterSpacingPoints); | |
486 var chars = text.split(''); | |
487 for (var i = 0; i < chars.length; i++) { | |
488 var char = chars[i]; | |
489 shape.path += this.renderGlyph(shape, face, char, offsetX, style) + ' '; | |
490 offsetX += face.glyphs[char].ha + letterSpacingPoints ; | |
491 } | |
492 | |
493 shape.style.marginRight = this.pixelsFromPoints(face, style, face.glyphs[' '].ha) + 'px'; | |
494 return shape; | |
495 } | |
496 | |
497 } | |
498 | |
499 }, | |
500 | |
501 setVectorBackend: function(backend) { | |
502 | |
503 var backendFunctions = ['renderWord', 'initializeSurface', 'renderGlyph']; | |
504 | |
505 for (var i = 0; i < backendFunctions.length; i++) { | |
506 var backendFunction = backendFunctions[i]; | |
507 this[backendFunction] = this.vectorBackends[backend]['_' + backendFunction]; | |
508 } | |
509 } | |
510 }; | |
511 | |
512 // IE won't accept real selectors... | |
513 var typefaceSelectors = ['.typeface-js', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; | |
514 | |
515 if (document.createStyleSheet) { | |
516 var styleSheet = document.createStyleSheet(); | |
517 for (var i = 0; i < typefaceSelectors.length; i++) { | |
518 var selector = typefaceSelectors[i]; | |
519 styleSheet.addRule(selector, 'visibility: hidden'); | |
520 } | |
521 | |
522 } else if (document.styleSheets && document.styleSheets.length) { | |
523 var styleSheet = document.styleSheets[0]; | |
524 document.styleSheets[0].insertRule(typefaceSelectors.join(',') + ' { visibility: hidden; }', styleSheet.cssRules.length); | |
525 } | |
526 | |
527 var backend = !!(window.attachEvent && !window.opera) ? 'vml' : window.CanvasRenderingContext2D || document.createElement('canvas').getContext ? 'canvas' : null; | |
528 | |
529 if (backend == 'vml') { | |
530 | |
531 document.namespaces.add("v"); | |
532 | |
533 var styleSheet = document.createStyleSheet(); | |
534 styleSheet.addRule('v\\:*', "behavior: url(#default#VML); display: inline-block;"); | |
535 } | |
536 | |
537 _typeface_js.setVectorBackend(backend); | |
538 | |
539 window._typeface_js = _typeface_js; | |
540 | |
541 // based on code by Dean Edwards / Matthias Miller / John Resig | |
542 | |
543 function typefaceInit() { | |
544 | |
545 // quit if this function has already been called | |
546 if (arguments.callee.done) return; | |
547 | |
548 // flag this function so we don't do the same thing twice | |
549 arguments.callee.done = true; | |
550 | |
551 // kill the timer | |
552 if (window._typefaceTimer) clearInterval(_typefaceTimer); | |
553 | |
554 _typeface_js.renderDocument( function(e) { e.style.visibility = 'visible' } ); | |
555 }; | |
556 | |
557 if (/WebKit/i.test(navigator.userAgent)) { | |
558 | |
559 var _typefaceTimer = setInterval(function() { | |
560 if (/loaded|complete/.test(document.readyState)) { | |
561 typefaceInit(); | |
562 } | |
563 }, 10); | |
564 } | |
565 | |
566 if (document.addEventListener) { | |
567 window.addEventListener('DOMContentLoaded', function() { typefaceInit() }, false); | |
568 } | |
569 | |
570 /*@cc_on @*/ | |
571 /*@if (@_win32) | |
572 | |
573 document.write("<script id=__ie_onload_typeface defer src=javascript:void(0)><\/script>"); | |
574 var script = document.getElementById("__ie_onload_typeface"); | |
575 script.onreadystatechange = function() { | |
576 if (this.readyState == "complete") { | |
577 typefaceInit(); | |
578 } | |
579 }; | |
580 | |
581 /*@end @*/ | |
582 | |
583 })(); |