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