In this chapter of Land of Lisp, we are going to build an SVG file writer…to demonstrate macros!
It is generally good practice to keep your macros as simple as you can for their given functionality, so something that is generally a good idea is to move everything that can be put into a function, into a function!
For our SVG writer, since SVG is an XML format, we will need a means to create arbitrary XML tags, we can do so with the following:
(defun print-tag (name alist closingp)
(princ #\<)
(when closingp
(princ #\/))
(princ (string-downcase name))
(mapc (lambda (att)
(format t " ~a=\"~a\"" (string-downcase (car att)) (cdr att)))
alist)
(princ #\>))
Now that we have our helper function out of the way, lets write a macro!
(defmacro tag (name atts &body body)
`(progn (print-tag ',name
(list ,@(mapcar (lambda (x)
`(cons ',(car x) ,(cdr x)))
(pairs atts)))
nil)
,@body
(print-tag ',name nil t)))
This macro makes use of a function we defined a chapter before:
pairs
…
(defun pairs (lst)
(labels ((f (lst acc)
(split lst
(if tail
(f (cdr tail) (cons (cons head (car tail)) acc))
(reverse acc))
(reverse acc))))
(f lst nil)))
…which in turn, uses a macro we made earlier called split
:
(defmacro split (val yes no)
(let1 g (gensym)
`(let1 ,g ,val
(if ,g
(let ((head (car ,g))
(tail (cdr ,g)))
,yes)
,no))))
…which in turn uses let1
:
(defmacro let1 (var val &body body)
`(let ((,var ,val))
,@body))
With that all out of the way, lets start making some actually-SVG related code.
The following is a macro that will let us create our SVG output:
(defmacro svg (&body body)
`(tag svg (xmlns "http://www.w3.org/2000/svg"
"xmlns:xlink" "http://www.w3.org/1999/xlink")
,@body))
When working with an image format, it is nice to have a way to play around with color values in a noticeable manner.
In our case, we are going to write a brightness
function that will
take a color (RGB as a three-value list) and generate a darker or
lighter version of it:
(defun brightness (col amt)
(mapcar (lambda (x)
(min 255 (max 0 (+ x amt))))
col))
Now, if we are going to be generating SVG, we should also have a way to set the style of an SVG element:
(defun svg-style (color)
(format nil
"~{fill:rgb(~a, ~a, ~a); stroke:rgb(~a, ~a, ~a)~}"
(append color (brightness color -100))))
This will style an SVG element with the color we pass in, and make the stroke color for it a darker version of that color.
Of course, what kind of SVG would we have without shapes?
We can define a circle easily using our existing tag
macro like so:
(defun circle (center radius color)
(tag circle (cx (car center)
cy (cdr center)
r radius
style (svg-style color))))
SVG isn’t all about circles, we need to be able to draw polygons too~!
The following is the suggested implementation for implementing these:
(defun polygon (points color)
(tag polygon (points (format nil
"~{~a, ~a ~}"
(mapcan (lambda (tp)
(list (car tp) (cdr tp)))
points))
style (svg-style color))))
We won’t be generating the points that this function accepts by hand, but rather generate them using a “random walk”.
Below is the definition for such a function:
(defun random-walk (value length)
(unless (zerop length)
(cons value
(random-walk (if (zerop (random 2))
(1- value)
(1+ value))
(1- length)))))
Now lets generate a whole bunch of random walks and output them to a file:
(with-open-file (*standard-output* "random_walk.svg"
:direction :output
:if-exists :supersede)
(svg (loop repeat 10
do (polygon (append '((0 . 200))
(loop for x from 0
for y in (random-walk 100 400)
collect (cons x y))
'((400 . 200)))
(loop repeat 3
collect (random 256)))))
"random_walk.svg")