# Refracted Glass (Unicorn Studio)

> Refracted glass visuals with luminous distortion — atmospheric hero treatment for brand and product stories.

**Definition:** Refracted Glass (Unicorn Studio) is a components UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Components
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-01
- Canonical page: https://html.bosconvertpro.com/collection/refracted-glass-unicorn-studio
- Full-page preview: https://html.bosconvertpro.com/collection/refracted-glass-unicorn-studio/full
- Live demo: https://html.bosconvertpro.com/demos/refracted-glass-unicorn-studio/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/refracted-glass-unicorn-studio/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/refracted-glass-unicorn-studio/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/refracted-glass-unicorn-studio.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Refracted Glass (Unicorn Studio)?

Refracted Glass (Unicorn Studio) is a components UI animation from collections.html. Refracted glass visuals with luminous distortion — atmospheric hero treatment for brand and product stories.

### What is Refracted Glass (Unicorn Studio) built with?

Refracted Glass (Unicorn Studio) is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Refracted Glass (Unicorn Studio) in my project?

Open the animation page (https://html.bosconvertpro.com/collection/refracted-glass-unicorn-studio), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/refracted-glass-unicorn-studio/index.html.

### Is Refracted Glass (Unicorn Studio) free to use?

Yes. The Refracted Glass (Unicorn Studio) demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Refracted Glass</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link rel="preconnect" href="https://cdn.jsdelivr.net" />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="app">
      <div
        class="refract"
        data-refract
        role="img"
        aria-label="Male portrait with animated refracted glass effect. Move your cursor across the image."
      >
        <canvas class="refract__canvas" data-refract-canvas></canvas>
        <div class="refract__loader" data-refract-loader aria-hidden="true">
          <span class="refract__loader-bar"></span>
        </div>
      </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --bg-0: #050505;
  --bg-1: #0c0c0e;
  --bg-2: #141418;
  --card-radius: 0.85em;
  --card-max: 32em;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}

html,
body {
  width: 100%;
  height: 100%;
}

body {
  font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
  color: #f2f2f2;
  background:
    radial-gradient(
      120% 90% at 50% 42%,
      rgba(40, 40, 48, 0.55) 0%,
      transparent 58%
    ),
    radial-gradient(80% 60% at 50% 100%, rgba(20, 20, 28, 0.9) 0%, transparent 55%),
    linear-gradient(165deg, var(--bg-2) 0%, var(--bg-1) 42%, var(--bg-0) 100%);
  background-color: var(--bg-0);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   Stage
   -------------------------------------------------------------------------- */

.app {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.25rem, 4vw, 2.5rem);
}

/* --------------------------------------------------------------------------
   Card
   -------------------------------------------------------------------------- */

.refract {
  --aspect: 1 / 1.25;

  position: relative;
  width: min(100%, var(--card-max));
  aspect-ratio: var(--aspect);
  border-radius: var(--card-radius);
  overflow: hidden;
  background: #0a0a0a;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.06),
    0 1px 0 rgba(255, 255, 255, 0.04) inset,
    0 24px 48px rgba(0, 0, 0, 0.55),
    0 48px 100px rgba(0, 0, 0, 0.45);
  cursor: none;
  isolation: isolate;
  /* Entrance start state — GSAP animates these */
  opacity: 0;
  transform: scale(0.96);
  will-change: transform, opacity;
}

.refract.is-ready {
  cursor: crosshair;
}

.refract__canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* --------------------------------------------------------------------------
   Loader
   -------------------------------------------------------------------------- */

.refract__loader {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: #0a0a0a;
  transition: opacity 0.45s var(--ease-out);
  pointer-events: none;
  z-index: 2;
}

.refract__loader.is-hidden {
  opacity: 0;
  visibility: hidden;
}

.refract__loader-bar {
  width: 2.5rem;
  height: 2px;
  border-radius: 999px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.7) 50%,
    transparent 100%
  );
  background-size: 200% 100%;
  animation: loader-sweep 1.1s ease-in-out infinite;
}

@keyframes loader-sweep {
  0% {
    background-position: 100% 0;
  }
  100% {
    background-position: -100% 0;
  }
}

