Week 4: Data Visualization

Overview:

This week our task was to either make some sort of word art or create a data visualization. Pursing the latter, I decided the most relevant data that is out there right now is information relating to COVID-19. My goal was to create a color valued map, the most intuitive data visualization in my mind.

Process:

The trickiest part in my opinion was creating the map itself. To create shapes which could colored in processing I would have to create a shape through a series of points. At first I considered loading a map and the pixel values of the boarders by hand but I quickly realized how time consuming and inaccurate this would be when considering features such as rivers. I had a look at an SVG picture of the US and realized that to render the vector based image all the coordinates were already stored there by state. I then had to create a reader to take in this SVG path data, and convert this information to processing equivalents. I found what each svg command meant, C, L, M, z, and translated those into Processing shape functions.

With each path stored in a custom State object. I could easily draw a shape with a simple function call, and set the fill of it with a method.

I then had to take in my COVID data from the CDC, and process it to each state. Using the state codes that the data is associated with, I stored the respective data with the respective state in a table. This way when the state is drawn, it could easily find a value for a certain data point such as total cases.

With the data stored, I had to map it to a fill color. To do so I found the max value and min value of each type of data by iterating through the states. I then mapped a HSB saturation value from the min to the max. This gave me a range of colors from white to red, with the darkest red being the highest values.

I then used the min and max values and color mapping to create a scale that displayed is there was null data points, the min color, the max color, and the third and two third colors and their respective values. This gives viewers a sense of severity.

I then made this interactive where the right and left arrow keys allow users to toggle between data types such as deaths, cases, etc.

Results:

Here is my code and screenshots of the resulting maps.

 

//Variables for laoding state data file
String[] lines;  //Used to process svg file 

ArrayList<State> stateArray = new ArrayList<State>(); //Holds all states
ArrayList<Data> dataArray = new ArrayList<Data>();  //Holds data for states  

int col = 0;  //Which col of the dataArray is being shown

int xOffset, yOffset;

void setup() {
  size(1280, 720);
  lines = loadStrings("mapData.txt");
  loadStateData();
  loadCovidData();
  colorMode(HSB, 100);
}

void loadStateData() {
  int index = 0;
  //Loop through states
  while (index < lines.length) {
    int stringIndex = 0;
    String[] words = split(lines[index], ' ');  //Split each cord or 
    State newState = new State(words[stringIndex]);
    stringIndex++;
    //Loop through data and save into the state's table
    while (stringIndex < words.length) {
      //See what the command is
      if (words[stringIndex].equals("M")) {
        String[] points = split(words[stringIndex+1], ',');
        newState.saveSVGData(words[stringIndex], float(points[0]), float(points[1]));
        stringIndex += 2;   //For command and point pair
      } 
      else if (words[stringIndex].equals("L")) {
        String[] points = split(words[stringIndex+1], ',');
        newState.saveSVGData(words[stringIndex], float(points[0]), float(points[1]));
        stringIndex += 2;   //For command and point pair
      } 
      else if (words[stringIndex].equals("C")) {
        String[] pointOne = split(words[stringIndex+1], ',');
        String[] pointTwo = split(words[stringIndex+2], ',');
        String[] pointThree = split(words[stringIndex+3], ',');
        newState.saveSVGData(words[stringIndex], float(pointOne[0]), float(pointOne[1]), float(pointTwo[0]), float(pointTwo[1]), float(pointThree[0]), float(pointThree[1]));
        stringIndex += 4;   //For command and 3 point pairs
      } 
      else if (words[stringIndex].equals("z")) {
        newState.saveSVGData(words[stringIndex]);
        stringIndex += 1;   //For command and point pair
      } 
      else {
        stringIndex++;
        println("Error loading data");
      }
    }
    stateArray.add(newState);
    index++;
  }

  //Move US to center of screen (hardcoded cause translate doesnt work)
  xOffset = width/7;
  yOffset = height/15;
}

void loadCovidData() {  
  // Load CSV file into a Table objects
  Table covidTable = new Table();
  covidTable = loadTable("united_states_covid19_cases_and_deaths_by_state.csv", "csv");
  //Loop through data columns  
  for (int j = 1; j < 13; j++) {
    Table subTable = new Table();
    subTable.addColumn("State");
    subTable.addColumn("Data");
    //Loop through all states
    for (int i = 1; i < covidTable.getRowCount(); i++) {
      TableRow newRow = subTable.addRow();
      newRow.setString("State", covidTable.getRow(i).getString(0));
      //Check for no data points
      if (covidTable.getRow(i).getString(j).equals("null") || covidTable.getRow(i).getFloat(j) == 0) {
        newRow.setFloat("Data", -1);
      } 
      else {
        newRow.setFloat("Data", covidTable.getRow(i).getFloat(j));
      }
    }
    Data newDataPoint = new Data(covidTable.getRow(0).getString(j), subTable);  //Create new datapoint
    dataArray.add(newDataPoint);
  }
}

void draw() {
  background(0, 0, 100);
  //Draw states
  for (State state : stateArray) {
    float stateData = dataArray.get(col).returnData(state.getState());
    color stateColor = dataArray.get(col).returnColor(stateData);
    state.drawState(xOffset, yOffset, stateColor);
  }
  
  //Write title
  textAlign(CENTER);
  fill(0);
  textSize(50);
  text(dataArray.get(col).getHeader(), width/2, 50);
  
  //Draw scale
  dataArray.get(col).drawScale(width - width/5,height - height/8);
}


//Change what data is being displayed
void keyPressed() {
  if (keyCode == LEFT) {
    if (col == 0) {
      col = 11;
    } 
    else {
      col--;
    }
  }
  if (keyCode == RIGHT) {
    col = (col+1)%12;
  }
}
class Data {
  String header;  //What is the data of
  Table dataTable;  //Table of the data values with the state identifier 
  float min = -1;
  float max = -1;  //min and max val variables
  int scaleWidth = 50;  //how wide is the scale
  int hue = 1;  //HSB hue value, 0-100

