// Inline shaders (bundled for Webflow compatibility) const vertexShader = ` precision highp float; uniform vec2 u_resolution; attribute vec2 a_position; attribute vec4 a_color; varying vec4 v_color; void main(){ vec2 zeroToOne = a_position / u_resolution; vec2 clipSpace = (zeroToOne * 2.0 - 1.0); v_color = a_color; gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0); gl_PointSize = 3.5; }`; const fragmentShader = ` precision highp float; varying vec4 v_color; void main(){ if (v_color.a < 0.01) discard; vec2 coord = gl_PointCoord - vec2(0.5); float dist = length(coord); float alpha = 1.0 - smoothstep(0.0, 0.5, dist); gl_FragColor = vec4(v_color.rgb, v_color.a * alpha); }`; const config = { logoAttr: "data-logo-path", logoSize: 1400, logoColor: "#F8F8FF", canvasBg: "transparent", distortionRadius: 1000, forceStrength: 0.0020, maxDisplacement: 100, returnForce: 0.025, } // Get logoPath from canvas attribute function getLogoPath() { const canvasElement = document.getElementById("footer_cnvs"); return canvasElement ? canvasElement.getAttribute(config.logoAttr) || "https://cdn.prod.website-files.com/6686ac1afeef42fcf1929457/697cd10dfd3f68c63811957d_HYP_Lockup_White.svg" : "https://cdn.prod.website-files.com/6686ac1afeef42fcf1929457/697cd10dfd3f68c63811957d_HYP_Lockup_White.svg"; } let canvas, gl, program; let particles = []; let positionArray, colorArray; let positionBuffer, colorBuffer; let mouse = {x: 0, y: 0}; let animationCount = 0; let imageWidth = 0, imageHeight = 0; let initialCanvasWidth = 0, initialCanvasHeight = 0; function setupCanvas(){ canvas = document.getElementById("footer_cnvs"); const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; // Canvas size is controlled by CSS, don't override it } function setupWebGL(){ gl = canvas.getContext("webgl",{ alpha: true, depth: false, stencil: false, antialias: true, powerPreference: "high-performance", premultipliedAlpha: false, }); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); } function setupShaders(){ const vs = compileShader(gl.VERTEX_SHADER, vertexShader); const fs = compileShader(gl.FRAGMENT_SHADER, fragmentShader); program = gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); } function compileShader(type, source){ const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); return shader; } function loadLogo(){ const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = function(){ // Use canvas width as base for logoSize (responsive) const baseLogoSize = canvas.width; // Calculate aspect ratio preserving dimensions const imageAspect = image.width / image.height; const scale = 0.9; let canvasWidth, canvasHeight; let drawWidth, drawHeight; if (imageAspect > 1) { // Image is wider than tall (landscape) canvasWidth = baseLogoSize; canvasHeight = baseLogoSize / imageAspect; drawWidth = canvasWidth * scale; drawHeight = canvasHeight * scale; } else { // Image is taller than wide (portrait) or square canvasWidth = baseLogoSize * imageAspect; canvasHeight = baseLogoSize; drawWidth = canvasWidth * scale; drawHeight = canvasHeight * scale; } const tempCanvas = document.createElement("canvas"); const ctx = tempCanvas.getContext("2d"); tempCanvas.width = canvasWidth; tempCanvas.height = canvasHeight; // Fill canvas with transparent background ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Center the image const offsetX = (canvasWidth - drawWidth) / 2; const offsetY = (canvasHeight - drawHeight) / 2; ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight); const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight); imageWidth = canvasWidth; imageHeight = canvasHeight; createParticles(imageData.data, canvasWidth, canvasHeight); }; image.src = getLogoPath(); } function createParticles(pixels, imageWidth, imageHeight){ const centerX = canvas.width / 2; const centerY = canvas.height / 2; const positions = []; const colors = []; function hexToRgb(hex){ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16) / 255, g: parseInt(result[2], 16) / 255, b: parseInt(result[3], 16) / 255, }:{ r: 1, g: 1, b: 1 }; } const logoTint = hexToRgb(config.logoColor); for (let i = 0; i < imageHeight; i++){ for (let j = 0; j < imageWidth; j++){ const pixelIndex = (i * imageWidth + j) * 4; const alpha = pixels[pixelIndex + 3]; if (alpha > 10){ const particleX = centerX + (j - imageWidth / 2) * 1.0; const particleY = centerY + (i - imageHeight / 2) * 1.0; positions.push(particleX, particleY); const originalR = pixels[pixelIndex] / 255; const originalG = pixels[pixelIndex + 1] / 255; const originalB = pixels[pixelIndex + 2] / 255; const originalA = pixels[pixelIndex + 3] / 255; colors.push( originalR * logoTint.r, originalG * logoTint.g, originalB * logoTint.b, originalA, ); particles.push({ originalX: particleX, originalY: particleY, velocityX: 0, velocityY: 0, pixelCol: j, pixelRow: i, }); } } } positionArray = new Float32Array(positions); colorArray = new Float32Array(colors); // Store initial canvas dimensions for responsive scaling initialCanvasWidth = canvas.width; initialCanvasHeight = canvas.height; createBuffers(); animate(); } function createBuffers(){ positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, positionArray, gl.DYNAMIC_DRAW); colorBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW); } function animate(){ updatePhysics(); render(); requestAnimationFrame(animate); } function updatePhysics(){ if(animationCount <= 0) return; animationCount--; const radiusSquared = config.distortionRadius * config.distortionRadius; for (let i =0; i< particles.length;i++){ const particle = particles[i]; const currentX = positionArray[i*2]; const currentY = positionArray[i*2 + 1]; const deltaX = mouse.x - currentX; const deltaY = mouse.y - currentY; const distanceSquared = deltaX * deltaX + deltaY * deltaY; if(distanceSquared < radiusSquared && distanceSquared > 0){ const force = -radiusSquared / distanceSquared; const angle = Math.atan2(deltaY, deltaX); const distFromOrigin = Math.sqrt( (currentX - particle.originalX) ** 2 + (currentY - particle.originalY) ** 2 ); const forceMultiplier = Math.max(0.1, 1 - distFromOrigin / (config.maxDisplacement * 2)); particle.velocityX += force * Math.cos(angle) * config.forceStrength * forceMultiplier; particle.velocityY += force * Math.sin(angle) * config.forceStrength * forceMultiplier; } particle.velocityX *= 0.82; particle.velocityY *= 0.82; const targetX = currentX + particle.velocityX + (particle.originalX - currentX) * config.returnForce; const targetY = currentY + particle.velocityY + (particle.originalY - currentY) * config.returnForce; const offsetX = targetX - particle.originalX; const offsetY = targetY - particle.originalY; const distFromOrigin = Math.sqrt(offsetX * offsetX + offsetY * offsetY); if(distFromOrigin > config.maxDisplacement){ const excess = distFromOrigin - config.maxDisplacement; const scale = config.maxDisplacement / distFromOrigin; const dampedScale = scale + (1-scale) * Math.exp(-excess * 0.02); positionArray[i * 2] = particle.originalX + offsetX * dampedScale; positionArray[i * 2 + 1] = particle.originalY + offsetY * dampedScale; particle.velocityX *= 0.7; particle.velocityY *= 0.7; }else{ positionArray[i * 2] = targetX; positionArray[i * 2 + 1] = targetY; } } gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferSubData(gl.ARRAY_BUFFER, 0, positionArray); } function render(){ function hexToRgb(hex){ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16) / 255, g: parseInt(result[2], 16) / 255, b: parseInt(result[3], 16) / 255, }:{ r: 0, g: 0, b: 0 }; } gl.viewport( 0, 0, canvas.width, canvas.height); if (config.canvasBg === "transparent" || config.canvasBg === "rgba(0,0,0,0)") { gl.clearColor(0, 0, 0, 0); } else { const bgColor = hexToRgb(config.canvasBg); gl.clearColor(bgColor.r, bgColor.g, bgColor.b, 1.0); } gl.clear(gl.COLOR_BUFFER_BIT); if(particles.length === 0) return; gl.useProgram(program); const resolutionLoc = gl.getUniformLocation(program, "u_resolution"); gl.uniform2f(resolutionLoc, canvas.width, canvas.height); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); const positionLoc = gl.getAttribLocation(program, "a_position"); gl.enableVertexAttribArray(positionLoc); gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); const colorLoc = gl.getAttribLocation(program, "a_color"); gl.enableVertexAttribArray(colorLoc); gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.POINTS, 0, particles.length); } function setupEvents(){ document.addEventListener("mousemove", (event)=>{ const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; mouse.x = (event.clientX - rect.left) * dpr; mouse.y = (event.clientY - rect.top) * dpr; animationCount = 300; }) window.addEventListener("resize", ()=>{ setupCanvas(); if(particles.length > 0 && imageWidth > 0 && imageHeight > 0 && initialCanvasWidth > 0 && initialCanvasHeight > 0){ const centerX = canvas.width / 2; const centerY = canvas.height / 2; // Calculate scale factors for responsive sizing const scaleX = canvas.width / initialCanvasWidth; const scaleY = canvas.height / initialCanvasHeight; // Use uniform scaling to maintain aspect ratio const scale = Math.min(scaleX, scaleY); // Recalculate positions with proportional scaling for (let i = 0; i < particles.length; i++){ const particle = particles[i]; if (particle.pixelCol !== undefined && particle.pixelRow !== undefined){ // Scale the particle spacing proportionally const repositionX = centerX + (particle.pixelCol - imageWidth / 2) * scale; const repositionY = centerY + (particle.pixelRow - imageHeight / 2) * scale; particle.originalX = repositionX; particle.originalY = repositionY; positionArray[i * 2] = repositionX; positionArray[i * 2 + 1] = repositionY; } } gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferSubData(gl.ARRAY_BUFFER, 0, positionArray); } }); } function init(){ setupCanvas(); setupWebGL(); setupShaders(); loadLogo(); setupEvents(); } init();