01:38 11.06.11

Another day, another Photoshop layout with drop shadows, gradients, rounded corners, whatnot, your designers gives you to slice. You do and something changes, like – column gets wider – and you slice it again, and again. And then background color changes. In the end of the day you ask yourself, isn’t there a better way? A way where you can keep all the pieces separate and change them independently, easy and fast. Well, there is.

Following I introduce you to layers in HTML and I do it backwards, I’ll show how I construct my sites and then I’ll explain how it is done in HTML & CSS.

Ok, let’s start with part of the actual design of actual website. This is what I received. Being professional slicer, you know how to handle this - top piece, center piece (set as repeatable background), bottom piece. But wait, this is resizable news box, isn’t it? And that stupid designer did vertical gradient. Hey, this CAN’T be done! Oh and by the way, before mentioning that there are -rounded-corners- etc, this layout is supposed to work even in IE6.

How does it look like?

I would like to mention that this particular layout was delivered in Indesign, so I had an opportunity not to guess but to know exact numbers (corner radiuses, stroke widths, gradient points, transparency used) of building parts and that made my work a lot easier. Following I concentrate on the right news column only.

  1. First I set body background and position container div. Filled that container with header and example body text.
  2. Then I lay background layer which is 15% white.
  3. Now I produce rectangular header image with gradient (this can be again done more granularly, but let’s leave it like it is) and force it to the size I need with width/height settings. I lay that image on top of background layer.
  4. I produce rectangular sprite image with white border and transparent cutout inside and place it four times on top of previous layers so that every one is 50% height and 50% aligning each to different corner — box with rounded corners achieved.
  5. Now to the fun part. I lay layer with left-right gradient image on top of that.
  6. Then I lay layer with top-bottom gradient on top of all previous layers. Both gradient images are stretched to the desired dimensions.
  7. So the box is done. Exactly by spces, using 4 basic images, each easy to change and tune (if needed) separately, plus the size of the box can be varied without changing any graphics at all.

How this is achieved?

CSS magic first

I’m using two specifically CSS-crafted div elements – layers and fills.

Layer is absolute positioned div, layer positions itself below or above the parent element and by default takes his exact size. Layer position is defined by z-index, usually, as by drawing backgrounds, you position layer below the actual content, If you need links to be work, DO NOT mess with z-index on ALL other elements (leave z-index intact). And as you can see I use !important on CSS properties forbidden to change for everything to work as intended.

div.lhl-layer {
    margin: 0!important;
    padding: 0!important;
    border: none!important;
    position: absolute!important;
    overflow:hidden!important;
     
    top:0; 
    left:0;
    width:100%;
    height: 100%;
}

Fill is a box that as the name suggest fills the outer box to his dimensions. On fill you need to set padding and borders. And here’s The Heureka that allows all this - CSS property box-sizing - set box-sizing to border-box! Now 100% of width and height includes padding and borders! Again margin must be 0, position relative and overflow must be hidden. You may ask what the difference between layers and fills? Fill flows naturally (no absolute positioning) and can have padding and borders, while layer cannot. Layer is the foundation, fill is the walls. Why not use box-sizing on layer too? Well, IE6 is the reason. YES everything I describe here works on IE6 too, to achieve that CSS-expressions must be used and the fewer the better is the best practice. So, let’s keep layer’s box model a traditional one.

div.lhl-fill, div.lhl-fill-width, div.lhl-fill-height, img.lhl-fill {
    margin: 0!important;
    box-sizing: border-box!important;
    -webkit-box-sizing: border-box!important;
    -moz-box-sizing: border-box!important;
    -ms-box-sizing: border-box!important;
    overflow:hidden!important;
}

div.lhl-fill, div.lhl-fill-width, img.lhl-fill {
    width:100%;
}

div.lhl-fill, div.lhl-fill-height, img.lhl-fill {
    height: 100%;
}

img.lhl-fill {
    position:absolute!important; /* image must be positioned absolute in Safari, I don't know why*/
}

