A List Apart

Menu

Sliced and Diced Sandbags

by Published in CSS, The Server Side, Layout & Grids · 44 Comments

A note from the editors: This article, a fascinating glimpse into the strange desires of mid-2000s web designers, is now obsolete.

A few weeks ago, while walking through the beautiful Sussex countryside, I decided I wanted to find a way to automate text-wrapping around images with irregular outlines.

Issue № 222

Seriously, I need to get out more. This article is the result of that ramble through the hills.

The concept

The concept is simple. We want to create a series of “sandbag” divs that we’ll lay over our image; it will be these sandbags and not the actual image that the text will flow around. But we don’t really want to do that ourselves: we want to get PHP to do it for us.

Step 1: Working out the size of the sandbags

We’ll begin by creating an array with the sizes and positions of our “perfect” sandbags. Our “perfect” sandbags would be only 1px high to allow for the smoothest text flow; working these out manually would be absurd, so let’s find a technique that works!

Our sample image, which we want to right-align, currently looks like this:

Our sample image, with left-aligned text.

Looking at our image, I know how I’d size up the sandbags by eye, and that’s exactly what we’re going to do with PHP. We scan along from left to right, if we hit a transparent pixel we think “ooh, let’s go a bit further;” if we hit a solid pixel we say “righto, that’s where that sandbag needs to be.” Then we’ll go down to the next row of pixels and repeat this process until we hit the bottom of the image.

To put it a tad more technically, we want a loop that scrolls through the Y axis, starting at 0 and finishing at the height of the image. For each row, we’ll scroll through the X axis, starting at 0 and ending at the width of the image. Each time we hit a transparent pixel, we’re going to add 1 to the array for that row. When we hit a non-transparent pixel, we’re going to break out of our X loop and go to the next row of pixels. To put it even more technically, that translates to the following code (line wraps marked » —Ed.):

<?php$image  = imagecreatefrompng( 'an_image.png' );
$width  = imagesx( $image );
$height = imagesy( $image );for ( $y=0; $y < $height; $y++ ){
  $imagemap[$y] = 0;
  for ( $x=0; $x < $width; $x++ ){
    $c = imagecolorsforindex( $image, »
imagecolorat( $image, $x, $y ) );
    if ( $c['alpha'] < 127 ){
      break;
    } else {
      $imagemap[$y]++;
    }
  }
}?>

Phew—surely that’s one of the hardest bits out of the way. The imagecolorsforindex function here is great isn’t it? It returns an array of the red, green, blue, and alpha components. If the alpha component of any given pixel is less than 127, then the pixel isn’t fully transparent.

Positioning our bags

The CSS we’re using is pretty minimal. However it turns out that Internet Explorer doesn’t like having a div only 1px high, so we need a little bit of trickery. To keep IE happy with our 1px high sandbags we use the following CSS:

.sandbag-right {
  border: 0; 
  padding: 0;
  font-size: 0;
  margin: 0 0 -1px 0;
  height: 2px;
  float: right; 
  clear: right;
  background: red;
}

By setting the font-size to zero, we can achieve 2px-high divs. If we then throw in a negative bottom margin of 1px before IE really notices what’s going on, we can make them appear to be 1px high. Bingo.

(The red background is just so we can see the sandbags at this stage; we’ll be removing this later.)

Laying down the sandbags

We’ve now got everything in place, so let’s create those sandbags. This requires a simple foreach loop through our $imagemap array. To get the width of our sandbag, we need to remember it’s not the value we’ve already calculated that we want, as that’s actually the size of the empty space next to the sandbag. The actual width of our sandbag is the width of the image minus the value in our array.

We’re going to use the printf function to echo our sandbags into a template. This will make things neater in the long run. (line wraps marked » —Ed.)

<?php$sandbagTemplate = '
';foreach ( $imagemap as $position => $blankPixels ){ $sandbagWidth = $width-$blankPixels; printf( $sandbagTemplate,$sandbagWidth ); }?>

Step 1: The text-wrap so far.

You can check out the code for step one.

Step 2: Too much of a good thing…

Now we have an array that lets us generate a perfect series of sandbags. After a bit of testing, we see, somewhat annoyingly, that Opera seems to be the only browser in which these “perfect” sandbags work as expected. In most other browsers, we find that our text disobediently overlaps our sandbags.

I tried playing around with various margin sizes, but this just didn’t cut the mustard. Finally, I reluctantly concluded that we needed less-perfect sandbags. Bigger than 1px. But how big? Well, let’s leave that up to the user. I found that 10px to 50px seemed to work quite well, but it may well vary, so we’ll leave it flexible in the function.

Although we’ve found out how to trick IE into allowing us to have those 1px-high sandbags, there’s not really much point if they don’t work, and our function becomes much neater if we enforce a 2px-high minimum. It was upsetting to lose this neat trick, but I took a deep breath and disallowed 1px-high sandbags from the function. (Don’t be sad, this knowledge will come in handy later on.)

Less than perfect

Now we want to loop through that “perfect” array we generated earlier. If we have a sandbag height of 10px, we would want to look at the array in clusters of 10, taking the largest sandbag from each cluster and then outputting that value into a new array. Of course, rather than using a constant number, we’ll just use our variable $sandbagHeight.

The largest sandbag from each section is actually the one with the smallest value in the array (as the array represents the transparent dead space and not the actual sandbag), so we’ll use the handy PHP function min to return the lowest value from the array.

