We can draw things with Javascript.
The painting above was created with a fairly simple Javascript program. It is made up of randomly drawn rectangles on what we call the "canvas", a place to paint things.
Our HTML is very simple. We declare the canvas to be 800 pixels wide and 494 pixels high, with a black border. Then we call our Javascript program.
<html> <head> <script src="mondrian.js"></script> </head> <body onload="create_painting();"> <canvas id="Mondrian" width="800" height="494" style="border:4px solid black;"></canvas> </body> </html>
In our Javascript program, we get a handle (called a 'context') for the canvas that we will call "painting":
let painting = document.getElementById( 'Mondrian' ).getContext( '2d' );
To draw a rectangle, we use two methods on the context. They are fillRect() and strokeRect(). The first one draws a rectangle that is filled in with a particular color. The second one just draws and empty rectangle. We use that to frame our filled-in rectangle.
There are two properties in the context that control what fillRect() and strokeRect() do. These are fillStyle, where we set our color, and lineWidth, which determines how wide the line is that strokeRect() draws.
Both fillRect() and strokeRect() take an x and y point (the upper left corner of the rectangle) and a width and a height. To draw a blue rectangle that is 300 pixels wide and 200 pixels high, starting at the point 30 pixels from the left and 90 pixels from the top, we would say:
painting.beginPath(); painting.lineWidth = 2; painting.fillStyle = 'blue'; painting.fillRect( 30, 90, 300, 200 ); painting.strokeRect( 30, 90, 300, 200 ); painting.stroke();
The methods beginPath() and stroke() control when the drawing is actually done. We tell the canvas to begin collecting drawing commands, and when we call stroke(), those commands are actually executed to paint onto the screen.
We are going to define a few helper functions and a couple classes to make our program simpler and easier to understand. The first of these is a function to pick a random color we will use for a rectangle:
let colors = [ 'white', 'lime', 'cyan', 'magenta', 'red', 'blue', 'yellow', 'white' ]; function random_color() { return colors[ Math.floor( Math.random() * colors.length ) ]; }
We have an array of eight colors. We use Math.random() to give us a random number between 0 and 1, such as 0.4377652.
We multiply the length of the array (eight) by that number to get 3.5021216. We then use the Math.floor() function to throw away the decimal part, leaving 3. Then we return colors[3], which is 'magenta'. (Remember that in Javascript, array indices start at zero.)
Our next helper class defines a point:
class Point { constructor( x, y ) { this.x = x this.y = y } }
We will discuss classes, constructors, and the 'this' object a little bit later.
Along with the Point class, we define a Rectangle class. This class is going to do the bulk of the work for us, so we will break it down into little pieces and discuss each piece.
The Rectangle class has a method called draw(), and it draws the rectangle, like we did above.
draw() { painting.beginPath(); painting.lineWidth = 4; painting.fillStyle = this.color; painting.fillRect( this.min.x, this.min.y, this.width, this.height ); painting.strokeRect( this.min.x, this.min.y, this.width, this.height ); painting.stroke(); }
The constructor for the Rectangle class takes two points, the upper left corner of the rectangle, and the lower right corner, which we will call min and max:
constructor( min, max ) { this.min = min; this.max = max; this.width = max.x - min.x; this.height = max.y - min.y; this.a = null; this.b = null; this.landscape = this.width > this.height; this.color = random_color(); this.draw(); }
It calculates the width and height from min and max, sets two objects (a and b) to null (we'll discuss them in a minute), saves a variable that tells us if the rectangle is 'portrait' or 'landscape' in orientation, picks a random color, and then draws the rectangle.
The first thing we do in the program is create a rectangle that is the size of our canvas:
root = new Rectangle( new Point( 0, 0 ), new Point( 800, 494 ) );
We call it 'root' because it is the starting point of our painting. The next thing we do is divide that root rectangle into two parts, by calling a method on Rectangle called divide():
root.divide();
The divide() method is where all the work really happens:
divide() { let rnd = 0.6 + Math.random() * 0.4; if( this.width > min_span && this.height > min_span ) { if( this.landscape ) { let x = this.min.x + Math.floor( this.height * rnd ); this.a = new Rectangle( this.min, new Point( x, this.max.y ) ); this.b = new Rectangle( new Point( x, this.min.y ), this.max ); } else { let y = this.min.y + Math.floor( this.width * rnd ); this.a = new Rectangle( this.min, new Point( this.max.x, y ) ); this.b = new Rectangle( new Point( this.min.x, y ), this.max ); } this.a.divide(); this.b.divide(); } }
The divide() method first picks a random number between 0.6 and 1.0. This number determines where we will slice our rectangle.
If the width or height is too small, we do nothing.
If the rectangle is wider than it is tall ('landscape' orientation), then we select a point near the middle to divide the rectangle into two. Each new rectangle (left and right) is saved in the variables 'a' and 'b'. We do the same for portrait mode rectangles, but we divide those into top and bottom rectangles, storing those in 'a' and 'b' as before.
Then we call the divide() method on the two new rectangles we just created, to split them up in turn, and so on, until the width or height is too small, in which case, we are done.
A function that calls itself in this manner is called a recursive function.
Putting it all together, our program now looks like this:
function create_painting() { let min_span = 120; let root = null; let colors = [ 'white', 'lime', 'cyan', 'magenta', 'red', 'blue', 'yellow', 'white' ]; function random_color() { return colors[ Math.floor( Math.random() * colors.length ) ]; } class Point { constructor( x, y ) { this.x = x this.y = y } } class Rectangle { constructor( min, max ) { this.min = min; this.max = max; this.width = max.x - min.x; this.height = max.y - min.y; this.a = null; this.b = null; this.landscape = this.width > this.height; this.color = random_color(); this.draw(); } draw() { painting.beginPath(); painting.lineWidth = 4; painting.fillStyle = this.color; painting.fillRect( this.min.x, this.min.y, this.width, this.height ); painting.strokeRect( this.min.x, this.min.y, this.width, this.height ); painting.stroke(); } divide() { let rnd = 0.6 + Math.random() * 0.4; if( this.width > min_span && this.height > min_span ) { if( this.landscape ) { let x = this.min.x + Math.floor( this.height * rnd ); this.a = new Rectangle( this.min, new Point( x, this.max.y ) ); this.b = new Rectangle( new Point( x, this.min.y ), this.max ); } else { let y = this.min.y + Math.floor( this.width * rnd ); this.a = new Rectangle( this.min, new Point( this.max.x, y ) ); this.b = new Rectangle( new Point( this.min.x, y ), this.max ); } this.a.divide(); this.b.divide(); } } } let painting = document.getElementById( 'Mondrian' ).getContext( '2d' ); root = new Rectangle( new Point( 0, 0 ), new Point( 800, 494 ) ); root.divide(); }
You can run the program by clicking on this link. Every time you refresh the page, you get a new painting.
Classes, objects, and constructors
A class is a way of grouping data with code that acts upon that data. Classes often represent concepts, like out Point and Rectangle classes. They make the program easier to understand, and they isolate code and data from other code, which reduces the opportunity for error.
When we make an instance of a class, we get an object. You can create any number of objects from a class. We have only one Rectangle class, but we created many objects that were rectangles. We did this by using the new operator. The new operator creates an object which is a new instance of a class.
Sometimes we want the data in our new object to be initialized to particular values. In the case of our Point object, we want to make sure that the x and y values are set. We do this by using a constructor. Both our Point and our Rectangle classes have constructors. When we create a new object from either of those classes, we pass arguments to their constructors to tell them how to initialize the data. Some data items in the object are initialized without arguments, such as the a and b objects in the Rectangle class. They were set to null. Likewise, the color property was set to a random color.
The constructor can also perform other tasks, such as drawing the rectangle.
When designing a program, classes allow you to break up the program into concepts (such as Point and Rectangle). Then you can treat the objects as 'black boxes' that handle all of the details involved in the concept, so the rest of the code is not concerned with them. We create a Rectangle object, and let the object worry about handling the 'rectangleness' of things.