This article draws inspiration from an old post dubbed “Cantor Circles”, but which turned out to render bisecting circles rather than Cantor sets. Nevertheless, it gained some interest, and the exact image below, from the 2002 post, can now be easily found on Google Image search. In this post, the old code is revived, and animation and colors are added for nice patterns and fun. Cantor’s ternary set is also implemented.

I suspect the origin of the bisecting circles was a simple example for recursive code, and simply dividing by two makes the code and rendering easy to understand. In the block below, the function drawCircles() is recursive, calling itself twice with parameters to half the next circles. A recursive function needs a stopping condition, and in this case it is the parameter times which stops further recursions when it reaches 0. The helper function drawMidCircle() makes it more convenient to set the coordinates and size of the circle.

  private void drawCircles(int x, int y, int radius, int times, Graphics g) {
    System.out.printf("x=%d, y=%d, r=%d, times=%d\n", x, y, radius, times);

    if (times > 0) {
      drawMidCircle(x, y, radius, g);
      drawCircles(x + radius / 2, y, radius / 2, times - 1, g);
      drawCircles(x - radius / 2, y, radius / 2, times - 1, g);
    }
  }

  private void drawMidCircle(int x, int y, int radius, Graphics g) {
    g.drawOval(x - radius, y - radius, 2 * radius, 2 * radius);
  }

The next example adds animation. The recursion is the same, but adds an angle parameter to rotate the circles. Also notice that the angle a is negated, which leads to the alternating clockwise and counter-clockwise rotation within each small circle.

Also of interest here, is the Swing double buffering (actually, triple buffering is used in this example) using the BufferStrategy class. Notice that the paint() method of the Frame is no longer overridden to render the graphics, but rather the Graphics object is provided by the BufferStrategy and passed to the custom render() function.

  private void drawCircles(int x, int y, int r, double a, int times, Graphics g) {
    if (times <= 0) {
      return;
    }

    drawMidCircle(x, y, r, g);

    int x1 = (int) (r / 2 * cos(a));
    int y1 = (int) (r / 2 * sin(a));
    drawCircles(x + x1, y + y1, r / 2, -a, times - 1, g);
    drawCircles(x - x1, y - y1, r / 2, -a, times - 1, g);
  }
    createBufferStrategy(3);
    BufferStrategy strategy = getBufferStrategy();

    new Thread(() -> {
      while (true) {
        Graphics g = strategy.getDrawGraphics();
        render(g);
        g.dispose();
        strategy.show();
      }
    }).start();

The last class in this article adds colors, a few options and a spartan UI to tune them. The images and animation below give a quick impression of what is possible by adjusting the sliders. Also note, the color palette can be easily changed during runtime by pasting in hex based color string found on pages like colourlovers.com.

The GIF animations were created using ImageMagick, with commands like these:

convert -delay 2 -loop 0 cantor*png cantor.gif

convert -delay 2 -loop 0 -resize 300x300 -layers Optimize -duplicate 1,-2-1 cantor0{0000..1200}.png cantor.gif
Cantor.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.graphics;

import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JFrame;

/**
 * Draws recursive circles
 */
@SuppressWarnings("serial")
class Cantor extends JFrame {

  public static void main(String args[]) {
    new Cantor();
  }

  Cantor() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(900, 900);
    setVisible(true);
  }

  @Override
  public void paint(Graphics g) {
    g.setColor(Color.white);
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(Color.black);

    drawCircles(450, 450, 400, 7, g);
  }

  private void drawCircles(int x, int y, int radius, int times, Graphics g) {
    System.out.printf("x=%d, y=%d, r=%d, times=%d\n", x, y, radius, times);

    if (times > 0) {
      drawMidCircle(x, y, radius, g);
      drawCircles(x + radius / 2, y, radius / 2, times - 1, g);
      drawCircles(x - radius / 2, y, radius / 2, times - 1, g);
    }
  }

  private void drawMidCircle(int x, int y, int radius, Graphics g) {
    g.drawOval(x - radius, y - radius, 2 * radius, 2 * radius);
  }
}
CantorSpin.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.graphics;

import static java.lang.Math.*;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;

import javax.swing.JFrame;

/**
 * Draws recursive spinning circles.
 * 
 * Uses Double Buffering for smooth rendering. See:
 * http://docs.oracle.com/javase/tutorial/extra/fullscreen/bufferstrategy.html
 */
@SuppressWarnings("serial")
class CantorSpin extends JFrame {

  private final int size;

  private double angle;

  public static void main(String args[]) {
    new CantorSpin(800);
  }

