Case Study: Watching Grass Grow¶
In these notes you will learn how to:
- Create a helper class for representing numeric ranges.
- Model a real-world object using OOP.
- Generate animated graphical scenes using random numbers.
Introduction¶
In this note we’ll create a program that displays some grass and dandelions that grow over time. Each individual blade of grass will have its own corresponding object that controls how it is drawn and how it grows.
The graphics are quite simple but still effective, e.g. a blade of grass is a line segment with a random height, thickness, color, and orientation.
Storing a Range of Numbers¶
In our example programs so far, we have often given a variables random values chosen from a given range, e.g.:
float lowValue = -2.43;
float highValue = 8.04;
float r = random(lowValue, highValue);
The problem with this is that if you have many different ranges (as we will for the program below), then keeping track of so many variables and values is a real headache.
So instead of storing the low value and high value of a range in separate variables, we will create a new kind of object that stores both at the same time:
class Range {
float lo, hi;
Range(float lo_init, float hi_init) {
lo = lo_init;
hi = hi_init;
}
float randValue() {
return random(lo, hi);
}
}
This class is simple but useful. It reduces the number of variables we need to represent a range. The example above can be re-written like this:
Range rng = new Range(-2.43, 8.04);
float r = rng.randValue();
We can also access the low and high values if we need them, e.g.:
Range rng = new Range(-2.43, 8.04);
println("rng = [" + rng.lo + ", " + rng.hi + "]");
Blades of Grass¶
We’re going to represent each blade of grass as its own GrassBlade
object.
The attributes of a blade of grass that we’ll include are its position,
thickness, length, angle, color, and rate of growth:
Range thicknessRange = new Range(1, 3);
Range lengthRange = new Range(1, 3);
Range angleRange = new Range(-25, 25);
Range greenRange = new Range(200, 256);
// ...
class GrassBlade {
float x, y;
float thickness;
float length;
float angle;
color clr;
float growthRate = 0.01;
GrassBlade(float x_init, float y_init) {
x = x_init;
y = y_init;
thickness = thicknessRange.randValue();
length = lengthRange.randValue();
angle = angleRange.randValue();
clr = color(0, greenRange.randValue(), 0);
}
// a blade of grass is drawn as a line
void render() {
pushMatrix();
stroke(clr);
strokeWeight(thickness);
translate(x, y);
rotate(radians(angle));
line(0, 0, 0, -length);
popMatrix();
}
void grow() {
if (random(0.0, 1.0) < growthRate)
length += 1;
}
} // class GrassBlade
The grow()
function is serves the same purpose as the update()
function we’ve seem in previous examples; we use the name grow
here
because it is more suggestive of how the blade changes than the generic term
update
. It works by sometimes increasing the length of the blade.
Dandelions¶
For us, a dandelion will consist of a stem and a circular “flower” on one end:
Range greenRange = new Range(200, 256);
Range radiusRange = new Range(4, 7);
Range stemLengthRange = new Range(10, 15);
Range stemThicknessRange = new Range(3, 5);
class Dandelion {
float x, y;
float radius;
float stemLength;
float stemThickness;
float stemAngle;
color headColor;
color stemColor;
float growthRate = 0.02;
Dandelion(float x_init, float y_init) {
x = x_init;
y = y_init;
radius = radiusRange.randValue();
stemLength = stemLengthRange.randValue();
stemThickness = stemThicknessRange.randValue();
stemAngle = angleRange.randValue();
headColor = color(255, 255, 0);
stemColor = color(0, greenRange.randValue(), 0);
}
// dandelion is a line with a circle on top
void render() {
pushMatrix();
translate(x, y);
rotate(radians(stemAngle));
stroke(stemColor);
strokeWeight(stemThickness);
line(0, 0, 0, -stemLength);
noStroke();
fill(headColor);
ellipse(0, -stemLength, 2 * radius, 2 * radius);
popMatrix();
}
void grow() {
if (random(0.0, 1.0) < growthRate)
stemLength += 1;
}
} // class Dandelion
A Growing Lawn¶
Finally, we need code to draw the lawn. For this program, a lawn is collection of grass blades and dandelions:
ArrayList<GrassBlade> lawn;
float bladeGap;
ArrayList<Dandelion> weeds;
int numBlades = 500;
int numWeeds = 5;
Range thicknessRange = new Range(1, 3);
Range lengthRange = new Range(1, 3);
Range angleRange = new Range(-25, 25);
Range greenRange = new Range(200, 256);
Range gapRange = new Range(1, 3);
Range radiusRange = new Range(4, 7);
Range stemLengthRange = new Range(10, 15);
Range stemThicknessRange = new Range(3, 5);
void setup() {
size(500, 500);
// initialize the grass blades
lawn = new ArrayList<GrassBlade>();
bladeGap = gapRange.randValue();
int i = 0;
while (i < numBlades) {
GrassBlade g = new GrassBlade(bladeGap * i, 250);
lawn.add(g);
++i;
}
// initialize the weeds
weeds = new ArrayList<Dandelion>();
i = 0; // no "int" because i was already defined above
while (i < numWeeds) {
Dandelion d = new Dandelion(random(10, width - 10), 250);
weeds.add(d);
++i;
}
} // setup
void draw() {
background(224, 255, 255);
// draw weeds
for(Dandelion d : weeds) {
d.render();
d.grow();
}
// draw blades of grass
for(GrassBlade g : lawn) {
g.render();
g.grow();
}
} // draw
It’s not hard to spot problems, e.g. the dandelions seem to grow too big too quickly. Plus, you can probably think of other features you might add to this simulation to make it more realistic or interesting. The simplest thing to try is to change the ranges for the various variables — the results can be quite intriguing.
Questions¶
- Add maximum height variables to the grass and dandelions that neither can grow beyond.
- Nothing grows forever. Modify the
grow
function ofGrassBlade
so that, eventually, the blade of grass stops growing bigger, and starts to get smaller and, at the same time, turn brown.
The Source Code¶
int numBlades = 500;
int numWeeds = 5;
float bladeGap; // distance between each blade of grass
// ranges for blades of grass
Range thicknessRange = new Range(1, 3);
Range lengthRange = new Range(1, 3);
Range angleRange = new Range(-25, 25);
Range greenRange = new Range(200, 256);
float grassBladeGrowthRate = 0.01;
// distance between blades of grass
Range gapRange = new Range(1, 3);
// ranges for dandelions
Range headRadiusRange = new Range(4, 7); // size of dandelion head
Range stemLengthRange = new Range(10, 15);
Range stemThicknessRange = new Range(3, 5);
Range weedPositionRange = new Range(10, 490);
float weedGrowthRate = 0.01;
ArrayList<GrassBlade> lawn;
ArrayList<Dandelion> weeds;
void setup() {
size(500, 500);
// initialize the grass blades
lawn = new ArrayList<GrassBlade>();
bladeGap = gapRange.randValue();
int i = 0;
while (i < numBlades) {
GrassBlade g = new GrassBlade(bladeGap * i, 250);
lawn.add(g);
++i;
}
// initialize the weeds
weeds = new ArrayList<Dandelion>();
i = 0; // no "int" because i was already defined above
while (i < numWeeds) {
Dandelion d = new Dandelion(weedPositionRange.randValue(), 250);
weeds.add(d);
++i;
}
} // setup
void draw() {
background(224, 255, 255);
// draw weeds
for (Dandelion d : weeds) {
d.render();
d.grow();
}
// draw blades of grass
for (GrassBlade g : lawn) {
g.render();
g.grow();
}
} // draw
///////////////////////////////////////////////////////////////////
class Dandelion {
float x, y;
float radius;
float stemLength;
float stemThickness;
float stemAngle;
color headColor;
color stemColor;
Dandelion(float x_init, float y_init) {
x = x_init;
y = y_init;
radius = headRadiusRange.randValue();
stemLength = stemLengthRange.randValue();
stemThickness = stemThicknessRange.randValue();
stemAngle = angleRange.randValue();
headColor = color(255, 255, 0);
stemColor = color(0, greenRange.randValue(), 0);
}
void render() {
pushMatrix();
translate(x, y);
rotate(radians(stemAngle));
stroke(stemColor);
strokeWeight(stemThickness);
line(0, 0, 0, -stemLength);
noStroke();
fill(headColor);
ellipse(0, -stemLength, 2 * radius, 2 * radius);
popMatrix();
}
void grow() {
if (random(0.0, 1.0) < weedGrowthRate) {
stemLength += 1;
}
}
} // class Dandelion
///////////////////////////////////////////////////////////////////
class GrassBlade {
float x, y;
float thickness;
float length;
float angle;
color clr;
float growthRate = 0.01;
GrassBlade(float x_init, float y_init) {
x = x_init;
y = y_init;
thickness = thicknessRange.randValue();
length = lengthRange.randValue();
angle = angleRange.randValue();
clr = color(0, greenRange.randValue(), 0);
}
void render() {
pushMatrix();
stroke(clr);
strokeWeight(thickness);
translate(x, y);
rotate(radians(angle));
line(0, 0, 0, -length);
popMatrix();
}
void grow() {
if (random(0.0, 1.0) < grassBladeGrowthRate)
length += 1;
}
} // class GrassBlade
class Range {
float lo, hi;
Range(float lo_init, float hi_init) {
lo = lo_init;
hi = hi_init;
}
float randValue() {
return random(lo, hi);
}
} // class Range