Step 4: Lines and Paths

Welcome to the fourth lesson in the Sketch-n-Sketch tutorial. We will cover:

  • How to use the line function to make simple line segments and our own version of the polygon function,
  • How to use the path function to create arbitrary forms,
  • How to introduce parameters into the path function as a useful design pattern, and
  • How to programmaticaly generate paths to make complex designs.

Along the way, you will also learn a little more about the programming features in Little.

Drawing Lines

One kind of basic SVG shape that we have not mentioned is a finite line segment. Just like the other basic shapes, there is a built-in function that allows the creation of the appropriate SVG node, naturally called line:

(line fill w x1 y1 x2 y2)

This creates a line from the point (x1,y1) to (x2,y2) with the color fill and width w. This function works very well for making very simple line segments between shapes.

We can also draw polygons by linking line segments together. In fact, let's do just that. Our goal is to write a function ourPolygon that, given a list of points [[x1 y1] [x2 y2] ... ], creates the outline of a polygon by drawing line segments between all the adjacent points. To do this, we will need to introduce a couple more programming features in Little.

case Expressions and Recursive Functions

We want our ourPolygon function to work no matter how many points are provided in the input list. To achieve this, we will write a recursive function that walks through each point in the list and "does its thing," where "its thing" will fall into different cases depending on what the list of points happens to look like. Let's start with the latter.

The built-in case function takes an expressionToExamine and a sequence of branches to evaluate depending on what pattern expressionToExamine matches:

(case expressionToExamine
  (pattern1 returnThisIfExpressionMatchesPattern1)
  (pattern2 returnThisIfExpressionMatchesPattern2)
  (_        returnThisIfExpressionMatchesNoPatterns))

The extra newlines and spaces between the cases are optional but often make these expressions easier to read. As you've probably come to expect by now, all of the parentheses — around the entire expression and around each of the branches — are required.

The idea behind this expression is that if expressionToExamine matches one of the patterns, then the expression that is associated with that pattern is returned. The patterns are checked from the top down, and an underscore _ matches anything. If you don't have too much experience with functional languages, you can think of case statements as somewhat analogous to if-expressions found in most programming languages. However, case is generalized to work on more than just Boolean values and with more than just two different branches.

Pattern matching is most often useful for dealing with lists, for which there are two kinds of patterns. First, a list pattern of the form [x1 ... xn] matches lists with exactly n elements. Second, a list pattern of the form [x1 ... xn | xrest] matches lists with at least n elements, where all extra elements (if any) get bound to the variable xrest. You can think of this second kind of pattern as matching the first n elements from the "head" of the list and then giving the name xrest to the "tail" of the list.

Thought Exercise: Can you guess what the Little expressions [e1 ... en] and [e1 ... en | erest] mean?

Here is a sample case function and some example inputs, where the comments below describe which patterns get matched:

