4
\$\begingroup\$

I decided to write a Sierpinski Carpet generator. This has been something I wanted to try since watching the Coding Train this summer. A screenshot of it in action with some experimental coloring:

Example screenshot

This is my second version, since I decided to try rewriting it from scratch to see if I could come up with a better design. Instead of subdividing the tile into 9 then popping out the middle, I decided to just not generate the centre tile from the start. I also decided to hard-code splitting tiles in 3x3 divisions (although it's still adjustable). It didn't seem worth it to thread that information everywhere when anything other than a 3x3 division is basically the same thing, it just becomes more expensive sooner.

A few things that I'd appreciate comments on:

  • When working with Swing's style of painting, I'm having a hard time feeding the paint method the state to draw. I'm used to Quil's draw method that manages the animation state for you, and automatically passes the state everywhere so it can be used. The best way I've found is to store the state in an atom, and give the state atom to paint so it can deref it as needed. Given the paint method isn't fed any information by Swing/Seesaw, I don't understand how else I'm supposed to tell paint what to draw.

  • To take strain off the UI thread, I decided to do the calculations inside a Future. This turned my atom of tiles to draw into an atom of future tiles to draw. Inside the paint method, I'm checking both if the atom has been set yet, and if it has, if it's realized?. If it is, I deref it, and draw the tiles it gives me. The seems a little messy though. Any suggestions here would be appreciated.

I appreciate any and all input though! Don't feel restricted to doing massive reviews or focusing on the above points!

Note, I'm using the term "menger sponge" throughout the program because that's actually originally what I was going to do, then realized that Seesaw doesn't seem to support 3D drawing. A Sierpinski Carpet is basically a 2D Menger Sponge though, so I didn't think a full refactor of everything was necessary.


(ns sponge.menger.helpers
  (:import [java.awt Component]
           [javax.swing RootPaneContainer JButton]))

(defn component-size
  "Returns a pair of [width height] of the component"
  [^Component c]
  [(.getWidth c) (.getHeight c)])

(defn wrap
  "Wraps n so it's between min-n (inclusive) and max-n (exclusive)."
  [n min-n max-n]
  (let [limit (- max-n min-n)]
    (+ (mod n limit) min-n)))

(defn map-range
  "Maps a value in the inclusive range [start1 stop1] to a value in the range [start2 stop2].
  Stolen and translated from Processing."
  [value start1 stop1 start2 stop2]
  (+ start2
     (* (- stop2 start2)
        (/ (- value start1)
           (- stop1 start1)))))

(defn set-enter-default
  "Causes the given button to be fired when the Enter key is pressed."
  [^RootPaneContainer root, ^JButton target]
  (.setDefaultButton (.getRootPane root) target))

(ns sponge.menger.second-sponge)

(def subdivions 3)

(defrecord Tile [tl-x tl-y width])

(defn subdivide-tile-without-center
  "Divides a tile into subdivions many divisions per side, skipping the middle tile.
  A subdivions of 3 will split the tile into a grid of 8 tiles."
  [tile]
  (let [{:keys [tl-x tl-y width]} tile
        sub-width (double (/ width subdivions))

        ; Calculates the limits for a dimension based on the given starting coordinate
        dim-range #(range % (+ % (- width sub-width)) (dec sub-width))

        ; The middle square index. Assumes subdivions is odd.
        remove-i (int (/ subdivions 2))]

    (for [[yi y] (map-indexed vector (dim-range tl-y))
          [xi x] (map-indexed vector (dim-range tl-x))
          :when (not= xi yi remove-i)]

      (->Tile x y sub-width))))

(defn fully-subdivide
  "Divides the collection of tiles into sub-tiles"
  [tiles]
  (mapcat subdivide-tile-without-center tiles))

(defn fully-subdivide-n-iterations
  "Iterates fully-subdivide n-iterations many times."
  [tiles n-iterations]
  (nth (iterate fully-subdivide tiles)
       n-iterations))

(ns sponge.menger.first-try
  (:require [seesaw.core :as sc]
            [seesaw.graphics :as sg]
            [seesaw.color :as s-col]
            [seesaw.font :as sf]

            [sponge.menger.second-sponge :as p]
            [sponge.menger.helpers :as ph]))

(def screen-width 1500)

(def default-sponge-width 1e9)

(def control-font (sf/font :size 30))

(defn sponge->canvas
  "Returns a triplet of [mapped-x, mapped-y, mapped-width]"
  [canvas sponge-x sponge-y sponge-width]
  (let [[width height] (ph/component-size canvas)
        small-dim (min width height)
        map-r #(ph/map-range %, 0 default-sponge-width, 0 small-dim)]

    [(map-r sponge-x) (map-r sponge-y) (map-r sponge-width)]))

(defn test-color-f [x y width]
  (let [w #(ph/wrap % 1 256)
        i-width (/ 1 width)]
    (s-col/color (w (* x 5 i-width))
                 (w (* y 0.01 i-width))
                 (w (* x y i-width 0.001)))))

(defn paint
  "Partially accepts
    - An atom containing a future list of tiles to be drawn
    - A function that accepts the top-left x/y coordinates of a tile, and the tile's width,
      and returns a Color to draw the tile as.

   To be used as a :paint function."
  [future-tiles-atom color-f cvs g]
  (let [fut-tiles? @future-tiles-atom]
    (when (and fut-tiles? (realized? fut-tiles?))
      (doseq [{:keys [tl-x tl-y width]} @fut-tiles?
              :let [[sx sy sw] (sponge->canvas cvs tl-x tl-y width)
                    color (color-f tl-x tl-y width)]]
        (sg/draw g
          (sg/rect sx sy sw)
          (sg/style :background color))))))

(defn future-tiles
  "Returns a future that will evaluate to a sequence of tiles to be drawn.
  Repaints the canvas once done."
  [canvas n-iterations]
  (future
    (let [tiles (p/fully-subdivide-n-iterations
                  [(p/->Tile 0 0 default-sponge-width)]
                  n-iterations)]

      (doall tiles)

      (sc/invoke-later
        (sc/repaint! canvas))

      tiles)))

(defn new-canvas [tiles-atom]
  (sc/canvas :paint (partial paint tiles-atom test-color-f)))

(defn iterations-selector-controls
  "Returns controls that reset the tiles-atom with a new future when activated."
  [canvas tiles-atom]
  (let [selector (sc/spinner :font control-font,
                             :model (sc/spinner-model 1 :from 0, :to 20, :by 1))
        sub-button (sc/button :text "Generate", :font control-font, :id :sub-btn)]

     (sc/listen sub-button
       :action (fn [_]
                 (swap! tiles-atom
                        (fn [old-fut]
                          (when old-fut
                            (future-cancel old-fut))

                          (future-tiles canvas (sc/value selector))))))

     (sc/horizontal-panel :items [selector sub-button])))

(defn new-main-panel []
  (let [future-tiles-atom (atom nil)
        canvas (new-canvas future-tiles-atom)
        controls (iterations-selector-controls canvas future-tiles-atom)]

    (sc/border-panel :center canvas, :south controls)))

(defn frame []
  (let [content (new-main-panel)
        f (sc/frame :size [screen-width :by screen-width], :content content)
        sub-btn (sc/select f [:#sub-btn])]

    (ph/set-enter-default f sub-btn)

    f))

(defn -main []
  (-> (frame)
      (sc/show!)))

I tried to keep the centres to see what the coloring alone looked like. It actually looks pretty cool:

Cool color design

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.