  Data(String header, Table data) {
    this.header = header; //Data type
    dataTable = data;

    //Find max, min
    for (int i = 0; i < dataTable.getRowCount(); i++) {
      //If Max
      if (max == -1 || dataTable.getFloat(i, "Data") > max) {
        max = dataTable.getFloat(i, "Data");
      }
      //If min
      if (min == -1 || dataTable.getFloat(i, "Data") < min) {
        min = dataTable.getFloat(i, "Data");
      }
    }
  }

  //Returns float associated with state
  float returnData(String state) {
    return dataTable.matchRow(state, "State").getFloat("Data");
  }
  String getHeader() {
    return header;
  }
  //Return the mapped color
  color returnColor(float data) {
    //If no data
    if (data == -1) {
      colorMode(HSB, 100);
      return color(100, 100, 0);
    } else {
      colorMode(HSB, 100);
      float s = map(data, min, max, 0, 100);
      return color(hue, s, 100);
    }
  }

  //Draw the scale being used
  void drawScale(int x, int y) {
    colorMode(HSB, 100);
    //Draw null data
    fill(color(100, 100, 0));
    rect(x, y, scaleWidth, scaleWidth);
    //Draw min
    fill(returnColor(min));
    rect(x+scaleWidth, y, scaleWidth, scaleWidth);
    //Draw 1/3
    fill(returnColor(min+((max-min) * 0.333)));
    rect(x+scaleWidth*2, y, scaleWidth, scaleWidth);
    //Draw 2/3
    fill(returnColor(min+((max-min) * 0.666)));
    rect(x+scaleWidth*3, y, scaleWidth, scaleWidth);
    //Draw max
    fill(returnColor(max));
    rect(x+scaleWidth*4, y, scaleWidth, scaleWidth);

    //Text
    fill(color(100, 100, 0));
    textAlign(CENTER);
    textSize(10);
    text("No data", x+(scaleWidth/2), y + (1.2 * scaleWidth));
    text(int(min), x+(scaleWidth*1)+(scaleWidth/2), y + (1.2 * scaleWidth));
    text(int(min+((max-min) * 0.333)), x+(scaleWidth*2)+(scaleWidth/2), y + (1.2 * scaleWidth));
    text(int(min+((max-min) * 0.666)), x+(scaleWidth*3)+(scaleWidth/2), y + (1.2 * scaleWidth));
    text(int(max), x+(scaleWidth*4)+(scaleWidth/2), y + (1.2 * scaleWidth));
  }
}
class State {
  String code;  //2 Character code to identify state
  Table data = new Table();  //Table of svg command at points

  //Default constructor
  State() {
    code = "";

    //Populate table columns
    data.addColumn("Command");
    data.addColumn("x1");
    data.addColumn("y1");
    data.addColumn("x2");
    data.addColumn("y2");
    data.addColumn("x3");
    data.addColumn("y3");
  }  
  State(String code) {
    this.code = code;

    //Populate table columns
    data.addColumn("Command");
    data.addColumn("x1");
    data.addColumn("y1");
    data.addColumn("x2");
    data.addColumn("y2");
    data.addColumn("x3");
    data.addColumn("y3");
  }

  //Getter and setter for state code
  String getCode() {
    return code;
  }
  void setCode(String code) {
    this.code = code;
  }

  //Passed a stiring with an SVG command and series of points, processes them into the table, three methods store different amound of cords
  void saveSVGData(String command) {
    TableRow newRow = data.addRow();
    newRow.setString("Command", command);
  }
  void saveSVGData(String command, float x1, float y1) {
    TableRow newRow = data.addRow();
    newRow.setString("Command", command);
    newRow.setFloat("x1", x1);
    newRow.setFloat("y1", y1);
  }
  void saveSVGData(String command, float x1, float y1, float x2, float y2) {
    TableRow newRow = data.addRow();
    newRow.setString("Command", command);
    newRow.setFloat("x1", x1);
    newRow.setFloat("y1", y1);
    newRow.setFloat("x2", x2);
    newRow.setFloat("y2", y2);
  }
  void saveSVGData(String command, float x1, float y1, float x2, float y2, float x3, float y3) {
    TableRow newRow = data.addRow();
    newRow.setString("Command", command);
    newRow.setFloat("x1", x1);
    newRow.setFloat("y1", y1);
    newRow.setFloat("x2", x2);
    newRow.setFloat("y2", y2);
    newRow.setFloat("x3", x3);
    newRow.setFloat("y3", y3);
  }

  //Draw state from given SVG path data
  void drawState(int xOffset, int yOffset, color fillColor) {
    fill(fillColor);
    //Iterate through data table and draw shape based on symbols.
    for (TableRow row : data.rows()) {
      //Get command      
      //Move
      if(row.getString("Command").equals("M")){
        beginShape();
        vertex(row.getFloat("x1")+xOffset, row.getFloat("y1")+yOffset);
      }
      //Line
      else if(row.getString("Command").equals("L")){
        vertex(row.getFloat("x1")+xOffset, row.getFloat("y1")+yOffset);
      }
      //Bezier Curve
      else if(row.getString("Command").equals("C")){
        bezierVertex(row.getFloat("x1")+xOffset, row.getFloat("y1")+yOffset, row.getFloat("x2")+xOffset, row.getFloat("y2")+yOffset, row.getFloat("x3")+xOffset, row.getFloat("y3")+yOffset);
      }
      //End shape
      else if(row.getString("Command").equals("z")){
        endShape();
      }
    }
  }
  
  //getter for state name, no setter as wont change
  String getState(){
    return code;
  }
  
}

Data Distortion / Misrepresentation:

This data is all taken from the CDC so can be considered fairly reliable in my opinion. The issue with any data visualization is that misrepresentation is fairly easy. For starters, some states don’t report on the same data or with the same accuracy, hence why in the last image “Confirmed Deaths”, there are several states blacked out with no data points. Furthermore, color can easily distort the severity of a situation. With a simple mapping from the min to max, in the map “Death Rate per 100k in the Last 7 Days”, the state of Ohio looks much more severe than the rest of the country, when in reality its value is at 5, only slightly higher than most other states around 3. The type of data being displayed is also important. In the total case and total death maps, large and high population states appear far more severe. This may be in terms of quantity, but in reality per 100k is a much more useful metric. Would a city with 5 cases and 10 residents really be considered safer to be in that a city with 10 times as many cases but 1000 times as many residents?

 

