(ns my.reagent-examples
  (:require
    [clojure.string :as string]
    [reagent.core :as reagent]
    [reagent.dom.server]
[reagent.ratom]))

(enable-console-print!)
(defn str->url [s t]
(let[blob (js/Blob. #js [s] #js {:type t})]
              (.createObjectURL js/URL blob) ))

(def worker-url (let[ gif-worker-src (.-textContent (. js/document (getElementById "gif-js-worker-code")))] 
(str->url gif-worker-src "application/javascript")))

(defn svgs->animated-gif-url![cb delays svgs]
  (let [delays (if (number? delays) (repeat delays) delays)
       [w h] ((comp (juxt :width :height) second first) svgs)
         gif (js/GIF. #js {:workers 4
                          :quality 1
                          :width w
                          :height h
                          :workerScript worker-url
                          })
        process (fn process[svgs delays](let[img (js/Image.)
                                             svg-url (str->url (reagent.dom.server/render-to-static-markup 
                                                                 (first svgs))
                                                               "image/svg+xml")]
                                          (do
                                            (set! (.-onload img)
                                                  (fn[](do
                                                         (.addFrame gif img #js{:copy true :delay (first delays)})
                                                         (let[r (rest svgs)]
                                                           (if (seq r)
                                                             (process r (rest delays))
                                                             (.render gif)
                                                             )))))
                                            (set! (.-src img) svg-url))))]
    (do
      (.on gif "finished" cb);; partial does not work ?!
      (process svgs delays))))

(defn save-svgs![filename delays svgs]
(letfn [(download-blob! [filename blob]
  (let[download-link (. js/document (createElement "a"))]
    (do
      (set! (.-download download-link) filename)
      (set! (.-href download-link) (.createObjectURL js/URL blob))
      (.click download-link))))]
(svgs->animated-gif-url! (fn[blob](download-blob! filename blob)) delays svgs)))

(defn display-svgs![parent delays svgs]
(svgs->animated-gif-url! (fn[blob]
                          (let[img (js/Image.)]
                            (do
                            (.appendChild parent img)
                            (set! (.-src img) (.createObjectURL js/URL blob)))))
                            delays svgs))

(defn svg-polyline[ps]
(let[[[x0 y0] & ps] ps
init-point (str "M " x0 ", " y0 " ")
seg (fn[[x y]] (str "L " x ", " y " "))]
(reduce #(str %1 (seg %2)) init-point ps)))
(defn draw-polylines[[w h] pss]
          [:svg {:xmlns "http://www.w3.org/2000/svg" :width w :height h}
[:rect {:x 0 :y 0 :width w :height h :fill "white"}]
          [:path {:stroke "black" :stroke-width 1 :fill "none" :d  (reduce str (map svg-polyline pss))}] ])
(defn add [[x0 y0][x1 y1]]
[(+ x0 x1)(+ y0 y1)])
(def make-polylines-transform (comp (partial partial mapv) (partial partial mapv) ))
(defn sin[x]
  (.sin js/Math x))
(defn cos[x]
          (.cos js/Math x))

(def PI
  (.-PI js/Math))
    (def sqrt #(.sqrt js/Math %))

    (defn rotate [a [x y]]
    (let [c (cos a)
          s (sin a)]
    [(- (* c x) (* s y)) (+ (* s x) (* c y))]))
(defn scale [k p]
(mapv (partial * k) p))
(def minus (partial scale -1.))
(def -INF (.-NEGATIVE_INFINITY js/Number))
(def INF (.-POSITIVE_INFINITY js/Number))

    (defn bounding-box[pss]
    (->> pss (reduce into [])(reduce (fn[[[x-min y-min][x-max y-max]][x y]] [[(min x-min x) (min y-min y)][(max x-max x)(max y-max y)]]) [[INF INF][-INF -INF]])))
      (defn make-fitting-transform[[w h] pss]
      (let[[[x-min y-min][x-max y-max]](bounding-box pss)
      s (min (/ w (- x-max x-min)) (/ h (- y-max y-min)))
      center (scale 0.5  (add [x-min y-min] [x-max y-max]))]
      (comp (partial add [(/ w 2) (/ h 2)]) (partial scale s) (partial add (minus center)))))
      (defn draw-fitted-polylines[wh pss]
      (draw-polylines wh ((make-polylines-transform (make-fitting-transform wh pss)) pss)))
(defn make-rotate-around [r a]
  (comp (partial add r)(partial rotate a) (partial add (minus r))))
(defn spirograph[rks]
  (fn[a]
    (into [[(- (reduce + (map first rks))) 0]]
          (first (reduce (fn[[res c][r k]]
                           (let[next-c (- c r)]
                             [(map (make-rotate-around [next-c 0] (* k a))
                                   (conj res [c 0])) next-c]))
                         ['() 0]
                         (reverse rks))))))
(defn spiro-1[n]
  (let[c (/ (condp = n
              10 15
              8 4
              9 10
              20 25
              15)
            200)
       a (/ (+ 1 c) 2)
       b-size (+ 1 (/ (sqrt 2) 2))
       c-size (/ (- 2 (sqrt 2)) 4)
       b-c-ratio (/ b-size c-size)
       b (/ (- 1 a) (+ 1. (/ 1 b-c-ratio)))]
    [[a 1][b (- n)][(/ b b-c-ratio) (* 4 n)]]))

(def spiro-2 (let[m (/ 1 (+ 3 (/ 1 3)))
                  s (/ m 6)][[(+ (* 2 m) s) 1][m -12][s (* 6 12)]]))
(def curves (mapv (fn[[rks n]] 
(mapv (comp (spirograph rks) (partial * PI (/ n 256))) (range 513)))
                  [[[[50 1][45 -4]] 1]
                   [[[50 1][45 -3.25]] 4]
                   [[[1 1][(/ 1. 2) -7]] 1]
                   [[[1  1][(/ 1. 2) 4]] 1]
                   [[[1  1][(/ 1. 2) 4][ (/ 1. 6) 16]] 1]
                   [[[1  1][(/ 1. 2) 8][ (/ 1. 6) 16]] 1]
                   [[[1  1][(/ 1. 2) 2][(/ 1. 4) 6][ (/ 1. 4) 5]] 1]
                   [(spiro-1 10) 1]
                   [spiro-2 1]]))

(def wh [120 120])
(def fitting-transforms (mapv (comp make-polylines-transform 
(partial make-fitting-transform wh) 
vector 
(partial mapv last)) curves)) 
(defn arm+curve[pps n] [(nth pps n) (mapv last (take n pps))])
(def spirograph-state (reagent.core/atom {:step 200}))

The nice thing about drawing is that one can easily draw nice patterns, using compositions of primitive patterns.

One of the most elementary "pattern" would be the circle, with the most basic Circular symmetry.

While a plain circle is not that pretty, just composing a few of them with spirographs can result in pretty drawings :



(def curves (mapv (fn[[rks n]] 
(mapv (comp (spirograph rks) (partial * PI (/ n 256))) (range 513)))
                  [[[[50 1][45 -4]] 1]
                   [[[50 1][45 -3.25]] 4]
                   [[[1 1][(/ 1. 2) -7]] 1]
                   [[[1  1][(/ 1. 2) 4]] 1]
                   [[[1  1][(/ 1. 2) 4][ (/ 1. 6) 16]] 1]
                   [[[1  1][(/ 1. 2) 8][ (/ 1. 6) 16]] 1]
                   [[[1  1][(/ 1. 2) 2][(/ 1. 4) 6][ (/ 1. 4) 5]] 1]
                   [(spiro-1 10) 1]
                   [spiro-2 1]]))

(def wh [120 120])
(def fitting-transforms (mapv (comp make-polylines-transform 
(partial make-fitting-transform wh) 
vector 
(partial mapv last)) curves)) 
(defn arm+curve[pps n] [(nth pps n) (mapv last (take n pps))])
(def spirograph-state (reagent.core/atom {:step 200}))


(dorun (map (fn[i](let[c (nth curves i)]
                    (display-svgs! js/klipse-container 200
                                   (map (comp (partial draw-polylines (map (partial * 1.2) wh)) 
                                              (nth fitting-transforms i) 
                                              (partial arm+curve c)
                                              (partial * 4))
                                        (range 128)))))
            (range (count curves))))

(defn gui-spiro1[]
  (let[step (:step @spirograph-state)]
    [:div 
     [:div [:input {:type "range" :value (:step @spirograph-state) :min 0  :max (* 1 520)  :style {:width "90%"}
                    :on-change (fn[e] (swap! spirograph-state assoc :step (int (js/parseFloat (.-target.value e)))))}]]
     (into [:div]
           (map (fn[i](let[c (nth curves i)]
                        [draw-polylines (map (partial * 1.2) wh) ((nth fitting-transforms i) 
(arm+curve c (:step @spirograph-state)))])) (range (count curves))))]))

Of course, some parameters make for prettier drawings :



(defn spiro-1[n]
  (let[c (/ (condp = n
              10 15
              8 4
              9 10
              20 25
              15)
            200)
       a (/ (+ 1 c) 2)
       b-size (+ 1 (/ (sqrt 2) 2))
       c-size (/ (- 2 (sqrt 2)) 4)
       b-c-ratio (/ b-size c-size)
       b (/ (- 1 a) (+ 1. (/ 1 b-c-ratio)))]
    [[a 1][b (- n)][(/ b b-c-ratio) (* 4 n)]]))

(def spiro-2 (let[m (/ 1 (+ 3 (/ 1 3)))
                  s (/ m 6)][[(+ (* 2 m) s) 1][m -12][s (* 6 12)]]))
[:div
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph (spiro-1 10)) (partial * PI (/ 1 256))) (range 513)))]]
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph spiro-2) (partial * PI  (/ 1 256))) (range 512)))]]]

