More for the ClojureScript SVG Toolbox
》New Goals
The fool that I am decided to fork the startpage project from the previous post into two projects: one that’s static content suitable for sharing with the rest of the world, and one that’s more dynamic and relies on sources of data that are only available on my home network.
That meant really pinning down the features required for the simplified page before moving on to the fully-featured one.
The main features were limited by what could be served off of a local filesystem. That narrowed the options down to:
A calendar widget
Sets of page links
Configurable color
》Genesis of a Library
The calendar widget put what I had learned before to the test. The final result is made of a series of lines for a "time ruler," a triangle indicator, and a series of chevrons. The result looks like this:
The supporting functions to allow for that started to resemble the shape of a library, so of course the most painful way to proceed was refactoring those functions into a separate project, documenting it, and publishing it.
Thus cljs-polys-etc was born,
which wraps polybooljs
and adds some functions for trig and layout.
》Overcoming CORS
Reason: CORS request not HTTP
The hardest problem was loading the set of URLs from the filesystem. Turns out
CORS has evolved, and JS is not allowed to fetch file:
URLs anymore. A kind
soul from the Clojurians Slack recommened putting the data into a JS file and
loading that with the page, which would not have ever occured to my mind. The
data structure from the JS file becomes available to the CLJS code that
construts the page.
The first part is pretty simple - just include the config as another script
src
before the main app is loaded:
<script src="pages.js"></script>
Though I would have preferred edn
, the config isn’t that terrible:
var jspages = {
default: [
["github", "https://github.com/"],
["shadow-cljs",
"https://shadow-cljs.github.io/docs/UsersGuide.html"],
["clojuredocs", "https://clojuredocs.org/"],
["babashka", "https://book.babashka.org/"],
["mdn", "https://developer.mozilla.org/en-US/"]
],
}
With that done, accessing jspages
was easy. It is available as js/jspages
,
which gets transformed into a nice Clojure data structure and stuffed in an
atom:
(defonce pages (r/atom (js->clj js/jspages :keywordize-keys true)))
A much more puzzling task remained. Adding page parameters to select which set
of URLs/icons to display was the goal. It was easy to figure out that
js/window.location
holds the current URL, but harder to figure out how to
sanely transform that into something useful. The trick was using a js/URL
object on the location and then extracting the .-searchParams
into a
URLSearchParams
object. However, that can’t be translated into a Clojure
hashmap without first using Object.fromEntries
to make an object that
js→clj
can handle. Putting it all together is a nice one-liner, which is
expanded here for readability:
(-> js/window.location
js/URL.
.-searchParams
js/Object.fromEntries
(js->clj :keywordize-keys true))
》Technique
》General Approach: Define, Rotate, Translate
Returning to the toolbox, let us cover the steps used to construct and render
the calendar. The general approach for making any widget with cljs-polys-etc
is to construct shapes at the origin [0, 0]
, rotate them if necessary, and then
translate them into relative position. After the full set of shapes is in
place, the entire group can be translated into the final position and rendered
as SVG polygon
elements.
Creating a few boxes:
(let [box-template [[0 0] [10 0] [10 10] [0 10]]
box1 (-> box-template
(polys/translate-poly 30 30))
box2 (-> box-template
(polys/rotate-poly 30)
(polys/translate-poly 30 30))
box1path (polys/poly2path box1)
box2path (polys/poly2path box1)
]
[:polygon {:points box1path}]
[:polygon {:points box2path}])
》Handling Groups of Polygons
The ruler for the calendar is just a series of lines of differing lengths to indicate the day of the week, with even spacing between them. I created a function that turns information about the size and pattern into a vector of polys aligned on the origin.
(defn ruler-poly [max-height max-value spacing pattern-vec]
(for [[bar i] (map #(vector %1 %2) pattern-vec (range ))
:let [y (* i spacing)
x (* max-height (/ bar max-value))]]
[[0 y] [x y]]))
That returns a vector of vectors of vectors. Conceptually that is easier to think of as a vector of polys, with each poly being a vector of points.
》(into) Magic
The translate-polys
function applies translation to each poly in that vector.
A simple for
list comprehension turns each poly into a :polygon
.
The clever bit here is using (into [:<>])
to collect the new SVG elements into
a fragment! That saves the trouble of assigning a metadata key to each polygon, and
avoids all those warning messages from React saying "Warning: Each child in a
list should have a unique key prop".
(let [ruler-pattern [1 1 1 1 3 3 1 1 1 1 1 3 3 1 1 1 1 1]
ruler (ruler-poly 10 3 5 ruler-pattern)
ruler (polys/translate-polys 100 10)]
(into [:<>]
(for [poly ruler]
[:polygon {:points (poly/poly2path poly)}])))
》Color
Almost more of an aside at this point, but since I listed color config as a goal it deserves a mention. The colors are defined in CSS variables like this:
root{
--glow-color: #0F0;
}
Then they are easy to override in CLJS:
(js/document.body.style.setProperty "--glow-color" "#0FF")
》N3WT48
After half-jokingly soliciting ideas for a name, a friend suggested
the amazingly terrible N3WT48
- or NEWTAB
in leet speak and it
was just too bad to let go. Give it a shot.
Here’s a preview of what it looks like: