Final project – Hamdah AlSuwaidi

The project’s concept:

The allure of a jukebox, with its nostalgic charm and tangible interaction with music selection, inspires a unique blend of past and present in your project. This modern reinterpretation of the classic jukebox isn’t just about listening to music—it’s an experiential dive into the ritual of choosing sounds from different eras and styles, echoing the tactile joy that came from flipping through vinyl records or pressing the physical buttons on a jukebox. Your project revives this delightful sensory interaction by blending physical buttons and digital outputs, allowing users to actively engage with the music rather than passively streaming playlists. It embodies a revival of the golden age of jukeboxes but with a contemporary twist, using today’s technology to recreate a piece of the past that resonates with both nostalgia and the new digital era.

The decision to incorporate a variety of music channels such as English, Classical, and Al Khalidiya channels suggests a celebration of diversity and the rich tapestry of global music culture. It reflects a yearning to bring the world closer together through the universal language of music, wrapped in the classic format of a jukebox. This project does more than just play music; it invites users to journey through different cultures and time periods at the push of a button. It’s a bridge between generations and geographies, enabling a shared experience that is both educational and entertaining, which is likely what sparked the idea to reinvent the jukebox for a modern audience. This blend of educational purpose and entertainment, rooted in technological innovation, makes your jukebox project a meaningful nod to the past while eagerly embracing the future of interactive media.

P5js code:

// Initial state settings
let loading = true;
let channels = [[], [], []]; // Arrays for storing songs by channel: 0 - English, 1 - Classical, 2 - Al Khalidiya
let numberOfSongs = 0;
let numberOfSongsLoaded = 0; 
let coinSound; // Sound effect for the coin insert
let selectedChannel; // Currently selected channel
let playing = false; // Is a song currently playing?
let songToPlay; // Current song playing

// Preload function to load sounds and other resources before the program starts
function preload() {
  soundPaths = loadStrings("soundFileNames.txt"); // Load list of sound file names
  coinSound = loadSound("sounds/coin.mp3"); // Load coin sound effect
}

// Setup function to initialize the environment
function setup() {
  createCanvas(600, 400);
  textAlign(CENTER, CENTER);

  // Loop through the sound paths and assign them to channels based on file names
  for (let i = 0; i < soundPaths.length; i++) {
    let words = soundPaths[i].split("_");
    let channel = words[words.length - 2] + "_" + words[words.length - 1]; // Determine the channel from the file name
    switch (channel) {
      case "english_channel.mp3":
        channels[0].push("sounds/" + words.join("_"));
        break;
      case "classical_channel.mp3":
        channels[1].push("sounds/" + words.join("_"));
        break;
      case "khalidiya_channel.mp3":
        channels[2].push("sounds/" + words.join("_"));
        break;
    }
  }
  numberOfSongs = soundPaths.length; // Total number of songs loaded
  // Load each song in the channels array
  for (let i = 0; i < channels.length; i++) {
    for (let j = 0; j < channels[i].length; j++) {
      channels[i][j] = loadSound(channels[i][j], () => {
        numberOfSongsLoaded += 1; // Increment the count of loaded songs
      });
    }
  }
  ratioPos = { x: width / 2, y: height * 2 };
  selectedChannel = floor(random(3)); // Randomly select a channel to start
  fft = new p5.FFT(); // Initialize Fast Fourier Transform for audio visualization
}

// Draw function to continuously execute and render the canvas
function draw() {
  background(40);
  if (loading) {
    // Show loading screen until all songs are loaded
    rectMode(CORNER);
    strokeWeight(2);
    textSize(34);
    fill(255);
    text("LOADING...", width / 2, height / 2 - 20);
    noStroke();
    fill(255);
    rect(width / 2 - 150, height / 2 + 20, 300, 40);
    fill(20);
    rect(
      width / 2 - 150,
      height / 2 + 20,
      map(numberOfSongsLoaded, 0, numberOfSongs, 0, 300),
      40
    );
    if (numberOfSongsLoaded == numberOfSongs) {
      loading = false;
    }
  } else {
    // Display the sound spectrum and UI once loading is complete
    let wave = fft.waveform();
    stroke(255, 50);
    noFill();
    beginShape();
    for (let i = 0; i < wave.length; i++) {
      let x = map(i, 0, wave.length, 0, width);
      let y = map(wave[i], -1, 1, height, 0);
      curveVertex(x, y);
    }
    endShape();

    rectMode(CENTER);
    ratioPos.y = lerp(ratioPos.y, height / 2, 0.1); // Smoothly move the UI element
    textSize(16);
    let channelName = "";
    switch (selectedChannel) { // Display the name of the selected channel
      case 0:
        channelName = " ENGLISH SONGS ";
        break;
      case 1:
        channelName = " CLASSICAL SONGS ";
        break;
      case 2:
        channelName = " KHALIDIYA SONGS ";
        break;
    }
    drawRadio(channelName, playing);
    drawChannels();
  }
}

// Event-driven functions to respond to keyboard presses for controlling the jukebox
function keyPressed() {
  switch (key) {
    case "n":
      nextChannel(); // Go to the next channel
      stopMusic(); // Stop the currently playing music
      break;
    case "b":
      prevChannel(); // Go to the previous channel
      stopMusic(); // Stop the music
      break;
    case " ":
      if (!playing) {
        playMusic(); // Start playing music if not already playing
      } else {
        stopMusic(); // Stop the music if playing
      }
      break;
  }
}

// Utility functions to control channels and playback
function nextChannel() {
  selectedChannel += 1; // Increment the channel index
  if (selectedChannel >= 3) {
    selectedChannel = 0; // Wrap around to the first channel
  }
}
function prevChannel() {
  selectedChannel -= 1; // Decrement the channel index
  if (selectedChannel < 0) {
    selectedChannel = 2; // Wrap around to the last channel
  }
}
function stopMusic() {
  if (songToPlay) {
    songToPlay.stop(); // Stop the currently playing song
  }
  playing = false;
}
function playMusic() {
  coinSound.play(); // Play the coin sound effect
  songToPlay = random(channels[selectedChannel]); // Select a random song from the current channel
  playing = true;
  songToPlay.loop(); // Start playing the selected song
}

// Drawing utility functions for UI elements
function drawChannels() {
  fill(100, 120, 100);
  rect(0, 150, 320, 70);
  fill(60, 70, 60);
  rect(0, 150, 300, 50);
  push();
  textAlign(LEFT, CENTER);
  let channels = ["English", "Classical", "Khalidiya"];
  textSize(12);
  for (let i = 0; i < 3; i++) {
    let x = 0;
    let y = 130 + 20 * i;
    noFill();
    if (selectedChannel == i) {
      fill(60, 90, 60);
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text("_" + channels[i], x - 150, y);
    } else {
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text(" " + channels[i], x - 150, y);
    }
  }
  pop();
}

// Function to draw the radio interface
function drawRadio(channel, playing = false) {
  translate(ratioPos.x, ratioPos.y);
  // Visual elements for the radio disk
  noStroke();
  fill(150, 150, 220, 100);
  circle(0, 100, 450);

  fill(20);
  circle(0, 100, 350);
  fill(200, 100, 100);
  circle(0, 100, 150);

  let channelName = channel.split("");
  push();
  translate(0, 100);
  if (playing) rotate(-frameCount / 60);
  push();
  for (let i = 0; i in channelName.length; i++) {
    rotate(TWO_PI / channelName.length);
    fill(255);
    text(channelName[i].toUpperCase(), 0, -50);
  }

  pop();
  pop();
  fill(180);
  circle(0, 100, 80);

  stroke(255);
  noFill();
  arc(0, 100, 420, 420, -PI / 2 + 0.4, -PI / 2 + 0.8);

  noStroke();
  strokeWeight(2);
  fill("#606A42");
  rect(0, 290, 500, 400, 40);
}

code with Arduino:

let loading = true;
let channels = [[], [], []]; //0 english channel , 1 - classical channel, 2 Al Khalidiya channel
let numberOfSongs = 0;
let numberOfSongsLoaded = 0;
let coinSound;
let selectedChannel;
let playing = false;
let songToPlay;
let toSend = 0;

function preload() {
  soundPaths = loadStrings("soundFileNames.txt");
  coinSound = loadSound("sounds/coin.mp3");

  // for (let i = 0; i < soundPaths.length; i++) {
  //   let words = soundPaths[i].split("_");
  //   //here we'll store sound paths in different arrays as different channels
  //   let channel = words[words.length - 2] + "_" + words[words.length - 1];
  //   switch (channel) {
  //     case "english_channel.mp3":
  //       channels[0].push("sounds/" + words.join("_"));
  //       break;
  //     case "classical_channel.mp3":
  //       channels[1].push("sounds/" + words.join("_"));
  //       break;
  //     case "khalidiya_channel.mp3":
  //       channels[2].push("sounds/" + words.join("_"));
  //       break;
  //   }
  // }
  // print(channels)
  // numberOfSongs = soundPaths.length;
  // //load every song in channel
  // for (let i = 0; i < channels.length; i++) {
  //   for (let j = 0; j < channels[i].length; j++) {
  //     channels[i][j] = loadSound(channels[i][j], () => {
  //       numberOfSongsLoaded += 1;
  //     });
  //   }
  // }
  /*
  ahlam_al_khalidiya_channel.mp3
cello_suite_classical_channel.mp3
gabl _aaarfak_al_khalidiya_channel.mp3
gymnopédie_classical_channel.mp3
i_lived_english_channel.mp3
mushfi_jorouhi_al_khalidiya_channel.mp3
overture_classical_channel.mp3
raheeb__al_khalidiya_channel.mp3
rouhe_thebak_al_khalidiya_channel.mp3
sundress_english_channel.mp3
swan_lake_suite_classical_channel.mp3
// sza_english_channel.mp3
virginia_beach_english_channel.mp3
whats_on_ur_mind_english_channel.mp3
  */

  {
    let sound = loadSound("sounds/i_lived_english_channel.mp3");
    channels[0].push(sound);
  }

  {
    let sound = loadSound("sounds/virginia_beach_english_channel.mp3");
    channels[0].push(sound);
  }
  
   
 


  
  
  
  
  
  
  
  
}

