The big picture

An aurora is the hardest kind of thing to fake, because it has no edges. Real curtains of light are diffuse everywhere: no outline, no core, no point you could point at. This component takes that as an engineering constraint rather than a mood. The colour comes from a handful of soft circles tiled through space; the shape comes from warping the coordinate before the lookup; and every way the cursor touches the field is deliberately not local.

Aurora drift, the live component

Live, try it
The component itself, live. Sweep the cursor (Swell mode by default) and watch the aurora breathe harder near you without ever showing you a circle. Click for the one local event.

Three decisions carry the whole piece. The visual is one blob stack, tiled by mod(). The shape is two fBm samples displacing the lookup. And the interactivity obeys a rule most cursor effects ignore: if the visual is soft, the inputs have to be soft too.

One blob, tiled forever

Strip the motion away and the colour is almost embarrassingly simple: two to six circles, stacked vertically, each blended over the background by its Euclidean distance, softened by a smoothstep. On its own that renders one ribbon in the middle of the screen. The mod() is what makes it an atmosphere: fold every pixel of an infinite plane into one canonical cell, and the cell, blobs and all, repeats forever.

The tile and the blob stack, abridged
// One blob stack, tiled across infinite space. mod() folds every// pixel into a canonical cell centred on zero; the blob positions// hang below it, spaced by blobSpacing.vec2 op = pos - vec2(u_blobOffsetX, u_blobOffsetY);float sp = u_tileSpacing;op = mod(op - sp, vec2(sp * 2.0)) - sp;float totalRot = u_blobRotation + u_autoRotation * t;op = rot2d(op, -totalRot);op /= max(u_blobSize, 0.01);op *= vec2(1.0 / max(u_blobSpread, 0.01), 1.0);   // anisotropy// …then, per blob: col = mix(colour_i, col,//     smoothstep(0.0, bs, distance(op, vec2(0.0, y_i))));
the one math blockmod(pos − sp, 2·sp) − sp folds every pixel into the central cell: draw it once, see it everywhere
The fold, drawn: nine copies of one cell. The highlighted centre is the only maths that exists; the eight neighbours are the same maths seen through mod().

Two refinements turn circles into curtains. The rotation, a fixed base plus a slow auto-rotation, tilts the whole lattice so the ribbons run diagonal to the frame. And the spread is anisotropic: the x axis is divided by blobSpread while y is not, stretching each circle into a horizontal bar of light. That single asymmetry is most of what your eye reads as "aurora" rather than "dots".

One mod() draws an infinite field

Interactive
The field, rendered on the CPU with the same tile expression: spacing, spread, softness, and a simplex warp stand-in. Show the seams to see where mod() wraps; warp to zero to see the raw tiled ribbons.

The shear that makes it aurora

Untiled, unwrapped, the ribbons would sit there like a barcode. The shear that makes them live is domain warping, again in its two-sample form: one noise field displaces x, another displaces y, and the tile grid is read after the displacement, so the warp bends the seams themselves and the tiling never shows.

The warp. The +5.3 offset is the whole trick: two samples of the same field, far enough apart to be uncorrelated
// Two uncorrelated fBm samples, one per axis. The seed drifts// along the noise's third axis at its own speed, so the field// morphs instead of scrolling.float d1 = fbm(vec3(pos * u_warpScale + 0.5, animSeed), oct);float d2 = fbm(vec3(pos * u_warpScale + 0.5 + 5.3, animSeed + 1.7), oct);pos += vec2(d1, d2) * u_warpStrength * warpBoost;// warpBoost is 1.0, except under Swell, where the cursor lifts it.

The noise is 3D, and the third dimension is doing the work of time, but not as a scroll. The seed drifts along the noise's third axis at seedSpeed, which morphs the field in place: curtains churn and re-form without ever translating across the screen. That difference, morphing versus scrolling, is what separates weather from wallpaper.

Notice the warpBoost factor at the end of the displacement line, sitting at 1.0. It is a hand-off: the one place where the cursor is allowed to change the field's behaviour. Its story is the next section.

Five soft answers to a cursor

The obvious way to make a cursor affect a soft field is a radius: inside it, displace; outside, don't. An earlier iteration of this shader did exactly that, and the effect was ruinous in a specific way: the aurora warped in a visible circle that followed the cursor like a magnifying glass. The input had a harder edge than the thing it was touching.

Globalno position dependence, the sameeffect lands on every pixelBreeze · ParallaxUnbounded falloff1 / (1 + r²·k): influence decaysforever but never reaches zeroTideParameter modulationthe cursor changes an amplitude,not a position; no displacement at allSwell · Shimmerno smoothstep radius anywhere: there is no circle to find
Three ways to be soft. Each of the five modes commits to one, and none of them ever produces a boundary you could trace.

Breeze and Parallax go global: no position dependence at all. Breeze accumulates pointer velocity into a decaying wind vector (the field remembers how you moved for a moment after you stop) and shifts every pixel by the same amount.

