A. Dancing Particles – Avril – Professor Gottfried Haider
B. PROJECT CONCEPT AND DESIGN:
At the start, my project was all about playing with on-screen particles in different ways. I thought about using my hands to move them around, like bringing them closer or spreading them out, and even blowing on them to make them go in specific directions. However, when I tried using a joystick and a loudness sensor during testing, it wasn’t as fun and interactive as I hoped.
This is an abstract from my proposal:
Project Goal
This project aims to create an interactive particle system that responds to a user’s breath and hand gestures. The system will use sensors to detect the user’s interactions and manipulate virtual particles in real-time. This project aims to explore how physical inputs can control and shape digital particles, creating an immersive experience.
Hardware Components
Breath sensor: To detect the user’s breathing intensity. This sensor will provide input to control particle speed and shape to intimate the particle when it is blown.
Gesture sensor: To track hand movement and position. This sensor will allow the user to “sculpt” the particles by moving their hand.
After getting advice from my professor, I decided to stick to the idea of using hands to control everything. I wanted to make forces on both sides of the screen that users could control with their hands using an ultrasonic sensor. But when I tried to code this, it became tricky. Making the forces equal on both sides to move the particles was hard. The forces only depended on speed, not how far things were.
I worked with Professor Gohai to try different things, like adding a repellor or changing the repellors to attractors. Sadly, none of these attempts worked, and it felt like we couldn’t go any further.
Then, while working on the code, I got a new idea. Instead of sticking to the original plan, I created a backup plan for the presentation—a game for two players. One player controls a force to attract all the particles in a short time, and the other player tries to stop them and win. This change brought a new twist to the project and showed how it could adapt when faced with challenges.
C. FABRICATION AND PRODUCTION:
Fabrication
The fabrication includes two parts.
Box for the ultrasonic sensor
Box using pepper ghost effect
D. CONCLUSIONS:
I feel this project provided an invaluable learning experience, especially given both the challenges we faced and our ultimate success. There are two key aspects I feel I gained insight from:
First, as Professor Andy noted in the presentation, we encountered numerous obstacles along the way where the path forward was unclear. However, it was those failures and attempts that inspired us to persist in our journey. Pushing through setbacks built our resilience.
Second, as Professor Gohai emphasized, having contingency plans like Plan B is wise, as is drawing from past attempts. However, their comments also reminded me that when facing difficulties with the original goal, another approach is to broaden your resources by engaging others for assistance or alternative perspectives, rather than abandoning the initial aim prematurely.
Overall, this project has taught me about the importance of perseverance in the face of challenges, as well as flexibility in problem-solving through both independent efforts and collaboration. I’m grateful for all that I’ve learned through both the struggles and successes of this experience.
Finally, I would like to extend my gratitude to Professors Gottfried, Eric, and Andy, along with the IMA fellows and Interaction Lab Learning Assistants, for their valuable support in code and fabrication.
A heartfelt special thanks to Professor Moon for his exceptional assistance.
E. DISASSEMBLY:
F. APPENDIX:
inspiration for proposal
My goal is to integrate Arduino sensors like an ultrasonic distance sensor, as shown in the video “TouchDesigner & Arduino – Interactive particles controlled by ultrasonic sensors” (https://www.youtube.com/watch?v=Q949m9bvlD8), a noise sensor as demonstrated in the short video “Interactive particles, Arduino noise sensor + TD” (https://www.youtube.com/shorts/M5ZGyz_cPc0), or a time-of-flight sensor introduced in the tutorial “Arduino Laser Time-of-Flight Sensor Touch-Free Input Tutorial” (https://www.youtube.com/watch?v=VnSfw9ynemc) to map real-time input to the behavior and movement of 3D particle shapes in Processing, as represented in the tutorial “Rasterize 3D (Processing Tutorial)” (https://www.youtube.com/watch?v=WEBOTRboXBE). I also plan to experiment with different mapping techniques, taking inspiration from the “Folding as Drawing|Processing & Arduino Interaction Design” video (https://www.youtube.com/watch?v=mM1lD8l6T-M) that demonstrates how sensor input can control particle structures. Through this project, I seek to deepen my understanding of sensor integration with Processing while experimenting with novel interactive visualization designs. The intended impact is a demonstration of how physical computing concepts can enliven computational art through embodied user input controlling generative visualization systems.
circuit
Code for the game
Arduino (from the example code)
// defines pins numbers const int trigPin1 = 9; const int echoPin1 = 10; const int trigPin2 = 8; const int echoPin2 = 11; // defines variables long duration1; int distance1; long duration2; int distance2; void setup() { pinMode(trigPin1, OUTPUT); // Sets the trigPin1 as an Output pinMode(echoPin1, INPUT); // Sets the echoPin1 as an Input pinMode(trigPin2, OUTPUT); pinMode(echoPin2, INPUT); Serial.begin(9600); // Starts the serial communication } void loop() { // Sensor 1 digitalWrite(trigPin1, LOW); delayMicroseconds(2); digitalWrite(trigPin1, HIGH); delayMicroseconds(10); digitalWrite(trigPin1, LOW); duration1 = pulseIn(echoPin1, HIGH); distance1 = duration1 * 0.034 / 2; if(distance1 <= 16 and distance1 >= 3){ Serial.print(distance1); Serial.print(","); }else{ Serial.print(0); Serial.print(","); } // Sensor 2 digitalWrite(trigPin2, LOW); delayMicroseconds(2); digitalWrite(trigPin2, HIGH); delayMicroseconds(10); digitalWrite(trigPin2, LOW); duration2 = pulseIn(echoPin2, HIGH); distance2 = duration2 * 0.034 / 2; if(distance2 <= 16 and distance2 >= 3){ Serial.print(distance2); }else{ Serial.print(0); } Serial.println(); //delay(10); // Add a delay between readings to make it easier to read on the serial monitor }
Processing
import java.util.ArrayList; import processing.serial.*; import processing.sound.*; Serial serialPort; ArrayList<Mover> movers; Attractor attractor1; Repeller repeller; int NUM_OF_VALUES_FROM_ARDUINO = 2; int arduino_values[] = new int[NUM_OF_VALUES_FROM_ARDUINO]; float smoothed[] = new float[2]; int gameTime = 20; int startTime; int moversRemovedCount = 0; int n =50; int remainingTime; boolean gameStarted = false; SoundFile collectSound; void setup() { size(640, 360); collectSound = new SoundFile(this, "sound.wav"); fullScreen(); serialPort = new Serial(this, "/dev/cu.usbmodem1101", 9600); movers = new ArrayList<Mover>(); for (int i = 0; i < n; i++) { movers.add(new Mover(width, height)); } attractor1 = new Attractor(width / 4, height / 2); repeller = new Repeller(3 * width / 4, height / 2); // Adjust the position as needed startTime = millis(); remainingTime = gameTime; } void draw() { if (!gameStarted) { displayStartInterface(); } else { int elapsedTime = (millis() - startTime) / 1000; remainingTime = max(0, gameTime - elapsedTime); background(255); getSerialData(); fill(0); textSize(24); textAlign(CENTER, CENTER); text("Time: " + remainingTime, width / 2, height - 20); if (arduino_values[0] != 0) { smoothed[0] = smoothed[0] * 0.90 + arduino_values[0] * 0.10; } if (arduino_values[1] != 0) { smoothed[1] = smoothed[1] * 0.90 + arduino_values[1] * 0.10; } if (elapsedTime < gameTime & moversRemovedCount < n) { float x1 = map(smoothed[0], 3, 15, -50, width+50); float x2 = map(smoothed[1], 3, 15, width+50,-50); attractor1.update(x2, height / 2); repeller.update(x1, height / 2); for (int i = movers.size() - 1; i >= 0; i--) { Mover mover = movers.get(i); PVector force1 = attractor1.attract(mover); PVector repelForce = repeller.repel(mover); mover.applyForce(force1); mover.applyForce(repelForce); // Check if the mover is close to attractor1 if (dist(mover.location.x, mover.location.y, attractor1.location.x, attractor1.location.y) < 20) { // Remove the mover from the list when it touches the attractor moversRemovedCount++; movers.remove(i); collectSound.play(); } else { mover.update(); mover.display(); } } attractor1.display(); repeller.display(); } else { if (moversRemovedCount == n) { println("player1=true;"); background(255); fill(0); textSize(32); textAlign(CENTER, CENTER); text("Attractor Win", width / 2, height / 2); println("player1win;"); } else { println("player2=true;"); background(255); fill(0); textSize(32); textAlign(CENTER, CENTER); text("Repellor Win", width / 2, height / 2); println("player2win;"); } } } } class Mover { PVector location; PVector velocity; PVector acceleration; float mass; float minX, minY, maxX, maxY; // Define bounds color moverColor; // Color variable boolean isAttracted = true; Mover(float maxX, float maxY) { location = new PVector(random(width), random(height)); velocity = new PVector(0, 0); acceleration = new PVector(0, 0); mass = 1.0; this.minX = width/10*2; this.minY = height/10*2; this.maxX = width/10*8; this.maxY = height/10*8; moverColor = color(random(255), random(255), random(255)); } void applyForce(PVector force) { acceleration.add(force); } void update() { velocity.add(acceleration); location.add(velocity); acceleration.mult(0); // Constrain the location within the specified bounds location.x = constrain(location.x, minX, maxX); location.y = constrain(location.y, minY, maxY); } void display() { noStroke(); fill(moverColor); ellipse(location.x, location.y, mass * 50, mass * 50); } } class Attractor { float mass; PVector location; float G; Attractor(float x, float y) { location = new PVector(x, y); mass = 400; G = 0.4; } PVector attract(Mover m) { PVector force = PVector.sub(location, m.location); float distance = force.mag(); // For the attractor that collects movers if (m.isAttracted) { distance = constrain(distance, 5.0, 25.0); // Adjust the constraint values as needed } else { // For the repelling attractor distance = constrain(distance, 25.0, 100.0); // Adjust the constraint values as needed } force.normalize(); // For the attractor that collects movers if (m.isAttracted) { float strength = (G * mass * m.mass) / (distance * distance); force.mult(strength); } else { // For the repelling attractor float strength = -G * mass * m.mass / (distance * distance); force.mult(strength); } return force; } void update(float x, float y) { location.set(x, y); } void display() { noStroke(); fill(0, 100, 0, 150); ellipse(location.x, location.y, mass/3, mass/3 ); fill(0); textSize(24); textAlign(CENTER, CENTER); text("Attractor", location.x, location.y); } void integrateMover(Mover mover) { // Remove the mover from the list movers.remove(mover); collectSound.play(); } } class Repeller { float mass; PVector location; float G; Repeller(float x, float y) { location = new PVector(x, y); mass = 1000; // Adjust the mass value as needed G = 0.4; } PVector repel(Mover m) { PVector force = PVector.sub(m.location, location); float distance = force.mag(); distance = constrain(distance, 5.0, 100.0); // Adjust the constraint values as needed force.normalize(); float strength = G * mass * m.mass / (distance * distance); force.mult(strength); return force; } void update(float x, float y) { location.set(x, y); } void display() { noStroke(); fill(200, 0, 0, 150); // Adjust the color as needed float triangleSize = mass / 10; float triangleHeight = (float) (triangleSize * sqrt(3) / 2); // Corrected calculation // Draw an equilateral triangle centered at the repeller's location beginShape(); vertex(location.x, location.y - triangleHeight / 2); vertex(location.x - triangleSize / 2, location.y + triangleHeight / 2); vertex(location.x + triangleSize / 2, location.y + triangleHeight / 2); endShape(CLOSE); fill(0); textSize(20); textAlign(CENTER, CENTER); text("Repellor", location.x, location.y + triangleHeight / 2 -20); } } void displayStartInterface() { background(255); fill(0); textSize(40); textAlign(CENTER, CENTER); text("Click to Start", width / 2, height / 2 - 30); textSize(30); text("Attractor collects, Repeller obstructs. ", width / 2, height / 2+40); text("Attractor wins if collect all the movers, otherwise Repellor wins.", width / 2, height / 2+80); } void mousePressed() { if (!gameStarted) { startTime = millis(); gameStarted = true; } } void getSerialData() { while (serialPort.available() > 0) { String in = serialPort.readStringUntil(10); // 10 = '\n' Linefeed in ASCII if (in != null) { print("From Arduino: " + in); String[] serialInArray = split(trim(in), ","); if (serialInArray.length == NUM_OF_VALUES_FROM_ARDUINO) { for (int i=0; i<serialInArray.length; i++) { arduino_values[i] = int(serialInArray[i]); } } } } }
Code for the dancing particle
Arduino (from the example code)
// defines pins numbers const int trigPin1 = 9; const int echoPin1 = 10; const int trigPin2 = 8; const int echoPin2 = 11; // defines variables long duration1; int distance1; long duration2; int distance2; void setup() { pinMode(trigPin1, OUTPUT); // Sets the trigPin1 as an Output pinMode(echoPin1, INPUT); // Sets the echoPin1 as an Input pinMode(trigPin2, OUTPUT); pinMode(echoPin2, INPUT); Serial.begin(9600); // Starts the serial communication } void loop() { // Sensor 1 digitalWrite(trigPin1, LOW); delayMicroseconds(2); digitalWrite(trigPin1, HIGH); delayMicroseconds(10); digitalWrite(trigPin1, LOW); duration1 = pulseIn(echoPin1, HIGH); distance1 = duration1 * 0.034 / 2; if(distance1 <= 16 and distance1 >= 3){ Serial.print(distance1); Serial.print(","); }else{ Serial.print(0); Serial.print(","); } // Sensor 2 digitalWrite(trigPin2, LOW); delayMicroseconds(2); digitalWrite(trigPin2, HIGH); delayMicroseconds(10); digitalWrite(trigPin2, LOW); duration2 = pulseIn(echoPin2, HIGH); distance2 = duration2 * 0.034 / 2; if(distance2 <= 16 and distance2 >= 3){ Serial.print(distance2); }else{ Serial.print(0); } Serial.println(); //delay(10); // Add a delay between readings to make it easier to read on the serial monitor }
Processing (by Professor Moon and Professor Gohai and Avril)
import processing.serial.*; Particle[] particles = new Particle[50]; float sensor1; // position of the left sensor in pixels float sensor2; // position of the right sensor in pixels Serial serialPort; int NUM_OF_VALUES_FROM_ARDUINO = 2; int arduino_values[] = new int[NUM_OF_VALUES_FROM_ARDUINO]; float smoothed[] = new float[2]; void setup() { size(400, 400); noCursor(); fullScreen(); serialPort = new Serial(this, "/dev/cu.usbmodem1101", 9600); // create particle objects for (int i = 0; i < particles.length; i++) { particles[i] = new Particle(random(width), random(height), random(1, 50)); } // start with the sensors at the edge of the canvas sensor1 = 0; sensor2 = width; } void draw() { background(0); getSerialData(); if (arduino_values[0] != 0) { smoothed[0] = smoothed[0] * 0.90 + arduino_values[0] * 0.10; } if (arduino_values[1] != 0) { smoothed[1] = smoothed[1] * 0.90 + arduino_values[1] * 0.10; } float sensor1 = map(smoothed[0], 3, 16, width/2, 0); float sensor2 = map(smoothed[1], 3, 16, width/2, width); // turn the sensor pixel-values into a magnitude 0-5 float magLeft = constrain(map(sensor1, 0, width/2, 0, 5), 0, 2); float magRight = constrain(map(sensor2, width/2, width, 5, 0), 0, 2); // create vectors for the forces from either side // and scale them PVector forceFromLeft = new PVector(1, 0); forceFromLeft.setMag(magLeft); PVector forceFromRight = new PVector(-1, 0); forceFromRight.setMag(magRight); for (int i=0; i < particles.length; i++) { Particle p = particles[i]; // get the difference in x-position between // particle and left sensor and use it to // adjust the strength of the force float diffLeft = abs(sensor1 - p.pos.x); PVector fLeft = forceFromLeft.copy(); float forceAdjLeft = map(diffLeft, 0, width/2, 1, 0); fLeft.mult(forceAdjLeft); p.applyForce(fLeft); float diffRight = abs(sensor2 - p.pos.x); PVector fRight = forceFromRight.copy(); float forceAdjRight = map(diffRight, 0, width/2, 1, 0); fRight.mult(forceAdjRight); p.applyForce(fRight); float sinValue = sin(frameCount*0.05); PVector centerVec = new PVector(width/2, height/2); PVector vector = PVector.sub(centerVec, p.pos); vector.mult(0.01 * sinValue); p.applyForce(vector); // have a random force act on the vectors in the y-position PVector randomForce = new PVector(0, random(-0.5, 0.5)); p.applyForce(randomForce); p.limitPosition(); p.reappear(); p.run(); } // visualize sensor values /*stroke(255, 0, 0); line(sensor1, 0, sensor1, height); stroke(0, 0, 255); line(sensor2, 0, sensor2, height);*/ } class Particle { PVector pos; PVector vel; PVector acc; float rad; float mass; Particle(float x, float y, float rad) { this.pos = new PVector(x, y); this.vel = new PVector(0, 0); this.acc = new PVector(0, 0); this.rad = rad; this.mass = rad * 0.1; // 10% } void run() { this.vel.add(this.acc); this.pos.add(this.vel); // cancel the acceleration this.acc.mult(0); // limit the velocity to reasonable limits this.vel.limit(5); // call display automatically fill(random(0,255)); this.display(); } void reappear() { // Bounce back when the particle hits the canvas borders if (this.pos.x < 0 || this.pos.x > width) { this.vel.x *= -1; } if (this.pos.y < 0 || this.pos.y > height) { this.vel.y *= -1; } } void limitPosition() { // this keeps the particle within the position of the left and right sensors if (this.pos.x < sensor1) { this.pos.x = sensor1; } if (this.pos.x > sensor2) { this.pos.x = sensor2; } } void applyForce(PVector f) { PVector force = f.copy(); if (this.mass > 0) { force.div(this.mass); this.acc.add(force); } } void display() { noStroke(); circle(this.pos.x, this.pos.y, this.rad*2); } } void getSerialData() { while (serialPort.available() > 0) { String in = serialPort.readStringUntil(10); // 10 = '\n' Linefeed in ASCII if (in != null) { print("From Arduino: " + in); String[] serialInArray = split(trim(in), ","); if (serialInArray.length == NUM_OF_VALUES_FROM_ARDUINO) { for (int i=0; i<serialInArray.length; i++) { arduino_values[i] = int(serialInArray[i]); } } } } }
Videos and photos for the Game
Videos and photos for the Dancing particles