School Java Project Mondrian Art

The recursion you can see and enjoy :-D

拇指 muzhi.com
Published in
11 min readJan 16, 2020

--

For most people the very first recursive example they are shown is something like factorial.

🄹 vim Factorial.java                    
🄹 javac Factorial.java && java Factorial
5! = 120

Here is how it is implemented:

class Factorial {
private static int fact(int n) {
if(n == 0) {
return 1;
}
return n * fact(n - 1);
}
public static void main(String[] args) {
System.out.println("5! = " + fact(5));
}
}

Today let’s play recursion visually with Mondrian art. Piet Mondrian was a Dutch painter and theoretician who is regarded as one of the greatest artists of the 20th century. He is known for being one of the pioneers of 20th century abstract art, as he changed his artistic direction from figurative painting to an increasingly abstract style, until he reached a point where his artistic vocabulary was reduced to simple geometric elements.

For simplicity we’ll put all code inside a single file Piet.java.

🄹 vim Piet.java

Let’s wet our hands by drawing a line in JFrame.

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Graphics;
class CanvasPanel extends JPanel {

public void paintComponent(Graphics g) {
g.drawLine(0, 0, getWidth(), getHeight());
}
}
class Piet {
Piet() {
JFrame f = new JFrame("Piet Mondrian");
f.setSize(500, 309);
f.setLocation(50, 50);
f.add(new CanvasPanel());
f.setVisible(true);
}

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

Compile and run it.

🄹 javac Piet.java && java Piet

Refactor out the line drawing code into a function named drawLineInRect. And try a vertical line this time.

The complete code so far:

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Rectangle;
class CanvasPanel extends JPanel {

public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
int randomOffset = rect.width / 3;
int fromX = rect.x + randomOffset;
int fromY = rect.y;
int toX = fromX;
int toY = rect.y + rect.height;
g.drawLine(fromX, fromY, toX, toY);
}
}
class Piet {
Piet() {
JFrame f = new JFrame("Piet Mondrian");
f.setSize(500, 309);
f.setLocation(50, 50);
f.add(new CanvasPanel());
f.setVisible(true);
}

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

Let’s make this vertical line random.

The complete code so far:

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.util.Random;
class CanvasPanel extends JPanel {
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
int randomOffset = rand.nextInt(rect.width);
int fromX = rect.x + randomOffset;
int fromY = rect.y;
int toX = fromX;
int toY = rect.y + rect.height;
g.drawLine(fromX, fromY, toX, toY);
}
}
class Piet {
Piet() {
JFrame f = new JFrame("Piet Mondrian");
f.setSize(500, 309);
f.setLocation(50, 50);
f.add(new CanvasPanel());
f.setVisible(true);
}

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

Fill the left small rectangle with yellow colour.

The complete code so far:

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Color;
import java.util.Random;
class CanvasPanel extends JPanel {
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
int randomOffset = rand.nextInt(rect.width);
int fromX = rect.x + randomOffset;
int fromY = rect.y;
int toX = fromX;
int toY = rect.y + rect.height;
g.drawLine(fromX, fromY, toX, toY);
Rectangle rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
g.setColor(Color.orange);
g.fillRect(rect0.x, rect0.y, rect0.width, rect0.height);
}

}
class Piet {
Piet() {
JFrame f = new JFrame("Piet Mondrian");
f.setSize(500, 309);
f.setLocation(50, 50);
f.add(new CanvasPanel());
f.setVisible(true);
}
public static final void main(String[] args) {
new Piet();
}
}

Fill the two small rectangles with different colours.

class CanvasPanel extends JPanel {
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
int randomOffset = rand.nextInt(rect.width);
int fromX = rect.x + randomOffset;
int fromY = rect.y;
int toX = fromX;
int toY = rect.y + rect.height;
g.drawLine(fromX, fromY, toX, toY);
Rectangle rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
g.setColor(Color.orange);
g.fillRect(rect0.x, rect0.y, rect0.width, rect0.height);
Rectangle rect1 = new Rectangle(rect.x + randomOffset, rect.y, rect.width - randomOffset, rect.height);
g.setColor(Color.green);
g.fillRect(rect1.x, rect1.y, rect1.width, rect1.height);

}
}

There is a problem: the vertical line is gone, i.e. covered by the two small rectangles. What we need is something like:

It was fixed by drawing the line after drawing the two rectangles. We also need to set black colour for the line.

