Implementing a multi-touch event system to deliver blob events to registered widgets (and creating a demo photo application with inertia) part 1 of 2

In my previous posts we've discussed blob extraction and tracking. Now we'll take it one step further and design an event system to handle those events and deliver them to registered widgets. In this post we will focus on the event system and the widget base class. In the next post we will extend the widget base class to create a photo application like below.

The photo application we will implement in part 2.

I've drawn up a quick flowchart of what we'll be attempting to implement. Everything in the left column under "Input System" we've already completed. The event queue is at the core of the "Event System". It will be responsible for accepting blob events from our tracking system and parsing those events out to the appropriate widgets. Our widgets will belong to the "Output System" and will all be derived from an abstract base class. These widgets will be registered with the event queue, and the event queue will store a vector of pointers to their base classes. As mentioned the event queue will parse out the blob events to the registered widgets, and it will then cycle through each widget calling the update and render methods. The widgets themselves will store a vector of pointers to the blob events belonging to them. The widgets determine how to respond to those events when their update method is called and, similarly, how to render themselves.

A flowchart of the event queue we will be attempting to implement.

In the source below we declare our event queue object, cQueue. Within this object we will store a vector of registered widgets, and we have a couple of methods, registerWidget() and unregisterWidget(), to bind and unbind our widgets to and from the event queue. The final method, processEvents(), will parse out the events to the widgets and update them appropriately.

#ifndef QUEUE_H
#define QUEUE_H

#include <algorithm>
#include "blob.h"
#include "widget.h"



class cQueue {
  private:
	std::vector<cWidget *> widgets;
    
  protected:
    
  public:
	cQueue();
	~cQueue();

	void registerWidget(cWidget& widget);
	void unregisterWidget(cWidget& widget);
	
	void processEvents(std::vector<cBlob>& blobs);
};

#endif

In the definition below we first define the functors compareBlobID and compareWidgetsZ. These functors will be used to prioritize our blob events by their id (older events are assigned a lower id) and our widgets by their z-index (nearer blobs are evaluated first for event containment). In our registerWidget() method we first determine if a widget instance is already registered and, if not, push it onto the vector. Similarly, unregisterWidget() seeks the specified widget and removes it from the vector. In our processEvents() method we first sort our blob events by their identification. Older events will be given priority. We then sort the registered widgets by their z-index. This will allow us to check for the containment of blob events as we descend through the widget layers. We evaluate if an event was attached to a widget in the last frame, and, if so, reattach it to the same widget (this would be a BLOB_MOVE event). We descend through the widget layers evaluating whether the blob events are contained in the current layer. If a blob event is contained within a widget, we add the event to the widget and store a pointer to the widget in the blob instance. Lastly, each widget contains an update() method to process the events and a render() method to render the result.

#include "queue.h"



struct compareBlobID {
	bool operator()(const cBlob& lhs, const cBlob& rhs) const { return (lhs.id < rhs.id); }
};

struct compareWidgetsZ {
	bool operator()(const cWidget *lhs, const cWidget *rhs) const { return (lhs->z < rhs->z); }
};



cQueue::cQueue() {
}

cQueue::~cQueue() {
}

void cQueue::registerWidget(cWidget& widget) {
	int i;
	for (i = 0; i < widgets.size(); i++) {
		if (widgets[i] == &widget) {
			break;
		}
	}
	if (i == widgets.size()) { widgets.push_back(&widget); widget.bringForward(); }
}

void cQueue::unregisterWidget(cWidget& widget) {
	for (int i = 0; i < widgets.size(); i++) {
		if (widgets[i] == &widget) {
			widgets.erase(widgets.begin() + i);
			break;
		}
	}
}

void cQueue::processEvents(std::vector<cBlob>& blobs) {
	sort(blobs.begin(), blobs.end(), compareBlobID());		// process older blobs first
	sort(widgets.begin(), widgets.end(), compareWidgetsZ());	// process widgets based on zindex ordering

	for (int i = 0; i < widgets.size(); i++) widgets[i]->resetEvents();

	for (int i = 0; i < blobs.size(); i++) {
		if (blobs[i].widget) {
			(static_cast<cWidget *>(blobs[i].widget))->addEvent(blobs[i]);
			blobs[i].tracked = true;
		} else blobs[i].tracked = false;
	}

	for (int i = widgets.size() - 1; i > -1; i--) {
		for (int j = 0; j < blobs.size(); j++) {
			if (blobs[j].tracked == true) continue;

			if (widgets[i]->containsPoint(blobs[j].location)) {
				widgets[i]->addEvent(blobs[j]);
				blobs[j].widget = widgets[i];
				blobs[j].tracked = true;
			}
		}
	}

	for (int i = widgets.size() - 1; i > -1; i--) {
		widgets[i]->update();
	}

	for (int i = 0; i < widgets.size(); i++) {
		widgets[i]->render();
	}
}

