Skip to main content

Final Project Documentation

Xinyi Peng

Initial Minimum Viable Test
#

Blow Structure Test
#


At first the circuit counldn’t give the correct HIGH/LOW signal as I expected. From Matti I know there is two problems:

  1. The glue on the back of the conductive tape is not conductive, so you can’t make it discontinuous;
  2. Matti suggest for testing I should use bread board to connect the components to prevent the unstable serial transformation.

Arduino & Processing Connection
#

This is my initial Arduino code:

int i;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(21,INPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  i = digitalRead(21);
  Serial.println(i);
}

and here is the Processing code:

import processing.serial.*;
Serial myPort;  // Create object from Serial class
int val;     // Data received from the serial port

void setup()
{
  // On Windows machines, this generally opens COM1.
  // Open whatever port is the one you're using.
  String portName = Serial.list()[2]; //change the 0 to a 1 or 2 etc. to match your port
  myPort = new Serial(this, portName, 9600);
}

void draw()
{
  println(val);
}

But the problem is the signal look fine in the arduino serial monitor, but in processing I can’t print the signal. It only shows 0. With Matti’s help I know that there are several problems behind:

  1. The speed arduino sending the signals is faster than the processing refeshing(60FPS), so I need to add a delay(30)in arduino. This is the explaination of Gemini: set the Arduino delay to 30ms (approx. 33Hz) to maintain a safe 1:2 ratio with Processing’s 60Hz read rate, ensuring the buffer clears faster than it fills to prevent data backlog.
  2. The code println(val) is not actually getting the serial data so that I need to use str = myPort.readStringUntil('\n');. But this only gets the String type of data(pure text) but I need int or float, so Matti says I need use this val = float(str); to force the data become a float. But at first we want to try int(), but there is some unknown error, but float works fine. The code I got that can actually work is this:
import processing.serial.*;
Serial myPort;  // Create object from Serial class
float val;     // Data received from the serial port
String str;

void setup()
{
  // On Windows machines, this generally opens COM1.
  // Open whatever port is the one you're using.
  println(Serial.list());
  
  String portName = Serial.list()[0];
  myPort = new Serial(this, portName, 9600);
  
  //myPort.bufferUntil('\n');
}

void draw()
{
  background(255);
  if ( myPort.available() > 0) {  // If data is available,
    str = myPort.readStringUntil('\n');  // read it and store it in str
    //
    //println(str);
    if(str != null){
      //String[] splitData = split(str, ","); // I'll need this for more int
      val = float(str); // change the text we get into real number we can do calculation
     // y = float(splitData[1]);
     // z = float(splitData[2]);
    }
  }
  
  println(val);
  text(val,20,20);
}

Here is the final prototype that works with simple visual in processing:

Ball-Slide Sensor Structure Test
#


The first version works if I use conductive tap to be the bridge of every pair of interrupts, but I failed to reach my goal of using the ball as trigger. Here is the initial version that failed:

There are several reasons I concluded from this:

  1. The width of the conductive tap areas now is 5mm, is should be bigger for the ball to touch and stay;
  2. The diameter of ball I use now is 6mm, I should use bigger ball to increase the conductive area(so I got myself some 1.2mm large ones);
  3. Also for increasing the conductive region, instead to the current flat structure, I should use a “U valley” shape as the ball-rolling zone.

The second prototype version can give me a signal when I pivoted the box up side down! But to make this stable, I must squeeze this box a little bit to provide more contact area between the ball and the surface, which means that I should design a structure that bends to fit the ball shape.

Paper Folding Sensor Structure Test
#

When I was introducing this idea to everyone in our class, it is really interesting to find that people all played this before, so I guess this interaction must be a fun experience as a connection of their childhood memories and familiar gestures. The final prototype works like this:

Although in concern of time, I don’t have enough effort to use this sensor as a trigger of something, but I think in the future I can do some with this. Also, I have summarized some after prototyping:
  1. The electric wires should use the soft ones with more flexibility;
  2. The plus and minus side soldering should be carefully avoided touching when pressing two side together.

