Tuesday, May 31, 2022

A first look at Java 2D

The Java 2D API brings basic graphical capabilities to the JVM. We will see how it can be used to create graphical applications in either Java or Clojure. To do anything more advanced it is good to use OpenGL with a library like JOGL or LWJGL.

Image viewer

We will be using Graphics2D to create a number of graphical images. It would be nice then if we could have a Swing window to render and display them.
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;

public class ImageViewer extends JFrame {
    public BufferedImage image;

    public ImageViewer(BufferedImage image) {
        this.image = image;
        this.initialize();
    }

    public void initialize() {
        var p = new JPanel();
        this.add(p);

        var imageIcon = new ImageIcon(this.image);
        p.add(new JLabel(imageIcon));

        this.setSize(this.getPreferredSize());
        this.setTitle("Image viewer");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public Dimension getPreferredSize() {
        var insets = this.getInsets();

        return new Dimension(this.image.getWidth()+ insets.left+insets.right + 30,
                this.image.getHeight() + insets.top + insets.bottom + 30);
    }

}

This is the class we will be using to display the various images we generate using Java 2D. Here is similar Clojure code.
(defn display-image
  [img]

  (let [f (JFrame.)
        icon (ImageIcon. img)
        label (JLabel. icon)]
    (.setTitle f "Image viewer")
    (.setSize f 500 500)
    (.add f label)
    (.setVisible f true)))

The Clojure program just displays an image, but it doesn't go about doing anything more then that so it is a little bit simpler.

Graphics programming with Java

The 2D API contains functions for clipping and rotating. So here is how you would create something that looks like a wheel, which is an image with a bunch of lines around it.
public class BasicImageProducer {

    public static BufferedImage makeImage() {
        var rval = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        var g = rval.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g.setColor(Color.BLACK);
        Ellipse2D oval = new Ellipse2D.Double(5,5,90,90);
        g.setStroke(new BasicStroke(5));
        g.draw(oval);

        g.setStroke(new BasicStroke(2));
        g.setClip(oval);
        int l = 10;
        for(int i = 0; i <= l; i++) {
            g.setTransform(AffineTransform.getRotateInstance(Math.PI * ((double) i / l), 50, 50));
            g.drawLine(0,0,128,128);
        }

        return rval;
    }

}

This produces a graphical output that looks like this: So that previous image has a bunch of lines clipped inside of its circle. So one thing we can do is instead placing those lines outside the circle to get something like this.
public class AlternativeImageProducer {

    public static BufferedImage makeImage() {
        var rval = new BufferedImage(400, 400, BufferedImage.TYPE_INT_ARGB);
        var g = rval.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g.setColor(Color.LIGHT_GRAY);
        g.setStroke(new BasicStroke(10));
        int l = 16;
        for(int i = 0; i <= l; i++) {
            g.setTransform(AffineTransform.getRotateInstance(Math.PI * ((double) i / l), 200, 200));
            g.drawLine(0,0,400,400);
        }

        g.fillOval(100,100,200,200);

        return rval;
    }

}

It is only a slight change of the code and it produces this result:
These are some interesting examples of what is possible with the Java 2D API.

Graphics programming with Clojure

Its always nice to be able to use Clojure, so we will do something with it too.
(defn triangle-image
  []

  (let [img (BufferedImage. 400 410 BufferedImage/TYPE_INT_ARGB)
        g (.createGraphics img)]
    ; process the graphics
    (.setColor g Color/RED)
    (.setStroke g (new BasicStroke 5))

    (.setRenderingHint g RenderingHints/KEY_ANTIALIASING RenderingHints/VALUE_ANTIALIAS_ON)

    (let [path (Path2D$Double.)]
      (.moveTo path 200 0)
      (.lineTo path 0 400)
      (.lineTo path 400 400)
      (.lineTo path 200 0)
      (.closePath path)
      (.draw g path))

    (.dispose g)

    ; return the image
    img))

The 2D API doesn't have a builtin triangle class. So to create one we just use a Path2D. The previous example is a bit elementary. We can do a lot more if we get some interesting paints involved like the radial gradient paint.
(defn radial-paint
  []

  (let [w 400
        h 400
        img (BufferedImage. w h BufferedImage/TYPE_INT_ARGB)
        g (.createGraphics img)]
    ; process the graphics
    (.setRenderingHint g RenderingHints/KEY_ANTIALIASING RenderingHints/VALUE_ANTIALIAS_ON)

    (let [p (RadialGradientPaint.
              (new Point2D$Float (* 0.5 w) (* 0.5 h))
              (float (* 0.5 w))
              (float-array [0.0 1.0])
              (into-array Color [Color/CYAN Color/MAGENTA]))
          shape (new Ellipse2D$Double 0 0 400 400)]
      (.setPaint g p)
      (.fill g shape))

    (.dispose g)

    ; return the image
    img))

This now produces the following interesting image. As we will see there is much more that can be done with the Java 2D graphics API. It is readily available in both Java and Clojure.

No comments:

Post a Comment