More Objects

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:

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:

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.

base

// 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:

Creating a New Class of Flowers

Here is a new version with a new flower type, Violet.

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.

flowerarray

// 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.

randomviolets

// 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?