Generative Typography

Recursion and 3D

Students will leave the session with the ability to create 3D sketches using WEBGL and p5.js and an understanding of the concept of Recursion.

Class Agenda

Class was canceled this week and the material was covered the following week.

Further Resources

Code Lecture

Recursion

Recursion is the concept of a function that calls itself.

function recurse() {
  // function code
  recurse();
}

recurse();

But in order for your program not to crash the recursive function needs a conditional to decide when to call itself.

function recurse() {
  if (condition) {
    recurse();
  } else {
    // stop calling recurse()
  }
}

recurse();

Let's look at an example of how this might differ from a loop. This code counts down from the number 10 to 1.

for (var number = 10; number > 0; number -= 1) {
  console.log(number);
}

And how would this work as a recursive function:

function countDown(number) {
  // Print the number
  console.log(number);

  // Call the function again
  if (number > 1) {
    countDown(number - 1);
  }
}

countDown(10);

Grid

Let's create a grid function using some code from our previous lectures.

function setup() {
  createCanvas(400, 400);
}

function grid(width, height, numberOfColumns, numberOfRows) {
  var cellWidth = width / numberOfColumns;
  var cellHeight = height / numberOfRows;

  for (var rowIndex = 0; rowIndex < 4; rowIndex += 1) {
    for (var columnIndex = 0; columnIndex < 4; columnIndex += 1) {
      // x position increases by cellWidth each loop
      var posX = cellWidth * columnIndex;
      // y position increases by cellHeight each loop
      var posY = cellHeight * rowIndex;

      rect(posX, posY, cellWidth, cellHeight);
    }
  }
}

function draw() {
  background(220);

  grid(width, height, 3, 3);
}

With the following we can now define a grid that divides a given area into a number of columns and rows. Let's update our grid function to also take coordinate positions

function grid(x, y, width, height, numberOfColumns, numberOfRows) {
  push();
  translate(x, y);

  var cellWidth = width / numberOfColumns;
  var cellHeight = height / numberOfRows;

  for (var rowIndex = 0; rowIndex < 4; rowIndex += 1) {
    for (var columnIndex = 0; columnIndex < 4; columnIndex += 1) {
      // x position increases by cellWidth each loop
      var posX = cellWidth * columnIndex;
      // y position increases by cellHeight each loop
      var posY = cellHeight * rowIndex;

      rect(posX, posY, cellWidth, cellHeight);
    }
  }

  pop();
}

With this addition we can now build up our recursive grid.

Recursion Grid

The recursiveGrid allows us to continue to further divide the grid using recursion.

function setup() {
  createCanvas(400, 400);
  noLoop();
}

function recursiveGrid(
  x,
  y,
  width,
  height,
  numberOfColumns,
  numberOfRows,
  numberOfRecursions
) {
  push();
  translate(x, y);

  var cellWidth = width / numberOfColumns;
  var cellHeight = height / numberOfRows;

  for (var rowIndex = 0; rowIndex < 4; rowIndex += 1) {
    for (var columnIndex = 0; columnIndex < 4; columnIndex += 1) {
      // x position increases by cellWidth each loop
      var posX = cellWidth * columnIndex;
      // y position increases by cellHeight each loop
      var posY = cellHeight * rowIndex;

      // If we have reached the end then draw the rectangle
      if (numberOfRecursions < 1) {
        if (rowIndex % 2 === 0) {
          rect(posX, posY, cellWidth, cellHeight);
        } else {
          ellipse(
            posX + cellWidth / 2,
            posY + cellHeight / 2,
            cellWidth,
            cellHeight
          );
        }
      } else {
        recursiveGrid(
          posX,
          posY,
          cellWidth,
          cellHeight,
          numberOfColumns,
          numberOfRows,
          numberOfRecursions - 1
        );
      }
    }
  }

  pop();
}

function draw() {
  background(220);

  recursiveGrid(0, 0, width, height, 2, 2, 1);
}

We can update the logic to make our recursion randomized.