In the code below I have isolated the declaration of the cWidget object. This is an abstract base class that will store our transformations, the widget color, the events vector, and the z-index. We've seen the use of resetEvents() and addEvent() in the code above. We have four methods, bringForward(), translateWidget(), rotateWidget(), and scaleWidget(), for transforming our widget and three methods to evaluate containment, update the widget state based on current blob events, and render the current state. The three pure virtual functions will be discussed in the next post when we extend this class to create widgets that we can use (i.e. widgets we can instantiate).

class cWidget {
  private:

  protected:
	point translate; double rotate, scale;
	point translate_previous; double rotate_previous, scale_previous;
	rgba color;

	std::vector<cBlob *> events;

  public:
	int z;
	static int zindex;
	
	cWidget(point translate, double rotate, double scale, rgba color);
	~cWidget();

	void resetEvents();			// reset events vector to empty
	void addEvent(cBlob& event);		// event queue populates events vector

	void bringForward();			// increase z index
	void translateWidget(const point& p);
	void rotateWidget(const point& p0, const point& p1);
	void scaleWidget(const point& p0, const point& p1);

	virtual bool containsPoint(const point& p) const = 0;		// so event queue knows if an event belongs to a particular instance of cWidget
	virtual void update() = 0;					// called when events vector is populated
	virtual void render() = 0;					// called once events have been processed
};

Below I have isolated the definitions of the methods for this object. resetEvents() simply clears out the events vector, addEvent() pushes an event onto the events vector, and bringForward() simply increases the widget's z-index to bring it to the top. In translateWidget() we store the previous translation as a simple weighting function. This will allow us to add inertia to our widget when it isn't receiving any events. This method accepts a single vector, e.g. a BLOB_MOVE event contains a vector from its origin to its current location, and the widget's translate property is updated accordingly. Both the scaleWidget() and rotateWidget() methods require two vectors and a point about which to update the translation property. The vectors we will pass in are blue in the diagram below, and the point we will pass is 0.5 * vector[1].

Event vectors evaluated for the scale and rotate methods.

Our rotation method evaluates the angle between the new and old vectors and updates the rotation property accordingly. It then updates our translation property by rotating the vector, translate - p2, by the evaluated rotation angle. Similarly, the scale method evaluates the magnitude of the event vectors to update the scale property, and, again, attempts to update the translation property by scaling the vector, translate - p2, by the evaluated scale factor.

int cWidget::zindex = 0;

cWidget::cWidget(point translate, double rotate, double scale, rgba color) :
	translate(translate), rotate(rotate), scale(scale),
	translate_previous(point(0,0)), rotate_previous(0.0), scale_previous(1.0),
	color(color), z(zindex++) {
}

cWidget::~cWidget() {
}

void cWidget::resetEvents() {
	events.clear();
}

void cWidget::addEvent(cBlob& event) {
	events.push_back(&event);
}

void cWidget::bringForward() {
	z = zindex++;
}

void cWidget::translateWidget(const point& p) {
	translate_previous = translate_previous * 0.5 + p * 0.5;
	translate += p;
}

void cWidget::rotateWidget(const point& p0, const point& p1, const point& p2) {
	double angle = (atan2(p1.y, p1.x) - atan2(p0.y, p0.x));
	rotate_previous = rotate_previous * 0.5 + angle * 0.5;
	rotate += angle;

	point s0 = translate - p2, s1;
	s1.x = s0.x*cos(angle) - s0.y*sin(angle);
	s1.y = s0.x*sin(angle) + s0.y*cos(angle);
	translate = p2 + s1;
}

void cWidget::scaleWidget(const point& p0, const point& p1, const point& p2) {
	double s = p1.magnitude() / p0.magnitude();
	scale_previous = scale_previous * 0.5 + s * 0.5;
	scale *= s;
	
	point s0 = translate - p2, s1;
	s1 = s0*s;
	translate = p2 + s1;
}

The widget.h and widget.cc files available in the project download contain classes derived from cWidget: cRectangle, cTexture, cTextureBackground, cTextureBorder, cTextureVideo, cCalibration, and cFluid. We will discuss a few of these in the second part of this post as we implement the photo application. cTextureVideo uses libVLC to render a video widget and cFluid is an implementation of Jos Stam's Stable Fluids. cCalibration is a preliminary calibration widget implemented using barycentric coordinates. We will likely discuss a calibration method for multi-touch displays in a future post.

As always I look forward to hearing from you.

Download this project: event.tar.bz2

Leave a Reply

Your email address will not be published.