  CantorSpin(int size) {
    this.size = size;

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(size, size);
    setVisible(true);

    createBufferStrategy(3);
    BufferStrategy strategy = getBufferStrategy();

    new Thread(() -> {
      while (true) {
        Graphics g = strategy.getDrawGraphics();
        render(g);
        g.dispose();
        strategy.show();
      }
    }).start();
  }

  private void render(Graphics g) {
    clear(g);

    drawCircles(size / 2, size / 2, (int) (size * 0.4), angle, 7, g);
    angle += 0.01;
  }

  private void clear(Graphics g) {
    g.setColor(Color.white);
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(Color.black);
  }

  private void drawCircles(int x, int y, int r, double a, int times, Graphics g) {
    if (times <= 0) {
      return;
    }

    drawMidCircle(x, y, r, g);

    int x1 = (int) (r / 2 * cos(a));
    int y1 = (int) (r / 2 * sin(a));
    drawCircles(x + x1, y + y1, r / 2, -a, times - 1, g);
    drawCircles(x - x1, y - y1, r / 2, -a, times - 1, g);
  }

  private void drawMidCircle(int x, int y, int r, Graphics g) {
    g.drawOval(x - r, y - r, 2 * r, 2 * r);
  }
}
CantorColors.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.graphics;

import static java.lang.Math.*;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;

import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.JToggleButton;

/**
 * Draws recursive spinning circles with colors.
 */
@SuppressWarnings("serial")
class CantorColors extends JFrame {

  private Canvas canvas;

  private JPanel colorOut;

  private String colorsList =
      "#000fff,#18A3AC,#F48024,#178CCB,#052049,#FBAF3F,#000fff";
  // "#18A3AC,#052049,#178CCB ,#FBAF3F,#F48024,#18A3AC,#052049,#178CCB";
  // "#722059,#A92268,#E02477,#DB7580,#D6C689,#722059,#A92268,#E02477";
  // "#F28E98,#FF9985,#E1CBA6,#DFCFB8,#E7E4D3,#F28E98,#FF9985,#E1CBA6";

  private Color[] colors;

  private double angle;
  private double angleStepSize;
  private double alternateSign;

  private int sleepMs;

  private float transparency;

  private int recursions;

  private boolean cantor;

  private boolean clearScreen;

  private boolean save;

  private int index;

  private BufferedImage bufImg;

  private Graphics imgG;

  public static void main(String args[]) {
    CantorColors cc = new CantorColors();
    cc.initUi();
    cc.start();
  }

