Making JavaScript Art (P. 1)

August 12, 2021 · app devtool

Or, in other words, turning code into pictures.

Link to the project: makejsart.pelmers.com

15-second Video demo

The land of JavaScript source code formatting is teeming with projects that promise an outcome, like beautifier and prettify. They'll all polish up the uneven spaces, missing semicolons, and inconsistent quote marks, like a make-up routine for your code. But how about I make a promise even more bold than theirs? Can I make your code into art?

Well maybe not... but at least we can try!

This post will share how I built makejsart.pelmers.com and document some of the interesting technical challenges along the way.

makejs.art_

see angular.js in its true form

The Goal

The problem I aim to solve is to format JavaScript code into the style of ascii art.

wikipedia example

Example of the effect

Approach

I realized that I could break the problem into two principal components.

  1. Format source code to an arbitrary shape (this post)
  2. Given a picture, decompose it into a collection of shapes (next post)

I figured that if I solve these two pieces, I'd just have to put them together to satisfy my goal. Easy enough!

Code Formatting

As I mentioned at the outset, JavaScript already has plenty of source code formatters out there. Because I've looked at the source code for one, I will confidently declare they all follow the same general recipe:

  1. Parse source code into an AST
  2. Print the AST back out with appropriate spaces between nodes

I'm all for being lazy where possible, so why not re-use this recipe for our needs? If we take an off-the-shelf JavaScript parser, then the problem of matching code to shapes boils down to just figuring out where to add whitespace.

// step 1 is a one-liner!
import { parse } from '@babel/parser';
const ast = parse(code);

Whitespace marker insertion

Step 1 was easy, but how about step 2? How do we know between which syntax tree nodes we can add or remove spaces? Let's consider how white space in JavaScript boils down to 3 main classes:

  1. optional spaces: there can be zero or more spaces, or new lines, between the two tokens. For example, these are all valid and the same:
log(        "hello")
log(

 "hello"
)
log("hello")
  1. required space: at least one space is required, but it can be a new line. Usually happens with keywords.
function hello() {} // valid
functionhello() {} // invalid

function
hello() {} // valid
  1. unbreakable space: at least one space is required, and it must not be a new line.
() => {
  return 1;
}
() => {
  return
  1;
} // different behavior, function will return 'undefined'

To implement this, I examined the internals of an existing AST-to-source transformation and made some surgical adjustments to its operation. Or we can call it what it is, which is monkey-patching. Instead of emitting plain spaces, I print out special "white space markers" that classify each space as one of these three classes. The marker can be any string that won't appear in the original code, such as a universal unique id. The relevant source code is in generator.ts.

A little bit of geometry

For now, if we limit the discussion to polygons, we can use the line-centric nature of code to define a shape as a function that maps line number to the pair (leading whitespace, desired width). For example, to make a rectangle, your shape function would return the same width on each line, in other words (0, constant). To create a triangle with base \(b\), height \(h\), and the pointy end on top, the shape to width function would be \(s(n) = (\frac{n}{h})(b)\).

Naturally, the follow-up is how do you find the base and height? For that, we need to make an assumption about the font used for display, specifically the font's ratio of line height to character width. Then we can compute the area of the code and map that to the shape's proportions to derive its height and width. If the font's ratio is \(height = width * 1.7\), then its area (technically in ch units) is just \(1.7 * code length\) (imagine the text all stretched out on one line).

Final step: code to shape!

Alright, now we have two ingredients: 1) the whitespace-marked source code, and 2) a formula to compute the width of each line. To put these together, all we need is a straight-forward algorithm:

split the code on whitespace markers into a list of alternating code and space items
while we still have code left, take the next item,
  if the item is unbreakable or it's code,
    add it to the current line
  if it's a space,
    find the next possible breakable point in the list
    if that point exceeds the current line's target width,
      create a new line
    otherwise,
      add the space to the current line

The runnable code is also well-commented (if you'll excuse a bit of self-praise): reshape.ts.

This process usually leaves each line with some room at the end, so finally a justification routine randomly intersperses spaces into the line until it reaches the target width and makes it all nice and tidy.

                     function  
                    wN(  Xo  ){ 
                  "use strict"  ;
                function io( nv){Xo
               .JY(nv, 'font-weight'
             ,'bold') ;   Xo . JY( nv,
            'font-weight'  );Xo .JY(nv, 
          'stroke-width','0px') ; if (Xo.
         mm ()  .property(nv,'type'  )  ==
        'ACTIVITY_SPAWN'){Xo.Rc(nv,  'fill',
      'rgb(161,217,155)');}}function Xg(nv,UW 
     ){ for (var pm=0;pm<UW .length;PN++ ){var 
   Se=UW[pm] ; if (Xo.mm().edge_property( nv ,Se,
  'type') === 'join'   ){Xo.lM(nv ,Se , 'cursor' ,
'pointer'  ); }    }  } Xo  .  Pk(    io,  Xg) ;   }

An example to tie everything togther

Pictures to shapes

You'll have to see part 2! That covers salient region detection in JavaScript and room for future improvements.

If you read this far and haven't tried it out, what are you waiting for???

Here, have a link: makejsart.pelmers.com. If you're looking for some sample code to feed in, try copy-pasting this React source code.

Previous: Metroid Prime React Components
Next: Making JavaScript Art (P. 2)
View Comments