blog@tomauger.com


ActionScript 2.0 and Custom Cursors: using a Second-order Predictor (Acceleration) to Detect When Mouse Leaves the Stage

Posted in Tips and Tricks, Flash / ActionScript by admin on the July 2nd, 2008

Can you believe it? You go through the pain of creating a custom cursor in Flash, and then when the user does something pesky like roll the mouse outside the Flash area, your custom cursor (which is really nothing more than a MovieClip that follows your cursor in disguise) stays in its last-known position - wedged somewhere near one of the outer boundaries of your stage, but definitely not looking right, since the “real” mouse is now happily tracking (or whatever it is real mice do) somewhere else on the screen. Two cursors, one big egg in your beer.

After doing some research, it seems the most advanced thinking on the subject (other than those crackpots who tell you to look to external JavaScript to solve the problem - which is bogus) is to use a first-order predictor (velocity) to “guess” (well, “predict” to be precise) where the mouse would be during the next poll. Well, I tried to implement that and I found that the results left somewhat to be desired. Considerably somewhat.

The way this “first-order prediction” business works is: once the mouse leaves the stage, _root.xmouse and _root.ymouse no longer give us meaningful information, because Flash is unable to track the mouse position outside of its own “domain”. So if you’re relying on, say a test to see whether _root.xmouse > 0 to detect whether your cursor has exited stage left, you’re SOL - _root.xmouse will still be reading 4, or 10 or wherever your mouse was LAST time it checked. So what we want to do is to predict where the mouse will be next time, based on where it was last time. In other words: velocity. That’s your “first-order predictor”. And boy does it look good. At first.
As you may or may not remember from your grade X Physics class (I actually didn’t remember - I was too busy oogling my teachers emphatic retort to Newton’s law of gravity - yeah, you Centennial CVI boys know what I’m talking about) velocity can be measured as the change in distance (”delta”) between two points over time. Now since we’re not really dealing with time per se as much as mouse movement over a frame, the equation is simplified quite a bit. As in vx = x2 - x1. So now, if you want to see whether the mouse will be off to the left of the stage, you can just add the velocity into your test to see whether _root.xmouse + vx > 0. Following me? Of course you are.

But, just so I can be clear for myself: if the first time we checked the xmouse was at, say 5 (near the left of the stage) and the second time (now) it’s at 2, x2 - x1 = 2 - 5 = -3. So now we check to see where it will be next time: _xmouse + vs = 2 + (-3) = 2 - 3 = -1 < 0. Great. So the mouse is off the stage. Roll drums, toot horns.

The problem occurs when you “flick” the mouse rapidly off the stage. What’s happening is that now our velocity is changing rapidly, which means that our prediction underestimates where the next position will be, and usually undershoots the mark. If you think about it, you’re not moving the mouse, then all of a sudden you flick it to the left. But, since the mouse is a physical object, it’s subject to the laws of physics, as are you, and so the mouse doesn’t really go from 0 - 60 in an instant. Like a car, it accelerates. Which means that the velocity is increasing over time - so our wonderful first-order prediction is hokey because it’s basing its calculation on a constant velocity.

So I took it one step further and used acceleration in the calculation, rather than velocity. Or rather, in the second poll I still use velocity (because you need three coordinates to calculate acceleration, and at the second poll we only have two points), but in the third poll I discard the velocity predictor for my second-order predictor and get much more accurate results.

Enough mumb-jumbo. Give us the code already, right?

Okay. Let’s see if I can format this biotch (WP is pissing me off for quoting code, but it’s probably just my own ignorance. Or a bad stylesheet.)

mc_cursor.onMouseMove = function() {
    var screenMargin:Number = 5;
	// 5 pixel safe area - will reduce a large number of errors
    var hideCursor:Boolean = true;
    if (showCustomCursor){
        hideCursor = false;
        // This will never be 100% perfect.
	// Look to AS3's stage.addEventListener(Event.MOUSE_LEAVE, mouseLeaveHandler);
        var curMouseX:Number = this._parent._xmouse;
        var curMouseY:Number = this._parent._ymouse;

        if (lastMouseX > -1){
            var deltaX:Number = curMouseX - lastMouseX;
            var deltaY:Number = curMouseY - lastMouseY;

            var predX:Number = screenMargin;
            var predY:Number = screenMargin;

            // second-order predictor: acceleration
            if (lastMouseDX || lastMOuseDY){

                // do them individually to avoid divide-by-zero
                if (lastMouseDX){
                    var accelX:Number = deltaX / lastMouseDX;
                    predX = curMouseX + (deltaX * accelX);
                }
                if (lastMouseDY){
                    var accelY:Number = deltaY / lastMouseDY;
                    predY = curMouseY + (deltaY * accelY);
                }

            // first-order predictor: velocity
            } else {
                predX = curMouseX + deltaX;
                predY = curMouseY + deltaY;
            }

            if (predX > Stage.width - screenMargin || predX < screenMargin
		 || predY > Stage.height - screenMargin || predY < screenMargin){
                hideCursor = true;
            }

            lastMouseDX = deltaX;
            lastMouseDY = deltaY;
        }

        lastMouseX = curMouseX;
        lastMouseY = curMouseY;
    }


    if (hideCursor) {
        this._visible = false;
        Mouse.show();
    } else {
        Mouse.hide();
        this._visible = true;
        this._x = this._parent._xmouse;
        this._y = this._parent._ymouse;
        updateAfterEvent();
    }
}

Not sure if that’s self-explanatory or not. I should provide some context, and then you can do the figuring out. The idea behind this script is that I don’t want a cursor on ALL the time - which is why we’re checking a global variable (showCustomCursor) before we go anywhere. If showCustomCursor has been set to true (presumably by some onRollOver event) then we’re in business. Monkey business.

I could dissect this code, but dinner’s cooking and I still have to bike home so I’ll leave you to post a comment if you have any questions.

Leave a Reply

You must be logged in to post a comment.