Making Runes Pretty

Thu, August 20, 2020
Design tension: Runes should look good, but be easy to produce.

Usually, things that look good take time. And things that are quickly made look shabby.

But I really want the best of both worlds here: CodeSpells community members (including me) should be able to quickly produce a Rune, but without having to boot up Photoshop or Illustrator.

In my wizard tower yesterday, I wove a bit of magic to these ends.

As you may recall, my hand-edited, slow runes looked like this:

And the unfinished ones I showed in yesterday's post looked like this:

And I said they were created with this code:

(typeset-runes
  basic-lang
  (build small))

As I was improving the look and feel, I decided that someone out there might want Runes that look more cartoony. Who am I to judge? So I decided that basic-lang ought to be a function (not a constant), so that it can take parameters. This will allow bindings from identifiers (like build and small) to HTML widgets like and to be more easily re-skinned, without having to redefine every single rune.

Case in point: This code...

(typeset-runes
  (basic-lang) ;Note the new parens
  (build small))

...now produces...

Not only does it look "crunchier" and less cartoony, you can also get the old one back like this:

I've provided a few different filters (like the default crunchier and the crunchy one used above). Such filters can be used to alter all of the id-to-Rune bindings in a language. This is an easy way to make Runes fit with the aesthetic of a CodeSpells Authored Work without having to redraw them all. Hopefully, this will facilitate the sharing of Runes across different worlds in the CodeSpells Canon.

A few more examples:

(typeset-runes-block
   (parameterize
     ([svg-filter blurry])
     (basic-lang))
   (build small))

Gives:

And:

(typeset-runes-block
   (parameterize
     ([svg-filter van-gogh])
     (basic-lang))
   (build small))

Gives:

You can even mix and match if you want to have different Runes with different aesthetics (perhaps to signify that they have different semantics or different lore). Suppose (my-lang) has a binding for the id X, whereas (basic-lang) has bindings for build, small, and define. You can easily combine them into your own new language, applying filters as necessary.

(define combined-lang
  (append-rune-langs
   (parameterize
     ([svg-filter blurry])
     (basic-lang))
    (my-lang)))

(typeset-runes-block
  combined-lang
  (define X
    (build small)))

And now the Rune for X uses the default crunchier filter amidst the rest of the Runes that use the van-gogh filter.

Oh! And filters can do anything an SVG <filter/> element can do. So you can define your own too. Those interested in doing so are welcome to track down how I did it in codespells-runes.

But that's enough about filters. Let's talk about where the Rune for X came from -- and how Rune widgets are defined in the first place.

Creating new Runes

To ease the definition of new runes, I've added support for one of my favorite functional image libraries. I've used this one for years to teach coding to kids as young as 5; and I've used it myself in production systems. It was a natural choice for Rune definitions.

It lets you do simple things like:

(circle 20 'solid 'red)

Which would produce a simple red circle:



Or, you can position images relative to other images, which returns a new image:

(define c (circle 20 'solid 'red))
(beside c c)

Giving:



Or, you can write functions that take and return images:

(define c (circle 20 'solid 'red))

(define (grid image)
  (above
    (beside image image)
    (beside image image)))

(grid c)

Giving:



Let's use the above tricks to define a Rune language.

(define (my-lang)
  (rune-lang 'my-lang
             (list
               (html-rune 'X
                          (my-rune
                            (grid c)))
               (html-rune 'Y
                          (my-rune
                            (grid
                              (grid c))))
               (html-rune 'Z
                          (my-rune
                            (grid
                              (grid
                                (grid c))))))))

Now we combine this lang with (basic-lang) so we can reuse Rune definitions for open and close parentheses. And we typeset the expression (X Y Z):

(typeset-runes-block
    (append-rune-langs
      (basic-lang)
      (my-lang))
    (X Y Z))


That expression, though, wouldn't actually run. The variables X, Y, and Z all have Rune definitions, but that doesn't mean they have any meaning. Just an appearance. The following, however, would actually run in-game. Can you figure out what it's equivalent to?



Here's the text-based code that I typeset.

(define X small)
(define (Z Y)
  (build Y))
(Z X)

And the expresssion (Z X) at the end is equivalent to (build small). Note that the Y Rune is used as a parameter in the Z function


In the last post, I mentioned a key technical challenge in Rune-based coding: user-defined variables

With the above, we are a bit closer to solving the problem. There is -- at least in the modding language -- a way to create the look and feel for new Runes, and to make collections out of them. And those Runes can be typeset alongside Runes from other collections, like (basic-lang).

But as it stands, you'd need to have Runes for all the variables you're going to use in writing a spell -- equivalent to having to pre-define all of your variable names before writing a program. Annoying to say the least.

(short term vs long term)

I might actually resort to the above in the short term if I feel I've been spending too much time on Runes and the spellcrafting experience. I miss voxels. More importantly, I want to be able to show the community some of the cool in-game stuff, I've been working on.

It's not the worst from a gamification point of view either. One could imagine that, as players progress through the 3D world, they accumulate a collection of "Unbound Runes" -- ones they are free to use in any spell as variables. The more you have, the more variables you can have in a program.

Community members could also create and share Unbound Runes. They could have silly textual names, like amulgagars-weird-rune-number-97, and players would select whichever Rune's image seems to match whatever semantic task the variable is intended to perform.

But for the record: Eventually, no matter what, I want user-defined variable Runes to be creatable while you're writing a spell. I need this for my own sanity.

Now that the basics of Rune creation and typesetting are done, I'm going to be working on the spell writing experience for the rest of the week -- you know, dragging Runes around on a canvas to construct spells. I'll also be exploring a solution to the user-defined variables problem. My general plan is to have a meta-Rune that, when clicked, gives you an interface for designing a new Rune (or picking from a list of previously designed ones). The new Rune would then be available in your program. For bonus points, I'll try to make it available in places where the variable would actually be in scope. We'll see!

But rest assured, I don't want to get blocked on a technical challenge like this one for too long (especially since there is a short-term solution). So if I can't get it quite right in the mean time or if I feel it's delaying other milestones, I'll table it for later and come back to it.

- Stephen R. Foster


P.S. Please consider supporting us on Patreon. We can't do this without you!