ASCII Art Animations in Lisp

Written on 2019-01-07

ASCII art may have fallen out of popular favour a couple of decades ago with the rise of “proper” computer graphics, but they are still fun to create. Having made a few myself, I always had the itch to not just create a static ASCII image, but to try my hand at an ASCII animation. Well, I finally did it. In this post I will show you how to create a very simple animation using Common Lisp and the classic Unix text-user-interface library, ncurses.

ASCII Art Animations

ASCII banner, created with Figlet.

Setting up

To follow along with this tutorial, you will need Linux and ncurses (if you have the former, you probably have the latter as well), a Common Lisp implementation such as SBCL and Quicklisp.

You will also need to grab a copy of croatoan, as this is the wrapper library we will use to call ncurses from Lisp. More specifically, you will need a development version of croatoan. I recently wrote an extension for the package that provides a basic shape-drawing API, which we will be using in the following. It has not yet been pushed to the Quicklisp repo, but you can get it by going to your $QUICKLISP_DIR/local-projects directory and doing git clone https://github.com/veddox/croatoan. ¹

You can find the complete code for this tutorial here.

To the drawing table

Our first block of code looks like this: ²

(ql:quickload :croatoan)
(in-package :croatoan)

(defun display-shape (shape &optional (squarify T))
    "Open a screen display and draw the shape."
    (with-screen (scr :input-blocking T :enable-colors T :input-echoing NIL
                     :cursor-visibility NIL :input-reading :unbuffered)
        (clear scr)
        (draw-shape shape scr squarify)
        (event-case (scr event)
            (otherwise (return-from event-case)))))

First of all, we load the croatoan package and change into it. The next function is one we don't actually need, strictly speaking, but that is useful for development. It takes a shape object (basically a list of character coordinates) and plots it on the current terminal screen. Let's have a closer look:

The macro with-screen is the entry point for croatoan. It hands control of the user's terminal window over to ncurses, changing it from line-mode into character-mode, and back again when the application terminates. We then clear the screen and draw the given shape. The event-case macro is the main program loop provided by croatoan that responds to key and even mouse clicks. We simply want to exit the shape display whenever any key is pressed, though, so we just use the catch-all otherwise clause. (See the croatoan documentation for more details.)

Note: The option squarify is necessary because terminal fonts are rectangular, not square – thus, you cannot simply treat a character-cell display as the geometric equivalent of a pixel screen. (If you plot a mathematically correct circle on a character-cell display, it will still look like an oval.) To correct this, draw-shape inserts a horizontal space after every character when squarify is true.

We can get a first taste of our shape drawing abilities by calling the following in our REPL (assuming you've typed in the above):

* (display-shape (circle 12 20 8 :filled T))

The circle function creates a shape object in the form of a circle, this is passed to our display-shape, and hey-presto, we have a neat circle drawn in our terminal window. Note that for some silly reason, ncurses coordinates are given in the reverse order to what we're used to. Thus, the 12 above is the Y/row coordinate, while the 20 is the X/column coordinate, counting from 0/0, the upper left hand corner. (8 is the circle radius.)

Reinventing the wheel

Our actual animation is going to feature a cart driving across the screen. To do this, we're first going to write a function to create a wheel shape, complete with spokes and all:

(defun wheel (y0 x0 radius &optional (rotation 0) (spokes 4))
    "Create a wheel shape with variable rotation and number of spokes"
    (let ((wheel (circle y0 x0 radius :char (make-instance 'complex-char :simple-char #\O))))
        (dotimes (s spokes wheel)
            (setf wheel (merge-shapes wheel
                            (angle-line y0 x0 (+ rotation (* s (/ 360 spokes))) radius))))))

A few things to point out here. All shape functions take a :char keyword option, which let's you specify how the shape is to be plotted. croatoan's complex-char class includes a character, a colour scheme and an optional effect (italics, bold, etc.). So our wheels will be drawn using a white-on-black (the default colour scheme) O character.

The function angle-line takes an origin, a bearing, and a distance; then returns the corresponding line shape. And lastly, merge-shapes does just what it says: it takes multiple shapes and merges them into a single new shape.

Let's test our wheel function using display-shape:

* (setf w1 (wheel 12 10 5 0 4))

* (setf w2 (wheel 12 30 8 0 8))

* (display-shape (merge-shapes w1 w2))

This should give us the following:

wheels

Building a cart

Obviously, a cart consists of more than just a wheel or two. Here's the rest:

(defun draw-cart (scr x)
    "Draw a cart on the screen at the given X coordinate"
    (clear scr)
    (let* ((h (.height scr)) (w (.width scr))
              (ground (line (1- h) 0 (1- h) (1- w)
                          :char (make-instance 'complex-char :simple-char #\i
                                    :color-pair '(:green :black))))
              (w1 (wheel (- h 8) (- x 12) 6 (* x 45)))
              (w2 (wheel (- h 8) (- x 36) 6 (+ 45 (* x 45))))
              (cart (quadrilateral (- h 16) x (- h 8) (- x 4)
                        (- h 8) (- x 46) (- h 16) (- x 46) :filled T
                        :char (make-instance 'complex-char :simple-char #\#
                                  :color-pair '(:red :black)))))
        (draw-shape ground scr)
        (draw-shape cart scr T)
        (draw-shape w1 scr T)
        (draw-shape w2 scr T)))

This function draws straight to the screen, so we cannot use it in conjunction with display-shape anymore. (We'll take care of that in the next section.) It uses the size information of the screen object it has been passed to calculate the position of the cart's components, then initializes these and finally draws them. New functions here are line and quadrilateral, which should be fairly self-explanatory, though.

Hitting the road

Finally, we are ready to animate our pretty ASCII art! Here's the code to do so:

(defun animate (&optional (fps 10))
    "Show the animation of the moving cart"
    (let ((running T) (current-x 0))
        (with-screen (scr :input-blocking (round (/ 1000 fps)) :enable-colors T
                         :input-echoing NIL :cursor-visibility NIL
                         :input-reading :unbuffered)
            (clear scr)
            (event-case (scr event)
                (#\space (setf running (not running)))
                ((NIL) (when running
                           (incf current-x)
                           (when (= current-x (+ 46 (round (/ (.width scr) 2))))
                               (setf current-x 0))
                           (draw-cart scr current-x)))
                (otherwise (return-from event-case))))))

What have we changed here, compared to display-shape? First of all, the :input-blocking option is now set to a number, instead of T. This has the effect of setting an “update frequency” for the screen. Now, if there is no user input to generate events, the (NIL) event will be generated instead at the specified frequency (in milliseconds). We use this to advance our animation by one frame, wrapping around when the cart drives off the screen. Secondly, instead of just terminating on any key press, we use the space bar to pause/unpause the animation.

And this is what our cart looks like in action:

We're on the road!

Wrapping it up

Of course, our little ASCII art animations won't make the next AAA game (mind the pun), but they could be used as the basis for a jump-and-run or somesuch. Not that you have to find a use for them. Some things are self-justifying, after all, for the simple reason that they are fun :-)

- - -

Updates

1) My shapes extension has been merged (with minimal changes) into the croatoan master branch and should be available in the February version of Quicklisp.

2) In the newest version of croatoan, :input-reading :unbuffered has been changed to :input-buffering nil. Also, draw-shape now takes the window object (scr in this example) as its first, not second argument.

Tagged as computers, lisp, tutorials


Unless otherwise credited all material Creative Commons License by Daniel Vedder.
Subscribe with RSS or Atom. Powered by c()λeslaw.