Multitouch gesture transformations with EaselJS

One thing I really liked about EaselJS in the first place was its support for touch events despite the lack of multitouch gestures. So I started to implement them myself as it’s not really rocket science.

Here’s a simple example with a square that can be moved around the canvas but also scaled and rotated by using two fingers. You can either try it on your iPad or by holding down the shift key on a desktop computer: Multitouch Example

By enabling touch support in EaselJs the individual events can be handled like this:

1
2
3
4
5
6
7
8
createjs.Touch.enable(stage);
 
var element = new createjs.Container();
element.addEventListener('pressmove', pressmove);
 
function pressmove(event){
  console.log('finger #' + event.pointerID + ' at ' + event.stageX + ', ' + event.stageY);
}

It’s actually as if you would handle mouse events with the addition of a pointerID (the ID of the touchpoint – your finger), that is dispatching the event.

With that information it’s really simple to handle the events. The most important thing is to keep track of all the touchpoint-positions for later calculations.

Abstract touchpoint-tracker class

By using some utility functions I previously wrote and like to use for creating classes – sys.js – you can create a subclass of EaselJS‘ Container that keeps track of touchpoint-positions for later calculations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
setPackage('com.firsara.display');
 
com.firsara.display.Transformable = (function(){
  var Parent = createjs.Container;
 
  var Transformable = function(){
    // reference to instance
    var self = this;
 
    self._activeFingers = 0;
    self._fingers = [];
 
    var _changed = false;
 
    var Init = function(){
      // call super constructor
      if (Parent) Parent.call(self);
 
      self._initialized = true;
 
      self.addEventListener('mousedown', _mousedown);
      self.addEventListener('pressmove', _pressmove);
      self.addEventListener('pressup', _pressup);
      self.addEventListener('tick', _enterFrame);
    };
 
    // store initial touchpoint-position
    var _mousedown = function(event){
      if (! event.pointerID) event.pointerID = -1;
 
      self._fingers[event.pointerID] = {
        start: {x: event.stageX, y: event.stageY},
        current: {x: event.stageX, y: event.stageY},
        old: {x: event.stageX, y: event.stageY}
      };
 
      _calculateActiveFingers();
 
      self.dispatchEvent('start');
    };
 
    // update touchpoint-positions
    var _pressmove = function(event){
      if (! event.pointerID) event.pointerID = -1;
 
      self._fingers[event.pointerID].current.x = event.stageX;
      self._fingers[event.pointerID].current.y = event.stageY;
 
      _calculateActiveFingers();
 
      _changed = true;
    };
 
    // if positions changed (through pressmove): dispatch update-event for later usage and keep track of old point-position
    // dispatch updates only on tick to save some performance
    var _enterFrame = function(){
      if (_changed) {
        _changed = false;
        self.dispatchEvent('update');
 
        for (var pointerID in self._fingers) {
          if (self._fingers[pointerID].start) {
            self._fingers[pointerID].old.x = self._fingers[pointerID].current.x;
            self._fingers[pointerID].old.y = self._fingers[pointerID].current.y;
          }
        }
      }
    };
 
    // delete old and unused finger-positions
    var _pressup = function(event){
      if (! event.pointerID) event.pointerID = -1;
 
      if (self._fingers[event.pointerID]) {
        delete(self._fingers[event.pointerID]);
      }
 
      _calculateActiveFingers();
 
      self.dispatchEvent('complete');
    };
 
    // calculates currently active fingers, can be used later in subclasses
    var _calculateActiveFingers = function(){
      self._activeFingers = 0;
 
      for (var pointerID in self._fingers) {
        if (self._fingers[pointerID].start) {
          self._activeFingers++;
        }
      }
    };
 
    Init();
  };
 
  // export public Transformable definition
  Transformable.prototype = {};
 
  // extend Transformable with defined parent
  if (Parent) sys.inherits(Transformable, Parent);
 
  // return Transformable definition to public scope
  return Transformable;
})();

I won’t explain all the different functions of the class in depth, they’re pretty simple and straight forward and I’m sure you can figure it all out by yourself!

The class basically just keeps track of all the touchpoint-positions and dispatches events when positions are changed. It’s an abstract class that can be used by subclasses to calculate the transformations later on.

Implementing subclasses with transformations

With our base class Transformable we can create subclasses and calculate the individual transformations. My approach was to define three different classes, one for each type of transformation (x and y positions, scale, rotation) and one last class that uses the other ones as a mixin to combine all of the transformation-classes into single one.

This way it’s much easier to adjust behaviours and fix bugs. Plus you can also save a lot of performance by only using the class you actually need if you don’t have to have all transformations on a single element.

Let’s start with the easiest one:

rotation

Rotation can simply be set by constantly adding the difference of the angles between finger movements.

Let’s say you initially touch with two fingers at a 45° angle and move one finger in a circle-form around your object to 20°. Your object will be rotated to -25° (new angle subtracted by old angle). Apply this calculation every time the touchpoint-positions change and you’re done.

The angle between two points can be calculated by using Math.atan2:

1
2
3
var deltaX = point1.x - point2.x;
var deltaY = point1.y - point2.y;
var angle = Math.atan2(deltaY, deltaX);

Note that the angle is in radians so you’ll have to multiply by 180/Math.PI to get the corresponding angle in degrees.

With that formula we can implement a RotateClip like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
setPackage('com.firsara.display');
 
