Peek-a-boo

Rather than just allow my face to be a puppeteer of a “solid” virtual object, I want to allow the motions of my face to create my puppet as my program ran. I also didn’t want the puppet to seem like a mass, but more like a swarm and so I opted to create my puppet out of particles. In order to create these particles, the user must blink or close their eyes. This effectively only allows the user to see themselves as a mirrored particle-based reflection after looking away from it. The particles are based on the particle system I created for my Thousand line Project, but modified for circular motion.

Here’s the program!

//
// a template for receiving face tracking osc messages from
// Kyle McDonald's FaceOSC https://github.com/kylemcdonald/ofxFaceTracker
//
// 2012 Dan Wilcox danomatika.com
// for the IACD Spring 2012 class at the CMU School of Art
//
// adapted from from Greg Borenstein's 2011 example
// http://www.gregborenstein.com/
// https://gist.github.com/1603230
//
import oscP5.*;
OscP5 oscP5;

ArrayList<ZoomLine> zoomers;
ZoomLine guy;

// num faces found
int found;

// pose
float poseScale;
PVector posePosition = new PVector();
PVector poseOrientation = new PVector();

// gesture
float mouthHeight;
float mouthWidth;
float eyeLeft;
float eyeRight;
float eyebrowLeft;
float eyebrowRight;
float jaw;
float nostrils;

void setup() {
  size(640, 480,OPENGL);
  frameRate(30);
  zoomers = new ArrayList<ZoomLine>();
  
  guy = new ZoomLine(50,50,200,200,10);
  
  oscP5 = new OscP5(this, 8338);
  oscP5.plug(this, "found", "/found");
  oscP5.plug(this, "poseScale", "/pose/scale");
  oscP5.plug(this, "posePosition", "/pose/position");
  oscP5.plug(this, "poseOrientation", "/pose/orientation");
  oscP5.plug(this, "mouthWidthReceived", "/gesture/mouth/width");
  oscP5.plug(this, "mouthHeightReceived", "/gesture/mouth/height");
  oscP5.plug(this, "eyeLeftReceived", "/gesture/eye/left");
  oscP5.plug(this, "eyeRightReceived", "/gesture/eye/right");
  oscP5.plug(this, "eyebrowLeftReceived", "/gesture/eyebrow/left");
  oscP5.plug(this, "eyebrowRightReceived", "/gesture/eyebrow/right");
  oscP5.plug(this, "jawReceived", "/gesture/jaw");
  oscP5.plug(this, "nostrilsReceived", "/gesture/nostrils");
}

void draw() {  
  background(255);
  stroke(0);
  makeZoomer();
  
  
  
  if(found > 0) {
    translate(posePosition.x, posePosition.y);
    rotateY(poseOrientation.y);
    rotateX(poseOrientation.x);
    rotateZ(poseOrientation.z);
    scale(poseScale);

    drawLines();
  }
}

// OSC CALLBACK FUNCTIONS

public void found(int i) {
  println("found: " + i);
  found = i;
}

public void poseScale(float s) {
  println("scale: " + s);
  poseScale = s;
}

public void posePosition(float x, float y) {
  println("pose position\tX: " + x + " Y: " + y );
  posePosition.set(x, y, 0);
}

public void poseOrientation(float x, float y, float z) {
  println("pose orientation\tX: " + x + " Y: " + y + " Z: " + z);
  poseOrientation.set(x, y, z);
}

public void mouthWidthReceived(float w) {
  println("mouth Width: " + w);
  mouthWidth = w;
}

public void mouthHeightReceived(float h) {
  println("mouth height: " + h);
  mouthHeight = h;
}

public void eyeLeftReceived(float f) {
  println("eye left: " + f);
  eyeLeft = f;
}

public void eyeRightReceived(float f) {
  println("eye right: " + f);
  eyeRight = f;
}

public void eyebrowLeftReceived(float f) {
  println("eyebrow left: " + f);
  eyebrowLeft = f;
}

public void eyebrowRightReceived(float f) {
  println("eyebrow right: " + f);
  eyebrowRight = f;
}

public void jawReceived(float f) {
  println("jaw: " + f);
  jaw = f;
}

public void nostrilsReceived(float f) {
  println("nostrils: " + f);
  nostrils = f;
}

// all other OSC messages end up here
void oscEvent(OscMessage m) {
  
//  /* print the address pattern and the typetag of the received OscMessage */
//  println("#received an osc message");
//  println("Complete message: "+m);
//  println(" addrpattern: "+m.addrPattern());
//  println(" typetag: "+m.typetag());
//  println(" arguments: "+m.arguments()[0].toString());
  
  if(m.isPlugged() == false) {
    println("UNPLUGGED: " + m);
  }
}


void makeZoomer() {
  if (found > 0) {
      if ((eyebrowLeft > 8.8) && (eyebrowLeft > 8.8)) {
          zoomers.add(new ZoomLine(random(width) / poseScale, random(height) / poseScale ,
                        random(width) / poseScale, random(height) / poseScale, int(random(10,20))));
      }
  }
}
  
void drawLines() {
  for (int i = 0; i < zoomers.size(); i++) {
    zoomers.get(i).drawMe();
    zoomers.get(i).update();
  }
  for (int j = 0; j < zoomers.size(); j++) {
    if (zoomers.get(j).aliveFor >= zoomers.get(j).lifeSpan) { 
       zoomers.remove(j);
    }
  }
}

class ZoomLine {
  Position start;
  Position lineLoc;
  Position lineEnd;
  Position goal;
  
  Position body;
  Position velocity;
  
  float lineLength;
  float angle;
  
  boolean moving;
  
  float noiseStart = random(100);
  float noiseChange = .007;
  
