Skip to content

Commit 1948e30

Browse files
Merge pull request #41 from ClojureCivitas/macros-matter
add macros matter article
2 parents 887895d + 25a00d1 commit 1948e30

File tree

3 files changed

+171
-1
lines changed

3 files changed

+171
-1
lines changed

deps.edn

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
org.eclipse.elk/org.eclipse.elk.graph {:mvn/version "0.10.0"}
1919
org.eclipse.elk/org.eclipse.elk.graph.json {:mvn/version "0.10.0"}
2020
org.eclipse.elk/org.eclipse.elk.alg.common {:mvn/version "0.10.0"}
21-
org.eclipse.elk/org.eclipse.elk.alg.layered {:mvn/version "0.10.0"}}
21+
org.eclipse.elk/org.eclipse.elk.alg.layered {:mvn/version "0.10.0"}
22+
backtick/backtick {:mvn/version "0.3.5"}}
2223

2324
:aliases
2425
{;; Build the site with `clojure -M:clay -a [:markdown]`

src/games/macros_matter.clj

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
^{:kindly/hide-code true
2+
:clay {:title "Macros, Matter, & Malleability"
3+
:quarto {:author [:timothypratley]
4+
:description "How to add UI components and ClojureScript to your Clojure namespace"
5+
:category :clojure
6+
:type :post
7+
:date "2025-08-01"
8+
:tags [:game :browser]}}}
9+
(ns games.macros-matter
10+
(:require [scicloj.kindly.v4.kind :as kind]
11+
[backtick :as backtick]
12+
[civitas.explorer.geometry :as geometry]
13+
[civitas.db :as db]))
14+
15+
;; What if we embedded live UI and JavaScript logic directly into a Clojure namespace?
16+
;; Sounds wild? Let's see how.
17+
18+
;; [Clay](https://github.com/scicloj/clay)
19+
;; renders [Hiccup](https://github.com/weavejester/hiccup) views,
20+
;; [Scittle](https://github.com/babashka/scittle) executes ClojureScript in the browser,
21+
;; and [Reagent](https://reagent-project.github.io/) enables components as functions.
22+
;; `kind/hiccup` is how we annotate forms to be rendered as HTML via Clay.
23+
24+
;; Check out this mini-app for manipulating the Civitas logo.
25+
26+
(kind/hiccup
27+
[:div
28+
'(defonce state (atom {}))
29+
'(declare reset explode)
30+
[:div#app {:style {:width "100%"}}]
31+
['(fn []
32+
[:div
33+
[:em "Click and drag the hexagons"]
34+
[:div
35+
[:button {:on-click #'reset} "reset"]
36+
[:button {:style {:color "red"} :on-click #'explode} "explode"]]])]])
37+
38+
;; Fun right?
39+
40+
;; Notice that Clay accepts normal hiccup,
41+
;; but further treats forms as code, and function forms as components.
42+
;; We'll get to the implementations of `reset`, `explode`, and the view setup later.
43+
;; But first I want to introduce you to the killer feature for this kind of code; Macros!
44+
;; Not just any macro, but the ultimate macro, `sneeze`.
45+
46+
(defmacro sneeze
47+
"Generates Hiccup with a quasiquote-like templating syntax,
48+
enabling content insertion using unquote (~) and unquote-splicing (~@)."
49+
[& forms]
50+
`(kind/hiccup ~(backtick/quote-fn identity forms)))
51+
52+
;; This macro `sneeze` behaves just like `quote` but respects unquote and unquote-splicing.
53+
;; It also suggests that Clay treat the result as Hiccup,
54+
;; which further means that forms will be treated as Scittle.
55+
;; So it's a convenient way to write hiccup that contains code,
56+
;; without quoting, and being able to mix in Clojure values with `~` and `~@`.
57+
58+
(sneeze
59+
(defn my-component []
60+
[:strong ~(str "Wow, so random " (rand))])
61+
[:div [#'my-component]])
62+
63+
;; This is weird right, why do we need `~`?
64+
;; Well, when we define `my-component` we are writing symbolic code.
65+
;; This is going to be interpreted in the browser, not in Clojure.
66+
;; We can conveniently insert calculations made in Clojure into our UI code.
67+
;; It's like Scittle just inherited macros!
68+
;; That's exciting... in a way JavaScript has inherited macros.
69+
70+
;; Speaking of JavaScript, we need to bring in the MatterJS.
71+
;; MatterJS handles the physics simulation and rendering for our mini-app.
72+
73+
(kind/hiccup
74+
[:script {:src "https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"}])
75+
76+
;; O.K. so here's the code for the hexagons,
77+
;; notice toward the end the use of `~` to reuse Clojure code inside our Scittle code.
78+
79+
(sneeze
80+
;; Set up a MatterJS simulation to render to the `app` div.
81+
(def app (js/document.getElementById "app"))
82+
(def client-width (.-clientWidth app))
83+
(def engine (js/Matter.Engine.create (clj->js {:gravity {:y 0}})))
84+
(def world-width 600)
85+
(def w2 (/ world-width 2.0))
86+
(def render (js/Matter.Render.create (clj->js {:element app
87+
:engine engine
88+
:options {:wireframes false
89+
:background "rgba(0,0,128,0.05)"
90+
:hasBounds true
91+
:width client-width
92+
:height client-width}})))
93+
94+
;; Bounds create a fixed coordinate system, similar to a view-box in SVG.
95+
(set! (.. render -bounds -min -x) 0)
96+
(set! (.. render -bounds -min -y) 0)
97+
(set! (.. render -bounds -max -x) world-width)
98+
(set! (.. render -bounds -max -y) world-width)
99+
(def mouse (js/Matter.Mouse.create (.-canvas render)))
100+
(let [scale (/ world-width client-width)]
101+
(js/Matter.Mouse.setScale mouse (clj->js {:x scale
102+
:y scale})))
103+
(def mouse-constraint (js/Matter.MouseConstraint.create engine (clj->js {:mouse mouse})))
104+
(js/Matter.World.add (.-world engine) mouse-constraint)
105+
(js/Matter.Render.run render)
106+
(def runner (js/Matter.Runner.create))
107+
(js/Matter.Runner.run runner engine)
108+
109+
;; Handle when the browser window dimensions change
110+
(js/window.addEventListener
111+
"resize"
112+
(fn []
113+
(let [w (.-clientWidth app)]
114+
(-> render .-options .-width (set! w))
115+
(-> render .-options .-height (set! w))
116+
(-> render .-canvas .-width (set! w))
117+
(-> render .-canvas .-height (set! w))
118+
(let [scale (/ world-width w)]
119+
(js/Matter.Mouse.setScale mouse (clj->js {:x scale
120+
:y scale})))
121+
(js/Matter.Render.setPixelRatio render js/window.devicePixelRatio))))
122+
123+
;; Add some walls to keep everything inside the view
124+
(defn create-boundaries [width height thickness]
125+
(let [options (clj->js {:isStatic true
126+
:restitution 0.9
127+
:friction 0.1})
128+
x-mid (/ width 2.0)
129+
y-mid (/ height 2.0)
130+
t2 (/ thickness 2.0)]
131+
(for [[x y rw rh] [[(- 0 t2) y-mid thickness height]
132+
[(+ width t2) y-mid thickness height]
133+
[x-mid (- 0 t2) width thickness]
134+
[x-mid (+ height t2) width thickness]]]
135+
(js/Matter.Bodies.rectangle x y rw rh options))))
136+
(js/Matter.World.add (.-world engine)
137+
(clj->js (create-boundaries world-width world-width 500)))
138+
139+
;; The Civitas logo consists of hexagons
140+
(defn make-hexagon [x y radius color]
141+
(js/Matter.Bodies.polygon x y 6 radius
142+
(clj->js {:restitution 0.9
143+
:friction 0.1
144+
:render {:fillStyle color}})))
145+
;; Notice that we can make use of the colors from a different, Clojure namespace
146+
(let [hexagons (for [[x y c] ~(mapv conj (geometry/hex 100) db/get-colors)]
147+
(make-hexagon (+ w2 x) (+ w2 y) 50 c))]
148+
(js/Matter.World.add (.-world engine) (clj->js hexagons))
149+
(swap! state assoc :hexagons hexagons))
150+
151+
;; These functions are attached to the on-click of the buttons
152+
(defn reset []
153+
(doseq [[hex [x y]] (map vector (:hexagons @state) ~(vec (geometry/hex 100)))]
154+
(js/Matter.Body.setAngle hex 0)
155+
(js/Matter.Body.setPosition hex (clj->js {:x (+ w2 x), :y (+ w2 y)}))))
156+
(defn explode []
157+
(doseq [hex (:hexagons @state)]
158+
(js/Matter.Body.setVelocity hex (clj->js {:x (- (rand 50) 25),
159+
:y (- (rand 50) 25)})))))
160+
161+
;; I'll be the first to admit that this is a zany way to write code.
162+
;; But it's a lot of fun, powerful, concise, and best of all is there is no setup.
163+
;; Everything is here in this one Clojure namespace.
164+
;; No build steps.
165+
;; I send my namespace to Clay and get a HTML page with Scittle baked in.
166+
;; It blends macros, UI, and JavaScript into a single file.
167+
;; For learning, tinkering, and demos, it’s a joy.
168+
169+
;; ![Hexagons escaping](macros_matter.jpg)

src/games/macros_matter.jpg

2.89 KB
Loading

0 commit comments

Comments
 (0)