Breeze: the wind is one vec2, shared by the whole canvas
// Breeze (JS): velocity accumulates into a wind vector that// decays each frame; the wind remembers recent motion.windX = windX * decay + velocity.x * gain;windY = windY * decay + velocity.y * gain;// Breeze (GLSL): the same offset lands on EVERY pixel.pos += u_mouseWind * u_mouseStr;

Tide is the unbounded falloff: 1 / (1 + r²·k) decays smoothly forever and never reaches zero. The subtle half of the idea is multiplying by the un-normalized direction vector, so the pull goes to zero exactly at the cursor; a normalized vector there would spin the field into a pinwheel at the one point you are looking at.

Tide, soft at both ends: no cut-off outside, no singularity inside
// Tide: an unbounded soft well. 1/(1 + r²·k) never reaches zero,// and multiplying by the UN-normalized toCursor makes the pull// vanish AT the cursor instead of pinwheeling there.float grav = 1.0 / (1.0 + mDist * mDist * 0.8);pos -= toCursor * grav * u_mouseStr * 0.7;

Swell and Shimmer modulate a parameter instead of a position. Swell is the default and the subtlest: cursor proximity multiplies the warp amplitude by up to 2.5× through a broad Gaussian. Nothing is displaced, so nothing can have an edge; the aurora simply "breathes harder" where you are.

Swell: the warpBoost hand-off from the last section, filled in
// Swell: proximity is a broad Gaussian on the warp AMPLITUDE.// No displacement: there is no circle because nothing moves.float sigma2 = max(u_mouseRadius * u_mouseRadius, 0.04);float proximity = exp(-mDist * mDist / sigma2);warpBoost = 1.0 + proximity * u_mouseStr * 2.5;

Shimmer applies the same Gaussian to a global high-frequency noise layer: the sparkle pattern exists everywhere, and proximity decides how much of it is mixed in. Five modes, three strategies, one rule, and you can feel the rule by switching modes on the component's page while moving the cursor in slow circles.

The one local event

Hovering is ambient, so it stays ambient. Clicking is a deliberate act, and the shader answers in kind: the pulse is the only effect with a position and a boundary: a ring that expands from where you clicked, displacing the field as it passes.

The shockwave: a Gaussian shell at radius age × speed
// The click: the one deliberately local effect. A Gaussian// shell at an expanding radius, cubically faded.float ringR = u_pulseAge * u_pulseSpeed;float ringDelta = pDist - ringR;float ringMask = exp(-(ringDelta * ringDelta) / (u_pulseWidth * u_pulseWidth));pos += pdirN * ringMask * u_pulseStr;
t = 0.00st = 0.25st = 0.50st = 0.75st = 1.00sradius = age × speed; a Gaussian shell around that radius is the ringamplitude = strength × fade³: a slam, then a settle
Radius grows linearly with age; amplitude is strength × fade³. The cube is what turns a linear fade into a slam-and-settle: most of the energy is spent in the first third of the life.

The engineering is edge-triggered on the JS side (a pulse starts on the pointer-down transition, not while the button is held), and its whole state is four uniforms: position, age, and a strength that the CPU multiplies by fade³ each frame until it retires. Because the ring displaces the lookupbefore the warp and the tiles, the wave fronts break across the curtains like a bow wave; that composure is the payoff of doing every effect in coordinate space.

Palettes and film

The palette is five colour dials (four curtains and a background), and eleven named presets from Boreal Green to Nebula Drift are just that palette, re-picked. A colour shift dial adds a slow hue rotation in YIQ space, so a preset can be set drifting through its own neighbourhood of moods without ever leaving them.

The finish is film grain, and its one interesting decision is temporal: the value noise is re-seeded in discrete steps of floor(t · grainSpeed), so each grain pattern holds for a frame count and then jumps. Continuous grain crawls like sand; stepped grain flickers like film. The jump is the look.

Stepped re-seeding: the flicker is discrete by construction
// Film grain that flickers instead of crawling: re-seed the// value noise in DISCRETE time steps.float tStep = floor(t * max(u_grainSpeed, 0.0));gp += vec2(tStep * 13.17, tStep * 7.91);col += valNoise(gp) * u_grainAmount;

Steal the rule, not the shader

The shader is a good aurora, but the transferable part of this component is the discipline it imposes on input. Any time you hook a cursor into something diffuse (fog, smoke, water, light), the same three strategies apply: go global, use a falloff that never reaches zero, or modulate an amplitude instead of a position. The test is simple: if a viewer could trace the boundary of your cursor's influence, the input is harder than the visual, and one of them should give.

Every strategy here is a mode on the Aurora drift's page, next to the live preview and the full panel: warp, tiles, framing, all five cursor modes with their own strength and radius dials, and the palette presets. Tune the field to your mood and the build prompt carries your exact values to your stack.