avatar image - a green paisley swirl bits all the way down
January 5, 2024
By: Nundrum

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:

calendar widget detail showing the month as a ruler and the year as twelve chevrons in a stack

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:

preview of new tab page

Tags: svg interop clojurescript
« Going places I shouldn't with JNA Building A ClojureScript SVG Toolbox »

A blog by Nundrum

Links

  • @Nundrum on Mastodon
  • Archives
  • RSS
  • Clojurians Slack
  • Contacting Nundrum
  • P.E.P.R. Jacket Project

Recent Posts

  • Fountainvoid
  • Introducing XScreenBane
  • A Jacket, Microcontrollers, and Clojure

Tags

  • re-frame
  • clojure
  • svg
  • async
  • interop
  • jna
  • screensaver
  • platformio
  • font
  • pepr jacket project
  • cyberpunk
  • clojurescript

Copyright © 2025 Nundrum

Powered by Cryogen