{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dissolve-transition",
  "type": "registry:block",
  "title": "Dissolve Transition",
  "description": "An image frame that dissolves into the next picture through fbm noise, starting where you click: the edge burns with embers, rolls through as ink or breaks into pixels. Hovering singes a small preview hole under the pointer.",
  "docs": "Dissolve Transition was added to public/vividsites/dissolve-transition.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/dissolve-transition",
  "categories": [
    "3d",
    "3d-effects"
  ],
  "files": [
    {
      "path": "components/vividsites/dissolve-transition.html",
      "type": "registry:file",
      "target": "public/vividsites/dissolve-transition.html",
      "content": "<!--\n  vividsites · dissolve-transition\n  An image frame that dissolves into the next picture through fbm noise. The front\n  starts where you click and spreads outward, a ragged edge that burns, bleeds like\n  ink or breaks into pixels. Hovering singes a small preview hole under the pointer.\n  Click / tap / Enter to advance; clicking mid-transition rushes it and queues the next.\n\n  Markup:\n    data-images   comma list of \"src [focusX]\" — focusX (0–1) is where the crop centres\n                  when the frame is narrower than the photo (e.g. on phones)\n    data-style    burn | ink | pixel\n    data-auto     seconds between automatic advances (0 = click only)\n    Each <img> child is the no-WebGL fallback and supplies the alt text.\n  Customise (CSS variables on .vs-dissolve-transition):\n    --vs-dissolve-transition-edge       colour of the burning edge / ink / pixels\n    --vs-dissolve-transition-scale      noise scale, .4 (big islands) … 2.5 (fine grain)\n    --vs-dissolve-transition-duration   transition length, e.g. 1.8s\n    --vs-dissolve-transition-radius     corner radius\n    --vs-dissolve-transition-width / -height   frame size\n  JS:\n    el.vsDissolveTransition.next([x, y])   advance; x/y 0–1 in the frame (origin of the front)\n    el.vsDissolveTransition.go(i)          jump to image i\n    el.vsDissolveTransition.set({ edge, scale, duration, style, auto })\n  Events: 'vs-dissolve-transition:change' → detail.index\n-->\n<div class=\"vs-dissolve-transition\" data-style=\"burn\" data-auto=\"0\"\n     data-images=\"fig.webp .22, hero.webp .8, pair.webp .78, smoke.webp .82\">\n  <img class=\"vs-dissolve-transition-img\" src=\"fig.webp\" alt=\"A split fig on a travertine plinth against a brown velvet curtain\">\n  <img class=\"vs-dissolve-transition-img\" src=\"hero.webp\" alt=\"An amber perfume bottle on a travertine plinth\" hidden>\n  <img class=\"vs-dissolve-transition-img\" src=\"pair.webp\" alt=\"Two amber perfume bottles side by side on travertine\" hidden>\n  <img class=\"vs-dissolve-transition-img\" src=\"smoke.webp\" alt=\"A smouldering palo santo stick sending up a curl of smoke\" hidden>\n  <canvas class=\"vs-dissolve-transition-canvas\" aria-hidden=\"true\"></canvas>\n</div>\n\n<style>\n  .vs-dissolve-transition {\n    --vs-dissolve-transition-edge: #ff5a1f;\n    --vs-dissolve-transition-scale: 1;\n    --vs-dissolve-transition-duration: 2.4s;\n    --vs-dissolve-transition-radius: 22px;\n    --vs-dissolve-transition-width: 100%;\n    --vs-dissolve-transition-height: min(80vh, 720px);\n    position: relative; width: var(--vs-dissolve-transition-width); height: var(--vs-dissolve-transition-height);\n    overflow: hidden; border-radius: var(--vs-dissolve-transition-radius); background: #1c1916;\n    isolation: isolate; cursor: pointer; -webkit-tap-highlight-color: transparent; outline: none;\n    transform: translateZ(0);\n  }\n  .vs-dissolve-transition::after { content: \"\"; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 2;\n    box-shadow: inset 0 0 0 .5px rgba(255,255,255,.1), inset 0 1px 0 rgba(255,255,255,.08); }\n  .vs-dissolve-transition-img, .vs-dissolve-transition-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n  .vs-dissolve-transition-img { object-fit: cover; transition: opacity .6s cubic-bezier(.16,1,.3,1); }\n  .vs-dissolve-transition-img[hidden] { display: block; opacity: 0; }\n  .vs-dissolve-transition-canvas { opacity: 0; transition: opacity .5s cubic-bezier(.16,1,.3,1); }\n  .vs-dissolve-transition[data-vs-state=\"ready\"] .vs-dissolve-transition-canvas { opacity: 1; }\n  .vs-dissolve-transition:focus-visible { box-shadow: 0 0 0 2px #141210, 0 0 0 3.5px var(--vs-dissolve-transition-edge); }\n  .vs-dissolve-transition:active { cursor: grabbing; }\n</style>\n\n<script>\n(() => {\n  const NOISE = `\n  float h21(vec2 p){ p = fract(p*vec2(123.34, 456.21)); p += dot(p, p+45.32); return fract(p.x*p.y); }\n  float vnoise(vec2 p){ vec2 i = floor(p), f = fract(p); f = f*f*(3.-2.*f);\n    return mix(mix(h21(i), h21(i+vec2(1,0)), f.x), mix(h21(i+vec2(0,1)), h21(i+vec2(1,1)), f.x), f.y); }\n  float fbm(vec2 p){ float s = 0., a = .5; mat2 r = mat2(.8,.6,-.6,.8);\n    for (int i = 0; i < 5; i++) { s += a*vnoise(p); p = r*p*2.03 + 17.1; a *= .5; } return s/.97; }\n  uniform vec2 uOrigin, uMouse; uniform float uAspect, uScale, uSeed, uMaxD, uT, uHover, uCells;\n  /* the dissolve field: distance from where it started, broken up by fbm */\n  float field(vec2 uv){\n    vec2 p = vec2(uv.x*uAspect, uv.y);\n    float n = fbm(p*uScale*2.4 + uSeed);\n    float fine = vnoise(p*uScale*46. + uSeed) - .5;\n    float d = distance(p, vec2(uOrigin.x*uAspect, uOrigin.y))/uMaxD;\n    return d*.6 + (n-.5)*.72 + fine*.018 + .25;\n  }\n  /* hover preview: a small ragged hole that smoulders around the pointer */\n  float hoverE(vec2 uv){\n    vec2 p = vec2(uv.x*uAspect, uv.y);\n    float n = fbm(p*uScale*4. + uSeed + 3.7);\n    float fine = vnoise(p*uScale*46. + uSeed) - .5;\n    float d = distance(p, vec2(uMouse.x*uAspect, uMouse.y));\n    return d*.9 + (n-.5)*.22 + fine*.018 - uHover*.05;\n  }`;\n\n  const VS = `#version 300 es\n  in vec2 aP; out vec2 vUv;\n  void main(){ vUv = aP*.5+.5; gl_Position = vec4(aP,0.,1.); }`;\n\n  const FS = `#version 300 es\n  precision highp float;\n  uniform sampler2D uA, uB; uniform vec4 uXA, uXB; uniform vec3 uEdge; uniform float uStyle, uTime;\n  ${NOISE}\n  in vec2 vUv; out vec4 o;\n  vec3 texA(vec2 uv){ return texture(uA, uv*uXA.xy + uXA.zw).rgb; }\n  vec3 texB(vec2 uv){ return texture(uB, uv*uXB.xy + uXB.zw).rgb; }\n  vec3 shoulder(vec3 x){ return mix(x, .75 + .25*(1.-exp(-(x-.75)/.25)), step(.75, x)); }\n  void main(){\n    vec2 uv = vUv;\n    vec2 cuv = uv;\n    if (uStyle > 1.5) { vec2 cells = vec2(uCells*uAspect, uCells); cuv = (floor(uv*cells)+.5)/cells; }\n    float e = min(field(cuv) - uT, uHover > .002 ? hoverE(cuv) : 9.);\n    vec3 edge = uEdge;\n    vec3 c;\n    if (uStyle < .5) {\n      /* burn: old → toasted → charred → glowing rim → new */\n      float heat = smoothstep(.09, 0., e);\n      vec2 shimmer = (vec2(vnoise(uv*30. + uTime*3.1), vnoise(uv*30. - uTime*2.7)) - .5) * .005 * heat;\n      vec3 A = texA(uv + shimmer), B = texB(uv);\n      float lum = dot(A, vec3(.299,.587,.114));\n      vec3 toast = vec3(lum)*vec3(.7,.46,.28)*.8;\n      c = mix(A, toast, smoothstep(.1, .03, e));\n      float crumb = vnoise(uv*160.);\n      c = mix(c, vec3(.02,.014,.011) + crumb*.02, smoothstep(.034, .012, e) * (.8 + .2*crumb));\n      float flick = .75 + .5*vnoise(uv*14. + uTime*5.);\n      float rim = smoothstep(.013, .002, e);\n      vec3 hot = mix(edge*1.5, vec3(2.4,1.9,1.25), smoothstep(.007, .0005, e) * flick);\n      c = mix(c, hot, rim);\n      c += edge * exp(-e/.022) * .45 * flick * step(0., e);\n      if (e < 0.) c = B + edge * (exp(e/.006) * 1.2 * flick + exp(e/.035) * .14);\n      c = shoulder(c);\n    } else if (uStyle < 1.5) {\n      /* ink: a cloud of pigment rolls across, the new picture clears out of it */\n      float W = .11;\n      vec2 q = uv*vec2(uAspect,1.)*5.;\n      vec2 wq = vec2(fbm(q + uSeed + uTime*.15), fbm(q - uSeed - uTime*.12));\n      float cloud = fbm(q*1.6 + wq*2.2 + uTime*.1);\n      vec2 warp = (wq - .5) * .018 * smoothstep(W*1.6, 0., abs(e));\n      vec3 A = texA(uv + warp), B = texB(uv - warp*.6);\n      float ahead = smoothstep(W*1.1, 0., e);\n      float behind = smoothstep(-W*1.8, -W*.1, e);\n      float dens = ahead * behind * clamp(cloud*1.9 - .25, 0., 1.);\n      dens += exp(-abs(e - .004)/.005) * .5 * clamp(cloud*2., 0., 1.);\n      vec3 ink = edge * .3 + .012;\n      c = e < 0. ? B : A;\n      c = mix(c, ink, clamp(dens, 0., 1.) * .94);\n    } else {\n      /* pixel: the old picture quantises, a band of lit cells passes, the new one resolves */\n      float W = .035;\n      vec2 cells = vec2(uCells*uAspect, uCells);\n      vec2 g = fract(uv*cells);\n      float grid = step(.07, g.x) * step(.07, g.y);\n      vec3 A = texA(e < W*2.4 ? cuv : uv), B = texB(e > -W*1.4 ? cuv : uv);\n      float band = smoothstep(W, W*.2, e) * step(0., e);\n      float r = h21(floor(uv*cells) + floor(uTime*18.));\n      c = e < 0. ? B : A;\n      vec3 lit = mix(edge, vec3(1.,.96,.9), r*r*.6) * (.55 + r*.9);\n      c = mix(c, lit * grid + c*.12, band * step(.18, r + band*.3));\n      c += edge * exp(-abs(e)/.006) * .35 * grid;\n    }\n    o = vec4(c, 1.);\n  }`;\n\n  /* embers: each spark has a fixed spot; it lifts off when the front passes that spot */\n  const PVS = `#version 300 es\n  precision highp float;\n  in vec4 aS; uniform float uClock, uDur, uDpr; uniform vec2 uRes;\n  ${NOISE}\n  out float vA; out float vHeat;\n  void main(){\n    float f = field(aS.xy);\n    float born = (f + .1)/1.3 * uDur;\n    float life = .7 + aS.z*1.1;\n    float age = uClock - born;\n    gl_PointSize = 0.; gl_Position = vec4(2.,2.,0.,1.); vA = 0.; vHeat = 0.;\n    if (aS.w > .5 || age < 0. || age > life) return;\n    float k = age/life;\n    vec2 p = aS.xy;\n    p.y += age*(.05 + aS.z*.12) + age*age*.05;\n    p.x += sin(age*(2.5+aS.w*9.) + aS.z*40.)*.012*age + (aS.w-.25)*.05*age;\n    gl_Position = vec4(p*2.-1., 0., 1.);\n    gl_PointSize = (2. + aS.z*aS.z*5.) * uDpr * (1. - k*.6);\n    vA = (1.-k)*(1.-k) * smoothstep(0., .06, age);\n    vHeat = 1. - k;\n  }`;\n  const PFS = `#version 300 es\n  precision highp float;\n  uniform vec3 uEdge; in float vA; in float vHeat; out vec4 o;\n  void main(){\n    vec2 q = gl_PointCoord - .5; float r = length(q)*2.;\n    float a = smoothstep(1., 0., r); a *= a;\n    vec3 c = mix(uEdge*.9, vec3(1.,.92,.75), vHeat*vHeat*smoothstep(.6, 0., r));\n    o = vec4(c*a*vA*2.2, 0.);\n  }`;\n\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const STYLES = { burn: 0, ink: 1, pixel: 2 };\n  const loadImg = src => new Promise((res, rej) => { const i = new Image(); i.decoding = 'async'; i.onload = () => res(i); i.onerror = rej; i.src = src; });\n  const parseTime = v => { v = String(v).trim(); const n = parseFloat(v); return !Number.isFinite(n) ? 1.8 : /ms$/.test(v) ? n / 1000 : n; };\n  const hexRgb = s => { const cv = document.createElement('canvas').getContext('2d'); cv.fillStyle = '#000'; cv.fillStyle = s; const h = cv.fillStyle;\n    if (h[0] === '#') return [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16) / 255);\n    const m = h.match(/[\\d.]+/g) || [0, 0, 0]; return m.slice(0, 3).map(x => x / 255); };\n\n  function mount(root) {\n    if (root.vsDissolveTransition) return;\n    const canvas = root.querySelector('.vs-dissolve-transition-canvas');\n    const imgs = [...root.querySelectorAll('.vs-dissolve-transition-img')];\n    const list = (root.dataset.images || imgs.map(i => i.getAttribute('src')).join(',')).split(',').map(s => s.trim()).filter(Boolean)\n      .map(s => { const [src, fx] = s.split(/\\s+/); return { src, fx: fx != null ? +fx : .5 }; });\n    const N = list.length;\n    let index = 0, target = -1, queued = null;\n    const props = { edge: [1, .35, .12], scale: 1, dur: 1.8, style: 0, auto: 0 };\n    function readProps() {\n      const cs = getComputedStyle(root);\n      props.edge = hexRgb(cs.getPropertyValue('--vs-dissolve-transition-edge').trim() || '#ff5a1f');\n      props.scale = parseFloat(cs.getPropertyValue('--vs-dissolve-transition-scale')) || 1;\n      props.dur = Math.max(.2, parseTime(cs.getPropertyValue('--vs-dissolve-transition-duration') || '1.8s'));\n      props.style = STYLES[root.dataset.style] ?? 0;\n      props.auto = parseFloat(root.dataset.auto) || 0;\n      dirty = true; kick();\n    }\n    const setAlt = () => { imgs.forEach((im, i) => { im.hidden = i !== index; }); root.setAttribute('aria-label', (imgs[index]?.alt || 'Image') + ' — activate for the next image'); };\n    root.setAttribute('role', 'button'); if (!root.hasAttribute('tabindex')) root.tabIndex = 0;\n\n    /* fallback: plain crossfade of the <img> children */\n    const fallbackNext = () => { index = (index + 1) % N; setAlt(); root.dispatchEvent(new CustomEvent('vs-dissolve-transition:change', { detail: { index } })); };\n    const gl = canvas.getContext('webgl2', { antialias: false, alpha: false, premultipliedAlpha: false, powerPreference: 'high-performance' });\n    let api;\n    if (!gl) {\n      root.dataset.vsState = 'nogl';\n      api = { next: fallbackNext, go: i => { index = ((i % N) + N) % N; setAlt(); }, set: o => apply(o), get index() { return index; } };\n    }\n\n    let P = {}, quad, pvao, W = 1, H = 1, dpr = 1, raf = 0, visible = true, lost = false, last = 0;\n    const tex = new Array(N).fill(null), images = new Array(N).fill(null);\n    let dirty = true, clock = 0, running = false, rush = 1, origin = [.5, .5], seed = Math.random() * 50, autoT = 0;\n    const mouse = { x: .5, y: .5, tx: .5, ty: .5, h: 0, th: 0, in: false };\n    const EMBERS = 900;\n\n    const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x);\n      if (!gl.getShaderParameter(x, gl.COMPILE_STATUS) && !gl.isContextLost()) throw new Error(gl.getShaderInfoLog(x)); return x; };\n    const prog = (vs, fs) => { const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs));\n      gl.bindAttribLocation(p, 0, vs === VS ? 'aP' : 'aS'); gl.linkProgram(p);\n      if (!gl.getProgramParameter(p, gl.LINK_STATUS) && !gl.isContextLost()) throw new Error(gl.getProgramInfoLog(p));\n      const u = {}; const n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS);\n      for (let i = 0; i < n; i++) { const k = gl.getActiveUniform(p, i).name; u[k] = gl.getUniformLocation(p, k); }\n      return { p, u }; };\n    function upload(i) {\n      if (!images[i]) return;\n      const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[i]);\n      gl.generateMipmap(gl.TEXTURE_2D);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_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      tex[i] = t;\n    }\n    function init() {\n      P.main = prog(VS, FS); P.emb = prog(PVS, PFS);\n      quad = gl.createVertexArray(); gl.bindVertexArray(quad);\n      let b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b);\n      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n      gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n      pvao = gl.createVertexArray(); gl.bindVertexArray(pvao);\n      const s = new Float32Array(EMBERS * 4);\n      for (let i = 0; i < EMBERS; i++) { s[i * 4] = Math.random(); s[i * 4 + 1] = Math.random(); s[i * 4 + 2] = Math.random(); s[i * 4 + 3] = Math.random() * 1.6; }\n      b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, s, gl.STATIC_DRAW);\n      gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);\n      gl.bindVertexArray(null);\n      tex.fill(null); for (let i = 0; i < N; i++) upload(i);\n    }\n    /* cover-crop each photo into the frame, centred on its focus */\n    function xf(i) {\n      const im = images[i]; if (!im) return [1, 1, 0, 0];\n      const fa = W / H, ia = im.naturalWidth / im.naturalHeight;\n      if (fa > ia) { const sy = ia / fa; return [1, sy, 0, (1 - sy) / 2]; }\n      const sx = fa / ia; const ox = Math.min(1 - sx, Math.max(0, list[i].fx - sx / 2)); return [sx, 1, ox, 0];\n    }\n    function maxD(o) { const a = W / H; let m = 0; for (const [x, y] of [[0, 0], [1, 0], [0, 1], [1, 1]]) m = Math.max(m, Math.hypot((x - o[0]) * a, y - o[1])); return m; }\n\n    function common(u) {\n      gl.uniform2f(u.uOrigin, origin[0], origin[1]); gl.uniform2f(u.uMouse, mouse.x, mouse.y);\n      gl.uniform1f(u.uAspect, W / H); gl.uniform1f(u.uScale, props.scale); gl.uniform1f(u.uSeed, seed);\n      gl.uniform1f(u.uMaxD, maxD(origin)); gl.uniform1f(u.uHover, reduce.matches ? 0 : mouse.h);\n      gl.uniform1f(u.uT, running ? -.1 + Math.min(1, clock / props.dur) * 1.3 : -.4);\n      gl.uniform1f(u.uCells, Math.round(40 / Math.sqrt(props.scale)));\n    }\n    function draw(now) {\n      const nextI = running ? target : (index + 1) % N;\n      gl.viewport(0, 0, W, H);\n      gl.bindVertexArray(quad); gl.useProgram(P.main.p); const u = P.main.u; common(u);\n      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex[index]); gl.uniform1i(u.uA, 0);\n      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, tex[nextI] || tex[index]); gl.uniform1i(u.uB, 1);\n      gl.uniform4f(u.uXA, ...xf(index)); gl.uniform4f(u.uXB, ...xf(tex[nextI] ? nextI : index));\n      gl.uniform3f(u.uEdge, ...props.edge); gl.uniform1f(u.uStyle, props.style); gl.uniform1f(u.uTime, now / 1000);\n      gl.disable(gl.BLEND); gl.drawArrays(gl.TRIANGLES, 0, 3);\n      if (running && props.style === 0 && !reduce.matches) {\n        gl.bindVertexArray(pvao); gl.useProgram(P.emb.p); const v = P.emb.u; common(v);\n        gl.uniform1f(v.uClock, clock); gl.uniform1f(v.uDur, props.dur); gl.uniform1f(v.uDpr, dpr); gl.uniform3f(v.uEdge, ...props.edge);\n        gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE); gl.drawArrays(gl.POINTS, 0, EMBERS); gl.disable(gl.BLEND);\n      }\n    }\n    function frame(now) {\n      raf = 0; if (lost || !tex[index]) return;\n      const dt = Math.min(.05, (now - (last || now)) / 1000); last = now;\n      /* pointer + hover on critically damped springs */\n      const k = 1 - Math.exp(-dt * 14); mouse.x += (mouse.tx - mouse.x) * k; mouse.y += (mouse.ty - mouse.y) * k;\n      mouse.th = mouse.in && !running ? 1 : 0; mouse.h += (mouse.th - mouse.h) * (1 - Math.exp(-dt * (mouse.th ? 3.2 : 7)));\n      if (running) {\n        clock += dt * rush * (reduce.matches ? 4 : 1);\n        if (clock >= props.dur + (props.style === 0 && !reduce.matches ? 1.9 : 0) || (rush > 1 && clock >= props.dur)) finish();\n      } else if (props.auto > 0 && !mouse.in && !reduce.matches) {\n        autoT += dt; if (autoT >= props.auto) { autoT = 0; const a = Math.random() * Math.PI * 2; start(null, [.5 + Math.cos(a) * .42, .5 + Math.sin(a) * .42]); }\n      }\n      if (dirty || running || mouse.h > .002) { draw(now); dirty = false; }\n      if (root.dataset.vsState !== 'ready') root.dataset.vsState = 'ready';\n      if (visible && (dirty || running || mouse.h > .002 || mouse.th > 0 || props.auto > 0)) raf = requestAnimationFrame(frame);\n    }\n    function kick() { if (gl && !raf && visible && !lost && tex[index]) { last = 0; raf = requestAnimationFrame(frame); } }\n    function finish() {\n      index = target; running = false; target = -1; clock = 0; rush = 1; dirty = true; setAlt();\n      root.dispatchEvent(new CustomEvent('vs-dissolve-transition:change', { detail: { index } }));\n      if (queued) { const q = queued; queued = null; start(q.i, q.o); }\n    }\n    function start(i, o) {\n      if (running) { rush = 3.2; queued = { i, o }; kick(); return; }\n      const t = i == null ? (index + 1) % N : ((i % N) + N) % N;\n      if (t === index || !tex[t]) return;\n      target = t; origin = o || [mouse.x, mouse.y]; seed = Math.random() * 50; clock = 0; rush = 1; running = true; autoT = 0; kick();\n    }\n\n    function resize() {\n      dpr = Math.min(2, devicePixelRatio || 1);\n      const w = Math.max(1, Math.round(root.clientWidth * dpr)), h = Math.max(1, Math.round(root.clientHeight * dpr));\n      if (w === W && h === H) return; W = canvas.width = w; H = canvas.height = h;\n      dirty = true; kick();\n    }\n    const local = e => { const r = root.getBoundingClientRect(); return [(e.clientX - r.left) / r.width, 1 - (e.clientY - r.top) / r.height]; };\n    root.addEventListener('pointermove', e => { const [x, y] = local(e); mouse.tx = x; mouse.ty = y; if (e.pointerType === 'mouse') { if (!mouse.in) { mouse.x = x; mouse.y = y; } mouse.in = true; } kick(); });\n    root.addEventListener('pointerleave', () => { mouse.in = false; kick(); });\n    root.addEventListener('pointerdown', e => { const [x, y] = local(e); mouse.tx = mouse.x = x; mouse.ty = mouse.y = y; });\n    root.addEventListener('click', e => { if (!gl) return fallbackNext(); const [x, y] = local(e); start(null, [x, y]); });\n    root.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); gl ? start(null, [.5, .5]) : fallbackNext(); }\n      if (e.key === 'ArrowRight') { e.preventDefault(); gl ? start(null, [0, .5]) : fallbackNext(); } if (e.key === 'ArrowLeft' && gl) { e.preventDefault(); start(index - 1, [1, .5]); } });\n\n    function apply(o = {}) {\n      if (o.edge != null) root.style.setProperty('--vs-dissolve-transition-edge', o.edge);\n      if (o.scale != null) root.style.setProperty('--vs-dissolve-transition-scale', o.scale);\n      if (o.duration != null) root.style.setProperty('--vs-dissolve-transition-duration', typeof o.duration === 'number' ? o.duration + 's' : o.duration);\n      if (o.style != null) root.dataset.style = o.style;\n      if (o.auto != null) root.dataset.auto = o.auto;\n      if (gl) readProps();\n    }\n    setAlt();\n    if (!gl) { root.vsDissolveTransition = api; return; }\n    new MutationObserver(readProps).observe(root, { attributes: true, attributeFilter: ['style', 'class', 'data-style', 'data-auto'] });\n    readProps();\n    canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); lost = true; cancelAnimationFrame(raf); raf = 0; root.dataset.vsState = 'lost'; });\n    canvas.addEventListener('webglcontextrestored', () => { lost = false; try { init(); W = H = 1; resize(); kick(); } catch { root.dataset.vsState = 'nogl'; } });\n    new IntersectionObserver(([en]) => { visible = en.isIntersecting; if (visible) kick(); }).observe(root);\n    document.addEventListener('visibilitychange', () => { visible = !document.hidden; if (visible) kick(); });\n\n    try { init(); } catch (err) { root.dataset.vsState = 'nogl'; console.warn('[dissolve-transition]', err.message); return; }\n    root.vsDissolveTransition = { next: o => start(null, o), go: (i, o) => start(i, o || [.5, .5]), set: apply, get index() { return index; } };\n    new ResizeObserver(resize).observe(root); resize();\n    /* first image first, the rest right after */\n    list.forEach((it, i) => loadImg(it.src).then(im => { images[i] = im; if (!lost) upload(i); if (i === index) { root.dispatchEvent(new CustomEvent('vs:ready')); kick(); } }).catch(() => {}));\n  }\n  const boot = () => document.querySelectorAll('.vs-dissolve-transition').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', boot) : boot();\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "3d",
    "group": "3D effects",
    "tags": [
      "transition",
      "dissolve",
      "burn",
      "noise",
      "image",
      "gallery",
      "shader",
      "embers"
    ],
    "tech": [
      "GLSL",
      "JS",
      "CSS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#ff5a1f",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/dissolve-transition/index.html",
    "thumbnail": "https://vividsites.app/api/component/dissolve-transition/thumb.jpg",
    "video": "https://vividsites.app/api/component/dissolve-transition/thumb.mp4",
    "page": "https://vividsites.app/c/dissolve-transition"
  }
}