Week 10: Glow Rush Game

Concept

This week, I set out to blend analog and digital elements into a single creative project, leading me to design a reaction game inspired by the “reaction lights” game by Lummic I stumbled upon a few months ago. In this game, players test their reflexes by pressing a button as soon as its corresponding light turns on.

I integrated a tricolor RGB light that serves as a real-time indicator of the player’s reaction time—green signals excellent speed, blue is moderate, and red denotes a slower response. To add a twist and enhance engagement, I included a potentiometer to adjust the game’s difficulty, effectively changing the time intervals that define the three color responses. This setup not only challenges players but also gives them control over the complexity of the task, making each round adaptable and uniquely challenging.

Images


Components Used

  1. Arduino Uno
  2. Tricolor RGB LED
  3. Pushbuttons (3)
  4. Resistors:
    1. 330 ohm resistors (3)
    2. 10k ohm resistors (3)
  5. Potentiometer
  6. Breadboard
  7. Jumper Wires

Circuit Setup

  1. RGB LED
    • The common cathode of the RGB LED connects to the Arduino’s ground.
      The red, green, and blue anodes are connected to PWM-capable digital pins (9, 10, and 11) through 330-ohm resistors to limit current.
  2. Pushbuttons
    • Each button is connected to one of the digital input pins (2, 3, and 4).
      The other side of each button is linked to the ground through a 10k-ohm resistor to ensure the pin reads LOW when the button is unpressed.
  3. Potentiometer
    • One outer pin connects to the 5V on the Arduino, and the other to the ground. The middle wiper pin is connected to analog input A0, allowing the Arduino to read varying voltage levels as the potentiometer is adjusted.

Video

Code

void setup() {
  for (int i = 0; i < 3; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
    pinMode(ledPins[i], OUTPUT);
  }

  // Set up RGB LED pins as outputs
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);

  Serial.begin(9600);
}


Input pins (buttonPins): Configured with INPUT_PULLUP to use the internal pull-up resistors, ideal for buttons.
Output pins (ledPins and RGB LED pins): Set as outputs to control LEDs.

void loop() {
  int potValue = analogRead(potPin); 
  // Adjust the max delay range for more sensitivity
  int maxDelay = map(potValue, 0, 1023, 5000, 2000); 
  Serial.print("Potentiometer Value: ");
  Serial.print(potValue);
  Serial.print(", Max Delay (ms): ");
  Serial.println(maxDelay);

  if (!gameActive) {
    gameActive = true;
    targetLed = random(0, 3);
    digitalWrite(ledPins[targetLed], HIGH);
    startTime = millis();
    Serial.print("Game started, respond before ");
    Serial.print(maxDelay);
    Serial.println(" ms for the best score!");
  }

  if (digitalRead(buttonPins[targetLed]) == LOW) {
    // Debounce by ensuring at least 50 ms has passed
    if (millis() - startTime > 50) { 
      unsigned long reactionTime = millis() - startTime;
      Serial.print("Reaction Time: ");
      Serial.println(reactionTime);

      setColorFromReactionTime(reactionTime, maxDelay);
      // Show result for 1 second
      delay(1000); 
      // Turn off RGB LED
      setColor(0, 0, 0); 
      digitalWrite(ledPins[targetLed], LOW);
      gameActive = false;
      delay(1000); 
    }
  }
}

There are 3 main steps in the loop function:

  1. Read Potentiometer: Determines the difficulty level by reading the potPin and mapping its value to define maxDelay, the maximum time allowed for a response.
  2. Game Control
    1. If gameActive is false, the game starts by picking a random LED to light up and marks the start time.
    2. If the corresponding button is pressed (digitalRead(buttonPins[targetLed]) == LOW), it checks for debouncing (to ensure the button press is genuine) and then calculates the reaction time.
  3. Serial Output: Outputs debug information such as potentiometer value and maximum delay time to the serial monitor.
void setColorFromReactionTime(unsigned long reactionTime, int maxDelay) {
  // Set RGB LED color based on reaction time as a fraction of maxDelay
  if (reactionTime < maxDelay / 5) {
    // Fast: Green
    setColor(0, 255, 0); 
  } else if (reactionTime < maxDelay / 2) {
    // Moderate: Blue
    setColor(0, 0, 255); 
  } else {
    // Slow: Red
    setColor(255, 0, 0); 
  }
}

