Quantcast
Channel: sixserv blog » Web
Viewing all articles
Browse latest Browse all 10

jQuery-SVG UI

0
0

Recently I played a little with SVG, using the JavaScript library Raphael and later jQuery-SVG. I want to share some of my experiences with it. My test setup of browsers include:

Chrome 20.0.1132.57
Firefox 12.0
Opera 11.60

The idea I had, involved creating a user interface controlled by JavaScript. For the sake of this article I limit this to the following: Multiple windows constructed out of a border and background, a title bar and a ‘knob’ for resizing, that only appears on hovering. The user should be able to move the window by dragging the title bar and to resize the window by dragging the resize knob, both events should bring the window to the front.

Because I’d never done anything before with SVG, I used Raphael at first, a very easy to use API. In Raphael, I created a Set with the various elements: paths for the outline, title and resize knob. To move and resize the window I used the drag event provided by Raphael.

The problem with this, that I only later realized, is that Sets are just arrays to logically group elements together, they are not grouped within the SVG document. If you change a transformation on a Set, Raphael is manually updating all transformations on all elements within that Set, a potentially very expensive operation. This is how our window document would look like with Raphael:

<svg version="1.1">
  <path d="[window border path string]" transform="translate(100, 100)" />
  <path d="[window title path string]" transform="translate(100, 100)" />
  [...]
  <path d="[bottom right resize button]" transform="translate(100, 100)" />
  [...]
</svg>

Turns out SVG already supports real element grouping (d’oh) with the <G> element! That was the moment I ditched Raphael (even though there is an experimental plugin that uses <G> for sets, but who knows what else is done this inefficiently?). jQuery-SVG is a nice alternative that provides a more direct API for SVG manipulation.

The same document using this approach would look something like this:

<svg version="1.1">
  <g id="window_id" transform="translate(100, 100)">
    <path d="[window border path string]" class="outline" />
    <path d="[window title path string]" class="title" />
    [...]
    <path d="[bottom right resize button]" class="resize">
  </g>
</svg>

Another interesting SVG feature I explored was the <DEFS> and <USE> elements. Its possible to declare groups within <DEFS> and to instantiate it with a <USE> later on, <USE> provides X and Y attributes so you don’t even need to transform/translate anything! That sounded really great in theory, but then the browser bugs started coming in:
Firefox has an 8 year old bug that prevents any events of elements within <DEFS> to be propagated (and a related bug: .getBBox() is also broken). Chrome and Opera work great until you discover their inability to show :hover effects correctly. At the end and after a lot of wasted time on (not so) possible workarounds, I decided to forget about <DEFS> and <USE> completely.

To theme the window you can use a stylesheet like this:

.outline, .title {
    fill: white;
    stroke: black;
}
.title:hover {
    fill: black;
}
.resize {
    fill: none;
    pointer-events: all; /* otherwise no mousemove event would be propagated */
}
.resize:hover {
    fill: black;
}

Now to create the window, I first created functions that return the paths based on the current dimensions of the window (you could also use translate in some cases, has that an impact on performance? Idk):

function outlinePath(width, height) {
    return [
        'm', 0.5, 0.5,
        'h', width,
        'v', height - 10,
        'l', -10, 10,
        'h', -width,
        'v', -(height-10),
        'l', 10, -10,
        'z'].join(' ');
    }
function titlePath(width) {
    return [
        'm', 5.5, 5.5,
        'h', width-10,
        'v', 10,
        'l', -10, 10,
        'h', -(width-10),
        'v', -10,
        'l', 10, -10,
        'z'
    ].join(' ');
}
function resizePath(width, height) {
    return [
        'm', width + 1.5, height - 10 + 1.5,
        'v', 10,
        'h', -10,
        'z'
    ].join(' ');
}

and to create the svg structure procedurally:

var x = 50, y = 50, width = 100, height = 100;
var g = svg.group({id: 'window_id'}); 
function moveWindow(x, y) {
    svg.change(g, {transform: 'translate('+x+','+y+')'});
}
var outline = svg.path(g, outlinePath(width, height), {class_:'outline'});
var title = svg.path(g, titlePath(width), {class_:'title'});
var resize = svg.path(g, resizePath(width, height), {class_:'resize'});
moveWindow(x, y);

first lets allow to move the window by dragging the title:

var lastDragX, lastDragY;
var mousemove = function (event) {
    moveWindow(event.clientX - lastDragX, event.clientY - lastDragY);
    lastDragX = event.clientX;
    lastDragY = event.clientY;
}.bind(this);
var mouseup = function (event) {
    $(window).unbind('mousemove', mousemove);
    $(window).unbind('mouseup', mouseup);
}.bind(this);
$(title).mousedown(function (event) {
    lastDragX = event.clientX;
    lastDragY = event.clientY;
    $(window).bind('mousemove', mousemove);
    $(window).bind('mouseup', mouseup);
    event.preventDefault();
}.bind(this));

almost the same for resize, just instead of moving the window:

width += event.clientX - lastDragX;
height += event.clientY - lastDragY;
svg.change(outline, {d: outlinePath(width, height)});
svg.change(title, {d: titlePath(width)});
svg.change(resize, {d: resizePath(width, height)});

To bring a window to the front you can just do this:

svg.root().appendChild(group);

You can look at a demo here.

This looks really easy now, but it took me a while to get to this point. I recommend anyone who wants to do something serious with SVG, to look at the specs and to use a low level API, also make sure to look at different browsers to see what bugs they throw at you.


Viewing all articles
Browse latest Browse all 10

Latest Images

Trending Articles





Latest Images