class CanvasPanel extends JPanel {
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
int randomOffset = rand.nextInt(rect.width);
Rectangle rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
g.setColor(Color.orange);
g.fillRect(rect0.x, rect0.y, rect0.width, rect0.height);
Rectangle rect1 = new Rectangle(rect.x + randomOffset, rect.y, rect.width - randomOffset, rect.height);
g.setColor(Color.green);
g.fillRect(rect1.x, rect1.y, rect1.width, rect1.height);
int fromX = rect.x + randomOffset;
int fromY = rect.y;
int toX = fromX;
int toY = rect.y + rect.height;
g.setColor(Color.black);
g.drawLine(fromX, fromY, toX, toY);
}
}

Modify method drawLineInRect so it draws a vertical line for a portrait rectangle and a horizontal line for a landscape rectangle.

class CanvasPanel extends JPanel {
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
int randomOffset;
Rectangle rect0, rect1;
int fromX, fromY, toX, toY;
if (rect.width < rect.height) {
randomOffset = rand.nextInt(rect.height);
rect0 = new Rectangle(rect.x, rect.y, rect.width, randomOffset);
rect1 = new Rectangle(rect.x, rect.y + randomOffset, rect.width, rect.height - randomOffset);
fromX = rect.x;
fromY = rect.y + randomOffset;
toX = fromX + rect.width;
toY = fromY;
} else {
randomOffset = rand.nextInt(rect.width);
rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
rect1 = new Rectangle(rect.x + randomOffset, rect.y, rect.width - randomOffset, rect.height);
fromX = rect.x + randomOffset;
fromY = rect.y;
toX = fromX;
toY = rect.y + rect.height;
}
g.setColor(Color.orange);
g.fillRect(rect0.x, rect0.y, rect0.width, rect0.height);
g.setColor(Color.green);
g.fillRect(rect1.x, rect1.y, rect1.width, rect1.height);
g.setColor(Color.black);
g.drawLine(fromX, fromY, toX, toY);
}
}

We can test the portrait mode by changing the frame width:

class Piet {
Piet() {
JFrame f = new JFrame("Piet Mondrian");
f.setSize(250, 309);
f.setLocation(50, 50);
f.add(new CanvasPanel());
f.setVisible(true);
}

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

Change it back to 500 we get a rectangle in landscape mode.

class Piet {
Piet() {
JFrame f = new JFrame("Piet Mondrian");
f.setSize(500, 309);
f.setLocation(50, 50);
f.add(new CanvasPanel());
f.setVisible(true);
}

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

Are you ready? We’ll make the first try of recursive call.

class CanvasPanel extends JPanel {
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
if (rect.width < 50 && rect.height < 50) {
return;
}
int randomOffset;
Rectangle rect0, rect1;
int fromX, fromY, toX, toY;
if (rect.width < rect.height) {
randomOffset = rand.nextInt(rect.height);
rect0 = new Rectangle(rect.x, rect.y, rect.width, randomOffset);
rect1 = new Rectangle(rect.x, rect.y + randomOffset, rect.width, rect.height - randomOffset);
fromX = rect.x;
fromY = rect.y + randomOffset;
toX = fromX + rect.width;
toY = fromY;
} else {
randomOffset = rand.nextInt(rect.width);
rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
rect1 = new Rectangle(rect.x + randomOffset, rect.y, rect.width - randomOffset, rect.height);
fromX = rect.x + randomOffset;
fromY = rect.y;
toX = fromX;
toY = rect.y + rect.height;
}
g.setColor(Color.orange);
g.fillRect(rect0.x, rect0.y, rect0.width, rect0.height);
g.setColor(Color.green);
g.fillRect(rect1.x, rect1.y, rect1.width, rect1.height);
g.setColor(Color.black);
g.drawLine(fromX, fromY, toX, toY);
drawLineInRect(rect0, g);
drawLineInRect(rect1, g);

}
}

And we get:

The problem came from that filled rectangles cover some of lines. It can be fixed by only filling the inset of each rectangle:

int gap = 1;
g.setColor(Color.orange);
g.fillRect(rect0.x + gap, rect0.y + gap, rect0.width - 2*gap, rect0.height - 2*gap);
g.setColor(Color.green);
g.fillRect(rect1.x + gap, rect1.y + gap, rect1.width - 2*gap, rect1.height - 2*gap);

Cool! Let’s make it cooler by introducing random filling colours.

class CanvasPanel extends JPanel {
private static final int LINE_WIDTH = 1;
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
if (rect.width < 100 && rect.height < 62) {
return;
}
int randomOffset;
Rectangle rect0, rect1;
int fromX, fromY, toX, toY;
if (rect.width < rect.height) {
randomOffset = rand.nextInt(rect.height);
rect0 = new Rectangle(rect.x, rect.y, rect.width, randomOffset);
rect1 = new Rectangle(rect.x, rect.y + randomOffset, rect.width, rect.height - randomOffset);
fromX = rect.x;
fromY = rect.y + randomOffset;
toX = fromX + rect.width;
toY = fromY;
} else {
randomOffset = rand.nextInt(rect.width);
rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
rect1 = new Rectangle(rect.x + randomOffset, rect.y, rect.width - randomOffset, rect.height);
fromX = rect.x + randomOffset;
fromY = rect.y;
toX = fromX;
toY = rect.y + rect.height;
}
fillInRect(rect0, LINE_WIDTH, g);
fillInRect(rect1, LINE_WIDTH, g);
g.setColor(Color.black);
g.drawLine(fromX, fromY, toX, toY);
drawLineInRect(rect0, g);
drawLineInRect(rect1, g);
}
private void fillInRect(Rectangle rect, int gap, Graphics g) {
float red = rand.nextFloat();
float grn = rand.nextFloat();
float blu = rand.nextFloat();
Color randomColor = new Color(red, grn, blu);
g.setColor(randomColor);
g.fillRect(rect.x + gap, rect.y + gap, rect.width - 2*gap, rect.height - 2*gap);
}

}