function setup() {
  createCanvas(600, 400);
  textAlign(CENTER, CENTER);

  //
  ratioPos = { x: width / 2, y: height * 2 };
  selectedChannel = floor(random(3));
  fft = new p5.FFT();
}

function draw() {
  background(40);
  if (loading) {
    rectMode(CORNER);
    strokeWeight(2);
    textSize(34);
    fill(255);
    text("LOADING...", width / 2, height / 2 - 20);
    noStroke();
    fill(255);
    rect(width / 2 - 150, height / 2 + 20, 300, 40);
    fill(20);
    rect(
      width / 2 - 150,
      height / 2 + 20,
      map(numberOfSongsLoaded, 0, numberOfSongs, 0, 300),
      40
    );
    if (numberOfSongsLoaded == numberOfSongs) {
      loading = false;
    }
  } else {
    //
    //draw Sound Spectrum
    let wave = fft.waveform();
    stroke(255, 50);
    noFill();
    beginShape();
    let x, y;
    for (let i = 0; i < wave.length; i++) {
      x = map(i, 0, wave.length, 0, width);
      y = map(wave[i], -1, 1, height, 0);
      curveVertex(x, y);
    }
    endShape();

    rectMode(CENTER);
    ratioPos.y = lerp(ratioPos.y, height / 2, 0.1);
    textSize(16);
    let channelName = "";
    switch (selectedChannel) {
      case 0:
        channelName = " ENGLISH SONGS ";
        break;
      case 1:
        channelName = " CLASSICAL SONGS ";
        break;
      case 2:
        channelName = " KHALIDIYA SONGS ";
        break;
    }
    drawRadio(channelName, playing);
    drawChannels();
  }
}

//functions to call from arduino's button
function keyPressed() {
  switch (key) {
    case "n":
      nextChannel();
      stopMusic();
      break;
    case "b":
      prevChannel();
      stopMusic();
      break;
    case " ":
      if (!playing) {
        playMusuc();
      } else {
        stopMusic();
      }
      break;
  }
}

function nextChannel() {
  selectedChannel += 1;
  if (selectedChannel >= 3) {
    selectedChannel = 0;
  }
}
function prevChannel() {
  selectedChannel -= 1;
  if (selectedChannel < 0) {
    selectedChannel = 2;
  }
}
function stopMusic() {
  if (songToPlay) {
    songToPlay.stop();
  }
  playing = false;
}
function playMusuc() {
  if (!playing) {
    coinSound.play();
    print("sel: " + selectedChannel);
    songToPlay = channels[selectedChannel][Math.floor(random(3))]; //random(channels[selectedChannel]);
    playing = true;
    songToPlay.loop();
  }
}

function drawChannels() {
  fill(100, 120, 100);
  rect(0, 150, 320, 70);
  fill(60, 70, 60);
  rect(0, 150, 300, 50);
  push();
  textAlign(LEFT, CENTER);
  let channels = ["English", "Classical", "Khalidiya"];
  textSize(12);
  for (let i = 0; i < 3; i++) {
    // text(channels[i],20,40+15*i);
    let x = 0;
    let y = 130 + 20 * i;
    noFill();
    if (selectedChannel == i) {
      fill(60, 90, 60);
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text("_" + channels[i], x - 150, y);
    } else {
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text(" " + channels[i], x - 150, y);
    }
  }
  pop();
}

function drawRadio(channel, playing = false) {
  translate(ratioPos.x, ratioPos.y);
  //disk
  noStroke();
  fill(150, 150, 220, 100);
  circle(0, 100, 450);

  fill(20);
  circle(0, 100, 350);
  fill(200, 100, 100);
  circle(0, 100, 150);

  let channelName = channel.split("");
  push();
  translate(0, 100);
  if (playing) rotate(-frameCount / 60);
  push();
  for (let i = 0; i < 20; i++) {
    rotate(TWO_PI / 20);
    fill(255);
    text("_", 0, -170);
  }
  pop();
  push();
  for (let i = 0; i < channelName.length; i++) {
    rotate(TWO_PI / channelName.length);
    fill(255);
    text(channelName[i].toUpperCase(), 0, -50);
  }

  pop();
  pop();
  fill(180);
  circle(0, 100, 80);

  stroke(255);
  noFill();
  arc(0, 100, 420, 420, -PI / 2 + 0.4, -PI / 2 + 0.8);

  noStroke();
  strokeWeight(2);
  fill("#606A42");
  rect(0, 290, 500, 400, 40);
}

function readSerial(data) {
  ////////////////////////////////////
  //READ FROM ARDUINO HERE
  ////////////////////////////////////

  if (data != null) {
    print(data);
    // make sure there is actually a message
    // split the message
    let fromArduino = split(trim(data), ",");
    // if the right length, then proceed
    if (fromArduino.length == 4) {
      // only store values here
      // do everything with those values in the main draw loop

      // We take the string we get from Arduino and explicitly
      // convert it to a number by using int()
      // e.g. "103" becomes 103
      let start = int(fromArduino[0]);
      let stop = int(fromArduino[1]);
      let next = int(fromArduino[2]);
      let prev = int(fromArduino[3]);

      if (start == 1) {
        print("start");
        playMusuc();
      } else if (stop == 1) {
        print("stop");
        stopMusic();
      } else if (next == 1) {
        print("next");
        nextChannel();
        stopMusic();
      } else if (prev == 1) {
        print("prev");
        prevChannel();
        stopMusic();
      }

      //////////////////////////////////
      //SEND TO ARDUINO HERE (handshake)
      //////////////////////////////////
      let sendToArduino = 0 + "\n";
      writeSerial(sendToArduino);
    }
  }
}

function keyPressed() {
  if (key == " ") {
    // important to have in order to start the serial connection!!
    setUpSerial();
  }
}

Showcase:

Struggles:

When I initially incorporated Arduino buttons into the program, I envisioned a seamless integration that would enhance the user experience by providing tangible controls for the digital jukebox. However, this integration proved to be more challenging than anticipated. The entire program malfunctioned, leading to unexpected behaviors where buttons would either not respond or trigger incorrect actions. This technical hurdle was a significant setback, as it compromised the core functionality of the project—interacting with the music playlist through physical controls.

Faced with these difficulties, I realized that a pivot was necessary to maintain the integrity and usability of the project. I revisited the drawing board, reevaluating the interfacing between the Arduino and the software. This required stripping down complex parts of the code, simplifying the communication protocols, and implementing more robust error handling mechanisms to ensure that each button press accurately corresponded to the intended action. Although this pivot was a detour from my original vision, it was a crucial learning experience that emphasized the importance of adaptability and thorough testing in the development process. The revamped approach not only resolved the issues but also reinforced the project’s functionality, making it more reliable and user-friendly.

Future Improvements:

Based on the experiences and challenges encountered with the integration of Arduino buttons and the development of the digital jukebox, several future improvements can be outlined to enhance the project further:

1. Enhanced Error Handling and Debugging Tools: Implementing more sophisticated error handling mechanisms can help identify and resolve issues more efficiently when they arise during the interaction between the Arduino hardware and the software. Additionally, developing a suite of debugging tools or visual indicators in the software can help monitor the state and health of the system in real-time.

2. User Interface Improvements: Enhancing the user interface to provide clearer feedback and more intuitive controls can significantly improve the user experience. This could include visual indicators of button presses, more responsive animations, or a more aesthetically pleasing layout that mimics the classic jukebox style.

3. Expanded Music Library and Categorization: Expanding the music library to include more diverse genres and new channels can cater to a broader audience. Implementing a more dynamic categorization system where users can create custom playlists or choose from themed channels could add a new layer of interaction.

4. Wireless Control Options: Introducing wireless control options such as Bluetooth or Wi-Fi connectivity could allow users to control the jukebox from their smartphones or other devices. This could be particularly useful for accessibility purposes and to accommodate larger venues.

5. Improved Audio Quality and Effects: Upgrading the sound output hardware or integrating software that allows for better sound equalization and effects can enhance the overall listening experience. This might include features like bass boost, echo, and balance adjustments that users can control directly.

