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:
// 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 ]
};
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][1]-path[i-1][1]) / (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>=-2 && d_i<=-0.5 ? -1 : d_i>=0.5 && d_i<=2 ? 1 /* diagonal moves */
: Math.abs(d_i)<=0.5 ? 0 : d_i; /* horizontal moves */
};
for(var i=0;i<=l-min_length;++i) /* create sub paths of min_length-2 points */
{
segs[i]=derived_path.slice(i,i+min_length-2);
seg_types[i]=[0,0];
if(path[i+7][1]<path[i][1]){seg_types[i][1]=1;}; /* four quadrants */
if(path[i+7][1]>path[i][1]){seg_types[i][1]=-1;};
if(path[i+7][0]>path[i][0]){seg_types[i][0]=1;};
if(path[i+7][0]<path[i][0]){seg_types[i][0]=-1;};
}
for(var i=0;i<=l-min_length;++i)
{
segs[i].sort(function(a,b){return a - b;}); /* sorting to count duplicates */
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)
{count++;}
else
{
if (count > max_count)
{
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;
var cnt = count > max_count ? count : max_count;
max_freqs[i]=[pop,cnt];
}
}
for(var i=max_freqs.length-1; i>=0; i--)
{
if(max_freqs[i][1]<=4){max_freqs.splice(i,1);seg_types.splice(i,1);}; /* eliminate sub paths without clear direction */
}
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--)
{
if(previous[0]==max_freqs[i][0] && (previous_type[0]==seg_types[i][0] && previous_type[1]==seg_types[i][1] /* merge consecutive sub paths */
|| Math.abs(max_freqs[i][0])>500)
|| (max_freqs[i][0]==0 && previous_type[0]==seg_types[i][0]))
{
if(previous[1]>max_freqs[i][1]){max_freqs.splice(i,1);seg_types.splice(i,1);}
else{max_freqs.splice(i+1,1);seg_types.splice(i,1);};
}
previous = max_freqs[i];
previous_type = seg_types[i];
}
var typezor="";
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][1];
if(i>0){typezor += "->";};
typezor += 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",typezor:typezor});
}
}
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.typezor); });
or simply by replacing the start.origin.trigger({type:"unicorn",typezor:typezor});
line by an alert/console.log.