Additionally I need an container box into where to place all fills and layers. Position absolute or relative and overflow hidden, this again is important, thanks to these settings container box accommodates everything you put inside it, it is important to set borders, padding and margin 0, otherwise width and height in % inside it will not work.

div.lhl-container {
    overflow:hidden!important;
    margin: 0!important;
    padding: 0!important;
    border: none!important;

    position: relative;
}

A propos! If you need, you can set paddings and margins and borders for layers and fills, but then you MUST set explicit widths and heights. This reduces HTML tag count, but adds CSS complexity and on most cases this is not a reasonable thing to do. Best is to let layers and fills get widths and heights “magically” from their parent elements.

Lets have some html.

First, container box. Set “master” width for container, all other widths inside this container will inherit for that one.

<div class="lhl-container" style="width: 200px;">
    ...
</div>

For our container to have height first we must place box with inside it first - float the text in. Text box is special fill, it fills 100% in width, but grows freely in height, as much text we have.

<div class="lhl-container" style="width: 200px;">
    <div class="lhl-fill-width">
        ...some text...
    </div>
</div>

Now when our container has dimensions let’s add some layers. Heres the important part:Layer must be containers child and contents sibling.

<div class="lhl-container" style="width: 200px;">
    <div class="lhl-fill-width">
        ...some text...
    </div>
    <div class="layer" style="z-index:-10"> ... </div>
    ...
    <div class="layer" style="z-index:-10"> ... </div>
</div>

It is wise to let layer intact and not add any CSS properties. The only properties you may use are background and opacity, for anything else use fills. You can do what you like with fills, except, remember, margins. How to overcome that limitation? Easy, use fill inside fill with padding. Like this:

<div class="fill" style="padding: 10px">
    <div class="fill">
        ... this fill now has 10px margin ...
    </div>
</div>

Continuing with our example, everything in HTML example code should be understandable now.

<!-- master layer -->
<div class="lhl-layer" style="z-index: -100;">
    
    <!- adding 2px margin for the next fill with help of this  -->
    <div class="lhl-fill" style="padding: 2px;">
        <div class="lhl-fill">
        
            <!-- 15% white background layer -->
            <div class="lhl-layer box-background" style="z-index: -50;"></div>
        
            <!-- header image on layer where height is set exactly, image fills the space --> 
            <div class="lhl-layer" style="z-index: -50; height: 24px;">
                <img src="f/nws_header.png" class="lhl-fill">
            </div>
            
            <!-- box built using sprite image -->
            <div class="lhl-layer" style="z-index: -50;">
                <div class="lhl-fill lhl-sprite-box px2-box">
                    
                    <!-- those 4 parts of box each have 50% width height and they are aligned to 4 different corners -->
                    <div class="lhl-layer bottom-right" style="z-index: -50;"></div>
                    <div class="lhl-layer bottom-left" style="z-index: -50;"></div>
                    <div class="lhl-layer top-right" style="z-index: -50;"></div>
                    <div class="lhl-layer top-left" style="z-index: -50;"></div>
                </div>
            </div>
            
            <!-- left-right gradient stretched -->
            <div class="lhl-layer" style="z-index: -50;">
            
                <!-- NB! this layer actually flows 24px outside of the box borders from bottom, but thanks to overlay:hidden we can not see this ->
                <img src="f/nws_gradient.png" class="lhl-fill" style="top: 24px;">
            </div>
            
            <!-- top-bottom gradient stretched -->
            <div class="lhl-layer" style="z-index: -50;">
                <img src="f/nws_gradient2.png" class="lhl-fill">
            </div>
            
        </div>
    </div>
</div>

For clarity, here’s CSS behind sprite-box above

div.lhl-sprite-box {
    padding: 0;
    margin: 0;
}

