More Objects
Here, I will borrow some code from Khan Academy on object-oriented programming. This online course is really well done. It uses a variant of p5.js and also a variant on function definition.
Functions as values
This is a good place to describe the alternative syntax for function definition used by Khan Academy and others.
Recall that we define functions as follows:
1 2 3 |
function area() { // compute area of canvas return width * height; } |
This gives the appearance that “area
” is something special — a function — but at some level, “area
” is just a global variable, like “width
” or “height
,” only its value is a function. It may seem odd that a function can be a value. What does that mean? If a function is a value, you should be able to assign it. You can!
var canvasArea = area;
Now, canvasArea
is another name for area
, and we can call it with canvasArea()
. Notice we did not write:
var canvasArea = area();
By putting the “()
” after area
, we’re saying “call the area
function and use the return value (in this case, a number), so in this case, canvasArea
would be set to the area of the canvas, not the function that computes the area of the canvas.
Anonymous Functions
The syntax used by Khan Academy (and many others) for function definition looks like this:
1 2 3 |
var area = function() { return width * height; } |
In this expression, function()
— and notice the important parentheses — means “create a new function as follows.” This is an expression that returns a function (which is a value). It does not give the function a name, so it is an anonymous function. The var area =
part assigns the function value to a global variable, area
, which has the same effect as our familiar style of function definition: function area() { ... }
.
Note: When you write an anonymous function expression, you can also add parameters as you would expect, e.g. function(x, y) { ... }
.
Syntactic Sugar
Languages often introduce special syntax to make things look pretty when no special syntax is needed. JavaScript does not really need the syntax we use for function definition. All you need as variable declarations and anonymous functions. The addition of a “prettier” form of function declaration is often called “syntactic sugar.” Another example might be “+=
”. Instead of a += 2
; we can always write a = a + 2
, so “+=
” does not add any new capability.
Flower Grower Examples
Here is some code based on the Khan Academy “Challenge: Flower Grower” example. Click to “grow” the flowers.
function Tulip(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
fill(255, 0, 0); // red
var y = this.y - this.height;
ellipse(this.x + 5, y, 44, 44);
triangle(this.x - 16, y, this.x + 20, y, this.x - 20, y - 31);
triangle(this.x - 14, y, this.x + 24, y, this.x + 3, y - 39);
triangle(this.x + -4, y, this.x + 26, y, this.x + 29, y - 36);
};
this.growBy = function(amount) {
this.height += amount;
}
};
function Sunflower(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
stroke(0, 0, 0);
fill(255, 221, 0); // yellow
// var y =
ellipse(this.x - 10, this.y - this.height, 20, 18);
ellipse(this.x + 5, this.y - this.height - 15, 20, 18);
ellipse(this.x + 5, this.y - this.height + 15, 20, 18);
ellipse(this.x + 20, this.y - this.height, 20, 18);
fill(20, 20, 20); // dark center
ellipse(this.x + 5, this.y - this.height, 20, 20);
};
this.growBy = function(amount) {
this.height += amount;
}
};
var tulip;
var sunflower;
function setup() {
createCanvas(400, 400);
tulip = new Tulip(38, 390, 150);
sunflower = new Sunflower(186, 390, 100);
frameRate(5);
}
function draw() {
background(207, 250, 255);
tulip.draw();
sunflower.draw();
};
function mousePressed() {
tulip.growBy(5);
sunflower.growBy(10);
}
Using “this”
Note the use of this
, e.g. this.y
or this.height
to reference properties of the object inside functions draw
and growBy
.
Common subexpressions
The Sunflower draw function computes this.y-this.height
a total of 5 times! In Khan Academy’s version, Tulip’s draw
used the expression 10 times! Notice, in Tulip’s draw
function, I’ve defined a new variable, y = this.y - this.height
. Now, I can rewrite the code in terms of y
, saving a lot of typing and resulting in much nicer code.
Exercise: do the same for Tulip
. I’ve included // var y =
to suggest how and where to make this change.
Methods vs. Functions
The functions draw
and growBy
are properties of flower objects, so we call them using “dot” notation, e.g. tulip.draw()
. A function associated with an object or class of objects is conventionally called a method. (You may also hear the term member function.)
Why methods? Why not just write a function? Yes, we could define tulipDraw(tulip)
and sunflowerDraw(sunflower)
and accomplish the same thing. The advantage of using methods is that with methods we can write: flower.draw()
where flower is a variable containing either a tulip or a sunflower. The alternative is to figure out which function to call:
1 2 3 4 5 |
if (isTulip(flower)) { tulipDraw(flower); } else if (isSunflower(flower)) { sunflowerDraw(flower); } |
Creating a New Class of Flowers
Here is a new version with a new flower type, Violet
.
function Tulip(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
fill(255, 0, 0); // red
var y = this.y - this.height;
ellipse(this.x + 5, y, 44, 44);
triangle(this.x - 16, y, this.x + 20, y, this.x - 20, y - 31);
triangle(this.x - 14, y, this.x + 24, y, this.x + 3, y - 39);
triangle(this.x + -4, y, this.x + 26, y, this.x + 29, y - 36);
};
this.growBy = function(amount) {
this.height += amount;
}
};
function Sunflower(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
stroke(0, 0, 0);
fill(255, 221, 0); // yellow
// var y =
ellipse(this.x - 10, this.y - this.height, 20, 18);
ellipse(this.x + 5, this.y - this.height - 15, 20, 18);
ellipse(this.x + 5, this.y - this.height + 15, 20, 18);
ellipse(this.x + 20, this.y - this.height, 20, 18);
fill(20, 20, 20); // dark center
ellipse(this.x + 5, this.y - this.height, 20, 20);
};
this.growBy = function(amount) {
this.height += amount;
}
};
function Violet(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke()
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals -- rotate an ellipse
stroke(0, 0, 0);
fill(73, 92, 160);
push();
translate(this.x + 5, this.y - this.height);
for (var i = 0; i < 5; i++) {
ellipse(0, 15, 20, 30);
rotate(radians(360/5));
}
fill(255, 221, 0); // yellow
ellipse(0, 0, 15, 15);
pop();
};
this.growBy = function(amount) {
this.height += amount;
}
};
var tulip;
var sunflower;
var violet;
function setup() {
createCanvas(400, 400);
frameRate(5);
tulip = new Tulip(38, 390, 150);
sunflower = new Sunflower(186, 390, 100);
violet = new Violet(250, 390, 125);
}
function draw() {
background(207, 250, 255);
tulip.draw();
sunflower.draw();
violet.draw();
};
function mousePressed() {
tulip.growBy(5);
sunflower.growBy(10);
violet.growBy(8);
}
In Violet
’s draw
function, I tried to improve on the style of the other draw
methods. The other methods drew a lot of detail at this.x
and this.y - this.height
. Thus the flower was “translated” in x
and y
by adding these x
and y
offsets. I thought it would be nicer to just use translate
to make the origin (0, 0) be the center of the flower. Then, since flower petals have radial symmetry, let’s use rotate inside a loop to draw each petal. (Originally, I drew 4 petals, and it was quite gratifying to simply change the rotation angle and the loop count in order to draw 5 petals, which look much nicer.)
By now, you should be able to make another object, say a house or a dog, with position, size, and other properties and a draw method.
An Array of Flowers
This is getting a little tedious, with an explicit draw call to each flower on the canvas. Why don’t we just put everything to draw in an array and iterate through the array, drawing each object there?
Exercise: Change the previous code example to use an array of flowers. See if your changes match the code below.
function Tulip(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
fill(255, 0, 0); // red
var y = this.y - this.height;
ellipse(this.x + 5, y, 44, 44);
triangle(this.x - 16, y, this.x + 20, y, this.x - 20, y - 31);
triangle(this.x - 14, y, this.x + 24, y, this.x + 3, y - 39);
triangle(this.x + -4, y, this.x + 26, y, this.x + 29, y - 36);
};
this.growBy = function(amount) {
this.height += amount;
}
};
function Sunflower(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
stroke(0, 0, 0);
fill(255, 221, 0); // yellow
// var y =
ellipse(this.x - 10, this.y - this.height, 20, 18);
ellipse(this.x + 5, this.y - this.height - 15, 20, 18);
ellipse(this.x + 5, this.y - this.height + 15, 20, 18);
ellipse(this.x + 20, this.y - this.height, 20, 18);
fill(20, 20, 20); // dark center
ellipse(this.x + 5, this.y - this.height, 20, 20);
};
this.growBy = function(amount) {
this.height += amount;
}
};
function Violet(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke()
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals -- rotate an ellipse
stroke(0, 0, 0);
fill(73, 92, 160);
push();
translate(this.x + 5, this.y - this.height);
for (var i = 0; i < 5; i++) {
ellipse(0, 15, 20, 30);
rotate(radians(360/5));
}
// var y =
fill(255, 221, 0); // yellow
ellipse(0, 0, 15, 15);
pop();
};
this.growBy = function(amount) {
this.height += amount;
}
};
var tulip;
var sunflower;
var violet;
var flowers = []; // an empty array
function setup() {
createCanvas(400, 400);
frameRate(5);
flowers.push(new Tulip(38, 390, 150));
flowers.push(new Sunflower(186, 390, 100));
flowers.push(new Violet(250, 390, 125));
}
function draw() {
background(207, 250, 255);
for (var i = 0; i < flowers.length; i++) {
flowers[i].draw();
}
};
function mousePressed() {
for (var i = 0; i < flowers.length; i++) {
flowers[i].growBy(5);
}
}
What changed? I made a flowers
array and initialized it to an empty array []
. I used flowers.push()
to insert new flowers at the end of the array. Then, I wrote a for
loop to call the draw
method on every element of the array and another for loop to call growBy
in mousePressed
. (I did not implement a different growth amount for each flower.)
Exercise: Give each flower a different amount to grow by when the mouse is pressed.
Many Flowers — Random Acts of Violets
In this example, I removed the initialization that creates 3 flowers, and I add code to create a new random flower each time a key is pressed.
Think about how you would do this before reading the code.
function Tulip(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
fill(255, 0, 0); // red
var y = this.y - this.height;
ellipse(this.x + 5, y, 44, 44);
triangle(this.x - 16, y, this.x + 20, y, this.x - 20, y - 31);
triangle(this.x - 14, y, this.x + 24, y, this.x + 3, y - 39);
triangle(this.x + -4, y, this.x + 26, y, this.x + 29, y - 36);
};
this.growBy = function(amount) {
this.height += amount;
}
};
function Sunflower(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke();
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals
stroke(0, 0, 0);
fill(255, 221, 0); // yellow
// var y =
ellipse(this.x - 10, this.y - this.height, 20, 18);
ellipse(this.x + 5, this.y - this.height - 15, 20, 18);
ellipse(this.x + 5, this.y - this.height + 15, 20, 18);
ellipse(this.x + 20, this.y - this.height, 20, 18);
fill(20, 20, 20); // dark center
ellipse(this.x + 5, this.y - this.height, 20, 20);
};
this.growBy = function(amount) {
this.height += amount;
}
};
function Violet(x, y, height) {
this.x = x;
this.y = y;
this.height = height;
this.draw = function() {
noStroke()
fill(16, 122, 12);
rect(this.x, this.y - this.height, 10, this.height);
// petals -- rotate an ellipse
stroke(0, 0, 0);
fill(73, 92, 160);
push();
translate(this.x + 5, this.y - this.height);
for (var i = 0; i < 5; i++) {
ellipse(0, 15, 20, 30);
rotate(radians(360/5));
}
// var y =
fill(255, 221, 0); // yellow
ellipse(0, 0, 15, 15);
pop();
};
this.growBy = function(amount) {
this.height += amount;
}
};
var tulip;
var sunflower;
var violet;
var flowers = []; // an empty array
function setup() {
createCanvas(400, 400);
frameRate(5);
// flowers.push(new Tulip(38, 390, 150));
// flowers.push(new Sunflower(186, 390, 100));
// flowers.push(new Violet(250, 390, 125));
}
function draw() {
background(207, 250, 255);
noStroke();
fill(0);
text("Press key for acts of random violets.", 10, 20);
for (var i = 0; i < flowers.length; i++) {
flowers[i].draw();
}
};
function mousePressed() {
for (var i = 0; i < flowers.length; i++) {
flowers[i].growBy(5);
}
}
function keyPressed() {
// pick a random flower maker
var AllFlowers = [Tulip, Sunflower, Violet];
var index = floor(random(AllFlowers.length));
var Flower = AllFlowers[index];
flowers.push(new Flower(random(width), height - random(100), 90 + random(50)));
}
Choosing a Random Element
In keyPressed
, we choose a flower to create. There are many ways to do this. I chose a method based on a p5.js example. Here, the choices are in an array, so we make an array with the choices:
var AllFlowers = [Tulip, Sunflower, Violet];
Notice that the choices are all functions!
Next we pick a random number between 0 and the length of the array. The number returned by random
is a floating point (fractional) number, but we need an integer to serve as an array index, so we use the floor
function, which rounds down to the nearest integer. (Don’t round to the nearest integer or round up because the result could be AllFlowers.length
, which is greater than the last valid index. Recall than the valid index values go from 0 through length-1.)
var index = floor(random(AllFlowers.length));
Finally, we get the random flower function from the array:
var Flower = AllFlowers[index];
Even though this is truly a variable and the value of the variable is randomly determined, we know it is a function, and we can call it using new Flower(...)
to make a new flower. The new flower is immediately pushed onto the end of the flowers array:
flowers.push(new Flower(random(width), height - random(100), 90 + random(50)));
Exercise: Modify the code to remove flowers one-by-one. How would you remove the oldest flower? How would you remove the youngest flower?