Final Prototype Structure
#

Coding Part
#

Arduino Code
#

Ball OSC_codeToProcessing
#

#include <WiFi.h>
#include <ArduinoOSCWiFi.h>

// WiFi stuff
const char* ssid = "mainframe";
const char* pwd = "12345678";

// OSC setting
// computer ip address
const char* host = "192.168.50.231"; 
const int recv_port = 12345;    // Arduino recieve com
const int publish_port = 54321; // Arduino send com

float ballA,ballB,ballC,ballD,ballE;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);

  // WiFi ------------------>
  // WiFi stuff (no timeout setting for WiFi)
    WiFi.mode(WIFI_STA);

    // Connect to the WiFi network
    WiFi.begin(ssid,pwd);
    while (WiFi.status() != WL_CONNECTED) {
        Serial.print(".");
        delay(500);
    }

    Serial.print("WiFi connected, IP = ");
    Serial.println(WiFi.localIP()); // print arduino's own IP

    // subscribe to receive osc messages that control the robot
    OscWiFi.subscribe(recv_port, "/control",
        [](const OscMessage& m) {
            Serial.print(m.remoteIP());
            Serial.print(" ");
            Serial.print(m.remotePort());
            Serial.print(" ");
            Serial.print(m.size());
            Serial.print(" ");
            Serial.print(m.address());
            Serial.print(" ");
            Serial.print("Value: ");
            float val = m.arg<float>(0);
            Serial.println(val);
        });

  //ball pin
  pinMode(D0,INPUT_PULLUP);
  pinMode(D1,INPUT_PULLUP);
  pinMode(D2,INPUT_PULLUP);
  pinMode(D3,INPUT_PULLUP);
  pinMode(D4,INPUT_PULLUP);
}

void loop() {
  // update the OSC sending and receiving
  OscWiFi.update();

  // put your main code here, to run repeatedly:
  //ball serial define
  ballA=digitalRead(D0);
  ballB=digitalRead(D1);
  ballC=digitalRead(D2);
  ballD=digitalRead(D3);
  ballE=digitalRead(D4);

  OscWiFi.send(host, publish_port, "/sensors", 
      ballA,    // index 1
      ballB,    // index 2
      ballC,    // index 3
      ballD,    // index 4
      ballE     // index 5
  );

  Serial.print("A: ");
  Serial.print(ballA);
  Serial.print("B: ");
  Serial.print(ballB);
  Serial.print("C: ");
  Serial.print(ballC);
  Serial.print("D: ");
  Serial.print(ballD);
  Serial.print("E: ");
  Serial.println(ballE);

  delay(30);
}

BLOW_OSC_codeToProcessing
#

#include <WiFi.h>
#include <ArduinoOSCWiFi.h>

// WiFi stuff
const char* ssid = "mainframe";
const char* pwd = "12345678";

// OSC setting
// computer ip address
const char* host = "192.168.50.231"; 
const int recv_port = 12345;    // Arduino recieve com
const int publish_port = 54321; // Arduino send com

float blow;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);

  // WiFi ------------------>
  // WiFi stuff (no timeout setting for WiFi)
    WiFi.mode(WIFI_STA);

    // Connect to the WiFi network
    WiFi.begin(ssid,pwd);
    while (WiFi.status() != WL_CONNECTED) {
        Serial.print(".");
        delay(500);
    }

    Serial.print("WiFi connected, IP = ");
    Serial.println(WiFi.localIP()); // print arduino's own IP

    // subscribe to receive osc messages that control the robot
    OscWiFi.subscribe(recv_port, "/control",
        [](const OscMessage& m) {
            Serial.print(m.remoteIP());
            Serial.print(" ");
            Serial.print(m.remotePort());
            Serial.print(" ");
            Serial.print(m.size());
            Serial.print(" ");
            Serial.print(m.address());
            Serial.print(" ");
            Serial.print("Value: ");
            float val = m.arg<float>(0);
            Serial.println(val);
        });

  //blow pin
  pinMode(D10,INPUT_PULLUP);
}