It is based on the player’s reaction time, this function sets the color of the RGB LED:

  • Fast Response: Less than 1/5 of maxDelay, the LED turns green.
  • Moderate Response: Less than 1/2 but more than 1/5 of maxDelay, it turns blue.
  • Slow Response: Slower than 1/2 of maxDelay, it turns red.

Week 10: Reading Response

The main focus of Tom Igoe’s perceptive essay is the change from the artist serving as the exclusive storyteller to acting as a facilitator. Artists have historically used their creations to directly communicate a message or an emotion. On the other hand, interactive art plays a transforming function, whereby the artwork acts as a catalyst for the audience’s discovery. I like this transition as this aligns with the tenets of user experience design, which emphasize empowering users to choose their paths rather than trying to control them.

In traditional art, viewers often interpret a piece based on the artist’s description and the experience they want to show their viewers. Interactive art, however, offers a different dynamic. Each interactive piece is a canvas for numerous personal narratives, evolving with every user interaction. Here, the artwork doesn’t present a fixed story; instead, it allows for a multitude of stories to emerge, each shaped by individual interactions. The artist sets the framework, but it’s the participants who create their unique narratives through their engagement with the piece.

This is evident in modern interactive installations like “Rain Room,” where the experience of walking through a rainstorm without getting wet engages visitors in a unique conversation with the elements. I feel that the development of virtual reality environments adheres to these same principles that Igoe is proving in his argument. VR creators set the stage for experiences, but it is ultimately the users, through their actions and decisions, who navigate and mold these virtual worlds.

The text even highlighted that the user must be given hints or basic information about the interactive art piece. However, this made me ponder how the balance between guiding the audience and allowing freedom shapes the outcome of interactive art. Can there be too much or too little of either?

Week 9: Unusual Switch

Overview

In this assignment, I was tasked with designing a unique switch that doesn’t use hands for activation. I embraced the challenge with creativity, creating two distinct switches: a water detection switch and a bicep flexion switch.

Switch 1: Water Detection System with Visual Indicator

Concept

The concept behind this water detection system is rooted in the fundamental principle of electrical conductivity in water. Utilizing a simple circuit design with an Arduino, the project aims to detect the presence or absence of water through basic electronic components. When water connects two strategically placed wires, it completes a circuit, allowing current to flow, which the Arduino interprets to trigger a visual signal.

Inspiration

The inspiration for this project came from the need to monitor water levels or detect water presence in various situations, such as checking if a plant needs watering or preventing overflow in tanks.

Image

Components Used

  1.  Arduino Uno
  2. Green LED
  3. Red LED
  4. 330Ω Resistors (2)
  5. Jumper Wires
  6. Breadboard

How It Works

  • I connected the anode of each LED (green and red) through a 330Ω resistor to digital pins 13 and 12 on the Arduino, respectively. The cathodes were connected to the ground (GND).
  • I prepared two wires as water sensors by stripping a small section of insulation off each end. One wire was connected to digital pin 2 on the Arduino, and the other wire was connected to GND. These wires were then placed close to each other but not touching, ready to be submerged in water.
  • I wrote and uploaded the code to the Arduino that checks the electrical connection between the sensor wires. If water is present (completing the circuit), the green LED turns on. If the circuit is open (no water detected), the red LED illuminates.

Video

Switch 2: Bicep Switch

Concept

The goal was to create a device that could detect muscle flexion, particularly of the bicep, and provide immediate visual feedback.

The circuit setup and the code used are the same as the water detection switch.

Image

Components Used

  1.  Arduino Uno
  2. Green LED
  3. Red LED
  4. 330Ω Resistors (2)
  5. Jumper Wires
  6. Breadboard
  7. Cardboard Pieces
  8. Copper Tape

How It Works

When the bicep is flexed, the circuit completes, and the green LED lights up, indicating muscle activity. Conversely, when the muscle relaxes, the circuit breaks and the red LED turns on, indicating that the muscle is at rest.

Video

Week 8a: Reading Response

RESPONSE: Her Code Got Humans on the Moon—And Invented Software Itself

