Stable Distributions for Animations

In many time-controlled creative coding projects like animations, I need pseudo-random parameters to move around smoothly without straying too far from the origin. For a simple approach, I assign each value a position xtx_t velocity vtv_t. For a discrete time variable tt, the position updates as usual according to the Semi-implicit Euler method xt+1=xt+vt+1x_{t+1}=x_t+v_{t+1}, this brings numerical stability and is more straightforward to implemen. For the velocity, I choose

vt+1=γvtkxt+ϵt.v_{t+1}=\gamma v_t−kx_t+\epsilon_t.

where 0<γ<10<\gamma<1 is a friction term, 0<k0<k is a spring constant which keeps parameters close to 00, and ϵ\epsilon is noise from a Gaussian distribution with variance σ2\sigma^2.

Often we want to start the animation with a random constellation drawn from a distribution that remains stable over time, so the parameters on average shouldn’t come closer, spread further or change speed during the process.

To describe the spread of the system, we use the variances of xx and vv together with their covariance. Remember, this is what our system looks like:

xt+1=xt+vt+1=(1k)xt+γvt+ϵtvt+1=γvtkxt+ϵt\begin{align*} x_{t+1}&=x_t+v_{t+1}=(1-k)x_t+\gamma v_t+\epsilon_t\\ v_{t+1}&=\gamma v_t-k x_t+\epsilon_t\\ \end{align*}

By following the simple computation rule for covariance of linear combinations, we arrive at the following covariance matrix:

Var[xt+1]=(1k)2Var[xt]+γ2Var[vt]+γ(22k)Cov[xt,vt]+σ2Var[vt+1]=k2Var[xt]+γ2Var[vt]2γkCov[xt,vt]+σ2Cov[xt+1,vt+1]=(k2k)Var[xt]+γ2Var[vt]+γ(12k)Cov[xt,vt]+σ2\begin{align*} \var[x_{t+1}]&=(1-k)^2\var[x_t]+\gamma^2\var[v_t]+\gamma(2-2k)\cov[x_t,v_t]+\sigma^2\\ \var[v_{t+1}]&=k^2\var[x_t]+\gamma^2\var[v_t]-2\gamma k\cov[x_t,v_t]+\sigma^2\\ \cov[x_{t+1},v_{t+1}]&=(k^2-k)\var[x_t]+\gamma^2\var[v_t]+\gamma(1-2k)\cov[x_t,v_t]+\sigma^2 \end{align*}

Assuming that variance and covariance do not depend tt (stable distribution), we can ask WolframAlpha to solve this system for V:=Var[x]V:=\var[x], W:=Var[v]W:=\var[v] and C:=Cov[x,v]C:=\cov[x,v].

Writing down the solution and simplify a bit, we get a commond denominator D:=(1γ)(2(1+γ)k)D:=(1-\gamma)(2(1+\gamma)-k) and then:

V=(γ+1)σ2kD,W=2σ2D,C=σ2DV=\frac{(\gamma+1)\sigma^2}{kD},\quad W=\frac{2\sigma^2}{D},\quad C=\frac{\sigma^2}{D}

This means that if we start the system in a distribution satisfying this covariance matrix, the matrix won’t change during the run. However, a distribution is more than just its covariance matrix, so the distribution itself could still change.

However, we don’t need to worry when choosing a bivariate normal distribution, which you can think of (assuming all means are zero) as two variables constructed as linear combinations from two independent standard normal distributions. This family of distributions has the useful property of being closed under linear combinations and also fully specified by its covariance matrix. This means, if we start with a bivariate distribution satisfying our stable matrix, we get indeed a truly stable distribution.

To implement this distribution, it’s better to work with the standard deviations σx:=Var[x]\sigma_x:=\sqrt{\var[x]}, σv:=Var[v]\sigma_v:=\sqrt{\var[v]} and correlation ρxv:=Cov[x,v]/(σxσv)\rho_{xv}:=\cov[x,v]/(\sigma_x\sigma_v). The correlation always lives in [1,1][-1,1], and vice versa, every number in this interval is a valid correlation, as long as the standard deviations are positive, while the covariance matrix must be positive definite, which is more difficult to check.

We can express a bivariate distribution as linear combination of two independent standard normal distributions z0z_0 and z1z_1 as follows (this is not the unique decomposition, but a canonical form):

x=σxz0,v=σv(ρxvz0+1ρxv2z1)x=\sigma_x z_0,\quad v=\sigma_v(\rho_{xv} z_0+\sqrt{1-\rho_{xv}^2} z_1)

Before implementing this stable distribution, one more consideration is that in some environments, like the web browser’s requestAnimationFrame() method, time intervals between frames can vary. To handle this, let Δ\Delta denote the elapsed time between frame tt and t+1t+1. In this case, we may update position according to xt+1=xt+Δvt+1x_{t+1}=x_t+\Delta v_{t+1}, and velocity according to vt+1=γΔvtΔkxt+Δϵv_{t+1}=\gamma^\Delta v_t−\Delta kx_t+\sqrt{\Delta}\epsilon. This is a good heuristic choice, if we set two of the three parameters to zero, we can check that different subdivisions of the same time interval lead to the exact same outcome (in the case of ϵ\epsilon, same outcome in a stochastic sense).

Here is a simple implementation in JavaScript, click to reset the simulation: