Vector Graphics in Lil

RodgerTheGreat1 pts0 comments

Vector Graphics in Lil

Dr. Allen Vincent Hershey developed some of the first digital typefaces, described by simple sequences of straight line-segments. These were originally published in his 1967 report Calligraphy for Computers.

The hershey module for the Decker ecosystem is concerned with laying out text using these typefaces. Decker’s scripting language, Lil, is of an unusual APL-influenced design. This article will explore Lil in the context of 2-dimensional vector graphics, expanding upon examples also discussed in the interactive documentation linked above.

Foundations

A point is an (x,y) pair of numbers describing rectangular coordinates on a canvas- a drawing surface. Points are represented in Lil as lists with a length of 2:

point:(3,-5)

A stroke is a list of points representing a sequence of lines to draw on the canvas, and thus also a list of lists of numbers. Given a list of four points, the stroke connects the first point to the second, the second point to the third, and the third point to the fourth. If there are N points in a stroke, it describes N-1 connected lines. We might also call a stroke a "polyline":

stroke:((list 3,-5),(list 2,5),(list 0,3))

A path is a list of strokes, and thus also a list of lists of points, and a list of lists of lists of numbers. A path can represent the strokes that make up a single shape- like the letter "A"- or a sequence of letters representing an entire word or sentence. The strokes within a path are ragged: they may vary in length.

path:(list (list 1,2),(list 3,4)),<br>(list (list 5,6),(list 7,8),(list 9,10))

The hershey.textpath[] function assembles a complex path based on a Lil string using the glyphs of a font, each of which is a simpler path:

p:hershey.textpath[hf_futura "ABC"]

Thus constructed, we can draw the text path to a canvas c using either an explicit each loop or the equivalent shorthand @ operator. The Decker canvas widget can draw strokes with the canvas.line[] function, given an appropriate list of lists of numbers:

each stroke in p<br>c.line[stroke]<br>end

c.line @ p

Wherever possible, Decker’s scripting APIs are designed to work as collective operations, applying to a large data structure at once. Drawing a single straight line-segment is the simple case for a more general polyline-drawing operation. If canvas.line[] could only draw a single straight line-segment, we would have to explicitly manage the off-by-one relationship between the N points of a path and the N-1 lines they represent. For example,

each stroke in p<br>each point i in stroke<br>if i>0<br>canvas.line[stroke[i-1] point]<br>end<br>end<br>end

A small ergonomic flaw like this would be magnified as it appeared throughout a program, obscuring the simplicity of the programmer’s actual intent.1

Path-Mangling

Now that we can obtain and render interesting paths, let’s look at manipulating them.

Combining paths is easy: Lil’s , operator concatenates lists. Concatenating a list of strokes and a list of strokes results in a list of strokes.

Scaling paths is more interesting. As discussed previously, many of Lil’s arithmetic operators will implicitly conform over nested listy structures. Multiplying a scalar number by a list of numbers spreads the multiplication to each element of the list, and the same process works recursively for nested lists. We can therefore uniformly scale points, strokes, or paths with simple multiplication2:

(11,22,33)*2<br># (22,44,66)

((list 3,-5),(list 2,5),(list 0,3))*2<br># ((6,-10),(4,10),(0,6))

A non-uniform scale runs into trouble. We now want to multiply the x-coordinates of every point nested inside the path with a different value from the y-coordinates. If we multiply our path by a pair of numbers, the * operator doesn’t implicitly know that we want to "push" that conforming process down to the innermost pairs. When * is given lists to the left and to the right with different lengths, it treats the right list as if it were repeated or truncated to match the length of the left list:

(1,2,3,4)*(10,100)<br># (10,200,30,400)

If we first enclose the right operand with list, * will repeat the element contained in that length-1 list and pair it up with each element of the left argument:

(1,2,3,4)*list(10,100)<br># ((10,100),(20,200),(30,300),(40,400))

For a path, we need to enclose the right operand twice: once to spread the scale to each stroke of the path, and then again to spread the scale to each point of the stroke:

p * list list scalex,scaley

Scaling the x or y axis by a negative number will mirror a path horizontally or vertically, respectively:

p * list list -1,1

Translating a path can use the same trick as scaling, but with + instead of *:

p + list list transx,transy

To shear horizontally, we need to offset the x coordinate of every point in a path proportionally to the y-coordinate. We can use the last primitive to extract the y-coordinates of all the points in a path. The @ operator allows us to "push" last down into the right operand. The last of...

list path stroke point lists line

Related Articles