It is indeed reassuring to read about Hamilton’s journey and her crucial part in a momentous occasion in human history when one considers the strength of perseverance and innovation. It also draws attention to the structural obstacles that women in STEM areas have encountered and still face. This was even highlighted in the article, “It was 1960, not a time when women were encouraged to seek out high-powered technical work.” This article reminded me of the contributions of women to the first computer Electronic Numerical Integrator and Computer, ENIAC.

In the case of the ENIAC, it was programmed by six women: Kay McNulty, Betty Jennings, Betty Snyder, Marlyn Meltzer, Fran Bilas, and Ruth Lichterman. These women were integral to the operation and programming of the ENIAC, performing complex calculations and programming tasks that were critical to the computer’s success. Women’s entry into the industry was perceived as a means of freeing up males for more “skilled” work, as the field was not considered prestigious. Despite their essential roles and substantial technical accomplishments, their contributions were not widely recognized during their lifetimes, and the narrative of computing history often sidelined their efforts.

Question: What mechanisms are present today that either hinder or promote diversity and inclusion in tech and engineering?

RESPONSE: Emotion & Design: Attractive things work better

The theme that I could connect with was “the pleasure of use. As someone deeply invested in the intersection of design and user experience, I’ve always been captivated by products that deliver not just functionality but also the pleasure of use. Take, for example, the tactile feedback of a high-quality mechanical keyboard. Each keystroke produces a satisfying click, transforming the mundane task of typing into an enjoyable experience. It’s not just about the act of typing; it’s about how the device makes me feel while I’m using it.

true beauty in product design is multi-dimensional, integrating functionality, ease of use, and pleasure

Question: How can designers balance usability and aesthetics in product design, ensuring that neither aspect is compromised?

Midterm Project: Dungeon Maze

Concept

This dungeon-based maze game’s premise centers on an exciting and dynamic challenge where players are sent into several mazes that are produced by an algorithm, each with its own layout and set of coins and teleporters.

The game’s main goal, set against an engrossing dungeon backdrop, requires players to maneuver tactically through the maze, collecting gold coins while racing against the clock to locate the exit. The game uses teleporters as a game-changing element to enhance depth and complexity. Players can quickly go to different mazes, which resets the maze’s layout and offers unexpected twists. This makes the ‘Dungeon Master’ change their strategy.

In addition, an interesting limited vision feature simulates the real difficulty of traversing a dimly lit dungeon by preventing players from seeing beyond their immediate surroundings and forcing them to depend solely on memory and spatial awareness in order to advance. A fascinating and unpredictable journey that tests players’ intelligence and flexibility through the mix of random maze creation and other mechanisms like timers and limited vision, making every playthrough a unique experience.

Sketch

Snapshots of the Game

Elements of Pride

The choices made in the design of features like the drawLimitedVision effect, the generateRandomMaze function, and the adaptive design for fullscreen and windowed modes are a demonstration of good game designs and good technical decisions.

generateRandomMaze() {
    let maze = new Array(this.rows);
    for (let y = 0; y < this.rows; y++) {
      maze[y] = new Array(this.cols).fill('#');
    }
    const carvePath = (x, y) => {
      const directions = [[1, 0], [-1, 0], [0, 1], [0, -1]];
      this.shuffle(directions);

      directions.forEach(([dx, dy]) => {
        const nx = x + 2 * dx, ny = y + 2 * dy;
        if (nx > 0 && nx < this.cols - 1 && ny > 0 && ny < this.rows - 1 && maze[ny][nx] === '#') {
          maze[ny][nx] = ' ';
          maze[y + dy][x + dx] = ' ';
          carvePath(nx, ny);
        }
      });
    };

    const startX = 2 * Math.floor(Math.random() * Math.floor((this.cols - 2) / 4)) + 1;
    const startY = 2 * Math.floor(Math.random() * Math.floor((this.rows - 2) / 4)) + 1;
    maze[startY][startX] = ' ';
    carvePath(startX, startY);
    let coinCount = Math.floor(Math.random() * (12 - 8 + 1)) + 8; // 8 to 12 coins
    let teleporterCount = Math.floor(Math.random() * (2 - 1 + 1)) + 1; // 1 to 2 teleporters
    maze[this.rows - 2][this.cols - 2] = 'E';
    this.addRandomItems(maze, 'C', coinCount);
    this.addRandomItems(maze, 'T', teleporterCount);

    return maze;
  }

