I was recently doing a bunch of string formatting to produce a series of SVG Path commands that would draw an 'X' for a Tic-Tac-Toe game developed in ClojureScript.
(defn- make-x-mark [x y]
(let [path (apply str (compose-x-path x y))]
[:path {:class :mark-x, :d path}]))
The SVG path element allows you to specify a series of 'commands' to a 'pen' that can be moved to draw lines. Basically, it's a very flexible way to draw any shape. My goal was to define a single path element to draw the 'X' game symbol by doing the following (non-optimized steps):
- move the pen (without drawing) to what would be the center of the 'X'
- draw a line from the center to form the upper-left 'arm' of the 'X'
- move the pen (without drawing) back to the center (starting point)
- draw the upper-right 'arm'
- move the pen (without drawing) back to the center (starting point)
- draw the lower-left 'arm'
- move the pen (without drawing) back to the center (starting point)
- draw the lower-right 'arm'
The compose-x-path
function referenced above receives the [x, y]
center point and produces the sequence of 'move' (M
) and 'line' (L
) commands described above. Here's what we expect from that function (given an x
value of 1
and a y
value of 2
):
(it "composes the path of the rendered 'X' mark"
(should= ["M 1 2 ", "L 0.75 1.75 " ; center -> upper-left
"M 1 2 ", "L 1.25 1.75 " ; center -> upper-right
"M 1 2 ", "L 0.75 2.25 " ; center -> lower-left
"M 1 2 ", "L 1.25 2.25 "] ; center -> lower-right
(compose-x-path 1 2)))
Here's an early draft of the compose-x-path
function:
(defn compose-x-path [x y]
(let [len 0.25
center (string/format "M %f %f" x y)
to-upper-left (string/format "L %f %f" (- x len) (- y len))
to-upper-right (string/format "L %f %f" (+ x len) (- y len))
to-lower-left (string/format "L %f %f" (- x len) (+ y len))
to-lower-right (string/format "L %f %f" (+ x len) (+ y len))]
[center to-upper-left
center to-upper-right
center to-lower-left
center to-lower-right]))
Aside: The familiar format
function we normally use from clojure.core
isn't available in ClojureScript except through the 'Google Closure Library' (goog.string.format
). It is generally 'required' and then used as follows:
(ns rostering.components.services
(:require
[goog.string :as gstring]
[goog.string.format]))
(string/format "%s" "Hello")
Ok, back to the compose-x-path
function. Notice the duplication of the format strings and also how the string/format
calls clutter up the essence of the function because they are at a slightly lower level of abstraction. This is where the partial
function comes in very handy!
(def move (partial string/format "M %f %f "))
(def line (partial string/format "L %f %f "))
The partial
function:
Takes a function f and fewer than the normal arguments to f, and returns a fn that takes a variable number of additional args. When called, the returned function calls f with args + additional args.
So, it's like we are freezing a 'partial' call to a function for use later!
(defn compose-x-path [x y]
(let [len 0.25
center (move x y)
to-upper-left (line (- x len) (- y len))
to-upper-right (line (+ x len) (- y len))
to-lower-left (line (- x len) (+ y len))
to-lower-right (line (+ x len) (+ y len))]
[center to-upper-left
center to-upper-right
center to-lower-left
center to-lower-right]))
Much better!
Except, you may have thought of a more efficient way to draw the same shape (as I just have). The line
command I've been using draws from the where the 'pen' currently is to a specified point. What if we just drew two long lines that intersected to make the X? We can do that by using a 'line' command that receives both the starting and the stopping points.
(it "composes the path of the rendered 'X' mark"
(should= ["M 0.75 1.75 " ; move upper-left
"L 1.25 2.25 " ; line lower-right
"M 1.25 1.75 " ; move upper-right
"L 0.75 2.25 "] ; line lower-left
(compose-x-path 1 2)))
...
(def move (partial string/format "M %f %f "))
(def line (partial string/format "L %f %f "))
(defn compose-x-path [x y]
(let [len 0.25]
[(move (- x len) (- y len)) ; move upper-left
(line (+ x len) (+ y len)) ; line lower-right
(move (+ x len) (- y len)) ; move upper-right
(line (- x len) (+ y len))])) ; line lower-left
Even better! Hey, thanks for being my 'rubber duck' on that one.