Week 3: Object Oriented Game Ballz

Overview:

This weeks task was to apply modularity concepts for object oriented programming through the of classes in either a game or work of art. I decided to try my hand at making knockoff of a (what I though was simple) IOS game Ballz. In this game the player shoots off a series of balls that bounce off tiles until they are destroyed. The tiles must not reach the bottom of the screen otherwise the player loses.

Programming: 

To accomplish the object oriented process, I used two custom objects, the ball and the block, and then a central main runner with several assisting methods.

My first task was to simulate an IPhone screen. I did this by maximizing the height or width, and constraining the screen to a 13 by 6 aspect ratio. By only printing in this rectangle. I had to define new variables as the native height and width values would not be applicable to my project. I also needed a game board area within the simulated IPhone screen which had to be just as wide as the phone, but smaller in height as a perfect fit for 7 x 9 tiles. With this area defined, I made a method that would reprint my new phone shaped background.

I then had to determine stages of the game. There are two main stages. First the player aims the ball, and can see where the shot will go based on a preview of balls on the screen, and then the game progresses to where the balls are actually moving and bouncing off the tiles.

The first stage was much more simple. I used polar coordinates determined based on a starting location of the ball at the bottom of the screen, and the players clicked mouse to create a line of balls coming out of the source. I could then save the angle this created and use it for the next stage of the game.

The next stage was far more complicated. I had to manage several balls, blocks, angles, and values all at the same time. For each ball I had to tell if it was hitting a block, and if so where it would go next. I had to determine the interaction each ball had on a collision, as well as other game mechanics such as the ability to get more balls.

This took some time but using equations to calculate which edge of a box a ball had hit, I was able to simulate the ball bouncing off of a box.

Results:

Here is a link to a google drive with the executables as well as the code

https://drive.google.com/drive/folders/1jCDt0v6tsLXJFusUTlFkGKHPYW1y-drF?usp=sharing

Here is the code:

Main Class

//Inspired by IOS game Ballz
//Created by Cole Beasley for Intro to Interactive Media
//7-8/2/21

import java.util.Iterator;  // Import the class of Iterator

//Designed for IPhone X screen res of 13:6 ratio, will be full screnned contained by height 

//To do
/*
Initalize board, set squares to be 7 sqaures across, 7 down, plus empty row on top and bottom (9 vertical rows total)
 Initialize ball with size proportional 1/6th of square size
 Have a difficulty value
 
 Every round:
 Include one "new ball" square
 Random number of squares 1-7 show up with random values, higher values as rounds go on, color of box based on value of box
 
 On shot line up
 Draw line of darker balls to show where shot will go from mouse pull back
 */

//Global Variables
int phoneWidth;
int phoneHeight;

float gameBoardOriginX;
float gameBoardOriginY;
float gameBoardHeight;
float gameBoardWidth;

int timer = 0;
int savedTime = 0;

float gridSize;  //Square dimensions without padding
float blockPadding = 0.1; //What percent of a block should be left to padding around it, 0 will be no padding and blocks touch, 1 will be 100% padding and no block visible
float blockSize;  //Square dimensions minus padding
float ballSize;

//2D array containing current block elements
Block blocks[][] = new Block[9][7];
ArrayList<Ball> balls = new ArrayList<Ball>();

//Game control variables
boolean newRound = true;  //Should the squares advance
boolean gameOver = false;  //Did they lose
boolean activeRound = false;  //Is a round running is is someone aiming
boolean firstBallDone = false;  //Has a ball landed to set the starting pos for next round
float ballStartPosX;  //Where the launch will come from 
float initTheta;  //The initial theta that will be used as a reference
float ballSpeed = 10;  //how fast the ball is going (going faster increases hit errors)
int ballTimer = 100;  //How frequently the balls are released, 1 = 1ms)
boolean firstBall = true;  //Controller as to if the first ball has landed yet to set ballStartPosX

//Mouse control variables
boolean mouseDown = false;
int mouseOriginX;
int mouseOriginY;

//Game score
int gameScore = 1;
int highScore = 1;
int reserveBalls = 1;

//Ball Colors
color ballColor = color(200);
color ballPreviewColor = color(150);

void setup() {
  fullScreen();

  //Set "phone" screen to 13x6 aspect ratio (IPhone X)
  if (height / 13 > width / 6) {
    phoneWidth = width;
    phoneHeight = int((width / 6) * 13);
  } else {
    phoneHeight = height;
    phoneWidth = int((height / 13) * 6);
  }
  //Set basic variables defining game area width height and pos
  gameBoardWidth = phoneWidth;
  gameBoardHeight = (phoneWidth / 7) * 9.1666;
  gameBoardOriginX = (width/2) - (gameBoardWidth/2);
  gameBoardOriginY = (height/2) - (gameBoardHeight/2);
  //Set block sizes
  gridSize = gameBoardWidth / 7;
  blockSize = gridSize - (gridSize * blockPadding);
  ballSize = gridSize/6;
  ballStartPosX = width/2;
}


