Skip to content

Latest commit

 

History

History
213 lines (176 loc) · 6.03 KB

svg.org

File metadata and controls

213 lines (176 loc) · 6.03 KB

SVG creation and macros!

In this chapter of Land of Lisp, we are going to build an SVG file writer…to demonstrate macros!

A helper function

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 #\>))

The tag macro

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)))

Dependencies

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.

SVG macro

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))

Playing around with colors

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))

Playing around with SVG styles

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.

Shapes

Of course, what kind of SVG would we have without shapes?

Circle

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))))

Polygons

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")

Metadata