div.lhl-sprite-box div.top-left, div.lhl-sprite-box div.top-right, div.lhl-sprite-box div.bottom-left, div.lhl-sprite-box div.bottom-right {
    padding: 0;
    margin: 0;
    background-color:transparent;
    background-repeat:no-repeat;
    width: 50%;
    height: 50%;
    
}

div.lhl-sprite-box div.top-left {
    background-position: left top;
}

div.lhl-sprite-box div.top-right {
    left: 50%;
    background-position: right top;
}

div.lhl-sprite-box div.bottom-left {
    top: 50%;
    background-position: left bottom;
}

div.lhl-sprite-box div.bottom-right {
    top: 50%;
    left: 50%;
    background-position: right bottom;
}

Here, have a cake.

Finished example. Grab a stylesheet from here.

Pretty awesome, isn’t it, even if I say it by myself? And what’s more important, easily manageable. Content (text) is fully separated from background and independent, you do not have to worry about insets etc. HTML code is manageable and understandable, when compared to the usual tag soup for example in here http://www.schillmania.com/projects/dialog2/. All graphics are simple and reusable. A fully working website using this technique can be observed here, be aware, its not in English (yet), but this shouldn’t matter.

That’s it. Enjoy XXI century of HTML layout and if everything goes well, the follow-up on how to use layers with grid based layout and what great and impossible things you can do, is coming.

10:27 23.02.10

and have really nice VML rendering in IE as a bonus.

Why?

One day I thought that if I need to use SVG’s for my web layouts (and I do) the better way is to draw them with the tool I’m used to - Adobe Illustrator - and just convert them into Javascript. Easier said than done, it seem that there’s no public project for this. What the heck, I have done something like this before and as SVG is pure XML all I need to do is write some small PHP script. So here we go. I have a logo I use in one of my client sites, let’s use it as a test.

Parsing SVG

I set up simple PHP XML parser with start and end tag handlers. The first issue is that Raphaël does not support groups directly, but fortunately has sets. When you set set’s attributes they are propagated manually to the individual elements, so when converting one must not set attributes when declaring that set, but rather collect that groups attributes and set them only after closing that group.

class svgToRaphaelParser {

    var $result = "\n"; // output string with Raphael code
    var $group_stack = array(); // holds group arguments
    var $default_shape_attrs = array('stroke' => 'none'); // add those if not present 
    var $nl = ";\n"; // js line ending
    var $container = 'this'; // Raphael containre name
    var $canvas = 'c'; // Raphael canvas name
    var $group = 'g'; // Raphel set name


    function parse($fname, $container = 'container') {
        $parser = __CLASS__;
        $p = new $parser();
        return $p->_parse( @file_get_contents($fname) );
    }