void draw() {
  //Draw the game board background
  gameBackground();

  //Check if it is a new round
  if (newRound) {
    advanceRound();
    newRound = false;
  }

  drawSquares();

  //Detect if game is over
  if (gameOver()) {
    noLoop();
  }

  //Print out game score, highscore
  textAlign(CENTER, BOTTOM);
  textSize(30);
  fill(255);
  text("Score " + gameScore, width/2, gameBoardOriginY);
  textAlign(LEFT, BOTTOM);
  textSize(20);
  text("Highscore: " + highScore, gameBoardOriginX, gameBoardOriginY);

  //If round is not yet running
  if (!activeRound) {
    //Draw ball at start pos
    fill(ballColor);
    noStroke();
    ellipse(ballStartPosX, gameBoardOriginY + gameBoardHeight - ballSize/2, ballSize, ballSize);

    //If mouse is down and in the board and below original click, draw preview balls
    if (mouseDown && mouseY > mouseOriginY && mouseInBounds()) {
      float initX = ballStartPosX;
      float initY = gameBoardOriginY + gameBoardHeight - ballSize/2;
      float w = mouseX - mouseOriginX;
      float h = mouseY - mouseOriginY;
      float theta = atan(h/w);
      //Adjust if in other quad
      if (theta > 0) {
        theta += PI;
      }
      initTheta = theta;
      float r = 50; //Distance between preview balls
      float x = r * cos(theta);
      float y = r * sin(theta);
      fill(ballPreviewColor);
      while ((initX + x - (ballSize/2) > gameBoardOriginX && initX + x + (ballSize/2) < gameBoardOriginX + gameBoardWidth) && (initY + y - (ballSize/2) > gameBoardOriginY && initY + y + (ballSize/2) < gameBoardOriginY + gameBoardHeight)) {
        pushMatrix();
        translate(initX, initY);
        ellipse(x, y, ballSize, ballSize);
        x = r * cos(theta);
        y = r * sin(theta);

        //Adjust conditions in statement
        initX += x;
        initY += y;

        popMatrix();
      }
    }
  }
  //Round is running
  else {
    //Spawn in more balls if there are some still and the timer has passed half a second
    timer = millis() - savedTime;
    if (reserveBalls > 0 && timer >= ballTimer) {
      savedTime = millis();  //Reset timer for future ball spawns
      balls.add(new Ball(ballColor, ballStartPosX, gameBoardOriginY + gameBoardHeight - ballSize/2, initTheta, ballSize));
      reserveBalls--;
    }
    //Draw balls and advance their pos
    Iterator<Ball> ballIterator = balls.iterator();
    while (ballIterator.hasNext()) {
      Ball ball = ballIterator.next(); // must be called before you can call i.remove()

      //Move the ball
      Block hitBlock = ball.moveBall(ballSpeed, blocks, gameBoardOriginX, gameBoardOriginX + gameBoardWidth, gameBoardOriginY, gameBoardOriginY + gameBoardHeight, gridSize);

      //See if a block was hit
      if (hitBlock != null) {
        //See if it is a ball increase block
        if (hitBlock.freeBall) {
          gameScore++;
          blocks[hitBlock.gridY(ball.y, gridSize, gameBoardOriginY)][hitBlock.gridX(ball.x, gridSize, gameBoardOriginX)] = null;
        } else {
          hitBlock.decrease();
          //See if block was destroyed
          if (hitBlock.strength <= 0) {
            blocks[hitBlock.gridY(ball.y, gridSize, gameBoardOriginY)][hitBlock.gridX(ball.x, gridSize, gameBoardOriginX)] = null;
          }
        }
      }
      //Check to see if any balls should be removed 
      if (ball.y > gameBoardOriginY + gameBoardHeight) {
        //If it is the first ball to be removed, set the next rounds start position to its x pos
        if (firstBall) {
          firstBall = false;
          ballStartPosX = ball.x;
        }
        ballIterator.remove();
      } else {
        ball.drawBall();
      }
    }
    //Check to see if the round ended
    if (balls.size() == 0 && reserveBalls == 0) {
      newRound = true;
      activeRound = false;
      reserveBalls = gameScore;
      if (gameScore > highScore) {
        highScore = gameScore;
      }
    }
  }
}

//Acts like background function, but draws for aspect ratio of phone
void gameBackground() {
  //translate to center of screen
  pushMatrix();
  translate(width/2, height/2);

  //draw main game background dark gray
  rectMode(CENTER);
  noStroke();
  fill(30);
  rect(0, 0, phoneWidth, phoneHeight);

  //draw board area
  fill(20);
  //Game board width is defined as the max width, gameboard ratio is 9.166/7 height to width
  rect(0, 0, gameBoardWidth, gameBoardHeight);
  popMatrix();
}


//When called advances the round, moves all the blocks down a row and creates a new row in the block matrix
void advanceRound() {
  //First move each square down a row
  for (int i = 8; i > 0; i--) {
    for (int j = 0; j < 7; j++) {
      blocks[i][j] =  blocks[i-1][j];
    }
  }
  //Clear first row
  for (int i = 0; i < 7; i++) {
    blocks[1][i] = null;
  }
  //Generate new row
  for (int i = 0; i < 7; i++) {
    //random chance for new block to be added
    if (random(1) > 0.5) {
      //Get random strength valuse roughly twice current score
      int strength = int(random(0.8, 2) * gameScore) + 1;
      blocks[1][i] = new Block(strength);
    }
  }
  //Set the one block which gives you a ball
  Block ballBlock = new Block();
  ballBlock.freeBall = true;
  blocks[1][int(random(0, 7))] = ballBlock;

  firstBall = true;
}

//Draw the squares
void drawSquares() {
  for (int i = 0; i < 9; i++) {
    for (int j = 0; j < 7; j++) {
      if (blocks[i][j] != null) {
        blocks[i][j].drawBlock(gameBoardOriginX + (gridSize/2) + (j*gridSize), gameBoardOriginY + (gridSize/2) + (i*gridSize), blockSize);
      }
    }
  }
}

//See if game is over
boolean gameOver() {
  for (int i = 0; i < 7; i++) {
    if (blocks[8][i] != null) {
      //Print game over
      textAlign(CENTER, CENTER);
      textSize(50);
      fill(255);
      text("Game Over =(", width/2, height/2);
      return true;
    }
  }
  return false;
}

//Mouse processing
void mousePressed() {
  //Make sure not in round and not yet counted as clicked
  if (!activeRound && !mouseDown) {
    mouseDown = true;
    mouseOriginX = mouseX;
    mouseOriginY = mouseY;
  }
}
void mouseReleased() {
  //Make sure not in round
  if (!activeRound) {
    mouseDown = false;
    //Start round if mouse pos Y is below origin y
    if (mouseY > mouseOriginY) {
      activeRound = true;
      savedTime = millis() - ballTimer;
    }
    mouseOriginX = 0;
    mouseOriginY = 0;
  }
}

//Check to see if mouse is on board
boolean mouseInBounds() {
  if ((mouseX > gameBoardOriginX && mouseX < gameBoardOriginX + gameBoardWidth) && (mouseY > 0 && mouseY < height)) {
    return true;
  }
  return false;
}

Ball Class

class Ball {

  boolean directionY = true; //true = down
  boolean directionX = true; //true = right
  float incrementX = 0;
  float incrementY = 0;
  float theta;
  float ballSize;
  float x = 0;
  float y = 0;
  color colorValue;

