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

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:

Creating a New Class of Flowers

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

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.

flowerarray

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.

randomviolets

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?