Liquid Aurora Background

  • Skia
  • Reanimated

The cheapest thing in the catalog: one Fill, one shader, no layers and no state beyond a clock. Two octaves of value noise drifting past each other, mixed between two colours. Worth reading for what it leaves out.

Two octaves and a mix

The whole image is six lines. One noise field at three cells across the screen, a second at six and half the weight, and they drift at different speeds in different directions — that opposition is the only reason it reads as moving air rather than sliding fog. The sum is clamped, then used to mix between the two colours.

float t = u_time * 0.15;float n = noise(uv * 3.0 + float2(t, t * 0.7));n += 0.5 * noise(uv * 6.0 - float2(t * 0.5, t));n = clamp(n * u_intensity, 0.0, 1.0);half3 col = mix(u_c1, u_c2, n);

Intensity is a balance control

It multiplies the noise before the clamp, not the colour after the mix, so turning it up does not brighten the image. It pushes more of the field past 1 and holds it there, which widens the second colour and narrows the gradient between them. The name says brightness; the behaviour is how much of the sky each colour gets.

n = clamp(n * u_intensity, 0.0, 1.0);half3 col = mix(u_c1, u_c2, n);return half4(col, 1.0);

Why there is no sine in the hash

The usual fract(sin(dot(p, …))) hash failed on an Adreno device once the clock had carried the noise domain far from the origin: its cheap sine range reduction cannot resolve a large argument, so neighbouring lattice cells returned equal values. It showed first as drifting lines, then as soft rectangles. This hash multiplies instead, and the lattice is wrapped so the input stays small.

float hash(float2 p) {  p = fract(p * float2(123.34, 456.21));  p += dot(p, p + 45.32);  return fract(p.x * p.y);}float2 wrap256(float2 p) { return mod(p, 256.0); }

Tuning

  • #B86BFF
  • #2D9CFF
  • 1.1
<AuroraBackground params={{ c1: "#B86BFF", c2: "#2D9CFF", intensity: 1.1 }} fireSignal={0} />