void loop() {
  // update the OSC sending and receiving
  OscWiFi.update();

  // put your main code here, to run repeatedly:
  //blow serial define
  blow=digitalRead(D10);

  OscWiFi.send(host, publish_port, "/sensors_2", 
      blow
  );
  
  // 1. Blow
  Serial.print("ValBlow: ");
  Serial.println(blow);

  delay(30);
}

Processing Code
#

// connect arduino with processing
import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress myRemoteLocation;

float valBlow, prevBlow=0, blowCount=0; // reserve sata received from the serial port
float valBallA,valBallB,valBallC,valBallD,valBallE;

//Box2D library
import shiffman.box2d.*;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.joints.*;
import org.jbox2d.collision.shapes.*;
import org.jbox2d.collision.shapes.Shape;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.*;
import org.jbox2d.dynamics.contacts.*;
Box2DProcessing box2d;
Boundary mainFloor; // save the floor

//Boundary part
ArrayList<Boundary> walls; //save the walls
//the variable need for boundary transformation
float ang, smoothFactor=0.1, activeAng; // the angle for the boundary twist
float BallPins[] = {valBallA,valBallB,valBallC,valBallD,valBallE};
float targetAngles[] = {radians(30), radians(14.5), radians(0), radians(-14.5), radians(-30)};
float smoothingFactor = 0.1;
float finalTarget = 0;
boolean isSoundActive;

//Object part
String[] dictA = {"but", "time", "is", "a", "wind", "that", "never", "stops", "blowing", "from", "dust", "we", "rise", "and", "to", "dust", "we", "shall", "return", "breaking", "apart", "into", "atoms", "into", "memories", "into", "light"};
String[] dictB = {"we", "are", "made", "of", "starstuff", "we", "are", "a", "way", "for"};
String[] dictC = {"a", "fleeting", "moment", "of", "being","in", "the", "vast", "design", "gravity", "holds", "us", "for", "a", "while"};
String[] dictD = {"the", "cosmos", "to", "know", "itself"};

Dandelion[] dandelions = new Dandelion[4];
Body centerAnchor;
//delay the trigger
boolean isWaiting = false;
int startTime = 0;
int targetIndex = -1;

//the center coordinates
float ori_X;
float ori_Y;
boolean isBlown = false;

//the sound part
import ddf.minim.*;
Minim minim;
AudioPlayer windPlayer;
AudioPlayer bgMusic;
float targetPan, currentPan = 0.5;

//reset the dandelion
int resetStartTime = 0;
boolean isResetting = false;


void setup()
{
  size(1920,1080);
  
  //OSC setting
  frameRate(60);
  //XIAO send ---> processing
  oscP5 = new OscP5(this, 54321);
  //processing gives order to ---> XIAO //maybe I dont need but keep it
  myRemoteLocation = new NetAddress("192.168.50.172", 12345);
  
  minim = new Minim(this);
  windPlayer = minim.loadFile("soft_wind.mp3"); //load the widn sound effect
  windPlayer.setGain(10.0);
  bgMusic = minim.loadFile("bg_forest.mp3");
  bgMusic.loop();
  bgMusic.setGain(5.0);
  
  //give value to the center of the dandelion
  ori_X = width/2;
  ori_Y = 200;
  
  //Box2D part
  box2d = new Box2DProcessing(this);
  box2d.createWorld();
  box2d.setGravity(0, -50);
  mainFloor = new Boundary(width/2, height + 100, width+300, 300, radians(ang));
  walls = new ArrayList<Boundary>();
  walls.add(new Boundary(0, height/2, 20, height, 0)); //left wall
  walls.add(new Boundary(width, height/2, 20, height, 0));
  
  //Object dandelions
  float spacing = width / 5.0; 
  dandelions[0] = new Dandelion(width/2-350, height*0.65, 90, 90, 90, 20, 90, dictB); 
  dandelions[1] = new Dandelion(width/2+450, height*0.72, -90, 40, -90, 40, 90, dictD);
  dandelions[2] = new Dandelion(width/2+150, height*0.55, -90, 90, -20, -20, 110, dictC);
  dandelions[3] = new Dandelion(width/2-220, height*0.35, 160, 160, 160, 90, 120, dictA);

  box2d.listenForCollisions();
}

