For this assignment, I decided to do a simple portrait as it was my first time coding. However, I did want to play around with the background and try new codes, so I decided to do a background that changes color at random when you toggle around the mouse. At first, I thought it was too hard, and it took me time to figure out the codes, but I watched some tutorials on YouTube and went over the presentations in class, and I finally figured it out and was definitely proud of the outcome. I honestly found it difficult to figure out the shapes and where everything goes, but the Mouse X and Y function was a lifesaver, and it was a matter of getting the hang of it. The hair, however, was my least favorite part as it was getting really confusing with the placements of each circle. Maybe there was an easier way instead of just repeating each circle in a different location, so hopefully, I will figure it out in the future. Overall, after a long process of figuring out each shape, size, and code I’m happy with the outcome.
here is the background code which I am most proud of:
let x, y, r, g, b;
function setup() {
createCanvas(400, 400);
}
function draw() {
// background that changes color
background(mouseX, mouseY, 100, 7);
r = random(0, 255);
g = 0;
b = random(0, 255);
x = random(0, 600);
y = random(0, 400);
noStroke();
fill(r, g, b, 100);
circle(x, y, 24);
When I look at how F1 drivers train their reflexes, one of the machines inspired me to create this game. However, this game is not to train your reflexes but your memory. Inspired by this, my project, “SpongBlob” aims to enhance users’ color memory skills through an interactive game developed using Arduino and p5.js. This game not only serves as an entertaining experience but also as an educational tool to study color perception and memory. The game is also aimed for kids who are at risk of losing their focus span to the developing social media and short video world, which makes this game a tool for them to gain their focus back. This inspired the SpongeBob theme, so that it is more kid friendly.
Game setup:
The game has an interactive set up with 4 buttons replacing the keys on the keyboard. By pressing the buttons, the players are interacting with the game on the P5 sketch. This is shown in the following pictures:
As shown in the picture, the aesthetics of the game are really important especially since this game is also targeting kids. Having a nice set up of “Bikini Bottom” was really important in my opinion to make sure that the game is not just functioning well but also looks appealing to the players.
The use of these buttons makes it so much easier than the keyboard according to the users that tested the game. Moreover, when user testing, most of the students suggested having a less hectic and messy background so that the player can focus and memorize the colors of the circles without getting distracted by the background. There was some debate over whether the users should get to see the score they achieved or not. Since it is a memory game, I thought the players should focus more on memorizing and enhancing their focus span than focus on the score, so I did not make it visible to the players.
// Define pin numbers for buttons
const int leftButtonPin = 2;
const int upButtonPin = 3;
const int downButtonPin = 4;
const int rightButtonPin = 5;
void setup() {
Serial.begin(9600);
// Set button pins as inputs
pinMode(leftButtonPin, INPUT);
pinMode(upButtonPin, INPUT);
pinMode(downButtonPin, INPUT);
pinMode(rightButtonPin, INPUT);
}
void loop() {
// Read button states and send data over serial
int leftButton = digitalRead(leftButtonPin);
int upButton = digitalRead(upButtonPin);
int downButton = digitalRead(downButtonPin);
int rightButton = digitalRead(rightButtonPin);
// Send button states to serial
Serial.print(leftButton);
Serial.print(",");
Serial.print(upButton);
Serial.print(",");
Serial.print(downButton);
Serial.print(",");
Serial.println(rightButton);
// Delay to control the rate of data transmission
delay(100);
}
This Arduino sketch manages four buttons connected to the board, using pins 2, 3, 4, and 5 for left, up, down, and right buttons respectively. The setup() function initializes serial communication at 9600 baud and sets the button pins to input mode. The loop() function continuously reads the state of each button using digitalRead() and sends these states over the serial connection using Serial.print(). Each button state is outputted sequentially and separated by commas, with a newline at the end of each set via Serial.println(). A delay(100) is included to control the rate of data transmission, preventing data overflow and ensuring manageable communication speeds.
function drawGamePage() {
//song.play();
background(backgroundImage);
if (currentPage == 2 || currentPage == 3) {
let selected = -1;
// Map arrow keys to grid positions
if (ArrowUp == 1 && ButtonPressed == 0) {
selected = 0; // Top-left
ButtonPressed = 1;
} else if (ArrowDown == 1 && ButtonPressed == 0) {
selected = 1;
ButtonPressed = 1; // Top-right
} else if (ArrowRight == 1 && ButtonPressed == 0) {
selected = 2;
ButtonPressed = 1; // Bottom-left
} else if (ArrowLeft == 1 && ButtonPressed == 0) {
selected = 3;
ButtonPressed = 1; // Bottom-right
} else if (
ArrowLeft == 0 &&
ArrowRight == 0 &&
ArrowUp == 0 &&
ArrowDown == 0
) {
ButtonPressed = 0;
}
// Check if the selected color matches the currentColor
if (selected != -1) {
if (gridColors[selected] == currentColor) {
prepareNextLevel();
} else {
currentPage = -1;
}
}
}
fill(nextColor);
ellipse(windowWidth / 2, 200, 120, 120);
let padding1 = windowWidth / 2 - 75;
let padding2 = windowHeight / 2 - 100;
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
fill(gridColors[i * gridSize + j]);
ellipse(
padding1 + j * (ellipseSize + 50),
padding2 + i * (ellipseSize + 50),
ellipseSize,
ellipseSize
);
}
}
}
function drawOverPage() {
//laugh.play();
background(gameOverImage);
textAlign(CENTER, CENTER);
textSize(32);
fill(0);
text("Game Over", width / 2, height / 2 - 100);
// Draw "Again" button
fill(200); // Light grey button background
rect(width / 2 - 100, height / 2, 200, 50);
fill(0); // Black text
text("Again", width / 2, height / 2 + 25);
// Draw "Home" button
fill(200); // Light grey button background
rect(width / 2 - 100, height / 2 + 70, 200, 50);
fill(0); // Black text
text("Home", width / 2, height / 2 + 95);
}
function drawInstructionPage() {
//song.play();
background(instructionsImage);
fill(0);
textSize(24);
textAlign(LEFT, LEFT);
text("Instructions", width - 810, height - 710);
textSize(16);
text("Welcome to the Memory Game! Here's how to play:", width - 840, height - 655);
text("1. Memorize the colors shown on top of the screen.", width - 840, height - 625);
text("2. Use the buttons to select the correct color from the grid.", width - 840, height - 595);
text("3. Match the colors correctly to advance to the next level.", width - 840, height - 565);
text("Press Space Bar to select Serial Port", width - 840, height - 535);
textAlign(CENTER, CENTER);
rect(width - 780, height / 2 - 40, 80, 40);
fill(200);
text("Continue", width - 740, height / 2 - 20);
}
function draw() {
background(220);
if (!serialActive) {
text("Press Space Bar to select Serial Port", 20, 30);
} else {
text("Connected", 20, 30);
}
if (currentPage == 0) {
drawStartPage();
} else if (currentPage == 1) {
drawInstructionPage();
} else if (currentPage == 2) {
drawFirstPage();
} else if (currentPage == 3) {
drawGamePage();
} else if (currentPage == -1) {
drawOverPage();
}
}
The P5 sketch code is maily about the game page which has the conditions that make the game more interesting. The drawGamePage() function in this p5.js code is designed for a memory game, where it handles the game logic and user interactions during gameplay. It first sets the background and checks if the game is on specific pages (like a game level). The function maps arrow key inputs to grid selections, managing state with a ButtonPressed flag to avoid repeated selections. If a selected color from the grid matches a target color (currentColor), the game progresses to the next level; otherwise, it switches to a game over page. It dynamically renders colored ellipses on a grid, representing game elements. Additionally, other functions like drawOverPage() handle the game over screen, displaying buttons for restarting or returning to the home screen, and drawInstructionPage() displays the game instructions. The main draw() function coordinates these pages based on the current game state, updating the display and handling transitions between different parts of the game, such as starting, instructions, gameplay, and game over scenarios.
Aspects of the project I am proud of:
Game Logic Implementation: The effective mapping of user inputs (arrow keys) to game actions and the incorporation of game state management ensures that the gameplay is both challenging and engaging.
Serial Communication: The use of serial communication to connect Arduino inputs to the p5.js game logic demonstrates a robust application of cross-platform communication techniques, vital for interactive media projects.
Areas for Future Improvement:
Complexity and Features: Introducing additional levels of difficulty, more complex game mechanics, or multiplayer capabilities could increase the game’s replay value and appeal to a broader audience. Also having new shapes, characters to memorize and not just the circles can be fun!
Extensive Testing and Debugging: Conducting more thorough testing across different platforms and setups could identify and resolve any existing bugs or issues with user interactions, ensuring a smooth and reliable user experience.
To summarize this project was about making a game that was like Russian Roulette but without the gun. Instead two players would stand opposite each other and would have two options. First there is a “gun” (it just exists in the p5js sketch) that is loaded with a certain amount of live rounds and the rest are blank and then like the real Russian Roulette each player would get chances to shoot the other opponent or not or shoot themselves. If a player shoots themselves and its a blank then they get another turn making it more likely that the next round will be live and hence more likely that they will win and if they do shoot the other player and it is blank then their turn ends and they swap and the cycle continues. There is also 3 lives for each player and if one of them loses, i.e. runs out of lives they have to use a shock pen. The purpose of the shock pen was to create some kind of stakes just like the real Russian Roulette but more sane.
To me this project represents more than a fun creation. Genuinely, while I did enjoy it this was a huge learning experience and not just when it came to coding and soldering and whatever other physical things I did. I’ll explain how so as I go from beginning to end.
I originally had this idea before the midterm and I thought about creating a Russian roulette for my midterm. I chose a different path and made a Mexican Standoff game for my midterm. It was arguably slightly harder to make and for me going into it I really had little idea what I was doing. I learned how to change from different scenes (thanks to Pi telling me about a scene management class), but more importantly I learned a lot about planning as halfway through making it I had to sit down and I sketched a diagram of what I wanted the game to be like on a whiteboard in a library study room.
So in the midterm I just began to plan things properly and I wasn’t very efficient at it. So going into this project I also planned but unlike before I also had to consider the physical: Making and wiring all the components that I’ll need. Even planning it was difficult to be honest. Difficult because I didn’t really know where to start but also I didn’t know what was available in the intro to IM lab. I was very fortunate to learn more about what was available through our lessons where we learned about renting equipment but also saw the different sections in the lab, like the consumables or the blue closet (i dont know if it has a name).
Then when I had to start I had to learn serial communication. I think a lot of people can agree when I say it was very confusing and I would argue it remained a problem with my project from beginning to end even though I thought I understood it at the beginning. That was the biggest hurdle by far mentally because it had been bothering me for a couple days before because when I would go to try to understand the code from professor Aaron and change it to fit my project (4 buttons, a potentiometer and 2 LEDs) it wouldn’t work even though just before that it I felt I understood it when working with Snehil and Khalifa on the previous assignment.
Include some pictures / video of your project interaction
How does the implementation work?
Description of interaction design
Description of Arduino code and include or link to full Arduino sketch
Description of p5.js code and embed p5.js sketch in post
Description of communication between Arduino and p5.js
What are some aspects of the project that you’re particularly proud of?
What are some areas for future improvement?
The way that my project works is first that the Arduino detects the button presses from each of the four buttons as well as the input from the potentiometer. Then using serial communication the Arduino sends the signals received from the buttons and potentiometer to the p5js sketch and in response the sketch relays back a message to create a handshake.
The p5js code is slightly more complex. First I have multiple scenes each of which managed by a scene management class where depending on the number that the class receives it calls on different functions, each of those functions are actually the scenes. For example the disclaimer scene is actually just a function which has all the shapes and so on being drawn and the function itself is only called upon when the scene number is 2. The same goes for the other scenes. As for the game itself: Firstly, after the animation there is an array of booleans totaling 6. If an element is true it means it is a live round and vice versa and then there is another global variable called selected_round and that is added up incrementally to signify which round is being selected from the chamber. As for the players, they have two options represented as two buttons. One button (the red one) means that the player can shoot themselves, and the other (the blue button) means that the player can shoot the other person in front of them. If the player chooses to shoot themselves and it’s a blank then it remains their turn and makes it more likely that the next round will be live and hence more likely that they can win the round. However, if a player chooses to shoot the other but it’s blank they switch turns making the odds in the favor of the other player as its now more likely for them that the next round is live. Furthermore, I added difficulties which means that depending on the difficulty chosen by the player there will be a varying amount of live rounds with the easiest difficulty being “Standard” which means only one live round, and the hardest being “Masochist” which can have between 3 and 5. Each of the players have 3 lives, then at the end of it all, if they choose to the loser has to use a shock pen so that there are stakes and to make the game more interesting.
I’m particularly proud of the UI elements, specifically holding down a button to go to the next page and so on. To me it felt the most satisfying.
For the future there are a lot of things I would change. I would have started by making the game aspect first, as I had made the mistake of starting at the start page ironically. This would have made my life a lot easier but at the same time I am somewhat glad that I did what I did because I felt that the UI was a large part of what I liked and I hope others liked about the game.
The sketch on p5js may be a bit weird because I had coded it on vscode and made it such that the canvas fit my mac screen so it may take a while to load and/or may be hard to use.
My game was all about creating a virtual archery game and linking a flex sensor to the bow, so it detects the bending motion and once bent the arrow shoots onto the target. For the theme of the entire game, I decided to link it with my childhood as well and use Minecraft as the main theme. As a 9 year old, I used to go to the archery range and play Minecraft at the time so it brought up a sense of nostalgic feeling.
For the bow, in order to give it a pixelated look, I decided to super glue the tiny wooden square blocks and paint over it. I attached the flex sensor on the top of the bow and attached a string to the flex sensor so when the string is pulled it shoots the arrow.
Future Improvements:
For future improvements, I’d like to add more visuals. I felt like it would’ve been more engaging if I did.
const serial = new p5.WebSerial();
let startButton;
let portButton;
let closeButton;
let sensorValue = 0;
let width = 900;
let height = 506;
let arrowSpeed = 5; // Speed at which arrow moves
let arrowDirection = 1;
let score = 0;
let shooting = false; // Indicates whether the arrow is currently being shot
let arrowX = width / 2; // X-coordinate of the arrow
let arrowY = height; // Y-coordinate of the arrow
let arrowScaleX = 1;
let arrowScaleY = 1;
let arrowWidth = 60;
let arrowHeight = 120;
let targetX = width / 2;
let targetY = height / 2;
let targetRadius = 100;
let started = false;
let bgImg;
let arrowImg;
let targetImg;
let sliderX = width - 50;
let sliderY = height / 2;
let lastScore = 0;
let textOpacity = 0;
let sliderHeight = 0;
let startShootFlag = 0;
let sliderIncrementor = 1;
let sliderTotalHeight = 100;
let startShootThreshHold = 40; //set flex sensor value at which we start for targetting
function allSerialStuff() {
if (!navigator.serial) {
alert("WebSerial is not supported in this browser. Try Chrome or MS Edge.");
}
// check for any ports that are available:
serial.getPorts();
// if there's no port chosen, choose one:
serial.on("noport", makePortButton);
// open whatever port is available:
serial.on("portavailable", openPort);
// handle serial errors:
serial.on("requesterror", portError);
// handle any incoming serial data:
serial.on("data", serialEvent);
serial.on("close", makePortButton);
// add serial connect/disconnect listeners:
navigator.serial.addEventListener("connect", portConnect);
navigator.serial.addEventListener("disconnect", portDisconnect);
}
function serialEvent() {
sensorValue = Number(serial.read());
console.log(sensorValue);
//if certain value from flex sensor get passed we get prepared for the shoot
if (!shooting && sensorValue > startShootThreshHold) {
startShootFlag = 1;
sliderHeight = sensorValue + 20; // add 20 to elevate the value we need something in between 0-120
}
//if that value again crossed then we shoot
if (!shooting && sensorValue < startShootThreshHold && startShootFlag) {
startShootFlag = 0;
if (!shooting && arrowY == height) {
shooting = true; // Start shooting
}
}
}
// if there's no port selected,
// make a port select button appear:
function makePortButton() {
// create and position a port chooser button:
portButton = createButton("Choose Port");
portButton.position(innerWidth / 2, 10);
portButton.center("horizontal");
// give the port button a mousepressed handler:
portButton.mousePressed(choosePort);
}
// make the port selector window appear:
function choosePort() {
if (portButton) portButton.show();
serial.requestPort();
}
// open the selected port, and make the port
// button invisible:
// open the selected port, and make the port
// button invisible:
function openPort() {
// wait for the serial.open promise to return,
// then call the initiateSerial function
serial.open().then(initiateSerial);
// once the port opens, let the user know:
function initiateSerial() {
console.log("port open");
}
// hide the port button once a port is chosen:
if (portButton) portButton.hide();
makeCloseButton();
if (closeButton) closeButton.show();
}
// pop up an alert if there's a port error:
function portError(err) {
alert("Serial port error: " + err);
}
// read any incoming data as a string
// (assumes a newline at the end of it):
// try to connect if a new serial port
// gets added (i.e. plugged in via USB):
function portConnect() {
console.log("port connected");
serial.getPorts();
}
// if a port is disconnected:
function portDisconnect() {
serial.close();
console.log("port disconnected");
}
// if there's no port selected,
// make a port select button appear:
function makeCloseButton() {
// create and position a port chooser button:
closeButton = createButton("Close Port");
closeButton.position(innerWidth / 2, 10);
closeButton.center("horizontal");
// give the close port button a mousepressed handler:
closeButton.mousePressed(closePort);
}
function closePort() {
serial.close();
if (closeButton) closeButton.hide();
}
function preload() {
bgImg = loadImage("/assets/background.jpg");
targetImg = loadImage("/assets/target.png");
arrowImg = loadImage("/assets/arrow.png");
}
function setup() {
createCanvas(width, height);
startButton = createButton("Start Game");
startButton.addClass("start-button");
startButton.position(innerWidth / 2, innerHeight / 2 + 10);
startButton.center("horizontal");
startButton.mousePressed(startGame);
allSerialStuff();
}
function draw() {
imageMode(CORNERS);
image(bgImg, 0, 0, width, height);
if (!started) {
drawMenu();
} else {
// Draw target
drawTarget();
if (startShootFlag) {
drawSlider();
}
//Draw score
textAlign(LEFT, TOP);
textSize(26);
fill("red");
text("Score: " + score, 10, 10);
makeShooting();
drawArrow();
drawAddedScore();
}
}
function drawMenu() {
textSize(48);
fill("#ff0033");
textStyle(BOLD);
textAlign(CENTER, BASELINE);
text("VR Archery", width / 2, height / 2 - 100);
}
function startGame() {
if (startButton) startButton.hide();
started = true;
}
function makeShooting() {
if (shooting) {
// Calculate the trajectory towards the target
let deltaY = height - targetY; // Difference in y-coordinates between arrow and target
arrowY -= deltaY / 50; // Move the arrow towards the target
if (arrowScaleY > 0.4) {
arrowScaleY -= 0.004;
}
if (arrowScaleX > 0.4) {
arrowScaleY -= 0.005;
}
// Stop shooting when arrow reaches the target
if (arrowY - (arrowHeight * arrowScaleY) / 2 <= targetY) {
shooting = false;
// Check if the arrow hits the target
let distance = dist(arrowX, arrowY - (arrowHeight * arrowScaleY) / 2, targetX, targetY); // Calculate distance between arrow tip and target center
if (distance <= targetRadius - 20) {
console.log("Hit!");
arrowSpeed = 0;
textOpacity = 255;
updateScore(distance);
//reset arrow after 2 seconds
setTimeout(() => {
arrowY = height;
arrowScaleX = 1;
arrowScaleY = 1;
arrowSpeed = 5;
textOpacity = 0;
}, 2000);
} else {
console.log("Miss!");
arrowY = height;
arrowScaleX = 1;
arrowScaleY = 1;
shooting = false;
}
}
} else {
// Move arrow
arrowX += arrowSpeed * arrowDirection;
if (arrowX >= width || arrowX <= 0) {
arrowDirection = -arrowDirection; // Reset arrow when it goes beyond the canvas
}
}
}
function drawTarget() {
imageMode(CENTER);
image(targetImg, targetX, targetY, targetRadius * 2, targetRadius * 2);
}
function drawArrow() {
imageMode(CENTER);
image(arrowImg, arrowX, arrowY, arrowWidth * arrowScaleX, arrowHeight * arrowScaleY);
}
function drawAddedScore() {
fill(80, textOpacity);
text("+ " + lastScore, targetX + 70, targetY - 60);
}
function drawSlider() {
line(sliderX, sliderY, sliderX, sliderY + 120);
line(sliderX - 10, sliderY, sliderX + 10, sliderY);
line(sliderX - 10, sliderY + 60, sliderX + 10, sliderY + 60);
line(sliderX - 10, sliderY + 120, sliderX + 10, sliderY + 120);
fill(sliderHeight + 100, 200, 0);
rect(sliderX - 10, sliderY, 20, sliderHeight);
}
//update score
function updateScore(distance) {
console.log(distance);
if (distance < 10) {
score += 100;
lastScore = 100;
} else if (distance < 25) {
score += 80;
lastScore = 80;
} else if (distance < 40) {
score += 60;
lastScore = 60;
} else if (distance < 55) {
score += 40;
lastScore = 40;
} else {
score += 20;
lastScore = 20;
}
}
//handle mouse click event
function mouseClicked() {
if (!shooting && arrowY == height) {
//If arrow is not currently being shot
shooting = true; // Start shooting
}
}
function windowResized() {
if (startButton) startButton.position(innerWidth / 2, innerHeight / 2 + 10).center("horizontal");
if (portButton) portButton.center("horizontal");
if (closeButton) closeButton.center("horizontal");
}
The initial idea of this project is a bit different from what I ended up with. The initial idea was designed for two players, each placing one hand on a heart rate monitor. This game uses heart rate data to measure and display the level of affection or excitement between the participants. The faster the heartbeats, the higher the presumed love connection. But after I heard that we’d have to return the items we borrowed from the IM lab I thought it would be better if I order my own two heart rate monitors and solder them myself, then I wouldn’t have to take apart my hard work. With my not-so-good soldering skills I ruined one heart rate monitor and had only one to work with. I had to improvise and solve this issue working with only one heart rate monitor which led to changing the theme of the game a bit. Professor Aaron helped me with coming up with a new game that measures the players Happiness Level by reading their heart rates through the heart monitor. The game was initially supposed to start by pressing on a yellow button but due to time constrains and many other technical difficulties, such as linking the Arduino and the P5 together, I still managed to make it work fine with the keyboard even though I feel like the yellow button gives it more of a “gamey” feel to it which is what I would’ve much preferred.
Arduino Code:
const int buttonPin = 3; // Pin where the button is connected
const int heartRatePin = A0; // Analog pin for heart rate sensor
int heartRateValue = 0;
bool buttonPressed = false;
void setup() {
pinMode(buttonPin, INPUT_PULLUP); // Set the button pin as input with internal pull-up resistor
pinMode(heartRatePin, INPUT);
Serial.begin(9600);
while (Serial.available() <= 0) { // on/blink while waiting for serial data
Serial.println("0,0");
delay(50);
}
}
void loop() {
heartRateValue = analogRead(heartRatePin); // Read the value from the heart rate sensor
int bpm = calculateBPM(heartRateValue); // Convert the analog reading to BPM
// Read button state
int buttonState = 1 - digitalRead(buttonPin);
while (Serial.available()) {
int left = Serial.parseInt();
if (Serial.read() == '\n') {
Serial.print(buttonState);
Serial.print("0,50");
Serial.println(bpm);
}
}
// Always send the current heart rate and button state
}
// Function to simulate BPM calculation - replace this with your sensor-specific calculation
int calculateBPM(int sensorValue) {
return sensorValue / 10; // Simplified calculation for demonstration
}
P5.Js Code:
class MainMenu extends Menu {
constructor(id) {
super(id);
this.pos = createVector(width / 2, height / 2.7);
this.size = 240;
this.strokeCol = color(
random(100, 255),
random(100, 255),
random(100, 255)
);
this.hearts = [];
for (let i = 0; i < 20; i++) {
this.hearts.push({ x: random(width), y: random(height) });
}
this.random_seed = random(100, 10000); //use for heard animations in the back
this.heartPos = { x: width / 2, y: height * 2 };
}
render() {
background("#24182e");
textAlign(CENTER, CENTER);
textSize(44);
textFont(pixel_font);
fill("#8249c6");
stroke(this.strokeCol);
strokeWeight(4);
text("HAPPINESS LEVEL", width / 2, 50);
//change strokcol every 20 farmes
if (frameCount % 60 == 0) {
this.strokeCol = color(
random(100, 255),
random(100, 255),
random(100, 255)
);
this.random_seed = random(100, 10000);
}
textSize(30);
stroke(200, 100, 100);
push();
randomSeed(this.random_seed);
textFont("arial");
for (let h of this.hearts) {
for (let h2 of this.hearts) {
if (dist(h.x, h.y, h2.x, h2.y) < 10) {
strokeWeight(2);
line(h.x, h.y, h2.x, h2.y);
}
}
text("♥", h.x, h.y);
h.x = lerp(h.x, random(width), 0.01);
h.y = lerp(h.y, random(height), 0.01);
}
pop();
push();
textFont("arial");
textSize(160);
this.heartPos.y = lerp(this.heartPos.y, height / 1.5, 0.1);
stroke(255);
fill("#B28CDEAA");
text("♥", this.heartPos.x, this.heartPos.y);
textSize(30);
noStroke();
fill(255);
textFont(pixel_font);
text(
"PLACE YOUR FINGER ON THE HEART",
this.heartPos.x,
this.heartPos.y + 100
);
pop();
noStroke();
}
}
class GameMenu extends Menu {
constructor(id) {
super(id);
this.heart = new Heart(createVector(width / 2, height / 2.7), 240);
}
render() {
textAlign(CENTER, CENTER);
textSize(18);
background("#24182e");
fill("#A88DC7");
text("CHECKING YOUR LOVE LEVEL!", width / 2, height - 30);
fill("#8249C67C");
textFont("arial");
textSize(34);
for (let i = 0; i < 12; i++) {
for (let j = 0; j < 8; j++) {
let x = map(i, 0, 11, 0, width);
let y = map(j, 0, 7, 0, height);
if (frameCount % 40 < 20) {
if (i % 2 == 0 || j % 2 == 0) {
text("♥", x, y);
}
} else {
if (i % 2 != 0 || j % 2 != 0) {
text("♥", x, y);
}
}
}
}
this.heart.render();
}
update() {
this.heart.update();
// Removed the timer decrement and check
}
reset() {
this.heart = new Heart(createVector(width / 2, height / 2.7), 220);
}
}
class EndMenu extends Menu {
constructor(id) {
super(id);
this.finalScore = null;
this.hearts = [];
for (let i = 0; i < 2; i++) {
this.hearts.push({ x: random(width), y: random(height) });
}
this.random_seed = random(100, 10000); //use for heard animations in the back
}
render() {
background("#24182e");
push();
stroke(200, 100, 100);
randomSeed(this.random_seed);
textFont("arial");
textSize(34);
for (let h of this.hearts) {
for (let h2 of this.hearts) {
if (dist(h.x, h.y, h2.x, h2.y) < 100) {
line(h.x, h.y, h2.x, h2.y);
}
}
text("♥", h.x, h.y);
h.x = lerp(h.x, random(width), 0.01);
h.y = lerp(h.y, random(height), 0.01);
}
if (frameCount % 60 == 0) {
this.random_seed = random(100, 10000);
}
pop();
fill("#A88DC7");
stroke(255);
textFont(pixel_font);
textSize(60);
textAlign(CENTER, CENTER);
text("THANK YOU !", width / 2, 160);
noStroke();
textSize(24);
// text(
// `${this.finalScore}\n\nYOUR COMPATIBILITY SCORE`,
// width / 2,
// height / 1.5
// );
// push();
// noStroke();
// fill(0);
// rect(0, 0, width, height);
// fill(255);
// textStyle(BOLD);
// textAlign(CENTER, CENTER);
// textSize(96);
// text("GAME OVER", width / 2, height / 4);
// textSize(40);
// text(`COMPATIBILITY SCORE: ${this.finalScore}`, width / 2, height / 2);
// textStyle(NORMAL);
textSize(16);
text("TRY AGAIN?", width / 2, height - 60);
text("Yes", 100, height - 60);
text("No", width - 100, height - 60);
push();
textFont("arial");
pop();
// textSize(40);
// text("YES? OR NO?", width / 2, 640);
// pop();
}
reset() {
this.finalScore = null;
}
}
I’m content with the final product despite getting extremely sick two days before the showcase while having other final projects due the day before it. For future improvements I’d like to incorporate my initial idea and also add a different game mode, Lie Detector Mode, which sounds fun to make with using the heart rate monitor. Overall, I feel like I got exposed to many things in this course which makes me much more comfortable with the things we’ve been working with during the entirety of this semester. I’d also like to thank professor Aaron for being extremely patient and helpful with me 🙂
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
}
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.
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.
“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!
“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.