6. Sustainability and Maintenance: Considering the long-term sustainability and ease of maintenance in the design can ensure the jukebox remains functional and enjoyable for years. This could involve using more durable materials for the hardware, making the system modular for easy repairs, or providing software updates to keep the system secure and efficient.

7. Interactive Features and Gamification: Introducing interactive features such as music trivia, user contests, or gamification elements where users can earn points or rewards for their interaction can increase engagement and provide a more entertaining experience.

These improvements aim to refine the functionality, broaden the appeal, and ensure the longevity of the digital jukebox project, making it not only a nostalgic piece but a cutting-edge feature for any social or personal space.

Arduino code:

// Define the pin numbers for the buttons
const int pinGreen = 11; // Start
const int pinYellow = 9; // Stop
const int pinBlue = 5; // Next Channel
const int pinBlack = 3; // Previous Channel

void setup() {
  // Initialize the buttons as inputs
  pinMode(pinGreen, INPUT);
  pinMode(pinYellow, INPUT);
  pinMode(pinBlue, INPUT);
  pinMode(pinBlack, INPUT);

  // Start serial communication at 9600 bps
  Serial.begin(9600);
  // start the handshake
  while (Serial.available() <= 0) {
    Serial.println("0,0,0,0"); // send a starting message
    delay(50);
  }
}

void loop() {
  // Read the state of each button
  int stateGreen = digitalRead(pinGreen);
  int stateYellow = digitalRead(pinYellow);
  int stateBlue = digitalRead(pinBlue);
  int stateBlack = digitalRead(pinBlack);
  // Serial.println(toSend);

  // String toSend;

  // Send different commands based on button presses
  // if (stateGreen == HIGH) {
  //   toSend = "start";
  // }
  // else if (stateYellow == HIGH) {
  //   Serial.println("stop"); // Command to stop the radio
  // }
  // else if (stateBlue == HIGH) {
  //   Serial.println("next"); // Command to go to next channel
  // }
  // else if (stateBlack == HIGH) {
  //   Serial.println("prev"); // Command to go to previous channel
  // }

  while (Serial.available()) {
    int fromP5 = Serial.parseInt();
   
    if (Serial.read() == '\n') {
      Serial.print(stateGreen);
      Serial.print(",");
      Serial.print(stateYellow);
      Serial.print(",");
      Serial.print(stateBlue);
      Serial.print(",");
      Serial.println(stateBlack);
    }
  }

  // delay(100); // Delay to debounce and prevent multiple sends
}

 

Selfie4Two (Final Project)

Concept

For my final project, I decided to create a Photo Booth that questions and criticises the act of taking an individual selfie while encouraging the processes of documenting memories with others. The purpose of the work is to discourage and eliminate the ego-centric approach behind how we make use of our smartphone’s camera. In the same way that the widespread use of smartphones has increased the awareness of responsible online practices, I wanted to create something that spoke to the importance of how we utilise this incredibly accessible method of documenting our lives. Essentially, Selfie4Two is a work that focuses on celebrating interpersonal connection through technology.

Technical Approach & Challenges

Initially, my approach in creating this Photo Booth concept was to utilise machine learning technology through an image classification model. This model would discern whether there were one or two people in the frame and whether they were holding up their phone (suggesting that they are taking a picture of themselves). I would then incorporate this model into p5js and load an ASCII webcam rendering based on what was detected, presenting question marks for one person and hearts for groups. Screenshots of the image classification model are seen below.

I was able to functionally incorporate it into p5 with some initial testing but could not effectively do so with the ASCII webcam rendering that I had made, meaning I was forced to avoid using it in order to finish the project.  This certainly limited the usability of the project as well as minimising how effective the concept was conveyed. In an attempt to resolve this, I included emojis (maintaining the theme of internet culture) onto the initial webcam display that can be clicked to display the ASCII renderings.

In terms of the ASCII webcam rendering, I was able to create two variations with one being comprised of question marks and the other of hearts. These are both displayed below.

After incorporating these into my final code, however, they ran much slower and did not display correctly. Unfortunately, I could not figure out how to fix this and had to present this version of the code as my final project.

const density = "  \u2661";
const density1 = "  ?";
let video;
let emojiSize = 32; // Size of the emojis

function setup() {
  createCanvas(window.innerWidth, window.innerHeight);
  video = createCapture(VIDEO);
  video.hide();
  let button = createButton("reset");
  button.mousePressed(resetSketch);
}

function draw() {
  background(0);
  video.loadPixels();

  // Draw the video
  image(video, 0, 0, width, height);

  let emoji = "🤳🧍❔";
  let emoji1 = "🤳🧑‍🤝‍🧑❤️";

  text(emoji1, width / 6, height / 4);
  text(emoji, width / 1.2, height / 4);
  textSize(32);
  textAlign(CENTER, CENTER);

  // Calculate bounding boxes for emojis
  let emojiBox = {
    x: width / 6 - emojiSize / 2,
    y: height / 4 - emojiSize / 2,
    width: textWidth(emoji),
    height: emojiSize,
  };

  let emojiBox1 = {
    x: width / 1.2 - emojiSize / 2,
    y: height / 4 - emojiSize / 2,
    width: textWidth(emoji1),
    height: emojiSize,
  };

  // Check if mouse click is inside the bounding box of the first emoji
  if (
    mouseX > emojiBox.x &&
    mouseX < emojiBox.x + emojiBox.width &&
    mouseY > emojiBox.y &&
    mouseY < emojiBox.y + emojiBox.height
  ) {
    coupleAscii();
  }

  // Check if mouse click is inside the bounding box of the second emoji
  if (
    mouseX > emojiBox1.x &&
    mouseX < emojiBox1.x + emojiBox1.width &&
    mouseY > emojiBox1.y &&
    mouseY < emojiBox1.y + emojiBox1.height
  ) {
    singleAscii();
  }
}

function singleAscii() {
  clear();
  textSize(10);
  background(0);
  video.loadPixels();
  fill(255, 180, 180);
  stroke(255, 180, 180);
  strokeWeight(1);
  let asciiImage1 = "";
  for (let j = 0; j < video.height; j++) {
    for (let i = 0; i < video.width; i++) {
      const pixelIndex = (i + j * video.width) * 4;
      const r = video.pixels[pixelIndex + 0];
      const g = video.pixels[pixelIndex + 1];
      const b = video.pixels[pixelIndex + 2];
      const avg = (r + g + b) / 3;
      const len = density1.length;
      const charIndex = floor(map(avg, 0, 255, 0, len));
      const c = density1.charAt(charIndex);
      text(
        c,
        map(i, 0, video.width, 0, width),
        map(j, 0, video.height, 0, height)
      );
    }
  }
}

function coupleAscii() {
  clear();
  textSize(10);
  background(0);
  video.loadPixels();
  fill(255, 180, 180);
  stroke(255, 180, 180);
  strokeWeight(1);
  let asciiImage = "";
  for (let j = 0; j < video.height; j++) {
    for (let i = 0; i < video.width; i++) {
      const pixelIndex = (i + j * video.width) * 4;
      const r = video.pixels[pixelIndex + 0];
      const g = video.pixels[pixelIndex + 1];
      const b = video.pixels[pixelIndex + 2];
      const avg = (r + g + b) / 3;
      const len = density.length;
      const charIndex = floor(map(avg, 0, 255, 0, len));
      const c = density.charAt(charIndex);
      text(
        c,
        map(i, 0, video.width, 0, width),
        map(j, 0, video.height, 0, height)
      );
    }
  }
}
function resetSketch() {
  clear();
  background(0);
  video.loadPixels();

  // Draw the video
  image(video, 0, 0);

  push();
  let emoji = "🤳🧍❔";
  let emoji1 = "🤳🧑‍🤝‍🧑❤️";

  text(emoji1, width / 6, height / 4);
  text(emoji, width / 6, height / 2);
  textSize(32);
  textAlign(CENTER, CENTER);
  pop();

  // Calculate bounding boxes for emojis
  let emojiBox = {
    x: width / 6 - emojiSize / 2,
    y: height / 4 - emojiSize / 2,
    width: textWidth(emoji),
    height: emojiSize,
  };

  let emojiBox1 = {
    x: width / 6 - emojiSize / 2,
    y: height / 2 - emojiSize / 2,
    width: textWidth(emoji1),
    height: emojiSize,
  };

  // Check if mouse click is inside the bounding box of the first emoji
  if (
    mouseX > emojiBox.x &&
    mouseX < emojiBox.x + emojiBox.width &&
    mouseY > emojiBox.y &&
    mouseY < emojiBox.y + emojiBox.height
  ) {
    coupleAscii();
  }

  // Check if mouse click is inside the bounding box of the second emoji
  if (
    mouseX > emojiBox1.x &&
    mouseX < emojiBox1.x + emojiBox1.width &&
    mouseY > emojiBox1.y &&
    mouseY < emojiBox1.y + emojiBox1.height
  ) {
    singleAscii();
  }
}

I also intended to add an Adafruit Neopixel LED Strip through Arduino into my project in order to emphasise the experience of a Photo Booth. The LED strip would be attached to the screen and would turn on if the image classification model detected two people. As I was already experiencing issues with the code, I was unable to incorporate this into my final project despite attempts at wiring and programming it.

Improvements