The first art is out:

I just can’t help baking another one:

To increase the stroke width we need the help of Graphics2D. But that’s hardly an issue, as every Graphics in Swing is a Graphics2D object (it just keeps the old interface for compatibility reasons).

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.BasicStroke;

import java.awt.Rectangle;
import java.awt.Color;
import java.util.Random;
class CanvasPanel extends JPanel {
private static final int LINE_WIDTH = 5;
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(0, 0, getWidth(), getHeight());
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
if (rect.width < 200 && rect.height < 124) {
return;
}
int randomOffset;
Rectangle rect0, rect1;
int fromX, fromY, toX, toY;
if (rect.width < rect.height) {
randomOffset = rand.nextInt(rect.height);
rect0 = new Rectangle(rect.x, rect.y, rect.width, randomOffset);
rect1 = new Rectangle(rect.x, rect.y + randomOffset, rect.width, rect.height - randomOffset);
fromX = rect.x;
fromY = rect.y + randomOffset;
toX = fromX + rect.width;
toY = fromY;
} else {
randomOffset = rand.nextInt(rect.width);
rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
rect1 = new Rectangle(rect.x + randomOffset, rect.y, rect.width - randomOffset, rect.height);
fromX = rect.x + randomOffset;
fromY = rect.y;
toX = fromX;
toY = rect.y + rect.height;
}
fillInRect(rect0, LINE_WIDTH / 2, g);
fillInRect(rect1, LINE_WIDTH / 2, g);
g.setColor(Color.black);
Graphics2D g2 = (Graphics2D)g;
g2.setStroke(new BasicStroke(LINE_WIDTH));
g2.drawLine(fromX, fromY, toX, toY);
drawLineInRect(rect0, g);
drawLineInRect(rect1, g);
}
private void fillInRect(Rectangle rect, int gap, Graphics g) {
float red = rand.nextFloat();
float grn = rand.nextFloat();
float blu = rand.nextFloat();
Color randomColor = new Color(red, grn, blu);
g.setColor(randomColor);
g.fillRect(rect.x + gap, rect.y + gap, rect.width - 2*gap, rect.height - 2*gap);
}
}

The outmost frame is not covered. This can be fixed by using negative insets for the first rectangle.

class CanvasPanel extends JPanel {
private static final int LINE_WIDTH = 5;
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(-LINE_WIDTH/2, -LINE_WIDTH/2, getWidth() + LINE_WIDTH, getHeight() + LINE_WIDTH);
drawLineInRect(rect, g);
}
// ...

This is the art with LINE_WIDTH = 8:

Run again we get another:

The complete source code:

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.Rectangle;
import java.awt.Color;
import java.util.Random;
class CanvasPanel extends JPanel {
private static final int LINE_WIDTH = 8;
private Random rand = new Random();
public void paintComponent(Graphics g) {
Rectangle rect = new Rectangle(-LINE_WIDTH/2, -LINE_WIDTH/2, getWidth() + LINE_WIDTH, getHeight() + LINE_WIDTH);
drawLineInRect(rect, g);
}
private void drawLineInRect(Rectangle rect, Graphics g) {
if (rect.width < 200 && rect.height < 124) {
return;
}
int randomOffset;
Rectangle rect0, rect1;
int fromX, fromY, toX, toY;
if (rect.width < rect.height) {
randomOffset = rand.nextInt(rect.height);
rect0 = new Rectangle(rect.x, rect.y, rect.width, randomOffset);
rect1 = new Rectangle(rect.x, rect.y + randomOffset, rect.width, rect.height - randomOffset);
fromX = rect.x;
fromY = rect.y + randomOffset;
toX = fromX + rect.width;
toY = fromY;
} else {
randomOffset = rand.nextInt(rect.width);
rect0 = new Rectangle(rect.x, rect.y, randomOffset, rect.height);
rect1 = new Rectangle(rect.x + randomOffset, rect.y, rect.width - randomOffset, rect.height);
fromX = rect.x + randomOffset;
fromY = rect.y;
toX = fromX;
toY = rect.y + rect.height;
}
fillInRect(rect0, LINE_WIDTH / 2, g);
fillInRect(rect1, LINE_WIDTH / 2, g);
g.setColor(Color.black);
Graphics2D g2 = (Graphics2D)g;
g2.setStroke(new BasicStroke(LINE_WIDTH));
g2.drawLine(fromX, fromY, toX, toY);
drawLineInRect(rect0, g);
drawLineInRect(rect1, g);
}
private void fillInRect(Rectangle rect, int gap, Graphics g) {
float red = rand.nextFloat();
float grn = rand.nextFloat();
float blu = rand.nextFloat();
Color randomColor = new Color(red, grn, blu);
g.setColor(randomColor);
g.fillRect(rect.x + gap, rect.y + gap, rect.width - 2*gap, rect.height - 2*gap);
}
}
class Piet {
Piet() {
JFrame f = new JFrame("Piet Mondrian");
f.setSize(500, 309);
f.setLocation(50, 50);
f.add(new CanvasPanel());
f.setVisible(true);
}

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

The most interesting part is the recursive call against function drawLineInRect.

This is a similar Mondrian art I implemented with Swift using Xcode:

I put the corresponding Swift code here for comparison:

import UIKitclass PietView: UIView {
let lineWidth: CGFloat = 11
let percent: CGFloat = 0.46
var minW: CGFloat = 0
var minH: CGFloat = 0
override func draw(_ rect: CGRect) {
minW = rect.width * percent
minH = rect.height * percent

let inset = -lineWidth/2
let rectWithInset = rect.inset(by: UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset))
drawLineIn(rect: rectWithInset)
}
func drawLineIn(rect: CGRect) {
if rect.width < minW && rect.height < minH {
return
}

let path = UIBezierPath()

let randomOffset: CGFloat
let fromX, fromY, toX, toY : CGFloat
let rect0: CGRect
let rect1: CGRect

if rect.width < rect.height {
randomOffset = CGFloat(arc4random() % UInt32(rect.height))
fromX = rect.origin.x
fromY = rect.origin.y + randomOffset
toX = fromX + rect.width
toY = fromY
rect0 = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width, height: randomOffset)
rect1 = CGRect(x: rect.origin.x, y: rect.origin.y + randomOffset, width: rect.width, height: rect.height - randomOffset)
} else {
randomOffset = CGFloat(arc4random() % UInt32(rect.width))
fromX = rect.origin.x + randomOffset
fromY = rect.origin.y
toX = fromX
toY = fromY + rect.height
rect0 = CGRect(x: rect.origin.x, y: rect.origin.y, width: randomOffset, height: rect.height)
rect1 = CGRect(x: rect.origin.x + randomOffset, y: rect.origin.y, width: rect.width - randomOffset, height: rect.height)
}

fillIn(rect: rect0, inset: lineWidth/2)
fillIn(rect: rect1, inset: lineWidth/2)

path.move(to: CGPoint(x: fromX, y: fromY))
path.addLine(to: CGPoint(x: toX, y: toY))
path.lineWidth = lineWidth
path.stroke()

drawLineIn(rect: rect0)
drawLineIn(rect: rect1)
}

private func fillIn(rect: CGRect, inset: CGFloat) {
let rectWithInset = rect.inset(by: UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset))
let rectPath = UIBezierPath(rect: rectWithInset)
let r = CGFloat(Int(arc4random()) % 1000) / 1000
let g = CGFloat(Int(arc4random()) % 1000) / 1000
let b = CGFloat(Int(arc4random()) % 1000) / 1000
let a = CGFloat(Int(arc4random()) % 1000) / 1000
let color = UIColor(red: r, green: g, blue: b, alpha: a)
color.setFill()
rectPath.fill()
}
}

--

--