    function _parse(&$str) {
        if ( !($this->xml_parser = @xml_parser_create()) ) 
            return false;
        @xml_set_object($this->xml_parser, $this);
        @xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, 0);
        @xml_parser_set_option( $parser, XML_OPTION_SKIP_WHITE, 1 );
        @xml_set_element_handler($this->xml_parser, 'startElement', 'endElement');
        if ( !xml_parse($this->xml_parser, $str, true)) {
            return sprintf("XML error: %s at line %d\n", xml_error_string(xml_get_error_code($this->xml_parser)), xml_get_current_line_number($this->xml_parser));
        }
        @xml_parser_free($this->xml_parser);
        return $this->result;
    }

    /*
        attrs - array of attributes from XML parser
        ignore - array of names of attributes to ignore
        defaults - array of attributes to force, if not present in attrs

        result is string of Rapheal attr function - .attr({'stroke': 'none', 'fill' : '#000'})

        basically all svg attrubutes can be added as attr, so there's no need to parse x=0 y=0 width="100" height="100"
        into rect(0,0,100,100), rather we can parse it more universally (for other shapes too) as
        rect().attr()
    */
    function attrs2attr($attrs, $ignore = array(), $defaults = array(), $res = '' ) {
        foreach(array_merge($defaults, $attrs) as $key => $val)
            if ( !in_array($key, $ignore) )
                $res .= ",'$key':'$val'";
        return strlen($res) ? ".attr({" . substr($res, 1) . "})" : '';
    }   

    /*
        keys - string or array of strings of attribute names to include as Raphael function argument
        attrs - array of attributes from XML parser
        args - resulting array of arguments
        ignore - resulting array of attributes to ignore

        f.e. svg path tag has argument 'd', which is needed as first argument of Raphael.path() function, so its added
        to args and ignore

    */
    function attrs2args($keys, $attrs, &$args, &$ignore) {
        $keys = is_array($keys) ? $keys : array($keys);
        foreach($keys as $key):
            $args[] = $attrs[$key];
            $ignore[] = $key; // when attribute goes for argument, then ignore
        endforeach;
    }

    /*
        name - shape name
        attrs - attributes from XML parser
    */
    function shape2raph($name, $attrs, $ignore = array(), $args = array() ) {
        switch ($name):
            case 'path':
                /* on path we must set 'd' as path argument */
                $this->attrs2args('d', $attrs, $args, $ignore);
                break;
            case 'image':
                $this->attrs2args('xlink:href', $attrs, $args, $ignore);
                break;
        endswitch;
        $line = sprintf("%s.%s(%s)%s", $this->canvas, $name, count($args) ? "'" . implode("','", $args) . "'" : '', $this->attrs2attr($attrs, $ignore, $this->default_shape_attrs) );
        $this->result .= ($c = count($this->group_stack)) ? sprintf("%s%s.push(%s)%s", $this->group, $c, $line, $this->nl) : $line . $this->nl;
    }

    function startElement($parser, $name, $attrs) {
        $name = strtolower($name);
        switch($name):
            case 'svg':
                if ( isset($attrs['viewBox']) ):
                    list($x, $y, $w, $h) = explode(" ", $attrs['viewBox']);
                else:
                    $x = 0;
                    $y = 0;
                    $w = str_replace('px', '', $attrs['width']);
                    $h = str_replace('px', '', $attrs['height']);
                endif;
                $this->result .= sprintf("var %s = Raphael(%s, %s, %s, %s, %s)%s", $this->canvas, $this->container, ceil($w), ceil($h), floor($x), floor($y), $this->nl );
                break;

            case 'g':
                //start group, put arguments in stack
                $this->group_stack[] = $this->attrs2attr($attrs, array('display'), array());
                $this->result .= sprintf("var %s%s = %s.set()%s", $this->group, count($this->group_stack), $this->canvas, $this->nl);
                break;

            case 'path':
            case 'image':
            case 'rect':
            case 'ellipse':
            case 'circle':
                $this->shape2raph($name, $attrs);
                break;

            case 'line':
                // emulate line with path
                $attrs['d'] = sprintf("M %s %s L %s %s", $attrs['x1'], $attrs['y1'], $attrs['x2'], $attrs['y2']);
                $this->shape2raph('path', $attrs, array('x1','x2','y1','y2') );
                break;

            case 'polyline':
            case 'polygon':
                // emulate polyline and polygon with path
                $f = strpos($attrs['points'], ' ');
                $attrs['d'] = sprintf("M %s L %s%s", substr($attrs['points'], 0, $f-1), substr($attrs['points'], $f+1), $name == 'polygon' ?  'z' : '');
                $this->shape2raph('path', $attrs, array('points') );
                break;

        endswitch;
    }

    function endElement($parser, $name) {
        $name = strtolower($name);
        switch($name):
            case 'g':
                // if group had attributes, then set them now
                if ( strlen($str = array_pop($this->group_stack)) )
                    $this->result .= sprintf("%s%s%s%s", $this->group, count($this->group_stack)+1, $str, $this->nl);
                break;
        endswitch;
    }

}

Nothing difficult in converting. Basically its just converting SVG attribute array into Raphaël attr() function and simple creating Raphaël shapes.

