Radish Logo

radish-logo

A small literate program that generates the Radish Logo. I wanted to add a bit of design to the radish lib readme, and I figured I would tackle two projects in one:

  • create a logo (editable for improvements later)
  • write an org file with external deps to show the 'advanced-build' feature

This project is successfully built from a single .org file where dependencies are automatically detected and used to generate a shadow-cljs project which is finally compiled and packaged into a post-ready directory containing all resources necessary to have an interactive site.

I'm quite pleased with the result!

deps

{:deps
  {io.github.adam-james-v/svg-clj {:mvn/version "0.0.3-SNAPSHOT"}
   hiccup/hiccup                  {:mvn/version "2.0.0-alpha2"}}}

ns

(ns radish-logo.draw
 (:require  [svg-clj.utils :as utils]
  [svg-clj.elements :as el]
  [svg-clj.transforms :as tf]
  [svg-clj.composites :as comp :refer [svg]]
  [svg-clj.path :as path]
  [svg-clj.parametric :as p]
  [svg-clj.layout :as lo]))

utils

As I was writing this project, a few utility functions became necessary. In general, I don't overthink document structure while I am writing out my main program. Instead, I write notes and code as ideas and solutions pop into my brain. As intent and methodology is discovered through iteration, I can then more confidently structure the document. This means both re-arranging code blocks AND adding or removing prose to clarify intent.

This is, in short, a living document. At least up until publish time.

svg

Make an SVG function. It turns out that you can natively display SVG in emacs. This lets us export SVG to a file and immediately display it as a code block result. The svg! fn helps with this.

This is a side-effecting function that writes a file to the same directory as this org file. If you want to control where images are saved, you can change this function and/or the fn calls to save into a directory structure of your choosing.

The file name is returned as a symbol so that org-mode correctly inserts an inline image link to that filename. If you return it as a string, org-mode incorrectly has double quotes around the filename, resulting in no display of the image.

#_(defn svg!
  [svg-data fname]
  (let [svg-data (if (= (first svg-data) :svg)
                   svg-data
                   (svg svg-data))]
    (spit fname (html svg-data))
    (symbol fname)))

flip-y

A helper fn that multiplies every pt by [1 -1] to mirror along the x axis.