function recursiveGrid(
  x,
  y,
  width,
  height,
  numberOfColumns,
  numberOfRows,
  numberOfRecursions
) {
  push();
  translate(x, y);

  var amountToSubtract = Math.floor(random(1, 4));
  var cellWidth = width / numberOfColumns;
  var cellHeight = height / numberOfRows;

  for (var rowIndex = 0; rowIndex < 4; rowIndex += 1) {
    for (var columnIndex = 0; columnIndex < 4; columnIndex += 1) {
      // x position increases by cellWidth each loop
      var posX = cellWidth * columnIndex;
      // y position increases by cellHeight each loop
      var posY = cellHeight * rowIndex;

      if (numberOfRecursions < 1) {
        if (rowIndex % 2 === 0) {
          rect(posX, posY, cellWidth, cellHeight);
        } else {
          ellipse(
            posX + cellWidth / 2,
            posY + cellHeight / 2,
            cellWidth,
            cellHeight
          );
        }
      } else {
        recursiveGrid(
          posX,
          posY,
          cellWidth,
          cellHeight,
          numberOfColumns,
          numberOfRows,
          numberOfRecursions - amountToSubtract
        );
      }
    }
  }

  pop();
}

// recursiveGrid(0, 0, width, height, 2, 2, 4);

3D and WEBGL

In p5.js, there are two modes: 2D and WEBGL. Both use the html canvas element, but when we use the WEBGL mode we can draw in both 2D and 3D. To enable WEBGL, specify it as the third parameter in the createCanvas() function.

function setup() {
  createCanvas(400, 400, WEBGL);
}

That's it!

The first thing to note about WEBGL mode is that you introduce a third dimension to your compositions: Z. Where before you were working on a (x, y) coordinate system there are now three dimensions and the coordinate system is now (x, y, z). The z-dimension is the axis that points toward you from the screen. The second thing to note is that instead of (0, 0, 0) being in the top-left corner like we are used to, it is not located in the middle of the canvas.

function setup() {
  createCanvas(400, 400, WEBGL);
}

function draw() {
  background(220);
  box();
}

Rotation and Translation

In WEBGL mode we unlock rotateZ and translateZ. You can still use rotate and translate as well and instead of rotate(x, y) with only 2 values you can also pass in the z value: rotate(x, y, z). The following code uses the translate function to move our origin 100 units to the right, 100 units down and 100 away from the screen.

function setup() {
  createCanvas(400, 400, WEBGL);
}

function draw() {
  background(220);

  box();

  // move the origin
  translate(100, 100, -100);
  box();
}
function setup() {
  createCanvas(400, 400, WEBGL);
}

function draw() {
  background(220);

  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  rotateZ(frameCount * 0.01);

  box();
}

Camera and Orbit Control

You can program the placement and angle of the camera in WEBGL mode and you can also turn over control to the user by using the orbitControl() function. It allows the user to control the sketch using their mouse and track pad.

function setup() {
  createCanvas(400, 400, WEBGL);
}

function draw() {
  background(220);

  orbitControl();
  box();
}

Shapes

There are other shapes in the reference under the 3D Primitives group.

function setup() {
  createCanvas(400, 400, WEBGL);
}

function draw() {
  background(220);

  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  rotateZ(frameCount * 0.01);

  // box();
  // sphere();
  cone();
}

Adding textToPoints

Let's step through how we can apply textToPoints to our new 3D mode.

var bebasFont;
var bounds;
var points = [];
var fontSize = 500;
var rotateAmt = 0;

function preload() {
  /**
   * Please note that this is a font hosted by another project of mine.
   * There is no guarantee that this url will function in the future.
   */
  bebasFont = loadFont("https://garnet.website/static/fonts/BebasNeue.ttf");
}

function setup() {
  createCanvas(500, 500, WEBGL);

  textFont(bebasFont);
  textSize(fontSize);
  stroke("white");
  fill("white");
  angleMode(DEGREES);

  // create the points and points
  points = bebasFont.textToPoints("A", 0, 0, fontSize);
  bounds = bebasFont.textBounds("A", 0, 0, fontSize);
}

function draw() {
  background(220);

  const halfBoundHeight = bounds.h / 2;
  const halfBoundWidth = bounds.w / 2;

  // DEBUG
  // push()
  // fill('red');
  // circle(0, 0, 20);
  // fill('pink');
  // rect(0, -bounds.h, bounds.w, bounds.h)
  // pop()

  rotateY(frameCount);
  translate(-halfBoundWidth, halfBoundHeight);

  points.forEach((pt) => {
    var gap = 20;

    push();
    // translate backwards
    translate(0, 0, -gap);
    circle(pt.x, pt.y, 10);
    pop();

    push();
    // translate forwards
    translate(0, 0, gap);
    circle(pt.x, pt.y, 10);
    pop();

    line(pt.x, pt.y, -gap, pt.x, pt.y, gap);
  });
}