The generateRandomMaze function is a cornerstone of the game’s dynamic environment, creating a new, solvable maze for each session or level. The function uses a recursive backtracking algorithm, it selects a random starting point within the maze and carves out paths. This is done by moving two cells at a time in a chosen direction (to avoid creating isolated cells) and removing walls between cells to create a path. This process repeats recursively for each new cell reached that has all its surrounding cells intact.

Once the maze is generated, the function strategically places coins (‘C’) and teleporters (‘T’) within the maze. Coins act as collectibles, and teleporters serve as transport points to new mazes. An exit (‘E’) is placed at a predefined location, typically far from the start, ensuring that players must navigate through the maze to find it.

function drawLimitedVision(playerX,playerY,visibilityRadius) {

  // Draw a radial gradient from transparent to dark
  let radialGradient = drawingContext.createRadialGradient(playerX, playerY, 0, playerX, playerY, visibilityRadius);
  radialGradient.addColorStop(0, 'rgba(0,0,0,0)'); // Completely transparent at the center
  radialGradient.addColorStop(1, 'rgba(0,0,0,0.8)'); // More opaque towards the edges

  drawingContext.fillStyle = radialGradient;
  drawingContext.fillRect(0, 0, width, height);
}

The drawLimitedVision function enhances the game’s atmosphere by simulating a limited field of vision for the player, similar to carrying a light source in a dark dungeon. The function creates a radial gradient centered on the player’s current position. This gradient transitions from transparent (at the center) to opaque (at the edges) obscures parts of the maze not immediately near the player.

function calculateCellSize() {
  cellSize = min(width, height) / max(mazeColumns, mazeRows);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  calculateCellSize();
}

The code includes mechanisms to adapt to fullscreen and windowed modes. The calculateCellSize function dynamically calculates the size of each maze cell based on the current dimensions of the game window. This ensures that the maze scales correctly when switching between fullscreen and windowed modes, maintaining consistent gameplay and visual quality. The windowResized function listens for changes in the window size (including toggling fullscreen mode) and adjusts the canvas size accordingly.

Difficulties

One of the trickier parts of developing the game was actually implementing the limited vision functionality. Developing a smooth, radial-gradient of light that would dynamically follow the player’s motions and provide vision around them while keeping the remainder of the maze in complete darkness was the challenge. It took several tries to get this effect just right so that it seemed natural and didn’t interfere with gameplay by being too dark or changing too quickly.

There were further difficulties when trying to get the game to smoothly switch between fullscreen and regular window modes. Flexible design was necessary to make sure that the maze, player character, and user interface (UI) components all scaled and remained proportionate across a range of screen sizes and aspect ratios. This flexibility was essential to maintaining the game’s playability and graphic quality in any viewing mode.

Improvements

A possible area for enhancement is the visual design and user interface. For example, adding more complex visuals to the player’s character, treasures, and maze walls might greatly improve the game’s visual attractiveness. Furthermore, adding more variation to the maze layouts and obstacles—like traps or enemies—could add new and unique difficulties and amplify the action.

The project could also benefit from a more advanced level progression system, where the difficulty gradually increases through levels, possibly by enlarging the maze, decreasing the time limit, or increasing the required percentage of coins to collect before exiting.

 

Full Code

Midterm Project Progress (The Dungeon Maze)

MAZE GAME

Concept

The idea behind the game is that player must make their way through a sequence of difficult mazes using mathematical algorithms, each one generated randomly to provide a different challenge with each maze. The main objective of the game is for the player to navigate from the beginning point to the maze’s exit.

Gold coins are scattered throughout the maze and act as collectibles for players to find. This helps them to finish the game if they collect a certain number of gold coins. Also, 90% of the gold coins spawned on the maze must be collected for the player to exit the maze. However, they can use the teleporter without this condition.

There are teleporters placed in the maze that serve as an interesting twist to the game. Stepping upon a teleporter transports the player to a whole new maze, which changes the difficulty and forces them to adjust their approach on the fly.

Every maze in the game has a timer built into it, which creates a sense of urgency and tests the player’s ability to think quickly and make decisions under pressure. Also, the player has limited vision which makes it difficult for the player to plan their next move. This forces them to memorize their path to complete the maze.

Design 

The visual and thematic components of this maze game are designed to put the player in the shoes of a dungeon master.