  float speed;
  
  float offSetX;
  float offSetY;

  float startOffX;
  float startOffY;
  
  int faceIndex;
  int posIndex;
  
  int index;
  
  float radius;
  float points;
  
  float partWidth;
  float partHeight;
  
  float bodyPart;
  
  int aliveFor;
  int nextTime = millis() + 1000;
  
  int rot;
  
  float lifeSpan;
  
  ArrayList<Position> posList;
  
  ZoomLine(float startX, float startY, float goalX, float goalY, int points) {
    
    this.start = new Position(startX, startY);
    this.goal = new Position(goalX, goalY);
    
    this.speed = 5;
   
    this.lineLoc = new Position(startX, startY);
    
    this.index = 0;
    
    this.lifeSpan = random(4,7);
    
    if (random(1) < .5) {
      this.rot = -1;
    } else {
      this.rot = 1;
    }
   
    this.points = points;
    
    this.bodyPart = (int)random(4);
    
    findBody();
    
    makePosList();
    updateAngle();
    updateLength();
    updateLineEnd();
    
    float largest = max(width,height) / poseScale;
    this.offSetX = random(-largest * .01,largest * .01);
    this.offSetY = random(-largest * .01,largest * .01);
  }
  
  //Finds a body to float around
  void findBody() {
    //Left Eye
    if (this.bodyPart == 0) { 
      this.radius = 20;
      this.partWidth = 20;
      this.partHeight = 10;
      this.body = new Position(-25, eyeLeft * -9);
    }
    // Right Eye 
    else if (this.bodyPart == 1) {
      this.radius = 20;
      this.partWidth = 20;
      this.partHeight = 10;
      this.body = new Position(25, eyeRight * -9);
    } 
    //Nose
    else if (this.bodyPart == 2) {
      this.radius = 10;
      this.partWidth = 10;
      this.partHeight = 7;
      this.body = new Position(0,nostrils * -1);
    } 
    // Mouth
    else {
      this.radius = mouthWidth * 2;
      this.partWidth = mouthWidth * 2;
      this.partHeight = mouthHeight * 2;
      this.body = new Position(0, 20);
    }
  }
  
  //Creates a list of positions around a given point (goalX and Y in this case)
  void makePosList(){
    this.posList = new ArrayList<Position>();
    float w = this.partWidth;
    float h = this.partHeight;
    for (int i = 0; i < this.points; i++) {
        float theta = i * (6.28 / this.points );
        this.posList.add(new Position(this.body.x + (w * cos(theta)), this.body.y + (h * sin(theta)) ) );
    }
  }
  
  //Updates speed of the line based on distance to the target position.   
  void updateSpeed() {
    float distance = dist(this.goal.x, this.goal.y,
                          this.start.x, this.start.y);
    this.speed = distance / this.lineLength;
  }
      
  //Updates the angle between the goal and lineLoc.
  void updateAngle() {
    float dx = this.goal.x - this.lineLoc.x;
    float dy = this.goal.y - this.lineLoc.y;
    
    this.angle = atan2(dy,dx);
  }
  
  //Semi randomly determines length based on the start, goal and a noise value. 
  void updateLength() {
    float distance = dist(this.goal.x, this.goal.y,
                          this.start.x, this.start.y);
    
    this.noiseStart += this.noiseChange;
    
    this.lineLength = (distance * .4) * .5;
  }
  
  //Updates the end position of the line bases on the angle between the location and goal
  //as well as the length of the line. 
  void updateLineEnd() {
    float yChange = sin(this.angle) * this.lineLength;
    float xChange = cos(this.angle) * this.lineLength;
    this.lineEnd = new Position(this.lineLoc.x + xChange, this.lineLoc.y + yChange);
  }
  
  //Draws the line on the screen. 
  void drawMe() {
    strokeCap(ROUND);
    strokeWeight(4);
    line(this.lineLoc.x + offSetX, this.lineLoc.y + offSetY, 
         this.lineEnd.x + offSetX, this.lineEnd.y + offSetY);
  } 
  
  //Updates goal position.
  void updateGoal() {
    this.goal = this.posList.get(this.index);    
  }
  
  //Updates all variables for movement and drawing. 
  void update() {
    this.findBody();
    this.makePosList();
    this.updateGoal();
    this.updateAngle();
    this.updateLineEnd();
    this.move();
    if (millis() >= this.nextTime) {
      this.aliveFor += 1;
      this.nextTime = this.nextTime + 1000;
    }
  }
  
  //Moves line between lineLoc and goal. Resets Line if it hits goal.
  void move() {
    float distance = dist(this.lineLoc.x,this.lineLoc.y,this.goal.x, this.goal.y);
    this.lineLoc.x += this.speed*((this.goal.x - this.lineLoc.x)/distance);
    this.lineLoc.y += this.speed*((this.goal.y - this.lineLoc.y) / distance);
    
    if (dist(this.lineLoc.x, this.lineLoc.y,this.goal.x, this.goal.y) <= (width * .02)) {
          this.index = abs(this.index + 1) % (int)this.points;
          this.start.x = this.goal.x;
          this.start.y = this.goal.y;
          this.updateGoal();
          this.updateLength();
          this.updateSpeed();

    }
  }
}

//Returns a position vector with the input x, y.
class Position {
    float x;
    float y;
    Position(float x, float y) {
      this.x = x;
      this.y = y;
    }
}

//Returns a random Position in a rectangle drawn from startX, startY, to endX, endY.
class RandomPosition extends Position {
      RandomPosition(float startX, float endX, float startY, float endY) {
      super(random(startX, endX),random(startY, endY));
    }
}

Comments are closed.