Better fittext
How could you say that? You think you can do better?
A few weeks ago I heard about the fittext.js script created by Paravel. I’ve had it in the back of my mind as something I wanted to try. In redesigning the theme for this blog, I found a small way in which to incorporate it: the ‘not found’ page.
I always appreciate it when a website does something interesting with the not found page. They can be humorous, sarcastic or cute. I wish I remembered a few of the good examples. Often not found pages are very simple (though well done; see Owltastic). Sometimes they’re just forgotten (like on Paravel’s website; sorry to pick on them). I wanted a simple but bold page. How about a sign that just says “wrong way”? Incidentally, Hypatia Sans Pro, the font I’m now using on my websites thanks to the folks at Extensis, has a really wonderful cap W.
Here’s the end result. Unfortunately, it took me a long time to get this. Fittext.js, as it turns out, is pretty weak.
How could you say that? You think you can do better? Well, I did, and here’s an examination of how. First, a little bit of code from fittext.js:
var resizer = function () {
$this.css('font-size', Math.min($this.width() / (compressor*10), origFontSize));
};
This is really all there is to fittext. At first I was surprised. How could this create text that fills a line? Well, the answer is that it can’t. Not really. All this function does is divide the pixel width of the block element in which the text appears by 10, and then compares that value to the existing CSS font size setting. Whichever one is smaller is the size that the font is set to. In order to give the user some control over the math, they added the compressor variable. This is a value the user can supply as a function argument which will adjust how the new font size is calculated.
There are a number of problems here. First, the default font size must be large. The function uses the smaller of the two values as the font size. If the original size is very small, fittext.js won’t do anything. Second, the idea of dividing the block width by 10 is arbitrary. This is why the compressor variable is available. In order for text to actually fit the user must play with the compressor variable to find the right value. Finally, this is font specific. Because each font has a different em width, you will need different compressor values for different fonts. You better be sure the selected font is available, otherwise your text will be too large or too small.
While I appreciate the simplicity of this low bandwidth approach, I wanted something more robust, more exact, and more bulletproof. My solution is therefore more complicated in it’s execution. However, the math is really very simple. It’s about proportions:
a:b = c:d or a/b = c/d
You remember proportions from high school, yes? The relationship of a to b is equal to the relationship of c to d. We can use this expression to fit text using the assumption that the width of a line of text is linearly proportional to the font size. As it turns out, this assumption works well. Given a block element of a particular width, I can find the point size that will make a line of text equal to the width of the block like this:
origFontSize / newFontSize = currentLineLength / blockElementWidth
The original font size, current line length, and block element width can all be determined with jQuery. Rearranging the equation above to isolate the unknown variable I get:
newFontSize = (blockElementWidth / currentLineLength) * origFontSize
Finding the current line length is a little tricky. The easiest way I found is to temporarily set the block element to float (float:left). Because floats are required to have a width, the browser automatically sets the width of a floating element to the width of its contents (assuming no width has been specified. If you have a line of text inside a p tag and want to know how many pixels long that line is, float the p element and then check the width (it helps if your text is styled white-space:no-wrap) Here’s the basic jQuery code:
1 var limit_w = $(textfits[x]).width();
2 var current_w = $(textfits[x]).css('float','left').width();
3 var new_emsize = parseInt($(textfits[x]).css('fontSize')) * (limit_w / current_w) / 10
4 $(textfits[x]).css({'fontSize':new_emsize+'em','float':'none'});
In this code, “textfits[x]” is a reference to the block element in which I want text to fit. The ‘10’ at the end of the third line is used to convert the font pixel size to an em size (my document has 10px to 1em).
Oh, but there are caveats
Despite the fact that my math is sound, this approach did not work at first. The reason? I am using webfonts from Extensis. Webfonts are not low bandwidth; they take time to load. Even though I had placed this script at the bottom of my page, the script ran before Hypatia Sans Pro has a chance to load. The script was using the fallback font, Georgia, in the calculation of the text size. Because Georgia is much wider than Hypatia Sans, my text was not a perfect fit. In order to make the script work perfectly, I had to attach it to the window.onload event handler. (People using webfonts from TypeKit, Ascender, Google, or other services that deliver CSS files might be able to use Google’s WebFont loader to check for loaded fonts. Extensis provides links to the actual font resources.)
Of course running the script onload caused a “jump” which is probably familiar to many developers. The page, with the unfit text, is fully loaded before my fittext script runs, meaning it is visible in its unfitted state for a moment before the font size is corrected. Some people might not think that’s a big deal, but it looks unprofessional. The quick solution? Hide the unfitted text using CSS (visibility:hidden) and then show it after the script has run.
Whew. Much effort for a simple not found page. My wife thinks I’m crazy, but I learned some new things along the way. They say it’s the journey that counts, right?