The image is used for the background of the maze.

This is a snippet of the maze. The design is a work in progress as I am focusing on getting the basic functionality to work first. Currently, the big yellow is the player, the small bronze-colored circles are the gold coins, the blue circles are the teleporters, and the red square is the exit. I planning on replacing these with some avatars or icons.

Frightening Part

The most frightening part when I started to code was the main functionality of the game, The Maze. I wanted to have random mazes being generated every time the user plays the game or is teleported. So that it invokes a new sense of challenge.

The use of randomness and the cyclical structure of the path-carving process is what gives the procedure its complexity. Recursive functions include function calls that repeat back on themselves, they can be challenging to study and comprehend. I have

The code below shows how I tackled this issue:

function generateRandomMaze(rows, cols) {
  let maze = new Array(rows);
  for (let y = 0; y < rows; y++) {
    maze[y] = new Array(cols).fill('#');
  }

  function carvePath(x, y) {
    const directions = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    // Shuffle directions to ensure randomness
    shuffle(directions); 

    directions.forEach(([dx, dy]) => {
      const nx = x + 2 * dx, ny = y + 2 * dy;

      if (nx > 0 && nx < cols - 1 && ny > 0 && ny < rows - 1 && maze[ny][nx] === '#') {
        maze[ny][nx] = ' ';
        // Carve path to the new cell
        maze[y + dy][x + dx] = ' '; 
        carvePath(nx, ny);
      }
    });
  }

  // Randomize the starting point a bit more conservatively
  const startX = 2 * Math.floor(Math.random() * Math.floor((cols - 2) / 4)) + 1;
  const startY = 2 * Math.floor(Math.random() * Math.floor((rows - 2) / 4)) + 1;

  // Ensure starting point is open
  maze[startY][startX] = ' '; 
  carvePath(startX, startY);

  // Set exit
  maze[rows - 2][cols - 2] = 'E';

  // Place teleporters and coins after maze generation to avoid overwriting
  addRandomItems(maze, 'T', 3);
  addRandomItems(maze, 'C', 5);

  return maze;
}

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

The generateRandomMaze() function creates a random intricate maze by first populating it with walls and then utilizing the depth-first search algorithm to recursively find pathways. To ensure variation in the pathways formed, it shuffles possible directions (up, down, left, and right) starting from a randomly selected point. It improves the gameplay experience by slicing through the grid, placing an exit, and dispersing interactive features like coins and teleporters at random. The carvePath function’s use of recursion and randomness is essential to creating a maze that embodies the spirit of maze exploration and strategy and is both difficult and unique each time the game is played.

 

NOTE: I have used a lot of functions rather than classes. The functionality of the game is 80% achieved and from here on now I will change the functions to classes and complete 100% of the game/gameplay.

Reading Response: Computer Vision

I was interested in the Detecting Motion 9code listing 1) concept in this week’s reading. Frame differencing is a simple technique that may be used to detect and quantify motions of humans (or other objects) within a video frame.

The reading emphasizes how computer vision algorithms are not naturally capable of comprehending the subtleties of various physical surroundings, despite their advanced capacity to process and analyze visual input. The physical environments in which these algorithms function can greatly increase or decrease their effectiveness. The use of surface treatments like high contrast paints or controlled illumination like backlighting, for instance, is discussed in the reading as ways to enhance algorithmic resilience and performance. This suggests that the software and the real environment need to have a symbiotic connection in which they are both designed to enhance one another.

This concept reminded me of The Rain Room. In my opinion, this installation has motion sensors that act like computer vision that allow people to move through a space where it is raining everywhere but where they are standing. The computer vision system’s exact calibration with the actual environment is crucial to the immersive experience’s success because it allows the sensors to recognize human movement and stop the rain from falling on the people.

Reading Response: Design Principles in Everyday Objects

Don Norman’s refrigerator example from “The Design of Everyday Things” is a significant example that highlights several important design, user experience, and cognitive processes related to human-machine interaction concerns. The idea of cognitive dissonance in design, which occurs when users’ expectations and reality are distinct which causes annoyance and inefficiency, is embodied by the refrigerator’s confused controls. In my opinion, this example is a smaller issue of a much larger issue in the fields of design and technology.