Clearly, there is a lot of space for improvement within my final project. Other than improving the functionality of all the components I was unable to incorporate properly (or at all), one key area of improvement would have been testing and sharing my ideas with someone more experienced in order to gauge their feasibility. Had I known ml5 libraries tend to run slower in p5js, I may have chosen to pursue a different concept altogether.

Most importantly, this project is proof of the importance of simplifying ideas and starting to work on them ahead of time. The main issue that lead to this outcome was that I did not give myself enough time to understand these new concepts (image classification, ASCII) which naturally lead to me being unable to incorporate them successfully within a p5js sketch. Going forward, I will ensure that I set myself realistic goals and provide myself with a workable timeline by experimenting with ideas earlier on.

Final Project: Defuse Dash

Concept

The project, “Defuse Dash,” is an interactive game where players must solve physical and digital puzzles to “defuse” a bomb. The game uses an Arduino setup with various sensors and inputs, including buttons, potentiometers, adafruit trellis, and an ultrasonic distance sensor, integrated with a p5.js visual and interaction interface. The game aims to teach principles of problem-solving under pressure, as players must complete various tasks within time limits to defuse a virtual bomb.

Images

Project Interaction

Visual and Physical Setup

Players are presented with a physical board containing:

  • A series of buttons connected to LEDs that light up when pressed.
  • A potentiometer used to adjust resistance values.
  • An ultrasonic distance sensor to measure and input distances.
  • A keypad for code entry.

The p5.js interface displays:

  • Task instructions and success criteria.
  • A visual representation of the “bomb,” including a countdown timer.
  • Feedback sections that show task outcomes and game progress.

Game Flow

The game progresses through a series of tasks such as adjusting distances, matching potentiometer values, entering sequences through buttons, and inputting codes on a keypad. Each task completion is visually indicated, moving the player closer to defusing the bomb.

Implementation

Hardware Components

  • Buttons and LEDs: Create a direct interaction where pressing a button shows immediate visual feedback through an LED.
  • Potentiometer: Adjusts to match a required voltage displayed on the p5.js interface.
  • Ultrasonic Sensor: Measures how far away a player is from the sensor, requiring players to physically move to match the required distance.
  • Adafruit Trellis Keypad: Create a direct interaction where pressing a button shows immediate visual feedback through an LED.

Software Components

  • p5.js Visuals: Animates tasks, displays real-time data, and provides a graphical countdown timer and game status.
  • Serial Communication: Handles data transmission between the Arduino and the p5.js interface, using serial commands to start tasks and send results.

Interaction Design

The interaction design focuses on integrating tactile and visual feedback to enhance engagement:

  • Tactile: Feeling the buttons click and adjusting the potentiometer gives a hands-on experience.
  • Visual: Immediate updates on the interface when tasks are completed successfully or fail.

Communication Between Arduino and p5.js

The communication protocol is straightforward:

  • Sending Commands: p5.js sends numeric codes corresponding to different tasks.
  • Receiving Data: Arduino sends back results as comma-separated strings that p5.js parses and uses to update the game state.

Arduino

The Arduino sketch controls every hardware-related action that’s essential to gameplay. this sketch is designed to track inputs from a variety of sensors, including buttons via digital pins, potentiometers, adafruit trellis, and ultrasonic distance sensors. In order to guarantee accurate input detection, it manages button debouncing. It also regulates outputs, such as button and keypad LEDs, to give the player real-time feedback depending on the logic and status of the game.

For each cycle of the loop() function, the sketch checks the status of connected devices, updates the game state based on player interactions, and sends crucial gameplay data to the p5.js application over serial communication. This includes values like distance measurements, potentiometer levels, button sequence inputs, and keypad inputs, which are vital for progressing through the game’s challenges.

Code

#include <string.h>

// Define task states using an enum
enum Task {
  TASK_DISTANCE_SENSING,
  TASK_POTENTIOMETER_ADJUSTMENT,
  TASK_BUTTON_SEQUENCE,
  TASK_ADAFRUIT_KEYPAD,
  TASK_DEFUSED_SOUND,
  TASK_EXPLODED_SOUND
};

// Variables to store the current task and sensor values
volatile Task currentTask = TASK_DISTANCE_SENSING;
const int buttonPins[] = { 2, 3, 4, 5 };  // Pins for buttons
const int LEDpins[] = {10, 11, 12, 13}; // Pins for LED buttons
const int potPin = A0;
const int trigPin = 6; 
const int echoPin = 7;

// Variables to store sensor data
bool buttonStates[4] = { 0 };
int potValue = 0;
int distance = 0;

// Length of the button press sequence
const int sequenceLength = 5;   
// Array to store the button sequence     
int buttonSequence[sequenceLength]; 
// Index to keep track of the current position in the sequence 
int sequenceIndex = 0;               
bool lastButtonState[4] = { LOW, LOW, LOW, LOW };

String taskCommand = "0";
int taskNumber = 0;
// Initialize an empty string to hold the sequence
String sequenceString = "";  

#include <Wire.h>
#include "Adafruit_Trellis.h"

#define MOMENTARY 0
#define LATCHING 1
// set the mode here
#define MODE MOMENTARY

Adafruit_Trellis matrix0 = Adafruit_Trellis();
Adafruit_TrellisSet trellis = Adafruit_TrellisSet(&matrix0);

#define NUMTRELLIS 1

#define numKeys (NUMTRELLIS * 16)

#define INTPIN A2

int passcode[5];
int passcodeIndex = 0;
// Initialize an empty string for the passcode
String passcodeString = "";  

void setup() {
  Serial.begin(9600);  
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  for (int i = 0; i < 4; i++) {
    // Set button pins with internal pull-up resistors
    pinMode(buttonPins[i], INPUT_PULLUP);  
    pinMode(LEDpins[i], OUTPUT);
  }
  pinMode(buzzerPin, OUTPUT);
  pinMode(INTPIN, INPUT);
  digitalWrite(INTPIN, HIGH);
  trellis.begin(0x70);
  // light up all the LEDs in order
  for (uint8_t i = 0; i < numKeys; i++) {
    trellis.setLED(i);
    trellis.writeDisplay();
    delay(50);
  }
  // then turn them off
  for (uint8_t i = 0; i < numKeys; i++) {
    trellis.clrLED(i);
    trellis.writeDisplay();
    delay(50);
  }
}


void loop() {
  if (Serial.available() > 0) {
    // Read the next command until a newline character
    taskCommand = Serial.readStringUntil('\n');  
    if (taskCommand.length() > 0) {
      taskNumber = taskCommand.toInt();
      if (taskNumber == 0 || taskNumber == 1 || taskNumber == 2) {
        currentTask = TASK_DISTANCE_SENSING;
      } else if (taskNumber == 3 || taskNumber == 4 || taskNumber == 5) {
        currentTask = TASK_POTENTIOMETER_ADJUSTMENT;
      } else if (taskNumber == 6) {
        currentTask = TASK_BUTTON_SEQUENCE;
      } else if (taskNumber == 7) {
        currentTask = TASK_ADAFRUIT_KEYPAD;
      } else {
        currentTask = static_cast<Task>(taskNumber);  
      }
      // Function to execute tasks
      executeTask(currentTask); 
    }
  }
}

void executeTask(Task task) {
  switch (task) {
    case TASK_DISTANCE_SENSING:
      distance = measureDistance();
      Serial.println(distance);
      break;
    case TASK_POTENTIOMETER_ADJUSTMENT:
      potValue = analogRead(potPin);
      Serial.println(potValue);
      break;
    case TASK_BUTTON_SEQUENCE:
      readButtons();
      break;
    case TASK_ADAFRUIT_KEYPAD:
      readKeypad();
      break;
    default:
      break;
  }
}


// Function to measure distance using an ultrasonic sensor
int measureDistance() {
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
  // 38ms timeout for a max distance
  long duration = pulseIn(echoPin, HIGH, 38000);  
  // Calculate and return the distance
  return duration * 0.034 / 2;                    
}

// Function to read and report button states
void readButtons() {
  for (int i = 0; i < 4; i++) {
    // Invert because INPUT_PULLUP
    bool currentButtonState = !digitalRead(buttonPins[i]);  

    digitalWrite(LEDpins[i], currentButtonState ? HIGH : LOW);

    // Check if there is a change and the button is pressed
    if (currentButtonState && currentButtonState != lastButtonState[i] && sequenceIndex < sequenceLength) {
      // Store button number (1-based index)
      buttonSequence[sequenceIndex] = i + 1;  
      if (sequenceIndex > 0) {
        sequenceString += ",";  
      }
      // Add the button number to the string
      sequenceString += String(buttonSequence[sequenceIndex]);  
      sequenceIndex++;
    }

    // Update the last button state
    lastButtonState[i] = currentButtonState;
  }

  // Check if the sequence is complete
  if (sequenceIndex == sequenceLength) {
    // Send the complete sequence string
    Serial.println(sequenceString);  
    sequenceString = "";
    // Reset the sequence index to start new sequence capture
    sequenceIndex = 0;
  }
}

