06.26c10v4 Image Grayscale Hover Effect v1

<!-- 06.10cv4 Image Grayscale Hover Effect -->
<script>
window.IMG_EFFECT_CONFIG = {
  sectionSelector: '#page', // CSS selector — targets all image blocks on the page
  blackMaskAlpha: 0.15,  // Dark overlay opacity on grayscale state (0 = no overlay, 1 = fully black)
  enterDuration: 2.0,    // Duration in seconds for color reveal on mouse enter
  leaveDuration: 1.6,    // Duration in seconds for color fade out on mouse leave
  softEdge: 0.18,        // Softness of the reveal edge (0 = hard edge, higher = more feathered)
  enableWave: false,      // Enable/disable the ripple wave distortion effect (true / false)
  waveAmp: 0.012,        // Wave distortion strength (higher = more distortion)
  waveFreq: 18.0,        // Wave frequency — number of ripple rings (higher = more rings)
  waveSpeed: 3.5,        // Wave animation speed (higher = faster ripple)
  waveDecay: 3.5         // How quickly the wave fades with distance from mouse (higher = shorter wave reach)
};
</script>

<script>
(function() {
  var CONFIG = window.IMG_EFFECT_CONFIG;

  var VERT = `
    attribute vec2 a_position;
    attribute vec2 a_uv;
    varying vec2 v_uv;
    void main() {
      v_uv = a_uv;
      gl_Position = vec4(a_position, 0.0, 1.0);
    }
  `;

  var FRAG = `
    precision mediump float;
    uniform sampler2D u_texture;
    uniform float u_progress;
    uniform vec2 u_mouse;
    uniform float u_time;
    uniform float u_softEdge;
    uniform float u_blackAlpha;
    uniform float u_waveAmp;
    uniform float u_waveFreq;
    uniform float u_waveSpeed;
    uniform float u_waveDecay;
    varying vec2 v_uv;

    void main() {
      vec2 uv = v_uv;

      float dist = length(uv - u_mouse);

      float wave = u_waveAmp
        * exp(-u_waveDecay * dist)
        * sin(u_waveFreq * dist - u_waveSpeed * u_time)
        * u_progress
        * (1.0 - u_progress * 0.5);

      vec2 dir = normalize(uv - u_mouse + vec2(0.0001));
      uv += dir * wave;
      uv = clamp(uv, 0.0, 1.0);

      vec4 color = texture2D(u_texture, uv);
      float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114));
      vec4 gray = vec4(vec3(lum), color.a);

      float dx = max(u_mouse.x, 1.0 - u_mouse.x);
      float dy = max(u_mouse.y, 1.0 - u_mouse.y);
      float maxDist = length(vec2(dx, dy)) * 1.05;

      float adjustedProgress = u_progress * maxDist;
      float mask = smoothstep(adjustedProgress, adjustedProgress - u_softEdge * maxDist, dist);

      vec4 revealed = mix(gray, color, mask);
      float darken = u_blackAlpha * (1.0 - mask * 0.8);
      revealed.rgb *= (1.0 - darken);

      gl_FragColor = revealed;
    }
  `;

  var allInstances = [];

  function compileShader(gl, type, src) {
    var s = gl.createShader(type);
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      console.warn('Shader error:', gl.getShaderInfoLog(s));
      gl.deleteShader(s);
      return null;
    }
    return s;
  }

  function createProgram(gl) {
    var vs = compileShader(gl, gl.VERTEX_SHADER, VERT);
    var fs = compileShader(gl, gl.FRAGMENT_SHADER, FRAG);
    if (!vs || !fs) return null;
    var prog = gl.createProgram();
    gl.attachShader(prog, vs);
    gl.attachShader(prog, fs);
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
      console.warn('Program link error:', gl.getProgramInfoLog(prog));
      return null;
    }
    return prog;
  }

  function init() {
    document.querySelectorAll(CONFIG.sectionSelector).forEach(function(section) {
      if (section.dataset.imgEffectDone) return;
      section.dataset.imgEffectDone = '1';
      section.querySelectorAll('img[data-sqsp-image-block-image]').forEach(function(img) {
        setupEffect(img);
      });
    });
  }

  function setupEffect(img) {
    var container = img.closest('.fluid-image-container');
    if (!container || container.dataset.glDone) return;
    container.dataset.glDone = '1';

    var canvas = document.createElement('canvas');
    canvas.style.cssText = [
      'position:absolute','top:0','left:0',
      'width:100%','height:100%',
      'pointer-events:none','z-index:2','display:block'
    ].join(';');

    var gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });
    if (!gl) return;

    var prog = createProgram(gl);
    if (!prog) return;

    container.style.position = 'relative';
    container.appendChild(canvas);

    var quad = new Float32Array([
      -1,-1, 0,1,
       1,-1, 1,1,
      -1, 1, 0,0,
       1, 1, 1,0
    ]);
    var buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);

    var aPos = gl.getAttribLocation(prog, 'a_position');
    var aUV  = gl.getAttribLocation(prog, 'a_uv');
    var uTex      = gl.getUniformLocation(prog, 'u_texture');
    var uProgress = gl.getUniformLocation(prog, 'u_progress');
    var uMouse    = gl.getUniformLocation(prog, 'u_mouse');
    var uTime     = gl.getUniformLocation(prog, 'u_time');
    var uSoft     = gl.getUniformLocation(prog, 'u_softEdge');
    var uBlack    = gl.getUniformLocation(prog, 'u_blackAlpha');
    var uWAmp     = gl.getUniformLocation(prog, 'u_waveAmp');
    var uWFreq    = gl.getUniformLocation(prog, 'u_waveFreq');
    var uWSpeed   = gl.getUniformLocation(prog, 'u_waveSpeed');
    var uWDecay   = gl.getUniformLocation(prog, 'u_waveDecay');

    var texture = gl.createTexture();
    var textureReady = false;

    function loadTexture(src) {
      var image = new Image();
      image.crossOrigin = 'anonymous';
      image.onload = function() {
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        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);
        textureReady = true;
        img.style.opacity = '0';
        renderFrame();
      };
      image.onerror = function() {
        var src2 = src.split('?')[0];
        if (src2 !== src) { image.src = src2; return; }
        console.warn('WebGL texture load failed');
      };
      image.src = src;
    }

    var imgSrc = img.src || img.getAttribute('data-src') || img.getAttribute('data-image');
    if (imgSrc) loadTexture(imgSrc);

    var state = {
      progress: 0,
      targetProgress: 0,
      startProgress: 0,
      startTime: 0,
      duration: CONFIG.enterDuration,
      rafId: null,
      mouseUV: [0.5, 0.5],
      time: 0,
      lastTs: 0
    };

    var instance = {
      forceLeave: function() {
        if (state.progress <= 0) return;
        if (state.rafId) cancelAnimationFrame(state.rafId);
        state.startProgress = state.progress;
        state.targetProgress = 0;
        state.duration = 0.3;
        state.startTime = 0;
        state.lastTs = 0;
        state.rafId = requestAnimationFrame(animate);
      }
    };
    allInstances.push(instance);

    function syncSize() {
      var rect = container.getBoundingClientRect();
      var dpr = window.devicePixelRatio || 1;
      var w = Math.round(rect.width * dpr);
      var h = Math.round(rect.height * dpr);
      if (canvas.width !== w || canvas.height !== h) {
        canvas.width = w;
        canvas.height = h;
        gl.viewport(0, 0, w, h);
      }
    }

    function renderFrame() {
      if (!textureReady) return;
      syncSize();
      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.enable(gl.BLEND);
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
      gl.useProgram(prog);
      gl.bindBuffer(gl.ARRAY_BUFFER, buf);
      gl.enableVertexAttribArray(aPos);
      gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
      gl.enableVertexAttribArray(aUV);
      gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8);
      gl.activeTexture(gl.TEXTURE0);
      gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.uniform1i(uTex, 0);
      gl.uniform1f(uProgress, state.progress);
      gl.uniform2fv(uMouse, state.mouseUV);
      gl.uniform1f(uTime, state.time);
      gl.uniform1f(uSoft, CONFIG.softEdge);
      gl.uniform1f(uBlack, CONFIG.blackMaskAlpha);
      gl.uniform1f(uWAmp, CONFIG.enableWave ? CONFIG.waveAmp : 0.0);
      gl.uniform1f(uWFreq, CONFIG.waveFreq);
      gl.uniform1f(uWSpeed, CONFIG.waveSpeed);
      gl.uniform1f(uWDecay, CONFIG.waveDecay);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }

    function easeOut(t) { return 1 - (1-t)*(1-t)*(1-t); }
    function easeInOut(t) { return t < 0.5 ? 2*t*t : -1+(4-2*t)*t; }

    function animate(ts) {
      var dt = state.lastTs ? (ts - state.lastTs) / 1000 : 0;
      state.lastTs = ts;
      state.time += dt;

      if (!state.startTime) state.startTime = ts;
      var raw = Math.min((ts - state.startTime) / (state.duration * 1000), 1);
      var easeFn = state.targetProgress >= state.startProgress ? easeOut : easeInOut;
      state.progress = state.startProgress + (state.targetProgress - state.startProgress) * easeFn(raw);

      renderFrame();

      var waveActive = CONFIG.enableWave && state.progress > 0 && state.progress < 1;
      if (raw < 1 || waveActive) {
        state.rafId = requestAnimationFrame(animate);
      } else {
        state.progress = state.targetProgress;
        renderFrame();
        state.rafId = null;
        state.lastTs = 0;
      }
    }

    function startAnim(target, dur) {
      if (state.rafId) cancelAnimationFrame(state.rafId);
      state.startProgress = state.progress;
      state.targetProgress = target;
      state.duration = dur;
      state.startTime = 0;
      state.lastTs = 0;
      state.rafId = requestAnimationFrame(animate);
    }

    function getUV(e) {
      var rect = container.getBoundingClientRect();
      var nw = img.naturalWidth, nh = img.naturalHeight;
      var cw = rect.width, ch = rect.height;
      var fx = 0, fy = 0, fw = cw, fh = ch;
      if (nw && nh) {
        var scale = Math.min(cw/nw, ch/nh);
        fw = nw*scale; fh = nh*scale;
        fx = (cw-fw)/2; fy = (ch-fh)/2;
      }
      return [
        Math.max(0, Math.min(1, (e.clientX - rect.left - fx) / fw)),
        Math.max(0, Math.min(1, (e.clientY - rect.top  - fy) / fh))
      ];
    }

    container.style.pointerEvents = 'auto';

    container.addEventListener('mouseenter', function(e) {
      allInstances.forEach(function(inst) {
        if (inst !== instance) inst.forceLeave();
      });
      state.mouseUV = getUV(e);
      startAnim(1, CONFIG.enterDuration);
    });

    container.addEventListener('mouseleave', function(e) {
      state.mouseUV = getUV(e);
      startAnim(0, CONFIG.leaveDuration);
    });

    container.addEventListener('mousemove', function(e) {
      state.mouseUV = getUV(e);
      if (!state.rafId && state.progress > 0) renderFrame();
    });

    window.addEventListener('resize', function() { syncSize(); renderFrame(); });

    syncSize();
    renderFrame();
  }

  function run() { init(); }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', run);
  } else {
    run();
  }

  window.addEventListener('mercury:load', function() {
    allInstances.length = 0;
    document.querySelectorAll(CONFIG.sectionSelector).forEach(function(s) {
      delete s.dataset.imgEffectDone;
      s.querySelectorAll('[data-gl-done]').forEach(function(el) {
        delete el.dataset.glDone;
      });
    });
    init();
  });
})();
</script>

 

Buy me a coffee