Tutorial 6: Drawing with Randomness

Randomness

To produce variations on a design we need some way to change the values of variables used in the drawing process. One way to do this is to use a "random number generator", which typically uses a chaotic mathematical function to generate a sequence of pseudo-random numbers that follow an unpredicatable sequence (unless you have detailed knowledge of the algorithm that it uses and know the precise starting conditions). In Processing, the random() function is used to create unpredictable values within a range:

    random(high)
    random(low, high)
      // Assign f a random float value from 0 to 5.2
      float f = random(5.2); 
       
      // Try to assign a float to an int
      int i = random(5.2); // ERROR!
       
      // Assign j an int value from 0 to 5
      int j = int(random(5.2));

      Random Drawings

      We can use the random() function to very quickly write a simple sketch that is drawn differently each time it is run. The following sketch draws 5 lines, each of them is drawn from the left to the right-hand side of the display window, i.e., from 0 to width on the x-axis, and from a randomly chosen vertical position on the left to another randomly chosen vertical position on the right.

      • Show Sketch
      /** @peep sketchcode */
      size(200, 200);
      smooth(); 
      strokeWeight(20); 
      stroke(0, 130); 
      line(0, random(height), width, random(height)); 
      line(0, random(height), width, random(height)); 
      line(0, random(height), width, random(height)); 
      line(0, random(height), width, random(height)); 
      line(0, random(height), width, random(height));

      Alter the above program to draw bezier curves using the random() function to determine the y-coordinate of the four control points in much the same way that the sketch above uses the random() function to determine the y-coordinates of the end points of each line. The end result should look something like this:

      Random Bezier Curves

      Random Loops

      Using random() within a for structure is an easy way to generate lots of random numbers for drawing complex forms. The following example demonstrates how we can draw many, almost parallel, lines using random() to produce an interesting texture where the lines cross.

      • Show Sketch
      /** @peep sketchcode */
      size(200, 200);
      background(0); 
      stroke(255,60); 
      for (int i = 0; i < width; i++) { 
        float r = random(width/10); 
        strokeWeight(r);
        float offset = r * 5.0;
        line(i - width/5, height, i+offset, 0); 
      }

      Modify the above example to draw ellipses with random radii like the following:

      Random Ellipses

      The next example uses one for loop embedded within another to draw a grid of randomly sized squares. The size of each square is determined using the call to random(10, 40), meaning that each square will be between 10 and 40 pixels in size.

      • Show Sketch
      /** @peep sketchcode */
      size(200, 200);
      background(0); 
      fill(255,60); 
      noStroke();
      rectMode(CENTER);
      for (int x = 20; x < width; x += 20) { 
        for (int y = 20; y < height; y += 20) { 
          float r = random(20, 40);
          rect(x, y, r, r);
        }
      }

      Change the above code to experiment with the range of values that random() produces by changing the values for the low and high parameters. Observe the changes to the appearance of the sketch with relatively small changes in the parameter settings.

      Random Tests

      To use random values to determine the flow of the program, you can place the result of running the random() function in a relational expression used as a test in an for statement.

      • Show Sketch
      /** @peep sketchcode */
      size(200, 200);
      for (int y = 0; y < height; y += 20) {
        int w = int(random(width)) + 1; 
        for (int x = 0; x < w; x += 4) {
          line(x, y, x, y + 20); 
        }
      }

      We can also use the result of calling random() in conditional tests, i.e., if statements. For example, the following sketch builds on the examples of drawing repeating patterns using loops and conditionals from the previous tutorial. It uses random() instead of a calculation based on the % operator to determine which way to draw a diagonal line. This results in a non-repeating pattern:

      • Show Sketch
      /** @peep sketchcode */
      size(200, 200); 
      smooth();
      for (int y = 20; y <= height - 20; y += 10) {
        for (int x = 20; x <= width - 20; x += 10) {
          if (random(1) < 0.5) {
            line(x-5, y+5, x+5, y-5);
          } else {
            line(x-5, y-5, x+5, y+5);
          }
        } 
      }

      The type of conditional test using random() illustrated above can be thought of a simulating a coin toss, with "heads" being a value less than 0.5 and "tails" being a value greater than or equal to 0.5.

      Using the tests based on random() to choose between two drawing options, develop a sketch that draws differently at every point on a grid.

      Experiment with using different values in the random test, for example, how does the drawing of the grid change when you change the test from 50/50 to 80/20?

      Based on the first of the sketches drawing non-repeating patterns above, can you draw the following pattern by varying the probablility of choosing to draw in one direction rather than the other:

      Experiment 1Experiment 2

      Experiment with using the values of x and/or y to vary the random test. For example, can you reproduce the following designs that vary the choice of line orientation horizontally and vertically by testing random(200) against the current x or y position:

      VerticalHorizontal

      Notice how the majority of the lines at one side of each of the above images is in the opposite direction from those at the other side.

      Using Multiple Tests to Draw Shapes

      The following sketch uses two tests, and calls random() twice for each point in the regular grid to determine where to draw two diagonal lines at 90 degrees to each other.

      • Show Sketch
      /** @peep sketchcode */
      size(200, 200);
      for (int y = 20; y <= height-20; y += 10) {
        for (int x = 20; x <= width-20; x += 10) {
          if (random(1) < 0.5) {
            line(x, y, x+5, y-5); 
          } else {
            line(x, y, x+5, y+5); 
          }
          if (random(1) < 0.5) {
            line(x, y, x-5, y-5); 
          } else {
            line(x, y, x-5, y+5); 
          }
        }
      }

      Experiment with different shapes that can be drawn at different grid points. For example, can you produce interesting grid-like patterns using dots or Bezier curves:

      Circle Random GridBezier Random Grid

      Seeding Random Numbers

      It's sometimes desirable to include unpredictable numbers in your programs but to force the same sequence of numbers each time the program is run. The randomSeed(value) function is the key to producing such numbers. The value parameter must be an int. Use the same value parameter in a program each time it is run to force the same random numbers to be produced in the same order. Using randomSeed() allows use to produce the same picture each time the program is run. The following sketch will create the same drawing every time it is run because the random seed has been fixed.

      • Show Sketch
      /** @peep sketchcode */
      int s = 6; // Change this seed value to change the drawing
      randomSeed(s);
      size(200, 200);
      background(0); 
      stroke(255,60); 
      for (int i = 0; i < width; i++) { 
        float r = random(width/10); 
        strokeWeight(r);
        float offset = r * 5.0;
        line(i - width/5, height, i+offset, 0); 
      }

      Experiment with the above sketch by copying it to the editor and changing the value of s. Notice that when you don't change s between runs you get the same drawing.

      Noise

      Values produced using the random() function can be hard to control because each number is generated is independent from the previous. The noise() function is a more controllable way to create unexpected values. The noise() function uses the Perlin Noise technique, developed by Ken Perlin. Originally used for simulating natural textures through subtle irregularities, Perlin Noise is also used for generating shapes and realistic motion. The noise() function works by interpolating between random values to create smoother transitions than the numbers returned from random(). The noise function has between one and three parameters:

        noise(x) 
        noise(x, y) 
        noise(x, y, z)

        Different versions of the noise() function sample a space of noise values with 1, 2 or 3 dimensions:

        noise(x) creates random numbers based on a 1-dimensional space (i.e., an imaginary line) that can be mapped to drawing lines on the screen, etc.

        noise(x, y) creates random number based on sampling a 2-dimensional space (i.e., an imaginary plane) that can be used for generating 2D textures.

        noise(x, y, z) creates random number number based on sampling a 2-dimensional space that can be used for generating 3D shapes, textures or animated 2D textures.

        The following sketch is an example of using the 1-dimensional noise() function:

        • Show Sketch
        /** @peep sketchcode */
        size(400, 100); 
        float inc = 0.1; 
        noStroke(); 
        fill(0); 
        noiseSeed(0); 
        for (int x = 0; x < width; x += 4) { 
          float n = noise(x * inc) * 70.0; 
          rect(x, 10 + n, 3, 20); 
        }

        Experiment with the above sketch in the editor by changing the value for inc and observing how this changes the smoothness of the line of rectangles being drawn.

        The next example illustrates how to generate a 2D texture using the noise() function to ensure that it varies smoothly over small distances.

        • Show Sketch
        /** @peep sketchcode */
        float inc = 0.04; 
        size(200, 200);
        for (int y = 0; y < height; y++) { 
          for (int x = 0; x < width; x++) { 
            float gray = noise(x*inc, y*inc) * 255;
            stroke(gray); 
            point(x, y); 
          } 
        }

        Experiment with the value of inc in this example to see how it affects the smoothness of the texture generated.

        Turbulence

        Diverse textures can be created using noise() in collaboration with sin(). The following example deforms a regular sequence of bars created with sin() into a texture reminiscent of those found in nature. The power variable sets the amount the texture deforms from the lines and the density parameter d sets the granularity of the texture.

        • Show Sketch
        /** @peep sketchcode */
        float power = 6; // Turbulence power 
        float d = 16; // Turbulence density 
        size(200, 200);
        noiseSeed(0);
        for (int y = 0; y < height; y++) { 
          for (int x = 0; x < width; x++) { 
            float turbulence = 0.0; 
            for (float i = d; i >= 1; i = i/2) { 
              turbulence += power * noise(x/d, y/d) * i/d;
            }
            float gray = abs(sin(x*0.05 + y*0.03 + turbulence)) * 255;
            stroke(gray); 
            point(x, y); 
          } 
        }

        Experiment with the values for power and d in the above sketch to explore the space of possible textures that can be created.

        Using Randomness and Noise

        The ability to generate, constrain and control random and noisy values can be powerful tools for any designer exploring the space of possible designs using code. However, too many random values, or too much noise, can detract from the design itself. Compare the following examples.

        The first sketch shows how a regular grid can be distorted to add visual interest using the subtle application of noise(). The result suggests that the design may be for a regular grid but drawn by an unsteady hand.

        • Show Code
        /** @peep sketch */
        size(200, 200);
        smooth();
        float s = 4;
        noFill();
        for (int y = 20; y <= height-20; y += 10) {
          float ny = y * 0.04;
          beginShape();
          for (int x = 20; x <= width-20; x++) {
            float nx = x * 0.04;
            vertex(x + s*(noise(nx,ny)-0.5), y + s*(noise(ny,nx)-0.5));
          }
          endShape();
        } 
        for (int x = 20; x <= width-20; x += 10) {
          float nx = x * 0.04;
          beginShape();
          for (int y = 20; y <= height-20; y++) {
            float ny = y * 0.04;
            vertex(x + s*(noise(nx,ny)-0.5), y + s*(noise(ny,nx)-0.5));
          }
          endShape();
        }

        The second sketch uses much the same code except that the visual effect is that the second grid is dominated by the noisy distortion field and loses much of the sense of the underlying grid design.

        • Show Code
        /** @peep sketch */
        size(200, 200);
        smooth();
        float s = 40;
        noFill();
        for (int y = 20; y <= height-20; y += 10) {
          float ny = y * 0.04;
          beginShape();
          for (int x = 20; x <= width-20; x++) {
            float nx = x * 0.04;
            vertex(x + s*(noise(nx,ny)-0.5), y + s*(noise(ny,nx)-0.5));
          }
          endShape();
        } 
        for (int x = 20; x <= width-20; x += 10) {
          float nx = x * 0.04;
          beginShape();
          for (int y = 20; y <= height-20; y++) {
            float ny = y * 0.04;
            vertex(x + s*(noise(nx,ny)-0.5), y + s*(noise(ny,nx)-0.5));
          }
          endShape();
        }

        The only difference between these two sketches is that the scale factor for the noise used to offset the grid in the second is 30 while in the first it is 3. From the perspective of design, less (noise) can be much more.

        Exercises

        1. Use three variables assigned to random values to create a composition that is different every time the program is run.
        2. Create a composition using a for structure and random() to make a different dense composition every time the program is run.
        3. Use noise() and noiseSeed() to create the same irregular shape every time a program is run.

        Comments