void draw()
{
  //refresh the BallPins value
  BallPins = new float[]{valBallA, valBallB, valBallC, valBallD, valBallE};
  
  background(0);
  
  //serials shows on the window
  fill(255);
  textSize(15);
  text("Blow"+valBlow,40,100); 
  text("valBallA"+valBallA,40,140);
  text("valBallB"+valBallB,40,160);
  text("valBallC"+valBallC,40,180);
  text("valBallD"+valBallD,40,200);
  text("valBallE"+valBallE,40,220);
  
  //sound panning part
  if(valBallA == 1)      targetPan = 0.0;   
  else if(valBallB == 1) targetPan = 0.25;  
  else if(valBallC == 1) targetPan = 0.5;   
  else if(valBallD == 1) targetPan = 0.75;  
  else if(valBallE == 1) targetPan = 1.0;  
  //println(targetPan);

  currentPan=currentPan+(targetPan-currentPan)*smoothFactor;
  float minimPan = map(currentPan,0,1,-1,1);
  windPlayer.setPan(minimPan);
  
  //detect blow
  if (valBlow == 1 && prevBlow == 0) 
  {
    if (!isWaiting) 
    {
      int foundIndex = -1;

      //go through all the dandelions, find the first !isBlown
      for (int i = 0; i < dandelions.length; i++) 
      {
        if (dandelions[i].isBlown == false) 
        {
          foundIndex = i;
          windPlayer.rewind();
          windPlayer.play();
          break; //if find, stop loop
        }
      }

      //if find a dandelion that is not blown
      if (foundIndex != -1) 
      {
        targetIndex = foundIndex;
        startTime = millis(); // recored time
        isWaiting = true;     // start to break mode
      }
    }
  }

  if (isWaiting) 
  {
    if (millis() - startTime >= 800) 
    {
      if(targetIndex != -1 && targetIndex < dandelions.length)
      {
         dandelions[targetIndex].blow();
      }
      isWaiting = false; 
    }
  }
  prevBlow = valBlow; //refresh last state
  
  Vec2 wind = new Vec2(50, random(-10, 10)); 
  //Dandelion display
  for (Dandelion d : dandelions) 
  {
    d.display();      
    d.applyWind(wind); 
  }
  
  //Box2D part
  box2d.step();
  mainFloor.display();
  //boundary display
  for (Boundary wall: walls)
  {
    wall.display();
  }
  turn(); // execute boundary transformation
  
  //reset dandelion
  boolean allBlown = true;
  //detect whether all of them has bee blown
  for (Dandelion d : dandelions) 
  {
    if (!d.isBlown) 
    {
      allBlown = false;
      break;
    }
  }

  if (allBlown && !isResetting) 
  {
    resetStartTime = millis();
    isResetting = true;
  }

  //if in resetMode, check time, execute reset after 5s
  if (isResetting) 
  {
    if (millis() - resetStartTime > 15000) 
    {
      for (Dandelion d : dandelions) //execute reset
      {
        d.reset();
      }
      isResetting = false;
      blowCount = 0;  
      targetIndex = -1;
      isWaiting = false; 
    }
  }
}

