Copied!
Gallery

Liquid distortion slideshow

A slideshow of gradient and puzzle-piece slides that melts from one to the next through a flowing noise displacement, with prev/next, dots and arrow keys.

← Library

Element · Vanilla JS + WebGL + CanvasUpdated November 6, 2026

3DElementVanilla JS + WebGL + Canvas

Live preview needs JavaScript. The code below works without any dependencies.

Prompt for your AI assistant

Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.

Build "Liquid distortion slideshow" for my site using the Enrichr Library element at https://enrichr.dev/elements/liquid-distortion-slideshow.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/liquid-distortion-slideshow.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.

Settings:
- accent: #e6ff55
- intensity: 0.7
- duration: 1200
- scale: 3
- autoplay: 0

Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.

The same spec as JSON, for tools that read it directly: /elements/liquid-distortion-slideshow.json.

Code

Mount it on an element that contains the markup. It returns an object with update(options) and destroy().

HTML

<div class="ef-lds">
  <div class="ef-lds__wrap" tabindex="0" role="group" aria-roledescription="carousel" aria-label="Slideshow, use the left and right arrow keys">
    <canvas class="ef-lds__cv" aria-hidden="true"></canvas>
    <div class="ef-lds__cap" aria-live="polite"><span class="ef-lds__k"></span><span class="ef-lds__t"></span></div>
    <div class="ef-lds__ui">
      <button type="button" class="ef-lds__btn" data-d="-1" aria-label="Previous slide">&#8249;</button>
      <div class="ef-lds__dots"></div>
      <button type="button" class="ef-lds__btn" data-d="1" aria-label="Next slide">&#8250;</button>
    </div>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-lds { width: 100%; }
