The loglo, overhead, marking out CSV-5 in twin contrails, is a body of electrical light made of innumerable cells, each cell designed in Manhattan by imagineers who make more for designing a single logo than a Deliverator will make in his entire lifetime. Despite their efforts to stand out, they all smear together, especially at 120 kilometers per hour.
— Snow Crash, Neal Stephenson
Loglo is "LOGO for the Glowforge": an experimental — very experimental — programming environment by Avi Bryant. It's currently focused on the narrow domain of producing SVG output to feed to a CNC machine or laser cutter.
Loglo integrates:
- A spreadsheet-style user interface
- A Postscript-like minimalist stack-based language
- An immutable compositional API for describing vector graphics
If you're desperate to try it out, you can do so here, hopefully after reading the rest of this doc. Please note that there's no way to save, load, export, import, preserve, share, or really get any value whatsoever out of your work, and only the very basics of a standard library have been provided. But there might be just enough working for you to get a sense of it. If you do try it out, please send feedback to me at avi@avibryant.com.
There's also no reference documentation, and if there were, it would go immediately out of date. But if you're lucky, most of what I describe below is still true. Come, I'll show you around.
So. At first blush, Loglo is a very bare-bones spreadsheet, that looks suspiciously like it might have been produced by a developer with no design skills and only a vague grasp of frontend engineering. But: there are cells, and you can click on one, and type a number in the formula bar, and hit enter, and it shows up in the cell.
You can also do arithmetic, but only if you know RPN. (If you don't, that's exciting: you get to learn something new today. But not here, unfortunately. I'd recommend reading an article on stack-based programming first.)
No = signs needed, btw, everything's always in formula mode here.
What are we here to do, again? Oh right, draw things. Loglo thoughtfully provides exactly one drawing primitive, which produces a single line, 1 pixel long. If you squint, you can see it rendered inside the cell.
Single-pixel lines can be a bit limiting. We can use another primitive to embiggen it. Just accept the exclamation mark for now, I'll explain later.
It will probably not surprise you that, since this is a spreadsheet, we can reference other cells (case insensitively, thank god).
This one might surprise you though.
Instead of writing "line a1 scale!" all at once, we can leave the "line" in A2 and move the "a1 scale!" to the next cell over. Loglo evaluates rows from left to right, and maintains a single stack across a row. So splitting up that little program across more cells does not affect the semantics, just lets you show your work a little bit more.
While we're doing spreadsheet tricks, here's another one: "a1" was an absolute reference, but the "^" here is a relative one, which points up one row to the "70" in B1.
Using "^" allows this nice pattern where you pull out the parameterization of each transformation into a cell that's visible above the results and easily editable.
To build shapes, we need to link up line segments. "join!" will connect the start of one line to the end of another.
Finally, we can close the loop and turn the path into a shape with "fill!".
We made a triangle! That seems worth giving a name to. The colon introduces a symbol; the "." is like PostScript's "def", and creates a new entry in the global dictionary. In the cell, we just show the new label.
It probably goes without saying that we can transform the triangle.
That 180 degree rotation seems pretty useful, though. Could we give it a name?
Here, we're enclosing that little program fragment in curly braces to make it a code block, which is treated as a value rather than executed right away. We then name it "spin" just like we named "triangle" earlier.
(Whitespace, who needs whitespace.)
If we replace our explicit "180 rotate!" with "spin", though, the cell is showing us "{}". This means that the top of the stack is a code block object.
If we want that code block to actually execute, we have to use the "!" operator. That's why we've been seeing "scale!", "rotate!", and so on: those words are just referencing built-in code blocks, and we need to explicitly evaluate them. (On the other hand, operators like "+" and "!" itself are automatically evaluated; to defer them you'd need to surround them in {}).
Later, there will be other evaluation operators, most importantly for mapping ("@"?) and reducing ("%"?) over lists.
Switching gears slightly: below the spreadsheet grid, Loglo has an inspector where you can try out expressions and see the results in more detail than is possible inside a cell. Here, we've grouped together the triangle and its rotated sibling so that we can see them both at once. You might be surprised by their relative position. Why did the triangle get rotated around that upper-right corner, in particular?
If we inspect just the original triangle, and move our cursor over it, we see the 3 vertices, labeled from 0 to 2. The circle marking the 0th vertex is bigger, indicating that it's the "anchor" point. Any transformations will treat that point as the origin.
The anchor point is easily changed, however.
Voila!
The anchor point doesn't — well, won't — have to be one of the vertices. Once the standard library is fleshed out slightly more, you'll be able to set any other shape's vertex, or center point, or just an arbitrary point somewhere, as the anchor.
Working with vertex indices can be convenient, but sometimes you want a more stable identifier. "tag!" will take a symbol and label the current anchor with it; you can then use that symbol to (for example) set the anchor to that point later on.
These labels stay with their vertices during transformations, as you'd expect.
They also persist through more complex operations. As you can see here, when we grouped these two shapes, the vertices were re-numbered to their global index within the group...
... but the "a" label remained stable.
Let's finish things up by resetting the grid a little bit and creating a sideways-facing triangle. Immediately after this we will give it the imaginative name of "triangle2."
When grouped, you can see that triangle and triangle2 overlap. That definitely wasn't done on purpose just so that I could show off boolean operations.
Like this one!
Or this one!
No points for predicting this one.
Finally, just to remind ourselves that this is a spreadsheet, we will change that 30-degree rotation to a 50-degree rotation, and watch in awe as everything else updates live.
Thank you. You're very kind.
Some thoughts tacked on at the end
Reactive interfaces (like spreadsheets) really demand immutable, compositional APIs rather than imperative ones. Side effects get too messy and confusing when they're being constantly re-evaluated. So classic LOGO, or any of its descendants like the Canvas API, really wouldn't work here.
I've seen other compositional graphics APIs (shoutout to doodle), but always assuming you're starting with shapes or images as your primitives, rather than lines. The "join" operation on paths was a major breakthrough for me here.
LOGO never had curves, and bezier control points are too awful to contemplate using directly programmatically. I've implemented a "spline" operation that will convert a straight-edged shape into nice curves, including, most satisfyingly, converting a square into something that is really very nearly a circle, but I haven't quite yet written all of the code to properly do boolean operations with those curves so I'm not ready to show it off yet. (The approach I'm using is based on this post).
I originally assumed I'd use a notebook UI - specifically, Observable, which is reactive and fantastic. But the thing about spreadsheets is that they're so fantastically compact. You can have 100 spreadsheet cells in the space of a single notebook cell. That seems like it should be worth a lot.
Spreadsheets also allow for a more fluid, idiosyncratic spatial organization, like the old spatial Finder on Mac OS. (Hi Omar!)
But by the same token, spreadsheets demand a compact program representation, and encourage breaking your program into very small pieces. The most compact languages I've seen are stack languages, both in terms of number of tokens, and because their lack of nesting makes them more 1-dimensional, ie, they don't use newlines everywhere. This fits well with the traditional (and I think, useful) single-line input to spreadsheets.
Also, the aggressive lack of syntax in stack languages is reminiscent of the original LOGO and I suspect is a good thing for novice users. (Though I might be ruining that with all the K-like operators, which I should probably revisit.)
Keeping the same stack for an entire row of the spreadsheet was an idea that came relatively late in the game. I resisted it initially because it adds a fixed evaluation order which seems very antithetical to spreadsheets. But in practice I think people work left to right anyway, and the implicit reference to the left neighbor just feels so good (and compact!). Also: I love that you can write the same program left to right within a single cell, or use those same words, split up into chunks however you like, left to right across the row, and the semantics are identical).
Well, identical if you don't use the "^" operator. That one may be a mistake.
There's also strict top to bottom evaluation (so that the "^" operator can work (that one really might be a mistake)). This is more problematic than the left to right, but so far I'm convincing myself that because you can defer evaluation with {} you can get forward references that way and so it's ok.
I realized halfway through doing this doc that with the row-based evaluation, we really ought to switch the row/column notation and give rows letters so that A1, A2, A3 are all evaluated in sequence. But I don't want to redo all the screenshots so I'll do that later.
Stack languages are nice for spreadsheets, but spreadsheets are also nice for stack languages. Being able to peek at the top of the stack anywhere just by introducing a cell break is fantastic for visibility. And it also (implicitly, without any extra typing) gives you a name for those intermediate results that you can reference which makes you need dup/swap games much less. (You still want them for functions that you pull out, but less so in direct user code.)
Remember to send feedback! Black lives matter. Peace out.