Tracking the mouse with JavaScript
01 September 2008
Permalink:
http://douggreenall.co.uk/site/?p=154
Well it's been nearly two weeks since my last post so I figured I best get something up here - you may have noticed that I redesigned the blog today. I found a
rather swanky Photoshop tutorial so I've been playing around in Paint Shop Pro (I got used to PSP years ago and I just find Photoshop frustrating now) - I hope you like it anyway.
Right, instead of just putting up some arbitrary code and rambling on about it, today I'm going to post something that's actually useful. Getting a cross-browser mouse tracking mechanism going is something that I've found tricky in the past, but I've got a pretty solid implementation now that I'm going to explain here. I'll be using this for a load more stuff in the future too, so this is kind of the first part of what will become a reasonably comprehensive JavaScript library.
Okay, well the browser window is a
2D Cartesian plane so we'll start by implementing a simple class to store coordinates within that plane ? a vector:
function Vector2D(X, Y)
{
this._x = X;
this._y = Y;
}We can instantiate this class just like any other:
var vector = new Vector2D(0, 0);Of course on its own, this isn't especially useful - we really need to add some more functionality to make it worthwhile. Let's add some basic arithmetic functions:
function Vector2D(X, Y)
{
this._x = X;
this._y = Y;
// the following methods operate with a real number (1, 2, -4, 1.5, PI):
this.AddVal = function(Value) { this._x += Value; this._y += Value; return this; };
this.SubVal = function(Value) { this._x -= Value; this._y -= Value; return this; };
this.MulVal = function(Value) { this._x *= Value; this._y *= Value; return this; };
this.DivVal = function(Value)
{
if (Value) // any value that isn't 0, null or undefined is equivalent to boolean true
// remember - no division by 0 (although JavaScript recognises Infinity as a value,
// it isn't particularly useful here)
{
this._x /= Value;
this._y /= Value;
}
return this;
};
// the following methods operate with a Vector2D instance:
this.AddVec = function(Vector) { this._x += Vector._x; this._y += Vector._y; return this; };
this.SubVec = function(Vector) { this._x -= Vector._x; this._y -= Vector._y; return this; };
this.MulVec = function(Vector) { this._x *= Vector._x; this._y *= Vector._y; return this; };
this.DivVec = function(Vector)
{
if (Vector._x && Vector._y)
{
this._x /= Vector._x;
this._y /= Vector._y;
}
return this;
};
}Note how we always return the current instance at the end of each method - this allows us to string calls together like so:
var vector = new Vector2D(0, 0);
vector.AddVal(2).DivVec(new Vector2D(2, 3)).MulVal(5).SubVal(10);Finally we'll add a couple more methods to copy the object and return its string representation:
function Vector2D(X, Y)
{
.
.
.
this.toString = function() { return this._x + "|" + this._y; };
this.Clone = function() { return new Vector2D(this._x, this._y); };
}It's worth noting that the "toString" function is called implicitly whenever we output an instance of Vector2D as a string - this is true for any class we create with a "toString" function:
alert(new Vector2D(1, 2)); // outputs "1|2" in a browser alert boxNow this is by no means a comprehensive vector class - we can add a load more stuff for calculating products, lengths and angles etc. but for the purpose of tracking the mouse, this will do fine.
Next is the mouse tracking class itself - I'll start off with a complete listing and then explain what it does line by line:
function MouseTracker()
{
this._buttonDown = false;
this._position = new Vector2D(0, 0);
this._lastPosition = new Vector2D(0, 0);
this._offset = new Vector2D(0, 0);
this._timer = 0;
this._acceleration = new Vector2D(0, 0);
var This = this;
if (window.document.addEventListener)
{
window.document.addEventListener("mousedown", function(Event) { This._buttonDown = true; }, false);
window.document.addEventListener("mouseup", function(Event) { This._buttonDown = false; }, false);
window.document.addEventListener("mousemove", function(Event) { This.Move(Event); }, false);
}
else if (window.attachEvent)
{
window.document.attachEvent("onmousedown", function(Event) { This._buttonDown = true; });
window.document.attachEvent("onmouseup", function(Event) { This._buttonDown = false; });
window.document.attachEvent("onmousemove", function(Event)
{
if (window.event)
This.Move(window.event);
else
This.Move(Event);
});
}
///-methods-///////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
this.Move = function(Event)
{
++this._timer;
this._lastPosition = this._position.Clone();
this._position = new Vector2D(
Event.clientX + window.document.body.scrollLeft,
Event.clientY + window.document.body.scrollTop);
this._offset.AddVec(this._position.Clone().SubVec(this._lastPosition));
this._acceleration = this._offset.Clone().DivVal(this._timer);
};
this.Reset = function()
{
this._offset._x = 0;
this._offset._y = 0;
this._timer = 0;
this._acceleration._x = 0;
this._acceleration._y = 0;
};
}Let's start at the top - the following lines set up our internal variables for the mouse tracker:
this._buttonDown = false;
this._position = new Vector2D(0, 0);
this._lastPosition = new Vector2D(0, 0);
this._offset = new Vector2D(0, 0);
this._timer = 0;
this._acceleration = new Vector2D(0, 0);And the bottom - here is a reset function which restores some of these values to their initial states:
this.Reset = function()
{
this._offset._x = 0;
this._offset._y = 0;
this._timer = 0;
this._acceleration._x = 0;
this._acceleration._y = 0;
};As you can see, we have a boolean flag that records whether or not the mouse button is depressed (due to the browsers' context menus, we only really concern ourselves with left clicks) as well as a vector to record the current position of the mouse (as of the most recent mousemove event) and a vector called "_lastPosition" that records the previous position of the mouse. There is also a vector called "_offset" which is used to determine how far the mouse has travelled and a timer that records how many times the position has been updated since our last reset (we'll usually reset every time we query the tracker). When used in conjunction, these final two elements allow us to determine the acceleration of the mouse.
Now at this point it's worth establishing the difference between what's going on with the mouse handling and any other code that we may be running simultaneously - after all, it's all well and good knowing where the mouse is and where it's been, but it's pretty pointless if we don't do something with it. As an example, imagine an animation tied to the mouse - that animation will have a loop which will be executed periodically (say 1/50th of a second after the last time is was executed) but will also be running independently of any code which responds to the mouse event. Additionally, the code which we execute in our animation loop may itself take 1/50th of a second to complete, so in the cumulative time it takes to complete a single frame of animation, the mousemove event will have been triggered an arbitrary (n) number of times. Now we can't execute the animation loop within the mousemove handler itself for a number of reasons (we want a generic, global mouse handler for one and what's more, the animation would then become dependent on the movement of the mouse instead of reacting to it). Therefore, in order to accurately respond to the behaviour of the mouse, we need a tally of all the mouse movements since the last animation frame.
To achieve this, we set our vector "_lastPosition" to the previous position of the mouse, before querying the browser for the new position and finally calculating and adding the difference between these values to our offset vector each time the event handler is called:
this.Move = function(Event)
{
++this._timer;
this._lastPosition = this._position.Clone();
this._position = new Vector2D(
Event.clientX + window.document.body.scrollLeft,
Event.clientY + window.document.body.scrollTop);
this._offset.AddVec(this._position.Clone().SubVec(this._lastPosition));
this._acceleration = this._offset.Clone().DivVal(this._timer);
};In addition to this we also increment our timer and calculate our acceleration vector. After querying the mouse tracker during our animation loop, we'll reset these values to 0 and the whole process can start again.
This then brings us to the final piece of the puzzle - attaching the internal class method as a handler to the browser mousemove event. From what I've seen so far, most people deal with this either as a separately defined function or attach the method of a global class instance extenally to the class definition. I don't really like this approach - it's rather inelegant and besides, we're using JavaScript so there's always another way around the problem.
The answer lies in the way JavaScript handles scoping and data (remember, code is data). We can use a combination of inline function definitions and locally scoped, non-member variables to force the browser to reference any arbitrary instance of the class like so:
var This = this;
if (window.document.addEventListener)
{
window.document.addEventListener("mousedown", function(Event) { This._buttonDown = true; }, false);
window.document.addEventListener("mouseup", function(Event) { This._buttonDown = false; }, false);
window.document.addEventListener("mousemove", function(Event) { This.Move(Event); }, false);
}
else if (window.attachEvent)
{
window.document.attachEvent("onmousedown", function(Event) { This._buttonDown = true; });
window.document.attachEvent("onmouseup", function(Event) { This._buttonDown = false; });
window.document.attachEvent("onmousemove", function(Event)
{
if (window.event)
This.Move(window.event);
else
This.Move(Event);
});
}Most of this code is simply to attach inline functions to different mouse events, the top part for Firefox et al. and the bottom part for IE. Notice the line
var This = this;however? Well this is where the magic happens. If we were to setup the event handler
window.document.addEventListener("mousemove", function(Event) { This.Move(Event); }, false);and simply use the "this" keyword, the browser when calling the inline function would reference "this" as itself; that is, it would refer to "window.document". Now obviously, "window.document" has no knowledge of our "Move" function or any of our internal variables so instead it throws an exception. However, when we create a reference to our object using
var This = this;even though it's locally scoped, it will not only persist while the object instance exists but it's also within scope at the point at which we set up the handler and will instead provide a valid reference to our object instance. What's more, because it's locally scoped it won't interfere with any other locally scoped "This" variables in other classes or instances - now while this isn't particularly relevant for a mouse handler (because we'll realistically only ever need a single instance), for an AJAX wrapper around an XMLHttpRequest object, this becomes incredibly useful - we can wrap the XMLHttpRequest with a whole load of encapsulated functionality and have the request, the data and the response handler all contained within a single class (I'll post that one very soon). No matter how many simultaneous instances we create, they'll all reference themselves using exactly the same variable name ("This") and they'll all follow their own code paths independently, never interfering with one another.
Anyway, this post is becoming something of a behemoth so I'm going to finish off with a simple demonstration of the working code (you can also view the carousel I did in my
last post for a demonstration - how fast the carousel spins is determined by how quickly you throw it with the mouse, although it does need refining). Hold down the mouse button in the layer below and it will update showing the various values of the mouse tracker.
Download MouseTracker.js