{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora",
  "type": "registry:block",
  "title": "Aurora",
  "description": "Raymarched northern lights: three folding curtains with fine rays, a sharp pink-fringed hem and edge-on bright folds, over a spruce shore and a lake that mirrors them. Temporal-blended and dithered, so it never bands; the pointer pans the sky on a spring.",
  "docs": "Aurora was added to public/vividsites/aurora.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/aurora",
  "categories": [
    "3d",
    "shader-backgrounds"
  ],
  "files": [
    {
      "path": "components/vividsites/aurora.html",
      "type": "registry:file",
      "target": "public/vividsites/aurora.html",
      "content": "<!--\n  vs-aurora — raymarched aurora curtains over a spruce shore and a still lake that mirrors them.\n  Three folding curtains live in a real volume: each is a thin vertical sheet along a curve that\n  drifts and folds, with a sharp lower hem, fine rays of different lengths shimmering along it,\n  and a fringe of a second colour at its foot. Every frame a tiny pass bakes the curtains' shape\n  into a 1D texture, then a jittered march through the sky integrates them (edge-on folds come out\n  bright, as they do in the real thing) and a temporal blend removes the grain. It is composited\n  over stars, a faint Milky Way, far hills, a treeline and a lake with a Fresnel mirror and a slow\n  ripple; graded and dithered so the gradients never band. The pointer pans the sky, on a spring.\n\n  Fills its nearest positioned parent (position:absolute; inset:0). Put content above it.\n  CSS variables (set on .vs-aurora):\n    --vs-aurora-low        colour of the bright lower curtain (default green)\n    --vs-aurora-high       colour the rays turn as they climb\n    --vs-aurora-hem        the fringe along the curtain's lower edge\n    --vs-aurora-sky        the night sky at the horizon\n    --vs-aurora-intensity  brightness of the aurora, 1 = default\n    --vs-aurora-speed      animation speed, 1 = default\n    --vs-aurora-horizon    height of the lake line, 0–1 from the bottom (default .3; 0 = no lake, the sky runs\n                           down to a low treeline at the bottom edge)\n-->\n<div class=\"vs-aurora\" aria-hidden=\"true\"><canvas class=\"vs-aurora-canvas\"></canvas></div>\n<style>\n  .vs-aurora {\n    --vs-aurora-low: #38f2a4;\n    --vs-aurora-high: #2ab4d9;\n    --vs-aurora-hem: #ff5c9d;\n    --vs-aurora-sky: #0a1424;\n    --vs-aurora-intensity: 1;\n    --vs-aurora-speed: 1;\n    --vs-aurora-horizon: .3;\n    position: absolute; inset: 0; overflow: hidden; pointer-events: none;\n    background: radial-gradient(70% 45% at 50% 58%, color-mix(in oklab, var(--vs-aurora-low) 22%, transparent), transparent 70%), linear-gradient(#03060c, var(--vs-aurora-sky) 68%, #020406 70%);\n  }\n  .vs-aurora-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; opacity: 0; transition: opacity 1.4s cubic-bezier(.16, 1, .3, 1); }\n  .vs-aurora-ready .vs-aurora-canvas { opacity: 1; }\n</style>\n<script>\n(() => {\n  /* ---- shared runtime: WebGL2 context, resize, visibility, pointer spring, CSS-variable props ---- */\n  const VS = (root, spec) => {\n    const P = spec.prefix, canvas = root.querySelector('canvas');\n    const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n    const S = { root, canvas, gl: null, w: 1, h: 1, cssW: 1, cssH: 1, dpr: 1, t: spec.t0 || 0, rt: 0, speed: 1, frame: 0, visible: false, lost: false, raf: 0, reduced: reduce.matches, css: {},\n      ptr: { x: .5, y: .5, tx: .5, ty: .5, vx: 0, vy: 0, px: .5, py: .5, down: false, inside: false, active: 0, seen: false } };\n    const gl = canvas.getContext('webgl2', { antialias: false, alpha: false, depth: false, stencil: false, premultipliedAlpha: false, powerPreference: 'high-performance' });\n    if (!gl) { root.classList.add(P + '-fallback'); return null; }\n    S.gl = gl;\n    /* colours: any CSS colour string -> linear rgb */\n    const cc = document.createElement('canvas').getContext('2d', { willReadFrequently: true });\n    const toLin = (v) => (v <= .04045 ? v / 12.92 : Math.pow((v + .055) / 1.055, 2.4));\n    const color = (str) => { cc.clearRect(0, 0, 1, 1); cc.fillStyle = '#000'; cc.fillStyle = str; cc.fillRect(0, 0, 1, 1); const d = cc.getImageData(0, 0, 1, 1).data; return [toLin(d[0] / 255), toLin(d[1] / 255), toLin(d[2] / 255)]; };\n    const readVars = () => {\n      const cs = getComputedStyle(root);\n      for (const [k, def] of Object.entries(spec.vars || {})) {\n        const raw = cs.getPropertyValue(`--${P}-${k}`).trim() || String(def);\n        S.css[k] = typeof def === 'number' ? (parseFloat(raw) || 0) : color(raw);\n      }\n      S.speed = S.css.speed != null ? S.css.speed : 1;\n      spec.vars && spec.onVars && spec.onVars(S);\n    };\n    /* GL helpers */\n    const VERT = `#version 300 es\nvoid main(){ vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); gl_Position = vec4(p * 2. - 1., 0., 1.); }`;\n    S.program = (frag, vert) => {\n      const mk = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS) && !gl.isContextLost()) console.warn(P, gl.getShaderInfoLog(s)); return s; };\n      const p = gl.createProgram(); gl.attachShader(p, mk(gl.VERTEX_SHADER, vert || VERT)); gl.attachShader(p, mk(gl.FRAGMENT_SHADER, '#version 300 es\\nprecision highp float;\\nprecision highp int;\\n' + frag)); gl.linkProgram(p);\n      const u = {}; const n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS) || 0;\n      for (let i = 0; i < n; i++) { const a = gl.getActiveUniform(p, i); u[a.name.replace(/\\[0\\]$/, '')] = gl.getUniformLocation(p, a.name); }\n      return { p, u };\n    };\n    S.target = (w, h, o = {}) => {\n      const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t);\n      const half = o.half !== false && S.hf;\n      gl.texImage2D(gl.TEXTURE_2D, 0, half ? gl.RGBA16F : gl.RGBA8, w, h, 0, gl.RGBA, half ? gl.HALF_FLOAT : gl.UNSIGNED_BYTE, null);\n      const f = o.nearest ? gl.NEAREST : gl.LINEAR;\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, o.mip ? gl.LINEAR_MIPMAP_LINEAR : f); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, f);\n      const wrap = o.repeat ? gl.REPEAT : gl.CLAMP_TO_EDGE; gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);\n      const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);\n      gl.viewport(0, 0, w, h); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n      return { t, fb, w, h, mip: !!o.mip };\n    };\n    S.pair = (w, h, o) => { const a = S.target(w, h, o), b = S.target(w, h, o); return { a, b, w, h, swap() { const x = this.a; this.a = this.b; this.b = x; } }; };\n    S.free = (...ts) => ts.forEach((x) => { if (!x) return; if (x.a) { S.free(x.a, x.b); return; } gl.deleteTexture(x.t); gl.deleteFramebuffer(x.fb); });\n    /* draw a program into a target (null = screen); tex = {name: target|texture} bound to units in order */\n    S.draw = (pr, out, uni = {}, tex = {}) => {\n      gl.useProgram(pr.p);\n      if (out) { gl.bindFramebuffer(gl.FRAMEBUFFER, out.fb); gl.viewport(0, 0, out.w, out.h); } else { gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, S.w, S.h); }\n      let unit = 0;\n      for (const [k, v] of Object.entries(tex)) { gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, v && v.t !== undefined ? v.t : v); if (pr.u[k]) gl.uniform1i(pr.u[k], unit); unit++; }\n      for (const [k, v] of Object.entries(uni)) {\n        const l = pr.u[k]; if (!l) continue;\n        if (typeof v === 'number') gl.uniform1f(l, v); else if (v.length === 2) gl.uniform2fv(l, v); else if (v.length === 3) gl.uniform3fv(l, v); else if (v.length === 4) gl.uniform4fv(l, v); else gl.uniform3fv(l, v);\n      }\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n      if (out && out.mip) { gl.bindTexture(gl.TEXTURE_2D, out.t); gl.generateMipmap(gl.TEXTURE_2D); }\n    };\n    const setup = () => {\n      S.hf = !!gl.getExtension('EXT_color_buffer_float') || !!gl.getExtension('EXT_color_buffer_half_float');\n      gl.getExtension('OES_texture_float_linear');\n      gl.disable(gl.DEPTH_TEST); gl.disable(gl.BLEND);\n      spec.init(S); S.sized = false; readVars(); resize();\n    };\n    const resize = () => {\n      const r = root.getBoundingClientRect(); const scale = (spec.scale ? spec.scale(S) : 1);\n      S.cssW = Math.max(1, r.width); S.cssH = Math.max(1, r.height);\n      S.dpr = Math.min(window.devicePixelRatio || 1, 2) * scale;\n      const w = Math.max(1, Math.round(S.cssW * S.dpr)), h = Math.max(1, Math.round(S.cssH * S.dpr));\n      if (w === S.w && h === S.h && S.sized) return;\n      S.w = canvas.width = w; S.h = canvas.height = h; S.aspect = w / h; S.sized = true;\n      spec.resize && spec.resize(S);\n      if (S.reduced) still();\n    };\n    /* pointer: a spring toward the finger/mouse, in root-normalised coords (0..1, y up) */\n    const P_ = S.ptr;\n    const at = (cx, cy) => { const r = root.getBoundingClientRect(); P_.tx = (cx - r.left) / Math.max(1, r.width); P_.ty = 1 - (cy - r.top) / Math.max(1, r.height); P_.inside = P_.tx >= 0 && P_.tx <= 1 && P_.ty >= 0 && P_.ty <= 1; if (!P_.seen) { P_.x = P_.px = P_.tx; P_.y = P_.py = P_.ty; P_.seen = true; } P_.active = 1; P_.last = performance.now(); };\n    addEventListener('pointermove', (e) => { if (e.pointerType !== 'touch') at(e.clientX, e.clientY); }, { passive: true });\n    addEventListener('pointerdown', (e) => { at(e.clientX, e.clientY); if (P_.inside) { P_.down = true; spec.press && spec.press(S, true); } }, { passive: true });\n    addEventListener('pointerup', () => { if (P_.down) { P_.down = false; spec.press && spec.press(S, false); } }, { passive: true });\n    addEventListener('pointercancel', () => { if (P_.down) { P_.down = false; spec.press && spec.press(S, false); } }, { passive: true });\n    addEventListener('touchstart', (e) => { const t = e.touches[0]; if (t) { P_.seen = false; at(t.clientX, t.clientY); } }, { passive: true });\n    addEventListener('touchmove', (e) => { const t = e.touches[0]; if (t) at(t.clientX, t.clientY); }, { passive: true });\n    const stepPtr = (dt) => {\n      P_.px = P_.x; P_.py = P_.y;\n      const k = spec.ptrSpring || [170, 26]; const n = Math.ceil(dt / (1 / 120));\n      for (let i = 0; i < n; i++) { const h = dt / n; P_.vx += ((P_.tx - P_.x) * k[0] - P_.vx * k[1]) * h; P_.vy += ((P_.ty - P_.y) * k[0] - P_.vy * k[1]) * h; P_.x += P_.vx * h; P_.y += P_.vy * h; }\n      P_.active = Math.max(0, P_.active - dt * .6);\n    };\n    /* loop: only while on screen, tab visible and context alive */\n    let last = 0;\n    const loop = (now) => {\n      S.raf = 0; if (!canRun()) return;\n      const dt = last ? Math.min(.05, (now - last) / 1000) : 1 / 60; last = now;\n      stepPtr(dt); S.t += dt * S.speed; S.rt += dt; S.frame++;\n      spec.frame(S, dt);\n      schedule();\n    };\n    const canRun = () => S.visible && !document.hidden && !S.lost && !S.reduced;\n    const schedule = () => { if (!S.raf && canRun()) { S.raf = requestAnimationFrame(loop); } else if (!canRun()) last = 0; };\n    const still = () => { if (S.lost) return; (spec.still || ((s) => spec.frame(s, 1 / 60)))(S); };\n    new ResizeObserver(resize).observe(root);\n    new IntersectionObserver((es) => { S.visible = es[es.length - 1].isIntersecting; schedule(); }, { rootMargin: '64px' }).observe(root);\n    document.addEventListener('visibilitychange', schedule);\n    reduce.addEventListener('change', () => { S.reduced = reduce.matches; S.reduced ? still() : schedule(); });\n    new MutationObserver(() => { readVars(); if (S.reduced) still(); }).observe(root, { attributes: true, attributeFilter: ['style', 'class'] });\n    root.addEventListener(P + ':refresh', () => { readVars(); if (S.reduced) still(); });\n    canvas.addEventListener('webglcontextlost', (e) => { e.preventDefault(); S.lost = true; if (S.raf) cancelAnimationFrame(S.raf); S.raf = 0; });\n    canvas.addEventListener('webglcontextrestored', () => { S.lost = false; setup(); schedule(); });\n    S.api = { press: (on) => spec.press && spec.press(S, on) };\n    setup();\n    if (S.reduced) still();\n    root.classList.add(P + '-ready');\n    return S;\n  };\n\n  const NOISE = `\nfloat h12(vec2 p){ vec3 a = fract(p.xyx * .1031); a += dot(a, a.yzx + 33.33); return fract((a.x + a.y) * a.z); }\nvec2 h22(vec2 p){ vec3 a = fract(p.xyx * vec3(.1031, .103, .0973)); a += dot(a, a.yzx + 33.33); return fract((a.xx + a.yz) * a.zy); }\nfloat vn(vec2 p){ vec2 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); return mix(mix(h12(i), h12(i + vec2(1, 0)), f.x), mix(h12(i + vec2(0, 1)), h12(i + 1.), f.x), f.y); }\nfloat fbm(vec2 p){ float s = 0., a = .5; for (int i = 0; i < 5; i++){ s += a * vn(p); p = p * 2.03 + 17.1; a *= .5; } return s; }`;\n  /* the curtains' shape, baked along x each frame: 3 sheets x 2 rows (4096 x 6)\n     row A: fold offset in z, 1/sqrt(1+slope^2), altitude of the hem, brightness patch\n     row B: ray brightness, ray length, hem-fringe strength */\n  const XR = 16.;\n  const CURT = `${NOISE}\nuniform float uTime; out vec4 o;\nfloat fold(float x, float s, float t){\n  float A1 = s < .5 ? .5 : (s < 1.5 ? .85 : 1.7), f1 = s < .5 ? .95 : (s < 1.5 ? .55 : .24);\n  float A2 = s < .5 ? .26 : (s < 1.5 ? .38 : .7),  f2 = s < .5 ? 2.3 : (s < 1.5 ? 1.5 : .7);\n  float An = s < .5 ? .5 : (s < 1.5 ? 1.1 : 1.9);\n  return A1 * sin(x * f1 + t * .05 + s * 2.1) + A2 * sin(x * f2 - t * .085 + s * 4.7) + An * (vn(vec2(x * .42 + s * 13.1, t * .018)) - .5) * 2.;\n}\nvoid main(){\n  float row = floor(gl_FragCoord.y); float s = floor(row * .5); bool B = mod(row, 2.) > .5;\n  float x = (gl_FragCoord.x / 4096. * 2. - 1.) * ${XR.toFixed(1)};\n  float t = uTime;\n  if (!B){\n    float f = fold(x, s, t), e = .02; float sl = (fold(x + e, s, t) - fold(x - e, s, t)) / (2. * e);\n    float bottom = 1. + .16 * vn(vec2(x * .33 + s * 7.7, t * .012)) + s * .04;\n    float ptc = smoothstep(.22, .72, vn(vec2(x * (.2 - s * .04) + s * 3.3, t * .016))) * (.35 + .65 * smoothstep(.1, .6, vn(vec2(x * .9 + 5. + s, t * .05))));\n    o = vec4(f, inversesqrt(1. + sl * sl), bottom, ptc);\n  } else {\n    float rf = s < .5 ? 40. : (s < 1.5 ? 28. : 14.);\n    float r1 = vn(vec2(x * rf + s * 31., t * .45)), r2 = vn(vec2(x * rf * 2.6 - 7., -t * .9)), r3 = vn(vec2(x * rf * .3 + 2., t * .2));\n    float r14 = r1 * r1; r14 *= r14;\n    float rays = .06 + .94 * (r14 * (.4 + .6 * r2) * 2.4 + .3 * r3 * r3);\n    // rays pulse: a slow travelling brightening along the curtain\n    rays *= .75 + .5 * smoothstep(.3, .9, vn(vec2(x * 2.2 - t * .35, s * 9.)));\n    float len = .22 + .95 * pow(vn(vec2(x * rf * .22 + s * 5., t * .08)), 1.6);\n    float hem = smoothstep(.45, .85, vn(vec2(x * 1.1 + s * 17., t * .04)));\n    o = vec4(rays, len, hem, 1.);\n  }\n}`;\n  /* the aurora: march through the sky, in plan distance, over the three sheets */\n  const AUR = `\nuniform sampler2D uC; uniform vec2 uRes; uniform float uPan, uTilt, uHz, uFrame, uAsp; uniform vec3 uLow, uHigh, uHem; out vec4 o;\nconst float ZB[3] = float[3](-2.4, -4.3, -9.);\nconst float AM[3] = float[3](1.25, 2.35, 4.3);   // how far each sheet's folds can wander in z\nconst float WD[3] = float[3](.02, .034, .07);\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 q = vec2((uv.x - .5) * uAsp, uv.y - uHz);\n  vec3 rd = normalize(vec3(q.x * .9, q.y * .9 + .035 + uTilt, -1.));\n  vec3 acc = vec3(0.);\n  if (rd.y > .002){\n    float lxz = length(rd.xz); vec2 dir = rd.xz / lxz; float k = lxz / rd.y;\n    float s0 = max(.97 * k, 1.1), s1 = min(3.6 * k, 28.);\n    float jit = fract(52.9829189 * fract(dot(gl_FragCoord.xy + uFrame * vec2(5.588238, 3.12), vec2(.06711056, .00583715))));\n    for (int j = 0; j < 3; j++){\n      // march only the stretch of the ray where this sheet can be\n      float sa = max(s0, (ZB[j] + AM[j]) / dir.y), sb = min(s1, (ZB[j] - AM[j]) / dir.y);\n      if (sb <= sa) continue;\n      int N = j == 0 ? 44 : (j == 1 ? 28 : 22);\n      float ds = (sb - sa) / float(N);\n      float w = WD[j], we = max(w, ds * .5), ww = w / we;\n      float row = (float(j * 2) + .5) / 6.;\n      for (int i = 0; i < N; i++){\n        float s = sa + (float(i) + jit) * ds;\n        vec2 P = dir * s + vec2(uPan, 0.); float h = s / k;\n        float tx = (P.x / ${XR.toFixed(1)}) * .5 + .5;\n        vec4 A = texture(uC, vec2(tx, row));\n        float hh = h - A.z;\n        float d = (P.y - ZB[j] - A.x) * A.y;\n        float g = exp(-d * d / (we * we)) * ww;\n        if (hh < -.03 || g * A.w < .002) continue;\n        vec4 B = texture(uC, vec2(tx, row + 1. / 6.));\n        float up = max(hh, 0.);\n        float vert = smoothstep(-.014, .006, hh) * (.6 * exp(-up / (.05 + .22 * B.y)) + .4 * exp(-up / (.18 + .9 * B.y)));\n        float e = g * vert * A.w * B.x;\n        float climb = smoothstep(.05, .85, up / (.3 + .9 * B.y));\n        vec3 c = mix(mix(uLow, uHigh, climb), uHem, B.z * exp(-up * 45.) * .75);\n        acc += c * e * ds;\n      }\n    }\n    acc *= 3.6 * smoothstep(.002, .05, rd.y);\n  }\n  o = vec4(acc, .3);\n}`;\n  /* the night: sky, stars, the aurora, the hills and trees it lights, the lake that mirrors it all; graded */\n  const VIEW = `${NOISE}\nuniform sampler2D uAur; uniform vec2 uRes; uniform float uTime, uHz, uK, uPan, uFrame; uniform vec3 uSky, uLow; out vec4 o;\nvec3 toSRGB(vec3 c){ c = max(c, 0.); return mix(c * 12.92, 1.055 * pow(c, vec3(1. / 2.4)) - .055, step(.0031308, c)); }\nvec3 aces(vec3 x){ return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }\nfloat ridge(float x, float seed){ float s = 0., a = .5, f = 1.; for (int i = 0; i < 4; i++){ s += a * vn(vec2(x * f + seed, seed)); a *= .5; f *= 2.2; } return s; }\n// spruce: a jagged cone per cell, taller toward the sides of the frame\nfloat trees(float x, float y, float amp){\n  float cw = .011; float c = floor(x / cw); float m = 0.;\n  for (int i = -1; i <= 1; i++){\n    float ci = c + float(i); vec2 hh = h22(vec2(ci, 3.7));\n    float th = amp * (.35 + .65 * hh.x) * step(.18, hh.y);\n    float cx = (ci + .5 + (hh.y - .5) * .6) * cw;\n    float yy = y / max(th, 1e-4);\n    float half_ = (1. - yy) * cw * (.55 + .2 * hh.x) * (.72 + .28 * fract(yy * (7. + hh.y * 4.)));\n    m = max(m, step(abs(x - cx), half_) * step(0., yy) * step(yy, 1.));\n    // the trunk tip\n    m = max(m, step(abs(x - cx), cw * .04) * step(0., yy) * step(yy, 1.08));\n  }\n  return m;\n}\nvec3 sky(vec2 uv, float asp){\n  float y = uv.y - uHz;\n  vec3 top = vec3(.0008, .0016, .005);\n  vec3 col = mix(uSky * 1.05, top, smoothstep(0., .7, y));\n  // the Milky Way: a tilted band of dust and grain\n  vec2 sp = vec2((uv.x - .5) * asp + uPan * .05, uv.y);\n  float band = exp(-pow((sp.y - .42 - sp.x * .36) * 3.2, 2.)) * smoothstep(0., .25, y);\n  float dust = fbm(sp * 5. + 3.) * fbm(sp * 13.);\n  col += vec3(.5, .55, .7) * band * dust * .05;\n  col -= vec3(.005) * band * smoothstep(.5, .7, fbm(sp * 9. + 8.));\n  // stars: three sizes, twinkling, fewer in the horizon haze\n  vec3 st = vec3(0.);\n  for (int L = 0; L < 3; L++){\n    float sc = L == 0 ? 80. : (L == 1 ? 170. : 320.);\n    vec2 g = sp * sc; vec2 id = floor(g), f = fract(g) - .5; vec2 hh = h22(id + float(L) * 17.);\n    float on = step(L == 0 ? .972 : (L == 1 ? .95 : .92), hh.x);\n    float d = length(f - (hh - .5) * .6);\n    float tw = .6 + .4 * sin(uTime * (1.5 + hh.y * 4.) + hh.x * 40.);\n    st += on * smoothstep(L == 0 ? .12 : .08, 0., d) * tw * (L == 0 ? 1.3 : (L == 1 ? .6 : .3)) * mix(vec3(.75, .85, 1.), vec3(1., .9, .75), hh.y);\n  }\n  col += st * smoothstep(0., .1, y) * (1. + band * 2.);\n  // the aurora, and its own haze\n  vec3 a = texture(uAur, uv).rgb * uK;\n  vec3 glow = textureLod(uAur, uv, 3.).rgb * .18 + textureLod(uAur, uv, 5.).rgb * .3;\n  col += a + glow * uK;\n  // horizon airglow\n  col += uLow * .012 * exp(-max(y, 0.) * 22.) * uK;\n  // far hills, hazed and lit from above by the aurora; a spruce shore in front\n  float px = (uv.x - .5) * asp + uPan * .12;\n  float rg = ridge(px * 1.3, 3.1); rg = rg * rg * 1.6;\n  float hill = uHz + .003 + rg * .05 * (.3 + .9 * smoothstep(.1, 1.1, abs(px - .2)));\n  vec3 lit = textureLod(uAur, vec2(uv.x, uHz + .22), 6.).rgb * uK;\n  if (uv.y < hill){\n    float below = hill - uv.y;\n    col = uSky * .16 + vec3(.002, .003, .006) + lit * (.06 + .5 * exp(-below * 90.));\n  }\n  float px2 = (uv.x - .5) * asp + uPan * .3;\n  float side = smoothstep(.1, .75, abs(px2 + .06));\n  float shore = uHz + .003 + .006 * vn(vec2(px2 * 6., 1.));\n  if (uv.y < shore || trees(px2, uv.y - shore, .018 + .07 * side) > .5) col = vec3(.0015, .002, .0035) + lit * .012;\n  return col;\n}\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes; float asp = uRes.x / uRes.y;\n  vec3 col;\n  if (uv.y >= uHz || uHz <= 0.) col = sky(uv, asp);\n  else {\n    // the lake: a Fresnel mirror, broken by a slow ripple that grows toward the viewer\n    float dep = (uHz - uv.y) / max(uHz, .001);\n    float zz = 1. / (.05 + dep);\n    vec2 w = vec2(vn(vec2(uv.x * 26. * zz * .15, zz * 6. - uTime * .5)), vn(vec2(uv.x * 9. * zz * .15 + 4., zz * 3.1 + uTime * .3))) - .5;\n    vec2 m = vec2(uv.x + w.x * .005 * dep, 2. * uHz - uv.y + w.y * .018 * (.15 + dep));\n    float fres = mix(.9, .32, pow(dep, .7));\n    col = sky(m, asp) * fres;\n    col += uLow * .008 * uK * (1. - dep);\n    // low mist over the water near the far shore\n    col += uLow * .012 * uK * exp(-dep * 9.) * (.5 + fbm(vec2(uv.x * 4. + uTime * .01, dep * 6.)));\n  }\n  col = aces(col * 1.3);\n  col = toSRGB(col);\n  vec2 v = abs(uv - .5) * 2.; col *= 1. - .28 * smoothstep(0., 1., length(max(v - .45, 0.)) / .75);\n  col += (h12(gl_FragCoord.xy + floor(uFrame) * vec2(17.3, 41.9)) + h12(gl_FragCoord.yx + floor(uFrame) * 7.7) - 1.) * .016;\n  o = vec4(clamp(col, 0., 1.), 1.);\n}`;\n  document.querySelectorAll('.vs-aurora:not(.vs-aurora-ready)').forEach((root) => {\n    let pc, pa, pv, aur, curt, fresh = 0;\n    const render = (S, passes) => {\n      const gl = S.gl, P = S.ptr, pan = (P.x - .5) * .5 + S.t * .006, tilt = (P.y - .5) * .03, hz = Math.max(0, Math.min(.8, S.css.horizon));\n      S.draw(pc, curt, { uTime: S.t });\n      // each new march is blended over the last (constant alpha): the jitter averages out into a smooth glow\n      gl.enable(gl.BLEND); gl.blendFunc(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA);\n      for (let i = 0; i < passes; i++) {\n        gl.blendColor(0, 0, 0, passes > 1 ? 1 / (i + 1) : (fresh < 2 ? 1 : .25));\n        S.draw(pa, aur, { uRes: [aur.w, aur.h], uPan: pan, uTilt: tilt, uHz: hz, uFrame: (S.frame + i * 7) % 64, uAsp: S.cssW / S.cssH, uLow: S.css.low, uHigh: S.css.high, uHem: S.css.hem }, { uC: curt });\n      }\n      gl.disable(gl.BLEND); fresh++;\n      S.draw(pv, null, { uRes: [S.w, S.h], uTime: S.t, uHz: hz, uK: S.css.intensity, uPan: pan, uFrame: Math.floor(S.rt * 24), uSky: S.css.sky, uLow: S.css.low }, { uAur: aur });\n    };\n    VS(root, {\n      prefix: 'vs-aurora', t0: 40,\n      vars: { low: '#38f2a4', high: '#2ab4d9', hem: '#ff5c9d', sky: '#0a1424', intensity: 1, speed: 1, horizon: .3 },\n      ptrSpring: [40, 14],\n      /* the aurora is soft: march it at ~half the CSS resolution whatever the DPR */\n      init(S) { pc = S.program(CURT); pa = S.program(AUR); pv = S.program(VIEW); aur = null; curt = S.target(4096, 6); fresh = 0; },\n      resize(S) { S.free(aur); aur = S.target(Math.max(2, Math.round(S.cssW * .5)), Math.max(2, Math.round(S.cssH * .5)), { mip: true }); fresh = 0; },\n      frame(S) { render(S, 1); },\n      /* reduced motion: one still, the jittered march averaged over 16 passes so it is smooth */\n      still(S) { render(S, 16); },\n    });\n  });\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "3d",
    "group": "Shader backgrounds",
    "tags": [
      "aurora",
      "northern lights",
      "raymarch",
      "volumetric",
      "night sky",
      "reflection",
      "lake",
      "stars",
      "background",
      "webgl"
    ],
    "tech": [
      "WebGL2",
      "GLSL",
      "JS"
    ],
    "deps": [],
    "source": "hraun",
    "free": true,
    "accent": "#38f2a4",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/aurora/index.html",
    "thumbnail": "https://vividsites.app/api/component/aurora/thumb.jpg",
    "video": "https://vividsites.app/api/component/aurora/thumb.mp4",
    "page": "https://vividsites.app/c/aurora"
  }
}