(defn flip-y
  [pts]
  (mapv #(utils/v* % [1 -1]) pts))

(defn flip-x
  [pts]
  (mapv #(utils/v* % [-1 1]) pts))

drawing

This whole project is just one drawing, but when I'm doing things programmatically, I like to break the drawing down into smaller bits. It's a subjective process where I kind of follow intuition and iteration to figure out which 'bits' make sense to turn into functions. For a drawing like this, it's fairly obvious to me right away that I'll need a 'bulb' and a 'leaf'. Then, I can style and transform the basic shapes to compose a final logo.

Bulb

Start the drawing off by creating a function that combines two bezier curves to create a bulb shape.

(defn bulb
  [cpts]
  (let [beza (apply path/bezier cpts)
        bezb (apply path/bezier (flip-y cpts))
        lt (path/line (first cpts) (first (flip-y cpts)))
        lb (path/line (last cpts) (last (flip-y cpts)))
        shape (tf/merge-paths beza bezb)
        ctr (tf/centroid shape)]
    (-> shape
        (tf/rotate 270)
        (tf/translate (utils/v* ctr [-1 -1])))))

(svg (-> (bulb [[0 0] [55 80] [92 55] [104 0]])
         (tf/style {:fill "slategray"})))

Play around with the control points to see the bulb change its shape. Fun stuff, I say!

Leaf

A leaf function seems useful, but may be hard to parameterize fully. To keep it simple, I'm going to provide a single 'height' param and just make a leaf proportional to the h value.

Notably, my svg library doesn't have a mirror transform function yet, so I have to incorporate the logic of mirroring the shape myself. It's not the most elegant solution, but it works, and shows how one might use a library while still building their own functions to solve unique problems unanticipated by library authors.

(defn leaf
  [h & {:keys [mirror] :or {mirror false}}]
  (let [m (if mirror -1 1)
        main-pts [[0 0] [(* 0.125 h m) (* 0.275 h)] [0 h]]
        main (apply path/bezier main-pts)
        swoop-pts [[0 (* 0.125 h)]         [(* 0.03 h m) (* -0.15 h)]
                   [(* 0.2125 h m) (* -0.175 h)] [(* 0.3 h m) 0]]
        swoop (-> (apply path/bezier swoop-pts)
                  (tf/translate (utils/v* [(* -1) -1] (last swoop-pts))))]
    (tf/merge-paths
     main
     swoop
     (-> swoop (tf/rotate (* 270 m)) (tf/translate [(* -0.1375 h m) (* 0.315 h)]))
     (-> (apply path/bezier (drop-last swoop-pts))
         (tf/rotate (* 315 m)) (tf/translate [(* -0.175 h m) (* 0.515 h)]))
     (-> (apply path/bezier (drop-last swoop-pts))
         (tf/rotate (* 330 m)) (tf/translate [(* -0.1 h m) (* 0.825 h)])))))

(svg (-> (leaf 200)
         (tf/style {:fill "limegreen"})))

Linear Gradient

SVG is pretty great, but I don't completely love how things like patterns and gradients are defined. You have to build the structure into a tag within an SVG element. You define unique IDs for each gradient or pattern and can then use them as fills wit url(#id) . But that process isn't well handled yet by my svg-clj library, as it requires the ability to hold onto defs globally and 'inject' them into the svg at the end. There's clearly a few ways to handle this, but I don't have an ideal approach yet.

As such, I have a somewhat hacky approach in this post, but it gets the job done (for now).

(defn linear-gradient
  [deg col-a col-b]
  (let [[x1 y1] (utils/rotate-pt-around-center [0 50] deg [50 50])
        [x2 y2] (utils/rotate-pt-around-center [100 50] deg [50 50])
        id (gensym "gradient-")]
    [:linearGradient {:id id
                      :x1 (str x1 "%")
                      :y1 (str y1 "%")
                      :x2 (str x2 "%")
                      :y2 (str y2 "%")}
     [:stop {:offset "0%" :stop-color col-a}]
     [:stop {:offset "100%" :stop-color col-b}]]))

Compose and Style

Using the bulb, leaf, and linear-gradient functions, I can now add a bit of polish.

I'm creating the SVG structures without wrapping them in SVG tags so that I can incorporate things into one final SVG tag later.

(def radish-bulb
  (let [gradient (linear-gradient 230 "rgb(244,131,120)" "rgb(235,120,196)")
        gradient-id (get-in gradient [1 :id])
        shadows (tf/merge-paths
                 (-> (path/line [0 0] [10 10]) (tf/translate [12 30]))
                 (-> (path/line [0 0] [6 6]) (tf/translate [28 26]))
                 (-> (path/line [0 0] [6 6]) (tf/translate [7 45]))
                 (-> (path/line [0 0] [3 3]) (tf/translate [-31 -21])))
        roots (tf/merge-paths
               (path/bezier [0 61] [-8 71] [0 84]))]
    (-> (bulb [[0 0] [45 75] [90 50] [102 0]])
        (tf/merge-paths shadows roots)
        (tf/style {:fill "none"
                   :stroke-width 7
                   :stroke-linecap "round"
                   :stroke (str "url(#" gradient-id ")")})
        (->> (list [:defs gradient])))))

(svg radish-bulb)

(def radish-leaves
  (let [gradient (linear-gradient 103 "rgb(120,202,106)" "rgb(182,192,174)")
        gradient-id (get-in gradient [1 :id])]
    (->
     (tf/merge-paths
      (-> (leaf 80 :mirror true) (tf/rotate 3) (tf/translate [-2 -145]))
      (-> (leaf 100 :mirror true) (tf/rotate 12) (tf/translate [14 -97]))
      (-> (leaf 160 :mirror false) (tf/rotate -11) (tf/translate [-20 -158])))
     (tf/style {:fill "none"
                :stroke-width 6
                :stroke-linecap "round"
                :stroke (str "url(#" gradient-id ")")})
     (->> (list [:defs gradient])))))

(svg radish-leaves)

Final Result

Here's the final result, defined as rad , which makes me chuckle.

(def rad
  (let [[[_ leaves-grad] leaves] radish-leaves
        [[_ bulb-grad] bulb] radish-bulb]
    (-> (el/g
         (-> (el/rect 500 500)
               (tf/style {:fill "lavender"}))
         leaves
         (-> bulb (tf/translate [0 38]) (tf/style {:fill "rgba(244,131,120,0.2)"})))
        (tf/translate [200 220])
        (->> (list [:defs leaves-grad bulb-grad]))
        (svg 400 400)
        #_(svg! "radish.svg"))))

rad

Thoughts

I'm very pleased with the result of the radish advanced-build! function so far. There are a few improvements to be made with the library itself, and I want to figure out a way to incorporate the shadow-cljs compilation directly, instead of the (at time of writing this) 'solution' of shelling out and spawning another clojure process. This does give me the output I want, though, so it's not a loss, just a step in the right direction!

Thanks for reading.