The controls on the refrigerator expose a basic breakdown in the way designers and consumers communicate. The designers have a high in-depth knowledge of how the product works, but they often neglect what information users need to comprehend and utilize the product as intended. The belief that consumers will “figure it out” might result in designs that make sense to the designer but seem complex to the user.

After reading about the refrigerator it reminded me of “Computer Space” by Nolan Bushnell. This was one of the first arcade games which was made and did not conquer the market as the controls on the machine were way too complicated/complex for the user to comprehend. Although the vision of the game was intuitive, no one would want to read instructions to play a video game so the controls for the game became very hard for the users.

All of this made me wonder, how does the design of everyday objects influence our behavior and interactions with technology?

 

Assignment 4: Generative Text Output

Concept

The concept I aimed to replicate the captivating streams of 1s and 0s that are frequently shown in movies or GIFs, which represent the fundamentals of digital computing and communication. I wanted to achieve something similar to seeing a complex dance of binary digits, therefore I represented these streams with random characters.

Inspiration 

Sketch


Code

function draw() {
  background(0); 
  
  // Display and animate characters
  for (let x = 0; x < cols; x++) {
    for (let y = 0; y < rows; y++) {
      let yOffset = (millis() * speed + x * 50 + y * 50) % (height + 200) - 100;
      
      if (grid[x][y].bright) {
        // Set bright neon blue color for highlighted characters
        fill(0, 255, 255); 
      } else {
        // Set light neon blue color for other characters
        fill(0, 150, 255); 
      }
      
      text(grid[x][y].char, x * charSize, y * charSize + yOffset);
    }
  }
}

In the draw() method, I have a nested loop that iterates over each cell of the 2D grid array. The location of the characters are calculated and its appearance based on its coordinates and a time-based offset (yOffset). This offset is computed given the cell coordinates, a predetermined speed value, and the current value of millis(), which indicates the milliseconds since the sketch began operating.

if (grid[x][y].bright) {
        // Set bright neon blue color for highlighted characters
        fill(0, 255, 255); 
      } else {
        // Set light neon blue color for other characters
        fill(0, 150, 255); 
      }

Each character’s color is chosen according to the entry that corresponds to it in the grid array. fill() method applies a bright neon blue color to a cell if its bright attribute is set to true. Otherwise, ordinary characters are shown in a lighter neon blue hue.

Full Code

// 2D array to display the characters
let grid;
// the font size of the characters
let charSize = 20;
// columns and rows for the 2D array (grid)
let cols, rows;
// speed of the characters falling
let speed = 0.2;

function setup() {
  print(windowWidth,windowHeight)
  createCanvas(windowWidth, windowHeight);
  
  //creating the 2D array
  cols = floor(width / charSize);
  rows = floor(height / charSize);
  
  grid = create2DArray(cols, rows);
  //  initializing the characters font size 
  textSize(charSize);
  
  // Initialize grid with random characters
  for (let x = 0; x < cols; x++) {
    for (let y = 0; y < rows; y++) {
      grid[x][y] = {
        // calling the characters randomly      
        char: randomChar(),
        // Randomly determine if the character should be brighter or not
        bright: random(1) > 0.8 
      };
    }
  }
}

function draw() {
  background(0); 
  
  // Display and animate characters
  for (let x = 0; x < cols; x++) {
    for (let y = 0; y < rows; y++) {
      let yOffset = (millis() * speed + x * 50 + y * 50) % (height + 200) - 100;
      
      if (grid[x][y].bright) {
        // Set bright neon blue color for highlighted characters
        fill(0, 255, 255); 
      } else {
        // Set light neon blue color for other characters
        fill(0, 150, 255); 
      }
      
      text(grid[x][y].char, x * charSize, y * charSize + yOffset);
    }
  }
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  cols = floor(width / charSize);
  rows = floor(height / charSize);
  
  grid = create2DArray(cols, rows);
  
  // Reinitialize grid with random characters
  for (let x = 0; x < cols; x++) {
    for (let y = 0; y < rows; y++) {
      grid[x][y] = {
        char: randomChar(),
        // Randomly determine if the character should be brighter or not
        bright: random(1) > 0.8 
      };
    }
  }
}

// function to create the 2D array grid
function create2DArray(cols, rows) {
  let arr = new Array(cols);
  for (let i = 0; i < arr.length; i++) {
    arr[i] = new Array(rows);
  }
  return arr;
}