NB! Text is not supported at all (not interested), image support is probably broken (not interested), no support for transform attribute either. For full support of transform Raphaël needs to matrix and skew (as addition to the scale, rotate, etc). Any takers to implement?

Modifying Raphaël

So far so good. But as I digged deeper I noted that Raphaël is very nice and so on, but absolutely does not support real SVG files. You just cannot convert anything more than simple shapes into Raphaël’s code.

And that’s a problem because Raphaël is written in way that you cannot extend or rewrite any of crucial pieces needed, in javascript no outside access for private variables and methods exist, so the only option is to fork and write your own version of Raphaël. Which is kind of dumb thing to to, because you need to do that again when new version arrives. But what the hell.

Strokes

Raphaël, has hardcoded (?!? why) default black 1px stroke for shapes. So when converted shapes without stroke are drawn they always have that default stroke, unless you watch if there’s is a stroke definition and when not you force stroke:none. Which I did in the PHP code. Better way perhaps is to replace that hardcoded #000 with variable in Raphaël and set that to none.

Fill-rule

Raphaël, does not support fill-rule attribute. Private property availableAttrs needs to be extended with

    "fill-rule": "nonzero" 

and

    case "fill-rule":
        node[setAttribute](att, value);
        break;

handler is needed in setFillAndStroke private function.

viewBox

This was easy and is perhaps sufficent for somebody, but I needed viewBox support which Raphaël deeply lacks. I need that because when one draws in Illustrator then its pain in the donkey to do it exacly with dimensions needed for the final result needed for web. The right thing to do is just to draw (it’s a vector, it doesn’t matter) and not to think about width and height and then scale that SVG down with Raphaël. Also as I discovered later, there’s A VERY BIG BONUS for Internet Explorer hidden. IE does not draw vectors nicely in small scale, I think everybody touched Raphaël had noticed that. The solution is simple - to create BIG shapes (like in thousands of pixels) and scale them down. I’ll show it in the end of this article.

As it turned out, adding viewBox to SVG part of the Raphaël was rather easy. Adding to the VML part was fucking hard. But I got it covered eventually. Added function setView(width, height), for scaling. BTW setSize() does not work as a result, as for now, didn’t needed it, didn’t fix it.

Here’s how I did it.

Extending constructor.

Support for viewBox absolutely needs support for all four (x,y,width,height) parameters, because somehow one just cannot set x,y to 0,0 in Illustrator. Or one can, but I don’t know how and anyway standard supports not just 0,0.

So the first step is extend the constructor to support following syntax and not breaking existing things:

    var canvas = new Raphael(container, width, height, x, y);

in getContainer

    return {container: container, width: arguments[1], height: arguments[2]};

add:

    , x: arguments[3], y: arguments[4]

Now x&y go through to the create function.
Nothing to fear when you do not need viewBox support, use as usual, set only width, height, nothing will change to you.

Add setView() function to Paper

after: Paper[proto].setSize = setSize; add: Paper[proto].setView = setView;

SVG part

in function create

after:

    width = width || 512;
    height = height || 342;

add:

    x = x || 0;
    y = y || 0;

for cnvs:

        $(cnvs, {
            xmlns: "http://www.w3.org/2000/svg",
            version: 1.1,
            width: width,
            height: height
        });

add property:

    viewBox: x + " " + y + " "+ width + " " + height

after function SetSize() definition add:

    var setView = function (width, height) {
        width = width || this.width;
        height = height || this.height;
        this.canvas[setAttribute]("width", width);
        this.canvas[setAttribute]("height", height);
        return this;
    };

VML part

VML does not support viewBox directly, to achieve desired result we wrap special group element around all other elements.

Replace g.style.cssText and add left & top support.

    g.style.cssText = "position:absolute;left:"+vml.x+"px;top:"+vml.y+"px;width:" + vml.width + "px;height:" + vml.height + "px";

in functions thePath, theCircle, theRect, theEllipse, theImage and theText.

NB! add

    var vml=VML;

