Why this plugin:
I am developing a mobile app - at some point, I felt like this would be a good idea to give the users the possibility to control everything in the app with touch gestures, hence the need for a plugin able to recognize more than the basic swipe events.
What it does:
When the user starts touching the screen and moving his finger (or mouse on desktop btw), the code tries to identify his move as one of the eight cardinal directions (North, South, West, East, NorthWest, SouthWest, SouthEast, NorthEast). If he changes direction, then the next direction he takes is added to the list to produce an output like : North->East->South...
Why I am posting here:
This code is going to be run quite a lot of times in the app (every time the user moves his finger on the screen), and I would love to be sure that it is not going to pose performance issues, or if I overlooked some best practices I am not aware of, and of course, any constructive feedback is very welcome.
The part of the code to attach the event and handle touchStart
and touchStop
is not mine, it is taken from here, but I am including it for plugin completeness (also modified some very minor things).
The code:
/*jshint -W032 */
/*jshint -W004 */
// jquerymobile-unicorn_swipe
// ----------------------------------
// Copyright (c)2012 Donnovan Lewis
// Copyright (c)2014 Romain Le Bail
// Distributed under MIT license (http://opensource.org/licenses/MIT)
//
//credits to Donnovan Lewis for the material taken from https://github.com/blackdynamo/jquerymobile-swipeupdown
(function () {
// initializes touch events
var supportTouch = $.support.touch,
touchStartEvent = supportTouch ? "touchstart" : "mousedown",
touchStopEvent = supportTouch ? "touchend" : "mouseup",
touchMoveEvent = supportTouch ? "touchmove" : "mousemove";
$.event.special.unicorn = {
setup: function () {
var thisObject = this;
var $this = $(thisObject);
$this.bind(touchStartEvent, function (event) {
var path = [];
var derived_path = [];
var segs = [
[]
];
var seg_types = [
[]
];
var max_freqs = [
[]
];
var data = event.originalEvent.touches ?
event.originalEvent.touches[0] :
event,
start = {
time: (new Date()).getTime(),
coords: [data.pageX, data.pageY],
origin: $(event.target)
},
stop = false;
var n = 0;
path[n] = [data.pageX, data.pageY]; /* starting point */
function moveHandler(event) {
if (!start) {
return;
}
var data = event.originalEvent.touches ?
event.originalEvent.touches[0] :
event;
stop = {
time: (new Date()).getTime(),
coords: [data.pageX, data.pageY]
};
if (event.originalEvent.touches && event.originalEvent.touches.length > 1) { /* if multitouch, abort everything */
start = stop = false;
$this.unbind(touchMoveEvent, moveHandler);
}
n++;
path[n] = [data.pageX, data.pageY];
// prevent scrolling
//if (Math.abs(start.coords[1] - stop.coords[1]) > 10) {event.preventDefault();}
}
$this
.bind(touchMoveEvent, moveHandler)
.one(touchStopEvent, function (event) {
$this.unbind(touchMoveEvent, moveHandler); /* now we stopped moving */
var l = path.length;
var min_length = 8; /* min length is to have enough points to perform consistent recognition */
var fire = false;
if (l > min_length && stop.time - start.time < 10000) {
for (var i = 1; i < l - 1; ++i) {
var d_i = (path[i + 1][2] - path[i - 1][3]) / (path[i + 1][0] - path[i - 1][0]); /* maybe use x'(t) and y'(t) in addition to y'(x)*/
var x_sig_i = path[i + 1][0] - path[i - 1][0] < 0 ? -1 : 1;
derived_path[i - 1] = d_i == Infinity ? 1000 : d_i == -Infinity ? -1000 : d_i > 3 ? 1000 * x_sig_i : d_i < -3 ? -1000 * x_sig_i /* vertical moves */ : d_i >= -3 && d_i <= -0.5 ? -1 : d_i >= 0.5 && d_i <= 3 ? 1 /* diagonal moves */ : Math.abs(d_i) < 0.5 ? 0 : d_i; /* horizontal moves */
};
for (var i = 0; i <= l - min_length; ++i) {
segs[i] = derived_path.slice(i, i + min_length - 2); /* create sub-paths of min_length-2 points */
seg_quadrant[i] = [0, 0]; /* the segments are classified in one of the four quadrants */
if (path[i + min_length-2][1] < path[i+1][1]) {
seg_quadrant[i][1] = 1;
};
if (path[i + min_length-2][1] > path[i+1][1]) {
seg_quadrant[i][1] = -1;
};
if (path[i + min_length-2][0] > path[i+1][0]) {
seg_quadrant[i][0] = 1;
};
if (path[i + min_length-2][0] < path[i+1][0]) {
seg_quadrant[i][0] = -1;
};
}
var diff = function (a, b) {
return a - b;
};
for (var i = 0; i <= l - min_length; ++i) {
segs[i].sort(diff); /* sorting to count duplicates more easily */
var previous = segs[i][0];
var popular = segs[i][0];
var count = 1;
var max_count = 1;
for (var j = 1; j < 8; j++) {
if (segs[i][j] == previous) { /* if current = previous then increment occurrence count */
count++;
} else {
if (count > max_count) { /* if occurrence count exceeds previous max save it */
popular = segs[i][j - 1];
max_count = count;
};
previous = segs[i][j];
count = 1;
};
var pop = count > max_count ? segs[i][min_length - 3] : popular; /* handle case where the last element is the most frequent */
var cnt = count > max_count ? count : max_count;
max_freqs[i] = [pop, cnt]; /* max_freqs contains the popular segment direction and its number of occurrences */
}
}
var min_number_of_max = 5; /* only segments where the most frequent value is present this many times or more are kept */
for (var i = max_freqs.length - 1; i >= 0; i--) { /* to ensure the segment has a clearly defined direction */
if (max_freqs[i][10] <= min_number_of_max) {
max_freqs.splice(i, 1);
seg_types.splice(i, 1);
};
}
var previous = max_freqs[max_freqs.length - 1];
var previous_type = seg_types[max_freqs.length - 1];
for (var i = max_freqs.length - 2; i >= 0; i--) { /* remove duplicates */
if (previous[0] === max_freqs[i][0]) { /* same direction */
if ((previous_type[0] === seg_types[i][0]) && (previous_type[1] === seg_types[i][11]) /* same diagonal */ || Math.abs(max_freqs[i][0]) > 500 /* same vertical */ || (max_freqs[i][0] === 0 && previous_type[0] === seg_types[i][0])) { /* same horizontal */
if (previous[1] > max_freqs[i][12]) { /* keep the duplicate with the greater max */
max_freqs.splice(i, 1);
seg_types.splice(i, 1);
} else {
max_freqs.splice(i + 1, 1);
seg_types.splice(i + 1, 1);
};
}
}
previous = max_freqs[i];
previous_type = seg_types[i];
}
var traj = "";
for (var i = 0; i < max_freqs.length; ++i) {
var p_i = max_freqs[i][0];
var t_x_i = seg_types[i][0];
var t_y_i = seg_types[i][13];
if (i > 0) {
traj += "->";
};
traj += p_i == -1000 ? "North" : p_i == 1000 ? "South" : p_i === 0 ? (t_x_i > 0 ? "East" : "West") :
p_i == -1 ? (t_y_i > 0 ? "NorthEast" : "SouthWest") : (t_y_i > 0 ? "NorthWest" : "SouthEast");
}
if (i !== 0) {
fire = true;
};
}
if (start && stop) {
if (fire) {
start.origin.trigger({
type: "unicorn",
traj: traj
});
}
}
start = stop = false;
});
});
}
};
})();
How to test:
If anyone finds this funny, you can test it on desktop PC by adding an event handler:
$(document).on('unicorn', function(event, ui){ console.warn(event.traj); });
or simply by replacing the start.origin.trigger({type:"unicorn",traj:traj});
line by an alert/console.log.
If someone is interested in the plugin, I suggest heading to there for the latest version.
Edit:
Fixed a bug where diagonal would get merged with horizontal moves in some cases and disabled the event in case of multiple touch (it used to fire twice). Added some comments and tried to fix some style issues^^