// function to generate a random character
function randomChar() {
  return String.fromCharCode(floor(random(65, 91)));
}

 

Challenges

The significant challenge was how to keep the characters’ animation in a fluid and synced motion along with the shifting grid placements while maintaining readability and visual consistency.

Another challenge was assigning different brightness levels to different characters. This required careful coordination to make sure the highlighted characters shone out without overshadowing other characters or creating visual clutter.

Improvements

There is one improvement that I would like to implement in the future. I would want to improve the animation algorithm’s efficiency that may result in more fluid and flawless visual transitions, particularly when working with bigger grids or faster animation rates.

Another improvement could be the inclusion of user interactivity. The user could disrupt the falling characters using mouse hover and the characters trying to get back into the original stream.

Assignment 3: State of Mind

Concept

The goal of this week’s assignment was to create a unique work of art through imagination and creativity. The original idea was to create a dynamic screensaver with a range of shapes that had different colors and speeds and interacted with the canvas and each other when they collided. However, this concept was disregarded because a more abstract art piece was needed.

So I used the original code as the template, and the project developed into a complex particle-mimicking display of forms. These particles used the idea of flow fields to mimic more organic and natural motions, leaving behind a vibrant trail as they moved. The chaos of these trails of particles is a representation of my “State of Mind” while thinking of ideas for the assignment.

Sketches


Code

The particles’ motion is directed by a grid of vectors called the flow field. Perlin noise is used in its generation to provide a fluid, flowing transition between the grid’s vectors.

for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      let index = i + j * cols;
      let angle = noise(i * 0.1, j * 0.1, zoff) * TWO_PI * 4;
      flowField[index] = p5.Vector.fromAngle(angle);
    }
  }

The snippet is in the draw() loop that creates the flow field. It simulates a natural flow by using Perlin noise to generate vectors with smoothly shifting angles.

The shapes follow the flow field vectors, which guide their movement. This interaction is encapsulated in the follow() method of the BaseShape class.

follow(flowField) {
  let x = floor(this.pos.x / resolution);
  let y = floor(this.pos.y / resolution);
  let index = x + y * cols;
  let force = flowField[index];
  this.vel.add(force);
  this.vel.limit(2);
}

The shape’s position (this.pos) is used to determine its current cell in the flow field grid by dividing by the resolution. The index in the flowField array corresponding to the shape’s current cell is calculated using x + y * cols.
The vector (force) at this index is retrieved from the flowField array and added to the shape’s velocity (this.vel), steering it in the direction of the flow. this.vel.limit(2) ensures that the shape’s velocity does not exceed a certain speed, maintaining smooth, natural movement.

The trail effect is created by not fully clearing the canvas on each frame, instead drawing a semi-transparent background over the previous frame. This technique allows shapes to leave a fading trail as they move.

background(0, 0.3);

The purpose of the resetAnimation() method is to reset the animation setup and clean the canvas. To restart the flow field pattern, it first uses clear() to remove any existing shapes from the canvas, resets the shapes array to its initial state, initializeShapes() method to add a new set of randomly placed shapes, and resets the zoff variable for flow field noise.

function resetAnimation() {
  clear();
  // Clear existing shapes
  shapes = []; 
  // Repopulate with new shapes
  initializeShapes(); 
  // Reset the z-offset for flow field noise
  zoff = 0; 
 
}

The resetAnimation() method is called when the frameCount has reached 500 frames. This helps to see how the flow field changes every time it restarts.

Full Code

// array for shapes
let shapes = [];
// declare flow field variable
let flowField;
let resolution = 20;
// 2d grid for flow field
let cols, rows;
// noise increment variable
let zoff = 0;
// make the sketch again after this value
let resetFrameCount = 500;


function setup() {
  createCanvas(800, 600);
  colorMode(HSB, 255);
  blendMode(ADD);
  cols = floor(width / resolution);
  rows = floor(height / resolution);
  flowField = new Array(cols * rows);
  initializeShapes(); 
}