(case inputList
  ([]               ...) ; pattern 1
  ([x]              ...) ; pattern 2
  ([x y]            ...) ; pattern 3
  ([x]              ...) ; pattern 4
  ([ [a b] | rest ] ...) ; pattern 5
  (_                ...) ; pattern 6

;  sampleInput                the patterns that sampleInput matches
;  ------------------------   ----------------------------------------
;  []                         matches pattern 1
;  [ 2 ]                      matches pattern 2, and *not* pattern 4
;  [ 1 2 3 ]                  matches pattern 6
;  [ [ 2 4 ] ]                matches pattern 5
;  [ [ 2 3 4 ] ]              matches pattern 6
;  'oh my, i am not a list'   matches pattern 6
;  3.14                       matches pattern 6

Since Little does not enforce any type constraints, the catch-all pattern '_' can match any kind of input — even ones that you may sometimes not want to operate on at all, so be careful when using it!

A very common pattern that you will encounter is to use case in recursive functions that operate on lists by defining a base case for the empty list and a case for non-empty lists. Remember the built-in concat function that takes a list of lists and concatenates them into a single list? Here's how we can define it ourselves:

(defrec ourConcat (\listOfLists
  (case listOfLists
    ([]                   [])
    ([[] | rest]          (ourConcat rest))
    ([[ x | xs ] | rest]  [x | (ourConcat [ xs | rest ])]))))

Notice that we say defrec, rather than def, because the definition of ourConcat recursively calls itself. This function takes a listOfLists and returns something based on which of the following three patterns it matches:

  • if listOfLists is empty...
    • then their concatenation is the empty list
  • if the first list in listOfLists is empty...
    • then the concatenation is defined by recursively concatenating the rest of the lists in listOfLists
  • if the first list has some head element x and tail xs...
    • then the concatenation includes x plus everything in xs and rest.

If ourConcat is called with anything besides a list, then this function will crash at run-time (by design). If you have functional programming experience, this will likely look like a standard recursive definition — albeit with a different syntax than you're used to.

Drawing Polygons

With the knowledge of how to write functions that operate on lists, we can now return to the task of creating a polygon out of a list of points. Our approach is to draw a line between each pair of points, as well as the first and last ones:

(def [c w] ['black' 10]) ; color and width
(def connect (\([x1 y1] [x2 y2]) (line c w x1 y1 x2 y2)))

(defrec ourPolygon_ (\(first prev points)
  (case points
    ([]            [ (connect prev first) ])
    ([next | rest] [ (connect prev next) | (ourPolygon_ first next rest) ]))))

(def ourPolygon (\points
  (case points
    ([]              [])
    ([lonelyPoint]   [])
    ([first | rest]  (ourPolygon_ first first rest)))))

Awesome! Our function ourPolygon handles preparing the arguments for the helper function, ourPolygon_ which then operates recursively on the elements of points list. As it operates on each point, it connects it with a black line to the previous point.

Practice 4.1: Try out this program with a list of points in Sketch-n-Sketch to draw some polygons.

Exercise 4.1: Here's an excuse to get more practice with this syntax. As it is, calling ourPolygon with the list of points [[10 10] [50 50]] draws two nearly-identical line segments. Add an additional case in ourPolygon_ that results in only one line segment is drawn.

Exercise 4.2 (Optional): If you know what "fold" or "reduce" means and if you're feeling ambitious, redefine ourPolygon to use the built-in foldl or foldr functions instead. This exercise is completely optional for the purposes of this tutorial.

Basic Path Commands

Implementing ourPolygon was a good excuse to learn more about Little. But because polygons are so common, it should be no surprise that they are primitive in SVG. Therefore, Little also provides a polygon function to match; check out Prelude if you'd like to see how to use it.

Although drawing polygons with straight edges is useful, one often wants curved edges as well. For this, SVG offers a more general path primitive that subsumes line and polygon because it can also draw curved lines. The goal of this next section is to get you familiar enough with SVG paths so that you will be comfortable exploring all of the functionality the specification has to offer.

The general structure of an SVG path is a list of draw commands, each of which has associated "control points" that are interpreted, or evaluated, in order to draw the shape. This list of commands is put into a path node in SVG with some additional attributes, and then the browser handles the rendering of the overall path. Sketch-n-Sketch supports manipulation of the control points of a path for all of the most commonly used commands, which allows for intuitive direct manipulation of SVG paths.1 To create an SVG path node in Little, the path function can be invoked with arguments of the following form:

(path fillColor lineColor lineThickness pathCommands)

The arguments can be interpreted as follows:

  • fillColor represents the color of the interior of the path ('none' is a valid entry to specify no fill),
  • lineColor represents the color of the path itself (again, 'none' is a valid entry for no border),
  • lineThickness represents the thickness of the border that lineColor colors, and
  • pathCommands is a list of commands with their control points that define the SVG path.

In general, the list of path commands for any given path begins with a "moveto" command, which sets the "cursor" for the path to that location without drawing anything. Then, there is a sequence of draw commands which may optinally be followed by a "closepath" command which connects the end of the path back to the beginning. There are fancier things you can do with SVG paths, but even these commands are enough to make some complicated shapes and forms.

Here is an example path which draws a square with a light blue fill and a gray outline:

(path 'lightblue' 'gray' 5 
  [ 'M' 100 100
    'L' 200 100
    'L' 200 200
    'L' 100 200
    'Z' ])

We begin the path command list with a "moveto", denoted by a captial 'M'.2 Then, we have three successive "lineto" commands, denoted by a capital 'L', which draw three edges starting from the initial (100,100) coordinate going clockwise around a square with edge length 100. Then, we end the command list with a "closepath" command, indicated with a capital 'Z'. This draws an edge back to the start at (100,100). This syntax mirrors the syntax defined in the SVG path specification linked above, so you can use it as a reference for how to specify all the control points for each command.

Notice that the list of path commands above contains different "types" of values, namely, strings and numbers. This is no problem in Little, because there are no types. As a result, we have chosen this representation of path commands because we can, and because it makes it easier to copy-and-paste sample paths from any "raw" SVG path examples to Little.

This is definitely nicer than using a whole bunch of line segments like we did before! What's especialy nice is that SVG paths also have support for specifying a variety of curves. Here we will demonstrate only the quadratic Bézier curve, which is the simplest type of curve to define and is specified with a 'Q'. Below is an example that draws an unfilled, sinusoidal-looking curve.

(def curve
  (path 'none' 'black' 5
    [ 'M' 300 300
      'Q' 350 250
          400 300
      'Q' 450 350
          500 300 ]))

(svg [curve])

Practice 4.2: Go ahead and enter this into a document in Sketch-n-Sketch, and turn on the basic zones. See the points that are off of the line? Try manipulating them. These are the control points for the first and second quadratic curves, respectively.

Exercise 4.3: Now that you've seen how basic paths work, it's your turn. Try experimenting with mixing 'L' and 'Q' commands. For example, you may try drawing your favorite letter of the alphabet — just make sure it has at least one curve in it!

After that, look at some of the built-in examples that use paths in this way, such as the Chicago Botanic Garden Logo or the Eye Icon. In these designs, you will notice that there are many variables defined as top-level definitions that are directly or near-directly dropped in to the coordinate positions inside of the command list. This results in a lot of constants and variable names in play, which can get unwieldy. However, sometimes there is no way to avoid having a lot of parameters at once, as a complicated design (such as the Eye Icon) inherently requires them.

Advanced Programming with Paths

So far we have seen how Sketch-n-Sketch works with basic paths, but the tool also supports and displays control for the other sorts of curves in a similar fashion.3 Because SVG paths can become quite complex, we have found that working with them by mixing programmatic and direct manipulation is one area where Sketch-n-Sketch excels. Our built-in examples include many involved designs that rely heavily on paths. Next, we identify two design patterns that we have found to be useful.

Parameters in Paths

Often times the specification for a path can be improved upon by introducing a parameter that is shared by some or all the points. For example, if we knew that we wanted our curve to have both the up and down portions to have the same amplitude and that we would like the curves to be evenly spaced between each other, we could redefine it in the following way:

(def [amplitude spacing x0 y0] [50 100 300 300])
(def curve
  (path 'none' 'black' 5
    [ 'M' x0 y0
      'Q' (+ x0 (/ spacing 2!)) (- y0 amplitude)
          (+ x0 spacing) y0
      'Q' (+ x0 (* 1.5! spacing)) (+ y0 amplitude)
          (+ x0 (* 2! spacing)) y0 ]))

(svg [curve])

Practice 4.3: Enter the above into your document and manipulate the control points as before. You should now see that your changes affect the other parts of the path that depend upon the same parameters. Depending on your design, this can be a great improvement over defining each portion individually both in terms of number of parameters that are in play as well as adjusting attributes shared by many portions of your path.

Quite a few of the built-in examples that utilize paths fall into this pattern of design, including Active Trans, the logo, POP-PL logo, and the Wikimedia logo. The Active Trans logo is a particularly good example of this. Switch to it and look at the definition for grayPts. Notice how all of the points of the skyline are defined as offsets from a shared height parameter. Try manipulating the control points of the skyline and observe how they all move up and down together. Since the skyline profile of the design is meant to be fixed, this allows for easy direct manipulation of a design characteristic that would otherwise by very annoying to change manually. While you're at at, see how that example uses a "button" (which is just a slider that controls a Boolean value) to allow two different versions of the logo to be manipulated in sync.

Exercise 4.4 (Optional): Modify the Active Trans logo so that each of the buildings rise and fall independently rather than as one unit.

Exercise 4.5: Go ahead and define your own path in terms of a parameter that is shared between all or some of the points. Try to use at least one curve and one straight line in your design, but you can keep it fairly simple for now. If you'd like to continue with the alphabetic theme, you might select a letter that has some symmetry to it (such as S or B).

Generating Paths

While the former design pattern is suitable for a lot of the graphics that you will work with, there are certain types of designs that lend themselves to a program structure that generates a path instead of being specified by hand. This could be for a few different reasons, including that the path is such that the control points are tedious to write by hand, the path has such a large number of control points as to be unwieldy to edit, or that the user would like to experiment with different numbers of control points, which cannot be accomplished without changing the number of path commands, which can only be done either by hand or if the list of commands is being generated by a function.

A good example of the parameters being tedious to specify by hand is in the Pie Chart example. Notice that each wedge is a path with a specific fill and edge style, but that they all share exactly the same structure in terms of how the "pie slice" is defined (two straight edges with a circular curve). So, instead of defining all of the control points by for the number of pie slices that happen to be in the design at the moment, which would be both time consuming, repetitive, and difficult to add or remove a pie slice, the wedge function takes care of all of that. That function is then mapped over the unique information for each wedge, saving the user the trouble of writing it all themselves and making the program drastically more extensible. Furthermore, this allows for a much more dynamic design than is otherwise possible.

In the case of Pie Chart, each path is completely specified by one function, meaning that the initial "moveto" and "closepath" statement is encapsulated in the function definition. However, in the Sailboat example, the situation is different. As the waves are intended to be all one path, the commands to create each wave must be appended to a growing command list, depending on the particular value of the parameters that currently exist. Namely, the overall number of times that the wave should be dependent is determined both by the width of the wave and the overall width of the graphic. So, the number of control points and path commands that are involved need to be able to change. As such, the function mkwaves is defined such that it builds up the path one wave at a time.

When generating your own paths, make sure that the final command list begins with a "moveto" command and that the last point of each section is interacting with the first point of the next section exactly how you would like it to be. Now it's time for you to set sail.

Exercise 4.6: Your mission, should you choose to accept it, is the following:

  1. Pick a design that would be difficult or impossible to create using only basic shapes. Anything that involves non-circular curves or a large number of contiguous edges is a good candidate. One option is to choose an existing logo design of a favorite organization of yours or your place of work. Having an image of such a design on hand will be helpful for reference.
  2. Find a subset of the design that can be expressed as a path and in terms of just a few parameters. Write a function that genereates that portion of the path. It is okay if it's only part of the overall design that can be expressed in this way.
  3. Complete the remainder of the path that is associated with this generated subset.
  4. Continue the above two steps until all paths in the design have been completed.
  5. Specify the remainder of the design, attempting to share as many parameters that are logically linked between the paths and these elements.

We encourage you to be amibitious with this exercise! Paths open up a world of possiblities for expressing different graphics and is one of the places where being able to directly manipulate designs along with programmatic specification really shines. Don't forget to use the SVG path specification as a reference for the path commands and to use the practices or built-in examples we discussed above if you get stuck on syntax.

Next Up: Step 5

  1. The interpretation of supported commands can be found in the Sketch-n-Sketch source here.

  2. If you are already familiar with SVG paths, note that Sketch-n-Sketch currently supports path commands with absolute coordinates (commands with uppercase letters) but not relative coordinates (commands with lowercase letters).

  3. For an in depth look at the various sorts of paths and how to use their specific commands, we recommend that you check out the Mozilla Developer Network tutorial on the subject.