Oct 06 2008

5

Mouse Ghost

A friend of mine was really impressed with a flash demo that creates a cursor that follows the mouse. It works by recording the mouse coordinates, after the mouse stops moving it plays back the mouse movement.

I took a few minutes to create the same effect using javascript. While my version doesn’t have an image cursor it would be trivial to add one. I also set options to specify the delay, offsets, size and color. Since the script is fairly simple I thought I would take sometime explaining how I wrote it using classes in Mootools and why.

When I develop in Mootools, the majority of the time I break my problems into classes. Many people have issues with the concept of classes in javascript because classes technically don’t exist. This has to do with the fact that Javascript doesn’t follow traditional languages in its object inheritance, it uses prototype inheritance. Classes in Mootools is a object factory that makes dealing with the prototype chain much easier. In addition, Mootools provides some very nice functionality to make classes even more powerful. Enough with the technical jargon and on to the tutorial.

The first thing I do is create a skeleton.

var MouseGhost = new Class({
 
		Implements : [Options],
 
		options : {
 
		},
 
      		initialize : function(options){
			this.setOptions(options);
		}
});

Class is provided within the Mootools framework. It accepts an object that it uses to extend the prototype chain. Mootools has a few special features, like Mutators. They let you effect the Class your creating to make code reuse easier. Here I am using a standard mutator, Implements. My class implements the Options Class. By doing so, it will now have a new method called ’setOptions’. This makes it easy to configure my objects when I initialize them with the ‘new’ keyword.

Using my example of tracking the mouse movement I need to identify what my variables are.

var MouseGhost = new Class({
 
		Implements : [Options],
 
		points : [],
		tracepoints : [],
 
		options : {
			delay : 200,
			offset : { x : -20, y : 20 },
			color : '#666',
			size : 20,
			zindex : 20
		},
 
      		initialize : function(options){
			this.setOptions(options);
		}
});

Now my class has a default state defined in my options. The delay option is used to specify how long I want to wait for the mouse to stop moving before I playback the movement. The offset option allows us to specify an offset in x and y relative pixel coordinates from the actual mouse. The color option sets the color of our trace object, as size sets the width and height. I also added a zindex option to specify the css z-index attribute, this can be changed to make sure that the trace object appears over all the content on the page.

You will also notice that I added two additional properties to my class, ‘points’ and ‘tracepoints’. For now these are both empty arrays. I did not put them in options because they are not user changeable. The object will use these two variables as place holders. More on that later.

Next we want to define our ‘initialize’ function. Mootools, gives special treatment to methods named ‘initialize’. It will be the function within your object that Mootools will use to initialize your object when you use the keyword ‘new’. We only want to put the bare essentials into ‘initialize’ to make sure our object is created and will function properly. Generally, any DOM creation or manipulation is put into ‘initialize’ along with any event listeners.

		initialize : function(options){
			this.setOptions(options);
			this.cursor = new Element('div',{
				'styles' : {
					'position' : 'absolute',
					'top' : -1000,
					'left' : -1000,
					'height' : this.options.size,
					'width' : this.options.size,
					'background-color' : this.options.color,
					'z-index' : this.options.zindex
				}
			}).inject(document.body);
 
			window.addEvent('mousemove',this.listener.bindWithEvent(this));
		}

Inside our ‘initialize’ method we create a new Element, setting all the styles based on our options and inject it into document.body. We set top and left to negative numbers because we don’t know where the mouse is at the moment and we want it to be hidden. Setting these values to negative has the same effect as moving it off the page. Once the element is created we assign it to ‘this.cursor’ so we can reference it later.

The final thing we do within ‘initialize’ is add an event to the window. This event gets fired every time the mouse is moved. The function we call when the mouse moves is this.listener.bindWithEvent(this). It is important to understand that ‘this’ syntax refers to the current object we are within. The ‘listener’ method within our current object hasn’t been defined yet, we’ll do that in a second. We bind ‘this.listener’ with an event object using bindWithEvent. This method is available to all functions within Mootools. What makes this method so nice is Mootools hides all the browser differences in dealing with event objects. Mootools brings me back a standardize Event object that is accessible in the same way across all browsers. The ‘bind’ part means that when the function is called (fired) it sets the scope. Scope means what object (context) the code is running within. The important part to remember about bind is it sets the keyword ‘this’. In Mootools it is common practice to use the ‘this’ keyword within your classes. This give you the ability to have your code be able to refer to other parts of the object. By doing this it keeps the namespace within javascript clean. Here we are passing ‘bindWithEvent’ the object ‘this’. Meaning that when the ‘listener’ method is fired it is aware of its context within the current object (class).

Defining the listener method

		listener : function(event){
			$clear(this.timeout);
			this.points.push($merge(event.page,{t : new Date().getTime()}));
			this.timeout = this.traceback.delay(this.options.delay,this);
		}

It is important to notice that the ‘listener’ method takes one argument. When we used ‘bindWithEvent’ this argument is automatically set for us with the Mootools Event object. The first line introduces another Mootools function, $clear. $clear, resets (clears) any timeout or periodical that we have passed to it, in our case ‘this.timeout’.

We only need to know two things from our Event object, the x and y coordinates of the mouse. These are stored in event.page.x and event.page.y. In order to have our trace object play back in the proper timeline we need to know the current time. The second line pushes (adds) an object into the ‘points’ array. This is done by using $merge. $merge allows us to pass in two objects, the properties are combined to create a brand new object. The new object that gets created has three properties (x,y,t). The ‘x’ and ‘y’ properties are the mouse coordinates from the event object. The ‘t’ property is the current time.