void readKeypad() {
  // 30ms delay is required, don't remove me!
  delay(30);  

  if (MODE == MOMENTARY) {
    // If a button was just pressed or released...
    if (trellis.readSwitches()) {

      // Go through every button
      for (uint8_t i = 0; i < numKeys; i++) {
        // If it was pressed, turn it on
        if (trellis.justPressed(i)) {
          trellis.setLED(i);
          trellis.writeDisplay();

          if (passcodeIndex < 5) {
            passcode[passcodeIndex] = i+1;
            if (passcodeIndex > 0) {
              passcodeString += ","; 
            }
            // Add the key index to the string
            passcodeString += String(passcode[passcodeIndex]);  
            passcodeIndex++;
          }

          // If it was released, turn it off
          if (trellis.justReleased(i)) {
            trellis.clrLED(i);
            trellis.writeDisplay();
          }
        }
      }

      // Check if the passcode is complete
      if (passcodeIndex == 5) {
        // Send the complete passcode string
        Serial.println(passcodeString); 
        // Reset for next input 
        passcodeString = "";
        passcodeIndex = 0;  
      }
    }
  }
}

P5.js

The p5.js sketch is crafted to enrich the player’s experience with engaging visuals and responsive game dynamics. It visually represents the game state on a web interface, animating elements like timers, sensor data displays, and game status messages. This sketch plays a critical role in bridging the physical and digital aspects of the game by interpreting and displaying data received from the Arduino.

The sketch manages different game states, such as starting the game, transitioning between tasks, and handling win-or-lose conditions. Each state is visually distinct and designed to keep the player informed and engaged.

Code

Points of Pride

  • One of the aspects I’m particularly proud of is the seamless integration and synchronization between the Arduino and p5.js environments, which facilitated a dynamic and interactive experience.
  • The development of the narrative-driven game logic within p5.js, which utilized the sensor inputs from Arduino to unfold a storyline, is another high point. This narrative approach significantly enhanced user engagement, making the technical aspects of the project more relatable and enjoyable.
  • The use of creative riddles that required players to interpret clues and respond with physical interactions, such as adjusting distances and entering sequences, added an educational layer that subtly introduced users to concepts of measurement, spatial awareness, and logical sequencing.
  • Despite not initially being confident in my soldering skills, I successfully soldered numerous components, and they functioned perfectly. This experience not only enhanced my abilities but also boosted my confidence, affirming that I am now proficient in soldering.

Video(Before the Showcase)

Video(During the showcase)

Future Improvements

  • Interactivity Enhancements: One exciting direction for future development is enhancing the game’s interactivity by introducing multiplayer features. This could involve allowing multiple players to interact with the same game setup simultaneously, adding a competitive element by having players complete tasks in sequence, against a timer, or against each other.
  • Leaderboard/Scoreboard: Integrating score tracking could significantly extend the game’s appeal and replayability. A leaderboard system where players can register their scores and compare them with other players.

Final Project – “The Heist”

CONCEPT

“The Heist: Unlock the Safe” is an exciting puzzle/math game that challenges your mind to decode a sequence of numbers given a few hints, with each of them guiding you in eliminating potential numbers.  It’s pretty simple, the user is provided with instructions which then allows them to use their skills and crack the code. The theme behind it is to provide a sense of thrill of cracking a safe, just like in a bank robbery (not advised) or an escape room!

DOCUMENTATION

1) Teaser Video (pre-showcase)

2) Demo Video (how to play?)

3) Final video (showcase day!)

IMPLEMENTATION

Interaction Design

The box I created features two buttons and a potentiometer, positioned for user convenience. The red button, placed farthest from the user, serves as the game initiator and state mover, designed for occasional use. On the other hand, the knob (potentiometer) & white button are situated closer to the player, allowing easy access as they will be frequently used during the game to guess the code. Further, the box was designed to match the game’s theme, in terms of color and aesthetic (materials used: wood, wood paint, sponges, tape, glue, equipment, etc.).

Arduino

The Arduino code sets up the game with a three-digit combination lock (three possible combinations). It initializes variables such as the game state, the entered codes, and the correct combination. The safe can be in the following states: Menu (‘M’), Instruction (‘I’), Play (‘P’), and End (‘E’). Buttons and a potentiometer are used to input digits, and a servo motor controls the lock mechanism. The program continuously checks for button presses, serial input, and the correctness of the entered code. If the correct code is entered, the safe unlocks the servo motor, indicating success. Otherwise, it remains locked, prompting the user to retry. However, it’s worth noting that the initial plan to control the lock mechanism with the servo was later removed from the code. The checkCode() function, verifies if the entered code matches the correct combination when the game state is ‘E’ (End). It iterates through each digit of the entered code, comparing it with the corresponding digit in the correct combination. If any digit doesn’t match, it sets the correct flag to false and breaks out of the loop. If all digits match, meaning a correct code, it unlocks the safe by setting locked to false and open to true. Link to Arduino Sketch.

P5.js

The p5.js code begins by declaring variables such as various image files required for the game. Key components for communicating with an Arduino are included, such as variables to read data from Arduino (digit, button1, button2, locked) and variables to send data to Arduino (open, codes, button1State, button2State, buttonPressCount). The `keyPressed()` and `preload()` functions handle necessary setup tasks, such as loading sound and image files. The `setup()` function initializes the canvas and starts the game by calling the `restart()` function. The `draw()` function renders different game screens based on the current `gameState`, which then displays images accordingly. The `readSerial()` function manages data received from the Arduino, updating game variables accordingly.

Communication

At first, I used one button for both entering digits and moving to different game parts. But p5.js got mixed up with the signals from Arduino. So, I split the tasks between two buttons. Now, one button only enters digits (white button), and the other button (red button) handles moving between game states. In the communication between the Arduino & p5.js, the serial monitor informs p5.js about the status of buttons and the number from the potentiometer. Each line of data received has four parts (initially): two “true/false” statements indicating button states, followed by numbers representing the potentiometer reading. Using this data, p5.js updates variables like `button1`, `button2`, and `digit`, which helps control the game. For instance, if `button1` is true and `button2` is false, it means a certain action from the player, like moving to the next stage or entering a code digit. Dealing with serial communication was certainly one of the challenges I encountered in the process.

PROUD MOMENTS

I am particularly proud of how the box turned out in terms of its design matching the game aesthetic. I am also proud of how much I have learned throughout the process and through many many many many mistakes that costed hours of my time, but it was all worth it. Safe to say, I’m almost fluent in Arduino. I was also proud of the short amount of time I had to work on it and pulled through hours on end in the lab, especially given that I had missed the class where soldering was practiced, so I had to figure that out with the help of endless YouTube videos. Surprisingly, I am glad that I did not add a time element to the game because some people were taking less than a minute while others more than 5; a time limit would not have been ideal, I believe. More than anything, I am proud of how this project has brought many of me and my classmates together, and I am happy with how it turned out. If you had told me that I’d do this on the first day of classes, I would not have believed it was possible!

AREAS OF IMPROVEMENT

I initially had more in mind, such that the box would unlock and a beam of light would appear, or even better, serve some chocolate! Yet, I was faced with the unexpected measurement issue when the box was too small, and I underestimated how much space the wires would take. Regardless, I attempted to put something inside, and the wires kept moving and unplugging, leading to disaster every time I got close to it. To add to that, halfway through the showcase, the potentiometer stopped reaching the number 9, so I had to quickly edit my code & delete the possibility of a code generating a combination with the number 9. Other than that, it ran very smoothly. Another thing would have been to signify what the white button was, although many instinctively knew, given that the red button was labeled. Learned a lot, to say the least! Thank you!

Reading Reflection – Week 12

Design Meets Disability” was a really eye-opening read (pun intended). As we know, eyesight issues are common throughout the world, including myself. In recent years, there has been an explosion in the number of people wearing glasses. Although sight issues are increasing within the population, this isn’t the only reason why people are wearing glasses. Even with contact lenses or corrective surgery being a possibility for many people, lots of individuals are choosing glasses for fashion and stylistic reasons. To be specific, as of 1991, the design press declared that “eyeglasses have become stylish” (page 17). Even more than just eyesight problems, the book discusses many other physical disabilities such as hearing aids, where the company HearWear invested in its design unlike other companies. As Henrietta Thompson put it, “Over the decades, there has been an amazing amount of technical development of hearing aids, but in that time little or no design investment occurred” (page 25). I believe the reason is that they have focused much more on the technology and developing it more precisely, as they may be more complex than eye lenses.

Moving onto body wear, leg wear, and prosthetics, the book claims that there are only two approaches, which include realism and functionalism (page 35). In an increasingly complex world, we must value, as the author said in page 64, the word design in “designing for disability.” As a matter of fact, when googling “designing for disability,” the first thing that pops up is “To design for disability means giving your users control and options.” See, that’s the misunderstanding; there’s a huge focus on functionality that disregards design. Designing for disability isn’t as inclusive and thoughtful as I had assumed, and looking ahead, I’m hopeful for a future where designing for disability embraces creativity, to hopefully empower individuals of determination not to feel ashamed but to serve as inspirations for us all. I’m not sure if it was mentioned, but I wonder what the author’s take would be when it comes to braces or getting a cast— aspects that aren’t necessarily permanent but rather temporary.

Week 12 Final Project Proposal

Finalized concept