function draw() {
  
  if (frameCount % resetFrameCount === 0) {
    resetAnimation();
  } else {
    background(0, 0.3); 
  }

  // Flow field based on Perlin noise
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      let index = i + j * cols;
      let angle = noise(i * 0.1, j * 0.1, zoff) * TWO_PI * 4;
      flowField[index] = p5.Vector.fromAngle(angle);
    }
  }
  
  // Increment zoff for the next frame's noise
  zoff += 0.01; 

  // Update and display each shape
  // For each shape in the array, updates its position according to the flow field, moves it, displays its trail, and display it on the canvas.
  shapes.forEach(shape => {
    shape.follow(flowField);
    shape.update();
    shape.displayTrail();
    shape.display();
    shape.particleReset();
    shape.finish();
  });

}

function resetAnimation() {
  clear();
  // Clear existing shapes
  shapes = []; 
  // Repopulate with new shapes
  initializeShapes(); 
  // Reset the z-offset for flow field noise
  zoff = 0; 
 
}

// Initialized 30 number of shapes in random and at random positions
function initializeShapes() {
  for (let i = 0; i < 30; i++) {
    let x = random(width);
    let y = random(height);
    // Randomly choose between Circle, Square, or Triangle
    let type = floor(random(3)); 
    if (type === 0) shapes.push(new CircleShape(x, y));
    else if (type === 1) shapes.push(new SquareShape(x, y));
    else shapes.push(new TriangleShape(x, y));
  }
}

class BaseShape {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D();
    this.size = random(10, 20);
    this.rotationSpeed = random(-0.05, 0.05);
    this.rotation = random(TWO_PI);
    this.color = color(random(255), 255, 255, 50);
    this.prevPos = this.pos.copy();
  }

  follow(flowField) {
    let x = floor(this.pos.x / resolution);
    let y = floor(this.pos.y / resolution);
    let index = x + y * cols;
    let force = flowField[index];
    this.vel.add(force);
    // Smoother movement so velocity is limited
    this.vel.limit(2);  
  }

  update() {
    this.pos.add(this.vel);
    this.rotation += this.rotationSpeed;
  }

  display() {
    // Saves the current drawing state
    push();
    // Translates the drawing context to the shape's current position
    translate(this.pos.x, this.pos.y);
    // Rotates the shape's current rotation angle    
    rotate(this.rotation);
    fill(this.color);
    noStroke();
  }

  displayTrail() {
    strokeWeight(1);
    // Creates a semi-transparent color for the trail. It uses the HSB 
    let trailColor = color(hue(this.color), saturation(this.color), brightness(this.color), 20);
    stroke(trailColor);
    // Draws a line from the shape's current position to its previous position
    line(this.pos.x, this.pos.y, this.prevPos.x, this.prevPos.y);
  }

  finish() {
    this.updatePrev();
    pop();
  }

  updatePrev() {
    this.prevPos.x = this.pos.x;
    this.prevPos.y = this.pos.y;
  }

  particleReset() {
    if (this.pos.x > width) this.pos.x = 0;
    if (this.pos.x < 0) this.pos.x = width;
    if (this.pos.y > height) this.pos.y = 0;
    if (this.pos.y < 0) this.pos.y = height;
    this.updatePrev();
  }
}

class CircleShape extends BaseShape {
  display() {
    super.display();
    ellipse(0, 0, this.size);
    super.finish();
  }
}

class SquareShape extends BaseShape {
  display() {
    super.display();
    square(-this.size / 2, -this.size / 2, this.size);
    super.finish();
  }
}

class TriangleShape extends BaseShape {
  display() {
    super.display();
    triangle(
      -this.size / 2, this.size / 2,
      this.size / 2, this.size / 2,
      0, -this.size / 2
    );
    super.finish();
  }
}

Challenges

I spent a lot of time getting a grasp of noise functions and how to use them to mimic natural movements for my shapes and implement the flow field using Perlin noise. There was considerable time spent adjusting the noise-generating settings to produce a smooth, organic flow that seemed natural.

There was a small challenge to clear the canvas after a certain amount of frames. The logic of the code was fine however the previous shapes were not removed.

Reflections and Improvements

The overall experience was quite helpful. I learned to handle the different functions and classes using object-oriented programming concepts. It made it possible to use a modular. It even helped me to add more features to my code as I was creating my art piece.

I believe that there is one area of development where I could explore various noise offsets and scales to create even more varied flow fields. Playing around with these parameters might result in more complex and eye-catching visual effects.

References

I learned about flow fields through the resources provided to us. I discovered a YouTube channel, “The Coding Train“.