A delay is then set to call the method ‘traceback’. The delay method is similar to the ‘bindWithEvent’ we used earlier. Here, we are saying run ‘traceback’ after ‘this.options.delay’ has elapsed. The second parameter set a bind. Again we set it to ‘this’ to keep our scope within our object. The key point with ‘delay’ is it returns a timer id. We assign this to ‘this.timeout’. Returning back to line one, we now see that this timeout will be clear if we move the mouse again. If we continue to move the mouse within the elapsed time defined for our delay, ‘traceback’ will not be executed. As soon as there is a delay long enough in the mouse movement, ‘traceback’ will be called.

Defining traceback:

		traceback : function(){
			this.tracepoints = $A(this.points);
			this.points = [];
			this.animate();
		}

Remember ‘points’ and ‘tracepoints’ that were defined earlier, we are now ready to use them. First we make a copy of ‘points’ by using $A, and storing it in ‘tracepoints’. We make a copy because we are going to modify ‘points’ and if we just set this.tracepoint = this.points, we would create a reference. Meaning, when ‘points’ is changed, these changes would show up in ‘tracepoints’, not what we want. After we set ‘tracepoints’ we empty ‘points’. We are doing all this array juggling so our object doesn’t loose its place. If the code has entered into ‘traceback’ we know that 1. We have mouse movements stored inside of ‘points’. 2. The mouse has stopped moving longer then this.options.delay. Now that we have values in ‘tracepoints’ we can begin the animation process, by calling ‘animate’.

		animate : function(){
			var len = this.tracepoints.length;
			if(len){
				var p = this.tracepoints.shift();
				this.cursor.setStyles({
					'top' : p.y + this.options.offset.y,
					'left' : p.x + this.options.offset.x
				});
				if(len > 1){
					var d = this.tracepoints[0].t - p.t;
					this.animate.delay(d,this);
				}
			}
		}

The first thing we do inside the ‘animate’ method is get the length of ‘tracepoints’. If there is a length we ’shift’ the first point out of the array and store it in ‘p’ (point). We use the the ‘x’ and ‘y’ points within this object and add our option offsets and set the styles on ‘this.cursor’. If the length is greater then 1 we calculate the time difference to the next point and store it in ‘d’ (duration). If the length is greater then 1 we call ‘animate’ again, but this time with ‘delay’. We set the ‘delay’ on animate to the same time it took the user to move between those two points. Again, we set our bind to ‘this’ inside the ‘delay’ function so we make sure we remain in the same scope.

That’s it! The full listing.

var MouseGhost = new Class({
 
		Implements : [Options],
		points : [],
		tracepoints : [],
		options : {
			delay : 200,
			offset : { x : -20, y : 20 },
			color : '#666',
			size : 20,
			zindex : 20
		},
 
		initialize : function(options){
			this.setOptions(options);
			this.cursor = new Element('div',{
				'styles' : {
					'position' : 'absolute',
					'top' : -1000,
					'left' : -1000,
					'height' : this.options.size,
					'width' : this.options.size,
					'background-color' : this.options.color,
					'z-index' : this.options.zindex
				}
			}).injectInside(document.body);
 
			window.addEvent('mousemove',this.listener.bindWithEvent(this));
		},
 
		listener : function(event){
			$clear(this.timeout);
			this.points.push($merge(event.page,{t : new Date().getTime()}));
			this.timeout = this.traceback.delay(this.options.delay,this);
		},
 
		traceback : function(){
			this.tracepoints = $A(this.points);
			this.points = [];
			this.animate();
		},
 
		animate : function(){
			var l = this.tracepoints.length;
			if(l){
				var p = this.tracepoints.shift();
				this.cursor.setStyles({
					'top' : p.y + this.options.offset.y,
					'left' : p.x + this.options.offset.x
				});
				if(l > 1){
					var d = this.tracepoints[0].t - p.t;
					this.animate.delay(d,this);
				}
			}
		}
});

Now we are ready to use our new Class.

 new MouseGhost();

If we want to change our default settings all we need to do is pass an ‘options’ object.

 new MouseGhost({delay : 300, color: '#FF3300', 'offset' : {x: 30, y : -20 }, 'size' : 10});

The great thing about using classes in mootools is its easy to create multiple objects. Click on “attach” below to run this example.

new MouseGhost({delay : 400, color: '#33FF00'});
new MouseGhost({delay : 300, color: '#FF3300', 'offset' : {x: 30, y : -20 }, 'size' : 10});
new MouseGhost({delay: 200, color: '#3300FF', 'offset' : {x : 10, y : 0}, 'size' : 35});

Tags: , ,

5 Responses to “Mouse Ghost”

  1. Mouse Ghost Decay feature | nwhite.net says:

    [...] posting Mouse Ghost I had the idea to implement a decay feature. The way this class works is a bit different from the [...]

  2. Brian says:

    Hey Mr White,

    I remember you from the mootools forums, u really helped me out over there when I was trying to get my head around js. Really nice work man, bloody brillaint :)

  3. simonsito says:

    well,now the interestig will recorder this acctions in a array with server script, and after that can reproduce the movemement of this array with other script. any ideas please publish them…;)

  4. nwhite says:

    It is in interesting idea. There are several things to consider. If you are wanted to track the mouse in relation to the page for playback and you use a centered layout you will need to take the browser window size into consideration. In addition, if you are not careful with your css elements could be in different places on different browsers. The other important aspect is if you have any dynamic content it would be very difficult to track the mouse in relation unless you setup an api for saving application state as well. There are a few projects that address what your asking for I believe there is a ruby page heatmap script, but this only records the clicks. It may give you some ideas on how to setup your own web bug.

  5. Lucy Stone says:

    just awesome!

Leave a Reply