/* --------------------------------------------------------------------------
   Reduced motion
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .refract {
    opacity: 1;
    transform: none;
  }

  .refract__loader-bar {
    animation: none;
    background: rgba(255, 255, 255, 0.45);
  }
}
````

## Source — script.js
````javascript
(() => {
  "use strict";

  if (typeof gsap === "undefined") {
    console.error("[RefractedGlass] GSAP failed to load from CDN.");
    return;
  }

  /* ------------------------------------------------------------------------
   * Config
   * ------------------------------------------------------------------------ */

  const CONFIG = {
    /** Unsplash free — premium male editorial portrait */
    imageUrl:
      "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=1200&q=85",
    imageFallback: "https://picsum.photos/seed/refracted-glass-man/800/1000",

    /** Match Unicorn scene: frequency 0.25 → ~9.5 strip divisions */
    frequency: 0.25,
    /** Resting refraction amount (gradient) */
    amount: 0.5,
    /** Resting chromatic dispersion */
    dispersion: 0.08,
    /** Mouse influence on glass center (0–1) */
    mouseTrack: 0.18,
    /** Glass strip angle as fraction of a full turn (~0.25 ≈ vertical) */
    rotation: 0.2511,
    /** Strip scroll speed */
    speed: 0.05,
    /** Pointer lag (seconds) — lower = snappier */
    mouseLag: 0.55,
    /** Device pixel ratio cap */
    maxDpr: 2,
  };

  /* ------------------------------------------------------------------------
   * Shaders — port of Unicorn "blinds" refraction
   * ------------------------------------------------------------------------ */

  const VERT = /* glsl */ `
    attribute vec2 aPosition;
    varying vec2 vUv;

    void main() {
      vUv = aPosition * 0.5 + 0.5;
      gl_Position = vec4(aPosition, 0.0, 1.0);
    }
  `;

  const FRAG = /* glsl */ `
    precision highp float;

    varying vec2 vUv;

    uniform sampler2D uImage;
    uniform vec2 uResolution;
    uniform vec2 uImageSize;
    uniform vec2 uPos;
    uniform vec2 uMouse;
    uniform float uTime;
    uniform float uFrequency;
    uniform float uAmount;
    uniform float uDispersion;
    uniform float uMouseTrack;
    uniform float uRotation;
    uniform float uSpeed;

    const float PI = 3.14159265359;
    const float CA_STEPS = 8.0;

    mat2 rot(float a) {
      float c = cos(a);
      float s = sin(a);
      return mat2(c, -s, s, c);
    }

    /* Cover-fit UV so the portrait fills the card without letterboxing */
    vec2 coverUv(vec2 uv, vec2 canvas, vec2 image) {
      float canvasAspect = canvas.x / canvas.y;
      float imageAspect = image.x / image.y;
      vec2 scale = vec2(1.0);

      if (canvasAspect > imageAspect) {
        scale.y = imageAspect / canvasAspect;
      } else {
        scale.x = canvasAspect / imageAspect;
      }

      return (uv - 0.5) * scale + 0.5;
    }

    vec3 sampleImage(vec2 uv) {
      vec2 imageUv = coverUv(uv, uResolution, uImageSize);
      /* Soft clamp — darken slightly at extreme edges instead of hard wrap */
      vec2 edge = smoothstep(-0.02, 0.0, imageUv) * smoothstep(1.02, 1.0, imageUv);
      float mask = edge.x * edge.y;
      vec3 rgb = texture2D(uImage, clamp(imageUv, 0.0, 1.0)).rgb;
      return rgb * mask;
    }

    vec3 chromatic(vec2 st, vec2 aberrated) {
      vec3 sum = vec3(0.0);
      float inv = 1.0 / CA_STEPS;
      float invHalf = inv * 0.5;

      for (float i = 1.0; i <= CA_STEPS; i++) {
        vec2 offset = aberrated * (i * inv);
        sum.r += sampleImage(st - offset).r * inv;
        sum.b += sampleImage(st + offset).b * inv;
        sum.g += sampleImage(st - offset * 0.5).g * invHalf;
        sum.g += sampleImage(st + offset * 0.5).g * invHalf;
      }

      return sum;
    }

    /*
     * Glass-blinds refraction (ported from Unicorn Studio "blinds" effect).
     * 1. Rotate into strip space (~vertical panels)
     * 2. Segment UV with a soft triangle wave
     * 3. Push R/B channels in opposite directions (glass prism feel)
     * 4. Multi-tap average → streaky motion-glass look
     * 5. Optional chromatic dispersion
     */
    vec3 refractBlinds(vec2 uv) {
      float aspect = uResolution.x / max(uResolution.y, 1.0);
      vec2 mousePos = uPos + (uMouse - 0.5) * uMouseTrack;

      vec2 st = uv * vec2(aspect, 1.0);
      vec2 pos = mousePos * vec2(aspect, 1.0);

      float angle = uRotation * -2.0 * PI;
      mat2 R = rot(angle);
      mat2 Ri = rot(-angle);

      st = R * (st - pos) + pos;

      float divisions = 2.0 + uFrequency * 30.0;
      float distPow = 2.6;
      float amount = uAmount;

      /* Pixel footprint in strip-space — multi-tap = streaky glass blur */
      vec2 texel = vec2(
        1.5 / max(uResolution.x, 1.0) * aspect,
        1.5 / max(uResolution.y, 1.0)
      );

      /* Unrolled taps (WebGL1-safe — no dynamic array indexing) */
      vec2 accum = vec2(0.0);
      vec3 distortAvg = vec3(0.0);

      for (int i = 0; i < 5; i++) {
        vec2 tapOffset = vec2(0.0);
        if (i == 1) tapOffset = vec2(texel.x, 0.0);
        else if (i == 2) tapOffset = vec2(0.0, texel.y);
        else if (i == 3) tapOffset = vec2(-texel.x, 0.0);
        else if (i == 4) tapOffset = vec2(0.0, -texel.y);

        vec2 s = st + tapOffset;

        float segment = fract((s.y - pos.y + uTime * uSpeed * 0.2) * divisions);
        float tri = smoothstep(0.0, 0.5, segment) - smoothstep(0.5, 1.0, segment);
        segment = mix(segment, tri, 0.2);

        vec3 first = vec3(1.0, 0.0, 0.0);
        vec3 second = vec3(0.0, 1.0, 0.0);
        vec3 third = vec3(0.0, 0.0, 1.0);

        vec3 distort = mix(
          mix(first, second, clamp(segment * 2.0, 0.0, 1.0)),
          mix(second, third, clamp((segment - 0.5) / 0.5, 0.0, 1.0)),
          step(0.5, segment)
        );

        s.y -= pow(max(distort.r, 0.0), distPow) / 10.0 * amount;
        s.y += pow(max(distort.b, 0.0), distPow) / 10.0 * amount;

        s = Ri * (s - pos) + pos;
        s /= vec2(aspect, 1.0);

        accum += s;
        distortAvg += distort;
      }

      vec2 warped = accum / 5.0;
      distortAvg /= 5.0;

      if (uDispersion > 0.001 && amount > 0.001) {
        vec2 disp =
          vec2(
            pow(max(distortAvg.r, 0.0), distPow),
            pow(max(distortAvg.b, 0.0), distPow)
          ) *
          0.085 *
          amount *
          uDispersion;

        /* Dispersion runs mostly along the strip direction (near-vertical) */
        vec2 dir = Ri * vec2(0.0, 1.0);
        dir.x /= aspect;
        return chromatic(warped, dir * length(disp) * 1.4 + disp * vec2(0.35, 1.0));
      }

      return sampleImage(warped);
    }

    void main() {
      /* Flip Y so orientation matches a normal image element */
      vec2 uv = vec2(vUv.x, 1.0 - vUv.y);
      vec3 color = refractBlinds(uv);
      gl_FragColor = vec4(color, 1.0);
    }
  `;

  /* ------------------------------------------------------------------------
   * WebGL helpers
   * ------------------------------------------------------------------------ */

  function createShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      const info = gl.getShaderInfoLog(shader);
      gl.deleteShader(shader);
      throw new Error(`Shader compile failed: ${info}`);
    }

    return shader;
  }

  function createProgram(gl, vertSrc, fragSrc) {
    const program = gl.createProgram();
    const vs = createShader(gl, gl.VERTEX_SHADER, vertSrc);
    const fs = createShader(gl, gl.FRAGMENT_SHADER, fragSrc);

    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      const info = gl.getProgramInfoLog(program);
      gl.deleteProgram(program);
      throw new Error(`Program link failed: ${info}`);
    }

    gl.deleteShader(vs);
    gl.deleteShader(fs);
    return program;
  }

  function loadImage(url, fallback) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.crossOrigin = "anonymous";

      img.onload = () => resolve(img);
      img.onerror = () => {
        if (!fallback || url === fallback) {
          reject(new Error(`Failed to load image: ${url}`));
          return;
        }
        const retry = new Image();
        retry.crossOrigin = "anonymous";
        retry.onload = () => resolve(retry);
        retry.onerror = () => reject(new Error(`Failed to load fallback image`));
        retry.src = fallback;
      };

      img.src = url;
    });
  }

  /* ------------------------------------------------------------------------
   * RefractedGlass
   * ------------------------------------------------------------------------ */

  class RefractedGlass {
    constructor(root) {
      this.root = root;
      this.canvas = root.querySelector("[data-refract-canvas]");
      this.loader = root.querySelector("[data-refract-loader]");

      this.gl = null;
      this.program = null;
      this.uniforms = {};
      this.texture = null;
      this.imageSize = { x: 1, y: 1 };

      this.raf = 0;
      this.startTime = 0;
      this.running = false;
      this.pointerInside = false;

      /** Logical state driven by GSAP */
      this.state = {
        amount: 0,
        dispersion: 1,
        posX: 0.6,
        posY: 0.5,
        mouseX: 0.5,
        mouseY: 0.5,
      };

      this.onPointerMove = this.onPointerMove.bind(this);
      this.onPointerEnter = this.onPointerEnter.bind(this);
      this.onPointerLeave = this.onPointerLeave.bind(this);
      this.onResize = this.onResize.bind(this);
      this.draw = this.draw.bind(this);
    }

    async init() {
      this.gl = this.canvas.getContext("webgl", {
        alpha: false,
        antialias: false,
        premultipliedAlpha: false,
        preserveDrawingBuffer: false,
      });

      if (!this.gl) {
        throw new Error("WebGL is not available in this browser.");
      }

      this.setupProgram();
      this.setupGeometry();

      const image = await loadImage(CONFIG.imageUrl, CONFIG.imageFallback);
      this.uploadTexture(image);
      this.imageSize = { x: image.naturalWidth, y: image.naturalHeight };

      this.onResize();
      window.addEventListener("resize", this.onResize, { passive: true });

      this.root.addEventListener("pointermove", this.onPointerMove, {
        passive: true,
      });
      this.root.addEventListener("pointerenter", this.onPointerEnter, {
        passive: true,
      });
      this.root.addEventListener("pointerleave", this.onPointerLeave, {
        passive: true,
      });

      this.hideLoader();
      this.root.classList.add("is-ready");

      this.startTime = performance.now();
      this.running = true;
      this.raf = requestAnimationFrame(this.draw);

      this.playIntro();
    }

    setupProgram() {
      const gl = this.gl;
      this.program = createProgram(gl, VERT, FRAG);
      gl.useProgram(this.program);

      const names = [
        "uImage",
        "uResolution",
        "uImageSize",
        "uPos",
        "uMouse",
        "uTime",
        "uFrequency",
        "uAmount",
        "uDispersion",
        "uMouseTrack",
        "uRotation",
        "uSpeed",
      ];

      for (const name of names) {
        this.uniforms[name] = gl.getUniformLocation(this.program, name);
      }
    }

    setupGeometry() {
      const gl = this.gl;
      const buffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      /* Fullscreen triangle strip: clip-space quad */
      gl.bufferData(
        gl.ARRAY_BUFFER,
        new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
        gl.STATIC_DRAW
      );

      const loc = gl.getAttribLocation(this.program, "aPosition");
      gl.enableVertexAttribArray(loc);
      gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    }

    uploadTexture(image) {
      const gl = this.gl;
      this.texture = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, this.texture);
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
      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);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    }

    onResize() {
      const gl = this.gl;
      const rect = this.root.getBoundingClientRect();
      const dpr = Math.min(window.devicePixelRatio || 1, CONFIG.maxDpr);
      const w = Math.max(1, Math.round(rect.width * dpr));
      const h = Math.max(1, Math.round(rect.height * dpr));

      if (this.canvas.width !== w || this.canvas.height !== h) {
        this.canvas.width = w;
        this.canvas.height = h;
        gl.viewport(0, 0, w, h);
      }
    }

    /* ------------------------------------------------------------------
     * Pointer — GSAP-smoothed mouse uniforms
     * ------------------------------------------------------------------ */

    onPointerEnter() {
      this.pointerInside = true;
    }

    onPointerLeave() {
      this.pointerInside = false;
      /* Ease mouse back to center when leaving the card */
      gsap.to(this.state, {
        mouseX: 0.5,
        mouseY: 0.5,
        duration: 0.9,
        ease: "power3.out",
        overwrite: "auto",
      });
    }

    onPointerMove(event) {
      const rect = this.root.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) return;

      const x = (event.clientX - rect.left) / rect.width;
      const y = (event.clientY - rect.top) / rect.height;

      gsap.to(this.state, {
        mouseX: gsap.utils.clamp(0, 1, x),
        mouseY: gsap.utils.clamp(0, 1, y),
        duration: CONFIG.mouseLag,
        ease: "power3.out",
        overwrite: "auto",
      });
    }

    /* ------------------------------------------------------------------
     * Intro — mirrors Unicorn appear states
     * frequency fixed, amount 0→0.5, pos 0.6→0.5, dispersion 1→rest
     * ------------------------------------------------------------------ */

    playIntro() {
      const reduceMotion = window.matchMedia(
        "(prefers-reduced-motion: reduce)"
      ).matches;

      if (reduceMotion) {
        this.state.amount = CONFIG.amount;
        this.state.dispersion = CONFIG.dispersion;
        this.state.posX = 0.5;
        this.state.posY = 0.5;
        gsap.set(this.root, { opacity: 1, scale: 1 });
        return;
      }

      const tl = gsap.timeline({ defaults: { ease: "power3.out" } });

      tl.to(
        this.root,
        {
          opacity: 1,
          scale: 1,
          duration: 1.05,
          ease: "power4.out",
        },
        0
      );

      tl.to(
        this.state,
        {
          amount: CONFIG.amount,
          posX: 0.5,
          posY: 0.5,
          dispersion: CONFIG.dispersion,
          duration: 1.15,
          ease: "power2.out",
        },
        0.05
      );
    }

    hideLoader() {
      if (!this.loader) return;
      this.loader.classList.add("is-hidden");
      window.setTimeout(() => {
        this.loader?.remove();
        this.loader = null;
      }, 500);
    }

    /* ------------------------------------------------------------------
     * Render loop
     * ------------------------------------------------------------------ */

    draw(now) {
      if (!this.running) return;
      this.raf = requestAnimationFrame(this.draw);

      const gl = this.gl;
      const t = (now - this.startTime) * 0.001;
      const { state } = this;

      gl.useProgram(this.program);
      gl.activeTexture(gl.TEXTURE0);
      gl.bindTexture(gl.TEXTURE_2D, this.texture);

      gl.uniform1i(this.uniforms.uImage, 0);
      gl.uniform2f(
        this.uniforms.uResolution,
        this.canvas.width,
        this.canvas.height
      );
      gl.uniform2f(this.uniforms.uImageSize, this.imageSize.x, this.imageSize.y);
      gl.uniform2f(this.uniforms.uPos, state.posX, state.posY);
      gl.uniform2f(this.uniforms.uMouse, state.mouseX, state.mouseY);
      gl.uniform1f(this.uniforms.uTime, t);
      gl.uniform1f(this.uniforms.uFrequency, CONFIG.frequency);
      gl.uniform1f(this.uniforms.uAmount, state.amount);
      gl.uniform1f(this.uniforms.uDispersion, state.dispersion);
      gl.uniform1f(this.uniforms.uMouseTrack, CONFIG.mouseTrack);
      gl.uniform1f(this.uniforms.uRotation, CONFIG.rotation);
      gl.uniform1f(this.uniforms.uSpeed, CONFIG.speed);

      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }

    destroy() {
      this.running = false;
      cancelAnimationFrame(this.raf);
      window.removeEventListener("resize", this.onResize);
      this.root.removeEventListener("pointermove", this.onPointerMove);
      this.root.removeEventListener("pointerenter", this.onPointerEnter);
      this.root.removeEventListener("pointerleave", this.onPointerLeave);

      const gl = this.gl;
      if (gl && this.texture) gl.deleteTexture(this.texture);
      if (gl && this.program) gl.deleteProgram(this.program);
    }
  }

  /* ------------------------------------------------------------------------
   * Boot
   * ------------------------------------------------------------------------ */

  async function boot() {
    const root = document.querySelector("[data-refract]");
    if (!root) return;

    const effect = new RefractedGlass(root);

    try {
      await effect.init();
    } catch (err) {
      console.error("[RefractedGlass]", err);
      root.classList.add("is-ready");
      const loader = root.querySelector("[data-refract-loader]");
      if (loader) {
        loader.innerHTML =
          '<span style="color:#fff;font-size:0.75rem;opacity:0.7">Unable to load effect</span>';
      }
    }
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot, { once: true });
  } else {
    boot();
  }
})();
````
