33. (Extra) 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.

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

33.2. Storing a Range of Numbers

In graphics and animation we often want to set variables to have values within a given range of values. For example, we might want a blade of grass to have a thickness between 1 and 4 pixels, or that its angle be between -15 and +15 degrees.

It’s also common to randomly select a value from a range when creating random objects, 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 you have a lot of variables and values to keep straight.

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. For instance, 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 + "]");

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

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

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

33.6. Questions

  1. Add maximum height variables to the grass and dandelions that neither can grow beyond.
  2. Nothing grows forever. Modify the grow function of GrassBlade so that, eventually, the blade of grass stops growing bigger, and starts to get smaller and, at the same time, turn brown.

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