{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rain-glass",
  "type": "registry:block",
  "title": "Rain Glass",
  "description": "Rain on a window at night over an out-of-focus city: beads and runners that each act as a lens onto the sharp street, condensation that the pointer wipes away (taking the beads with it) and slowly breathes back. Sodium, neon or dusk city light, with adjustable rain, blur and fog.",
  "docs": "Rain Glass was added to public/vividsites/rain-glass.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/rain-glass",
  "categories": [
    "3d",
    "shader-backgrounds"
  ],
  "files": [
    {
      "path": "components/vividsites/rain-glass.html",
      "type": "registry:file",
      "target": "public/vividsites/rain-glass.html",
      "content": "<!--\n  vs-rain-glass — rain on a window at night, the city behind it out of focus.\n  The street is drawn in code twice, once sharp and once as bokeh (sodium lamps, lit windows, a\n  row of traffic whose tail lights drift one way and headlights the other). Drops bead on the\n  glass, run, and leave trails; every drop is a lens that shows the street sharp and upside down,\n  with a bright rim and a glint. Condensation fogs the pane; the running drops clear lines through\n  it, and the pointer (or a finger) wipes a streak that takes the beads with it. The fog breathes\n  back in patches, and the beads re-form a few seconds later.\n\n  Fills its nearest positioned parent (position:absolute; inset:0). Put content above it.\n  CSS variables (set on .vs-rain-glass):\n    --vs-rain-glass-sky       the night sky above the street\n    --vs-rain-glass-glow      the street-level glow (sodium)\n    --vs-rain-glass-warm      warm light colour (lamps, windows)\n    --vs-rain-glass-cool      cool light colour (LEDs, headlights)\n    --vs-rain-glass-signal    tail lights and the one neon sign\n    --vs-rain-glass-rain      how hard it rains 0–1 (default .7)\n    --vs-rain-glass-fog       how much condensation 0–1 (default .6)\n    --vs-rain-glass-blur      how far out of focus the street is 0–1 (default 1)\n    --vs-rain-glass-speed     time speed, 1 = default\n  Set data-wipe=\"off\" on the element to stop the pointer wiping the glass.\n-->\n<div class=\"vs-rain-glass\" aria-hidden=\"true\"><canvas class=\"vs-rain-glass-canvas\"></canvas></div>\n<style>\n  .vs-rain-glass {\n    --vs-rain-glass-sky: #070a12;\n    --vs-rain-glass-glow: #d9773a;\n    --vs-rain-glass-warm: #ffb45c;\n    --vs-rain-glass-cool: #bcd6ff;\n    --vs-rain-glass-signal: #ff3b4e;\n    --vs-rain-glass-rain: .7;\n    --vs-rain-glass-fog: .6;\n    --vs-rain-glass-blur: 1;\n    --vs-rain-glass-speed: 1;\n    position: absolute; inset: 0; overflow: hidden; pointer-events: none;\n    background: radial-gradient(8% 12% at 22% 58%, color-mix(in oklab, var(--vs-rain-glass-warm) 55%, transparent), transparent), radial-gradient(10% 14% at 70% 48%, color-mix(in oklab, var(--vs-rain-glass-signal) 45%, transparent), transparent), radial-gradient(90% 50% at 50% 100%, color-mix(in oklab, var(--vs-rain-glass-glow) 45%, transparent), transparent), var(--vs-rain-glass-sky);\n  }\n  .vs-rain-glass-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; opacity: 0; transition: opacity 1.2s cubic-bezier(.16, 1, .3, 1); }\n  .vs-rain-glass-ready .vs-rain-glass-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 HASH = `\nfloat h11(float p){ p = fract(p * .1031); p *= p + 33.33; p *= p + p; return fract(p); }\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); }\nvec3 h13(float p){ vec3 a = fract(vec3(p) * vec3(.1031, .11369, .13787)); a += dot(a, a.yzx + 19.19); return fract(vec3((a.x + a.y) * a.z, (a.x + a.z) * a.y, (a.y + a.z) * a.x)); }\nvec3 h32(vec2 p){ vec3 a = fract(vec3(p.xyx) * vec3(.1031, .103, .0973)); a += dot(a, a.yxz + 33.33); return fract((a.xxy + a.yzz) * a.zyx); }\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  /* the street, as lights: blur 0 = sharp points, 1 = full bokeh discs (energy kept constant) */\n  const CITY = `${HASH}\nuniform vec2 uRes; uniform float uTime, uBlur; uniform vec3 uSky, uGlow, uWarm, uCool, uSignal; out vec4 o;\n// a light: a disc whose edge is a touch brighter, like a lens's bokeh\nvec3 lamp(vec2 d, float r0, vec3 c){\n  float r = r0 + uBlur * .034; float l = length(d);\n  float disc = smoothstep(r, r * mix(.2, .82, uBlur), l);\n  float rim = smoothstep(r * .6, r * .95, l) * disc * .5 * uBlur;\n  float core = exp(-l * l / (r0 * r0) * 2.) * (1. - uBlur);\n  return c * ((disc + rim) * pow(r0 / r, 2.) * mix(.35, 1., uBlur) + core * 1.4);\n}\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes; float asp = uRes.x / uRes.y;\n  vec2 p = vec2(uv.x * asp, uv.y); float t = uTime;\n  // sky to street: dark overhead, a thin sodium haze at street level, a wet road below it\n  vec3 col = mix(uGlow * .1, uSky, smoothstep(.1, .55, uv.y));\n  col += uGlow * .13 * exp(-pow((uv.y - .33) * 7., 2.));\n  col *= mix(.55, 1., smoothstep(.0, .3, uv.y));\n  // building masses: soft darker blocks against the haze\n  float bx = floor(p.x * 3.2 + 7.); float bh = .5 + h11(bx) * .35;\n  col *= 1. - .35 * smoothstep(bh + .02, bh - .02, uv.y) * smoothstep(.3, .36, uv.y);\n  // lit windows: a grid, a few on, warm and cool\n  {\n    vec2 g = p * vec2(26., 30.); vec2 id = floor(g); vec2 f = fract(g) - .5;\n    for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++){\n      vec2 c = id + vec2(i, j); vec3 h = h32(c);\n      float y = (c.y + .5) / 30.;\n      if (h.x > .8 && y > .36 && y < .95){\n        float on = step(.35, sin(t * .05 + h.y * 40.) * .5 + .5);\n        vec3 wc = mix(uWarm, uCool, step(.72, h.z)) * (.25 + h.y * .5) * on;\n        col += lamp((f - vec2(i, j) - (h.yz - .5) * .3) / vec2(26., 30.) * vec2(1., 1.), .004, wc);\n      }\n    }\n  }\n  // street lamps: a sodium row, a little uneven\n  {\n    float cell = .19; float cx = floor(p.x / cell);\n    for (int i = -1; i <= 1; i++){\n      float c = cx + float(i); vec3 h = h13(c * 7.1 + 3.);\n      vec2 lp = vec2((c + .3 + h.x * .4) * cell, .5 + h.y * .06);\n      col += lamp(p - lp, .007 + h.z * .004, uWarm * (1.4 + h.z));\n      // its pool on the wet road below\n      col += uWarm * .07 * exp(-pow((p.x - lp.x) * 30., 2.)) * smoothstep(.34, .05, uv.y) * (.6 + .4 * vn(vec2(p.x * 80., uv.y * 6.)));\n    }\n  }\n  // traffic: tail lights drifting right, headlights left, reflections streaked into the road\n  for (int lane = 0; lane < 2; lane++){\n    float fl = float(lane); float dir = lane == 0 ? 1. : -1.;\n    float cell = .23 + fl * .06; float x = p.x - dir * t * (.05 + fl * .02);\n    float cx = floor(x / cell);\n    for (int i = -1; i <= 1; i++){\n      float c = cx + float(i); vec3 h = h13(c * 13.7 + fl * 51.);\n      if (h.x < .35) continue;\n      float lx = (c + .5 + (h.y - .5) * .5) * cell; float ly = .22 + fl * .05 + h.z * .01;\n      vec3 lc = lane == 0 ? uSignal * 1.3 : uCool * 1.1;\n      vec2 d = vec2(x - lx, uv.y - ly);\n      col += lamp(d - vec2(.012, 0.), .005, lc) + lamp(d + vec2(.012, 0.), .005, lc);\n      col += lc * .05 * exp(-pow(d.x * 50., 2.)) * smoothstep(ly, ly - .2, uv.y) * smoothstep(0., .08, uv.y);\n    }\n  }\n  // a near layer: a few large, very soft discs (lights much closer than the street)\n  {\n    float cell = .34; vec2 g = p / cell; vec2 id = floor(g);\n    for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++){\n      vec2 c = id + vec2(i, j); vec3 h = h32(c + 40.);\n      if (h.x < .55) continue;\n      vec2 lp = (c + .2 + h.yz * .6) * cell;\n      vec3 lc = h.z > .6 ? uCool : (h.y > .7 ? uSignal : uWarm);\n      float r = (.03 + h.y * .03) * mix(.25, 1., uBlur);\n      col += lc * .09 * smoothstep(r, r * .7, length(p - lp)) * (.5 + h.z);\n    }\n  }\n  // one neon sign, and its bloom on the building\n  vec2 ns = p - vec2(asp * .72, .6);\n  float sign = smoothstep(.02 + uBlur * .02, 0., abs(length(ns * vec2(1., 1.6)) - .03)) * (.8 + .2 * sin(t * 9.) * step(.97, h11(floor(t * 3.))));\n  col += uSignal * sign * mix(1.4, .5, uBlur) + uSignal * .15 * exp(-length(ns) * 9.);\n  o = vec4(col, 1.);\n}`;\n  /* the glass: drops (static beads + two layers of runners with trails), condensation, the city through both */\n  const GLASS = `${HASH}\nuniform sampler2D uSharp, uBokeh, uFog; uniform vec2 uRes; uniform float uTime, uRain, uFogK, uFrame, uDpr; out vec4 o;\n#define S(a, b, t) smoothstep(a, b, t)\nfloat saw(float b, float t){ return S(0., b, t) * S(1., b, t); }\nvec2 runners(vec2 uv, float t){\n  vec2 UV = uv; uv.y += t * .75;\n  vec2 a = vec2(6., 1.), grid = a * 2.;\n  vec2 id = floor(uv * grid); uv.y += h11(id.x * 3.7 + 1.);\n  id = floor(uv * grid);\n  vec3 n = h13(id.x * 35.2 + id.y * 2376.1);\n  vec2 st = fract(uv * grid) - vec2(.5, 0.);\n  float x = n.x - .5, y = UV.y * 20.;\n  x += sin(y + sin(y)) * (.5 - abs(x)) * (n.z - .5); x *= .7;\n  float ti = fract(t + n.z);\n  y = (saw(.85, ti) - .5) * .9 + .5;\n  vec2 p = vec2(x, y);\n  float d = length((st - p) * a.yx);\n  float drop = S(.4, 0., d);\n  float r = sqrt(S(1., y, st.y));\n  float cd = abs(st.x - x);\n  float trail = S(.23 * r, .15 * r * r, cd);\n  float front = S(-.02, .02, st.y - y);\n  trail *= front * r * r;\n  y = fract(UV.y * 10.) + (st.y - .5);\n  float beads = S(.3, 0., length(st - vec2(x, y)));\n  return vec2(drop + beads * r * front, trail);\n}\nfloat beads(vec2 uv, float t){\n  uv *= 40.; vec2 id = floor(uv); uv = fract(uv) - .5;\n  vec3 n = h13(id.x * 107.45 + id.y * 3543.654);\n  vec2 p = (n.xy - .5) * .7; float d = length(uv - p);\n  return S(.3, 0., d) * fract(n.z * 10.) * saw(.025, fract(t + n.z));\n}\nfloat gBead;\nvec2 drops(vec2 uv, float t, float l0, float l1, float l2){\n  float s = beads(uv, t) * l0 * gBead; vec2 m1 = runners(uv, t) * l1, m2 = runners(uv * 1.85, t) * l2;\n  return vec2(S(.3, 1., s + m1.x + m2.x), max(m1.y * l0, m2.y * l1));\n}\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;\n  vec2 uv = (gl_FragCoord.xy - .5 * uRes) / uRes.y;\n  float t = uTime * .2;\n  float rain = uRain;\n  float l0 = S(-.5, 1., rain) * 2., l1 = S(.25, .75, rain), l2 = S(0., .5, rain);\n  vec4 fg = texture(uFog, UV);\n  gBead = fg.g;\n  vec2 c = drops(uv, t, l0, l1, l2);\n  vec2 e = vec2(.0012, 0.);\n  float cx = drops(uv + e, t, l0, l1, l2).x, cy = drops(uv + e.yx, t, l0, l1, l2).x;\n  vec2 n = vec2(cx - c.x, cy - c.x);\n  // condensation: the stored field, cleared along the trails and under each drop\n  float fog = fg.r * uFogK;\n  fog *= (1. - S(.0, .25, c.y)) * (1. - S(.05, .3, c.x));\n  // clear glass shows the bokeh; each drop is a lens onto the street, sharp and upside down\n  vec3 bok = texture(uBokeh, UV + n * .5).rgb;\n  float lens = S(.1, .35, c.x);\n  // a drop gathers light from a wide cone: the sharp street, flipped, over a little of its glow\n  vec2 lu = UV + n * 2.2 - vec2(0., .045) * lens;\n  vec3 sharp = texture(uSharp, lu).rgb * 1.4 + textureLod(uBokeh, lu, 2.).rgb * .6;\n  vec3 col = mix(bok, sharp, lens);\n  // a thin specular crescent on each drop's upper shoulder\n  col += vec3(.9, .92, 1.) * S(.3, .6, c.x) * (1. - S(.6, .95, c.x)) * S(.015, .04, n.y) * .12;\n  // the trails: a thin film, slightly sharper than the pane\n  col = mix(col, texture(uBokeh, UV + n * .5, -1.).rgb * .9 + texture(uSharp, UV).rgb * .25, S(.1, .5, c.y) * (1. - lens) * .5);\n  // drop rims catch the light behind them; the upper edge goes dark\n  float rim = S(.12, .3, c.x) * (1. - S(.3, .7, c.x));\n  col *= 1. - rim * .35 * S(0., .015, n.y);\n  col += textureLod(uBokeh, UV + vec2(0., .04), 3.).rgb * S(.15, .45, c.x) * S(.0, .02, -n.y) * .9;\n  // a glint where the drop's upper-left shoulder catches the brightest light behind the glass\n  col += vec3(1., .95, .88) * S(.5, .95, c.x) * S(.012, .03, dot(n, vec2(.7, -.7))) * min(.45, dot(textureLod(uBokeh, UV, 5.).rgb, vec3(.6)) * 2.5);\n  // fog: the scattered street, lifted to a cold milk, with a fine grain of micro-droplets\n  vec3 milk = textureLod(uBokeh, UV, 4.).rgb * 1.1 + textureLod(uBokeh, UV, 6.).rgb * .7 + vec3(.022, .025, .03);\n  // micro-droplets: an irregular stipple, sized in CSS pixels so it reads the same at any DPR\n  vec2 g = gl_FragCoord.xy / (3.4 * uDpr); vec2 gi = floor(g); vec3 mh = h32(gi);\n  float md = S(.2 + mh.z * .25, .0, length(fract(g) - .5 - (mh.xy - .5) * .6)) * step(.35, mh.z);\n  milk *= 1. + md * .3 - .06;\n  col = mix(col, milk, S(0., 1., fog) * .92);\n  col = aces(col * 1.25);\n  col = toSRGB(col);\n  float l = dot(col, vec3(.2126, .7152, .0722)); col = mix(vec3(l), col, 1.12);\n  vec2 v = abs(UV - .5) * 2.; col *= 1. - .3 * S(0., 1., length(max(v - .45, 0.)) / .75);\n  col += (h12(gl_FragCoord.xy + floor(uFrame) * vec2(17.3, 41.9)) - .5) * .02;\n  o = vec4(clamp(col, 0., 1.), 1.);\n}`;\n  /* the condensation field: breathes back toward full, wiped along the pointer's path */\n  const FOG = `\nuniform sampler2D uPrev; uniform vec2 uRes, uA, uB; uniform float uDt, uWipe, uInit, uAsp; out vec4 o;\nfloat sdSeg(vec2 p, vec2 a, vec2 b){ vec2 pa = p - a, ba = b - a; float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0., 1.); return length(pa - ba * h); }\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); }\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 fb = uInit > .5 ? vec2(1.) : texture(uPrev, uv).rg;\n  float f = fb.x, b = fb.y;\n  // it regrows unevenly: patches first, the edges of the pane faster\n  vec2 e = abs(uv - .5) * 2.;\n  float target = clamp(.72 + .28 * smoothstep(.55, 1., max(e.x, e.y)) + (vn(uv * vec2(uAsp, 1.) * 5.) - .5) * .3, 0., 1.);\n  f = mix(f, target, 1. - exp(-uDt * (.12 + .1 * vn(uv * 9.))));\n  // beads re-form slowly, in speckles\n  b = min(1., b + uDt * (.12 + .2 * h12(floor(uv * uRes))));\n  if (uWipe > 0.){\n    vec2 A = vec2(uAsp, 1.);\n    float d = sdSeg(uv * A, uA * A, uB * A);\n    // a finger leaves streaks, not a clean stencil\n    float streak = vn(vec2(dot(uv * A, normalize((uB - uA) * A + 1e-5).yx * vec2(-1., 1.)) * 180., .5));\n    float w = smoothstep(.055, .025, d) * uWipe;\n    f = mix(f, .04 + .12 * streak, w);\n    b = mix(b, 0., w);\n  }\n  o = vec4(f, b, 0., 1.);\n}`;\n  document.querySelectorAll('.vs-rain-glass:not(.vs-rain-glass-ready)').forEach((root) => {\n    let pc, pg, pf, sharp, bokeh, fog, init = 1;\n    VS(root, {\n      prefix: 'vs-rain-glass', t0: 20,\n      vars: { sky: '#070a12', glow: '#d9773a', warm: '#ffb45c', cool: '#bcd6ff', signal: '#ff3b4e', rain: .7, fog: .6, blur: 1, speed: 1 },\n      ptrSpring: [300, 30],\n      init(S) { pc = S.program(CITY); pg = S.program(GLASS); pf = S.program(FOG); init = 1; },\n      resize(S) {\n        S.free(sharp, bokeh, fog);\n        const hw = Math.max(2, Math.round(S.w * .5)), hh = Math.max(2, Math.round(S.h * .5));\n        sharp = S.target(hw, hh); bokeh = S.target(hw, hh, { mip: true });\n        fog = S.pair(Math.max(2, Math.round(S.cssW / 4)), Math.max(2, Math.round(S.cssH / 4)));\n        init = 1;\n      },\n      frame(S, dt) {\n        const P = S.ptr, c = S.css;\n        const city = { uTime: S.t, uSky: c.sky, uGlow: c.glow, uWarm: c.warm, uCool: c.cool, uSignal: c.signal };\n        S.draw(pc, sharp, { ...city, uRes: [sharp.w, sharp.h], uBlur: 0 });\n        S.draw(pc, bokeh, { ...city, uRes: [bokeh.w, bokeh.h], uBlur: Math.max(0, Math.min(1, c.blur)) });\n        const moved = Math.hypot(P.x - P.px, P.y - P.py) > 1e-4;\n        S.draw(pf, fog.b, { uRes: [fog.w, fog.h], uA: [P.px, P.py], uB: [P.x, P.y], uDt: dt, uWipe: P.inside && moved && root.dataset.wipe !== 'off' ? 1 : 0, uInit: init, uAsp: S.aspect }, { uPrev: fog.a }); fog.swap(); init = 0;\n        S.draw(pg, null, { uRes: [S.w, S.h], uTime: S.t, uRain: c.rain, uFogK: c.fog, uFrame: Math.floor(S.rt * 24), uDpr: S.dpr }, { uSharp: sharp, uBokeh: bokeh, uFog: fog.a });\n      },\n    });\n  });\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "3d",
    "group": "Shader backgrounds",
    "tags": [
      "rain",
      "glass",
      "window",
      "drops",
      "bokeh",
      "city",
      "fog",
      "condensation",
      "wipe",
      "background",
      "webgl"
    ],
    "tech": [
      "WebGL2",
      "GLSL",
      "JS"
    ],
    "deps": [],
    "source": "wetstreets",
    "free": true,
    "accent": "#ffb45c",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/rain-glass/index.html",
    "thumbnail": "https://vividsites.app/api/component/rain-glass/thumb.jpg",
    "video": "https://vividsites.app/api/component/rain-glass/thumb.mp4",
    "page": "https://vividsites.app/c/rain-glass"
  }
}