Corona
A classic aurora: ribboned bands with a moving icy sheen on a near-black field, in teal, ice blue, and violet. Original OGL fragment shader, offscreen-paused and reduced-motion-safe.
oglfree
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { OglCanvas } from "@/registry/default/lib/ogl-canvas/ogl-canvas";
export interface CoronaProps {
/**
* Three tones for the ribbon gradient: `[crest, mid, base]`.
* Any hex string. Defaults to aurora teal / violet / ice blue.
*/
colors?: [string, string, string];
/** Number of ribbon bands, clamped 3–6. Default 4. */
bands?: number;
/** Flow speed multiplier. 1 = default. Default 1. */
speed?: number;
/** Overall brightness / bloom of the ribbons. 1 = default. Default 1. */
intensity?: number;
/** Freeze the animation, holding the current frame. */
paused?: boolean;
/** Extra classes for the wrapper. Positioned `absolute inset-0 -z-10`. */
className?: string;
}
const DEFAULT_COLORS: [string, string, string] = [
"#5eead4", // aurora teal — crest
"#8b5cf6", // violet — mid
"#93c5fd", // ice blue — base lift
];
/** Hex → linear-ish [r,g,b] in 0–1. Accepts #rgb / #rrggbb. */
function hexToRgb(hex: string): [number, number, number] {
let h = hex.replace("#", "").trim();
if (h.length === 3) {
h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
}
const int = parseInt(h, 16);
if (Number.isNaN(int) || h.length !== 6) return [0.37, 0.92, 0.83];
return [((int >> 16) & 255) / 255, ((int >> 8) & 255) / 255, (int & 255) / 255];
}
/**
* Original aurora-curtain fragment shader. Real northern lights are hanging
* vertical sheets, not horizontal bands — so each "band" here is a draping
* light CURTAIN: a soft vertical column whose horizontal position folds and
* sways with height (domain-warped fbm) like fabric in a slow wind, threaded
* with fine vertical ray filaments that shimmer, brightest along a ragged
* lower edge and fading up into darkness. Layers are stacked at different
* parallax depths for atmosphere, over a deep-space gradient and a faint
* twinkling starfield. The vertical color ramp runs teal -> violet -> ice.
* Exponential tone curve + film-grain dithering kill 8-bit banding — a real
* aurora has none. GLSL ES 1.00 (WebGL1/2 compatible). No fract() on spatial
* coordinates (only inside the hash), so there are no wrap seams.
*/
const FRAGMENT = /* glsl */ `
precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uPointer;
uniform vec3 uColorA; // crest — the bright teal body near the base
uniform vec3 uColorB; // mid — violet through the middle
uniform vec3 uColorC; // base — ice blue at the ragged tips
uniform float uBands;
uniform float uSpeed;
uniform float uIntensity;
const int MAX_BANDS = 6;
// --- compact value noise + fbm (original) ---
float hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float vnoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash21(i);
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float amp = 0.5;
// Rotated lacunarity kills the axis-aligned look of raw value noise.
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
for (int i = 0; i < 4; i++) {
v += amp * vnoise(p);
p = m * p;
amp *= 0.5;
}
return v;
}
// Vertical color ramp along a curtain: teal body low, violet mid, ice tips.
vec3 auroraRamp(float y) {
vec3 c = mix(uColorA, uColorB, smoothstep(0.10, 0.52, y));
c = mix(c, uColorC, smoothstep(0.52, 0.92, y));
return c;
}
void main() {
vec2 uv = vUv;
float aspect = uResolution.x / max(uResolution.y, 1.0);
float t = uTime * uSpeed;
int bandCount = int(clamp(uBands, 3.0, float(MAX_BANDS)));
// Curtains lean gently toward the pointer — a lean, never a spotlight.
float lean = clamp((uPointer.x - 0.5) * 2.0, -1.0, 1.0);
// ---- deep space: a cool vertical gradient, darker toward the top ----
vec3 col = mix(vec3(0.020, 0.030, 0.052), vec3(0.006, 0.009, 0.020),
smoothstep(0.0, 1.0, uv.y));
// Faint teal airglow hugging the horizon so the base never reads as dead.
col += uColorA * 0.055 * pow(1.0 - uv.y, 3.0);
// ---- starfield behind the aurora, strongest up in the dark sky ----
vec2 sg = uv * vec2(aspect, 1.0) * 118.0;
vec2 sid = floor(sg);
float pick = hash21(sid);
if (pick > 0.985) {
vec2 sp = vec2(hash21(sid + 3.17), hash21(sid + 7.73));
float sd = length(fract(sg) - sp);
float star = pow(max(0.0, 1.0 - sd * 3.0), 5.0);
float twinkle = 0.55 + 0.45 * sin(t * 1.6 + pick * 51.0);
col += vec3(0.72, 0.80, 1.0) * star * twinkle * smoothstep(0.18, 0.6, uv.y) * 0.9;
}
// ---- draping light curtains ----
for (int i = 0; i < MAX_BANDS; i++) {
if (i >= bandCount) break;
float fi = float(i);
float depth = fi / float(MAX_BANDS - 1); // 0 near .. 1 far
float par = mix(1.0, 1.9, depth); // farther curtains: tighter, more of them
float bri = mix(1.0, 0.42, depth); // and dimmer with distance
// Slow bodily drift of the whole curtain sideways across the sky.
float drift = t * (0.045 + 0.022 * fi);
// A slow clock feeds the domain warp — this is the sheet's WAVER. Kept
// gentle so the folds drape and evolve gracefully; a fast clock here makes
// the wisps whip around frantically, which is the opposite of a real aurora.
float warpT = t * (0.055 + 0.018 * fi);
// Horizontal FOLD: the curtain's x position bends as a function of height,
// domain-warped in three octaves so the sheet drapes and gathers like a
// hanging veil in a slow wind. The higher octaves evolve only a little
// faster than the base (not multiples faster) and carry less weight, so the
// sheet reads as one cohesive, fluid drape rather than a mess of ripples.
float foldA = fbm(vec2(uv.y * 1.05 + fi * 3.1, warpT)) - 0.5;
float foldB = fbm(vec2(uv.y * 2.4 - fi * 1.7, warpT * 1.3 + 4.0)) - 0.5;
float foldC = fbm(vec2(uv.y * 4.6 + fi * 2.3, warpT * 1.7 + 9.0)) - 0.5;
float fold = foldA * 1.4 + foldB * 0.42 + foldC * 0.14;
// Slow lateral undulation: the whole sheet breathes side to side, a couple
// of low sine modes phase-offset up its height so it sways rather than
// sliding rigidly. This is the big, calm aurora "wander" — kept unhurried.
float sway = sin(uv.y * 2.3 + t * 0.12 + fi * 1.7) * 0.32
+ sin(uv.y * 4.1 - t * 0.18 + fi) * 0.12;
// Sheet coordinate: pointer nudges the whole curtain sideways.
float sx = (uv.x - 0.5) * aspect * par + fold + sway + drift * 0.5
+ fi * 5.3 + lean * 0.22;
// Envelope: wide soft columns carved out of low-frequency noise, so the
// aurora is a few distinct curtains with dark gaps, not a wall. A slow time
// term lets each curtain widen and narrow — breathing — as it drifts.
float env = fbm(vec2(sx * 0.85, fi * 9.0 + warpT * 0.55));
float sheet = smoothstep(0.44, 0.74, env);
// Ray filaments: fine vertical striations threading the sheet, their phase
// jittered by noise so they never read as a comb. They shimmer and stream
// upward, giving the wispy, flickering texture of real curtain rays.
float rn = vnoise(vec2(sx * 3.2, uv.y * 1.4 - warpT * 1.6));
float rays = 0.60 + 0.40 * sin(sx * 17.0 + rn * 7.0 - uv.y * 5.0 + t * 1.15);
sheet *= mix(1.0, rays, 0.46);
// Vertical profile: fade in off the base, ragged top edge that now sways
// and frays in time with the sheet, and a brightness bias toward the body.
float topEdge = 0.86 + 0.15 * (fbm(vec2(sx * 0.7, warpT * 0.6 + 3.0)) - 0.5);
float vprof = smoothstep(0.04, 0.20, uv.y)
* (1.0 - smoothstep(topEdge - 0.34, topEdge, uv.y));
vprof *= mix(1.30, 0.55, smoothstep(0.12, 0.85, uv.y));
float lum = sheet * vprof * bri;
vec3 grad = auroraRamp(uv.y);
col += grad * lum * 1.55 * uIntensity;
// Hot lower spine: a tighter, brighter additive pass along the bright
// teal body so each curtain has a luminous core, not a flat wash.
vec3 hot = mix(uColorA, vec3(0.80, 1.0, 0.94), 0.32);
col += hot * pow(lum, 1.7) * 0.55 * uIntensity;
}
// Soft exponential tone curve so stacked curtains bloom toward white and
// roll off asymptotically instead of clipping to flat patches.
col = 1.0 - exp(-col * 1.55);
// Gentle vignette to sink the frame edges into space.
vec2 vg = (uv - vec2(0.5, 0.46)) * vec2(aspect, 1.0);
col *= 1.0 - 0.32 * dot(vg, vg);
// Film-grain dithering to defeat 8-bit banding in the smooth gradients.
float grain = hash21(gl_FragCoord.xy + fract(t) * 137.0) - 0.5;
col += grain * (1.8 / 255.0);
gl_FragColor = vec4(max(col, 0.0), 1.0);
}
`;
/**
* Corona — a real aurora. Draping vertical light curtains fold and sway with
* height, threaded with shimmering ray filaments, brightest along a ragged
* lower edge and fading up into a deep-space starfield. Vertical color ramp
* of teal / violet / ice by default. Built on the `ogl-canvas` harness:
* GPU-light, offscreen-paused, reduced-motion-safe. Drop it inside any
* `relative` container; it fills and sits behind content.
*/
export function Corona({
colors = DEFAULT_COLORS,
bands = 4,
speed = 1,
intensity = 1,
paused = false,
className,
}: CoronaProps) {
const uniforms = React.useMemo(
() => ({
uColorA: { value: hexToRgb(colors[0]) },
uColorB: { value: hexToRgb(colors[1]) },
uColorC: { value: hexToRgb(colors[2]) },
uBands: { value: bands },
uSpeed: { value: speed },
uIntensity: { value: intensity },
}),
[colors, bands, speed, intensity]
);
return (
<OglCanvas
aria-hidden
data-crucible="corona"
className={cn("absolute inset-0 -z-10", className)}
fragment={FRAGMENT}
uniforms={uniforms}
paused={paused}
reducedMotionTime={3.4}
/>
);
}
Installation
CLI
npx shadcn@latest add @crucible/coronaManual — install dependencies, then copy the source
npm install oglProps
| Prop | Type | Default | Description |
|---|---|---|---|
| colors | [string, string, string] | aurora teal / violet / ice blue | Three tones for the ribbon gradient: [crest, mid, base]. Any hex string. |
| bands | number | 4 | Number of ribbon bands, clamped 3–6. |
| speed | number | 1 | Flow speed multiplier. 1 = default. |
| intensity | number | 1 | Overall brightness / bloom of the ribbons. 1 = default. |
| paused | boolean | Freeze the animation, holding the current frame. | |
| className | string | Extra classes for the wrapper. Positioned absolute inset-0 -z-10. |
Honors prefers-reduced-motion with a designed static fallback, pauses offscreen and on hidden tabs, and passes className through.