  Ball(color colorVal, float x, float y, float theta, float ballSize) {
    colorValue = colorVal;
    this.x = x;
    this.y = y;
    this.theta = theta;
    this.ballSize = ballSize;
  }

  void drawBall() {
    ellipseMode(CENTER);
    fill(colorValue);
    noStroke();
    ellipse(x, y, ballSize, ballSize);
  }

  void moveBall(float speed) {
    incrementX = speed * cos(theta);
    incrementY = speed * sin(theta);
    x += incrementX;
    y += incrementY;
  }

  Block moveBall(float speed, Block blocks[][], float minX, float maxX, float minY, float maxY, float gridSize) {
    incrementX = speed * cos(theta);
    incrementY = speed * sin(theta);
    //If ball has bounced into a vertical wall
    if (checkTopBound(incrementY, minY)) {
      y = minY + ballSize/2;
      incrementY = 0;
      theta *= -1; //Adjust theta
    }
    //Check horizontal walls
    if (checkHorizontalLeftBound(incrementX, minX)) {
      x = minX + ballSize/2;
      incrementX = 0;
      //Adjust theta
      if (theta >= 0)
        theta = PI - theta;
      else
        theta = -PI - theta;
    }
    if (checkHorizontalRightBound(incrementX, maxX)) {
      x = maxX - ballSize/2;
      incrementX = 0;
      //Adjust theta
      if (theta >= 0)
        theta = PI - theta;
      else
        theta = -PI - theta;
    }

    //Check for a box collision
    Block hitBlock = checkBoxCollision(blocks, minX, minY, gridSize);
    
    x += incrementX;
    y += incrementY;
    
    if(hitBlock != null){
      return hitBlock;
    }
    return null;
  }

  //Check vertical bounds of game area
  boolean checkTopBound(float incrementY, float minY) {
    //Check if gone off top
    if ((y + incrementY - ballSize/2) < minY) {
      return true;
    }
    return false;
  }
  //Check walls of game area
  boolean checkHorizontalRightBound(float incrementX, float maxX) {
    //Check for too far right
    if ((x + incrementX + ballSize/2) > maxX) {
      return true;
    }
    return false;
  }
  boolean checkHorizontalLeftBound(float incrementX, float minX) {
    //Check if gone off left
    if ((x + incrementX - ballSize/2) < minX) {
      return true;
    }
    return false;
  }

  //Check to see if a box has been hit, return box that was hit if hit
  Block checkBoxCollision(Block blocks[][], float minX, float minY, float gridSize) {
    //First get which grid the ball is in and which grid it will be in
    int initGridX = int((x - minX)/gridSize);
    int initGridY = int((y - minY)/gridSize);
    int newGridX = int(((x + incrementX) - minX)/gridSize);
    int newGridY = int(((y + incrementY) - minY)/gridSize);

    //Check to see if ball has gone off the bottom
    if (initGridY == 9) {
      initGridY = 8;
    }
    if (newGridY == 9) {
      newGridY = 8;
    }
    //Check to see if moving to a different grid
    if (initGridX != newGridX || initGridY != newGridY) {
      //Check to see if new grid has a block
      if (blocks[newGridY][newGridX] != null) {
        //Check to see which edge has been hit first
        //Edge values
        float leftEdge = minX + (newGridX * gridSize);
        float rightEdge = minX + ((newGridX+1) * gridSize);
        float topEdge = minY + (newGridY * gridSize);
        float bottomEdge = minY + ((newGridY+1) * gridSize);
        //Which edges have been hit
        boolean hitLeft = false;
        boolean hitRight = false;
        boolean hitTop = false;
        boolean hitBottom = false;

        //Check if vertical slope first
        if ((x+incrementX)-x != 0) {
          //Slope of line between oringal point and collision point
          float m = ((y+incrementY)-y)/((x+incrementX)-x);

          //Check which two edges have been hit
          //Top edge
          if (((topEdge-y)/m)+x > leftEdge && ((topEdge-y)/m)+x < rightEdge) {
            hitTop = true;
          }
          //Bottom Edge
          if (((bottomEdge-y)/m)+x > leftEdge && ((bottomEdge-y)/m)+x < rightEdge) {
            hitBottom = true;
          }
          //Left edge
          if (m*(leftEdge-x)+y < bottomEdge && m*(leftEdge-x)+y > topEdge) {
            hitLeft = true;
          }
          //Right edge
          if (m*(rightEdge-x)+y < bottomEdge && m*(rightEdge-x)+y > topEdge) {
            hitRight = true;
          }

          //Calculate which of the two is closest
          if (hitRight && hitLeft) {
            //println("1");
            if (dist(x, y, leftEdge, y) < dist(x, y, rightEdge, y)) {
              hitRight = false;
            } else {
              hitLeft = false;
            }
          } else if (hitRight && hitTop) {
            //println("2");
            if (dist(x, y, rightEdge, y) < dist(x, y, x, topEdge)) {
              hitTop = false;
            } else {
              hitRight = false;
            }
          } else if (hitRight && hitBottom) {
            //println("3");
            if (dist(x, y, rightEdge, y) < dist(x, y, x, bottomEdge)) {
              hitBottom = false;
            } else {
              hitRight = false;
            }
          } else if (hitLeft && hitTop) {
            //println("4");
            if (dist(x, y, leftEdge, y) < dist(x, y, x, topEdge)) {
              hitTop = false;
            } else {
              hitLeft = false;
            }
          } else if (hitLeft && hitBottom) {
            //println("5");
            if (dist(x, y, leftEdge, y) < dist(x, y, x, bottomEdge)) {
              hitBottom = false;
            } else {
              hitLeft = false;
            }
          } else if (hitTop && hitBottom) {
            //println("6");
            if (dist(x, y, x, bottomEdge) < dist(x, y, x, topEdge)) {
              hitTop = false;
            } else {
              hitBottom = false;
            }
          }
        }
        //Vertical slope
        else {
          //println("7");
          if (dist(x, y, x, bottomEdge) < dist(x, y, x, topEdge)) {
            hitBottom = true;
          } else {
            hitTop = true;
          }
        }

        //Determine if the block being hit is actually the free ball block
        if(!blocks[newGridY][newGridX].freeBall){
          //Transform theta if hitting vertal side
          if (hitLeft || hitRight) {
            //Adjust theta
            if (theta >= 0)
              theta = PI - theta;
            else
              theta = -PI - theta;
          }
          //Transform theta if hitting horizontal side
          else {
            theta *= -1; //Adjust theta
          }
        }
      }
      return blocks[newGridY][newGridX];
    }
    return null;
  }
}

 

