Saturday, July 11, 2009

Kinematics

How do you make the touch list behave like a real physical object? What's the secret behind making the list bounce smoothly back to its original position? How do you decelerate it when you quickly flick through it like if it had some sort of "friction"? In short: how do you make it work like an iPhone list? The answer is simple: kinematics, the study of motion.

In this post I'm adding some kinematic effects to the list to make it work like if it had both weight and some "springs" to attach it to the extremes of the window. These "springs" will make the list bounce back when the list is either showing its first or last element and you force it to scroll down or up (respectively). Also, when scrolling through very large lists, you want them to slow down in a realistic fashion, like a real object would.

There are a lot of changes to the base CTouchWindow class in order to include these kinematic additions. Interestingly, some parts of the code actually became simpler. This base class is used to implement the touch list that only performs vertical scrolling, but it must also accommodate for a possible horizontal scrolling. To make the kinematics implementation easier, I created a helper class named CTouchMotion that is used to calculate the list position as a function of time. You initialize each instance (vertical and horizontal) with the desired acceleration (in pixels per squared second), initial speed (in pixels per second), the initial and final positions of the motion (the final position is redundant, me thinks) and the total motion time. The x and y positions of the motion (just y for the touch list) are calculated by the GetPosition function that takes the time offset (since the beginning of the motion) as milliseconds. The function just computes the classical x = 0.5 * a * t * t + vi * t + xi equation, where x is the position for time t, a is the acceleration, vi is the initial speed and xi is the initial position. Notice that the function must transform the input in milliseconds to seconds in order to get the appropriate result. This is a concern because I must perform all calculations using integers to get the best performance possible. If you look at the implementation of the GetPosition function you will see that I calculate the 0.5 * a * t term first and convert it to a pixels per second speed. Next I add the initial speed, multiply the result by t and then convert the result to pixels by dividing by 1000. Finally, xi is added and we get the proper result. Also notice how each step is accurately rounded to make sure nothing is lost during conversions.

While I was debugging this code I found a very interesting bug. Originally, the function was using the time offset as a DWORD (an unsigned value). Occasionally the list would simply disappear while scrolling. The bug was due to the conversions performed between signed and unsigned integers and sometimes they behaved differently from what I expected, and would throw the list origin to very large negative numbers. The problem was solved by casting the time offset to a signed integer.

You can also see that the function uses a time limit that works as a "break" if you accidentally call it with a time offset larger than the one you specified. Why is this? Simple: the equation is a simple parabola that computes a trajectory until the speed (the first derivative) is zero. This is when the list stops. If you don't limit the motion, you would actually see the list bouncing back with a positive acceleration like when you throw a ball up, and it comes back down. You only want the upward movement - the one where the speed decreases until zero.

Another big change happened in the way the motion timer is calculated. The previous version used the speed of the last WM_MOUSEMOVE (displacement and time). This concept was discarded and I assumed that the movements would have a set of fixed behaviors:
  • Bounce-back movements would last 500 milliseconds.
  • List deceleration would take 2 seconds.
You can see these calculations in the CTouchWindow::SetupMotion function. This is called when the list is about to move (on the WM_LBUTTONUP handler) and during the timer (OnTimer) function in order to test for boundary conditions. There is something that's still amiss here: when you transition from a scroll to a bounce-back, the initial speed is not being considered so you see a bit of a harsh transition. I will correct this later.

Finally, and because you can now have an empty space drawn before the list (and also after, but this was already in the previous implementation), I added a fourth color to the list: the canvas color. This is what lies "beneath" the list and helps to convey the notion that the list exists above another surface (thanks for the suggestion, Anatoly).

The code is not fully cleaned up yet and you will see some more changes to this kinematic code. Meanwhile, here is the project for your perusal:

Sample code: CrypSafe03.zip (99 KB)

1 comment:

vincent said...

Really impressive work.

Thanks