Paths: How to Make Custom Shapes and Curves - RaphaelJS: Graphics and Visualization on the Web (2014)

RaphaelJS: Graphics and Visualization on the Web (2014)

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:

1. “Using your pink pencil, start on the point at the coordinates (3,4), and draw a straight line eight units to the right.”

2. “Go down five units.”

3. “From there, draw a diagonal line back to the original point.”

4. “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

});

image with no caption

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");

image with no caption

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);

image with no caption

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");

image with no caption

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:

image with no caption

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.

image with no caption

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 or 1, 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:

image with no caption

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"});

image with no caption

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"});

image with no caption

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"});

image with no caption

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:

image with no caption

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"});

image with no caption

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.

image with no caption

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);

image with no caption

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);

image with no caption

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);

image with no caption

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.