Block Class

class Block {
  color colorValue;
  int strength;
  color colorArray[] = {color(#efb92b), color(#c0c038), color(#82b54a), color(#c6654c), color(#dd3a4d), color(#df1375), color(#1f76ba), color(#17998c)};
  boolean freeBall = false;  //Is this a block that when you hit it you get a ball?

  Block() {
    colorValue = color(int(random(0, 255)), int(random(0, 255)), int(random(0, 255)));
    strength = 10;
  }  

  //Block constructor that takes in a strength value and associates a color based on this
  Block(int strenth) {
    this.strength = strenth;
    colorValue = getColor(strength);
  }

  //Draw the block, parameters are: x position, y position: height of side
  void drawBlock(float x, float y, float sideLength) {
    if (!freeBall) {
      pushMatrix();
      translate(x, y);
      rectMode(CENTER);
      noStroke();
      fill(colorValue);
      rect(0, 0, sideLength, sideLength);
      fill(0);
      textAlign(CENTER);
      textSize(20);
      text(strength, 0, 10);
      popMatrix();
    } else {
      pushMatrix();
      translate(x, y);
      ellipseMode(CENTER);
      noStroke();
      fill(255);
      ellipse(0, 0, sideLength/4, sideLength/4);
      fill(0);
      ellipse(0, 0, sideLength/6, sideLength/6);
      popMatrix();
    }
  }

  //Decrease value and color, called when a ball hits it
  void decrease() {
    strength--;
    colorValue = getColor(strength);
  }

  //Get its grid x value, typically 0-6
  int gridX(float x, float grid, float minX) {
    return int((x - minX)/grid);
  }

  //Get its grid x value, typically 0-8
  int gridY(float y, float grid, float minY) {
    return int((y - minY)/grid);
  }

  //Computes the color of block based on its strength
  color getColor(int val) {
    //Yellow
    if (val <= 5){
      return colorArray[0];
    }
    //Olive
    else if(val > 5 && val <= 10){
      return colorArray[1];
    }
    //Green
    else if(val > 10 && val <= 15){
      return colorArray[2];
    }
    //Orange
    else if(val > 15 && val <= 20){
      return colorArray[3];
    }
    //Red
    else if(val > 20 && val <= 30){
      return colorArray[4];
    }
    //Pink
    else if(val > 30 && val <= 50){
      return colorArray[5];
    }
    //Blue
    else if(val > 50 && val <= 100){
      return colorArray[6];
    }
    //Teal
    else{
      return colorArray[7];
    }
  }
}

 

Some screenshots from the game

 

Problems:

There are several aspects of the game I am not quite happy with. For starters, the way Processing deals with movement is by stepping a shape a certain number of pixels. This is problematic as if a step is too large, it could skip a series of pixels where other shapes lie and as a result a collision is not detected. This is observed in my game when a ball hits a corner of a box, it often behaves as if the box is not there. This Ould be fixed with more rigorous calculations, but would also make the game more resource dependent as every grid would have to be checked for every ball which could quickly get out of hand.

I would also like to implement the ability to restart the round, but I ran out of time. In theory this would be easy as variables would just have to be reset and the game board cleared.

Cole Beasley – Loop Art

Overview:

This week’s task was to make some sort of artwork from the types of loops we had learned in class this week (for() and while()). To give us some inspiration, we were given some magazines on early forms of computer generated art coming out of the 1980s and 90s. Google searches yielded lots of examples to choose from in addition. Many were reminiscent of old Windows screen savers, and often featured bright colors. My favorite of several were the fractals that came up. Some were fairly simple, and others were complex using tricky mathematical formulas. I decided to go ahead and try one that pushed me to understand hard mathematical concepts such as imaginary numbers, and see if I could translate to Java.

Mandelbrot Set:

The Mandelbrot Set is a set of complex numbers that for each complex number, ‘c‘, does not diverge when the value for ‘z’ is iterated in the below formula.

This math was tricky for me as it involved the revolution around a point in a complex plane. Luckily this topic is well documented and this page had great explanations and even a sudocode algorithm to get me started in plotting the phenomena.

I was able to convert the code into similar Java code, and then make changes so that processing could do what it needed.

Problems:

Besides complex math formulas, some problems that were tricky to overcome included converting values to colors on a scale to iterate through with a loop, and a self created extra challenge of animating a zoom functionality to better demonstrate the fractal’s impressive effects.

To translate the value of the algorithm to color in processing, I had to use map() to take a wide range of numbers and make it understandable to a color value. I also decided to use HSB color as it would allow me to use a single variable that could be iterated for hue, rather than three separate values for RGB. This was new to me and took some experimentation to get right.

For zooming, it took me a long time to figure out how to scale to a certain coordinate in the plane, while not distorting in the width or height directions. Eventually I derived the formula on line 38,39 of the code below which involves taking the width and height times a zoom factor with the correct offsets.

Code:

The code for this took several attempts to get right. The initial variables defined are mostly for the animation of the zoom, but also include adjustable parameters for the formulas. I utilized the timeout algorithm which provides a good approximation if a number is or is not in the set. This value could be adjusted to make to the depiction more detailed, at the expense of computational difficulty. I then initialize a workspace with a 1.333 aspect ratio, and set the color mode to HSB for the reasons described previously. I then begin looping. Using nested for loops I iterate over each pixel in the graphic and determine its timeout value using the above mathematical process in a loop. With the timeout value ranging from 0-timeout max value, with 1000 being the one used in this instance, these values need to map to a hue. Since anything above the max timeout value is considered part of the set, they are assigned black, otherwise the value is mapped reversely onto a range from red to violet.

//PSEUDOCODE TAKEN FROM WIKIPEDIA ESCAPE TIME ALGO
//x0 := scaled x coordinate of pixel (scaled to lie in the Mandelbrot X scale (-2.5, 1))
//y0 := scaled y coordinate of pixel (scaled to lie in the Mandelbrot Y scale (-1, 1))
//x := 0.0
//y := 0.0
//iteration := 0
//max_iteration := 1000
//while (x*x + y*y ≤ 2*2 AND iteration < max_iteration) do
//  xtemp := x*x - y*y + x0
//  y := 2*x*y + y0
//  x := xtemp
//  iteration := iteration + 1
    
//color := palette[iteration]
//plot(Px, Py, color)


int WIDTH;
int HEIGHT;
int maxIteration = 10000;  //level of detail
float zoom = 0;  //Can't zoom much past 100,000
float xOffset = 0.004; //(-1 - 1) (-1 zooms in on far left edge, 1 on far right)
float yOffset = -.1; //(-1 - 1) (-1 zooms in on top edge, 1 on bottom)   (0.004, -0.1 is an interesting cord set)

void setup() {
  size(1066, 800);
  WIDTH = width;
  HEIGHT = height;
  colorMode(HSB, 360);
}

void draw() {
  background(0);
  for (int i = 0; i < WIDTH; i++) {
    for (int j = 0; j < HEIGHT; j++) {
      //float x0 = map (i, (zoom*WIDTH/2), (WIDTH-((WIDTH/2)*zoom)), -2.5, 1);
      //float y0 = map (j, (zoom*HEIGHT/2), (HEIGHT-((HEIGHT/2)*zoom)), -1, 1);
      float x0 = map (i, (-WIDTH*zoom)-(xOffset*zoom*WIDTH), (zoom*WIDTH)+WIDTH-(xOffset*zoom*WIDTH), -2.5, 1);
      float y0 = map (j, (-HEIGHT*zoom)-(yOffset*zoom*HEIGHT), (zoom*HEIGHT)+HEIGHT-(yOffset*zoom*HEIGHT), -1, 1);
      float x = 0.0;
      float y = 0.0;
      int iteration = 0;
      while(x*x + y*y <= 2*2 && iteration < maxIteration){
        float xtemp = x*x - y*y + x0;
        y = 2*x*y + y0;
        x = xtemp;
        iteration++;
      }
      if(iteration == maxIteration){
        stroke(0, 0, 0);
      }
      else{
        float hue = map(iteration, maxIteration, 0, 0, 255);
        stroke(hue, 360, 360);
      }
      point(i, j);
    }
  }
  if(zoom == 100000){
    noLoop();
  }
  if(zoom == 0){
    zoom++;
  }
  //println("ran");
  zoom = zoom * 10;
}

Results:

I am happy with how these images came out. It took a lot of time and tweaking of the code but the results were as I was hoping. I played with certain variables such as level of detail at higher levels, and quickly ran out of memory to work with. At high details the variables for the math become so large that they can’t really be stored or have no rounding errors.

Other Recursion Tests:

I enjoyed making this example of a fractal and went forward trying more simple images using polar coordinates and recursion. This one did not use any loops however so it does not count directly for this assignment, just a fun example. I played with some randomization to try and make it look more realistic too.

 

//Recursion parameters
int maxIterations = 9; //Number of iterations

//Tree parameters
int angle = 30; //Angle the branches spread;
float treeStroke = 5; //Starting stroke line
float treeStrokeShrink = 0.5; //Fraction lost every time
float percentDestroy = 1.0; //0-1 how much shorter the branches get with each iteration, 1 is 100%, 0 is no chance
float shorteningPercent = 0.75; // 0-1, How much shorter the branches get
float branchLength;

//Drawing variables

void setup() {
  size(1000, 800);
  branchLength = height/4;
}

void draw() {
  drawTree(0, maxIterations, width/2, height, branchLength, 270, treeStroke);
}

// Iteration that its on, max iterations, x, y, length of line, stroke size
void drawTree(int i, int m, float x, float y, float l, float o, float s){
  translate(x,y);
  
  // Convert polar to cartesian
  float x1 = l * cos(radians(o));
  float y1 = l * sin(radians(o));
  
  // Draw the line and set stroke
  strokeWeight(s);
  line(0, 0, x1, y1);
  
  //Recursion
  if(i < m){
    float randOne = random(0,1);
    if(randOne < percentDestroy){
      pushMatrix();
      drawTree(i + 1, m, x1, y1, l*shorteningPercent, o+angle, treeStroke*treeStrokeShrink);
      popMatrix();
    }
    float randTwo = random(0,1);
    if(randTwo < percentDestroy){
      drawTree(i + 1, m, x1, y1, l*shorteningPercent, o-angle, treeStroke*treeStrokeShrink);
    }
  }
  noLoop();
}

 

Conclussions:

I enjoyed this weeks tasks and pushed myself to try something new to me. It made the assignment more challenging and more rewarding when it finally worked out the way I was hoping. The variables and iterative programming allowed me tot week the code and write minimal amounts once I had an algorithm that worked.

 

 

A Self-Portrait Using Processing

Overview:

For this first assignment, I tried to utilize the principles learned in class to best create a portrait of myself using Processing. This involved the use of  Processing’s 2D Primitive shapes, including quads, triangles, arcs, lines and ellipses. I tried to stay within the bounds of what we have done thus far, but made extensive use of the basic coding principles we have covered including the use of variables to accomplish scaling and parameter specification.

Process:

For this assignment I took the warm up exercise we finished in class and continued on to make it an entire portrait. I first chose which shapes I wanted to utilize to compose a feature such as a nose or a mouth, and placed them in a 640×480 canvas. Once I was happy with how the face looked at this point I replaced everything with variables that could be easily adjusted to tweak certain features such as size or placement, as well as make it easily scalable to any 4×3 resolution just by changing the canvas size.

Code: 

In my code I made sure to divide everything out into clear sections with comments so that I could easily find and tweak features as needed. This allowed me to easily manipulate what I had already done. You will note that I initialize all my variables before running anything. These parameters define the face and could easily be tweaked to make any sort of face, not just one that looks like me! This gives futures possibilities for a UI that would allow a simple character creature.

Once all my parameters were defined I went ahead and created a larger 4×3 canvas, set certain position variables, and then continued on to draw my face with the parameters defined initially. I took advantage of how Processing layers shapes to create features such as the shirt, neck line, and ears with far less math by letting other features simple draw over the top as necessary instead of needing more complex shapes.

//Position variables
int centerX;
int centerY;
float faceScale = 1;

//Face Parameters
float faceWidth = 200;
float faceHeight = 250;

float neckHeight = 330; //arc from center
float neckWidth = 100; //arc from center

float pocketHeight = 80;
float pocketWidth = 40;
float pocketPosX = 60;  //relative to center of face
float pocketPosY = 180;  //relative to center of face

float earHeight = 0;  //0 is center of face
float earLength = 20;
float earWidth = 10;

float eyeScale = 1;
float eyeHeight = 20;  //0 is center of face
float eyeDistance = 30; //0 is pupil aligned with center

float mouthHeight = 30;  //0 is not center, center or curve
float mouthWidth = 100;
float mouthThickness = 110;

float hairLine = 70; // 0 is center
float hairHeight = 60;
float hairSweep = -30;
float sideBurnDepth = 20;
float sideBurnHeight = 60;

void setup() {
  size(640, 480); //!!Maintain 4x3 for right aspect ratio!! (640, 480), (1280, 960) etc..
  //size(1280, 960); //!!Maintain 4x3 for right aspect ratio!! (640, 480), (1280, 960) etc..
  centerX = width/2;
  centerY = height/2;
  faceScale = width/640;
}

void draw() {

  background(255);  //white
  //RGB
  stroke(0);

  //line(centerX, centerY, width, height); //draw line from center to bottom right corner

  //Shirt
  fill(#006994);  //Shirt Color
  triangle(centerX, centerY, centerX - faceScale*160, height, centerX + faceScale*160, height);
  fill(255, 233, 208);
  arc(centerX, centerY, faceScale*neckWidth, faceScale*neckHeight, 0, PIE, CHORD);//neckline
  fill(159,226,191);
  arc(centerX + faceScale*pocketPosX, centerY + faceScale*pocketPosY, faceScale*pocketWidth, faceScale*pocketHeight, 0, PIE, CHORD);//Pocket

  //Ears
  fill(255, 233, 208);
  ellipse(centerX - faceScale*faceWidth/2, centerY + faceScale*earHeight, faceScale*earWidth, faceScale*earLength);
  ellipse(centerX + faceScale*faceWidth/2, centerY + faceScale*earHeight, faceScale*earWidth, faceScale*earLength);
  

  //Face
  fill(255, 233, 208);
  ellipse(centerX, centerY, faceScale*faceWidth, faceScale*faceHeight); //(width, height, diameter)

  //EYES
  //Eyelash
  noFill();
  arc(centerX + faceScale*eyeDistance, centerY - faceScale*eyeHeight - 8, eyeScale*faceScale*30, eyeScale*faceScale*15, PI+QUARTER_PI, TWO_PI-QUARTER_PI);
  arc(centerX - faceScale*eyeDistance, centerY - faceScale*eyeHeight - 8, eyeScale*faceScale*30, eyeScale*faceScale*15, PI+QUARTER_PI, TWO_PI-QUARTER_PI);
  noStroke();
  //White Part
  fill(255);
  ellipse(centerX + faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*30, eyeScale*faceScale*15); //(width, height, diameter)
  ellipse(centerX - faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*30, eyeScale*faceScale*15); //(width, height, diameter)
  // Blue eyes
  fill(0, 0, 255);
  ellipse(centerX + faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*15, eyeScale*faceScale*15); //(width, height, diameter)
  ellipse(centerX - faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*15, eyeScale*faceScale*15); //(width, height, diameter)
  //Outline to see pupil
  fill(255);
  ellipse(centerX + faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*6, eyeScale*faceScale*6); //(width, height, diameter)
  ellipse(centerX - faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*6, eyeScale*faceScale*6); //(width, height, diameter)
  //Pupil
  fill(0);
  ellipse(centerX + faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*5, eyeScale*faceScale*5); //(width, height, diameter)
  ellipse(centerX - faceScale*eyeDistance, (centerY - faceScale*eyeHeight), eyeScale*faceScale*5, eyeScale*faceScale*5); //(width, height, diameter)
  
  
  //Nose
  stroke(0);
  line(centerX, centerY, centerX + faceScale*20, centerY + faceScale*30);
  line(centerX, centerY + faceScale*30, centerX + faceScale*20, centerY + faceScale*30);
  
  //Mouth
  stroke(191, 112, 104);
  fill(255);
  strokeWeight(faceScale*8);
  arc(centerX, centerY + faceScale*mouthHeight, faceScale*mouthWidth, faceScale*mouthThickness, QUARTER_PI, PI-QUARTER_PI, CHORD);
  strokeWeight(faceScale*1);
  
  //Hair
  noStroke();
  fill(227,204,136);
  quad(centerX + faceScale*faceWidth/2, centerY - faceScale*hairLine, centerX - faceScale*faceWidth/2, centerY - faceScale*hairLine, centerX - faceScale*faceWidth/2 + faceScale*hairSweep, centerY - faceScale*hairLine - faceScale*hairHeight, centerX + faceScale*faceWidth/2 + faceScale*hairSweep, centerY - faceScale*hairLine - faceScale*hairHeight);
  triangle(centerX + faceScale*faceWidth/2, centerY - faceScale*hairLine, centerX + faceScale*faceWidth/2 - faceScale*sideBurnDepth, centerY - faceScale*hairLine, centerX + faceScale*faceWidth/2, centerY - faceScale*hairLine + faceScale*sideBurnHeight);
  triangle(centerX - faceScale*faceWidth/2, centerY - faceScale*hairLine, centerX - faceScale*faceWidth/2 + faceScale*sideBurnDepth, centerY - faceScale*hairLine, centerX - faceScale*faceWidth/2, centerY - faceScale*hairLine + faceScale*sideBurnHeight);
}

 

Going Forward:

As mentioned previously, there is a lot more that could be done with this project. The goal was to use simplistic shapes and drawing techniques to make the best portrait we could. Using future techniques such as loops I would create more complex shapes such as eye lashes, or control statements to create a UI. This would all allow greater customizability overall.