In “The Heist,” players will be on a mission to crack a safe. Initially, I was contemplating a game related to an escape room, but I realized I needed to scale down the idea due to time constraints. Therefore, I opted for an aspect commonly found in escape rooms: a safe. Players will need to guess the correct digits based on hints to unlock the safe. In terms of technicality, when the game begins, Arduino receives a pre-determined three digit code from p5. This code is the key to unlocking the safe. A servo motor locks the safe box…lock function will set the servo to position 0 (lock position) & update locked status to true. Whereas the unlock function will set servo to position 90 (unlock position) and update locked status to false. Players will use a potentiometer to select digits ranging from 0 to 9. After choosing each digit, they press a button to confirm their selection. Further, the safe design will resemble a classic bank safe.

Arduino Program Description

1. Initializing Pins & Variables
– Assign pins for the servo, two buttons, and potentiometer. Specify the potentiometer’s pin and initialize its value, along with a boolean variable set to true and a digit variable to store potentiometer readings.
– Initialize the servo motor.

2. Loop Function
– Check conditions to determine whether to call the unlock or lock function.
– Read the state of the buttons and store it in a variable.
– Read the potentiometer’s value and map it to a digit between 0 and 9.
– Print the digit, button states (true/false), and the locked status (true/false) to the Serial Monitor.

3. Lock Function
– Set the servo to position 0 (locked position).
– Update the locked status to true.

4. Unlock Function
– Set the servo to position 90 (unlocked position).
– Update the locked status to false.

P5 Program Design

Game DESIGN

Reading Reflection – Week 11

The essay, or more accurately a “rant” as described by Bret Victor, mainly focuses on user interaction both presently and in the distant future. Victor argues that the sense of touch, essential to human work for millions of years, is important to understanding his argument. He emphasizes that technology doesn’t simply happen but is rather a result of careful development. In introducing the concept of “Pictures Under Glass,” which represents “an interaction paradigm of permanent numbness,” the author highlights how this denies our hands their natural capabilities. Consequently, he argues against accepting an Interface Of The Future that is less expressive than a when making a sandwich. To avoid that, Victor highlights how we must understand that the future hinges on our choices as people determine which ideas to pursue, which research to support, and how they’ll shape their professional paths. Last but not least, he wraps up with an interesting question: in a world where we control our entire bodies, should our interactions boil down to just a finger?

I understand where the author is coming from; revolutionary technology comes out of long research, but as we can see nowadays, technology is all around us, and what the author seems to be leaning toward is making technology a part of us. Don’t get me wrong, I support technology being improved & people being innovative, but I must fear that there will come a day when the line will be crossed and it will not simply be “pictures under glass,” but rather “pictures under skin. For better or for worse, this is one of my concerns, and I believe it is a concern shared by many, which is why we have remained in an era where interactions have “permanent numbness.  The “glass” referred to in “pictures under glass” should always be there; it is when that barrier is broken that ideas will have no limit, which is a good thing, but it is also when many other things will have no limit, and that’s what’s dangerous. That said, I believe the future shouldn’t be limited to just a single finger, but also not utilized by all of them.

Week 11 Final Project Prompt

I have two potential project ideas/prompts: Plan A involves designing a game that revolves around unlocking a safe. In this game, players will be tasked with guessing the correct digits of a passcode to unlock the safe. The passcode will be generated randomly at the start of the game and transmitted to an Arduino, which will control a servo motor to lock the safe box. Players can input their guesses using either buttons or potentiometers (either limit the digits to 1-5 and use five buttons OR use the potentiometer to choose the digits from 0-9 and press a button to enter  OR use four potentiometers for each digit without a button to enter), depending on the chosen design. The safe itself could be modeled after a traditional bank safe or a more modern safe box.

Alternatively (if plan a didn’t work out), Plan B would involve expanding upon my midterm project, which included the Elevator Rush game. This game was inspired by the frustration of waiting for elevators during busy times, such as the rush between classes. Players control an elevator, picking up students to ensure they reach class on time. I’m considering bringing this game to life by creating a physical model of the elevator/C2 space, which would allow users to interact with it physically.

Final Project (+ User Testing) – Stefania Petre

Concept:

For this final project, I envisioned entering a universe where art and technology collide to create something genuinely unique. I wanted to make it easy for people who have never drawn before to experience it for the first time.

Imagine an interactive space where art and technology merge, transforming bodily movements into a rainbow of colours on a digital canvas. My initiative encourages individuals to exhibit their ideas through motion. They use a paintbrush attached to an Arduino to navigate a symphony of colourful ellipses. This is more than simply an artwork; it’s an experience that captures the essence of movement and transforms it into a personalised digital masterpiece.

Arduino Part:

The foundation of the interaction is the Arduino Uno, on which I worked with a ZX Distance and Gesture Sensor. The sensor is adept at monitoring the paintbrush’s proximity as well as the artist’s minor hand gestures. To be honest, installation was rather simple, but the sensor itself was not as powerful as planned.

Input: Proximity data and gesture commands from the ZX Sensor.
Output: Serial communication to relay the sensor data to the computer running the p5.js sketch.
Data to p5.js: Real-time Z-axis data for proximity (distance of the hand or brush from the sensor) and X-axis data for lateral movement, along with gesture detections (swipes, taps).
From p5.js: Instructions may be sent back to calibrate gesture sensitivity or toggle the sensor’s active state.

P5.js Part:

On the digital front, p5.js will serve as the canvas and palette, with dynamic and malleable capabilities. It will translate the incoming data from the Arduino into a series of colours and movements on the screen.

Receiving Data: Interpreting proximity and gesture data from the Arduino.
Processing Movements: Real-time mapping of hand movements to colour strokes and splashes with varied intensity and spread on a digital canvas.
Visual Feedback: Dynamic visual changes that reflect the flow and dance of the user’s motions.
To Arduino: Signals for modifying the ZX Sensor parameters in response to real-time performance and user feedback.

Graphics Used:

Gesture: Swipe Left / Right, Tap

Visuals: Dynamic shapes, colors, and brush strokes based on movement data.

Development and User Testing
The ZX Distance and Gesture Sensor has now been integrated with Arduino, and the immediate goal is to ensure that data flows smoothly into the p5.js programme. By the time user testing begins next week, the system should respond to hand motions by presenting relevant visual modifications on the screen.

User Testing Objectives:

  • Assess how natural and fulfilling it is to paint in midair.
  • Ensure responsiveness and accuracy of gesture detection.
  • Gather feedback from participants regarding the ease of use and satisfaction with the interactive art experience.

User Testing Techniques:

    • Record interactions on video to analyze gesture accuracy and timing.

How it Works:

  1. Arduino Setup: Connect Arduino to the ZX Sensor and establish serial communication with p5.js.
  2. Gesture Detection: The Arduino reads gestures and proximity data and sends this information to the p5.js sketch.
  3. Canvas Response: p5.js interprets the data and creates a dynamic visual display that reflects the gestures and brush movements.
  4. Feedback Loop: p5.js sends calibration data back to Arduino to adjust the sensor settings if necessary.

Code 
Arduino Code Example:

#include <Wire.h>
#include <ZX_Sensor.h>

// Constants
const int ZX_ADDR = 0x10;  // ZX Sensor I2C address

// Global Variables
ZX_Sensor zx_sensor = ZX_Sensor(ZX_ADDR);
uint8_t x_pos;
uint8_t z_pos;
uint8_t handPresent = false;

void setup() {

  Serial.begin(9600);
  
  zx_sensor.init();

 
   while (Serial.available() <= 0) {
 
    Serial.println("0,0,0"); // send a starting message
 
    delay(50);
  }
}

void loop() {
  
  // If there is position data available, read and print it
  if ( zx_sensor.positionAvailable() ) {
    uint8_t x = zx_sensor.readX();
    if ( x != ZX_ERROR ) {
      x_pos=x;
    }
    uint8_t z = zx_sensor.readZ();
    if ( z != ZX_ERROR ) {
      z_pos=z;
    }
    handPresent=true;
  } else {
    handPresent=false;
  }
  
   while (Serial.available()) {
   

    int  inbyte = Serial.parseInt();
    if (Serial.read() == '\n') {
     
      Serial.print(x_pos);
      Serial.print(',');
      Serial.print(z_pos);
      Serial.print(',');
      Serial.println(handPresent);
    }
  }

}

 

P5 Code:

// FINAL PROJECT BY STEFANIA PETRE
// FOR INTRO TO IM

let img;
let brushSize = 19;
let colorHue = 0;
let previousX = 0,
  previousY = 0;
let xPos = 0;
let zPos = 0;
let smoothedX = 0;
let handPresent = 0;
let showDrawing = false;
let startButton; 
let mappedX = 0;
let mappedZ = 0;

function preload() {
  img = loadImage("start.webp");
}

function setup() {
  createCanvas(640, 480);
  colorMode(HSB, 360, 100, 100, 100);
  textSize(18);

  // Set up the start button
  startButton = createButton("Get Creative!");
  startButton.position(290, 175); 
  startButton.mousePressed(startDrawing);

  
  let fullscreenButton = createButton("Fullscreen");
  fullscreenButton.position(10, 10); 
  fullscreenButton.mousePressed(toggleFullScreen);

  // Set the initial hue
  colorHue = random(360);
}