//get the signal from arduino
void oscEvent(OscMessage theOscMessage) 
{
  if (theOscMessage.checkAddrPattern("/sensors") == true) 
  {
    valBallA = 1-theOscMessage.get(0).floatValue(); 
    valBallB = 1-theOscMessage.get(1).floatValue(); 
    valBallC = 1-theOscMessage.get(2).floatValue(); 
    valBallD = 1-theOscMessage.get(3).floatValue();
    valBallE = 1-theOscMessage.get(4).floatValue();
    println("matti_1");
  }
  if (theOscMessage.checkAddrPattern("/sensors_2") == true) 
  {
    valBlow  = 1-theOscMessage.get(0).floatValue(); 
    println("matti_2");
  }
}

Boundary
#

class Boundary
{
  // A boundary is a simple rectangle with x,y,width,and height
  float x,y,w,h,angle;
  
  // But we also have to make a body for box2d to know about it
  Body b;
  
  Boundary(float x,float y, float w, float h, float angle) 
  {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.angle = angle;
  
    PolygonShape sd = new PolygonShape();
    // Figure out the box2d coordinates
    float box2dW = box2d.scalarPixelsToWorld(w/2);
    float box2dH = box2d.scalarPixelsToWorld(h/2);
    // We're just a box
    sd.setAsBox(box2dW, box2dH);
    
    // Create the body
    BodyDef bd = new BodyDef();
    bd.type = BodyType.KINEMATIC;
    bd.position.set(box2d.coordPixelsToWorld(x,y)); //send boundary position to libray
    //processing is clockwise, Box2D is anticlockwise
    bd.angle = -angle; // send angle
    b = box2d.createBody(bd); // create object
    
    // Attached the shape to the body using a Fixture, name of this Fixture is fd
    FixtureDef fd = new FixtureDef();
    fd.shape = sd;
    fd.density = 1;
    // Friction: 0(like ice), 1(sticky like glue)
    fd.friction = 0.1;  
    fd.restitution = 0.5;
    // create this Fixture fd
    b.createFixture(fd);
    b.setUserData(this);
    b.setUserData(this);
  }
  
  void display() 
  {
    fill(0);
    stroke(0);
    rectMode(CENTER);
    
    pushMatrix();
    translate(x,y);
    rotate(angle);
    rect(0,0,w,h);
    popMatrix();
  }
  
  void updateAngle(float newAngle) {
    this.angle = newAngle; 
  
    // get the new position of boundary in Box2D world
    Vec2 currentPos = b.getPosition();
  
    //let the position fit possessing clockwise direction
    b.setTransform(currentPos, -newAngle); 
  }
}

Boundary Twist
#

void turn()
{
  int currentIndex = -1;
  for(int i=0; i<BallPins.length; i++)
  {
    if(BallPins[i]==1.0)
    {
      currentIndex = i;
    }
  }
  if(currentIndex != -1)
  {
    activeAng = targetAngles[currentIndex];
  }
  ang=ang+(activeAng-ang)*smoothFactor;
  mainFloor.updateAngle(ang);
  //println(ang);
}

Collision
#

void beginContact(Contact cp) 
{
  Fixture f1 = cp.getFixtureA();
  Fixture f2 = cp.getFixtureB();
  Body b1 = f1.getBody();
  Body b2 = f2.getBody();
  Object o1 = b1.getUserData();
  Object o2 = b2.getUserData();

  if (o1 == null || o2 == null) return;

  if (o1.getClass() == WordParticles.class && o2.getClass() == Boundary.class) 
  {
    groundParticle((WordParticles) o1);
  }
  else if (o2.getClass() == WordParticles.class && o1.getClass() == Boundary.class) 
  {
    groundParticle((WordParticles) o2);
  }

  else if (o1.getClass() == WordParticles.class && o2.getClass() == WordParticles.class) 
  {
    WordParticles p1 = (WordParticles) o1;
    WordParticles p2 = (WordParticles) o2;

    if (p1.hasHitGround && !p2.hasHitGround) 
    {
       groundParticle(p2); 
    }
    else if (!p1.hasHitGround && p2.hasHitGround) 
    {
       groundParticle(p1); 
    }
  }
}

