Prime Flow Field Background Animation

maxbrt2 pts0 comments

Prime Flow Field Background | brt.fyi<br>I&rsquo;ve gotten some influx of people visiting my webpage recently, and there were many questions about my prime flow field background, so I thought I&rsquo;d go over it in more detail. There were also some mentions about the poor performance, so I optimized it while I was at it .<br>At it&rsquo;s foundation is a flow field, that&rsquo;s made by particles drifting through a vector field and leaving little fading trails. I&rsquo;ll build it up from scratch as I go along.<br>Flow Field#<br>A flow field is a rule that hands you an arrow at every point $(x, y)$:<br>$$<br>\mathbf{F}(x, y) = \big(u(x, y),\; v(x, y)\big)<br>$$Here $u$ is how fast to move horizontally and $v$ how fast to move vertically. At each frame the particle checks the arrow where it stands and takes a small step along it:<br>$$<br>x \leftarrow x + u\,\Delta t, \qquad y \leftarrow y + v\,\Delta t<br>$$Doing that per particle while also fading the old trails a little each frames draws the desired effect.<br>The field, as a function#<br>We sample each point over and over again:<br>field(nx, ny, t)

nx, ny — where we&rsquo;re asking, in the normalized coordinates from above;<br>t — elapsed time, which lets the field drift<br>const [u, v] = field(particle.x, particle.y, t);<br>particle.x += u * dt;<br>particle.y += v * dt;

We choose a random spot on the canvas, a random lifetime within certain limits, color, and a random starting age:<br>spawn(pt) {<br>pt.x = Math.random() * this.W;<br>pt.y = Math.random() * this.H;<br>pt.life = 60 + Math.random() * 180;<br>pt.age = Math.random() * pt.life;<br>pt.color = this.palette.colors[(Math.random() * this.palette.colors.length) | 0];

Additionally, the particles &ldquo;melt&rdquo; in and out, which is accentuated by the trail. The swirls emerge and depend on the landscape described by our function. We need to define a good one in order to make the swirls appearance feel natural.<br>One swirl#<br>Any such landscape can be described as a collection of hills of different heights, $\psi(\tilde{x}, \tilde{y})$: hills where $\psi$ is large, pits where it is small. The velocity we hand the particles is the perpendicular gradient at the current location in the landscape:<br>$$<br>\mathbf{F} = \nabla^{\perp}\psi = \left(\frac{\partial \psi}{\partial \tilde{y}},\;<br>-\frac{\partial \psi}{\partial \tilde{x}}\right)<br>$$The ordinary gradient $\nabla\psi$ points straight uphill; by rotating it a quarter<br>turn we make the particles run along the contour lines of $\psi$ instead of across<br>them. The contour might be extended by other hills that intersect, but ultimately they are closed loops so the flow just goes round and round resulting in the swirl effect. As a bonus, since particles ride contour lines, they never drain into a point, so the flow never collects into clumps.<br>The simplest possible landscape is described by a single Gaussian hill, of height $h$ and width<br>$\sigma$:<br>$$<br>\psi(\tilde{x}, \tilde{y}) = h \, e^{-(\tilde{x}^2 + \tilde{y}^2) / 2\sigma^2}<br>$$psi(nx, ny) {<br>// one Gaussian hill of height h and width sigma<br>return h * Math.exp(-(nx * nx + ny * ny) / (2 * sigma * sigma));

We work that derivative out once, by hand — the perpendicular gradient of the<br>Gaussian is a clean rotation around the peak:<br>$$<br>\mathbf{F} = \nabla^{\perp}\psi = \frac{\psi}{\sigma^2}\,(-\tilde{y},\; \tilde{x})<br>$$At runtime, we never differentiate anything, but evaluate the closed-form velocity directly.<br>I&rsquo;ve added a small demo in the top right corner displaying a miniature screen and the hill placed inside of it. The actual behavior can be adjusted by dragging peak height below zero and turning the hill into a pit, thereby reversing the swirl.<br>to turn the hill into a pit and watch the swirl reverse.

Visually it is best in three dimensions, tracking a single particle. The shadow on the floor traces the path you&rsquo;d see from straight above.

drag to rotateI&rsquo;ve added a second hill to show how the particle behaves in the full landscape. It never leaves it&rsquo;s elevation and would eventually loop, but it&rsquo;s path is determined by the different hills that intersect at the same elevation. The velocity changes depending on the slope, so technically it keeps speeding up and slowing down again, but this is not as apparent since the lifetime of a particle is typically not long enough to observe it.<br>A grid of swirls#<br>One bump makes one swirl. To fill the screen we need such a landscape full of hills and<br>pits and the simplest one that repeats is a product of two sine waves:<br>$$<br>\psi(\tilde{x}, \tilde{y}) = \sin(k\tilde{x})\,\cos(k\tilde{y})<br>$$psi(nx, ny) {<br>const k = 2.2;<br>// the 1/k keeps the flow speed steady as k changes<br>return Math.sin(k * nx) * Math.cos(k * ny) / k;

Call that &ldquo;egg-carton&rdquo;.

So we need to get from this orderly, tidy grid to a scattered one that never quite repeats.<br>Stacking waves#<br>In order to break that regularity we need add more waves and sum them up. We chose $p$ out of a set of primes.<br>$$<br>\psi(\tilde{x}, \tilde{y}) = \sum_{p}\;...

tilde field rsquo flow particle random

Related Articles