Working with SVG

Dynamically Modifying SVG

Since we can now use JavaScript code to modify SVG images, we can do that work anywhere we want in our code. In particular, we can draw things as the result of a user event, the same way we have modified the HTML page in the past.

For example, we can include a <button> on our page, along with the empty container for an SVG image:

<div id="container"></div>
<button id="draw">Draw more</button>

Then in our setup function, we can create Raphaël paper object and connect the button's click event. We'll use the paper variable later in another function.

setup = function() {
  paper = Raphael('container', 50, 50)
  $('#draw').click(more)
}
$(document).ready(setup)

Finally, we can respond to the button click, and use a variable so we draw something slightly different each time.

radius = 20
more = function() {
  paper.circle(25, 25, radius)
  radius = radius - 3
}

The variable radius starts at 20 and decreases by 3 each time the function more runs.

You can test that page if you want: a circle should be drawn each time you click the button. Here's what it looked like for me after a few clicks:

Dynamic SVG after a few button clicks
Dynamic SVG after a few button clicks

The point here is that you can draw on the paper object when it is first created (probably in your setup) function, but also any other time you have JavaScript code running, such as in response to a user event. This will let us create pages where we dynamically create images for the user.

Paths: Lines and Curves

It's possible to draw lines and curves in images with the paper.path() function, but the way needs some explanation. The paper.path() takes one argument: an SVG path string which is a very compact way to specify a bunch of information about curves and lines.

Lines

We can start by creating our paper and drawing a simple line from (50,20) to (390,30) like this:

paper = Raphael('container', 400, 270)

p1 = paper.path('M50,20 L390,30')
p1.attr({'stroke': '#f00', 'stroke-width': '3'})

The path string here is 'M50,20 L390,30' which says “move (M) to the point (50,20) and draw a straight line (L) to (390,30).

If you'd like to see the result (along with the other paths drawn here), see the figure below. This line is red (as you should be able to guess from the .attr() call, with apologies to any colour-blind readers), and is labelled “p1”.

Remember that (0,0) is in the upper-left, so the “start” of the line at (50,20) is the left end, and (390,30) is the right (and slightly lower) end.

Several of these can be combined to make a multi-segment line:

p2 = paper.path('M50,40 L260,10 L280,40 L390,40')
p2.attr({'stroke': '#0c0', 'stroke-width': '3'})

This path starts at (50,40) and has straight line segments from there to (260,10), then (280,40), and finally (390,40). It is green and labelled “p2” below.

You can see that the path string is made up of several “commands” that need a point: “move to (50,40)” is written 'M50,40' and “draw a line to (260,10)” is written 'L260,10'. The path string is a series of these commands that will be used to draw the path.

The Z command in a path string means “close the path” or to draw a straght line back to the starting point. This example starts with two striaght line segments (just like before) and ends by closing the path to make a triangle:

p3 = paper.path('M50,60 L260,30 L280,60 Z')
p3.attr({'stroke': '#00d', 'stroke-width': '3'})

It is blue and labelled “p3” below.

Curves

There are more options for drawing curves: there are more ways to draw curves and more parameters for how they might look.

Probably the easiest way to draw a curved line is to use the “curve through” command T which draws a curve (of some shape) through the points you choose. This draws a curve startin at (50,80), through (150,80) and (150,100), ending at (390,80):

p4 = paper.path('M50,80 T150,80 T150,100 T390,80')
p4.attr({'stroke': '#f70', 'stroke-width': '3'})
paper.circle(150, 80, 3).attr({'fill': '#f70'})
paper.circle(150, 100, 3).attr({'fill': '#f70'})

It is labelled “p4” below and drawn in orange. small circles have been added at the points (150,80) and (150,100) so you can more easily see the points we required the curve to hit.

It can be hard to work with the T command since you don't get much control over the way the curve is drawn, only the points it passes through. We can use the Q command to get more control: it requires two point as arguments: the “control point” and the destination.

The destination point is the same as the other lines and curves: the place we'll end up. The control point is a point that “pulls” the curve toward it. For example, this draws a curve from (50,120) to (150,120) with (150,180) as the control point:

p5 = paper.path('M50,120 Q150,180 150,120')
p5.attr({'stroke': '#a0f', 'stroke-width': '3'})
paper.circle(150, 180, 3).attr({'fill': '#a0f'})

Again, a little circle is drawn beside “p5” (purple) so you can see the control point.

The curve “p6” (yellow) has endpoints spaced the same distance apart, but the control point is further away. This makes the curve more sharp:

p6 = paper.path('M200,120 Q380,170 300,120')
p6.attr({'stroke': '#cc0', 'stroke-width': '3'})
paper.circle(380, 170, 3).attr({'fill': '#cc0'})

You can combine all of these commands in a single path, making complex shapes:

p7 = paper.path(
  'M50,220 L180,260 Q220,260 220,220 Q350,220 300,240 Z')
p7.attr({'stroke': '#000', 'stroke-width': '3'})

After all of that, here's the resulting SVG that was drawn:

Result of path experiments
Result of path experiments

You can also view the page with that JavaScript and the resulting SVG.

The curves drawn by the T and Q (and C and S not described here) are Bézier curves. If words like “interpolation” and “quadratic” don't immediately frighten you, see the Wikipedia Bézier curve page. The way they're drawn isn't too mathematically-difficult and it will give you a better idea of what the control points are for.