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 banner, created with Figlet.
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.
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.)
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:
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.
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:
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 :-)
- - -
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.