.ef-lds__wrap { position: relative; box-sizing: border-box; width: 100%; aspect-ratio: 16 / 10; min-height: 16rem; border-radius: 1rem; overflow: hidden; background: #0c1e18; color: #fbf8f1; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; outline: none; }
.ef-lds__wrap:focus-visible { box-shadow: inset 0 0 0 3px var(--ef-acc, #e6ff55); }
.ef-lds__cv { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.ef-lds__cap { position: absolute; left: 1.1rem; bottom: 3.6rem; right: 1.1rem; pointer-events: none; text-shadow: 0 2px 12px rgba(12, 30, 24, 0.55); }
.ef-lds__k { display: block; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; opacity: 0.85; }
.ef-lds__t { display: block; margin-top: 0.2rem; font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; }
.ef-lds__ui { position: absolute; left: 0.8rem; right: 0.8rem; bottom: 0.8rem; display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.ef-lds__btn { width: 2.4rem; height: 2.4rem; border-radius: 50%; border: 0; background: rgba(12, 30, 24, 0.72); color: #fbf8f1; font-size: 1.4rem; line-height: 1; cursor: pointer; display: grid; place-items: center; padding: 0 0 0.12rem; transition: background 200ms, color 200ms; }
.ef-lds__btn:hover { background: var(--ef-acc, #e6ff55); color: #0c1e18; }
.ef-lds__btn:focus-visible, .ef-lds__dot:focus-visible { outline: 2px solid var(--ef-acc, #e6ff55); outline-offset: 2px; }
.ef-lds__dots { display: flex; gap: 0.5rem; padding: 0.45rem 0.7rem; border-radius: 999px; background: rgba(12, 30, 24, 0.72); }
.ef-lds__dot { width: 0.6rem; height: 0.6rem; padding: 0; border: 0; border-radius: 50%; background: rgba(251, 248, 241, 0.45); cursor: pointer; transition: background 200ms, transform 200ms; }
.ef-lds__dot.is-on { background: var(--ef-acc, #e6ff55); transform: scale(1.3); }
@media (prefers-reduced-motion: reduce) { .ef-lds__btn, .ef-lds__dot { transition: none; } }

JavaScript

// Enrichr Library · https://enrichr.dev
// Liquid distortion slideshow: slides are painted procedurally (gradients plus Enrichr puzzle-piece art, no image files) and the change
// between them runs through a flowing noise displacement in a WebGL fragment shader. Without WebGL it falls back to a plain crossfade
// on a 2D canvas. Prev/next buttons, dots and the arrow keys all work; reduced motion swaps slides instantly.
// An original rebuild from the general idea of a liquid slide transition.
// mount(root, { accent, intensity (0-1.5), duration (ms), scale (noise scale), autoplay (seconds, 0 = off) })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var wrap = root.querySelector(".ef-lds__wrap");
  var cv = root.querySelector(".ef-lds__cv");
  var capK = root.querySelector(".ef-lds__k");
  var capT = root.querySelector(".ef-lds__t");
  var dotsEl = root.querySelector(".ef-lds__dots");
  var opts = {}, offs = [], dots = [];
  var PIECES = [
    { vb: [1513.7, 1453.9, 322.3, 322.3], d: "M1821 1761.2h-292.3v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5.1 7.8 8.6 19.7 13.6 32.7 11.9 16.8-2.3 30.1-16 32-32.7 2.5-22.6-15.1-41.6-37.1-41.6-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.4-14.5-14.4v-111.7h292.3v111.7c0 8-6.4 14.4-14.5 14.4-4.2 0-8.1-1.8-10.9-5-7.9-8.6-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.6 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5z" },
    { vb: [1423.36, 860.8, 503.07, 322.3], d: "M1821 875.8v111.6c0 8 6.5 14.5 14.5 14.5 4.2 0 8.1-1.9 10.9-5 6.8-7.5 16.7-12.3 27.7-12.3 22 0 39.6 19 37.1 41.6-1.9 16.7-15.2 30.5-32 32.7-13 1.7-24.8-3.3-32.7-11.9-2.8-3.2-6.8-5-11-5-8 0-14.5 6.4-14.5 14.4v111.7h-111.6c-8 0-14.5-6.5-14.5-14.5 0-4.2 1.9-8.2 5-11 8.7-7.9 13.7-19.7 12-32.7-2.3-16.8-16-30.1-32.8-32-22.5-2.5-41.5 15.1-41.5 37.1 0 11 4.7 20.9 12.3 27.7 3.1 2.8 4.9 6.7 4.9 10.8v0.1c0 8-6.4 14.5-14.5 14.5h-111.6v-111.7c0-8-6.4-14.4-14.5-14.4-4.1 0-8 1.8-10.8 4.9-6.9 7.6-16.7 12.3-27.7 12.3-22 0-39.6-19-37.1-41.6 1.9-16.7 15.2-30.4 32-32.7 13-1.7 24.8 3.3 32.7 11.9 2.8 3.2 6.7 5.1 10.9 5.1 8.1 0 14.5-6.5 14.5-14.5v-111.6z" },
    { vb: [1513.7, 166.86, 322.3, 503.06], d: "M1528.7 564.5v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5 7.8 8.7 19.7 13.7 32.7 11.9 16.8-2.2 30.1-15.9 32-32.7 2.5-22.5-15.1-41.5-37.1-41.5-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.5-14.5-14.5v-111.6h111.6c8.1 0 14.5-6.5 14.5-14.5 0-4.1-1.8-8.1-4.9-10.9-7.6-6.8-12.3-16.6-12.3-27.6 0-22 19-39.6 41.5-37.1 16.8 1.8 30.5 15.2 32.8 32 1.7 12.9-3.3 24.8-12 32.7-3.1 2.8-5 6.7-5 10.9 0 8 6.5 14.5 14.5 14.5h111.6v111.6c0 8-6.4 14.5-14.5 14.5-4.2 0-8.1-1.9-10.9-5-7.9-8.7-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.5 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5v111.6h-111.6c-8 0-14.5 6.5-14.5 14.5 0 4.2 1.9 8.1 5 10.9 7.5 6.8 12.3 16.7 12.3 27.7 0 22-19 39.5-41.6 37.1-16.8-1.9-30.5-15.3-32.7-32-1.7-13 3.3-24.9 11.9-32.7 3.1-2.9 5-6.8 5-11 0-8-6.4-14.5-14.5-14.5z" }
  ];
  PIECES.forEach(function (p) { p.path = new Path2D(p.d); });
  var SLIDES = [["Slide 01", "Quiet structure"], ["Slide 02", "Bright signal"], ["Slide 03", "Deep canopy"]];
  var slides = [], ctxs = [];
  var cur = 0, anim = null, raf = 0, timer = 0, W = 0, H = 0, ro = null;
  var gl = null, prog = null, u = {}, texs = [], ctx2 = null;

  function piece(c, k, cx, cy, size, rot, fill, alpha) {
    var p = PIECES[k], vb = p.vb;
    var s = size / Math.max(vb[2], vb[3]);
    c.save();
    c.globalAlpha = alpha;
    c.translate(cx, cy);
    c.rotate(rot);
    c.scale(s, s);
    c.translate(-(vb[0] + vb[2] / 2), -(vb[1] + vb[3] / 2));
    c.fillStyle = fill;
    c.fill(p.path);
    c.restore();
  }
  function paint(i) {
    var c = ctxs[i], w = slides[i].width, h = slides[i].height, m = Math.min(w, h), g;
    if (i === 0) {
      g = c.createLinearGradient(0, 0, w, h); g.addColorStop(0, "#005236"); g.addColorStop(1, "#0c1e18");
      c.fillStyle = g; c.fillRect(0, 0, w, h);
      piece(c, 1, w * 0.18, h * 0.82, m * 0.9, -0.3, "#2f7d5b", 0.55);
      piece(c, 0, w * 0.66, h * 0.44, m * 0.78, 0.22, opts.accent, 1);
      piece(c, 2, w * 0.92, h * 0.12, m * 0.5, 0.5, "#fbf8f1", 0.9);
    } else if (i === 1) {
      g = c.createLinearGradient(w, 0, 0, h); g.addColorStop(0, "#e6ff55"); g.addColorStop(1, "#c6f26b");
      c.fillStyle = g; c.fillRect(0, 0, w, h);
      piece(c, 0, w * 0.82, h * 0.82, m * 0.7, 0.4, "#0a6b46", 0.5);
      piece(c, 1, w * 0.4, h * 0.45, m * 1.25, -0.12, "#005236", 1);
      piece(c, 2, w * 0.1, h * 0.18, m * 0.5, -0.4, "#0c1e18", 0.9);
    } else {
      g = c.createRadialGradient(w * 0.7, h * 0.3, 0, w * 0.5, h * 0.5, Math.max(w, h) * 0.85);
      g.addColorStop(0, "#2f7d5b"); g.addColorStop(0.5, "#0a6b46"); g.addColorStop(1, "#0c1e18");
      c.fillStyle = g; c.fillRect(0, 0, w, h);
      piece(c, 2, w * 0.5, h * 0.52, m * 1.1, 0.12, "#fbf8f1", 1);
      piece(c, 0, w * 0.14, h * 0.2, m * 0.5, -0.5, opts.accent, 0.95);
      piece(c, 1, w * 0.88, h * 0.86, m * 0.6, 0.3, "#c6f26b", 0.7);
    }
  }
  function paintAll() { for (var i = 0; i < 3; i++) paint(i); }

  var VS = "attribute vec2 p;varying vec2 v;void main(){v=p*0.5+0.5;gl_Position=vec4(p,0.,1.);}";
  var FS = "precision mediump float;varying vec2 v;uniform sampler2D a,b;uniform float t,k,s,tm;" +
    "float h(vec2 q){return fract(sin(dot(q,vec2(127.1,311.7)))*43758.5453);}" +
    "float n(vec2 q){vec2 i=floor(q),f=fract(q);f=f*f*(3.-2.*f);return mix(mix(h(i),h(i+vec2(1,0)),f.x),mix(h(i+vec2(0,1)),h(i+vec2(1,1)),f.x),f.y);}" +
    "float fb(vec2 q){return n(q)*.55+n(q*2.03+4.7)*.3+n(q*4.1+9.1)*.15;}" +
    "void main(){vec2 q=v*s+vec2(tm*.35,-tm*.2);vec2 d=vec2(fb(q)-.5,fb(q+vec2(5.2,1.3))-.5)*2.;" +
    "float w=sin(3.14159*t);vec2 ua=v+d*k*t*.5*(1.+w);vec2 ub=v-d*k*(1.-t)*.5*(1.+w);" +
    "float m=smoothstep(.0,1.,clamp((t*1.6-.3)+(fb(q*.8)-.5)*.9,0.,1.));" +
    "gl_FragColor=mix(texture2D(a,clamp(ua,0.,1.)),texture2D(b,clamp(ub,0.,1.)),m);}";

  function shader(type, src) {
    var sh = gl.createShader(type); gl.shaderSource(sh, src); gl.compileShader(sh);
    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error("shader");
    return sh;
  }
  function initGL() {
    try {
      gl = cv.getContext("webgl", { antialias: false, premultipliedAlpha: false });
      if (!gl) return false;
      prog = gl.createProgram();
      gl.attachShader(prog, shader(gl.VERTEX_SHADER, VS));
      gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, FS));
      gl.linkProgram(prog);
      if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return false;
      gl.useProgram(prog);
      var buf = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buf);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
      var loc = gl.getAttribLocation(prog, "p");
      gl.enableVertexAttribArray(loc);
      gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
      ["a", "b", "t", "k", "s", "tm"].forEach(function (n) { u[n] = gl.getUniformLocation(prog, n); });
      gl.uniform1i(u.a, 0); gl.uniform1i(u.b, 1);
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
      for (var i = 0; i < 3; i++) {
        var tx = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tx);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        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);
        texs.push(tx);
      }
      return true;
    } catch (e) { gl = null; return false; }
  }
  function upload() {
    if (!gl) return;
    for (var i = 0; i < 3; i++) {
      gl.bindTexture(gl.TEXTURE_2D, texs[i]);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, slides[i]);
    }
  }
  // draw: from slide a to slide b at progress t (0..1)
  function draw(a, b, t) {
    if (!W) return;
    if (gl) {
      gl.viewport(0, 0, cv.width, cv.height);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texs[a]);
      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, texs[b]);
      gl.uniform1f(u.t, t); gl.uniform1f(u.k, opts.intensity); gl.uniform1f(u.s, opts.scale);
      gl.uniform1f(u.tm, performance.now() / 1000);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    } else if (ctx2) {
      ctx2.globalAlpha = 1; ctx2.drawImage(slides[a], 0, 0);
      ctx2.globalAlpha = t; ctx2.drawImage(slides[b], 0, 0);
      ctx2.globalAlpha = 1;
    }
  }
  function size() {
    var r = wrap.getBoundingClientRect();
    var dpr = Math.min(window.devicePixelRatio || 1, 2);
    var w = Math.max(2, Math.round(r.width * dpr)), h = Math.max(2, Math.round(r.height * dpr));
    if (w === W && h === H) return;
    W = w; H = h;
    cv.width = w; cv.height = h;
    for (var i = 0; i < 3; i++) { slides[i].width = w; slides[i].height = h; }
    paintAll(); upload();
    draw(cur, cur, 1);
  }
  function caption(ix) {
    if (ix == null) ix = target();
    capK.textContent = SLIDES[ix][0] + " / 03";
    capT.textContent = SLIDES[ix][1];
    dots.forEach(function (d, i) {
      d.classList.toggle("is-on", i === ix);
      d.setAttribute("aria-current", i === ix ? "true" : "false");
    });
  }
  function frame(now) {
    raf = 0;
    if (!anim) return;
    var t = Math.min(1, (now - anim.t0) / opts.duration);
    var e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
    draw(anim.from, anim.to, e);
    if (t >= 1) { cur = anim.to; anim = null; draw(cur, cur, 1); schedule(); return; }
    raf = requestAnimationFrame(frame);
  }
  function go(to) {
    to = (to + 3) % 3;
    if (anim) { cur = anim.to; anim = null; if (raf) { cancelAnimationFrame(raf); raf = 0; } }
    if (to === cur) return;
    if (reduce || opts.duration < 30) { cur = to; draw(cur, cur, 1); caption(); schedule(); return; }
    anim = { from: cur, to: to, t0: performance.now() };
    caption(to);
    raf = requestAnimationFrame(frame);
  }
  function target() { return anim ? anim.to : cur; }
  function schedule() {
    clearTimeout(timer); timer = 0;
    if (!opts.autoplay || reduce) return;
    timer = setTimeout(function () { go(target() + 1); }, opts.autoplay * 1000);
  }
  function listen(el, ev, fn, op) { el.addEventListener(ev, fn, op); offs.push(function () { el.removeEventListener(ev, fn, op); }); }

  function build() {
    for (var i = 0; i < 3; i++) {
      slides.push(document.createElement("canvas"));
      ctxs.push(slides[i].getContext("2d"));
      var d = document.createElement("button");
      d.type = "button"; d.className = "ef-lds__dot";
      d.setAttribute("aria-label", "Go to slide " + (i + 1));
      (function (i) { listen(d, "click", function () { go(i); }); })(i);
      dotsEl.appendChild(d); dots.push(d);
    }
    if (!initGL()) { gl = null; ctx2 = cv.getContext("2d"); }
    Array.prototype.forEach.call(root.querySelectorAll(".ef-lds__btn"), function (b) {
      listen(b, "click", function () { go(target() + Number(b.getAttribute("data-d"))); });
    });
    listen(wrap, "keydown", function (e) {
      if (e.key === "ArrowRight") { e.preventDefault(); go(target() + 1); }
      else if (e.key === "ArrowLeft") { e.preventDefault(); go(target() - 1); }
    });
    listen(wrap, "mouseenter", function () { clearTimeout(timer); });
    listen(wrap, "mouseleave", schedule);
    listen(wrap, "focusin", function () { clearTimeout(timer); });
    listen(wrap, "focusout", schedule);
    ro = new ResizeObserver(size);
    ro.observe(wrap);
  }
  function update(next) {
    var prev = opts;
    opts = Object.assign({ accent: "#e6ff55", intensity: 0.7, duration: 1200, scale: 3, autoplay: 0 }, opts, next || {});
    wrap.style.setProperty("--ef-acc", opts.accent);
    if (W && prev.accent !== opts.accent) { paintAll(); upload(); if (!anim) draw(cur, cur, 1); }
    caption();
    schedule();
  }
  build();
  update(o);
  size();
  return {
    update: update,
    destroy: function () {
      cancelAnimationFrame(raf); raf = 0; anim = null; clearTimeout(timer);
      if (ro) ro.disconnect();
      offs.forEach(function (f) { f(); }); offs = [];
      if (gl) { var ext = gl.getExtension("WEBGL_lose_context"); if (ext) ext.loseContext(); }
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"accent":"#e6ff55","intensity":0.7,"duration":1200,"scale":3,"autoplay":0});

How it works

Each slide is painted once into an offscreen 2D canvas: a gradient background plus the Enrichr puzzle-piece paths drawn with Path2D. Those canvases are uploaded as WebGL textures, so there are no image files. A fragment shader samples the outgoing and incoming slide and pushes both along a fractal value-noise flow field. The push is strongest in the middle of the transition, and a noise-shaped mask decides where the new slide takes over, so the change looks like liquid rather than a fade. A requestAnimationFrame loop runs only while a transition is playing and the canvas is static otherwise. If WebGL is not available, the same offscreen slides are crossfaded on a 2D canvas.

Accessibility and performance

The slideshow is a focusable carousel group. Left and right arrow keys, the prev and next buttons and the dot buttons all change slide, and the caption is an aria-live region so the new slide is announced. The canvas itself is decorative and hidden from assistive tech. Reduced motion swaps slides instantly and turns autoplay off; autoplay also pauses on hover and focus.

One full-size canvas and one small fragment shader that runs only during a transition. The canvas is capped at 2x pixel ratio and slides are repainted only on resize or accent change. Idle cost is zero.

SectionGallery
TechniquesWebGL, Carousel, Reveal and intro
StackVanilla JS, WebGL, Canvas
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
Inspired byLiquidDistortion by Codrops on github.com. Original code, written from scratch.

Find this technique on a real site

Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.

Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns

Licence and provenance

Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.

This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.

WebGL image transitionThree generated slides that change with a WebGL shader transition: dissolve, wipe-warp or pixel. Shape clip-path slideshowA slideshow whose gradient slides are revealed through a clip-path shape that morphs between a circle, diamond, hexagon and star, with prev/next buttons, dots and arrow keys. Filter reveal slideshowA gradient slideshow where each slide change melts through an animated SVG turbulence displacement and blur, with prev/next, dots and arrow keys.

FAQ

Does it need image files?

No. The slides are drawn procedurally with gradients and vector paths, then used as textures. To use photos, draw them into the slide canvases with drawImage instead of the gradients.

What happens without WebGL?

It falls back to a 2D canvas and crossfades the same slides, so the slideshow still works.

How do I change how wobbly the transition is?

Use the Distortion and Ripple scale controls. Distortion sets how far pixels are pushed, ripple scale sets the size of the noise features.