Wednesday, October 31, 2012

Building SVG Paths with Raphaël

SVG enables building really complex vector graphics.  Most of that power comes from path shapes which are constructed through a series of special commands to define points in the shape and how to connect them.  Any primitive shape (ie circle, rectangle, line, etc) can be expressed as a path so paths could be universally used to draw all of the SVG shapes you define.  The problem is learning and reading the path strings - they can get quite cryptic.  Even if you use a library like Raphael, you still will need to know how to define paths using the SVG syntax.

If that's the case, then why even use Raphael?  Well, even though you'll still need to learn how to define paths, Raphael does have some functionality to make it easier to manage them.  As I was digging around the library, it became evident that many of the utilities in the library are designed to work with paths.  There are functions for primitive shapes like circles and rectangles which will generate those SVG objects, however, they will not be compatible with the path utilities.  As much as I've avoided learning the path commands, it seemed a good time to learn the basics and see how Raphael could help me leverage them.

Once you've defined your SVG canvas via the Raphael constructor, you can use it to define a path via the cleverly named path() function.  The documentation states this function takes a string that follows the same syntax as the standard SVG Path commands.  After reading the intro to that document, there's a few things to note:

  • There are only four basic commands - move, line, curve, and close path.  Some of these have specialized commands that make some assumptions thus simplifying the syntax.

  • Case matters - upper case uses absolute coordinates whereas lower case calculates actions relative to the last command's end point.

  • These commands have parallels to the Canvas API functions.  If you've used those, then the inputs to these commands might be easier to understand.

  • The SVG documentation states that commas are not necessary, however, the Raphael reference indicates that command parameters should be separated by commas.


One thing I noticed when working with the path() function is that it will also take an array of arrays that define the path.  So instead of writing "M450,175 l0,90 a 10,10,0,0,1,-10,10 l-390,0 a 10,10,0,0,1,-10,-10 l0,-190 a 10,10,0,0,1,10,-10 l390,0 a 10,10,0,0,1,10,10 l0,90 z" to create a rectangle with rounded corners, I can write:



path = paper.path( [
['M',450,175],
['l',0,90],
['a',10,10,0,0,1,-10,10],
['l',-390,0],
['a',10,10,0,0,1,-10,-10],
['l',0,-190],
['a',10,10,0,0,1,10,-10],
['l',390,0],
['a',10,10,0,0,1,10,10],
['l',0,90],
['z']
] );



This is much more readable and feels more like chaining function calls together to construct a path. As best I can tell, all of the path handling functions in Raphael use the array syntax to represent the path definition.

Although not documented (which means it might not be the same in future releases), there are functions that will create primitive shapes as paths. Inside the Raphael._getPath object, there are several functions that take similar arguments to the built-in shape functions:


path3 = paper.path( Raphael._getPath.circle({ attrs: { cx:120, cy:120, r:50 } }))
.attr( 'stroke-width', 3 )
.attr( 'stroke', 'rgb(80,80,255)' );


As a final example that illustrates the power of Raphael's path functions, I took a fairly complex path string and transformed it using Raphael.transformPath() prior to creating the SVG element:


pstr = "M295.186,122.908c12.434,18.149,32.781,18.149,45.215,0l12.152-17.736c12.434-18.149,22.109-15.005,21.5,6.986l-0.596,21.49c-0.609,21.992,15.852,33.952,36.579,26.578l20.257-7.207c20.728-7.375,26.707,0.856,13.288,18.29l-13.113,17.037c-13.419,17.434-7.132,36.784,13.971,43.001l20.624,6.076c21.103,6.217,21.103,16.391,0,22.608l-20.624,6.076c-21.103,6.217-27.39,25.567-13.971,43.001l13.113,17.037c13.419,17.434,7.439,25.664-13.287,18.289l-20.259-7.207c-20.727-7.375-37.188,4.585-36.578,26.576l0.596,21.492c0.609,21.991-9.066,25.135-21.5,6.986L340.4,374.543c-12.434-18.148-32.781-18.148-45.215,0.001l-12.152,17.736c-12.434,18.149-22.109,15.006-21.5-6.985l0.595-21.492c0.609-21.991-15.851-33.951-36.578-26.576l-20.257,7.207c-20.727,7.375-26.707-0.855-13.288-18.29l13.112-17.035c13.419-17.435,7.132-36.785-13.972-43.002l-20.623-6.076c-21.104-6.217-21.104-16.391,0-22.608l20.623-6.076c21.104-6.217,27.391-25.568,13.972-43.002l-13.112-17.036c-13.419-17.434-7.439-25.664,13.288-18.29l20.256,7.207c20.728,7.374,37.188-4.585,36.579-26.577l-0.595-21.49c-0.609-21.992,9.066-25.136,21.5-6.986L295.186,122.908z",

path1 = paper.path( Raphael.transformPath(pstr, 't-50,-60r125s0.5') )
.attr( 'stroke-width', 3 )
.attr( 'stroke', 'rgb(255,80,80)' );


There might not seem to be any benefit to doing this because you could just as easily create the element and then transform it once its attached to the DOM. However, some of the Raphael functions don't always account for the transform on the element. By altering the path prior to creating it, those functions will work as expected.

I've added the above examples to my sandbox. In my next post, I'll dig a little deeper into how to leverage some the other Raphael path functions to build something a little more complex.