Teamweek’s touch-enabled user interface

Teamweek has recently launched a new user interface, which is built on HTML canvas using the CreateJS libraries. In this blog post, I will show how to combine CreateJS with HammerJS to get all the HammerJS goodies into your canvas application.

CreateJS

EaselJS of the CreateJS suite of libraries, is what our canvas application is built upon. It offers a very nice API for drawing a user interface and even supports touch events out-of-the box.

However, the support for touch events is pretty basic and doesn’t contain any convenience wrappers for gesture detection or anything like that.

HammerJS

HammerJS is very convenient library for dealing with multitouch devices. It combines mouse and touch events into a single unified collection of events. It also offers gesture detection and some predefined gestures like “swipe”, “pinch zoom”, “rotate”, etc.

CreateJS + HammerJS

So how do we combine HammerJS with CreateJS so that we can bind HammerJS events to our DisplayObjects, instead of just HTML nodes?

CreateJS hasn’t really made any effort to make this simple for us yet, so it’s monkey patching all the way. Fortunately though, we can just get away with modifying only 1 method of the Stage class.

Note: createjs.Touch sounds like it should be the class to investigate but it turns out that the necessary code lives in the createjs.Stage class at the time of writing this article.

The method we want to look into is Stage.enableDOMEvents. What this does, is bind/unbind mouse event handlers to the window object. We want to override that with our own HammerJS-based event binding.

The default enableDOMEvents binds 4 events (mouseup, mousemove, dblclick, mousedown). It would be nice to be able to support all of HammerJS gestures, so we’re going to write our own custom event handler that will emulate the behaviour of the built-in handlers. Let’s call it _handleTouchEvent.

While we’re at it, we can add event propagation to the mix. What we end up with, is a more traditional event model found in the DOM and Adobe Flash. Event propagation should go into the EventDispatcher class, but I’ve included it right here for simplicity.

NB! This completely overrides the default mouse events and the EaselJS way of binding mousemove and mouseup handlers to the mousedown event (instead of DisplayObjects).

Here’s a mixin class for createjs.Stage along with the custom-built TouchEvent class:

MultiTouchStage = {
  _primaryPointerID: 1,
  _hammer_events: "Hold Tap DoubleTap Drag DragStart DragEnd DragUp DragDown DragLeft DragRight Swipe SwipeUp SwipeDown SwipeLeft SwipeRight Transform TransformStart TransformEnd Rotate Pinch PinchIn PinchOut Touch Release".split(' '),

  enableDOMEvents: function(enable) {
    var event_name, hammer, k, ls, v, i;
    ls = this._eventListeners;
    if (!enable && ls) { // Remove event listeners
      for (k in ls) {
        v = ls[k];
        v.t.off(k, v.f);
      }
      this._eventListeners = null;
    } else if (enable && !ls && this.canvas) { // Add event listeners
      hammer = $(this.canvas).hammer();
      if (!ls) {
        ls = this._eventListeners = {};
      }
      for (i = 0; i < this._hammer_events.length; i++) {
        event_name = this._hammer_events[i];
        ls[event_name.toLowerCase()] = (function(_this, event_name) {
          return {
            t: hammer,
            f: function(e) {
              return _this._handleTouchEvent(event_name, e);
            }
          };
        })(this, event_name);
      }
      for (k in ls) {
        v = ls[k];
        v.t.on(k, v.f);
      }
    }
  },

  _handleTouchEvent: function(event_name, e) {
    /* All HammerJS events end up here - let's find the touch targets and fire any event handlers.
     * This also adds event propagation (only bubbling).
     */
    var evt, i, o, obj, original_target, target;

    // Add a fake "pointer", set's up mouseX, mouseY as the gesture center point
    this._updatePointerPosition(this._primaryPointerID, e.gesture.center.pageX, e.gesture.center.pageY);

    // Finds the front-most inner-most child 
    target = original_target = this._getObjectsUnderPoint(this.mouseX, this.mouseY, null, null, true);
    if (!target) {
        target = this; // Stage will always receive touch events
    }

    while (target) { // Event propagation loop
      if (target["on" + event_name] || target.hasEventListener(event_name.toLowerCase())) {
        // Call any event handlers
        evt = new TouchEvent(event_name.toLowerCase(), target, original_target, e, this.mouseX, this.mouseY);
        if (target["on" + event_name]) {
          target["on" + event_name](evt);
        }
        target.dispatchEvent(evt);
        if (!evt._propagates) { // stopPropagation() support
          break;
        }
      }
      target = target.parent;
    }
  }
};

function TouchEvent(type, target, original_target, original_event, stageX, stageY) {
  this.type = type;
  this.target = target;
  this.original_target = original_target;
  this.original_event = original_event;
  this.stageX = stageX;
  this.stageY = stageY;
  this._propagates = true;
}

TouchEvent.prototype.clone = function() {
  return new createjs.ext.TouchEvent(this.type, this.target, this.original_target, this.original_event, this.stageX, this.stageY);
};

TouchEvent.prototype.toString = function() {
  return "[TouchEvent (type=" + this.type + " x=" + this.stageX + " y=" + this.stageY + ")]";
};

TouchEvent.prototype.stopPropagation = function() {
  return this._propagates = false;
};

You can use it simply by extending your stage object with the mixin (using underscore’s extend for example).

var myStage = _.extend(new createjs.Stage(myCanvas), MultiTouchStage);

 Note that, this is not a complete solution for a number of reasons:

  • It completely replaces EaselJS’s default mouse events with its own.

  • It doesn’t respect EaselJS’s enableMouseOver functionality and processes mousemove events immediately instead.

  • The DisplayObject, which dispatches the HammerJS events, is determined by the center point of the gesture which might move during the gesture, thus the start and end events of a gesture might be emitted by different objects.

  • Event propagation should live in the EventDispatcher class

Regardless, this should give you a starting point for providing full HammerJS support in your canvas application.

If you ever do modify the EventDispatcher to include event propagation, be wary of the ‘tick’, event which is called on every DisplayObject – you probably don’t want to propagate that or you’ll end up with a massive flood of ‘tick’ events to the root object.

So that’s basically how we’re using HammerJS and EaselJS in our Teamweek application. Monkey patching is not pretty but it gets things done. Fortunately though, EaselJS is a relatively simple and thin framework so getting into the guts of it, is not such a big endeavor.

Teamweek touch interface
TeamWeek is an online Project Planning Tool with a Team CalendarBeing an antidote to clumsy Gantt charts, it allows managers to respond to change faster.