void groundParticle(WordParticles p) 
{
  if (!p.hasHitGround) 
  {
    p.hasHitGround = true;        
    p.body.setLinearDamping(0.5); 
    p.scheduleSound(); //trigger text particle sound      
  }
}

Dandelion
#

class Dandelion 
{
  float x, y;                         //the center of the dandelion             
  ArrayList<WordParticles> particles; //list of the seed
  ArrayList<Joint> joints;            //list of the joints
  Body centerAnchor;                  //list of the anchors
  boolean isBlown = false;            
  float stemAlpha = 255;              
  String[] dict;    
  float radius;
  float cx1, cy1, cx2, cy2;          // the offset for drawing the stem lines

  Dandelion(float x, float y, float cx1, float cy1, float cx2, float cy2, float r, String[] dict) 
  {
    this.x = x;
    this.y = y;
    this.dict = dict;
    this.radius = r;
    this.cx1 = cx1;
    this.cy1 = cy1;
    this.cx2 = cx2;
    this.cy2 = cy2;
    
    particles = new ArrayList<WordParticles>();
    joints = new ArrayList<Joint>();
    createBody(); 
  }

  void createBody() 
  { 
    //create the body
    BodyDef bd = new BodyDef();
    bd.position.set(box2d.coordPixelsToWorld(x, y));//send boundary position to libray
    bd.type = BodyType.STATIC;
    centerAnchor = box2d.createBody(bd);

    //create the seeds
    int totalSeeds = max(20,dict.length);
    
    for (int i = 0; i < totalSeeds; i++) 
    {
      float angle = map(i, 0, totalSeeds, 0, TWO_PI);
      float px = x + this.radius * cos(angle);
      float py = y + this.radius * sin(angle);
      
      String txt = dict[i % dict.length]; 
      WordParticles p = new WordParticles(px, py, txt);
      particles.add(p);
      
      DistanceJointDef djd = new DistanceJointDef();
      djd.bodyA = centerAnchor;
      djd.bodyB = p.body;
      djd.length = box2d.scalarPixelsToWorld(this.radius);
      djd.frequencyHz = 3.0;
      djd.dampingRatio = 0.1;
      
      Joint j = box2d.world.createJoint(djd);
      joints.add(j);
    }
  }

  //start blowing
  void blow() 
  {
    if (!isBlown) 
    {
      for (Joint j : joints) 
      {
        box2d.world.destroyJoint(j);
      }
      joints.clear(); 
      isBlown = true; 
    }
  }

  void applyWind(Vec2 windForce) 
  {
    if (isBlown) 
    {
      for (WordParticles p : particles) 
      {
        p.body.applyForce(windForce, p.body.getWorldCenter());
      }
      //if isBlown, let stem disapear
      if (stemAlpha > 0) stemAlpha = stemAlpha - 2;
    }
  }

  void display() 
  {
    noFill();
    stroke(250, 255, 255, stemAlpha);
    strokeWeight(2);
    bezier(x, y, x + cx1, y + cy1, x + cx2, height - cy2, x, height);

    for (WordParticles p : particles) 
    {
      p.updateSound(); // use for detect delay sound play
      p.display();
    }
  }
  
  void reset() 
  {
    isBlown = false;
    stemAlpha = 255; 

    int totalSeeds = particles.size();
    
    for (int i = 0; i < totalSeeds; i++) 
    {
      float angle = map(i, 0, totalSeeds, 0, TWO_PI);
      float px = x + this.radius * cos(angle);
      float py = y + this.radius * sin(angle);

      WordParticles p = particles.get(i);
      p.resetPos(px, py);

      // the same with createBody
      DistanceJointDef djd = new DistanceJointDef();
      djd.bodyA = centerAnchor;
      djd.bodyB = p.body;
      djd.length = box2d.scalarPixelsToWorld(this.radius);
      djd.frequencyHz = 3.0;
      djd.dampingRatio = 0.1;
      
      Joint j = box2d.world.createJoint(djd);
      joints.add(j);
    }
  }
}

Text Particles Object
#