function draw() {
  if (!showDrawing) {
    background(img);
  } else {
    if (!serialActive) {
      background(0);
      fill(255);
      //text("Press the 'Get Creative!' button to start drawing", 20, 30);
    } else {
      if (handPresent == 1) {
        // Adjust mapping ranges according to your actual data
        mappedX = map(xPos, 180, 40, 0, width); 
        mappedZ = map(zPos, 240, 25, 0, height); 
       
        mappedX = constrain(mappedX, 0, width);
        mappedZ = constrain(mappedZ, 0, height);

        let weight = 10; // Adjust as needed
        let strokeColor = color(colorHue % 360, 100, 100);

        stroke(strokeColor);
        strokeWeight(weight);
        ellipse(mappedX, mappedZ, weight * 2, weight * 2);

        previousX = mappedX;
        previousY = mappedZ;
      }

      colorHue += 2;

      noStroke();
      fill(0, 0, 0.000000000000005, 1);
      rect(0, 0, width, height);
    }
  }
}

function startDrawing() {
  showDrawing = true; 
  startButton.hide(); 
  setUpSerial();
}

function toggleFullScreen() {
  let fs = fullscreen();
  fullscreen(!fs);
  resizeCanvas(windowWidth, windowHeight);
  startButton.position(windowWidth / 2 - 40, windowHeight / 2 - 30);
}

function readSerial(data) {
  if (data != null) {
    let fromArduino = split(trim(data), ",");
    if (fromArduino.length == 3) {
      xPos = int(fromArduino[0]);
      zPos = int(fromArduino[1]);
      handPresent = int(fromArduino[2]);
    }
    let sendToArduino = 0 + "\n";
    writeSerial(sendToArduino);
  }
}

function keyPressed() {
  if (key == " ") {
    setUpSerial();
  }
}

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

Final thoughts: 

Even though the project was not exactly how I have pictured it at the beginning, it still worked out well. People at the showcase liked it and everything worked for 3 hours and I am happy that I have chosen this path.

Signing Off,

Stefania Petre

Final Project – Khalifa AlShamsi

Concept:

The concept of this project revolves around creating an interactive UFO game titled “UFO Escape.” The main goal is to score points by avoiding collisions with asteroids and navigating through space. What sets this game apart is its unique control scheme: players use a glove equipped with a gyroscope sensor as the game controller. This setup allows the game to translate the player’s hand tilt movements into navigational commands within the game environment to turn the UFO in different directions.

Final Concept of the project:

Video of interaction:

 

Description of interaction design

Implementing the “UFO Escape” game involves an interaction design that merges physical computing with digital interfaces to create an immersive gaming experience. Here’s a detailed breakdown of the interaction design and implementation:

Hardware Setup:
Gyroscope Sensor (3-Axis Gyroscope L3GD20H):
This sensor is integrated into a wearable glove made from velcro, foam tape, and cloth. The glove is then connected to an Arduino UNO using 4 jumper wires soldered to a 90-cm length. It is then covered by heat-shrinkable rubber tubing for aesthetics and a better wire management setup.

I kept changing the glove’s prototypes due to either wiring issues or the fact that none were non-adjustable. Below are the different versions of the glove.

First Prototype:

This version did not allow the cables to function properly due to the positioning of the gyroscope.

Second Prototype:

This glove secured the gyroscope, but due to it not being adjustable, it kept causing issues with the wires whenever a different sized hand would wear it.

Final Prototype:

This glove concept fixed all the issues mentioned previously, from sizing difficulties to wire management, making it the ideal version for various hand sizes. It was made from scratch in the IM lab using different materials to secure the glove and a metal piece to lock in the velcro I got from home from a metal glove.

Cabling:

 

The cable was soldered on a stranded wire to extend the jumper wires for a more usable length for the glove.

How the code works:

The gyroscope sensor extracts the X and Y variables read by the Arduino to determine the hand’s position and movement. The Arduino Uno sends those as instructions for where the UFO should be positioned, which the P5 file reads and translates.

// Function to move the player based on arrow key inputs
  move() {
    let moveSpeed = 10; // Speed multiplier
    this.gotoX += gyroData.x * moveSpeed;
    this.gotoY += gyroData.y * moveSpeed;

    this.gotoX = constrain(this.gotoX, 30, width - 30);
    this.gotoY = constrain(this.gotoY, 30, height - 30);

    this.x = lerp(this.x, this.gotoX, 0.1);
    this.y = lerp(this.y, this.gotoY, 0.1);

    if (frameCount % 100 === 0) {
      obstacles.push(new Obstacle()); // Add new obstacles
    }

    this.gotoX = constrain(this.gotoX, 30, width - 30); // Keeps player within horizontal bounds
    this.gotoY = constrain(this.gotoY, 30, height - 30); // Keep player within vertical bounds
    // //This is only so the player cannot exist the canvas
  }

This processes the gyroscope data to interpret the player’s hand movements as specific commands (left, right, up, down, accelerate).

Serial Communication:
The Arduino transmits the processed data to a computer via serial communication, and in my case, it provided the P5 with the full library of serial communication, which made the process work for me.

/**
 * p5.webserial
 * (c) Gottfried Haider 2021-2023
 * LGPL
 * https://github.com/gohai/p5.webserial
 * Based on documentation: https://web.dev/serial/
 */

'use strict';

Game Mechanics:

The game is developed in a way that makes it hard for the player to achieve a high score by making the meteorites come down faster and faster as the player progresses to higher scores.

// Function to update obstacle position
  update() {
    this.y += map(score, 0, 10, gameSpeed, gameSpeed + 5); // Moves obstacles down the screen
    //this will make game harder as time grows
  }
}

Also, the player should be included in the provided map of the game so no cheating occurs and the game is played fairly based on skill levels.

this.gotoX = constrain(this.gotoX, 30, width - 30); // Keeps player within horizontal bounds this.gotoY = constrain(this.gotoY, 30, height - 30); // Keep player within vertical bounds // //This is only so the player cannot exist the canvas }

Player Interaction:
Players wear the gyroscope-equipped glove and move their hands to control the UFO within the game. Movements are intuitive: tilting the hand to the sides steers the UFO laterally, while tilting forward or backward would control vertical movement, and depending on how fast you tilt your hands, you could possibly send the UFO in any direction of the map faster than needed making you very aware of what move you would wanna make next as the meteoroids are coming down.

Visuals/Audio:
The game provides real-time visual feedback by updating the position and movements of the UFO based on the player’s actions as well as background noise for when the UFO is flying around.

P5 Sketch:

let port; // Serial port instance
let gyroData = { x: 0, y: 0 }; // Placeholder for gyro data
let serialInterval; // Interval for polling serial data

// Variables for game assets
let bgImage; // Variable to hold the background image for menu and game over screens
let bgMusic; // Variable to hold the background music for gameplay
let player; // Player object
let obstacles = []; // Array to store obstacles
let gameSpeed = 6; // Speed at which obstacles move
let score = 0; // Player's score
let gameState = "MENU"; // Initial game state; that is "MENU", "PLAYING", or "GAME OVER"
let rockImage; // Variable to hold the rock image
let gameOverImage;


// Preload function to load game assets before the game starts
function preload() {
  // bgImage = loadImage("space.png"); // Loads the background image
  menuImage = loadImage("menu.jpeg");
  // gameplayBgImage = loadImage("gameplay.jpg"); // Loads the gameplay background image
  bgMusic = loadSound("gameplaysound.mp3"); // Loads the background music
  rockImage = loadImage("rock-2.png"); // Loads the rock image
}

// The setup function to initialize the game
function setup() {
  createCanvas(750, 775).parent("canavs-container"); // Size of the game canvas
  openButton = createButton("Connect Arduino")
    .position(410, 20)
    .style("background-color", "rgba(244,238,238,0.1)(255, 50)")
    .style("border-radius", "70px")
    .style("padding", "10px 20px")
    .style("font-size", "15px")
    .style("color", "white")
    .mousePressed(openSerialPort); // Click to open serial port
  player = new Player(); // Initializes the player object
  textAlign(CENTER, CENTER); // Setting text alignment for drawing text
  textFont("arial");
}

function openSerialPort() {
  port = createSerial(); // Initialize the serial port instance
  if (port && typeof port.open === "function") {
    port.open("Arduino", 9600); // Open with a predefined preset
    // Set up polling to check for serial data every 100ms
    serialInterval = setInterval(readSerialData, 100); // Poll for data
  } else {
    console.error("Failed to initialize the serial port.");
  }
}

function readSerialData() {
  if (port && port.available()) {
    let rawData = port.readUntil("\n"); // Reads data till newline
    if (rawData && rawData.length > 0) {
      let values = rawData.split(","); // Splits by commas
      if (values.length === 2) {
        gyroData.x = parseFloat(values[0]); // Parse X value
        gyroData.y = parseFloat(values[1]); // Parse Y value
      } else {
        console.error("Unexpected data format:", rawData); // Error handling
      }
    }
  }
}

