<!-- 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>