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

27.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 will be quite simple, but still effective. We’ll think of a blade of grass as a line segent with a random height, thickness, colour, and orientation.

27.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, for example:

float lowValue = -2.43;
float highValue = 8.04;
float r = random(lowValue, highValue);

The problem with this 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 and high values 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 initLo, float initHi) {
        lo = initLo;
        hi = initHi;
    }

    float randValue() {
        return random(lo, hi);
    }
}

This class is simple but useful. It reduces the number variables we need to represent a range. For instance, the example above may be re-written to use Range:

Range rng = new Range(-2.43, 8.04);
float r = rng.randValue();

We can also access the low and high values if we need then, e.g.:

Range rng = new Range(-2.43, 8.04);
println("rng = [" + rng.lo + ", " + rng.hi + "]");

27.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, colour, and rage of growth:

// ... Range class here

Range thicknessRange = new Range(1, 3);
Range lengthRange = new Range(1, 3);
Range angleRange = new Range(-25, 25);
Range greenRange = new Range(200, 255);

class GrassBlade {
    float x, y;
    float thickness;
    float length;
    float angle;
    color grassColor;

    float growthRate;

    GrassBlade(float initX, float initY) {
        x = initX;
        y = initY;

        thickness = thicknessRange.randValue();
        length = lengthRange.randValue();
        angle = angleRange.randValue();
        grassColor = color(0, greenRange.randValue(), 0);
    }

    // a blade of grass is drawn as a line
    void render() {
        pushMatrix();
            stroke(grassColor);
            strokeWeight(thickness);
            translate(x, y);
            rotate(radians(angle));
            line(0, 0, 0, -length);
        popMatrix();
    }

    void grow() {
        if (random(0.0, 1.0) < growthRate) {
            length++;
        }
    }
}

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.

27.4. Dandelions

We will think of a dandelion as a stem and a circular “flower” on one end:

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 initX, float initY) {
        x = initX;
        y = initY;
        radius = radiusRange.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;
        }
    }
}

27.5. A Growing Lawn

Finally, we need code to draw the lawn. For this program, a lawn is a collection of grass blades and dandelions:

ArrayList<GrassBlade> lawn;
float bladeGap;

ArrayList<Dandelion> weeds;

int numBlades = 500;
int numWeeds = 5;

Range gapRange = new Range(1, 3);

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();
    }

}

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

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