package com.codefixia.input.multitouch; import java.util.ArrayList; import java.util.Iterator; import com.codefixia.drumcloud.DrumCloud; import processing.core.PApplet; import processing.core.PVector; //TODO: make distance thershold based on pixel density information! public class TouchProcessor { // heuristic constants long TAP_INTERVAL = 200; long TAP_TIMEOUT = 200; int DOUBLE_TAP_DIST_THRESHOLD = 30; int FLICK_VELOCITY_THRESHOLD = 50; float MAX_MULTI_DRAG_DISTANCE = 100; // from the centroid // A list of currently active touch points ArrayList<TouchPoint> touchPoints; // Used for tap/doubletaps TouchPoint firstTap; TouchPoint secondTap; long tap; int tapCount = 0; // Events to be broadcast to the sketch ArrayList<TouchEvent> events; // centroid information float cx, cy; float old_cx, old_cy; boolean pointsChanged = false; DrumCloud drumCloud; public long millis(){ return ((PApplet) DrumCloud.X).millis(); } //------------------------------------------------------------------------------------- TouchProcessor() { touchPoints = new ArrayList<TouchPoint>(); events = new ArrayList<TouchEvent>(); } public TouchProcessor(DrumCloud drumCloud) { touchPoints = new ArrayList<TouchPoint>(); events = new ArrayList<TouchEvent>(); this.drumCloud=drumCloud; } //------------------------------------------------------------------------------------- // Point Update functions public synchronized void pointDown(float x, float y, int id, float pressure) { TouchPoint p = new TouchPoint(x, y, id, pressure); touchPoints.add(p); updateCentroid(); if ( touchPoints.size() >= 2) { p.initGestureData(cx, cy); if (touchPoints.size() == 2) { // if this is the second point, we now have a valid centroid to update the first point TouchPoint frst = (TouchPoint)touchPoints.get(0); frst.initGestureData(cx, cy); } } // tap detection if (tapCount == 0) { firstTap = p; } if (tapCount == 1) { secondTap = p; } tap = millis(); pointsChanged = true; } //------------------------------------------------------------------------------------- public synchronized void pointUp(int id) { TouchPoint p = getPoint(id); touchPoints.remove(p); // tap detection // TODO: handle a long press event here? if ( p == firstTap || p == secondTap ) { // this could be either a Tap or a Flick gesture, based on movement float d = PApplet.dist(p.x, p.y, p.px, p.py); if ( d > FLICK_VELOCITY_THRESHOLD ) { FlickEvent event = new FlickEvent(p.px, p.py, new PVector(p.x-p.px, p.y-p.py)); events.add(event); } else { long interval = millis() - tap; if ( interval < TAP_INTERVAL ) { tapCount++; } } } pointsChanged = true; } //------------------------------------------------------------------------------------- public synchronized void pointMoved(float x, float y, int id) { TouchPoint p = getPoint(id); p.update(x, y); // since the events will be in sync with draw(), we just wait until analyse() to // look for gestures pointsChanged = true; } //------------------------------------------------------------------------------------- // Calculate the centroid of all active points void updateCentroid() { old_cx = cx; old_cy = cy; cx = 0; cy = 0; for (int i=0; i < touchPoints.size(); i++) { TouchPoint p = (TouchPoint)touchPoints.get(i); cx += p.x; cy += p.y; } cx /= touchPoints.size(); cy /= touchPoints.size(); } //------------------------------------------------------------------------------------- public synchronized void analyse() { handleTaps(); // simple event priority rule: do not try to rotate or pinch while dragging // this gets rid of a lot of jittery events if (pointsChanged) { updateCentroid(); if (handleDrag() == false) { handleRotation(); handlePinch(); } pointsChanged = false; } } //------------------------------------------------------------------------------------- // send events to the sketch public void sendEvents() { for (int i=0; i < events.size(); i++) { TouchEvent e = (TouchEvent)events.get(i); if ( e instanceof TapEvent ) drumCloud.onTap( (TapEvent)e ); else if ( e instanceof FlickEvent ) drumCloud.onFlick( (FlickEvent)e ); else if ( e instanceof DragEvent ) drumCloud.onDrag( (DragEvent)e ); else if ( e instanceof PinchEvent ) drumCloud.onPinch( (PinchEvent)e ); else if ( e instanceof RotateEvent ) drumCloud.onRotate( (RotateEvent)e ); } events.clear(); } //------------------------------------------------------------------------------------- void handleTaps() { if (tapCount == 2) { // check if the tap point has moved float d = PApplet.dist(firstTap.x, firstTap.y, secondTap.x, secondTap.y); if ( d > DOUBLE_TAP_DIST_THRESHOLD ) { // if the two taps are apart, count them as two single taps TapEvent event1 = new TapEvent(firstTap.x, firstTap.y, TapEvent.SINGLE); drumCloud.onTap(event1); TapEvent event2 = new TapEvent(secondTap.x, secondTap.y, TapEvent.SINGLE); drumCloud.onTap(event2); } else { events.add( new TapEvent(firstTap.x, firstTap.y, TapEvent.DOUBLE) ); } tapCount = 0; } else if (tapCount == 1) { long interval = millis() - tap; if (interval > TAP_TIMEOUT) { events.add( new TapEvent(firstTap.x, firstTap.y, TapEvent.SINGLE) ); tapCount = 0; } } } //------------------------------------------------------------------------------------- // rotation is the average angle change between each point and the centroid void handleRotation() { if (touchPoints.size() < 2) return; // look for rotation events float rotation = 0; for (int i=0; i < touchPoints.size(); i++) { TouchPoint p = (TouchPoint)touchPoints.get(i); float angle = PApplet.atan2( p.y-cy, p.x-cx ); p.setAngle(angle); float delta = p.angle - p.oldAngle; if ( delta > PApplet.PI ) delta -= PApplet.TWO_PI; if ( delta < -PApplet.PI ) delta += PApplet.TWO_PI; rotation += delta; } rotation /= touchPoints.size() ; if ( rotation != 0 ) events.add( new RotateEvent(cx, cy, rotation, touchPoints.size()) ); } //------------------------------------------------------------------------------------- // pinch is simply the average distance change from each points to the centroid void handlePinch() { if (touchPoints.size() < 2) return; // look for pinch events float pinch = 0; for (int i=0; i < touchPoints.size(); i++) { TouchPoint p = (TouchPoint)touchPoints.get(i); float distance = PApplet.dist(p.x, p.y, cx, cy); p.setPinch(distance); float delta = p.pinch - p.oldPinch; pinch += delta; } pinch /= touchPoints.size(); if (pinch != 0) events.add( new PinchEvent(cx, cy, pinch, touchPoints.size()) ); } //------------------------------------------------------------------------------------- boolean handleDrag() { // look for multi-finger drag events // multi-drag is defined as all the fingers moving close-ish together in the same direction boolean x_drag = true; boolean y_drag = true; boolean clustered = false; int first_x_dir = 0; int first_y_dir = 0; for (int i=0; i < touchPoints.size(); i++) { TouchPoint p = (TouchPoint)touchPoints.get(i); int x_dir = 0; int y_dir = 0; if (p.dx() > 0) x_dir = 1; if (p.dx() < 0) x_dir = -1; if (p.dy() > 0) y_dir = 1; if (p.dy() < 0) y_dir = -1; if (i==0) { first_x_dir = x_dir; first_y_dir = y_dir; } else { if (first_x_dir != x_dir) x_drag = false; if (first_y_dir != y_dir) y_drag = false; } // if the point is stationary if (x_dir == 0) x_drag = false; if (y_dir == 0) y_drag = false; if (touchPoints.size() == 1) clustered = true; else { float distance = PApplet.dist(p.x, p.y, cx, cy); if ( distance < MAX_MULTI_DRAG_DISTANCE ) { clustered = true; } } } if ((x_drag || y_drag) && clustered) { if (touchPoints.size() == 1) { TouchPoint p = (TouchPoint)touchPoints.get(0); // use the centroid to calculate the position and delta of this drag event events.add(new DragEvent(p.x, p.y, p.dx(), p.dy(), 1)); } else { // use the centroid to calculate the position and delta of this drag event events.add(new DragEvent(cx, cy, cx-old_cx, cy-old_cy, touchPoints.size())); } return true; } return false; } //------------------------------------------------------------------------------------- synchronized ArrayList getPoints() { return (ArrayList)touchPoints.clone(); } //------------------------------------------------------------------------------------- synchronized TouchPoint getPoint(int pid) { Iterator i = touchPoints.iterator(); while (i.hasNext ()) { TouchPoint tp = (TouchPoint)i.next(); if (tp.id == pid) return tp; } return null; } }