class WordParticles 
{
  float w;
  float h;
  String content;  
  Body body;
  color col;
  boolean hasHitGround = false;
  
  //sound play variable
  boolean hasPlayed = false;
  boolean isScheduled = false;
  long triggerTime = 0;
  //every textParticle has its own sound
  AudioSample mySound;
  
  WordParticles(float x, float y, String c) 
  {
    this.content = c;
    //col = color(random(50), random(100, 200), random(200, 255));//random blue
    col = 255;
    
    //load audio files according to text content
    try 
    {
      //go to file data to find the name of its audio
      mySound = minim.loadSample(content + ".mp3", 512);
    } 
    catch (Exception e) 
    {
      println("Could not load sound for: " + content);
    }
    
    //set the physical box accodingt to the text size
    textSize(30); 
    w = textWidth(content); 
    h = textAscent() + textDescent(); 
    
    //padding
    float padding = 2;
    w = w + padding;
    h = h + padding;
    
    //create a object with w&h at the pos of (x,y)
    makeBody(new Vec2(x, y), w, h);
  }
  
  void display() 
  {
    //invisible physical body part
    //get the position ofbody on screen
    Vec2 pos = box2d.getBodyPixelCoord(body);
    //get the rotate angle of body, save into a
    float a = body.getAngle();

    //draw this object on window according to the pos&rotate of the body
    pushMatrix();
    translate(pos.x, pos.y);
    rotate(-a);
    
    fill(col);
    noStroke();
    textAlign(CENTER, CENTER);
    textSize(28);
    text(content, 0, 0);
    popMatrix();
  }
  
  //create a body with physical attributes in Box2D world
  void makeBody(Vec2 center, float w, float h) 
  {
    this.w= w;
    this.h = h;
    
    //body define
    BodyDef bd = new BodyDef();
    bd.type = BodyType.DYNAMIC;
    bd.position.set(box2d.coordPixelsToWorld(center));//screen to world coordinates switch
    
    //high damping makes it float like a seed/feather
    bd.linearDamping = 8;
    bd.angularDamping = 1.0;
    
    //the birth of this body
    body = box2d.createBody(bd);
    //let the object change physical state accorfing to boundary twist
    body.setSleepingAllowed(false);

    //Figure out the box2d coordinates
    PolygonShape sd = new PolygonShape();
    float box2dW = box2d.scalarPixelsToWorld(w/2);
    float box2dH = box2d.scalarPixelsToWorld(h/2);
    sd.setAsBox(box2dW, box2dH);

    //defien fixture(texture)
    FixtureDef fd = new FixtureDef();
    fd.shape = sd;
    //low density so wind affects it easily
    fd.density = 0.5;
    fd.friction = 0.3;
    fd.restitution = 0.2; 

    //attach the fixture to body
    body.createFixture(fd);
    body.setUserData(this);
    body.setAngularVelocity(random(-1, 1));
  }
  
  void resetPos(float x, float y) 
  {
    Vec2 pos = box2d.coordPixelsToWorld(x, y);
    body.setTransform(pos, 0);       //return to previous position
    body.setLinearVelocity(new Vec2(0,0));  
    body.setAngularVelocity(0);      
    body.setLinearDamping(8.0);
    
    hasHitGround = false;
    hasPlayed = false;      
    isScheduled = false;
  }
  
  void scheduleSound() 
  {
    if (isScheduled || hasPlayed) return;
    
    isScheduled = true;
    int delay = (int)random(100, 11000); 
    triggerTime = millis() + delay;
  }
  
  void updateSound() 
  {
    if (isScheduled && !hasPlayed) 
    {
      if (millis() >= triggerTime) 
      {
        playSound();
        hasPlayed = true; 
      }
    }
  }
  
  void playSound() 
  {
     if (mySound != null) 
     {
       float randomGain = random(-20.0, -80.0); 
       mySound.setGain(randomGain);
       mySound.trigger(); 
     }
  }
}