// Draw function called repeatedly to render the game
function draw() {
  // Displays the space background image only during menu and game over states but displays a different image during gameplay
  if (gameState === "PLAYING") {
    background("#060C15");
    noStroke();
    fill(255);
    for (let star of stars) {
      circle(star.x, star.y, star.r);
      star.y += star.yv;
      if (star.y > height + 5) star.y = -5;
    }
  }

  // Handles game state transitions
  if (gameState === "MENU") {
    drawMenu();
  } else if (gameState === "PLAYING") {
    if (!bgMusic.isPlaying()) {
      bgMusic.loop(); // Looping the background music during gameplay
    }
    playGame();
  } else if (gameState === "GAME OVER") {
    bgMusic.stop(); // Stops the music on game over
    drawGameOver();
  } else {
    //info
    drawInfo();
  }
}

function drawInfo() {
  background("#060C15");
  noStroke();
  fill(255);
  for (let star of stars) {
    circle(star.x, star.y, star.r);
    star.y += star.yv;
    if (star.y > height + 5) star.y = -5;
  }
  textSize(16);
  text(
    "Connect the Arduino\n Wear the glove\n, Control the UFO through tilting your hand \nLeft, Right, Up and Down.",
    width / 2,
    height / 2
  );
  stroke(255);
  Button("MENU", width / 2, height - 100, 100, 40);
}

// ---------------Function to display the game menu
function drawMenu() {
  background(menuImage);
  fill(200, 100, 100);
  stroke(200, 100, 100);
  textSize(62);
  strokeWeight(2);
  text("UFO ESCAPE", width / 2, 140);
  fill(255);
  text(
    "UFO ESCAPE",
    width / 2 - map(mouseX, 0, width, -5, 5),
    135 - map(mouseY, 0, height, -2, 2)
  );
  let x, y;
  if (frameCount % 40 < 20) {
    x = width / 2 - map(frameCount % 40, 0, 20, -5, 5);
    y = 400;
  } else {
    x = width / 2 - map(frameCount % 40, 20, 40, 5, -5);
    y = 400;
  }
  Ufo(x, y, 60, 30);
  //startButton
  Button("START", width / 2, 550);
  //info page
  Button("INFO", width - 115, 40, 100, 40);
}

// Function to handle gameplay logic
function playGame() {
  fill(255);
  textSize(25);
  text(`Score: ${score}`, width / 2, 50);

  player.show(); // Displays the player
  player.move(); // Moves the player based on key inputs

  // Adding a new obstacle at intervals
  if (frameCount % 120 == 0) {
    obstacles.push(new Obstacle());
  }

  // Updates and displays obstacles
  for (let i = obstacles.length - 1; i >= 0; i--) {
    obstacles[i].show();
    obstacles[i].update();
    // Checks for collisions
    if (player.collidesWith(obstacles[i])) {
      gameOverImage = get();
      gameState = "GAME OVER";
    }
    // Removes obstacles that have moved off the screen and increment score
    if (obstacles[i].y > height) {
      obstacles.splice(i, 1);
      i--;
      score++;
    }
  }
}

// Function to display the game over screen
function drawGameOver() {
  if (gameOverImage) image(gameOverImage, 0, 0);
  fill(200, 100, 100);
  stroke(200, 100, 100);
  textSize(46);
  text("GAME OVER", width / 2, height / 2 + 50);
  fill(255);
  text(score, width / 2, height / 2 - 50);

  Button("RESTART", width / 2, 550);
}

// Function to reset the game to its initial state
function resetGame() {
  obstacles = []; // Clear existing obstacles
  score = 0; // Reset score
  player = new Player(); // Reinitialize the player
  stars = [];
  for (let i = 0; i < 100; i++) {
    let r = random(1, 3);
    stars.push({
      x: random(width),
      y: random(height),
      r: r,
      yv: map(r, 1, 3, 0.01, 0.1),
    });
  }
}

// Player class
class Player {
  constructor() {
    this.width = 60; // Width of the UFO
    this.height = 30; // Height of the UFO
    this.x = width / 2; // Starting x position
    this.y = height - 100; // Starting y position
    this.gotoX = this.x;
    this.gotoY = this.y;
    this.speed = 5;
  }

  // Function to display the UFO
  show() {
    this.y = lerp(this.y, this.gotoY, 0.1);
    this.x = lerp(this.x, this.gotoX, 0.1); //and change this.gotoX
    stroke(200, 100, 100);

    Ufo(this.x, this.y, this.width, this.height);
  }

  // Function to move the player based on arrow key inputs
  move() {
    let moveSpeed = 10; // Speed multiplier
    this.gotoX += gyroData.x * moveSpeed;
    this.gotoY += gyroData.y * moveSpeed;

    this.gotoX = constrain(this.gotoX, 30, width - 30);
    this.gotoY = constrain(this.gotoY, 30, height - 30);

    this.x = lerp(this.x, this.gotoX, 0.1);
    this.y = lerp(this.y, this.gotoY, 0.1);

    if (frameCount % 100 === 0) {
      obstacles.push(new Obstacle()); // Add new obstacles
    }

    this.gotoX = constrain(this.gotoX, 30, width - 30); // Keeps player within horizontal bounds
    this.gotoY = constrain(this.gotoY, 30, height - 30); // Keep player within vertical bounds
    // //This is only so the player cannot exist the canvas
  }

  // Function to detect collision with obstacles
  collidesWith(obstacle) {
    return (
      dist(
        obstacle.x + obstacle.radius,
        obstacle.y + obstacle.radius,
        this.x,
        this.y
      ) < 45
    );
  }
}

// Obstacle class
class Obstacle {
  constructor() {
    this.radius = random(15, 30); // Random radius for obstacle
    this.x = random(this.radius, width - this.radius); // Random x position
    this.y = -this.radius; // Starts off-screen so it looks like its coming towards you
  }

  // Function to display the rocks
  show() {
    image(rockImage, this.x, this.y, this.radius * 2, this.radius * 2); // Draws them as a circle
  }

  // Function to update obstacle position
  update() {
    this.y += map(score, 0, 10, gameSpeed, gameSpeed + 5); // Moves obstacles down the screen
    //this will make game harder as time grows
  }
}
function Button(txt, x, y, w = 200, h = 60) {
  fill(255, 50);
  if (
    mouseX > x - w / 2 &&
    mouseX < x + w / 2 &&
    mouseY > y - h / 2 &&
    mouseY < y + h / 2
  ) {
    fill(255, 80);
    if (mouseIsPressed) {
      mouseIsPressed = false; //so only one click happnes
      action(txt);
    }
  }
  rect(x, y, w, h, h / 2);
  fill(255);
  textSize(h / 2);
  text(txt, x, y);
}
function action(txt) {
  switch (txt) {
    case "START":
      gameState = "PLAYING";
      resetGame();
      break;
    case "RESTART":
      gameState = "MENU";
      resetGame();
      break;
    case "INFO":
      stars = [];
      for (let i = 0; i < 100; i++) {
        let r = random(1, 3);
        stars.push({
          x: random(width),
          y: random(height),
          r: r,
          yv: map(r, 1, 3, 0.01, 0.1),
        });
      }
      gameState = "INFO";
      break;
    case "MENU":
      gameState = "MENU";
      break;
  }
}

function Ufo(x, y, w, h) {
  fill(255); // Sets color to white
  rectMode(CENTER);
  rect(x, y, w, h, 20); // Draws the UFO's body
  fill(20); // Sets the glass color to red
  arc(x, y - h / 4, w / 2, h / 1, PI, 0, CHORD); // Draws the glass
  stroke(255);
  let a = map(x, 0, width, 0, PI / 4);

  arc(x, y - h / 4, w / 2 - 5, h - 5, -PI / 4 - a, -PI / 6 - a);

  for (let i = 1 + frameCount; i < 10 + frameCount; i++) {
    let x_ = map(i % 10, -1, 10, -30, 30);
    circle(x + x_, y, 5);
  }
}

Arduino Code:

#include <Wire.h>
#include <Adafruit_L3GD20_U.h>

// Initialize the L3GD20 object
Adafruit_L3GD20_Unified gyro = Adafruit_L3GD20_Unified(20); // Sensor ID

void setup() {
  Serial.begin(9600); // Start serial communication at 9600 baud

  // Initialize the L3GD20 gyroscope
  if (!gyro.begin()) {
    Serial.println("Failed to find L3GD20 gyroscope");
    while (1) {
      delay(10); // Halt if initialization fails
    }
  }
}

void loop() {
  sensors_event_t event;
  gyro.getEvent(&event);

  // Send the X and Y gyro data with a newline at the end
  Serial.print(event.gyro.x, 4); // X-axis gyro data with 4 decimal places
  Serial.print(","); // Comma separator
  Serial.println(event.gyro.y, 4); // Y-axis gyro data with 4 decimal places

  delay(100); // Adjust delay as needed
}

What are some aspects of the project that you’re particularly proud of?

To be honest, the whole final project makes me proud that I was able to create such a complicated code with actual use in the end. The glove prototypes are also something that I was proud of because I have never created a glove from scratch before. I am just glad that it all worked out in the end and that the cables stopped popping out of the gloves whenever someone would put them on.

What are some areas for future improvement?

I would say that expanding on the game itself and making different levels and maps for the UFO to fly around in would also exemplify the game. Also, creating a button in the glove to restart once you press it would make it easier for the player to control the whole game if they lose.