So what is a spirograph ? We want to have a function \( \alpha \rightarrow [x y] \) and the function will be parametrized by a sequence of [radius angular-velocity].

It is easier to go backward, from the last radius to the first, each time composing rotations around the previous center.



(defn spirograph[rks]
  (fn[a]
    (into [[(- (reduce + (map first rks))) 0]]
          (first (reduce (fn[[res c][r k]]
                           (let[next-c (- c r)]
                             [(map (make-rotate-around [next-c 0] (* k a))
                                   (conj res [c 0])) next-c]))
                         ['() 0]
                         (reverse rks))))))
[:div
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph [[50 1][45 -4]]) (partial * PI (/ 1 256))) (range 512)))]]
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph [[50 1][45 -3.25]]) (partial * PI 4 (/ 1 256))) (range 512)))]]
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph [[1 1][(/ 1. 2) -7]]) (partial * PI  (/ 1 256))) (range 512)))]]
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph [[1  1][(/ 1. 2) 4]]) (partial * PI (/ 1. 256))) (range 512)))]]
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph [[1  1][(/ 1. 2) 4][ (/ 1. 6) 16]]) (partial * PI  (/ 1. 256))) (range 513)))]]
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph [[1  1][(/ 1. 2) 8][ (/ 1. 6) 16]]) (partial * PI  (/ 1. 256))) (range 513)))]]
 [draw-fitted-polylines [200 200] [(mapv last (map (comp (spirograph [[1  1][(/ 1. 2) 2][(/ 1. 4) 6][ (/ 1. 4) 5]]) (partial * PI  (/ 1. 256))) (range 513)))]]
 ]

