Wednesday 22 May 2013

Adding text strokes with jquery and css text shadows

Due to the fact that css text stroke is not widely supported I decided to roll own using jquery, CSS text shadows, and a little bit of trigonometry.

It's easy enough to do, because CSS allows you to apply multiple text shadows to an object. If you set the blur on these to zero, and place them evenly around your text, you get a stroke.

A single CSS text-shadow rule looks like

.classname {
    text-shadow: 1px 1px 2px #ff0000;
}

giving a 1px offset in each direction, a 2px blur and a nice red shadow (urgh).

Multiple shadows can simply be joined by commas, like so

.classname {
    text-shadow: 1px 1px 2px #ff0000, 0px 1px 3px #00ff00, 1px 3px 2px #ffff00;
}

(Don't try this at home kids, it'll look disgusting).

The trick here is working out where to put the stroke in relation to the text. I want a smooth stroke with rounded corners and caps (I might extend this to square caps, but not right now!), so I'm going to place my strokes in a circular pattern. Imagine a full stop/period, then add a stroke above, right and down a bit, right and down a bit, reach 3 o'clock, down and left a bit, down and left a bit, 6 o'clock etc.

But how to calculate exactly where to put these shadows?

This is easy with the parametric equation of the circle, which is:

x = r * cos(t)
y = r * sin(t)

Don't worry about the specifics of this equation, just trust me that X and Y are the number of pixels up or down put our shadow and r is the width of the shadow. The variable t just steps around our circle, from 0 to 2π (360°) and gives us the x and y position.

So, to the code.

First, this is a jQuery plugin, so we start with

$.fn.textStroke = function(r, colour) {

r is the radius in pixels and colour is a hex code, e.g. '#ff0000'

var rules = [];
var steps = 24;

I'm creating an array to add each CSS text-shadow rule to as we go around the circle, and I'm using 24 steps - this is purely subjective. 24 steps was necessary for a smooth stroke on a big header, but 12 will probably do for smaller text. Note I've gone for multiples of 4 so that each quadrant has the same number of text-shadows.

for (var t=0;t<=(2*Math.PI);t+=(2*Math.PI)/steps){

Here i'm looping through a full circle in radians (2π radians to 360°, kids!), in the number of steps set above.

var x = r*Math.cos(t);
var y = r*Math.sin(t);

Here we get our x and y values. And we're done for this step! Except we're not, because sometimes things break. Because we'll convert x and y to strings in a minute, we hit a problem when we get really really tiny but not quite zero numbers at certain points around the circle - numbers like 5.2485234e-013 (a very, very small number) aren't understood by CSS, so...

x = (Math.abs(x) < 1e-6) ? '0' : x.toString();
y = (Math.abs(y) < 1e-6) ? '0' : y.toString();

...if the number is less than one millionth, we set it to zero. And convert to strings here because I like messy code. Finally, we make a rule from these values and add it to the rules array...

rules.push( x + "px " + y + "px 0px " + colour );

and continue the loop...

Finally, we join the rules we've created and apply using the css function in jQuery.

this.css('textShadow',rules.join());

Note I haven't mentioned vendor-specific prefixes here - that's because, thankfully, jQuery generates them automatically.

And that's it!

$.fn.textStroke = function(r, colour) {
 
var rules = [];
var steps = 24;

for (var t=0;t<=(2*Math.PI);t+=(2*Math.PI)/steps){
 
var x = r*Math.cos(t);
var y = r*Math.sin(t);

x = (Math.abs(x) < 1e-6) ? '0' : x.toString();
y = (Math.abs(y) < 1e-6) ? '0' : y.toString();

rules.push( x + "px " + y + "px 0px " + colour );

}

this.css('textShadow',rules.join());

};

So, all we have to do to add a lovely text stroke to any element is to do

$('#thingtobestroked').textStroke(5, '#ffffff')

and we're done.

There's a jsfiddle demo at http://jsfiddle.net/KjyYV/ , and you can check out my crappy code at my GitHub repo. It's currently a mess and doesn't follow the jQuery boilerplate, because I'm lazy and hacked this together in ten minutes, but it'll improve!