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.
// TULIP - define tulipDraw, tulipGrowBy,
// and the "constructor" function makeTulip
//
function tulipDraw() {
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);
};
function tulipGrowBy(amount) {
this.height += amount;
};
function makeTulip(x, y, height) {
var tulip = {x: x, y: y, height: height,
draw: tulipDraw, growBy: tulipGrowBy};
return tulip; // return the new object
};
// SUNFLOWER - define sunflowerDraw, sunflowGrowBy,
// and the "constructor" function makeSunflower
//
function sunflowerDraw() {
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);
};
function sunflowerGrowBy(amount) {
this.height += amount;
};
function makeSunflower(x, y, height) {
var sunflower = {x: x, y: y, height: height,
draw: sunflowerDraw, growBy: sunflowerGrowBy};
return sunflower; // return the new object
};
// MAIN PROGRAM STARTS HERE
var tulip;
var sunflower;
function setup() {
createCanvas(400, 400);
tulip = makeTulip(38, 390, 150);
sunflower = makeSunflower(186, 390, 100);
frameRate(5);
}
function draw() {
background(207, 250, 255);
tulip.draw();
sunflower.draw();
noStroke();
text("Press mouse to grow", 10, 20);
};
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 Sunflower
. 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
.
// TULIP - define tulipDraw, tulipGrowBy,
// and the "constructor" function makeTulip
//
function tulipDraw() {
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);
};
function tulipGrowBy(amount) {
this.height += amount;
};
function makeTulip(x, y, height) {
var tulip = {x: x, y: y, height: height,
draw: tulipDraw, growBy: tulipGrowBy};
return tulip; // return the new object
};
// SUNFLOWER - define sunflowerDraw, sunflowGrowBy,
// and the "constructor" function makeSunflower
//
function sunflowerDraw() {
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);
};
function sunflowerGrowBy(amount) {
this.height += amount;
};
function makeSunflower(x, y, height) {
var sunflower = {x: x, y: y, height: height,
draw: sunflowerDraw, growBy: sunflowerGrowBy};
return sunflower; // return the new object
};
// VIOLET - define violetDraw, violetGrowBy,
// and the "constructor" function makeViolet
//
function violetDraw() {
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();
};
function violetGrowBy(amount) {
this.height += amount;
}
function makeViolet(x, y, height) {
var violet = {x: x, y: y, height: height,
draw: violetDraw, growBy: violetGrowBy};
return violet;
};
// MAIN PROGRAM STARTS HERE
var tulip;
var sunflower;
var violet;
function setup() {
createCanvas(400, 400);
tulip = makeTulip(38, 390, 150);
sunflower = makeSunflower(186, 390, 100);
violet = makeViolet(250, 390, 125);
frameRate(5);
}
function draw() {
background(207, 250, 255);
tulip.draw();
sunflower.draw();
violet.draw();
noStroke();
text("Press mouse to grow", 10, 20);
};
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.
// TULIP - define tulipDraw, tulipGrowBy,
// and the "constructor" function makeTulip
//
function tulipDraw() {
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);
};
function tulipGrowBy(amount) {
this.height += amount;
};
function makeTulip(x, y, height) {
var tulip = {x: x, y: y, height: height,
draw: tulipDraw, growBy: tulipGrowBy};
return tulip; // return the new object
};
// SUNFLOWER - define sunflowerDraw, sunflowGrowBy,
// and the "constructor" function makeSunflower
//
function sunflowerDraw() {
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);
};
function sunflowerGrowBy(amount) {
this.height += amount;
};
function makeSunflower(x, y, height) {
var sunflower = {x: x, y: y, height: height,
draw: sunflowerDraw, growBy: sunflowerGrowBy};
return sunflower; // return the new object
};
// VIOLET - define violetDraw, violetGrowBy,
// and the "constructor" function makeViolet
//
function violetDraw() {
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();
};
function violetGrowBy(amount) {
this.height += amount;
}
function makeViolet(x, y, height) {
var violet = {x: x, y: y, height: height,
draw: violetDraw, growBy: violetGrowBy};
return violet;
};
// MAIN PROGRAM STARTS HERE
var tulip;
var sunflower;
var violet;
var flowers = []; // an empty array
function setup() {
createCanvas(400, 400);
tulip = makeTulip(38, 390, 150);
sunflower = makeSunflower(186, 390, 100);
violet = makeViolet(250, 390, 125);
flowers.push(tulip);
flowers.push(sunflower);
flowers.push(violet);
// or, you could write flowers = [tulip, sunflower, violet];
frameRate(5);
}
function draw() {
background(207, 250, 255);
for (var i = 0; i < flowers.length; i++) {
flowers[i].draw();
}
noStroke();
text("Press mouse to grow", 10, 20);
};
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.
// TULIP - define tulipDraw, tulipGrowBy,
// and the "constructor" function makeTulip
//
function tulipDraw() {
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);
};
function tulipGrowBy(amount) {
this.height += amount;
};
function makeTulip(x, y, height) {
var tulip = {x: x, y: y, height: height,
draw: tulipDraw, growBy: tulipGrowBy};
return tulip; // return the new object
};
// SUNFLOWER - define sunflowerDraw, sunflowGrowBy,
// and the "constructor" function makeSunflower
//
function sunflowerDraw() {
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);
};
function sunflowerGrowBy(amount) {
this.height += amount;
};
function makeSunflower(x, y, height) {
var sunflower = {x: x, y: y, height: height,
draw: sunflowerDraw, growBy: sunflowerGrowBy};
return sunflower; // return the new object
};
// VIOLET - define violetDraw, violetGrowBy,
// and the "constructor" function makeViolet
//
function violetDraw() {
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();
};
function violetGrowBy(amount) {
this.height += amount;
}
function makeViolet(x, y, height) {
var violet = {x: x, y: y, height: height,
draw: violetDraw, growBy: violetGrowBy};
return violet;
};
// MAIN PROGRAM STARTS HERE
var tulip;
var sunflower;
var violet;
var flowers = []; // an empty array
function setup() {
createCanvas(400, 400);
tulip = makeTulip(38, 390, 150);
sunflower = makeSunflower(186, 390, 100);
violet = makeViolet(250, 390, 125);
frameRate(5);
}
function draw() {
background(207, 250, 255);
for (var i = 0; i < flowers.length; i++) {
flowers[i].draw();
}
noStroke();
fill(0);
text("Press key for acts of random violets.", 10, 20);
text("Press mouse to grow", 10, 35);
};
function mousePressed() {
for (var i = 0; i < flowers.length; i++) {
flowers[i].growBy(5);
}
}
function keyPressed() {
// pick a random flower maker
var allFlowers = [makeTulip, makeSunflower, makeViolet];
// allFlowers is an array of functions! We did not *call* the
// functions, e.g. makeTulip(), so the array elements are functions
// themselves (or you can think of them as function names or
// references to functions.
var flower = random(allFlowers); // pick a random flower maker
// alternatively, you could compute a random index:
// var index = floor(random(AllFlowers.length));
// and then access the array to get a random flower:
// var flower = allFlowers[index];
// now, flower is a function! It is the same function as either
// makeTulip, makeSunflower, or makeViolet. Whatever it is, we call it:
flowers.push(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 put the choices in an array and pick one with random()
:
var allFlowers = [makeTulip, makeSunflower, makeViolet];
var flower = random(allFlowers);
Another method is to compute a random array index and access the array; this is shown in commented code.
Notice that the choices are all functions! Even though flower
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 flower(...)
to make a new flower. The new flower is immediately pushed onto the end of the flowers array:
flowers.push(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?