Symbolic mathematics in, mathematical animation out. Emmy computes, LaTeX carries, and a backend-neutral scene facade renders — to Manim video, or to pure data a browser can play.
Every scene on this page was recorded by desargues when the page was built.
(defn construct [stage]
(let [circles (mapv (fn [[x c]] (-> (s/circle :radius 1.0 :color c)
(s/move-to [x 0])
(s/fill! c :opacity 0.25)))
[[-3.6 :teal] [0 :gold] [3.6 :green]])
nums (mapv #(s/next-to (s/decimal 0) % :down) circles)]
(s/play! stage (s/stagger (map s/draw circles) :lag-ratio 0.3))
(s/play! stage (s/together
(map-indexed #(s/count-to %2 (* 100 (inc %1))) nums)))
(s/play! stage (s/together (map #(s/glide % [0 0]) circles)))
(s/hold! stage 1)));; Manim: writes scene.mp4 (the lazy default backend)
(s/render! "scene" construct)
;; Pure data, no Python: the scene on the previous slide
(s/with-backend (rec/recording-backend)
(s/render! "scene" construct))
;; => {:scene "scene" :nodes {1 {:node :circle ...} ...}
;; :steps [{:step :play :anims [...]} ... {:step :hold :seconds 1}]}The facade is desargues.scene; the seam is desargues.scene.protocols. Swap the backend, keep the scene.
(v/render-layout! "balance"
(v/column {:padding 0.8 :spacing 0.5 :align :center-x}
(v/text "Layout algebra" {:font-size 44 :color :gold})
(v/row {:width :fill :spacing 0.6}
(v/box {:width [:portion 1] :border :white}
(v/text "Bank A")
(v/text "ΔM = 1000" {:color :teal}))
(v/box {:width [:portion 1] :border [:teal 3]}
(v/text "Reserves")
(v/text "r · D" {:color :gold})))))column / row / box / text / math / image with :fill, [:portion n] and padding: measure → resolve → realize, on either backend.
A git coordinate. The pure half runs on the JVM alone.
;; deps.edn
{:deps {io.github.mentat-collective/desargues
{:git/tag "v0.1.1" :git/sha "6d26609"}}}Or from a clone: git clone https://github.com/mentat-collective/desargues && cd desargues && bb doctor
One conda env. desargues derives every path from CONDA_PREFIX; nothing is hardcoded.
conda create -n manim -c conda-forge python=3.12 manim
conda activate manim # sets CONDA_PREFIX; desargues.config picks it up
clojure -M:doctor # JVM ok? Manim env ok? LaTeX? ffmpeg?(require '[desargues.scene :as s]
'[desargues.scene.data :as rec]
'[desargues.layout.core :as l])
(rec/layout->scene-graph "hello"
(l/column {:padding 0.6 :spacing 0.4 :align :center-x}
(l/text "Reserves")
(l/math "\\Delta M = 1000"))
{})
;; => {:scene "hello" :nodes {1 {:node :text ...} 2 {:node :math ...}}
;; :steps [{:step :play ...} {:step :hold :seconds 3}] ...}Collect the symbolic facts, promote them to LaTeX. This line was computed by the build:
\(\frac{d}{dx}\,\mathsf{exp}\left(\sin\left(x\right)\right) = \mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right)\)
(pipe/derivative-spec (fn [x] (e/exp (e/sin x))))
;; => {:expr (exp (sin x))
;; :derivative-expr (* (cos x) (exp (sin x)))
;; :func-latex "e^{\\sin\\left(x\\right)}"
;; :deriv-latex "\\cos\\left(x\\right)\\,e^{\\sin\\left(x\\right)}"}Four derivatives of \(e^{\sin x}\), each computed and simplified by Emmy when this page was built:
\(f(x) = \mathsf{exp}\left(\sin\left(x\right)\right)\)
\(f'(x) = \mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right)\)
\(f''(x) = \mathsf{exp}\left(\sin\left(x\right)\right)\,{\cos}^{2}\left(x\right) - \sin\left(x\right)\,\mathsf{exp}\left(\sin\left(x\right)\right)\)
\(f'''(x) = - {\sin}^{2}\left(x\right)\,\mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right) - 3\,\sin\left(x\right)\,\mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right)\)
Emmy's taylor-series is a lazy stream of terms; the deck takes the first five:
\(e^{\sin x} = 1 + x + \frac{1}{2}\,{x}^{2} - \frac{1}{8}\,{x}^{4} + \cdots\)
(def f (fn [x] (e/exp (e/sin x))))
(mapv #(e/->TeX (e/simplify ((nth (iterate e/D f) %) 'x))) (range 4))
;; f, f', f'', f''' as LaTeX, ready for KaTeX or for Manim's MathTex
(take 5 ((e/taylor-series f 0) 'x))
;; => (1 x (* 1/2 (expt x 2)) 0 (* -1/8 (expt x 4)))TrajectorySolver is a protocol. Clojure RK4 ships; the :dynamics alias adds raster's Tsit5 and DP5, resampled onto the frame grid.
(require '[desargues.videos.physics :as phys]
'[desargues.infrastructure.raster-adapter :as ra])
(def pendulum (phys/make-pendulum {:length 3.0 :damping 0.1}))
(phys/evolve pendulum {:dt 0.01 :duration 10.0}) ; RK4 in Clojure
(phys/evolve pendulum {:dt 0.01 :duration 10.0 :solver (ra/raster-solver :tsit5)}) ; adaptive, resampled onto the dt gridWrite the bob positions as functions of the coordinates; Emmy differentiates the Lagrangian for you. The scene you just saw is this system, integrated.
(require '[desargues.videos.lagrangian :as lag])
;; Cartesian positions of the two bobs as functions of q = [th1 ph1 th2 ph2]
(def sys (lag/spherical-double-pendulum {:l1 1.4 :l2 1.2 :theta1 1.2 :theta2 2.2}))
;; L = T - V is built from those positions; Emmy's
;; Lagrangian->state-derivative turns it into q'' and the compiler makes
;; it fast. Then it is just another PhysicalSystem:
(phys/evolve sys {:dt 0.01 :duration 8.0})
(lag/positions-at sys (:state (last *1))) ; => [[x1 y1 z1] [x2 y2 z2]]bb bench # clojure -M:bench:dynamics
bb bench --manim # adds a Manim render per quality (needs the env)Writes bench/results/latest.edn and regenerates these charts under doc/bench/.
High-level facade: expr, func, derivative, animate-derivative, render-layout!
Backend-neutral animation DSL — the DIP seam (desargues.scene.protocols)
Two backends: libpython-clj → Manim, or an EDN recorder
elm-ui layout algebra with a pure extent estimator
Pure Clojure + Emmy, Typed-Clojure annotated
Everything about the environment, resolved once at the boundary
EPL-2.0 or GPL-2.0-or-later with Classpath exception.