  private void initUi() {
    setLayout(new BorderLayout());

    canvas = new Canvas();
    getContentPane().add(canvas, BorderLayout.CENTER);

    JPanel controls = new JPanel();
    controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS));
    getContentPane().add(controls, BorderLayout.NORTH);

    addSlider(controls, "Rotation step size", 1, 100, 5, slider -> {
      angleStepSize = log(slider.getValue());
    });

    addSlider(controls, "Frame delay", 0, 100, 0, slider -> {
      sleepMs = slider.getValue();
    });

    addSlider(controls, "Recursions", 1, 20, 7, slider -> {
      recursions = slider.getValue();
    });

    addSlider(controls, "Transparency", 1, 1000, 1000, slider -> {
      transparency = (float) slider.getValue();
      colors = decodeColors(colorsList);
      showColors();
    });

    JTextField colorIn = new JTextField(colorsList);
    colorIn.addKeyListener(new KeyAdapter() {
      public void keyTyped(KeyEvent e) {
        colorsList = colorIn.getText();
        colors = decodeColors(colorsList);
        showColors();
      }
    });
    controls.add(colorIn);

    colorOut = new JPanel();
    colorOut.setLayout(new BoxLayout(colorOut, BoxLayout.X_AXIS));
    colors = decodeColors(colorsList);
    showColors();
    controls.add(colorOut);

    JPanel buttons = new JPanel();
    buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
    controls.add(buttons);

    addToggleButton(buttons, "Bisect vs. Cantor", false, button -> {
      cantor = button.isSelected();
    });

    addToggleButton(buttons, "Alternate rotation", true, button -> {
      alternateSign = button.isSelected() ? -1 : 1;
    });

    addToggleButton(buttons, "Save PNGs", false, button -> {
      save = button.isSelected();
      index = 0;
    });

    JButton clearButton = new JButton("Clear");
    clearButton.addActionListener(l -> {
      clearScreen = true;
    });
    buttons.add(clearButton);

    canvas.addComponentListener(new ComponentAdapter() {
      @Override
      public void componentResized(ComponentEvent e) {
        createRenderImage();
      }
    });

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(600, 738);
    setVisible(true);
  }

  private void addSlider(JPanel controls, String text, int min, int max, int value,
      Consumer<JSlider> changeListener) {
    JPanel row = new JPanel();
    row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
    controls.add(row);

    JLabel label = new JLabel(text);
    label.setPreferredSize(new Dimension(150, (int) label.getPreferredSize().getHeight()));
    row.add(label);

    JSlider slider = new JSlider(min, max, value);
    row.add(slider);

    slider.addChangeListener(e -> {
      changeListener.accept(slider);
    });
  }

  private void addToggleButton(JPanel buttons, String text, boolean selected,
      Consumer<JToggleButton> actionListener) {
    JToggleButton button = new JToggleButton(text);
    button.setSelected(selected);
    buttons.add(button);

    button.addActionListener(e -> {
      actionListener.accept(button);
    });
  }

  private void showColors() {
    colorOut.removeAll();
    colorOut.setBackground(Color.WHITE);
    for (Color c : colors) {
      JPanel l = new JPanel();
      l.setPreferredSize(
          new Dimension(getWidth() / colors.length, (int) l.getPreferredSize().getHeight()));
      l.setBackground(c);
      colorOut.add(l);
    }
    colorOut.revalidate();
    colorOut.repaint();
  }

  private void start() {
    transparency = 1000f;
    angleStepSize = 0.005;
    alternateSign = -1;
    recursions = 7;
    colors = decodeColors(colorsList);
    showColors();

    createRenderImage();
    Graphics g = canvas.getGraphics();

    new Thread(() -> {
      while (true) {
        render(imgG);

        if (save && index++ % 3 == 0) {
          saveImage(bufImg, index);
        }

        g.drawImage(bufImg, 0, 0, this);

        sleep(sleepMs);
      }
    }).start();
  }

  private void createRenderImage() {
    bufImg = new BufferedImage(canvas.getWidth(), canvas.getHeight(), BufferedImage.TYPE_INT_ARGB);
    imgG = bufImg.createGraphics();
    clear(imgG);
  }

  private void saveImage(BufferedImage bi, int i) {
    try {
      String name = String.format("/tmp/cantor%05d.png", i);
      ImageIO.write(bi, "PNG", new File(name));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * Given a string of comma separated hex encoded RGB values, creates the
   * individual Color objects. All spaces are removed and ignored; the hex
   * values can be prefixed by a hash (#) or not. Adds the alpha transparency
   * setting from the UI.
   */
  private Color[] decodeColors(String hexColors) {
    try {
      String[] split = hexColors.replaceAll(" +", "").split(",");
      Color[] result = new Color[split.length];

      for (int i = 0; i < split.length; i++) {
        String str = (split[i].startsWith("#") ? "" : "#") + split[i];
        float[] comp = Color.decode(str).getRGBComponents(null);
        float a = max(0, min(1f, ((float) i) / transparency));
        result[i] = new Color(comp[0], comp[1], comp[2], a);
      }

      return result;
    } catch (Exception e) {
      System.out.println(e);
      return colors;
    }
  }

  private void sleep(long ms) {
    try {
      Thread.sleep(ms);
    } catch (InterruptedException e) {}
  }

  private void render(Graphics g) {
    if (clearScreen) {
      clear(g);
      clearScreen = false;
    }

    int size = min(canvas.getWidth(), canvas.getHeight());
    drawCantor(size / 2, size / 2, (int) (size * 0.45), angle, recursions, g);
    angle += angleStepSize;
  }

  private void drawCantor(int x, int y, int r, double a, int times, Graphics g) {
    if (times <= 0) {
      return;
    }

    g.setColor(colors[times % colors.length]);
    drawMidCircle(x, y, r, g);

    double nextAngel = alternateSign * a;

    if (cantor) {
      // Cantor set
      int x1 = (int) (r / 3 * cos(a));
      int y1 = (int) (r / 3 * sin(a));
      drawCantor(x + 2 * x1, y + 2 * y1, r / 3, nextAngel, times - 1, g);
      drawCantor(x - 2 * x1, y - 2 * y1, r / 3, nextAngel, times - 1, g);
    } else {
      // Bisect
      int x1 = (int) (r / 2 * cos(a));
      int y1 = (int) (r / 2 * sin(a));
      drawCantor(x + x1, y + y1, r / 2, nextAngel, times - 1, g);
      drawCantor(x - x1, y - y1, r / 2, nextAngel, times - 1, g);
    }
  }

  private void drawMidCircle(int x, int y, int r, Graphics g) {
    g.drawOval(x - r, y - r, 2 * r, 2 * r);
  }

  private void clear(Graphics g) {
    g.setColor(Color.white);
    g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
  }
}