doc/articles/uno-development/interaction-tracker-internals.md
This document tries to detail and clarify the implementation of InteractionTracker.
The interaction tracker has four states:
Currently, custom animation is not yet implemented. The transitioning between states is well-explained in InteractionTracker Class | Microsoft Learn.
This document is going to focus on the Inertia state. The core calculations for this state are in AxisHelper nested class (in InteractionTrackerInertiaHandler.AxisHelper.cs file).
First, there are two things we need to early calculate:
[!NOTE] All mentions of decay rates below are
1 - PositionInertiaDecayRate, unlessPositionInertiaDecayRateis explicitly written.
This is about the "natural" distance, i.e, not taking into account MinPosition/MaxPosition constraints.
The core important equation of this is:
float val = MathF.Pow(DecayRate, time);
return ((val - 1.0f) * InitialVelocity) / MathF.Log(DecayRate);
This equation represents the position of an object undergoing exponential decay over time.
Normally, the exponential decay is represented by DecayRate ^ t. This equation, however, is more about a "rate of change" in position. So, to get the position, we integrate that.
The integration of DecayRate ^ t is (DecayRate ^ t) / ln(DecayRate).
However, we want the distance to be zero at time t = zero. Note that at time t = 0 the numerator is DecayRate ^ 0 which is 1. So, we subtract one.
The formula is now x(t) = ((DecayRate ^ t) - 1) / ln(DecayRate). One last important thing that affects the distance is the initial velocity.
For a given decay rate and a given time, the effect of initial velocity is linear. So, we multiply the initial velocity. That is the final formula.
Now, let's visualize this by looking at a graph (assuming initial velocity = 60):
You can see the position distance at time zero is always zero, and at the beginning it's exponentially growing until it settles down to its final value.
TimeToMinimumVelocity)The core important equation of this is:
return (MathF.Log(minimumVelocity) - MathF.Log(initialVelocity)) / MathF.Log(decayRate);
Note that as we have exponential decay, the final velocity is calculated as v_f = v_i * r^t, where:
v_f: final velocityv_i: initial velocityr: decay ratet: timeTo get the time:
v_f / v_i = r^tln(v_f / v_i) = ln(r^t)ln(v_f) - ln(v_i) = t ln(r)t = (ln(v_f) - ln(v_i)) / ln(r)Currently, we fix the final velocity v_f to 30 px/sec, and call that minimumVelocity.
Note that the expression above will produce negative value if initialVelocity < minimumVelocity. So, in GetTimeToMinimumVelocity, if initialVelocity <= minimumVelocity, we return zero. The following graph visualizes the expression:
The red curve corresponds to decay rate 0.7, and the green one corresponds to decay rate 0.9. The x-axis is the initial velocity, and the y-axis is TimeToMinimumVelocity.
Note that in both cases, the intersection with x-axis is the minimum velocity. The higher the decay rate, the more TimeToMinimumVelocity. Again, decay rate mentioned here is 1 - PositionInertiaDecayRate.
So, actually as PositionInertiaDecayRate gets larger, the time gets smaller.
Earlier, we concluded that x(t) = InitialVelocity * ((DecayRate ^ t) - 1) / ln(DecayRate). This works well if "overpanning" isn't taken into account.
The overpanning happens when, in interacting state, the user active input goes beyond the restrictions of MinPosition and MaxPosition.
Two cases where this can happen:
In this case, once we have a value outside of the range while in inertia, we want to bring it back. For that, we use a damping animation.
In WinUI, underdamped animation is used, and potentially critically-damped animation is also used for low velocities.
The current Uno implementation is more simple, using only critically-damped animation.
So, once we get a value outside of the range in inertia, we set _dampingStateTimeInSeconds to the current elapsed time, and set _dampingStatePosition to the current position. These values will be used in future GetPosition calls.
Basically, we want the animation to settle in the remaining time, i.e, TimeToMinimumVelocity - _dampingStateTimeInSeconds.Value.
The following graph shows the critical damping equation with time being the x-axis. As you see, it starts from value of 0 until it settles to 1, and the settling time is approximately 5.8335 / w.
The settling time above is the 2% criterion, defined as "the time required for the response curve to reach and stay within 2% of the final value". The position calculation is then done by:
value * (FinalModifiedValue - _dampingStatePosition!.Value) + _dampingStatePosition.Value
where value is the result of the critically damped equation.
Note that at the beginning when value is zero, we get _dampingStatePosition, then as time goes on and we reach 1, we get FinalModifiedValue, where FinalModifiedValue is the natural position clamped between MinPosition and MaxPosition.