Copy-Fitted Text

I’m nerdy enough that I write code just for fun, if the problem’s interesting enough – like this one: What’s the best way to find an arbitrary value in a large range of numbers – say the maximum font-size for a line of text without wrapping?

Brute Force

The obvious, brute force approach? Keep adding 1 to the font-size until it wraps, then subtract 1, and voila! If the line of text is short, and the window is very wide, we’ll loop a few hundred times. What’s that? You want to adjust the text when the window is resized? That’s a few hundred more. You want to copy-fit two elements? Double. You want another one? Yes! Are you through? Not even close, bud!!

Obviously, that’s not going to work.

A Better Approach

What if I said we can find a magic number between 0 and 100,000 in less than 20 iterations – doesn’t matter what the number is, guaranteed? Yep, with a little brain-bending recursive goodness, we’ll have that sucker in no time.

function find_max( min, max, truth ) {
    var r = max - min;
    var n = min + Math.ceil(r / 2);
    if (r / 2 < 1) {
        return truth(max) ? max : min;
    } else if(truth(n)) {
        return arguments.callee(n, max, truth);
    } else {
        return arguments.callee(min, n, truth);
    }
}

function truth (i) {
    return i <= 66666;
}

find_max( 0, 100000, truth );

How It Works

With a min of 0 and a max of 100,000, we’ll ask: does 50,000 work? If it does, then the number is between 50,000 and 100,000. If not, it’s between 0 and 50,000. Either way, we’ve just eliminated half the possibilities. Not bad for a single iteration…

In the (arbitrary) case above, 50,000 does work. So, on the next iteration, we have a min of 50,000 and a max of 100,000. Does 75,000 work? Nope. So, the magic number is between 50,000 and 75,000. And so on and so forth until half the difference between min and max is less than one – at which point it must be one or the other. One more test and bingo! We’re in business.

Making It Work

To copy-fit text, we need to do three things:

  • add some css to prevent the text from wrapping as the font-size increases, and give us the actual width of the text
  • replace the arbitrary conditions in truth with code to change the font-size, and check if it’s too wide.
  • take the final result and use it as the copy-fitted font-size.

Markup

<h1 id="fitted">This text is copy-fitted.</h1>

CSS

#fitted {
    display: inline;
    white-space: nowrap; }

Script

function find_max( min, max, truth ) {
    var r = max - min;
    var n = min + Math.ceil(r / 2);
    if (r / 2 < 1) {
        return truth(max) ? max : min;
    } else if(truth(n)) {
        return arguments.callee(n, max, truth);
    } else {
        return arguments.callee(min, n, truth);
    }
}

function copyFit(element, width) {
    element.style.fontSize = find_max(0, 500, function(i){
        element.style.fontSize = i + 'px';
        return element.offsetWidth <= width;
    }) + 'px';
}

window.onload = window.onresize = function() {
    var c = document.body;
    var e = document.getElementById('fitted');
    e.style.fontSize = 0;               
    copyFit( e, c.offsetWidth );                
}

Conclusion

And there we go. A line of text that scales up or down to match the width of the body element. You could also use this in ActionScript, if you’re so inclined to scale text to fit in a TextField.

Note: this is a proof of concept, so it’ll need some bullet-proofing and cross-browser testing before being used in the wild.

Bonus

I haven’t found a use for it yet, but here’s a counter-part to find_max:

function find_min( min, max, truth ){   
    var r = max - min;
    var n = max - Math.ceil(r / 2);
    if (r / 2 < 1) {
        return truth(max) ? max : min;
    } else if(truth(n)) {
        return arguments.callee(min, n, truth);
    } else {
        return arguments.callee(n, max, truth);
    }           
}
Posted at 9am on 10/15/06 | Posted in , , , | no responses | read on