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 asrad
, 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"))))
radThoughts
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.