Chapter 4. Paths: How to Make Custom Shapes and Curves
Circles and squares are great for getting started with Raphael, but eventually you will probably want to branch out into something more complex. For that, we will use paths, a relatively simple set of instructions capable of making almost any shape or drawing you can imagine: squiggly lines, donuts, and figure eights, as well as complex shapes like people or animals.
To understand how paths work, consider the following standby of those tedious workplace team-building workshops: you and a partner have been placed back-to-back with a matching set of colored pencils. You each have a blank sheet of paper. Your job is to draw a picture and give your partner verbal instructions on how to recreate this picture on his or her own sheet of paper. No peeking.
To make things a little easier, letâs make it graphing paper.
First, you would be wise to establish with your partner that the upper-leftmost point on the paper has the coordinates (0,0). Then you might go about it something like this:
- âUsing your pink pencil, start on the point at the coordinates (3,4), and draw a straight line eight units to the right.â
- âGo down five units.â
- âFrom there, draw a diagonal line back to the original point.â
- âThen, using your green pencil, fill in the space bounded by those lines.â
Assuming youâve been paired with a halfway competent coworker, you should both now have a green triangle with a pink border. (Hopefully you donât work for a design company.)
For the next shape, you would probably say something like âStart a new shape on the coordinates (15,22)â so that your partner doesnât accidentally draw a line from the ending point of the last shape to the new one, Etch A Sketch style.
In case you havenât guessed, your partner here is a computer. Drawing paths in Raphael is an alchemical process of transforming instructions into shapes. And your partner never messes up, so long as you donât.
Syntax
Paths are represented in browsers as a long string of characters. These strings can be broken down into a series of points that tell the computer where to start, where to end up, and what to do on the way there.
A simple path might look like this:
var
d
=
"M 10,30 L 60,30 L 10,80 L 60,80"
;
This translates to: âMove (M) to the coordinates (10,30), draw a line (L) to the coordinates (60,30), then a line (L) to (10,80), and then a line (L) to (60,80).â
To see what a path looks like, initialize a Raphael project by declaring a new paper
object on a page, and add this line:
var
paper
=
Raphael
(
0
,
0
,
300
,
300
);
var
d
=
"M 10,30 L 60,30 L 10,80 L 60,80"
;
var
mark
=
paper
.
path
(
d
);
As you see, we made a Z pattern starting at (10,30) and ending at (60,80). All we had to do was tell Raphael where to start and define the three points it should visit, tracing a line behind it as it goes.
You have a little wiggle room when it comes to the precise syntax for paths. The spaces between the letters and the numbers arenât necessary, since the browser has no difficulty distinguishing when one segment ends and the next begins. The commas between numbers can be replaced with spaces if you prefer. Your syntax will probably condense as you get more experienced.
Dressing Up Your Paths
Paths can take many of the same attributes as shapes, including stroke
(the style governing lines) and fill (governing the space enclosed by those lines). To make a slightly less anemic-looking Z, letâs give it a few properties:
mark
.
attr
({
"stroke"
:
"#F00"
,
"stroke-width"
:
3
});
See this code live on jsFiddle.
If youâre fuzzy on why the browser understands #F00
as the color red, read up on âhexadecimal color codes.â
Just to see what happens, letâs also add some interior color:
mark
.
attr
(
"fill"
,
"#00C"
);
Hmmm. Since a Z is not a âclosedâ figure, in which the last point rejoins the first, Raphael guesses what to fill in by drawing an imaginary line from the end point to the starting point and then filling in anything thatâs bounded on all sides by lines. (This is in stark contrast to the old days of Microsoft Paint, when the fill tool would paint the entire screen if there was even a single pixel missing along the perimeter of your shape.) While the computer is reasonably smart about guessing what to do in these circumstances, itâs much better to just complete your shapes if you want them to have some internal color.
To do so, you could just add a final L10,30
command to the end of the path string, thus drawing a final line that reconnects with the original. The path syntax also offers a convenient command to do the same thing. If you end your path with a z
, it connects to the beginning automatically. Letâs try it alongside an alternate syntax for the path, just to make sure I was telling the truth above:
var
paper
=
Raphael
(
0
,
0
,
300
,
300
);
var
d
=
"M10 30L60 30L10 80L60 80z"
;
var
mark
=
paper
.
path
(
d
);
Relative paths
The commands M
and L
have younger siblings, m
and l
, which function identically except for one key factor: they understand coordinates to be relative to the previous coordinate. We could achieve the exact same Z in a more intutive manner like this:
var
d
=
"M10,30l50,0l-50,50l50,0"
;
We started at the same pointâusing a lowercase m
here would be meaningless since we donât have a starting point to be relative toâand then told the computer to move its imaginary pen 50 pixels to the right and zero pixels up, then to the left 50 and down 50, then 50 to the right again.
For simple cases like this one, itâs often much easier to use relative coordinates. In other cases, youâll have predetermined points on the screen that youâll want to connect without doing the math of how far apart they are relative to one another. Itâs up to you, and you can mix and match capital and lowercase letters in the same string.
There are two more commands that make life a little easier: H
, V
, and their tagalong siblings h
and v
, for âhorizontalâ and âvertical.â These commands only expect one number to follow them, and assume the other is zero. We can simplify our Z again like so (Iâve mixed in a capital and lowercase H
for demonstration):
var
d
=
"M10,30h50l-50,50H60"
;
Hopping Around
Paths should always begin with an M
. But if you need to âpick up the penâ during the course of drawing a path to jump to another spot, you can also use the M
or m
in the middle of the string. Hereâs a capital I:
var
I
=
paper
.
path
(
"M40,10h30m-15,0v50m-15,0h30"
);
This is another example where the relative coordinates that come using lowercase letters are very convenient. But just for practice, letâs make the same I using only âabsoluteâ coordinates:
var
I
=
paper
.
path
(
"M40,10H70M55,10V60M40,60H70"
)
Letâs say we want to make some solid shapes, like this irregular triangle, beginning from the lower right vertex:
var
d
=
"M90,90l-80,-20L50,5L90,90"
;
var
tri
=
paper
.
path
(
d
).
attr
({
"fill"
:
"yellow"
,
"stroke-width"
:
5
});
Since we were careful to make the last point the same as the first, there is no ambiguity as to what should get filled in. Here we have something that looks like a yield sign restructured by a driver who did not, in fact, yield:
Again, we can freely mix uppercase and lowercase letters in a path string, though doing so may not contribute to oneâs sanity during the creation of complex shapes.
Behind the scenes, Raphael stores paths as an array in which each object represents one command of a letter and some numbers. If you were to add the line console.log(tri)
at the end of the previous example and examine your code in Firebug, you would see something like this:
[Array[3], Array[3], Array[3], Array[3], toString: function] 0: Array[3] 0: "M" 1: 90 2: 90 length: 3
1: Array[3] 0: "L" 1: 10 2: 70 length: 3
2: Array[3] 0: "L" 1: 50 2: 5 length: 3
3: Array[3] 0: "L" 1: 90 2: 90 length: 3
The careful reader will note that Raphael converted the second point to absolute coordinates when converting the string to the array.
Itâs useful to understand how Raphael stores paths for the purpose of debugging and getting information about the path after the fact. (Perhaps you want the coordinates of the first and last point in order to draw some objects at either end of a line.) In fact, you can choose to deliver a path command to Raphael in this format as well. You can get the same irregular triangle in the above example using the array form:
var
tri
=
paper
.
path
([[
"M"
,
90
,
90
],
[
"L"
,
10
,
70
],
[
"L"
,
50
,
5
],
[
"L"
,
90
,
90
]]);
I personally find it easier and more concise to use the string format and let Raphael deal with converting it to an array, but the choice is yours.
Polygons
Given how common rectangles are in design, it makes sense for Raphael to offer a .rect()
function, even if it duplicates what can be done with paths with a few more lines. (Actually, this is a decision baked into the SVG specifications, not a shortcut unique to our library.) It would be highly inefficient, on the other hand, for Raphael to offer a .pentagon()
, .hexagon()
, and so forth. Fortunately, we now know enough to make any regular polygon we like. Letâs write a function to make a polygon of N
sides centered around an arbitrary point. Itâs going to take a very small amount of trigonometryâthree lines, I thinkâbut weâll get through it together. The function weâre going to write will take the center coordinates (like a circle or ellipse), the number of sides in our regular polygon, and the length of the sides, and return the path as a string.
function
NGon
(
x
,
y
,
N
,
side
)
{
// draw a dot at the center point for visual reference
paper
.
circle
(
x
,
y
,
3
).
attr
(
"fill"
,
"black"
);
var
path
=
""
,
n
,
temp_x
,
temp_y
,
angle
;
for
(
n
=
0
;
n
<=
N
;
n
+=
1
)
{
// the angle (in radians) as an nth fraction of the whole circle
angle
=
n
/
N
*
2
*
Math
.
PI
;
// The starting x value of the point adjusted by the angle
temp_x
=
x
+
Math
.
cos
(
angle
)
*
side
;
// The starting y value of the point adjusted by the angle
temp_y
=
y
+
Math
.
sin
(
angle
)
*
side
;
// Start with "M" if it's the first point, otherwise L
path
+=
(
n
===
0
?
"M"
:
"L"
)
+
temp_x
+
","
+
temp_y
;
}
return
path
;
}
Letâs fire this baby up with a few different values and see how we did.
var
paper
=
Raphael
(
0
,
0
,
500
,
500
);
paper
.
path
(
NGon
(
40
,
40
,
6
,
30
));
paper
.
path
(
NGon
(
130
,
60
,
9
,
40
));
paper
.
path
(
NGon
(
240
,
160
,
25
,
80
));
See this code live on jsFiddle.
As you see, a 25-sided polygon is pretty close to a circle, as we might expect. You might even say a circle is a polygon with infinite sides. From there, RaphaelJS will leave you to your musings.
Curves
Drawing lines that bend and curve is necessarily more difficult in Raphael because you have more decisions to make. So weâre drawing a curve from point A to point B. Should it curve up or down? By how much? Is it symmetrical?
The SVG specifications offer a couple of different commands for curves, but the documentation is pretty miserable. In this chapter, weâre going to cover the most intuitive type, the ellipitical curve.
The A
Command: Elliptical Curves
As you might predict, this command creates curves that look like segments taken from an ellipse. As such, they require a few peices of information. Donât worry if this is confusing at first. Itâs naturally confusing, but a few examples will illuminate these parameters.
Like lines, elliptical curves begin at the point where the previous command left off.
An A
command looks like this: C 50,75 0 0,1 400,200.
Those numbers represent:
- The horizontal and vertical radii of the imaginary ellipse weâre using as a guide
- An angle rotating the curveâs axis (for advanced users)
-
A Boolean value (or âflagâ) that is either
0
or1
, representing whether a curve goes clockwise or counterclockwise - A Boolean value (or âflagâ) representing whether the curve goes the long way or the short way
- The ending point
To explore what this means, weâre going to start with a point at [50, 50]
and end at a point at [200, 125]
. Letâs draw that and make some dotted lines for reference:
var
paper
=
Raphael
(
0
,
0
,
500
,
4000
);
var
starting_point
=
paper
.
circle
(
150
,
150
,
4
).
attr
({
fill
:
"green"
,
stroke
:
0
});
var
ending_point
=
paper
.
circle
(
250
,
220
,
4
).
attr
({
fill
:
"red"
,
stroke
:
0
});
var
path1
=
paper
.
path
(
"M 150,150 L 250,150 L 250,220"
).
attr
(
"stroke-dasharray"
,
"."
);
var
path2
=
paper
.
path
(
"M 150,150 v 70 h 100"
).
attr
(
"stroke-dasharray"
,
"-"
);
So far, so good:
Letâs try an elliptical arc with the angle and these two mysterious boolean values set to zero. Weâll use the length and the height of this rectangle as the radii.
var
curve1
=
paper
.
path
(
"M150,150 A100,70 0 0,0 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"blue"
});
Niceâwe have a beautiful sloping curve connecting the points. Letâs see what happens when we set the first flag to 1
instead of 0
:
var
curve2
=
paper
.
path
(
"M150,150 A100,70 0 1,0 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"cyan"
});
Whoa. The starting and ending points are the same, and weâre still following the path of an ellipse with the same radii, but we went the long way. The SVG specification calls this the âlong arc flag,â but I like to call it the âdetour value.â If the detour value is zero or false, the curve takes the shorter path to the destination. If itâs one, it takes the longer path.
Letâs try the other flag, setting the detour flag back to 0
:
var
curve3
=
paper
.
path
(
"M150,150 A100,70 0 0,1 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"pink"
});
This is the same as the first curve, but it takes a clockwise path instead of counterclockwise path. This is officially known as the âsweep flag,â but I like to think of it as the âclockwise flag.â You may notice that curve 3 âcompletesâ curve 2, since its flags have opposite values.
Can you guess what our last combination of flags looks like? If you said âa clockwise flag that takes the long way to get to its final destination,â you were correct:
var
curve4
=
paper
.
path
(
"M150,150 A100,70 0 1,1 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"orange"
});
Put together, we see that the four combinations describe the two ways an ellipse with an x
radius of 100 and a y
radius of 70 can intersect our starting and ending points:
See this code live on jsFiddle.
What about that fifth parameter, the angle, that weâve so far been setting to zero? Itâs a common mistake to assume that this is the angle that the curve traverses, but this is not the case. That angle is calculated automatically based on the radii and the end pointâno further information is needed. The angle that you set explicitly will rotate the imaginary ellipses. The easiest way to express this is visually. Letâs take the four arcs we just drew and rotate each of them by 45 degrees:
var
curve1
=
paper
.
path
(
"M150,150 A100,70 45 0,0 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"blue"
});
var
curve2
=
paper
.
path
(
"M150,150 A100,70 45 1,0 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"cyan"
});
var
curve3
=
paper
.
path
(
"M150,150 A100,70 45 0,1 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"pink"
});
var
curve4
=
paper
.
path
(
"M150,150 A100,70 45 1,1 250,220"
)
.
attr
({
"stroke-width"
:
2
,
stroke
:
"orange"
});
See this code live on jsFiddle.
As we can see, we have identically sized ellipses passing through the same points, and then rotated. Itâs actually a pretty neat geometric property, but I find it difficult to visualize. That said, I confess that I have never once found the need to rotate my elliptical curves in the wild.
The C
Command: Cubic Bézier Curves
The elliptical curve is extremely useful in schematics and other geometric drawings. Most of the curves we observe in art and nature, however, do not neatly fit along the path of an ellipse. In these cases, we make use of the cubic Bézier curve.
The C
command takes three pairs of coordinates: the destination and two control points that determine how the line bends. In most cases, the curve does not pass through these control points. Instead, we can think of them as invisible magnets that pull the line in their direction as it travels to its destination. This is best illustrated with a few examples in which we will place black dots over the control points for educational purposes.
To draw a cubic Bézier curve, one supplies these two control points first and then the destination as the third coordinate. Like all of the other SVG paths, it begins wherever the previous command left off.
var
paper
=
Raphael
(
0
,
0
,
500
,
500
);
// draw the control points for educational purposes
var
cp1
=
paper
.
circle
(
100
,
50
,
4
).
attr
(
"fill"
,
"black"
);
var
cp2
=
paper
.
circle
(
200
,
150
,
4
).
attr
(
"fill"
,
"black"
);
// draw the bezier curve
var
path
=
"M 50,100 C 100,50 200,150 250,100"
;
paper
.
path
(
path
);
See this code live on jsFiddle.
This path begins at coordinates (50,100) and ends up at (250,100), just like a regular old L
path. For the first two arguments, I set one control point above the line to the right of the starting point and a second one below and to the left.
If I move the first contol point to be below the starting point as well, at the same x
position, the curve assumes a more familiar shape:
var
paper
=
Raphael
(
0
,
0
,
500
,
500
);
var
cp1
=
paper
.
circle
(
100
,
150
,
4
).
attr
(
"fill"
,
"black"
);
var
cp2
=
paper
.
circle
(
200
,
150
,
4
).
attr
(
"fill"
,
"black"
);
var
path
=
"M 50,100 C 100,150 200,150 250,100"
;
paper
.
path
(
path
);
These examples both have some flavor of symmetry, but thereâs no reason the points need to reflect one another. Hereâs a wackier example:
var
paper
=
Raphael
(
0
,
0
,
500
,
500
);
var
path
=
"M 50,100 C 50,50 300,250 250,100"
;
var
cp1
=
paper
.
circle
(
50
,
50
,
4
).
attr
(
"fill"
,
"black"
);
var
cp2
=
paper
.
circle
(
300
,
250
,
4
).
attr
(
"fill"
,
"black"
);
paper
.
path
(
path
);
Exotic Paths
The SVG path specifications contain several more advanced commands for Bézier-like curves that reflect back on themselves. I will freely admit that Iâve never once found a use for any of them. Should you wish to dive in, an understanding of control points is all you need to get a sense for how they work. You can see a lovely interactive example on jsFiddle of one such exotic curve that allows you to manipulate the control points with your mouse.
Case Study: Play Ball!
We have a few other types of curves to cover, but Iâd like to point out that, halfway through Chapter 4âand that includes the Introduction, where you didnât even learn anythingâwe have already accumulated the skills to draw a baseball field.
Looking over Major League Baseballâs official rules, it looks like the minimum allowable distance from home plate to the foul pole is 250 feet. To make our visualization maximally flexible, letâs set that value as a variable, along with one for the scale of the graphic and point of origin for home plate:
//pixels per foot
var
paper
=
Raphael
(
0
,
0
,
500
,
500
),
SCALE
=
1
,
HOME_PLATE
=
{
x
:
250
,
y
:
350
},
FOUL_POLE
=
250
;
Of course, SVG graphics are meant to scale without us hard-coding a scaling factor. I find it convenient to define one in the code for situations like this, where there is an explicit scale between the screen and a real world object, whether itâs a stadium or a solar system. We can always scale the whole graphic again down the road if need be.
Youâll notice I use some uppercase variables. This is a personal convention of mine in JavaScript that I reserve for numerical values that are constant over the lifetime of the program, but that I may wish to alter by hand to change the specs of the graphic. It has no role whatsoever in determining how the program sees the variables. Iâve also stored the x
and y
coordinates of home plate in a simple object, rather than taking the time to write HOME_PLATE_X
and HOME_PLATE_Y
.
Okay, letâs make a shape that outlines the field. To draw the foul lines, weâll start at the position of home plate and draw the line 250 pixels to the left field foul pole. This involves a little trigonometry.
The foul pole is 45 degrees to the left if youâre standing on home plate facing the pitcher. JavaScriptâs trig functions need that in radiansâthat is, Ï/4.
var
foul_line_left
=
"M"
+
HOME_PLATE
.
x
+
","
+
HOME_PLATE
.
y
+
"l"
+
-
1
*
FOUL_POLE
*
Math
.
cos
(
Math
.
PI
/
4
)
+
","
+
-
1
*
FOUL_POLE
*
Math
.
sin
(
Math
.
PI
/
4
);
Instead of hardcoding the numbers into the paths, as we did in the first examples, itâs generally easier to compute the strings youâll pass to Raphael by making a string from numerical variables and the required function, as above. If youâre used to âstrongly typedâ languages like Java or Python, which throw an error when you try to add variables of different types, this will look like trouble. JavaScript is âweakly typed,â so itâs fine with adding numbers to strings, converting them to text in the process.
(Not that we make the x
and y
distances after the lowercase âlâ negative because weâre going left and up relative to home base.)
Now letâs draw an arc along the outfield fence to the other foul pole:
var
outfield_fence
=
"a"
+
FOUL_POLE
+
","
+
FOUL_POLE
+
" 0 0,1 "
+
2
*
HOME_PLATE
.
x
*
Math
.
sin
(
Math
.
PI
/
4
)
+
","
+
0
;
Weâre using the foul pole distance as the radius, meaning home plate will form the center of the circular ellipse describing the fence. We do not want to take the long route, so we set the first flag to 0
, but we do want to go clockwise, so we set the second one to 1
.
Last, weâll draw a line back to where we started, using the capital L for convenience:
var
foul_line_right
=
"L"
+
HOME_PLATE
.
x
+
","
+
HOME_PLATE
.
y
;
var
field
=
paper
.
path
(
foul_line_left
+
outfield_fence
+
foul_line_right
)
.
attr
({
stroke
:
"none"
,
fill
:
"green"
});
Looking good so far, though the center field fence looks a little close to me. We can remedy this by extending the second radius in the arc:
var
outfield_fence
=
"a"
+
FOUL_POLE
+
","
+
1.5
*
FOUL_POLE
+
" 0 0,1 "
+
2
*
HOME_PLATE
.
x
*
Math
.
sin
(
Math
.
PI
/
4
)
+
","
+
0
;
var
field
=
paper
.
path
(
foul_line_left
+
outfield_fence
+
foul_line_right
)
.
attr
({
stroke
:
"none"
,
fill
:
"green"
});
Much better. Now letâs make a square infield representing the basepaths and put some bases on it. To do so, we could make a path that starts at home and then goes 90 feet (pixels) northwest, then northeast, then southeast, then back to home. That would involve a lot of trig. I have a better idea that harkens back to Chapter 2: letâs just draw a square and rotate it into position.
First weâll construct the infield using home plate as an origin and not worrying about rotation.
This handy HTML color table suggests that #993300
is a nice dirt color.
var
infield
=
paper
.
set
();
infield
.
push
(
paper
.
rect
(
HOME_PLATE
.
x
,
HOME_PLATE
.
y
,
90
,
90
)
.
attr
({
stroke
:
"none"
,
fill
:
"#993300"
}));
infield
.
attr
(
"transform"
,
"R-135 "
+
HOME_PLATE
.
x
+
" "
+
HOME_PLATE
.
y
);
For the bases, Iâm going to make a loop that iterates four times and draws a base on each corner. (Yes, weâre cheating and make home plate a square, but you do have the capacity to draw one using paths for extra credit.)
//bases
for
(
var
c
=
0
;
c
<
4
;
c
+=
1
)
{
infield
.
push
(
paper
.
rect
(
HOME_PLATE
.
x
+
85
*
(
c
%
2
),
HOME_PLATE
.
y
+
85
*
(
c
>=
2
),
5
,
5
)
.
attr
({
stroke
:
"none"
,
"fill"
:
"white"
}));
}
Note that 85 * (c / 2 >= 1)
make use of the fact that a true/false statement resolves to zero or one.
To swing the infield into place, weâll rotate it 135 degrees, using home plate as the pivot point:
infield
.
attr
(
"transform"
,
"R-135 "
+
HOME_PLATE
.
x
+
" "
+
HOME_PLATE
.
y
);
See this code live on jsFiddle.
Beautiful! Of course, a real baseball field is much more refined, with dirt extending in a radius from the pitcherâs mound, grass in foul territory, and so forth. Iâll leave it as an exercise to the ambitious reader to extend this example. The point is, there is nothing about a baseball diagram that you cannot replacate with your current Raphael toolset.
Final Thoughts
You might be thinking: Wait, why did I mess around with all that trigonometry if I could have drawn the entire field on its side, with the left-field foul line perfectly horizontal, and then rotated the field 45 degrees, not unlike the strategy for drawing the diamond? To that I respond: Please file all complaints by snail mail.
Actually, thatâs a fantastic idea. In fact, thatâs precisely what engineers do all the time, applying a transformation to a dataset that makes it easier to work with. Both ways work, and the best route is always the one that youâre able to best visualize and understand. People who think more conceptually might like to draw the lines in the locations that they will ultimately appear. Those who think geometrically might prefer to draw something on its side, where diagonal lines become straight lines, and then rotate it. Coding is a collaborative process between your mind and the computerâs mind, and happy programmers are ones who find the ideal meeting point.
Get RaphaelJS now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.