An useful intuitive model might be to consider the encoder as two parts: the human-manipulated knob, and a virtual motorized rotating reference scale.
The knob in relation to the reference scale controls the velocity and direction.
The "virtual spring" is implemented as if the reference scale rotates towards the knob zero state. The only thing that matters, is how the reference scale "decays" to zero, how the reference scale rotates towards zero knob state, because that is exactly the way one can implement it in code also.
There are two intuitive decay models: exponential, and linear.
Exponential decay is typically specified as half time: the duration in which the velocity is halved. This is easy to implement when the update interval is fixed. The reference scale rotates quickly when the difference is large, but slowly when the difference is small.
Linear decay is specified as the deceleration rate: the rate of velocity drop per unit time. This is easier to use when the update interval varies. The reference scale rotates at a fixed speed, except it stays at zero when it reaches zero.
Let's look at the math. Within a time interval \$t\$, velocity changes from \$v_0\$ to \$v_1\$, and the state is updated by \$x\$.
For exponential decay, if \$T\$ is the decay half time, and \$T_e = T/\log 2 \approx 1.44295 T\$, these are
$$v_1 = v_0 e^{-\frac{t}{T_e}}, \quad x = v_0 T_e \left( 1 - e^{-\frac{t}{T_e}} \right)$$
When you use a regular interrupt, \$t\$ is a constant, and this simplifies to \$v_1 = C_1 v_0\$ and \$x = C_2 v_0\$, where
$$C_1 = e^{-\frac{t}{T_e}} = e^{-\frac{t \log 2}{T}}, \quad C_2 = T_e - T_e e^{-\frac{t}{T_e}} = \frac{T}{\log 2}\left(1 - e^{-\frac{t \log 2}{T}}\right)$$
and \$0 \lt C_1 \lt 1\$ and \$0 \lt C_2 \lt 1\$. With integer arithmetic, we can use v_1 = (v_0 * N1) / D1; and x = (v_0 * N2) / D2;, where D1 and D2 are powers of two (so that the division is optimized to a fast binary shift right), and 0 < N1 < D1 and 0 < N2 < D2. Just make sure the intermediate product does not overflow. You can always avoid that by casting to say 64-bit integers, v_1 = (v_0 * (int_fast64_t)N1) / D1; and x = (v_0 * (int_fast64_t)N2) / D2;.
For linear decay, \$a\$ is the rate of velocity drop per unit of time. We have
$$v_1 = \begin{cases}
v_0 - a t, & v_0 \ge a t \\
v_0 + a t, & -v_0 \ge a t \\
0, & \text{otherwise} \\
\end{cases}, \quad x = \begin{cases}
t v_0 - \frac{a}{2} t^2, & v_0 \ge a t \\
t v_0 + \frac{a}{2} t^2, & -v_0 \ge a t \\
\frac{v_0^2}{2 a}, & 0 \lt v_0 \lt a t \\
-\frac{v_0^2}{2 a}, & 0 \lt -v_0 \lt a t \\
0, & v_0 = 0 \\
\end{cases}$$
Using integer arithmetic, care must be taken to avoid integer overflow, but otherwise it is straightforward. The division by \$2 a\$ only occurs when the velocity decayed to zero during the last interval. This update is only needed at display frequency, so a few dozen times a second at most, so the division will not be too slow even on 8-bit microcontrollers.
Let's assume you use one (or two) pin state change interrupts to track the encoder knob. Whenever turned in positive direction, you increment some volatile integer-type variable by one; and decrement in the other direction. Following the above update, you disable interrupts, read and clear that variable, and enable interrupts, to obtain the amount of change during the last interval. Multiply the variable by some suitable constant, to get \$v_\Delta\$. Add \$v_\Delta\$ to \$v_1\$, but also update the state by \$0.5 v_\Delta t\$. This causes the encoder knob change to be taken into account as if it happened *during* the last interval. The state is updated as if it happened midway during the period, and velocity as if it all happened just before the update. (Not exactly physically correct, but makes sure even small encoder changes are reflected in the state change.)
This is exactly something I would recommend one experiments with, perhaps even with a simple simulator, before deciding on an implementation.
I would probably use an Arduino clone and an encoder, outputting the state via USB Serial, visualized using a simple Python application that represents the state as a turning wheel or moving dot, optionally displaying the speed or RPM.
I would use the dull-looking math above to calculate the real-world decay half time (in seconds) or deceleration rate (pulses per second per second), so that I would have human-scale constants to target in the code, instead of trying to port code from an Arduino to whatever else one might use, relying on things like interrupt frequency being the same, and so on. I do not particularly like math, but it is damn useful for deriving real-world/physical quantities that one can then implement in code, and get the same-feeling response, even if the implementation is completely different.