I made thing for this website/Glorpdown a little while ago. Thing that lets me render a diagram made with ASCII art as SVG on a webpage. So I can do:
And get:
(I kind of like that the diagrams are there when viewing the plaintexty Glorpdown and not just when viewing the rendered HTML.)
I looked a bit around before I made it. I tend to look for something like, some ways of thinking about the problem and maybe dividing it into subproblems, different approaches, tradeoffs, maybe some "patterns." I don't usually find what I'm looking for though. Found some existing solutions, but couldn't be bothered digging into them and figuring out what the important ideas were etc. So I just ended up doing something:
One thought I had was something like: If you draw a rectangle with ASCII art, maybe the thing should figure out that it's a rectangle and make an SVG <rect>
, and so on. The alternative seemed to be to process one character at a time: If the character's a hyphen, we make a little horizontal SVG <line>
, and so forth. I think it sounds nice to have an SVG with reasonably few, uh, "appropriate" SVG shapes in it. And less nice to have one with a ton of tiny lines that happen to form shapes.
So I had some thoughts like, if I find a hyphen, should I like try to follow along that line to see what shape it makes? And then, how do I deal with this-or-that? Should I "consume" a character once I've processed it as part of a shape? What if it's part of another shape as well? How do I know e.g. which way to follow it at the plus sign on the hello box there? Bunch of stuff that sounded like stuff that I didn't want to deal with.
So eh. Decided to do the one character at a time thing. If I did that and also had some "line joining" machinery, then I could probably keep things fairly simple in my head and also keep the SVG from getting too bad.
Like if you added a line from (1, 1) to (2, 2) and then a line from (2, 2) to (2, 1), the machinery would join those together. If you then added a line from (3, 3) to (4, 4), that would not connect to any existing (poly)line, so you'd end up with two "shapes:" One polyline from (1, 1) to (2, 2) to (2, 1), and one line from (3, 3) to (4, 4).
Anyway. The following is a somewhat minimal example. The version I'm using for the website has more stuff. It makes lines out of more characters, it does some SVG <text>
for regular text, handles Unicode stuff a little. But like the important ideas or the fundamentals or something should be in the example.
We'll only do vertical and horizontal lines (hyphens and pipes) and plus signs that can connect in all 4 direction. And we'll try to turn something like this:
Into something like this:
So we'll be using this as a test string:
We wanna draw some SVG stuff. We need points:
Testing:
We make sure that for any x/y-combination, there only exists one instance of a point. That's nice if we want to compare points with ==
or use them as keys in tables. It's like generally pretty convenient.
We'll make SVG. Here's SVG functions:
Because reasons, svgline
takes an iterator function as its points
argument, instead of e.g. a table/array. We'll make a helper function for testing:
Can make a drawing now:
Great.
We wanna turn a string into a data structure that lets us treat things as kind of a 2D map. The details aren't terribly important, but here's some thing:
We'll test it a little:
Seems fine.
We'll deal with the line joining machinery later. In order to have something to program against, we'll make something that doesn't actually join any lines:
Okay. I dunno. I like to think of each character as occupying some space within the larger map, but kind of having its own local coordinates. Characters are often roughly twice as tall as they are wide, particularly in monospacey ASCII art stuff, so e.g. 4 by 8:
So then we can think of the top left of the character as (0, 0), the middle as (2, 4), the bottom right as (4, 8), and so on.
Characters that are next to each other mildly overlap:
(4, 0) of one character is the same point as (0, 0) of the character to its right, etc.
When we're processing a character, we can have a helper function that translates coordinates like those into global coordinates. We'll make a helper function function:
Let's turn some characters into lines. We turn hyphens and pipes into horizontal and vertical lines. Plusses add lines connecting to neighbours, if there are any neighbour characters of the right kinds. We could image expanding this a lot, but like the essence is probably there: Adding lines, possibly depending on neighbours.
We're only adding line to the space occupied by the current characters. In my head I kind of organize the different characters as:
It seems maybe disciplined or something to me, but I'm sure we could have made the line characters look for neighbouring connectors instead.
Also: When looking at neighbours we kind of know about the different characters and what they mean. We could almost imagine looking at the points in the "linespace" or something instead. Like "if there's something at my (4, 4), draw a line connecting to it." But it sounds like it'd get messy trying to deal with "there's nothing at my (4, 4), but maybe there's going to be" stuff...
Eh. When we got lines, we can make SVG:
Test:
Looks okay.
Okay so just to illustrate that we're drawing very many tiny lines this way, we'll mess with the colours:
Those are several.
A few things:
For the extending in either direction part: One way to do it is to just have two array-like tables. Like a polyline with six points could look like this:
Like, if a polyline consists of one 2-element array (first
) and one 4-element array (last
), the full polyline would go like:
first[2]
, first[1]
, last[1]
, last[2]
, last[3]
, last[4]
last[4]
, last[3]
, last[2]
, last[1]
, first[1]
, first[2]
So you kind of count down in one array and then up in the other one in order to get all the points in a reasonable order. Makes it so you can always add new points to the end of one of the arrays in order to extend the polyline.
Some supporty helpy stuff. If a "half" of a polyline is an array connected to the other half, we can make an iterator function that counts down through the half we start with and then up through the one that it's connected to:
An test:
Later on, if we're extending a line and we're not changing direction, we can move the end of the line instead of adding a new point to it. Some direction helper stuff:
Those functions are not very smart. People with smart could make better versions of those. But they're fine for our use.
So uh, the actual joining machinery then. Same interface as nojoining
. We want to create something that has an add
function that takes two points. And after adding a bunch of stuff, we want the thing to be an array with a bunch of stuff. If an element in the array has a points
function, then we can call that to get an iterator function for the points in a polyline/polygon.
Internally, it will also keep track of where lines end and can be extended. Whenever something is added (lines.add
below), the two points are looked up in extendible
. The lookup returns the "half" that can be extended, if any.
line
extend
A polyline is joined to itself (if the two halves we found are each others other
s) by closing it (turning it into a polygon). A polyline is is joined to another one by removing the one with the fewest points and axtending the one we're keeping with all the points from the one we're removing. (With a different data structure for the polylines, we could have made polylines consist of more than two arrays when joining instead I guess. Not something I've bothered with but I dunno, might be fun.)
And we modify extendible
as we go: When we add new lines we add two extension points, one for each half. When we extend a half, we move (remove and add) its extension point. And when we join things we remove two extension points.
A function with a bunch of functions in it. The lines are very managed by the joining machinery, so I just put all of the stuff inside it. I dunno, I'm sure some stuff could be extracted, like maybe the extendible
stuff could exist as more its own thing, but I haven't really felt like it...
Let's test:
Fewer line elements :)
We can also see what the SVG looks like as text, and like confirm that there's a polygon in there and stuff:
Let's draw something else just because why not:
Okay.