How does one rotate a point around any center ? By composing translations and rotation around the origin.

As we will compose the resulting function, we might as well write a make-XXX that returns the rotating function curryfied / partially applied to the rotation center and the angle. Instead, we could define a function taking all three parameters (rotation center, angle, and point to be rotated) and partially apply it each time we want to compose it.



(defn make-rotate-around [r a]
  (comp (partial add r)(partial rotate a) (partial add (minus r))))
((make-rotate-around [1 0] (/ PI 2)) [2 0])

The rotation of a point around the origin is trivially defined.



(defn sin[x]
  (.sin js/Math x))
(defn cos[x]
          (.cos js/Math x))

(def PI
  (.-PI js/Math))
    (def sqrt #(.sqrt js/Math %))

    (defn rotate [a [x y]]
    (let [c (cos a)
          s (sin a)]
    [(- (* c x) (* s y)) (+ (* s x) (* c y))]))
(rotate (/ PI 4) [10 20])

As we want to be able to display drawing in canvas of a given size, we compose scaling and translating the polylines so that it fits the canvas.



(def -INF (.-NEGATIVE_INFINITY js/Number))
(def INF (.-POSITIVE_INFINITY js/Number))

    (defn bounding-box[pss]
    (->> pss (reduce into [])(reduce (fn[[[x-min y-min][x-max y-max]][x y]] [[(min x-min x) (min y-min y)][(max x-max x)(max y-max y)]]) [[INF INF][-INF -INF]])))
      (defn make-fitting-transform[[w h] pss]
      (let[[[x-min y-min][x-max y-max]](bounding-box pss)
      s (min (/ w (- x-max x-min)) (/ h (- y-max y-min)))
      center (scale 0.5  (add [x-min y-min] [x-max y-max]))]
      (comp (partial add [(/ w 2) (/ h 2)]) (partial scale s) (partial add (minus center)))))
      (defn draw-fitted-polylines[wh pss]
      (draw-polylines wh ((make-polylines-transform (make-fitting-transform wh pss)) pss)))
[draw-fitted-polylines [200 200] ((make-polylines-transform (comp (partial add [100 100]) (partial rotate (/ PI 4)))) [[[10 10][10 20][20 25]][[5 5][20 5][20 10]]])]

Scaling from the origin is trivially defined. We might as well define a unary minus while we are at it.



(defn scale [k p]
(mapv (partial * k) p))
(def minus (partial scale -1.))
(scale 2 [10 20])

Adding two vectors in the usual trivial way.



(defn add [[x0 y0][x1 y1]]
[(+ x0 x1)(+ y0 y1)])
(add [100 200] [10 20])

We are interested in transforming drawings. Drawings are sequences (vectors) of polylines, and polylines are vectors of points (and points are vectors of coords [x y] ).

So we can easily make a drawing-transforming function from a point-transforming function by composing sequence traversals and the given point-transforming function. We can even do it in a tacit way, which will be useful for other purposes (but for now, any "equivalent" definition would do).



(def make-polylines-transform (comp (partial partial mapv) (partial partial mapv) ))
[draw-polylines [400 400] ((make-polylines-transform (partial add [100 50])) [[[100 100][100 200][200 250]] [[50 50][200 50][200 100]]])]

Actually drawing (as in "displaying") the drawing is creating the svg fragment by concatenating the svg fragments for each polyline. TODO: check for empty drawing !



(defn draw-polylines[[w h] pss]
          [:svg {:xmlns "http://www.w3.org/2000/svg" :width w :height h}
[:rect {:x 0 :y 0 :width w :height h :fill "white"}]
          [:path {:stroke "black" :stroke-width 1 :fill "none" :d  (reduce str (map svg-polyline pss))}] ])
[draw-polylines [300 300] [[[100 100][100 200][200 250]]]]

The svg fragment for a polyline is just concatenating a Move to the first point and Line to each of the remain points.

TODO: check for empty polyline !



(defn svg-polyline[ps]
(let[[[x0 y0] & ps] ps
init-point (str "M " x0 ", " y0 " ")
seg (fn[[x y]] (str "L " x ", " y " "))]
(reduce #(str %1 (seg %2)) init-point ps)))
(svg-polyline [[10 10][10 20][20 20]])