I'm trying to visualize and animate some of the Advent of Code solutions with Clerk. I'm using a slider control with ::clerk/sync and an atom that contains the pre-rendered frames and the current frame number to show. It looks like the following (see the attached clip).
Pre-rendering is actually a performance optimization to avoid waiting for recompute.
Question: how to make it even more streamlined, by avoiding to wait for all the queued recomputes to happen? 🙂
can you try to alter-var-root! clerk/recompute! to do nothing while a recomputation is in progress and see if that’s what you want?
something along
(in-ns 'nextjournal.clerk)
(def !recomputing? (atom false))
(defn recompute!
"Recomputes the currently visible doc, without parsing it."
[]
(if @!recomputing?
(println "Skipping recompute...")
(when-not (eval/cljs? @webserver/!doc)
(reset! !recomputing? true)
(binding [*ns* (:ns @webserver/!doc)]
(let [{:keys [result time-ms]} (eval/time-ms (eval/eval-analyzed-doc @webserver/!doc))]
(println (str "Clerk recomputed '" @!last-file "' in " time-ms "ms."))
(webserver/update-doc! result)
(reset! !recomputing? false))))))nice hack, thanks. it does help for the toy example, but there is other (unrelated) problem with the actual 130×130 map and over 6,000 frames — it is painfully slow, the web client needs to reconnect often, and there is this warning on the server: Fri Dec 13 15:22:54 CET 2024 [server-loop] WARN - Max payload length 4m, get: 103069089
I guess that has to do with the approach where I put both the current frame number and all pre-rendered frames in a single atom that is synced between server and the client. is there a way to pass the rendered frames once and only sync the frame number, according to the slider position?
here's the code I'm currently running: https://github.com/a1exsh/advent-of-clerk-2024/blob/5d081f91c2ffa15530830db05be1239c4ffb19e8/src/advent_of_clerk/day_06.clj#L131-L159
yes, you should be able to make a sync atom with just the frame number
and then just use that downstream
so the slider just modifies the frame number, and you keep the (nth rendered-frames frame-number) out of the client and keep it on the JVM
or you keep the slider purely in the browser
how do I refer to the rendered frames from the render-fn?
you don’t
you render in clojure
(clerk/html [:pre (nth rendered-frames @frame-number)])
that is too slow due to sync/recompute. the whole point of doing in the render-fn is to avoid that roundtrip
are you sure?
what’s taking to so long?
well, I can try again
(def frames-viewer
{:transform-fn (comp (clerk/update-val symbol)
clerk/mark-presented)
:render-fn '(fn [sym]
(let [atom* @(resolve sym)
{:keys [max-value frame-number]} @atom*]
[:input {:type :range
:value frame-number
:min 0
:max max-value
:on-change #(swap! atom*
assoc
:frame-number
(int (.. % -target -value)))}]))})
^::clerk/sync
(defonce frames*
(atom {:frame-number 0
:max-value (-> rendered-frames count dec)}))
#_(reset! frames*
{:frame-number 0
:max-value (-> rendered-frames count dec)})
(-> (clerk/with-viewer frames-viewer
`frames*)
(assoc :nextjournal/width :full))
(def rendered-frames
(mapv render-board guard-path-frames))
^{::clerk/width :full}
(clerk/html [:pre (get rendered-frames (:frame-number @frames*))])this isn’t so bad but also not great
(def frames-viewer
{:transform-fn clerk/mark-presented
:render-fn '(fn [frames]
(reagent.core/with-let [frame* (reagent.core/atom 0)]
[:div
[:input {:type :range
:value @frame*
:min 0
:max (dec (count frames))
:on-change #(reset! frame*
(int (.. % -target -value)))}]
[:pre (get frames @frame*)]]))})
(clerk/with-viewer frames-viewer
(mapv render-board guard-path-frames))this keeps it completely in the client and then it’s fast
that's what I'm looking for! ❤️
@mkvlr perfect, that's exactly what I needed 🙂
great to hear!
just one final thought, if my frame is not text but an instance of BufferedImage instead — how do I use it from within render-fn?
clerk’s image viewer should render that
there’s currently some known issues around the performance of presenting BufferedImages
> clerk’s image viewer should render that
I'm hopeful. how do I use it from within hiccup of the render-fn?
(def image-frames-viewer
{:render-fn '(fn [frames]
(prn :frames frames)
(reagent.core/with-let [frame* (reagent.core/atom 0)]
[:div
[:input {:type :range
:value @frame*
:min 0
:max (dec (count frames))
:on-change #(reset! frame*
(int (.. % -target -value)))}]
[:div.flex.flex-col.items-center.not-prose
[:img {:src #?(:clj (nextjournal.clerk.render/url-for (:nextjournal/value (get frames @frame*)))
:cljs blob-or-url)}]]]))})
(clerk/with-viewer image-frames-viewer
(mapv #(clerk/image (format "/Users/mk/Desktop/%d.png" %))
(range 1 5)))no need for clerk/image if you already have your BufferedImage in a vector
it seems I was close. it doesn't seem to fully work though
the tags comes out like this: <img src="/_blob/?value={:path [1 0], :nextjournal/value {:blob-id "5dsq3A2Zfhjovuf5Cw4iyrGkKJN5UP", :path [1 0]}, :nextjournal/width :wide, :nextjournal/viewer {:name nextjournal.clerk.viewer/image-viewer, :render-fn #nextjournal.clerk.viewer.ViewerFn{:form (fn [blob-or-url] [:div.flex.flex-col.items-center.not-prose [:img {:src (nextjournal.clerk.render/url-for blob-or-url)}]]), :f #object[Function]}, :hash "5drT9JGE6qJ8mafafimFkbc2LneM6b"}, :nextjournal/content-type "image/png"}">
@alex.shulgin I think you’re missing a (:nextjournal/value ,,,)
see my snippet above
🤦🏻 I think I made a map out of it instead of using it as a look up. let me try again
yep, it works — awesome! 😍
not sure this will keep working when publishing a static html doc though, I’d be surprised if it does
no, but it doesn't have to — I'm pretty satisfied with the result this way. thanks a ton for helping out!
my pleasure
would be interested in a small video of the final result if it’s not too much trouble
sure
wait, it actually does work in the static build! that's a huge win as well
wow that’s cool!
it is is still not as smooth as I would like it to be, but I think I could use canvas and try rendering in the browser
feels pretty smooth here
but canvas might be a better fit, yes