Sunday, June 5, 2022

Drawing basic shapes with Clojure

I will conclude the current series of posts about doing graphics programming in Clojure using Java 2D by making a compilation of all the basic shapes, but I expect I will be talking a lot about computer graphics topics a lot from now on.

An image creating macro

I will now be creating a wide variety of images all at once. That would yield too much boilerplate if I didn't first declare a macro.
(defmacro argb-image
  [[width height] & body]

  (let [image-name (gensym "img")
        graphics-name (gensym "graphics")]
    `(let [~image-name (BufferedImage. ~width ~height BufferedImage/TYPE_INT_ARGB)
           ~graphics-name (.createGraphics ~image-name)]
       (doto ~graphics-name
         ~@body)
       ~image-name)))

Most of what we want to do to images can be encapsulated in a single doto block applied to its graphics. This macro encapsulates all that.

Creating the images

I ported the basic regular polygon creation function from the Java we saw previously to Clojure.
(defn create-polygon
  [vertices offset rect]

  (let [step (/ (* 2 Math/PI)
                vertices)
        x (int-array vertices)
        y (int-array vertices)
        xradius (/ (.-width rect) 2)
        yradius (/ (.-height rect) 2)]
    (dotimes [i vertices]
      (let [current-x (+ (.-x rect) xradius (int (* (Math/cos (+ offset (* i step))) xradius)))
            current-y (+ (.-y rect) yradius (int (* (Math/sin (+ offset (* i step))) yradius)))]
        (aset x i current-x)
        (aset y i current-y)))
    (Polygon. x y vertices)))

This enables us to now create our array of basic images created with Clojure.
(def line-image
  (argb-image
    [100 100]
    (.setColor Color/BLACK)
    (.drawLine 0 0 100 100)))

(def square-image
  (argb-image
    [100 100]
    (.setColor Color/RED)
    (.fill (new Rectangle 10 10 80 80))))

(def circle-image
  (argb-image
    [100 100]
    (.setColor Color/BLUE)
    (.fill (new Ellipse2D$Double 0 0 100 100))))

(def rectangle-image
  (argb-image
    [100 100]
    (.setColor Color/ORANGE)
    (.fill (new Rectangle 10 30 80 40))))

(def right-triangle-image
  (let [p (new GeneralPath)]
    (.moveTo p 0.0 0.0)
    (.lineTo p 100.0 100.0)
    (.lineTo p 0.0 100.0)
    (.lineTo p 0.0 0.0)
    (.closePath p)

    (argb-image
     [100 100]
     (.setColor Color/GREEN)
     (.fill p))))

(def trapezoid-image
  (let [p (new GeneralPath)]
    (.moveTo p 0.0 100.0)
    (.lineTo p 100.0 100.0)
    (.lineTo p 70.0 0.0)
    (.lineTo p 30.0 0.0)
    (.lineTo p 0.0 100.0)

    (argb-image
      [100 100]
      (.setColor Color/CYAN)
      (.fill p))))

(def ellipse-image
  (argb-image
    [200 100]
    (.setColor (new Color 0xBCD4E6))
    (.fill (new Ellipse2D$Double 0 0 200 100))))

(def ring-image
  (let [core-circle (new Ellipse2D$Double 0 0 100 100)
        subcircle (new Ellipse2D$Double 20 20 60 60)
        area (new Area core-circle)]
    (.subtract area (new Area subcircle))

    (argb-image
      [100 100]
      (.setColor Color/MAGENTA)
      (.setStroke (new BasicStroke 5))
      (.fill area)
      (.draw area))))

(def polygon-image
  (let [polygon (Polygon.
                  (int-array [40 100 80 90 55 10 0])
                  (int-array [100 80 60 10 5 30 50])
                  7)]

    (argb-image
      [100 100]
      (.setColor Color/ORANGE)
      (.fill polygon))))

(def cross-image
  (let [polygon (Polygon.
                  (int-array [20  80 80 100 100 80 80 20 20 0 0 20] )
                  (int-array [100 100 80 80 20 20 0 0 20 20 80 80])
                  12)]

    (argb-image
      [100 100]
      (.setColor Color/ORANGE)
      (.fill polygon))))

(def diamond-image
  (let [polygon (Polygon.
                  (int-array [50 100 50 0])
                  (int-array [100 50 0 50])
                  4)]

    (argb-image
      [100 100]
      (.setColor Color/GRAY)
      (.fill polygon))))

(def semicircle-image
  (argb-image
    [100 100]
    (.setColor Color/BLUE)
    (.fillArc 0 25 100 100 180 -180)))

(def pentagon-image
  (let [polygon (create-polygon 5 0 (new Rectangle 0 0 100 100))]
    (argb-image
      [100 100]
      (.setColor Color/RED)
      (.fill polygon))))

(def hexagon-image
  (let [polygon (create-polygon 6 0 (new Rectangle 0 0 100 100))]
    (argb-image
      [100 100]
      (.setColor (Color. 0xBFFF00))
      (.fill polygon))))

(def octagon-image
  (let [polygon (create-polygon 8 0 (new Rectangle 0 0 100 100))]
    (argb-image
      [100 100]
      (.setColor (Color. 0xEEDC82))
      (.fill polygon))))

(def pic-image
  (argb-image
    [100 100]
    (.setColor Color/BLACK)
    (.fillArc 0 0 100 100 20 320)))

As is this is a lot of images being created we might as well display them all at once.

Displaying the images

In order to display all the images at once I will create a JFrame and then put each image in a ImageIcon of JLabel, laying them out using a GridLayout. In particular, this uses the constructor for GridLayouts with two extra arguments so we can have added padding.
(def image-grid
  [[line-image right-triangle-image rectangle-image cross-image]
   [square-image circle-image pentagon-image diamond-image]
   [trapezoid-image ring-image polygon-image semicircle-image]
   [hexagon-image octagon-image pic-image ellipse-image]])

(defn display-image-grid!
  [images]

  (let [frame (JFrame. "Image grid")
        panel (JPanel.)]
    (.add frame panel)
    (.setLayout panel (new GridLayout (count images) (count (first images)) 10 10))
    (doseq [image-row images]
      (doseq [image image-row]
        (.add panel (new JLabel (new ImageIcon image)))))
    (.pack frame)
    (.setVisible frame true)))

This code produces a JFrame that looks like below. There is so much more that can be done with graphics and Clojure. I will be talking more about graphical applications in the future and the key role the JVM can play in them making them.

No comments:

Post a Comment