I\'m implementing the scrolling behaviour of a touch screen UI but I\'m too tired in the moment to wrap my mind around some supposedly trivial piece of math:
y (
[Short answer (assuming C
syntax)]
double v(double old_v, double dt) {
t = t_for(old_v);
new_t = t - dt;
return (new_t <= 0)?0:v_for(t);
}
double t_for(double v)
and double v_for(double t)
are return values from a v-to-t bidirectional mapping (function in mathematical sence), which is arbitrary with the constraint that it is monothonic and defined for v >=0
(and hence has a point where v=0
). An example is:
double v_for(double t) { return pow(t, k); }
double t_for(double v) { return pow(v, 1.0/k); }
where having:
k>1
gives deceleration decreasing by modulo as time passes.k<1
gives deceleration increasing by modulo as time passes.k=1
gives constant deceleration.[A longer one (with rationale and plots)]
So the goal essentialy is:
To find a function v(t+dt)=f(v(t),dt)
which takes current velocity value v
and time delta dt
and returns the velocity at the moment t+dt
(it does not require actually specifying t
since v(t)
is already known and supplied as a parameter and dt
is just time delta). In other words, the task is to implement a routine double next_v(curr_v, dt);
with specific properties (see below).
[Please Note] The function in question has a useful (and desired) property of returning the same result regardless of the "history" of previous velocity changes. That means, that, for example, if the series of consecutive velocities is [100, 50, 10, 0] (for the starting velocity v0=100
), any other sequence larger than this will have the same "tail": [150, 100, 50, 10, 0] (for the starting velocity v0=150
), etc. In other words, regardless of the starting velocity, all velocity-to-time plots will effectively be copies of each other just offset along the time axis each by its own value (see the graph below, note the plots' parts between the lines t=0.0
and t=2.0
are identical) .
Besides, acceleration w(t)=dv(t)/dt
must be a descending function of time t
(for the purpose of visually pleasing and "intuitive" behaviour of the moving GUI object which we model here).
The proposed idea is:
First you choose a monothonic velocity function with desired properties (in your case it is gradually decreasing acceleration, though, as the example below shows, it is easier to use "accelerated" ones). This function must not also have an upper boundary, so that you could use it for whatever large velocity values. Also, it must have a point where velocity is zero. Some examples are: v(t) = k*t
(not exactly your case, since deceleration k
is constant here), v=sqrt(-t)
(this one is ok, being defined on the interval t <= 0
).
Then, for any given velocity, you find the point with this velocity value on the above function's plot (there will be a point, since the function is not bound, and only one since it is monothonic), advance by time delta towards smaller velocity values, thus acquiring the next one. Iteration will gradually (and inevitably) bring you to the point where velocity is zero.
That's basically all, there is even no need to produce some "final" formula, dependencies on time value or initial (not current) velocities go away, and the programming becomes really simple.
For two simple cases this small python script produces the plots below (initial velocities given were 1.0
to 10.0
), and, as you can see, from any given velocity "level" and "downwards" the plots "behave" the same which is of couse because no matter at what velocity you start to slow down (decelerate), you are "moving" along the same curve RELATIVE to the point where velocity is (becomes) zero:
import numpy
import pylab
import math
class VelocityCurve(object):
"""
An interface for the velocity 'curve'.
Must represent a _monotonically_ _growing_
(i.e. with one-to-one correspondence
between argument and value) function
(think of a deceleration reverse-played)
Must be defined for all larger-than-zero 'v' and 't'
"""
def v(self, t):
raise NotImplementedError
def t(self, v):
raise NotImplementedError
class VelocityValues(object):
def __init__(self, v0, velocity_curve):
assert v0 >= 0
assert velocity_curve
self._v = v0
self._vc = velocity_curve
def next_v(self, dt):
t = self._vc.t(self._v)
new_t = t - dt
if new_t <= 0:
self._v = 0
else:
self._v = self._vc.v(new_t)
return self._v
class LinearVelocityCurve(VelocityCurve):
def __init__(self, k):
"""k is for 'v(t)=k*t'"""
super(LinearVelocityCurve, self).__init__()
self._k = k
def v(self, t):
assert t >= 0
return self._k*t
def t(self, v):
assert v >= 0
return v/self._k
class RootVelocityCurve(VelocityCurve):
def __init__(self, k):
"""k is for 'v(t)=t^(1/k)'"""
super(RootVelocityCurve, self).__init__()
self._k = k
def v(self, t):
assert t >= 0
return math.pow(t, 1.0/self._k)
def t(self, v):
assert v >= 0
return math.pow(v, self._k)
def plot_v_arr(v0, velocity_curve, dt):
vel = VelocityValues(v0, velocity_curve)
v_list = [v0]
while True:
v = vel.next_v(dt)
v_list.append(v)
if v <= 0:
break
v_arr = numpy.array(list(v_list))
t_arr = numpy.array(xrange(len(v_list)))*dt
pylab.plot(t_arr, v_arr)
dt = 0.1
for v0 in range(1, 11):
plot_v_arr(v0, LinearVelocityCurve(1), dt)
for v0 in range(1, 11):
plot_v_arr(v0, RootVelocityCurve(2), dt)
pylab.xlabel('Time ')
pylab.ylabel('Velocity')
pylab.grid(True)
pylab.show()
This gives the following plots (linear ones for the linear decelerating (i.e. constant deceleration), "curvy" -- for the case of the "square root" one (see the code above)):
Also please beware that to run the above script one needs pylab, numpy and friends installed (but only for the plotting part, the "core" classes depend on nothing and can be of course used on their own).
P.S. With this approach, one can really "construct" (for example, augmenting different functions for different t
intervals or even smoothing a hand-drawn (recorded) "ergonomic" curve) a "drag" he likes:)