com.firsara.display.RotateClip = (function(){
  var Parent = com.firsara.display.Transformable;
 
  var RotateClip = function(){
    var self = this;
 
    var Init = function(){
      // call super constructor, only if instance is not a mixin of another class
      if (Parent && ! self._initialized) Parent.call(self);
 
      self.addEventListener('update', _update);
    };
 
    var _update = function(event){
      if (self._activeFingers > 1) {
        var points = [];
 
        // extract touchpoints
        for (var k in self._fingers) {
          if (self._fingers[k].current) {
            points.push(self._fingers[k]);
            if (points.length >= 2) break;
          }
        }
 
        // calculate initial angle
        var point1 = points[0].old;
        var point2 = points[1].old;
        var startAngle = Math.atan2((point1.y - point2.y), (point1.x - point2.x)) * (180 / Math.PI);
 
        // calculate new angle
        var point1 = points[0].current;
        var point2 = points[1].current;
        var currentAngle = Math.atan2((point1.y - point2.y), (point1.x - point2.x)) * (180 / Math.PI);
 
        // set rotation based on difference between the two angles
        self.rotation += (currentAngle - startAngle);
 
        self.dispatchEvent('rotate');
      }
    };
 
    Init();
  };
 
  RotateClip.prototype = {};
  if (Parent) sys.inherits(RotateClip, Parent);
  return RotateClip;
})();

scaling

Scaling can be calculated by dividing the distance of the current touchpoint-positions by the distance of the old touchpoint-positions.
With that being said, a ScaleClip implementation will look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
setPackage('com.firsara.display');
 
com.firsara.display.ScaleClip = (function(){
  var Parent = com.firsara.display.Transformable;
 
  var ScaleClip = function(){
    var self = this;
 
    var Init = function(){
      if (Parent && ! self._initialized) Parent.call(self);
 
      self.addEventListener('update', _update);
    };
 
    var _getDistance = function(p1, p2) {
      var x = p2.x - p1.x;
      var y = p2.y - p1.y;
 
      return Math.sqrt((x * x) + (y * y));
    };
 
    var _update = function(event){
      if (self._activeFingers > 1) {
        var points = [];
 
        // extract touchpoints
        for (var k in self._fingers) {
          if (self._fingers[k].current) {
            points.push(self._fingers[k]);
            if (points.length >= 2) break;
          }
        }
 
        var scale = _getDistance(points[0].current, points[1].current) / _getDistance(points[0].old, points[1].old);
 
        self.scaleX += (scale - 1);
        self.scaleY = self.scaleX;
 
        self.dispatchEvent('scale');
      }
    };
 
    Init();
  };
 
  ScaleClip.prototype = {};
  if (Parent) sys.inherits(ScaleClip, Parent);
  return ScaleClip;
})();

movement

As we’re talking about multiple touchpoints here, movement is not only calculated by subtracting the current x- and y position from the old one, but by calculating an average of all touchpoints first. The class I came up with looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
setPackage('com.firsara.display');
 
com.firsara.display.MoveClip = (function(){
  var Parent = com.firsara.display.Transformable;
 
  var MoveClip = function(){
    var self = this;
 
    var Init = function(){
      if (Parent && ! self._initialized) Parent.call(self);
 
      self.addEventListener('update', _update);
    };
 
    var _update = function(event){
      var average = {x: 0, y: 0};
 
      // caluclate average movement between all points
      for (var pointerID in self._fingers) {
        if (self._fingers[pointerID].start) {
          average.x += (self._fingers[pointerID].current.x - self._fingers[pointerID].old.x);
          average.y += (self._fingers[pointerID].current.y - self._fingers[pointerID].old.y);
        }
      }
 
      average.x /= Math.max(1, self._activeFingers);
      average.y /= Math.max(1, self._activeFingers);
 
      // set new positions
      self.x += average.x;
      self.y += average.y
 
      self.dispatchEvent('move');
    };
 
    Init();
  };
 
  MoveClip.prototype = {};
  if (Parent) sys.inherits(MoveClip, Parent);
  return MoveClip;
})();

All transformations combined

With all those classes it’s easy to create the final TransformClip that combines all three together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
setPackage('com.firsara.display');
 
com.firsara.display.TransformClip = (function(){
  var Parent = com.firsara.display.Transformable;
 
  var TransformClip = function(){
    var self = this;
 
    var Init = function(){
      // call super constructor
      if (Parent) Parent.call(self);
      // mix in other classes
      if (com.firsara.display.MoveClip) com.firsara.display.MoveClip.call(self);
      if (com.firsara.display.RotateClip) com.firsara.display.RotateClip.call(self);
      if (com.firsara.display.ScaleClip) com.firsara.display.ScaleClip.call(self);
    };
 
    Init();
  };
 
  TransformClip.prototype = {};
  if (Parent) sys.inherits(TransformClip, Parent);
  return TransformClip;
})();

The final classes I built additionally fade out transformations when releasing your finger (as if you would throw some object on an icy surface). I didn’t explain that functionality here, but it’s not that complicated. You basically have to calculate an acceleration base and animate all the properties by a fraction of it. I like to use Tweenlite for those kind of things. It’s an insanely fast animation engine that outweighs jQuery by far.

Final words

It’s still an ongoing process, so you can follow the development on github: firsara/createjs-bootstrap

As I side note I have to admit that some calculations have been inspired by Hammer.js, which is an awesome framework for handling multitouch-gestures on dom elements.

Throughout my research I also found a nice article about overlaying Hammer.js on EaselJS. I didn’t try it out but I’m pretty sure it can work out well. I just like to keep my code short and simple and not using too many frameworks on top of each other!

With all that being said: happy coding!