{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "warp-starfield",
  "type": "registry:block",
  "title": "Warp Starfield",
  "description": "A starfield that jumps to hyperspace: hold, scroll or swipe and a velocity spring stretches thousands of instanced stars into blue-tailed streaks, with a zoom blur, a chromatic split and a streaming tunnel at full warp. Let go and it coasts back to soft, glowing points.",
  "docs": "Warp Starfield was added to public/vividsites/warp-starfield.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/warp-starfield",
  "categories": [
    "3d",
    "shader-backgrounds"
  ],
  "files": [
    {
      "path": "components/vividsites/warp-starfield.html",
      "type": "registry:file",
      "target": "public/vividsites/warp-starfield.html",
      "content": "<!--\n  vs-warp-starfield — a starfield that drops into hyperspace.\n  Thousands of stars are GPU-instanced streaks whose length follows the ship's speed. At cruise\n  they drift as soft points of different temperatures; press and hold anywhere (or scroll, spin\n  the wheel, or swipe on a phone) and a velocity spring winds the speed up: the streaks stretch\n  from blue tails to white heads, a zoom blur and a chromatic split open around the vanishing\n  point, and at full warp a mottled hyperspace tunnel streams past. Let go and it coasts down on\n  a softer spring. The pointer steers the vanishing point a little.\n\n  Fills its nearest positioned parent (position:absolute; inset:0). Put content above it.\n  CSS variables (set on .vs-warp-starfield):\n    --vs-warp-starfield-bg       deep space colour\n    --vs-warp-starfield-trail    tint of the streak tails and the tunnel at warp\n    --vs-warp-starfield-core     colour of the glow at the vanishing point\n    --vs-warp-starfield-nebula   faint nebula tint (set it to the bg colour to turn it off)\n    --vs-warp-starfield-density  star count, 1 = default (0.2–2.5)\n    --vs-warp-starfield-speed    cruise speed, 1 = default (0 = hang still)\n    --vs-warp-starfield-max      top speed at full warp, 1 = default\n    --vs-warp-starfield-streak   streak length at warp, 1 = default\n  JS: el.dispatchEvent(new CustomEvent('vs-warp-starfield:warp', {detail:{on:true}})) to engage from your own button;\n      el.vsWarp = { speed } (0 = cruise … 1 = full warp) for readouts.\n  Set data-press=\"off\" on the element to stop press-anywhere / scroll from engaging warp.\n-->\n<div class=\"vs-warp-starfield\" aria-hidden=\"true\"><canvas class=\"vs-warp-starfield-canvas\"></canvas></div>\n<style>\n  .vs-warp-starfield {\n    --vs-warp-starfield-bg: #020309;\n    --vs-warp-starfield-trail: #5b7cff;\n    --vs-warp-starfield-core: #cfe0ff;\n    --vs-warp-starfield-nebula: #0f2a44;\n    --vs-warp-starfield-density: 1;\n    --vs-warp-starfield-speed: 1;\n    --vs-warp-starfield-max: 1;\n    --vs-warp-starfield-streak: 1;\n    position: absolute; inset: 0; overflow: hidden; pointer-events: none;\n    background: radial-gradient(1px 1px at 20% 30%, #fff, transparent), radial-gradient(1px 1px at 70% 60%, #cfe0ff, transparent), radial-gradient(1px 1px at 40% 80%, #fff, transparent), radial-gradient(1.5px 1.5px at 85% 20%, #fff, transparent), radial-gradient(60% 60% at 50% 50%, color-mix(in oklab, var(--vs-warp-starfield-nebula) 50%, transparent), transparent), var(--vs-warp-starfield-bg);\n  }\n  .vs-warp-starfield-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; opacity: 0; transition: opacity 1s cubic-bezier(.16, 1, .3, 1); }\n  .vs-warp-starfield-ready .vs-warp-starfield-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  /* one instance per star: a quad stretched from its tail (further away) to its head */\n  const STAR_V = `#version 300 es\nprecision highp float;\nuniform vec2 uRes, uVp; uniform float uDist, uStreak, uWarp, uPx, uTime;\nout vec2 vQ; out vec2 vS; out float vB; out vec3 vCol; out float vW; flat out vec3 vG;\nvec3 h3(float n){ vec3 p = fract(vec3(n) * vec3(.1031, .1030, .0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }\nconst vec2 Q[6] = vec2[6](vec2(0, -1), vec2(1, -1), vec2(0, 1), vec2(0, 1), vec2(1, -1), vec2(1, 1));\nvoid main(){\n  float id = float(gl_InstanceID);\n  vec3 h = h3(id + .5), h2 = h3(id * 1.37 + 91.1);\n  float asp = uRes.x / uRes.y;\n  // a box of stars around the flight path, emptied along the axis so nothing flies into the lens\n  vec2 xy = (h.xy * 2. - 1.) * vec2(asp, 1.) * 1.3;\n  if (length(xy) < .07) xy = normalize(xy + 1e-3) * (.07 + h2.x * .1);\n  float z = fract(h.z - uDist);\n  float zh = mix(.02, 1., z);\n  float zt = zh + uStreak * (.55 + .9 * h2.y);\n  vec2 head = xy / zh * .5 + uVp, tail = xy / zt * .5 + uVp;\n  vec2 H = head * uRes.y, T = tail * uRes.y;\n  vec2 d = H - T; float len = length(d); vec2 dir = len > 1e-4 ? d / len : vec2(1., 0.);\n  // a power-law of magnitudes: most stars faint, a few bright ones\n  float mag = pow(h2.x, 4.);\n  float w = clamp((.7 + mag * 2.2) / zh * .11, .9, 3.4 + mag * 3.) * uPx;\n  // bright stars get a soft halo at cruise: the quad grows to hold it\n  float halo = (.15 + mag * 2.) * (1. - smoothstep(0., .12, uWarp));\n  float wq = w * (1. + 5. * halo);\n  T -= dir * wq; H += dir * wq; float L = len + 2. * wq;\n  vec2 q = Q[gl_VertexID];\n  vec2 P = mix(T, H, q.x) + vec2(-dir.y, dir.x) * wq * q.y;\n  gl_Position = vec4((P / uRes.y) / vec2(asp * .5, .5), 0., 1.);\n  vQ = q; vW = wq / max(L, 1.);\n  vS = vec2(q.x * L - wq, q.y * wq);        // pixels: along from the tail's end cap, across\n  vG = vec3(len, w, halo);\n  float fade = smoothstep(1., .8, z) * smoothstep(.0, .05, zh - .02);\n  float tw = 1. + .25 * sin(uTime * (2. + h.x * 5.) + id) * (1. - uWarp);\n  float bright = clamp(.1 / zh, 0., 3.) * (.35 + 2.4 * mag + .4 * h2.z) * tw;\n  // spread the energy along long streaks so they don't flare; a touch brighter overall at warp\n  vB = fade * bright * mix(1., clamp(12. * w / len, .6, 1.), uWarp) * (1. + uWarp * 1.1);\n  // colour temperature: blue-white to warm\n  vCol = mix(vec3(.66, .78, 1.), vec3(1., .86, .7), h2.y * h2.y);\n}`;\n  const STAR_F = `uniform vec3 uTrail; uniform float uWarp; in vec2 vQ; in vec2 vS; in float vB; in vec3 vCol; in float vW; flat in vec3 vG; out vec4 o;\nvoid main(){\n  // distance to the streak's centre segment, in core widths: a round core and a soft halo\n  float ds = max(0., max(-vS.x, vS.x - vG.x));\n  float d = length(vec2(ds, vS.y)) / vG.y;\n  float a = exp(-d * d * 1.6) + vG.z * .09 / (1. + d * d * .35) * (1. - smoothstep(1., 1. + 5. * vG.z, d));\n  float along = smoothstep(0., .92, vQ.x);\n  vec3 c = mix(uTrail * 1.1, vCol * 1.25, smoothstep(.45, 1., vQ.x));\n  c = mix(vCol, c, smoothstep(0., .5, uWarp));\n  o = vec4(c * a * mix(1., along, clamp(uWarp * 3., 0., 1.)) * vB, 1.);\n}`;\n  /* space, the stars, and the warp: zoom blur, chromatic split, the tunnel and the core, graded */\n  const VIEW = `\nuniform sampler2D uStars; uniform vec2 uRes, uVp; uniform float uWarp, uTime, uDist, uFrame, uFlash; uniform vec3 uBg, uNeb, uCore, uTrail; out vec4 o;\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); }\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); }\n// value noise that wraps every N cells in x (for the tunnel's angle)\nfloat vnw(vec2 p, float N){ vec2 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); float i0 = mod(i.x, N), i1 = mod(i.x + 1., N);\n  return mix(mix(h12(vec2(i0, i.y)), h12(vec2(i1, i.y)), f.x), mix(h12(vec2(i0, i.y + 1.)), h12(vec2(i1, i.y + 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.02 + 7.3; a *= .5; } return s; }\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.); }\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes; float asp = uRes.x / uRes.y;\n  vec2 vp = uVp * vec2(1. / asp, 1.) + .5;\n  vec2 rel = (uv - vp) * vec2(asp, 1.); float r = length(rel);\n  // nebula: a slow, faint cloud that swells as we pass through it\n  vec2 np = rel * (1.3 - uWarp * .45) + vec2(uTime * .008, 0.);\n  float n = fbm(np * 1.5 + 3.) * fbm(np * 3.1 - 1.7);\n  vec3 col = uBg + uNeb * smoothstep(.22, .6, n) * .3 * (1. - uWarp * .5) + uNeb * .06 * exp(-r * 1.5);\n  // the stars, zoom-blurred toward the vanishing point as the speed rises\n  vec3 sharp = texture(uStars, uv).rgb;\n  vec3 st = sharp;\n  if (uWarp > .02){\n    vec3 acc = vec3(0.); float ws = 0.; float k = uWarp * .035;\n    float j = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(.06711056, .00583715))));\n    for (int i = 0; i < 9; i++){\n      float fi = (float(i) + j) / 9.; float s = 1. - fi * k; float w = 1. - fi * .75;\n      vec2 u = vp + (uv - vp) * s; vec2 ca = (uv - vp) * .01 * uWarp;\n      acc += vec3(texture(uStars, u + ca).r, texture(uStars, u).g, texture(uStars, u - ca).b) * w; ws += w;\n    }\n    acc /= ws;\n    st = mix(sharp, max(acc, sharp * .9), smoothstep(.02, .3, uWarp));\n  }\n  vec3 glow = textureLod(uStars, uv, 3.).rgb * .5 + textureLod(uStars, uv, 5.).rgb * .6;\n  col += st + glow * (.22 + uWarp * .1);\n  // the tunnel: streaming walls of light around the flight path at full warp\n  float tw = smoothstep(.45, 1., uWarp);\n  if (tw > 0.){\n    float ang = atan(rel.y, rel.x) / 6.2831853 + .5;\n    float depth = .35 / max(r, .02);\n    float N = 96.;\n    float m = vnw(vec2(ang * N, depth * 1.2 - uDist * 2.4), N) * .65 + vnw(vec2(ang * N * 2., depth * 3. - uDist * 5.), N * 2.) * .35;\n    m = smoothstep(.5, 1., m); m *= m;\n    float wall = smoothstep(.15, .6, r) * exp(-r * 1.2);\n    col += mix(uTrail, uCore, .3) * m * wall * tw * .22;\n  }\n  // a cold sheath along the streaks and the core glow\n  col += uTrail * .035 * uWarp * smoothstep(.1, .8, r) * (1. - smoothstep(.8, 1.6, r));\n  col += uCore * (exp(-r * mix(26., 18., uWarp)) * (.05 + uWarp * .2) + exp(-r * 90.) * uWarp * .8);\n  col += uCore * uFlash * exp(-r * 3.) * .3;\n  col = aces(col * 1.15);\n  col = toSRGB(col);\n  vec2 v = abs(uv - .5) * 2.; col *= 1. - (.32 + uWarp * .2) * smoothstep(0., 1., length(max(v - .35, 0.)) / .85);\n  col += (h12(gl_FragCoord.xy + floor(uFrame) * vec2(17.3, 41.9)) - .5) * .016;\n  o = vec4(clamp(col, 0., 1.), 1.);\n}`;\n  document.querySelectorAll('.vs-warp-starfield:not(.vs-warp-starfield-ready)').forEach((root) => {\n    let ps, pv, rt, count = 2400, dist = 0, spd = 0, vel = 0, boost = 0, on = false, flash = 0, jumped = false, lastY = scrollY, touchY = null;\n    const vp = { x: 0, y: 0 };\n    const recount = (S) => { count = Math.round((S.cssW < 700 ? 1900 : 3600) * Math.max(.1, S.css.density || 1)); };\n    const S = VS(root, {\n      prefix: 'vs-warp-starfield', t0: 0,\n      vars: { bg: '#020309', trail: '#5b7cff', core: '#cfe0ff', nebula: '#0f2a44', density: 1, speed: 1, max: 1, streak: 1 },\n      ptrSpring: [26, 11],\n      onVars: recount,\n      init(S) { ps = S.program(STAR_F, STAR_V); pv = S.program(VIEW); },\n      resize(S) { S.free(rt); rt = S.target(S.w, S.h, { mip: true }); recount(S); },\n      press(S, down) { if (root.dataset.press !== 'off') engage(down); },\n      frame(S, dt) {\n        const gl = S.gl, P = S.ptr;\n        // the ship's speed: a velocity spring toward cruise or warp, plus whatever scrolling adds.\n        // winding up is firm; coasting down is softer and fully damped, so it never bounces below cruise\n        const target = Math.min(1.15, (on ? 1 : 0) + Math.min(.7, boost));\n        const up = target > spd, k = up ? 11 : 3.2, c = up ? 5.6 : 3.6;\n        const n = Math.ceil(dt / (1 / 120));\n        for (let i = 0; i < n; i++) { const h = dt / n; vel += ((target - spd) * k - vel * c) * h; spd += vel * h; }\n        if (spd < 0) { spd = 0; vel = Math.max(0, vel); }\n        boost *= Math.exp(-dt * 1.8); flash *= Math.exp(-dt * 4.5);\n        const w = Math.min(1, spd);\n        if (w > .9 && !jumped) { jumped = true; flash = Math.max(flash, .7); } else if (w < .5) jumped = false;\n        const cruise = .045 * S.css.speed, top = 1.45 * S.css.max;\n        dist += (cruise + (top - cruise) * w * w) * dt;\n        // steer: the vanishing point leans toward the pointer\n        const tx = P.seen ? (P.x - .5) * .16 * S.aspect : 0, ty = P.seen ? (P.y - .5) * .12 : 0;\n        vp.x += (tx - vp.x) * Math.min(1, dt * 2.5); vp.y += (ty - vp.y) * Math.min(1, dt * 2.5);\n        root.vsWarp = { speed: w };\n        const streak = (.0003 + w * w * .44) * Math.max(0, S.css.streak);\n        gl.bindFramebuffer(gl.FRAMEBUFFER, rt.fb); gl.viewport(0, 0, rt.w, rt.h); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT);\n        gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE); gl.useProgram(ps.p);\n        const u = ps.u;\n        gl.uniform2f(u.uRes, S.w, S.h); gl.uniform2f(u.uVp, vp.x, vp.y); gl.uniform1f(u.uDist, dist); gl.uniform1f(u.uStreak, streak); gl.uniform1f(u.uWarp, w); gl.uniform1f(u.uPx, S.dpr); gl.uniform1f(u.uTime, S.rt);\n        gl.uniform3fv(u.uTrail, S.css.trail);\n        gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);\n        gl.disable(gl.BLEND);\n        gl.bindTexture(gl.TEXTURE_2D, rt.t); gl.generateMipmap(gl.TEXTURE_2D);\n        S.draw(pv, null, { uRes: [S.w, S.h], uVp: [vp.x, vp.y], uWarp: w, uTime: S.rt, uDist: dist, uFrame: Math.floor(S.rt * 24), uFlash: flash, uBg: S.css.bg, uNeb: S.css.nebula, uCore: S.css.core, uTrail: S.css.trail }, { uStars: rt });\n      },\n    });\n    if (!S) return;\n    function engage(down) { if (down && !on) flash = Math.max(flash, .35); on = down; }\n    const live = () => root.dataset.press !== 'off';\n    root.addEventListener('vs-warp-starfield:warp', (e) => engage(!!(e.detail && e.detail.on)));\n    addEventListener('wheel', (e) => { if (live()) boost = Math.min(1.2, boost + Math.min(120, Math.abs(e.deltaY)) * .0016); }, { passive: true });\n    addEventListener('scroll', () => { if (live()) boost = Math.min(1.2, boost + Math.abs(scrollY - lastY) * .002); lastY = scrollY; }, { passive: true });\n    // a swipe on a phone adds speed like a scroll does\n    addEventListener('touchstart', (e) => { touchY = e.touches[0] ? e.touches[0].clientY : null; }, { passive: true });\n    addEventListener('touchmove', (e) => { const t = e.touches[0]; if (!t || touchY == null || !live()) return; boost = Math.min(1.2, boost + Math.abs(t.clientY - touchY) * .003); touchY = t.clientY; }, { passive: true });\n    addEventListener('blur', () => engage(false));\n  });\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "3d",
    "group": "Shader backgrounds",
    "tags": [
      "starfield",
      "hyperspace",
      "warp",
      "space",
      "stars",
      "streaks",
      "scroll",
      "press",
      "background",
      "webgl"
    ],
    "tech": [
      "WebGL2",
      "GLSL",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#5b7cff",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/warp-starfield/index.html",
    "thumbnail": "https://vividsites.app/api/component/warp-starfield/thumb.jpg",
    "video": "https://vividsites.app/api/component/warp-starfield/thumb.mp4",
    "page": "https://vividsites.app/c/warp-starfield"
  }
}