in he beginning of thePath (for some reason the parameter is VML there, instead of vml elsewhere), for theText and thePath

after:

    ol.width = vml.width;
    ol.height = vml.height;

add:

    ol.top = vml.y + "px";
    ol.left = vml.x + "px";

Function create needs to be modified rather heavily. Here’s short the explanation:

  • coordorigin and coordsize will be set to the exact x,y and width, height set in constructor (not 0,0 1000,1000)
  • a wrapper group element is created (nb! on that group always top=0 and left=0) and added as property to the paper
  • Raphaël changes container width & height, this is nixed
  • clip:rect style of wrapper div is nixed

    var create = function () {
        var con = getContainer[apply](null, arguments),
            container = con.container,
            height = con.height,
            s,
            width = con.width,
            x = con.x,
            y = con.y;
        if (!container) {
            throw new Error("VML container not found.");
        }
        var res = new Paper,
            c = doc.createElement("div"),
            cs = c.style;
        res.wrapper = c;
    
        width = width || 512;
        height = height || 342;
        x = x || 0;
        y = y || 0;
    
        var vb = createNode("group");
    
        vb.coordsize = width + " " + height;
        vb.coordorigin = x + " " + y;
    
        vb.style.cssText = R.format("position:absolute;left:0;top:0;width:px;height:px", width, height);
    
        res.width = width;
        res.height = height;
    
        res.x = x;
        res.y = y;
    
        res.coordsize = vb.coordsize;
        res.coordorigin = vb.coordorigin;
    
        res.span = doc.createElement("span");
        res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";
    
        c[appendChild](res.span);
        cs.cssText = R.format("width:px;height:px;position:absolute;", width, height);
    
        c[appendChild](vb);
        res.canvas = vb;
    
        if (container == 1) {
            doc.body[appendChild](c);
            cs.left = x + "px";
            cs.top = y + "px";
        } else {
            if (container.firstChild) {
                container.insertBefore(c, container.firstChild);
            } else {
                container[appendChild](c);
            }
        }
        plugins.call(res, res, R.fn);
        return res;
    };
    

Because of that wrapper group also clear() needs some changes, instead of recreating span property I just set innerHTML to empty string

delete this:

this.span = doc.createElement("span");
this.canvas[appendChild](this.span);

add this:

this.span.innerHTML = E;

after function SetSize() definition add:

    setView = function (width, height) {
        var cs = this.canvas.style;
        var w = this.wrapper.style;
        width == +width && (width += "px");
        height == +height && (height += "px");
        cs.width = w.width = width;
        cs.height = w.height = height;
    return this;
    },

Done! I’m pretty sure I did break something somewhere, but so far everything I need works perfectly.

Result

So now to the point, why all this trouble?

Original

This is the SVG converted from Illustrator file (default size I had) 190px x 154px. (btw all those images below are screenshots, not SVG)

Safari

Internet Explorer

It’s not pretty in IE isn’t it?

Scaled up

Now the first trick, let’s scale that SVG twice the size: c.setView(“380px”, “308px”);

Safari

Internet Explorer

Nice, but IE is still ugly

Scaled down

And now The Big Prize, I did open that same file in Illustrator, resized it a lot and saved, the result is SVG with viewBox of “-170 531.889 1304.02 1061.449”. As you can see, Illustrator set x&y to not zero and I just cannot get it back to 0&0, so that’s why I implementd full (not just width & height) viewBox support. This is so big image in original that I show you only the downsized version: c.setView(“380px”, “308px”);

Safari

Internet Explorer

BOOM!

There you have it - SVG converter and nice VML in IE!

Demo

I set up a demo page with real SVG’s here, of course you need to view that with IE to see some differences, for all other browsers - nothing to see there.

Download

My version of Raphaël and SVG to Raphaël converter. And if you need, the SVG files used: original and supersized.

Credits

Kudos to Vector Converter for the idea of wrapper group in VML. At least I got it from there…