clerk 2024-12-13

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?

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 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

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 &quot;5dsq3A2Zfhjovuf5Cw4iyrGkKJN5UP&quot;, :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 &quot;5drT9JGE6qJ8mafafimFkbc2LneM6b&quot;}, :nextjournal/content-type &quot;image/png&quot;}">

@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! 😍

🙌 1

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!

would be interested in a small video of the final result if it’s not too much trouble

wait, it actually does work in the static build! that's a huge win as well

1

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