The short Shapes tutorial
Welcome to this tutorial on Shapes!
This document is meant to serve as a short introduction to the Shapes language and related tools. It assumes you have already got a reason for being curious about Shapes. Likewise, being a short tutorial it will not waste your time telling about the history and background of Shapes; interested readers will find such information elsewhere.
Let's begin by working through a very small example. We'll just make a diagonal line — that's pretty much the simplest thing I can think of. (Making a very small spot would actually be simpler, but creating lines is such an important topic for Shapes that it ought to be part of the first example.)
A Shapes program may consist of just one expression. For this example,
[stroke (0cm,0cm)--(2cm,1cm)]
would be a valid program. However, to display one of the fundamental language constructs of Shapes, we shall look at a slightly more clumsy way of achieving the same result.
Please refer to
structure.html for an explanation of the various ways a Shapes program may define its output.
The following, slightly longer, program does the same job as the program in the previous section. It's still just one line of code, and it will be explained in just a moment:
•page << [stroke (0cm,0cm)--(2cm,1cm)]
If you don't like the bullet character (•), you may prefix
page with a hash sign (#) instead, like so:
#page << [stroke (0cm,0cm)--(2cm,1cm)]
In either case, make sure your file is encoded using utf-8 (if you avoid the bullet character, most eight bit encodings should be fine by intended coincidence). Call the file hello.shape.
So what does the code mean? As you might have guessed,
..Shapes..IO..•page, has something to do with the “page” we are drawing on, and the following
<< is an insertion operator (borrowed from
c++) that takes the following argument and
tacks it onto the page. The expression delimited by square brackets is a
function call; here
..Shapes..Graphics..stroke is the callee, and there is one argument. The argument is a straight path between two points, simply constructed by combining the two coordinate pairs using the
connection operator --.
The example displays the following traits of the Shapes language
Function calls have the callee inside the bracketing syntax, like in Scheme.
Statements are not terminated using a designated token; statements end by themselves, like in Haskell.
States (here,
..Shapes..IO..•page) and
variables (here,
..Shapes..Graphics..stroke) are distinguished by belonging to different name spaces, like variables are distinguished from types in Haskell. This is called
Hungarian notation, and is used also for other distinctions in Shapes.
Coordinates are specified as pairs of lengths, as opposed to pairs of scalars, unlike many other graphics languages where there is no distinction between lengths and scalars.
The syntax makes use of characters outside the ASCII range, but there is always ASCII fall-backs.
To obtain the pretty piece of graphics so carefully designed above, we just need to pass the file to the Shapes compiler:
shapes hello.shape
This will produce the file
hello.pdf, and if your favorite
pdf viewer happens to be xpdf, you could open the file by typing
xpdf hello.pdf &
However, when your file changes, you will want to update the xpdf window, and then Shapes comes to the rescue; try using the compiler with an extra option:
shapes --xpdf hello.shape
If you're on a Mac, you may replace --xpdf by --open.
If you reached this point, this shows you're up and running! You're ready to go on with more exciting examples! By the way, can you figure out how you would go about to add another stroke to your page, or a longer stroke connecting several points?
There are many things the user can control using command line options to the compiler, but these won't be covered in this tutorial. Please refer to the
man page for further details on this.
In this chapter, we'll quickly go through a couple of simple but important things about the Shapes language.
The following example shows how to produce a label typeset using
LaTeX:
•page << (TeX `$\Xi^{2}$´)
Things worth noting:
There is no function call here. The expression delimited by parentheses is a special form indicated by the TeX keyword.
The string delimiters is a pair of one grave and once acute accent. If you find these difficult to produce using your keyboard layout, there is an alternative syntax; the poor man's string combines straight double quote marks with a pair of parentheses, in this case ("$\Xi^{2}$").
The backslash character (\) is not an escape character (there is no escape character at all in normal strings).
LaTeX is also useful for including pictures via the includegraphics macro.
The string syntax is chosen carefully not to interfere with the use of LaTeX — that's why there is no escape character.
|
There is also a function that does the same job as the TeX special form, but there are good reasons for avoiding it.
|
Quotation marks in labels |
---|
|
Since Shapes allows pairwise `…´ inside normal strings, the ` cannot be used to create the left double quote in LaTeX, since there will be no matching ´ at the other end of the quote. This example presents ways to deal with this.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
##lookin ..Shapes..Geometry
##unit dy = 1.5cm
[Graphics..TeX ("\parbox{10cm}{``This \emph{poor man's string} has bulky delimiters, but can use the \texttt{\`{}\`{}} sequence for the opening left double quote, and uses a sequence of two \emph{impossible-to-produce-in-\LaTeX-apostrophe} for the closing right double quote.''}")]
& [[shift (0,~1dy)] [Graphics..TeX `\parbox{10cm}{“This string is entered with left and right double quotes, and can use the \emph{normal string} syntax.”}´]]
& [[shift (0,~2dy)] [Graphics..TeX "{{\parbox}7B{10cm}7D7BE2809C{This string is entered as a \emph}7B{data string}7D{, and would be OK for a computer program to generate. It is entered using plain \textsc}7B{ascii}7D{, have a look at the source!}E2809D7D}]]
|
When putting strokes on the page, there is a natural way of controlling where the strokes appear by simply specifying the desired coordinates. When using the
TeX special form, however, the produced graphics always ends up at the origin. We clearly need to be able to move things around… well, strictly speaking, we don't
move things in Shapes, since the language is functional. What we really do is create a new object which only differs from the original by a
transform, see
§Transform.
In Shapes each transform is represented as an object in itself, which can then be applied to objects of geometric nature. For example, to create the transform that moves (
shifts, see
..Shapes..Geometry..shift) things
2 cm to the right and
3 mm down, we write (we also give a name to the transform, so that it can be referred to later):
T1: [shift (2cm,~3mm)]
Note:
The sign of a negative number or length is written using the tilde (~) character. Negative values can also be written using the usual dash (-) if enclosed withing parentheses, for instance, (2cm,(-3mm)). The latter alternative is convenient when the source is produced by other programs.
Note:
The angle argument is basically interpreted as radians, but angles may be entered in degrees if immediately followed by the degree sign (°). In fact,
25° is just a way of entering the number 25⋅π/180. For the full syntax of floats and a note on how to type the degree sign, see
float.
The following program illustrates how the transforms can be applied:
lbl: (TeX `$x^{2} + y^{2}$´)
•page << [T1 lbl] << [T2 lbl] << lbl
The complete example with output is given below.
Transforms |
---|
|
A label painted once in its original location, once after a rotation, and once after a shift.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
##lookin ..Shapes..Geometry
T1: [shift (2cm,~3mm)]
T2: [rotate 25°]
lbl: [Graphics..TeX `$x^{2} + y^{2}$´]
IO..•page << [T1 lbl] << [T2 lbl] << lbl
|
Transforms can be combined using the multiplication operator (
*). Hence, the following two lines do the same thing:
•page << [T2 [T1 lbl]]
•page << [T2*T1 lbl]
It is almost allways the case that combining the operators is (sometimes much) more efficient than applying one transform after the other.
Well, that's about all we are going to say regarding transforms in this tutorial. Before ending, we'll just mention that there are other constructors beside the ones seen here, that (almost all) transforms can be inverted (see
..Shapes..Geometry..inverse), and that transforms also exist in 3
d (see
§Transform3D).
Path construction is at the heart of the Shapes language, and we shall cover only a small part of the powerful ways in which paths can be specified. The main documentation on path construction is in the
syntax chapter.
Basically, path construction is about entering coordinates in a convenient way. The cubic splines (often referred to as Bezier splines) that Shapes uses for paths (which is the obvious choice since this is was is supported by pdf) consist both of points on the path and interpolation points that control the shape of the path between the points that are on the path. Basically each path segment between two points on the path have one interpolation point associated with the starting point, and one associated with the final point. Each of the interpolation points may be omitted, which corresponds to the limiting case when the interpolation point approaches the associated point on the path. The following example tries to illustrate the roles of points on the path and of the interpolation points.
Single Bezier spline |
---|
|
Bezier splines. Points on the path are marked with circles, and interpolation points are marked with a cross and a dashed line showing which terminal point it belongs to. Absence of an interpolation point — that is, an interpolation point which is logically coinciding with the terminal point it is associated with — is simply represented by not marking the interpolation point at all. Note that the absence of an interpolation point is the limiting case as the interpolation point approaches the terminal point.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
##lookin ..Shapes..Geometry
pathStyle: Traits..@width: 1bp
controlStyle: Traits..@width: 0.3bp & Traits..@dash:[Traits..dashpattern 2mm 2mm] & Traits..@stroking:[Traits..gray 0.5]
onPathMark: Traits..@nonstroking:Traits..RGB..RED | [Graphics..fill [Geometry..circle 1mm]]
controlHead: \ p → (> cut:0bp
picture: Traits..@width:1bp & Traits..@stroking:Traits..RGB..RED & Traits..@dash:[Traits..dashpattern] | ([shift p.begin.p]*[rotate [angle p.begin.T]])[][Graphics..stroke (~1.5mm,~1.5mm)--(1.5mm,1.5mm) & (~1.5mm,1.5mm)--(1.5mm,~1.5mm)]
<)
twoHandlesHelper: \ p0 p1 p2 p3 →
(
[[shift p0] onPathMark]
&
[[shift p3] onPathMark]
&
( controlStyle | [Graphics..stroke p0--p1 head:controlHead] )
&
( controlStyle | [Graphics..stroke p3--p2 head:controlHead] )
&
( pathStyle | [Graphics..stroke p0>p1--p2<p3] )
)
frontHandlesHelper: \ p0 p1 p3 →
(
[[shift p0] onPathMark]
&
[[shift p3] onPathMark]
&
( controlStyle | [Graphics..stroke p0--p1 head:controlHead] )
&
( pathStyle | [Graphics..stroke p0>p1--p3] )
)
rearHandlesHelper: \ p0 p2 p3 →
(
[[shift p0] onPathMark]
&
[[shift p3] onPathMark]
&
( controlStyle | [Graphics..stroke p3--p2 head:controlHead] )
&
( pathStyle | [Graphics..stroke p0--p2<p3] )
)
noHandlesHelper: \ p0 p3 →
(
[[shift p0] onPathMark]
&
[[shift p3] onPathMark]
&
( pathStyle | [Graphics..stroke p0--p3] )
)
twoHandles: \ s1 s2 →
{
p0: (0cm,0cm)
p3: (6cm,1cm)
p1: p0+s1*1.5cm*[dir 60°]
p2: p3+s2*3cm*[dir ~165°]
[if s1 = 0
[if s2 = 0
[noHandlesHelper p0 p3]
[rearHandlesHelper p0 p2 p3]]
[if s2 = 0
[frontHandlesHelper p0 p1 p3]
[twoHandlesHelper p0 p1 p2 p3]]]
}
##unit dy = 1cm
##unit dx = 7cm
IO..•page << [shift (0dx,0dy)] [] [twoHandles 1 1]
<< [shift (0dx,~1dy)] [] [twoHandles 1 1/2]
<< [shift (0dx,~2dy)] [] [twoHandles 1 1/4]
<< [shift (0dx,~3dy)] [] [twoHandles 1 0.001]
<< [shift (0dx,~4dy)] [] [twoHandles 1 0]
<< [shift (1dx,~4dy)] [] [twoHandles 1/2 0]
<< [shift (2dx,~4dy)] [] [twoHandles 1/4 0]
<< [shift (3dx,~4dy)] [] [twoHandles 0.001 0]
<< [shift (4dx,~4dy)] [] [twoHandles 0 0]
<< [shift (1dx,0dy)] [] [twoHandles 1/2 1]
<< [shift (2dx,0dy)] [] [twoHandles 1/4 1]
<< [shift (3dx,0dy)] [] [twoHandles 0.001 1]
<< [shift (4dx,0dy)] [] [twoHandles 0 1]
<< [shift (4dx,~1dy)] [] [twoHandles 0 1/2]
<< [shift (4dx,~2dy)] [] [twoHandles 0 1/4]
<< [shift (4dx,~3dy)] [] [twoHandles 0 0.001]
|
In the next example we will see how to connect spline segments, and how offsets are interpreted.
Connected Bezier splines |
---|
|
Bezier splines. Points on the path are marked with circles, and interpolation points are marked with a cross and a dashed line showing which terminal point it belongs to. Solid lines with arrowhead indicates how offset coordinates are interpreted during path construction.
|
|
Source:
show/hide
—
visit
|
##needs ..Applications..Blockdraw
##preamble \usepackage[squaren]{SIunits}
##preamble \usepackage{url}
##lookin ..Shapes
##lookin ..Shapes..Geometry
##lookin ..Applications..Blockdraw
pathStyle: Traits..@width: 1bp
controlStyle: Traits..@width: 0.3bp & Traits..@dash:[Traits..dashpattern 2mm 2mm] & Traits..@stroking:[Traits..gray 0.5]
labelStyle: Traits..@width: 0.3bp & Traits..@stroking:[Traits..gray 0.5]
onPathMark: Traits..@nonstroking:Traits..RGB..RED | [Graphics..fill [Geometry..circle 1mm]]
controlHead: \ p → (> cut:0bp
picture: Traits..@width:1bp & Traits..@stroking:Traits..RGB..RED & Traits..@dash:[Traits..dashpattern] | ([shift p.begin.p]*[rotate [angle p.begin.T]])[][Graphics..stroke (~1.5mm,~1.5mm)--(1.5mm,1.5mm) & (~1.5mm,1.5mm)--(1.5mm,~1.5mm)]
<)
arcHead: [Graphics..ShapesArrow width:2mm ...]
coordsHelper: \ src base offset →
{
p: (base--offset).end.p
[Graphics..TeX
{ tmp: ( String..newString << `\url{´
<< src
<< `} $\Rightarrow (\unit{´
<< [String..sprintf `%.1f´ p.x/1cm]
<< `}{\centi\metre},\unit{´
<< [String..sprintf `%.1f´ p.y/1cm]
<< `}{\centi\metre})$´
) [Debug..log_before tmp tmp] }
]
}
arc: \ p0 p1 a → ( Geometry..@defaultunit:1%C | p0>(^[angle p1-p0]+a)--(^[angle p0-p1]-a)<p1 )
p0: [Debug..locate (5cm,4cm)]
p0r: [Debug..locate (+(~1cm,~1cm))]
p0f: [Debug..locate (+(2cm,8mm))]
p1: [Debug..locate (+(6cm,~2cm))]
p1r: [Debug..locate (+(~1cm,2cm))]
p1f: [Debug..locate (15cm,(+~3cm))]
p2: [Debug..locate ((+5cm),3cm)]
pth: p0r<p0>p0f--p1r<p1>p1f--p2
Traits..@width: 0.5bp
|
{
xMax: 18.9cm
yMax: 6.9cm
IO..•page << [Graphics..stroke (~1.5cm,0cm)--(xMax,0cm) head:[Graphics..ShapesArrow width:3.5mm ...]]
<< [[Data..range 1cm xMax 1cm].foldl
\ p e → ( p & [Graphics..stroke (e,0cm)--(+(0cm,~[if [Numeric..Math..abs [Numeric..Math..mod e 5cm]]<0.1mm 2.5mm 1mm]))] )
Graphics..null]
<< [putlabelBelow [Graphics..TeX `$\unit{5}{\centi\metre}$´] (5cm,~3mm) 0]
IO..•page << [Graphics..stroke (0cm,~1.5cm)--(0cm,yMax) head:[Graphics..ShapesArrow width:3.5mm ...]]
<< [[Data..range 1cm yMax 1cm].foldl
\ p e → ( p & [Graphics..stroke (0cm,e)--(+(~[if [Numeric..Math..abs [Numeric..Math..mod e 5cm]]<0.1mm 2.5mm 1mm],0cm))] )
Graphics..null]
<< [putlabelLeft [Graphics..TeX `$\unit{5}{\centi\metre}$´] (~3mm,5cm) ~0.5]
}
IO..•page << [[Data..range '0 [Geometry..duration pth]].foldl \ p e → p & [[shift [pth e*1].p] onPathMark] Graphics..null]
<< pathStyle | [Graphics..stroke pth]
{
pa: (0cm,0cm)
pb: p0
pTo: (pa--pb).end.p
pLbl: pTo + (~2cm,1.5cm)
IO..•page << [putlabelAbove [coordsHelper [Debug..sourceof pb] pa pb] pLbl 0]
<< labelStyle | [Graphics..stroke pLbl--pTo]
<< labelStyle | [Graphics..stroke [arc pTo pa ~30°] head:arcHead]
}
{
pa: [pth 0].p
pb: p0r
pTo: (pa--pb).end.p
pLbl: pTo + (0.5cm,~1cm)
IO..•page << controlStyle | [Graphics..stroke pa--pTo head:controlHead]
<< [putlabelBelow [coordsHelper [Debug..sourceof pb] pa pb] pLbl 0]
<< labelStyle | [Graphics..stroke pLbl--pTo]
<< labelStyle | [Graphics..stroke [arc pTo pa ~40°] head:arcHead]
}
{
pa: [pth 0].p
pb: p0f
pTo: (pa--pb).end.p
pLbl: pTo + (0.5cm,1.5cm)
IO..•page << controlStyle | [Graphics..stroke pa--pTo head:controlHead]
<< [putlabelAbove [coordsHelper [Debug..sourceof pb] pa pb] pLbl 0]
<< labelStyle | [Graphics..stroke pLbl--pTo]
<< labelStyle | [Graphics..stroke [arc pTo pa ~40°] head:arcHead]
}
{
pa: [pth 0].p
pb: p1
pTo: (pa--pb).end.p
pLbl: pTo + (~2.5cm,~1cm)
IO..•page << [putlabelBelow [coordsHelper [Debug..sourceof pb] pa pb] pLbl 0]
<< labelStyle | [Graphics..stroke pLbl--pTo]
<< labelStyle | [Graphics..stroke [arc pTo pa 20°] head:arcHead]
}
{
pa: [pth 1].p
pb: p1r
pTo: (pa--pb).end.p
pLbl: pTo + (1cm,1.5cm)
IO..•page << controlStyle | [Graphics..stroke pa--pTo head:controlHead]
<< [putlabelAbove [coordsHelper [Debug..sourceof pb] pa pb] pLbl 0]
<< labelStyle | [Graphics..stroke pLbl--pTo]
<< labelStyle | [Graphics..stroke [arc pTo pa 40°] head:arcHead]
}
{
pa: [pth 1].p
pb: p1f
pTo: (pa--pb).end.p
pLbl: pTo + (2cm,1.3cm)
IO..•page << controlStyle | [Graphics..stroke pa--pTo head:controlHead]
<< [putlabelAbove [coordsHelper [Debug..sourceof pb] pa pb] pLbl 0]
<< labelStyle | [Graphics..stroke pLbl--pTo]
<< labelStyle | [Graphics..stroke [arc pTo pa 40°] head:arcHead]
}
{
pa: [pth 1].p
pb: p2
pTo: (pa--pb).end.p
pLbl: pTo + (~1cm,1cm)
IO..•page << [putlabelAbove [coordsHelper [Debug..sourceof pb] pa pb] pLbl 0]
<< labelStyle | [Graphics..stroke pLbl--pTo]
<< labelStyle | [Graphics..stroke [arc pTo pa ~20°] head:arcHead]
}
|
Given the examples above, the following observations and claims regarding path construction should seem at least plausible:
When the path leaves a through-point, it does so in direction towards the next interpolation point, or the next through-point if there are no interpolation points.
Hence, to avoid a corner at a path point, its handles mush point in opposite directions.
To avoid discontinuity in the second order derivatives, the handles shall additionally be of equal lengths.
Since angles to interpolation points are so important, it is desirable to be able to enter interpolation points in polar coordinates, relative to the through-point.
Specifying the distance to the interpolation points is not as intuitive as specifying the angles.
Hence, some mechanism for automatic determination of such distances should be welcome.
A mechanism for automatic determination of angles should be welcome too.
Automatic choices and values set directly by the user should mix in a way that allows complete freedom to mix the two, while also being easy to learn and remember.
While it is possible to use polar coordinates to specify interpolation points relative to the through-point by using the
..Shapes..Geometry..dir function, for example
(+ r*[dir a]), this solution has no room for automatic choices.
Of course, all of these features are available in Shapes. The syntax may feel a bit clumsy initially, but that's the small price we pay for the powerful and easy to remember syntax, see
polar-handle-2D and
corner-point-2D.
While automatic angles are determined using a rather simple scheme, selecting the distances is much more complicated. In Shapes, angles are always determined before determining any distances. This means that a scheme for automatic determination of distances has to determine these based on the location of the through-points and the angles from the through-points to the intermediate interpolation points. (When the two distances at a path point shall be equal, and both are to be determined automatically, then both are determined individually first, and then the smaller is used on both sides of the path point.) It is easy to see that the task is over-parameterized as stated here, and that the coordinates of the through-points can be reduced to just the distance between. A
special unit is a mapping from the three resulting parameters to a length. The most frequently used special unit is
%C, which helps to produce circular arcs.
The use of polar handle coordinates and automatic determination of angles and distances will not be discussed further in this tutorial. The reader is encouraged to follow the links in the text to the reference documentation in order to learn more about path construction.
Path-painting |
---|
|
Filling, stroking, filling-and-stroking, and stroking a path with an arrow head. Spotting a point can be seen as a special case of stroking a singleton path.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
IO..•page << [Graphics..stroke (0cm,4cm)--(0cm,0cm)--(5cm,0cm)]
IO..•page << [Graphics..fill (1cm,1cm)--(3cm,2cm)--(2cm,3.5cm)--cycle]
IO..•page << Traits..@width:3bp & Traits..@nonstroking:[Traits..gray 0.7] | [Graphics..fillstroke (3cm,1.5cm)--(3.5cm,1cm)--(1.5cm,0.8cm)--cycle]
IO..•page << Geometry..@defaultunit:1%c | [Graphics..stroke (3mm,3mm)>(^0°)--(^~90°)<(4cm,3cm) head:Graphics..ShapesArrow]
IO..•page << Traits..@width:8bp & Traits..@stroking:[Traits..gray 0.5] | [Graphics..spot (1cm,3.5cm)]
|
Things to note:
The arrowheads are defined in a Shapes extension file. If your Shapes compiler has the path /usr/local/bin/shapes, then the extension files are typically located in /usr/local/share/shapes/extenions/.
Functions can take named arguments, and here the argument called head is some kind of object which defines what the arrowhead shall look like.
As usual, we use an example to show what the most important parameters are called, and how to set them.
Graphics-params |
---|
|
Changing parameters in the pdf graphics state, such as stroking and non-stroking (that is, fill) color, dashpattern, &c.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
##lookin ..Shapes..Geometry
pth: Geometry..@defaultunit:1%C | (0cm,0cm)>(^60°)--(^)<(2cm,0.5cm)>(^)--(^)<(3cm,~1cm)--(^)<(6cm,0cm)>(^90°)--(5cm,0.5cm)--(0.5cm,1cm)--cycle
yStep: 2cm
/** Topmost, we just stroke the path to show what it looks like with default parameters **/
IO..•page << [Graphics..stroke pth]
/** The default fill color is black: **/
IO..•page << [shift (0,~1*yStep)] [] [Graphics..fill pth]
/** Note the various ways to combine many several bindings, as shown in the following examples that set properties of a stroke. **/
/** This way, it is difficult to indent nicely since there is no ampersand associated with the first binding. **/
IO..•page <<
Traits..@stroking:[Traits..rgb 0.1 0.3 0.2]
& Traits..@width:2bp
& Traits..@cap:Traits..Cap..SQUARE
& Traits..@dash:[Traits..dashpattern 3mm 2mm 1bp 2mm]
|
[shift (0,~2*yStep)] [] [Graphics..stroke pth]
/** This way, one has to introduce <nullbind>, and indenting is still not an easy question. **/
IO..•page <<
nullbind
& Traits..@stroking:Traits..RGB..BLUE
& Traits..@width:2bp
& Traits..@cap:Traits..Cap..BUTT
& Traits..@dash:[Traits..dashpattern 3mm 2mm 1bp 2mm]
|
[shift (0,~2.25*yStep)] [] [Graphics..stroke pth]
/** This way, one has to abandon the good old & way of combining bindings, and learn about the new function <bindings>.
** However, indenting is easy here.
**/
IO..•page <<
[bindings
Traits..@stroking:Traits..RGB..GREEN
Traits..@width:1mm
Traits..@cap:Traits..Cap..ROUND
Traits..@join:Traits..Join..MITER
Traits..@miterlimit:4.5
Traits..@dash:[Traits..dashpattern 3mm 2mm 1bp 2mm]
]
|
[shift (0,~2.5*yStep)] [] [Graphics..stroke pth]
/** The fill color is changed as follows: **/
IO..•page << Traits..@nonstroking:Traits..RGB..BLUE | [shift (0,~3*yStep)] [] [Graphics..fill pth]
/** If a path is both to be filled and stroked, it is efficient to use the fillstroke command,
** here, the ampersand is used instead of the function <bindings>:
**/
IO..•page <<
Traits..@nonstroking:Traits..BW..WHITE
& Traits..@stroking:[Traits..rgb 0.5 0.5 0]
|
[shift (0,~4*yStep)]*[rotate 30°] [] [Graphics..fillstroke pth]
|
Things to note:
The various ways several dynamic bindings can be combined to one set of bindings, including using the function
..Shapes..bindings and inserting the empty set of bindings
..Shapes..nullbind explicitly to make it one operator
& per binding.
By now you know how to construct the most important basic elements that constitute the visual contents of a page. Thus you are ready to use Shapes as a meta-language for pdf — indeed, the first name of the language was MetaPDF — but that is not what Shapes is meant for. Shapes is all about how you put these elements on the page, and to this end you need to learn more about what programming in Shapes is like.
Under this overly generic title, we'll discuss basic programming concepts, such as states, functions, and structures. As usual, we'll often do this by giving relevant pointers to the reference documentation.
We have already seen how variable bindings can be introduced, like this:
a: 8 + 7
but we have not yet discussed where such code may appear. The proper place to introduce a variable binding is a
code bracket. A complete Shapes program is implicitly enclosed in a code bracket, and the user may introduce new code brackets by using the delimiters
{ and
}.
Here, the sum of 7 and 8 is squared by using a variable binding for the sum, and then multiplying that variable by itself:
{
a: 8 + 7
a * a
}
Code brackets serve many purposes. To learn more, see
code-bracket.
In this section, the pure functions will be introduced. These are my favorite function-like abstraction, and hence what you will find in most code examples. Other function-like abstractions will be introduced later, after state have been properly introduced.
A function has no side effects. Given arguments and a dynamic environment, a function returns a value. Given the same arguments and the same dynamic environment, the function will always return the same result. This implies that a function must not be allowed to access anything which can change from time to time. In Shapes these things which can change are the states, so the rule is simply that a pure function is not allowed to access any states. Note that this does not mean a function cannot use states internally to compute its result.
The following example shows how a function is constructed:
textGreater: \ x y .> [if x > y `true´ `false´]
The
formal parameters appear between
\ and
.>, and the expression that follows is the
body of the function. The result of calling the function is obtained by evaluating the body, with the formal parameters added to the
lexical scope For instance,
•stdout << [textGreater 4 5]
would result in
false being written to
stdout.
The above example code shows what a typical function call looks like, with the callee and arguments enclosed in square brackets. When there is only one argument, one may reorder things a little:
•stdout << sin [] 30°
or
•stdout << 30° >> sin
Please read the short discussion about
unary calls at this time.
Let us also show how
evaluated cuts can be used to bind just some of the arguments of a function:
textGreaterThanEight: [textGreater y:8 ...]
In combination with evaluated cuts of
log_after,
>> provides a rather clean way to write stuff to the log file at an arbitrary location in the program. Here is an exmaple:
fun: \ a →
{
b: 10 * a
a + b
>> [debuglog_after ( newString << `The value of a is ´ << a << "{n} ) ...]
>> [debuglog_after ( newString << `The value of b is ´ << b << "{n} ) ...]
}
Recall that global states such as
..Shapes..IO..•stderr is not in scope inside the function body, but by making
stderr the debug log file (see
man page), this is a way to get around the function's scoping barrier.
There is much more to say about evaluated cuts, but we refer to the language reference (including the section linked above, but please note that there are other related sections as well), and just include an example here.
Evaluated cuts |
---|
Evaluated cuts may take some time to grok. Use this example as an exercise!
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
foo: \ a b c → a + b + c
IO..•stdout << [foo `1´ `2´ `3´] << "{n}
bar: [foo b:`BAR´ ...]
ofo: [bar `ofo´ ...]
IO..•stdout << [ofo `2´] << "{n}
IO..•stdout << [bar `1´ `3´] << "{n}
IO..•stdout << [foo [...] b:`2´ [...] `1´ [...] `3´] << "{n}
|
|
stdout:
show/hide
|
123
ofoBAR2
1BAR3
123
|
Function taking an arbitrary number of arguments can be defined by the use of a
sink. This is an advanced topic, also discussed in
syntax.html.
The lexical scope is the set of bindings which can be “seen” from a particular point in the code. This is a very common concept, explained in more detail in
syntax.html (where the reader will also find the example included below), and the reader is referred to
[Google™ `"lexical binding"´] for further in-depth discussions. One thing to note about the lexical scope in Shapes, though, is that when a new binding is introduced, the scope where the binding is introduced is in scope where the right hand side expression is evaluated. That is, defining (mutually) recursive functions is trivial.
Lexical binding |
---|
Lexical scoping rules in Shapes make it easy to define (mutually) recursive functions. To access bindings shadowed in the current scope, one can reach out by using the ../ construct.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
a: 1
{
a: 2
{
a: 4
IO..•stdout << `a0 = ´ << a << "{n}
IO..•stdout << `a1 = ´ << ../a << "{n}
IO..•stdout << `a2 = ´ << ../../a << "{n}
IO..•stdout << `a1+a2 = ´ << ../(a+../a) << "{n}
}
}
{
odd: \ n → [if n = 0 false [even n-1]]
even: \ n → [if n = 0 true [odd n-1]]
IO..•stdout << `Is 0 odd? --> ´ << [odd 0] << "{n}
IO..•stdout << `Is 4 even? --> ´ << [even 4] << "{n}
}
|
|
stdout:
show/hide
|
a0 = 4
a1 = 2
a2 = 1
a1+a2 = 3
Is 0 odd? --> false
Is 4 even? --> true
|
Dynamic bindings, recognized by being prefixed with the at sign (@) differ from lexical bindings in that one cannot determine where they were bound by a lexical analysis of the code. Instead, one must (conceptually) unwind the chain of function calls to find the closest point where the dynamic variable was bound. We have already seen dynamic variables in use in
Changing graphics parameters, such as color, although the examples seen there were very simple and did not really display the power of dynamic binding. The main documentation is in
syntax.html (the example below also appears there), but interested readers should also try
[Google™ `"dynamic binding" lexical´] for further in-depth discussions. The thing to note about dynamic bindings in Shapes is that a dynamic variable can be bound to an expression that is to be evaluated each time the dynamic variable is accessed. This feature i called an
dynamic expression (note that these can only be bound to dynamic variables).
Since dynamic bindings and the way it is accomplished in Shapes is a very important topic for this tutorial, study the following examples carefully! The first example just motivates the notation used for one of the operators used to combine dynamic bindings, do not expect to understand this example completely until after reading the second example.
Notation of the asymmetric binding union operator |
---|
|
The story behind the notation of the operator &|, and a typical application of it.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
##lookin ..Shapes..Geometry
/** Below we combine a set of dynamic bindings using the operator & .**/
f0: Text..@font:Text..Font..COURIER & Text..@size:15bp & Text..@horizontalscaling:1.4
slim: Text..@horizontalscaling:0.8
/** Paint some text with the standard font f0. **/
IO..•page << f0 | [shift (0cm,0cm)] [] ( Text..newText << `Standard font´ )
/** Now we would like to change one of the text state parameters of f0. Then we might to like this: **/
IO..•page << f0 | slim | [shift (0cm,~1cm)] [] ( Text..newText << `Slim font using nested scopes´ )
/**
** But what if we would like to have a new binding for the text state bindings we just used?
** The following would be an error:
**/
|**f1: f0 & slim
/**
** The reason is that Text..@horizontalscaling is bound in both arguments to & .
** This is what we have to do:
**/
f1: f0 &| slim
IO..•page << f1 | [shift (0cm,~2cm)] [] ( Text..newText << `Slim font in one scope´ )
/**
** Note how the operator &| reminds of how the operator | could have been used unless we wanted a
** single dynamic bindings value to hold all bindings.
**/
|
Dynamic binding |
---|
Dynamic scoping in Shapes allows for both dynamically bound variables and dynamically evaluated expressions. The keyword dynamic is used both for introducing new dynamic bindings and to indicate that an expression is to be evaluated dynamically.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
/**
** First, let's have a look at some basic use of dynamic binding.
** If the first parameter after the name of the variable is the special <identity> function it is optimized away by the kernel.
** The second parameter after the name is the default value, and is not sent through the filter.
**/
dynamic @a identity 8
f: \ b → @a + b
IO..•stdout << [f 2] << "{n}
IO..•stdout << @a:6 | [f 2] << "{n}
IO..•stdout << [f 2] << "{n}
/**
** Note that the default value is delayed, so we can require that a dynamic variable must be bound by the user.
** This is also nice to know if the default expression would be expensive to compute but rarely used.
** Another application would be to detect whether the default value is ever used.
**
** However, the filter is evaluated immediately -- this simplifies the kernel business.
**/
dynamic @a_noDefault identity [error `Dynamic variable has no default binding.´]
IO..•stdout << @a_noDefault:9 | @a_noDefault << ` No error, see?´ << "{n}
dynamic @a_logDefault identity [Debug..log_before `The default value was used.´+"{n} ~9]
IO..•stdout << @a_logDefault << ` Check out the debug log!´ << "{n}
/**
** Next, we turn to dynamic expressions.
**/
dynamic @c identity 10
dynamic @b identity dynamic @a + 5
@a: dynamic @c * 4
& @c: 20
|
{
IO..•stdout << `@b: ´ << @b << "{n}
}
/**
** The rest of this file illustrates how to replace part of a dynamic bindings value by using the &| operator.
** The typical application of this would be to define a set of bindings for the text state, and then define a variation
** by changing just some of the parameters. However, since this example is meant to be text-oriented we use a silly
** mix of meaningless bindings instead.
**/
b0: Traits..@width:3mm & @a:20 & @b:21 & Traits..@stroking:[Traits..gray 0.5]
b1: Traits..@width:1mm & @b:11 & @c:12
/**
** Note that b0 & b1 would be illegal, since they both provide bindings for Traits..@width and @b.
**/
/**
** Combine b0 and b1 with priority to the bindings in b1:
**/
b01: Debug..locate [] b0 &| b1
b01
|
{
IO..•stdout << [Debug..sourceof b01] << ` :´ << "{n}
IO..•stdout << `@a: ´ << @a << "{n}
IO..•stdout << `@b: ´ << @b << "{n}
IO..•stdout << `@c: ´ << @c << "{n}
IO..•stdout << `Traits..@width: ´ << Traits..@width << "{n}
IO..•stdout << `Traits..@stroking: ´ << Traits..@stroking << "{n}
}
/**
** Combine b0 and b1 with priority to the bindings in b0:
**/
b10: Debug..locate [] b1 &| b0
b10
|
{
IO..•stdout << [Debug..sourceof b10] << ` :´ << "{n}
IO..•stdout << `@a: ´ << @a << "{n}
IO..•stdout << `@b: ´ << @b << "{n}
IO..•stdout << `@c: ´ << @c << "{n}
IO..•stdout << `Traits..@width: ´ << Traits..@width << "{n}
IO..•stdout << `Traits..@stroking: ´ << Traits..@stroking << "{n}
}
|
|
stdout:
show/hide
|
10
8
10
9 No error, see?
~9 Check out the debug log!
@b: 85
b0 &| b1 :
@a: 20
@b: 11
@c: 12
Traits..@width: 0.1cm
Traits..@stroking: [gray 0.5]
b1 &| b0 :
@a: 20
@b: 21
@c: 12
Traits..@width: 0.3cm
Traits..@stroking: [gray 0.5]
|
By now, we know that a pure function may not access states, and we have seen the globally defined states
..Shapes..IO..•page and
..Shapes..IO..•stdout in use in the examples. Here, the reader will learn how to set up new states, and what the basic operations on states are. How states are used with non-pure functions is postponed until
Non-pure functions.
A state may be introduced in a code bracket just like a variable binding:
•myState: newGroup
The right hand side is a value that is able to spawn new states. Shapes comes with both globally bound values with such capability (for example
..Shapes..Graphics..newGroup) and functions producing such values (for example
..Shapes..Numeric..Random..newRandom).
The states themselves are of
state type. For instance,
..Shapes..IO..•page is of type
§•Group. The operations on a state which we are about to describe briefly are further described in the documentation for state types.
There are three basic operations,
tack on,
peek, and
freeze. The first of these looks like a
c++ insertion operation, and may repeated:
•myState << (TeX `Hello!´)
<< [stroke (1cm,1cm)--(2cm,0cm)]
The state is free to modify itself as it pleases when values are tacked on to it.
The next operation,
peek, may also modify the state, but shall also return a value somehow representing the current state of the state. The syntax is simply to enclose the state in parentheses:
picture: (•myState)
Not all state types allows the state to be peeked, usually for efficiency reasons.
The last operation,
freeze, returns a value representing the state, and renders the state unusable in the future. This allows for efficient implementation. The state is frozen by writing a semicolon after the state, and this operation is only allowed as the right hand side of a variable binding, or as the last statement in a code bracket. For instance,
finalState: freese •myState
Not all state types allows the state to be frozen.
States can also be used without naming the state. For instance:
picture: ( newGroup << (TeX `Hello!´) << [stroke (1cm,1cm)--(2cm,0cm)] )
gives the same result as
picture: ( (TeX `Hello!´) & [stroke (1cm,1cm)--(2cm,0cm)] )
We end this section with an example.
States |
---|
States allow imperative style programming to some extent in Shapes. This example also illustrates non-pure functions, which have not yet been discussed.
|
|
Source:
show/hide
—
visit
|
##lookin ..Shapes
/** Basic use of states includes defining new states, tacking on to them, and finally using the result.
**/
•str: String..newString
•msg: String..newString
•str << `Say´ << ` ´
•msg << `Say´ << ` ´
•str << `hello!´
•msg << `goodbye!´
/** Here we freeze the state •str, assigning the final value to the variable str.
**/
str: freeze •str
/** Future access to •str is invalid; the following would cause an error:
**/
|**•str << `Too much´
/** We can also peek at the state. This is a potentially expensive operation, depending on
** the implementation of this particular kind of state.
**/
IO..•stdout << str << "{n} << (•msg) << "{n}
/** The following is legal:
**/
•msg << `This long message is never used´
/** States can also be created temporary without giving them names. This has the following syntax,
** where String..newString is used to spawn the unnamed state. The state is frozen at the end of the
** insertion sequence within parentheses, and the final value is returned.
**/
IO..•stdout << ( String..newString << `a ´ << `b´ << "{n} )
/** Next, we turn to the scoping rules for lexical access of states. The rule is that states
** cannot be accessed across function boundaries. This makes functions behave as functions.
**
** Versions of Shapes up to 0.5.0 also had procedures which did not have the "function boundary",
** and which had to be called using special syntax. However, these have been removed to make
** programs easier to understand.
**/
/** Let's begin with a legal program.
**/
fun: \ a →
{
•s: String..newString
•s << a << `Hey!´ << a
freeze •s
}
IO..•stdout << [fun `--´] << "{n}
/** The following would be illegal, since IO..•stdout is in the global environment,
** which is outside the function boundary.
**/
|** badFun: \ a →
|** {
|** IO..•stdout << a
|** `Hej!´
|** }
/** States may also be passed to functions. Functions that take states as arguments are like
** macros in that their evaluation cannot be delayed. However, since the states being modified
** are explicitly passed to the function, the code is much easier to understand than code that
** uses procedures.
**/
writeHello: \ •dst → { •dst << `Hello!´ << "{n} }
[writeHello IO..•stdout ]
|
|
stdout:
show/hide
|
Say hello!
Say goodbye!
a b
--Hey!--
Hello!
|
A non-pure function is like a pure function, except that it also takes state arguments by reference. This means that the result of a non-pure function call cannot be cached for reuse, and that the side effects the function may have are limited to the states explicitly passed in the call.
Non-pure functions are defined just like pure functions, and called the same way. The only difference is that states appear among the formal parameters, and that states needs to be passed for these formal parameters. The following example shows how a non-pure function is used to add a stroke to
..Shapes..IO..•page:
mark: \ •dst .> { •dst << [stroke (0cm,0cm)--(1cm,1cm)] }
[mark •page]
A non-pure expression such as [mark •page] may be used as a statement, which is completely unlike a pure expression such as [stroke (0cm,0cm)--(1cm,1cm)]. Statements may appear anywhere in a code bracket, while expressions only make sense at the end of a code bracket.
|
When a function is called, the formal parameters are bound in a scope where the body of the function is then evaluated. One can think of this scope as a barrier through which states cannot be reached. In early versions of Shapes, there were also procedures, which did not have such a barrier. Their use had to be controlled in certain ways during runtime, were always consiered an ugly feature of the language until the day they were removed. They could be removed without the pain that usually comes with backwards incompatible changes, simply because they had never been used in applications.
|
See the example under
States for more examples.
Structures are the simple means for data abstraction currently supported by Shapes.
A common application of structures that a user may need to know about is arrowheads. An arrowhead function shall, given a path, return a length that the path shall be shortened by, and a picture that is the actual arrowhead. This would be a valid arrowhead function:
\ pth .>
(>
cut: 2mm
picture: [fill [circle 1mm]] >> [shift pth.begin.p]
<)
Note that the names of the field in the structure is part of the structure's type, and hence of the function's signature.
Structures have many uses. Please refer to
syntax.html for an example and more details.
This is the end of the tutorial. When you are ready for more, you are recommended to look at the topics which are covered more in-depth in the
user's guide.