{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rgb-glitch",
  "type": "registry:block",
  "title": "RGB Glitch",
  "description": "An image that tears on hover: channels split along the pointer's motion, slices jump out of the frame at uneven video-like intervals and a click fires a full burst. Swapping the image glitches between the two.",
  "docs": "RGB Glitch was added to public/vividsites/rgb-glitch.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/rgb-glitch",
  "categories": [
    "3d",
    "3d-objects"
  ],
  "files": [
    {
      "path": "components/vividsites/rgb-glitch.html",
      "type": "registry:file",
      "target": "public/vividsites/rgb-glitch.html",
      "content": "<!--\n  vividsites · rgb-glitch\n  An image that tears when you touch it. Hover and the channels split along the\n  pointer's motion (faster = wider, a still pointer barely shimmers); slices of the\n  frame jump sideways near the pointer and flicker at uneven, video-like intervals;\n  a click fires a full burst. Changing the image glitches between the two.\n  The <img> stays in the DOM for layout, alt text and a no-WebGL fallback.\n\n  Customise (attributes, read at mount; all live through the JS API too):\n    data-effect   split | slices | both                   (default both)\n    data-idle     on | off                                (a short tear every few seconds; default on)\n  CSS variables:\n    --vs-rgb-glitch-bg         the page colour behind it (fills channel gaps)   (#e9e6e0)\n    --vs-rgb-glitch-width      image width                                      (min(520px, 86vw))\n    --vs-rgb-glitch-radius     corner radius                                    (14px)\n    --vs-rgb-glitch-intensity  0 – 2                                            (1)\n  JS:\n    el.vsRgbGlitch.set({ src: 'watch.webp', effect: 'split', intensity: 1.4, idle: false, bg: '#111' })\n    el.vsRgbGlitch.burst()      el.vsRgbGlitch.destroy()\n  Images from another origin need CORS headers (the texture reads the pixels).\n-->\n<figure class=\"vs-rgb-glitch\" data-effect=\"both\" data-idle=\"on\" tabindex=\"0\">\n  <img class=\"vs-rgb-glitch-img\" src=\"fig.webp\" alt=\"A split fig on a travertine plinth, lit from above\" width=\"960\" height=\"1200\">\n  <canvas class=\"vs-rgb-glitch-gl\" aria-hidden=\"true\"></canvas>\n</figure>\n\n<style>\n  .vs-rgb-glitch {\n    --vs-rgb-glitch-bg: #e9e6e0;\n    --vs-rgb-glitch-width: min(520px, 86vw);\n    --vs-rgb-glitch-radius: 14px;\n    --vs-rgb-glitch-intensity: 1;\n    position: relative; margin: 0; width: var(--vs-rgb-glitch-width); outline: none; cursor: crosshair; touch-action: pan-y; -webkit-tap-highlight-color: transparent;\n    border-radius: var(--vs-rgb-glitch-radius);\n  }\n  .vs-rgb-glitch::before { content: \"\"; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; box-shadow: 0 1px 2px rgba(0,0,0,.08), 0 18px 40px -18px rgba(0,0,0,.35); }\n  .vs-rgb-glitch-img { display: block; width: 100%; height: auto; border-radius: var(--vs-rgb-glitch-radius); user-select: none; -webkit-user-drag: none;\n    box-shadow: 0 1px 2px rgba(0,0,0,.08), 0 18px 40px -18px rgba(0,0,0,.35); }\n  .vs-rgb-glitch[data-vs-state=\"ready\"] .vs-rgb-glitch-img { visibility: hidden; }\n  /* the canvas bleeds past the image sideways so torn slices can escape the frame */\n  .vs-rgb-glitch-gl { position: absolute; top: 0; bottom: 0; left: -16%; width: 132%; height: 100%; display: block; pointer-events: none; }\n  .vs-rgb-glitch::after { content: \"\"; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; opacity: 0; transition: opacity .2s;\n    box-shadow: 0 0 0 2px var(--vs-rgb-glitch-bg), 0 0 0 4px #ff2a4a; }\n  .vs-rgb-glitch:focus-visible::after { opacity: 1; }\n  /* no WebGL: a CSS channel split on hover */\n  .vs-rgb-glitch[data-vs-state=\"nogl\"] .vs-rgb-glitch-img { transition: filter .25s cubic-bezier(.2,0,0,1); }\n  .vs-rgb-glitch[data-vs-state=\"nogl\"]:hover .vs-rgb-glitch-img { filter: drop-shadow(4px 0 0 rgba(255,0,60,.55)) drop-shadow(-4px 0 0 rgba(0,200,255,.55)); }\n</style>\n\n<script type=\"module\">\nconst VERT = 'attribute vec2 p; varying vec2 vUv; void main(){ vUv = p * 0.5 + 0.5; gl_Position = vec4(p, 0.0, 1.0); }';\nconst FRAG = /* glsl */`\nprecision highp float;\nvarying vec2 vUv;\nuniform sampler2D uTex, uTex2; uniform vec2 uRes, uPtr, uVel; uniform vec3 uBg;\nuniform float uBleed, uG, uSeed, uTime, uEffect, uRad, uMix, uHov;\nfloat hash(vec2 p){ p = fract(p * vec2(233.34, 851.73)); p += dot(p, p + 23.45); return fract(p.x * p.y); }\nfloat rbox(vec2 p, vec2 size, float r){ vec2 q = abs(p - size * 0.5) - size * 0.5 + r; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; }\nvec4 img(vec2 uv){ return mix(texture2D(uTex, uv), texture2D(uTex2, uv), uMix); }\nfloat inside(vec2 uv, vec2 px){ // anti-aliased rounded-rect mask in image space\n  float d = rbox(uv * px, px, uRad);\n  return clamp(0.5 - d, 0.0, 1.0);\n}\nvoid main(){\n  vec2 px = vec2(uRes.x / (1.0 + 2.0 * uBleed), uRes.y);      // image size in device px\n  vec2 uv = vec2((vUv.x * (1.0 + 2.0 * uBleed)) - uBleed, 1.0 - vUv.y);\n  float g = min(uG, 1.25);\n  float slices = step(0.5, uEffect) , split = 1.0 - step(1.5, uEffect) * 0.0;\n  if (uEffect > 0.5 && uEffect < 1.5) split = 0.25; // slices-only keeps a whisper of split\n  if (uEffect < 0.5) slices = 0.0;\n  // ---- slices: rows of uneven height jump sideways, most of them near the pointer\n  float rowsA = mix(5.0, 11.0, hash(vec2(uSeed, 5.3)));            // coarse bands\n  float rowA = floor(uv.y * rowsA + hash(vec2(uSeed, 1.9)) * 2.0);\n  float onA = step(1.0 - 0.22 * g, hash(vec2(rowA, uSeed + 4.0))) * slices;\n  float rows = mix(22.0, 64.0, hash(vec2(uSeed, 3.1)));             // fine slivers\n  float row = floor(uv.y * rows + hash(vec2(uSeed, 9.0)) * 3.0);\n  float r = hash(vec2(row, uSeed));\n  float dy = (uv.y - uPtr.y) * 3.0; float near = exp(-dy * dy) * uHov + (1.0 - uHov);\n  float onB = step(1.0 - 0.4 * g * near, r) * slices;\n  float shift = (hash(vec2(rowA, uSeed + 0.7)) - 0.5) * 0.1 * g * onA + (hash(vec2(row, uSeed + 1.7)) - 0.5) * 0.28 * g * onB;\n  float on = max(onA, onB);\n  uv.x += shift;\n  // a few datamosh tiles\n  vec2 tile = floor(uv * vec2(10.0, 13.0));\n  float tb = step(0.985 - 0.05 * min(g, 1.0), hash(tile + uSeed)) * slices * step(0.05, g);\n  vec2 tuv = uv + (vec2(hash(tile + 1.0), hash(tile + 2.0)) - 0.5) * vec2(0.08, 0.03) * g * tb;\n  // ---- channel split along the pointer's motion, plus a steady horizontal base\n  vec2 dir = uVel / max(1.0, length(uVel));\n  vec2 off = dir * 0.012 * g * split;\n  off += vec2(0.005, 0.0) * g * split + vec2(0.016, 0.0) * g * on * sign(shift + 1e-4);\n  off *= min(1.0, 0.028 / max(length(off), 1e-5));\n  vec2 ur = tuv + off, ub = tuv - off;\n  // scanline tear: one bright line that rides the burst\n  float tear = smoothstep(0.004, 0.0, abs(uv.y - fract(uSeed * 7.13))) * step(0.3, g) * slices;\n  float mr = inside(ur, px), mg = inside(tuv, px), mb = inside(ub, px);\n  vec3 c;\n  c.r = mix(uBg.r, img(ur).r, mr);\n  c.g = mix(uBg.g, img(tuv).g, mg);\n  c.b = mix(uBg.b, img(ub).b, mb);\n  float a = max(mr, max(mg, mb));\n  // scanlines and grain, only while glitching\n  float sl = 1.0 - 0.07 * step(0.5, fract(gl_FragCoord.y * 0.5)) * min(g, 1.0);\n  c *= sl; c += (hash(gl_FragCoord.xy + uSeed) - 0.5) * 0.06 * min(g, 1.0);\n  c += tear * 0.35;\n  gl_FragColor = vec4(c * a, a);\n}`;\n\nclass Spring {\n  constructor(x, k = 120, c = 18) { this.x = x; this.v = 0; this.t = x; this.k = k; this.c = c; }\n  step(dt) { const n = Math.ceil(dt / (1 / 240)); const h = dt / n; for (let i = 0; i < n; i++) { this.v += (-this.k * (this.x - this.t) - this.c * this.v) * h; this.x += this.v * h; } return this.x; }\n}\nconst EFFECTS = { split: 0, slices: 1, both: 2 };\nconst hex = (h) => { const m = /^#?([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec((h || '').trim()); return m ? [1, 2, 3].map((i) => parseInt(m[i], 16) / 255) : [0.91, 0.9, 0.88]; };\n\nfunction mount(el) {\n  const img = el.querySelector('.vs-rgb-glitch-img'), canvas = el.querySelector('.vs-rgb-glitch-gl');\n  const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\n  const cs = getComputedStyle(el);\n  const S = { vis: true, raf: 0, last: 0, t: 0, seed: 1, nextSeed: 0, burst: 0, nextIdle: 3, int: parseFloat(cs.getPropertyValue('--vs-rgb-glitch-intensity')) || 1,\n    bg: hex(cs.getPropertyValue('--vs-rgb-glitch-bg')), ptr: [0.5, 0.5], lastP: null, mix: 0, swapping: false };\n  const hov = new Spring(0, 90, 16), vx = new Spring(0, 60, 14), vy = new Spring(0, 60, 14), px = new Spring(0.5, 200, 26), py = new Spring(0.5, 200, 26), mixS = new Spring(0, 30, 11);\n\n  const gl = canvas.getContext('webgl', { premultipliedAlpha: true, alpha: true, antialias: false });\n  if (!gl) { el.dataset.vsState = 'nogl'; return null; }\n  let prog, U = {}, tex = [], ready = false;\n  function initGL() {\n    const sh = (t, src) => { const s = gl.createShader(t); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.warn(gl.getShaderInfoLog(s)); return null; } return s; };\n    prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, VERT)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FRAG)); gl.linkProgram(prog); gl.useProgram(prog);\n    const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const loc = gl.getAttribLocation(prog, 'p'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n    ['uTex', 'uTex2', 'uRes', 'uPtr', 'uVel', 'uBg', 'uBleed', 'uG', 'uSeed', 'uTime', 'uEffect', 'uRad', 'uMix', 'uHov'].forEach((n) => U[n] = gl.getUniformLocation(prog, n));\n    tex = [gl.createTexture(), gl.createTexture()];\n    tex.forEach((t) => { gl.bindTexture(gl.TEXTURE_2D, t); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); });\n    gl.uniform1i(U.uTex, 0); gl.uniform1i(U.uTex2, 1);\n  }\n  const upload = (i, image) => { gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, tex[i]); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); };\n  initGL();\n  const loadInto = (i, image) => (image.decode ? image.decode() : Promise.resolve()).then(() => { upload(i, image); });\n  loadInto(0, img).then(() => { upload(1, img); ready = true; el.dataset.vsState = 'ready'; S.burst = reduced ? 0 : 0.9; loop(); })\n    .catch(() => { el.dataset.vsState = 'nogl'; });\n\n  function resize() {\n    const w = canvas.clientWidth, h = canvas.clientHeight; if (!w) return;\n    const dpr = Math.min(devicePixelRatio, 2); canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr);\n  }\n  const rect = () => img.getBoundingClientRect();\n  const onMove = (e) => {\n    const r = rect(), x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height, now = performance.now();\n    if (S.lastP) { const dt = Math.max(8, now - S.lastP.t) / 1000; vx.t = Math.max(-3, Math.min(3, (x - S.lastP.x) / dt)); vy.t = Math.max(-3, Math.min(3, (y - S.lastP.y) / dt)); }\n    S.lastP = { x, y, t: now }; px.t = x; py.t = y; hov.t = 1;\n  };\n  const onEnter = (e) => { hov.t = 1; S.lastP = null; if (!reduced) S.burst = Math.max(S.burst, 0.5); };\n  const onLeave = () => { hov.t = 0; vx.t = vy.t = 0; S.lastP = null; };\n  const onDown = (e) => { onMove(e); burst(); };\n  const onKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); burst(); } };\n  function burst(k = 1) { S.burst = Math.max(S.burst, reduced ? 0.4 : k); }\n  el.addEventListener('pointermove', onMove); el.addEventListener('pointerenter', onEnter); el.addEventListener('pointerleave', onLeave);\n  el.addEventListener('pointerdown', onDown); el.addEventListener('keydown', onKey);\n\n  function frame(now) {\n    S.raf = 0; if (!S.vis || !ready) return;\n    const dt = Math.min(0.05, S.last ? (now - S.last) / 1000 : 0.016); S.last = now; S.t += dt;\n    vx.t *= Math.exp(-dt * 6); vy.t *= Math.exp(-dt * 6); // a still pointer lets the velocity fall away\n    const H = hov.step(dt), VX = vx.step(dt), VY = vy.step(dt), speed = Math.hypot(VX, VY);\n    S.burst *= Math.exp(-dt * 3.2);\n    if (el.dataset.idle !== 'off' && !reduced && H < 0.05 && S.t > S.nextIdle) { S.burst = Math.max(S.burst, 0.55 + Math.random() * 0.35); S.nextIdle = S.t + 2.8 + Math.random() * 3.2; }\n    const g = (H * (0.16 + 0.6 * Math.min(1, speed * 0.8)) + S.burst) * S.int * (reduced ? 0.4 : 1);\n    // the seed hops at uneven, video-like intervals; faster while glitching hard\n    if (!reduced && now > S.nextSeed) { S.seed = Math.random() * 100; S.nextSeed = now + (g > 0.5 ? 30 + Math.random() * 70 : 60 + Math.random() * 160); }\n    const M = mixS.step(dt);\n    if (S.swapping && M > 0.995) { S.swapping = false; upload(0, S.next); mixS.x = mixS.t = 0; }\n    gl.viewport(0, 0, canvas.width, canvas.height);\n    gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex[0]); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, tex[1]);\n    gl.uniform2f(U.uRes, canvas.width, canvas.height); gl.uniform2f(U.uPtr, px.step(dt), py.step(dt)); gl.uniform2f(U.uVel, VX * 0.6, VY * 0.25);\n    gl.uniform3fv(U.uBg, S.bg); gl.uniform1f(U.uBleed, 0.16 / 1); gl.uniform1f(U.uG, g); gl.uniform1f(U.uSeed, S.seed); gl.uniform1f(U.uTime, S.t);\n    gl.uniform1f(U.uEffect, EFFECTS[el.dataset.effect] ?? 2); gl.uniform1f(U.uRad, (parseFloat(getComputedStyle(el).getPropertyValue('--vs-rgb-glitch-radius')) || 0) * canvas.height / canvas.clientHeight);\n    gl.uniform1f(U.uMix, M); gl.uniform1f(U.uHov, H);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n    loop();\n  }\n  const loop = () => { if (!S.raf && S.vis) S.raf = requestAnimationFrame(frame); };\n  const ro = new ResizeObserver(resize); ro.observe(canvas); resize();\n  const io = new IntersectionObserver(([e]) => { S.vis = e.isIntersecting; if (S.vis) { S.last = 0; loop(); } }); io.observe(el);\n  const onVis = () => { S.vis = !document.hidden; if (S.vis) { S.last = 0; loop(); } };\n  document.addEventListener('visibilitychange', onVis);\n  canvas.addEventListener('webglcontextlost', (e) => { e.preventDefault(); ready = false; });\n  canvas.addEventListener('webglcontextrestored', () => { initGL(); upload(0, img); upload(1, img); ready = true; loop(); });\n\n  function set(o = {}) {\n    if (o.effect in EFFECTS) el.dataset.effect = o.effect;\n    if (o.idle != null) el.dataset.idle = o.idle ? 'on' : 'off';\n    if (o.intensity != null) { S.int = +o.intensity; el.style.setProperty('--vs-rgb-glitch-intensity', o.intensity); }\n    if (o.bg) { S.bg = hex(o.bg); el.style.setProperty('--vs-rgb-glitch-bg', o.bg); }\n    if (o.src) {\n      const next = new Image(); next.crossOrigin = 'anonymous'; next.src = o.src;\n      loadInto(1, next).then(() => { S.next = next; S.swapping = true; mixS.x = 0; mixS.t = 1; S.burst = Math.max(S.burst, 1.5); img.src = o.src; if (o.alt) img.alt = o.alt; });\n    }\n  }\n  function destroy() {\n    cancelAnimationFrame(S.raf); S.vis = false; ro.disconnect(); io.disconnect(); document.removeEventListener('visibilitychange', onVis);\n    el.removeEventListener('pointermove', onMove); el.removeEventListener('pointerenter', onEnter); el.removeEventListener('pointerleave', onLeave);\n    el.removeEventListener('pointerdown', onDown); el.removeEventListener('keydown', onKey);\n    gl.getExtension('WEBGL_lose_context')?.loseContext(); delete el.vsRgbGlitch; el.dataset.vsState = '';\n  }\n  return { set, burst, destroy };\n}\n\ndocument.querySelectorAll('.vs-rgb-glitch').forEach((el) => { if (!el.vsRgbGlitch) { const api = mount(el); if (api) el.vsRgbGlitch = api; } });\n</script>\n"
    }
  ],
  "meta": {
    "category": "3d",
    "group": "3D objects",
    "tags": [
      "glitch",
      "rgb split",
      "image",
      "hover",
      "datamosh",
      "chromatic aberration",
      "webgl"
    ],
    "tech": [
      "GLSL",
      "JS",
      "CSS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#ff2a4a",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/rgb-glitch/index.html",
    "thumbnail": "https://vividsites.app/api/component/rgb-glitch/thumb.jpg",
    "video": "https://vividsites.app/api/component/rgb-glitch/thumb.mp4",
    "page": "https://vividsites.app/c/rgb-glitch"
  }
}