The resulting loop looks like this:

for( $i=0;$i < count($imagemap ); $i = $i+$sandbagHeight) {
  for( $x=0; $x < $sandbagHeight; $x++ ){
    $b = $x + $i;
    if( isset( $imagemap[$b] ) ){
      $section[$b] = $imagemap[$b];
    }
  }
  $sandbag[] = min( $section );
  unset( $section );
}

We also need to set the height of each sandbag. We’ll do that by adding it to our sandbag template (line wraps marked » —Ed.):

$sandbagTemplate = '
';

We’re going to want the possibility of adding some extra padding above the first sandbags and below the last sandbag, and we can easily isolate the first and last item of the array and append an additional style to these as required.

If we now use our $sandbag array instead of our $imagemap array to generate the sandbags we get the following:

Step 2: Sandbags sans our sample image.

You can see how the code is looking in step two.

Step 3: Placing the image behind the sandbag

We’re on the homestretch now. We’ve got the sandbags. All we need to do now is stick the image behind them.  What could be simpler?

All we do is… ur… no, wait… we just…

… oh. Oh dear. This was meant to be the easy bit, and looking at it now it turns out it’s quite a big hill.

I’d always just imagined I’d either a) be able to just give the surrounding div a background image or b) actually use an img.

The problem is, the second we give the surrounding div a specific size in order to give it a background image, the text starts to wrap around the div. It overpowers our sandbags. The same goes for putting an img in there. I tried playing around with z-index layers, but every time I thought it was going to work, it fell apart in one browser or another. For the longest time, this had me utterly stumped.

We’ve got our sandbags and they work… it’s just we can’t seem to put anything else in front of our sandbags without destroying their functionality…

Using what we’ve got

… so let’s not put anything else in front of the sandbags.

This is where some radical thinking comes in.

Let’s use the actual sandbags themselves. Why not just give each sandbag the same background image and position that background image relative to each sandbag’s position.

Does it work? Oh yes it does.

Step 3: Sandbags with the sample image in place.

Here’s the code for step three.

Step 4: Adding a pseudo alt attribute

We’ve now got a working function, but we have to accept that we’ve lost a little bit of accessibility in doing this. Fear not, we can add pseudo alt and title attributes to the image. We’ll set these from the function, and put this new variable in an outer div as a title. We’ll also throw it in there as a hidden span before the first sandbag. That way if we turn off the stylesheet we get our alt text, and if we hover over anywhere on the image we get our title attribute.

We’re also going to add a no-repeat to the background, which means we never get the top of the image repeating again in the final sandbag.

Our stylesheet now looks like this:

.sandbagImage span {
  display: none;
}.sandbagRight {
  border: 0; 
  padding: 0;
  font-size: 0;
  margin: 0 0 0 25px;
  float: right; 
  clear: right;
  background: no-repeat;
}</style>

When we create the outer div we’re going to use the following code give us the option of including an alt attribute:

if($alt != ''){
  echo '
' . $alt .''; } else { echo '
'; }

(You don’t need to see the code for this, as it’s a minor tweak.)

Step 5: Keeping Safari happy

There’s a fight brewing here between two browser “constraints” that face us here.

In Safari, that “no-repeat” we added doesn’t work where there is negative background positioning, and so if the final sandbag is too big, we get the top of the image repeating in the final sandbag. That’s not good.

So what we can do is calculate the size of each sandbag as we add it to the array. The final time the variable is set will be the size of the final sandbag.  We do that by adding a simple “count” to our second loop:

for( $i=0;$i < count( $imagemap ); $i = $i+$sandbagHeight ){
  for( $x=0;$x < $sandbagHeight; $x++ ){
    $b = $x + $i;
    if( isset( $imagemap[$b] ) ){
      $section[$b] = $imagemap[$b];
    }
  }
  $sandbag[] = min( $section );
  $finalSectionSize = count( $section )-1;
  unset( $section );
}

Step 4: Safari-friendly sandbags.

Take a look at the code for step four.

Keeping IE happy

But we still can’t drop that no-repeat. Why not? Well…

Let’s suppose we have an image that is 121px high, split into 10px-high sandbags. Our final sandbag is going to be 1px high.

But as we know, in Internet Explorer we can’t have 1px high divs without some CSS hacking. If we give the final sandbag a negative bottom margin, we’re then going to have to go through a palaver of adding an extra “fake” sandbag to re-establish our bottom margin if required. We don’t want to do this.

That no-repeat still needs to be there because in this situation it could result in the top row of pixels in the image being repeated at the bottom.

Aaaand the code for step five.

Step 6: Allowing for left-alignment

The function as I’ve explained it only allows for right aligning. To left-align, we have to keep a few simple things in mind:

  1. The CSS for our sandbags should float and clear left, not right.
  2. When looping through our initial array we work from right to left, so we start at the width of the image and end at 0, subtracting from x each time instead of adding to it.
  3. The x axis of the background position is always 0px.

I won’t walk through these steps, but…

Our final function

…we can combine everything together into one final glorious function including the option for left-alignment, our last few bits of cross-browser fixing, and some error checking. The final function is simple to call (line wraps marked » —Ed.):

<?php alignedImage( 'an_image.png', 'right', »
'A right aligned blob',30 ); ?>

You can see the code for the final function: step six. Yay!

44 Reader Comments

Load Comments