A Pen by Filip Zrnzevic on CodePen.
Created
November 13, 2025 03:39
-
-
Save jerriclynsjohn/899794f267cec5da74834cc41e5414eb to your computer and use it in GitHub Desktop.
[threejs/gsap] ❍ Interactive Glass Lens Effect with Sound FX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="error-message" id="errorMessage"></div> | |
| <div class="fallback-bg" id="fallbackBg"></div> | |
| <div class="audio-enable"> | |
| <p>ENTER EXPERIENCE<br />WITH AUDIO</p> | |
| <button class="enable-button" id="enableBtn">START</button> | |
| </div> | |
| <div class="preloader" id="preloader"> | |
| <span id="counter">[000]</span> | |
| </div> | |
| <canvas id="canvas"></canvas> | |
| <p class="text-element description">THE ARCHIVE COLLECTS RECORDS OF ABANDONED WORLDS AND LOST TECHNOLOGIES, WAITING TO BE DISCOVERED.</p> | |
| <nav class="text-element nav-links"> | |
| <a href="#">_DATA VAULTS</a> | |
| <a href="#">_DEEP SPACE</a> | |
| <a href="#">_FORBIDDEN ZONES</a> | |
| <a href="#">_EXODUS LOGS</a> | |
| </nav> | |
| <div class="text-element footer"> | |
| <p>Err: [404 - SIGNAL LOST]<br /> | |
| SYSTEM TIME: CYCLE 2187.42<br /> | |
| <span style="opacity: 0.7; font-size: 0.6rem;">PRESS 'H' TO TOGGLE REFRACTION CONTROLS</span> | |
| </p> | |
| </div> | |
| <p class="text-element division"> | |
| PERFORMANCE ANALYSIS: <span id="fpsCounter"></span> FPS<br> | |
| OPTICAL REFRACTION ENGINE: ONLINE<br> | |
| ACCESS LEVEL: RESTRICTED.<br> | |
| TRACE INITIATED: SOURCE UNKNOWN. | |
| </p> | |
| <p class="text-element signal">_Uplink Pending...</p> | |
| <div class="text-element central-text"> | |
| THE VAULT RETAINS ECHOES OF CIVILIZATIONS ERASED BY TIME<br> | |
| DEAD SYSTEMS. BROKEN SIGNALS. MYTHS WRITTEN IN CODE. | |
| </div> | |
| <audio id="startClickSound" preload="auto"> | |
| <source src="https://assets.codepen.io/7558/preloader-2s-001.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="preloaderSound" preload="auto"> | |
| <source src="https://assets.codepen.io/7558/preloader-3s-002.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="hoverSound" preload="auto"> | |
| <source src="https://assets.codepen.io/7558/preloader-2s-001.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="backgroundMusic" loop preload="auto"> | |
| <source src="https://assets.codepen.io/7558/cinematic-02.mp3" type="audio/mpeg"> | |
| </audio> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as THREE from "https://esm.sh/[email protected]"; | |
| import { EffectComposer } from "https://esm.sh/[email protected]/examples/jsm/postprocessing/EffectComposer.js"; | |
| import { RenderPass } from "https://esm.sh/[email protected]/examples/jsm/postprocessing/RenderPass.js"; | |
| import { ShaderPass } from "https://esm.sh/[email protected]/examples/jsm/postprocessing/ShaderPass.js"; | |
| import { Pane } from "https://cdn.skypack.dev/[email protected]"; | |
| (function () { | |
| "use strict"; | |
| const supportsWebGL = () => { | |
| try { | |
| const c = document.createElement("canvas"); | |
| return !!( | |
| window.WebGLRenderingContext && | |
| (c.getContext("webgl") || c.getContext("experimental-webgl")) | |
| ); | |
| } catch { | |
| return false; | |
| } | |
| }; | |
| if (!Array.prototype.forEach) { | |
| Array.prototype.forEach = function (cb, ctx) { | |
| for (let i = 0; i < this.length; i++) cb.call(ctx, this[i], i, this); | |
| }; | |
| } | |
| if (!window.requestAnimationFrame) { | |
| window.requestAnimationFrame = (cb) => setTimeout(cb, 16.67); | |
| } | |
| const App = { | |
| PARAMS: { | |
| distortion: { | |
| strength: 0.15, | |
| radius: 0.2, | |
| size: 1, | |
| edgeWidth: 0.05, | |
| edgeOpacity: 0.2, | |
| rimLightIntensity: 0.3, | |
| rimLightWidth: 0.08, | |
| chromaticAberration: 0.03, | |
| reflectionIntensity: 0.3, | |
| waveDistortion: 0.08, | |
| waveSpeed: 1.2, | |
| lensBlur: 0.15, | |
| clearCenterSize: 0.3, | |
| followMouse: true, | |
| animationSpeed: 1, | |
| overallIntensity: 1, | |
| preset: "Classic Glass" | |
| }, | |
| presets: { | |
| Minimal: { | |
| strength: 0.05, | |
| radius: 0.12, | |
| size: 0.8, | |
| edgeWidth: 0.02, | |
| edgeOpacity: 0.1, | |
| rimLightIntensity: 0.1, | |
| rimLightWidth: 0.04, | |
| chromaticAberration: 0.01, | |
| reflectionIntensity: 0.15, | |
| waveDistortion: 0.02, | |
| waveSpeed: 0.8, | |
| lensBlur: 0.05, | |
| clearCenterSize: 0.5 | |
| }, | |
| Subtle: { | |
| strength: 0.08, | |
| radius: 0.16, | |
| size: 0.9, | |
| edgeWidth: 0.03, | |
| edgeOpacity: 0.15, | |
| rimLightIntensity: 0.2, | |
| rimLightWidth: 0.06, | |
| chromaticAberration: 0.02, | |
| reflectionIntensity: 0.2, | |
| waveDistortion: 0.04, | |
| waveSpeed: 1, | |
| lensBlur: 0.08, | |
| clearCenterSize: 0.4 | |
| }, | |
| "Classic Glass": { | |
| strength: 0.12, | |
| radius: 0.18, | |
| size: 1, | |
| edgeWidth: 0.04, | |
| edgeOpacity: 0.25, | |
| rimLightIntensity: 0.3, | |
| rimLightWidth: 0.08, | |
| chromaticAberration: 0.025, | |
| reflectionIntensity: 0.35, | |
| waveDistortion: 0.03, | |
| waveSpeed: 0.5, | |
| lensBlur: 0.12, | |
| clearCenterSize: 0.2 | |
| }, | |
| Dramatic: { | |
| strength: 0.25, | |
| radius: 0.35, | |
| size: 1.2, | |
| edgeWidth: 0.08, | |
| edgeOpacity: 0.4, | |
| rimLightIntensity: 0.5, | |
| rimLightWidth: 0.1, | |
| chromaticAberration: 0.06, | |
| reflectionIntensity: 0.5, | |
| waveDistortion: 0.15, | |
| waveSpeed: 1.8, | |
| lensBlur: 0.25, | |
| clearCenterSize: 0.15 | |
| }, | |
| "Chromatic Focus": { | |
| strength: 0.1, | |
| radius: 0.22, | |
| size: 1, | |
| edgeWidth: 0.06, | |
| edgeOpacity: 0.3, | |
| rimLightIntensity: 0.25, | |
| rimLightWidth: 0.07, | |
| chromaticAberration: 0.08, | |
| reflectionIntensity: 0.2, | |
| waveDistortion: 0.05, | |
| waveSpeed: 0.8, | |
| lensBlur: 0.1, | |
| clearCenterSize: 0.25 | |
| }, | |
| "Liquid Wave": { | |
| strength: 0.18, | |
| radius: 0.28, | |
| size: 1.1, | |
| edgeWidth: 0.05, | |
| edgeOpacity: 0.2, | |
| rimLightIntensity: 0.4, | |
| rimLightWidth: 0.09, | |
| chromaticAberration: 0.04, | |
| reflectionIntensity: 0.4, | |
| waveDistortion: 0.2, | |
| waveSpeed: 2.5, | |
| lensBlur: 0.15, | |
| clearCenterSize: 0.1 | |
| }, | |
| Gigantic: { | |
| strength: 0.4, | |
| radius: 0.65, | |
| size: 1.8, | |
| edgeWidth: 0.12, | |
| edgeOpacity: 0.6, | |
| rimLightIntensity: 0.8, | |
| rimLightWidth: 0.15, | |
| chromaticAberration: 0.1, | |
| reflectionIntensity: 0.7, | |
| waveDistortion: 0.25, | |
| waveSpeed: 1.5, | |
| lensBlur: 0.35, | |
| clearCenterSize: 0.05 | |
| } | |
| } | |
| }, | |
| scene: null, | |
| camera: null, | |
| renderer: null, | |
| composer: null, | |
| customPass: null, | |
| backgroundTexture: null, | |
| backgroundMesh: null, | |
| aspect: 1, | |
| backgroundScene: null, | |
| backgroundCamera: null, | |
| mousePosition: { x: 0.5, y: 0.5 }, | |
| targetMousePosition: { x: 0.5, y: 0.5 }, | |
| staticMousePosition: { x: 0.5, y: 0.5 }, | |
| performanceMonitor: { frameCount: 0, lastTime: 0, fps: 60 }, | |
| pane: null, | |
| isBackgroundPlaying: false, | |
| paneVisible: false, | |
| paneInitialized: false, | |
| isSceneReady: false, | |
| isTextureLoaded: false, | |
| webglSupported: supportsWebGL(), | |
| init() { | |
| this.setupAudio(); | |
| this.setupKeyboardControls(); | |
| this.bindEvents(); | |
| if (!this.webglSupported) { | |
| this.showFallback(); | |
| return; | |
| } | |
| this.waitForDependencies(); | |
| }, | |
| waitForDependencies() { | |
| const chk = setInterval(() => { | |
| if (window.gsap && window.SplitText) { | |
| clearInterval(chk); | |
| this.onDependenciesReady(); | |
| } | |
| }, 100); | |
| setTimeout(() => { | |
| clearInterval(chk); | |
| this.onDependenciesReady(); | |
| }, 10000); | |
| }, | |
| onDependenciesReady() {}, | |
| showError(m) { | |
| const el = document.getElementById("errorMessage"); | |
| if (!el) return; | |
| el.textContent = m; | |
| el.style.display = "block"; | |
| setTimeout(() => (el.style.display = "none"), 5000); | |
| }, | |
| showFallback() { | |
| document.getElementById("fallbackBg").classList.add("active"); | |
| this.finishPreloader(); | |
| }, | |
| setupAudio() { | |
| this.startClickSound = document.getElementById("startClickSound"); | |
| this.preloaderSound = document.getElementById("preloaderSound"); | |
| this.hoverSound = document.getElementById("hoverSound"); | |
| this.backgroundMusic = document.getElementById("backgroundMusic"); | |
| }, | |
| bindEvents() { | |
| document.getElementById("enableBtn").onclick = () => this.onStartClick(); | |
| }, | |
| onStartClick() { | |
| document.body.classList.add("loading-active"); | |
| this.startClickSound?.play().catch(() => {}); | |
| document.querySelector(".audio-enable").style.display = "none"; | |
| document.getElementById("preloader").style.display = "flex"; | |
| this.preloaderSound?.play().catch(() => {}); | |
| setTimeout(() => { | |
| if (this.backgroundMusic) { | |
| this.backgroundMusic.volume = 0.3; | |
| this.backgroundMusic.play().catch(() => {}); | |
| this.isBackgroundPlaying = true; | |
| } | |
| }, 500); | |
| this.webglSupported ? this.initializeScene() : this.showFallback(); | |
| this.startPreloader(); | |
| }, | |
| startPreloader() { | |
| let c = 0; | |
| const timer = setInterval(() => { | |
| const el = document.getElementById("counter"); | |
| if (el) | |
| el.textContent = | |
| "[" + (c < 10 ? "00" : c < 100 ? "0" : "") + ++c + "]"; | |
| if (c >= 100) { | |
| clearInterval(timer); | |
| setTimeout(() => { | |
| this.preloaderSound?.pause(); | |
| if (this.preloaderSound) this.preloaderSound.currentTime = 0; | |
| this.finishPreloader(); | |
| }, 200); | |
| } | |
| }, 30); | |
| }, | |
| finishPreloader() { | |
| const wait = () => { | |
| if ( | |
| (this.isSceneReady && this.isTextureLoaded) || | |
| !this.webglSupported | |
| ) { | |
| const pre = document.getElementById("preloader"); | |
| pre.classList.add("fade-out"); | |
| if (this.webglSupported) | |
| document.getElementById("canvas").classList.add("ready"); | |
| setTimeout(() => { | |
| document.body.classList.remove("loading-active"); | |
| pre.style.display = "none"; | |
| pre.classList.remove("fade-out"); | |
| this.animateTextElements(); | |
| }, 800); | |
| } else setTimeout(wait, 50); | |
| }; | |
| wait(); | |
| }, | |
| animateTextElements() { | |
| if (!window.gsap || !window.SplitText) { | |
| this.fallbackTextAnimation(); | |
| return; | |
| } | |
| const ease = window.CustomEase | |
| ? (CustomEase.create("customOut", "0.65,0.05,0.36,1"), "customOut") | |
| : "power2.out"; | |
| const containers = [ | |
| ".description", | |
| ".division", | |
| ".signal", | |
| ".central-text", | |
| ".footer" | |
| ]; | |
| gsap.set(containers.concat(".nav-links"), { opacity: 0 }); | |
| const splits = containers.map( | |
| (sel) => | |
| SplitText.create(sel, { type: "lines", linesClass: "line" }).lines | |
| ); | |
| const [descLines, divLines, sigLines, centralLines, footerLines] = splits; | |
| gsap.set(containers, { opacity: 1 }); | |
| gsap.set(splits.flat().concat(".nav-links a"), { opacity: 0, y: 30 }); | |
| const tl = gsap.timeline(); | |
| tl.to(descLines, { opacity: 1, y: 0, duration: 0.8, ease, stagger: 0.18 }) | |
| .to(".nav-links", { opacity: 1, duration: 0.2 }, 0.12) | |
| .to( | |
| ".nav-links a", | |
| { opacity: 1, y: 0, duration: 0.8, ease, stagger: 0.15 }, | |
| 0.12 | |
| ) | |
| .to( | |
| centralLines, | |
| { opacity: 1, y: 0, duration: 0.8, ease, stagger: 0.22 }, | |
| 0.25 | |
| ) | |
| .to( | |
| footerLines, | |
| { opacity: 1, y: 0, duration: 0.8, ease, stagger: 0.18 }, | |
| 0.4 | |
| ) | |
| .to( | |
| divLines, | |
| { opacity: 1, y: 0, duration: 0.8, ease, stagger: 0.18 }, | |
| 0.55 | |
| ) | |
| .to( | |
| sigLines, | |
| { opacity: 1, y: 0, duration: 0.8, ease, stagger: 0.18 }, | |
| 0.55 | |
| ); | |
| }, | |
| fallbackTextAnimation() { | |
| let d = 0; | |
| document.querySelectorAll(".text-element").forEach((el) => { | |
| setTimeout(() => { | |
| el.style.opacity = "1"; | |
| el.style.transform = el.classList.contains("central-text") | |
| ? "translateX(-50%) translateY(0)" | |
| : "translateY(0)"; | |
| }, d); | |
| d += 250; | |
| }); | |
| }, | |
| initializeScene() { | |
| if (!this.webglSupported) { | |
| this.isSceneReady = this.isTextureLoaded = true; | |
| return; | |
| } | |
| const canvas = document.getElementById("canvas"); | |
| this.renderer = new THREE.WebGLRenderer({ | |
| canvas, | |
| antialias: true, | |
| alpha: true, | |
| premultipliedAlpha: false | |
| }); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| this.renderer.autoClear = false; | |
| this.aspect = window.innerWidth / window.innerHeight; | |
| this.backgroundScene = new THREE.Scene(); | |
| this.backgroundCamera = new THREE.OrthographicCamera( | |
| -this.aspect, | |
| this.aspect, | |
| 1, | |
| -1, | |
| 0.1, | |
| 10 | |
| ); | |
| this.backgroundCamera.position.z = 1; | |
| this.scene = new THREE.Scene(); | |
| this.camera = new THREE.OrthographicCamera( | |
| -this.aspect, | |
| this.aspect, | |
| 1, | |
| -1, | |
| 0.1, | |
| 10 | |
| ); | |
| this.camera.position.z = 1; | |
| this.loadBackgroundTexture(); | |
| this.setupPostProcessing(); | |
| this.setupPane(); | |
| this.setupNavHoverSounds(); | |
| const onResize = this.onWindowResize.bind(this); | |
| const onMouseMove = this.onMouseMove.bind(this); | |
| const onTouchMove = this.onTouchMove.bind(this); | |
| const onTouchStart = this.onTouchStart.bind(this); | |
| window.addEventListener("resize", onResize); | |
| document.addEventListener("mousemove", onMouseMove); | |
| document.addEventListener("touchmove", onTouchMove); | |
| document.addEventListener("touchstart", onTouchStart); | |
| this.animate(); | |
| this.isSceneReady = true; | |
| }, | |
| onMouseMove(e) { | |
| if (this.PARAMS.distortion.followMouse) { | |
| this.targetMousePosition.x = e.clientX / window.innerWidth; | |
| this.targetMousePosition.y = 1 - e.clientY / window.innerHeight; | |
| } | |
| }, | |
| onTouchStart(e) { | |
| e.preventDefault(); | |
| if (e.touches.length) this.onTouchMove(e); | |
| }, | |
| onTouchMove(e) { | |
| e.preventDefault(); | |
| if (this.PARAMS.distortion.followMouse && e.touches.length) { | |
| const t = e.touches[0]; | |
| this.targetMousePosition.x = t.clientX / window.innerWidth; | |
| this.targetMousePosition.y = 1 - t.clientY / window.innerHeight; | |
| } | |
| }, | |
| loadBackgroundTexture() { | |
| new THREE.TextureLoader().load( | |
| "https://assets.codepen.io/7558/red-protocol-poster-03-bg.jpg", | |
| (tex) => { | |
| this.backgroundTexture = tex; | |
| this.createBackgroundMesh(); | |
| this.isTextureLoaded = true; | |
| }, | |
| undefined, | |
| () => (this.isTextureLoaded = true) | |
| ); | |
| }, | |
| createBackgroundMesh() { | |
| if (this.backgroundMesh) this.backgroundScene.remove(this.backgroundMesh); | |
| const imgAspect = | |
| this.backgroundTexture.image.width / | |
| this.backgroundTexture.image.height; | |
| const scAspect = window.innerWidth / window.innerHeight; | |
| let sx, sy; | |
| if (scAspect > imgAspect) { | |
| sx = scAspect * 2; | |
| sy = sx / imgAspect; | |
| } else { | |
| sy = 2; | |
| sx = sy * imgAspect; | |
| } | |
| const g = new THREE.PlaneGeometry(sx, sy); | |
| const m = new THREE.MeshBasicMaterial({ map: this.backgroundTexture }); | |
| this.backgroundMesh = new THREE.Mesh(g, m); | |
| this.backgroundScene.add(this.backgroundMesh); | |
| }, | |
| setupPostProcessing() { | |
| this.composer = new EffectComposer(this.renderer); | |
| const rp = new RenderPass(this.backgroundScene, this.backgroundCamera); | |
| this.composer.addPass(rp); | |
| this.setupDistortionPass(); | |
| }, | |
| setupDistortionPass() { | |
| const v = `varying vec2 vUv;void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`; | |
| const f = `uniform sampler2D tDiffuse;uniform vec2 uMouse;uniform float uRadius;uniform float uSize;uniform float uStrength;uniform float uEdgeWidth;uniform float uEdgeOpacity;uniform float uRimLightIntensity;uniform float uRimLightWidth;uniform float uChromaticAberration;uniform float uReflectionIntensity;uniform float uWaveDistortion;uniform float uWaveSpeed;uniform float uLensBlur;uniform float uClearCenterSize;uniform float uOverallIntensity;uniform float uAspect;uniform float uTime;varying vec2 vUv; | |
| vec4 blur(sampler2D i,vec2 uv,vec2 r,vec2 d,float it){vec4 c=vec4(0.);vec2 o=1.3333333*d*it;c+=texture2D(i,uv)*.2941176;c+=texture2D(i,uv+(o/r))*.3529412;c+=texture2D(i,uv-(o/r))*.3529412;return c;} | |
| void main(){ | |
| vec2 c=uMouse; | |
| vec2 a=vUv; | |
| a.x*=uAspect; | |
| c.x*=uAspect; | |
| float dist=distance(a,c); | |
| float rad=uRadius*uSize; | |
| vec4 orig=texture2D(tDiffuse,vUv); | |
| // Calculate the effect for all pixels | |
| float nd=dist/rad; | |
| vec2 dir=normalize(a-c); | |
| float cl=uClearCenterSize*rad; | |
| float df=smoothstep(cl,rad,dist); | |
| float powd=1.+nd*2.; | |
| vec2 dUv=a-dir*uStrength*pow(df,powd); | |
| float w1=sin(nd*8.-uTime*uWaveSpeed)*uWaveDistortion; | |
| float w2=cos(nd*12.-uTime*uWaveSpeed*.7)*uWaveDistortion*.5; | |
| dUv+=dir*(w1+w2)*df; | |
| dUv.x/=uAspect; | |
| float ab=uChromaticAberration*df*(1.+nd); | |
| vec2 rO=dir*ab*1.2/vec2(uAspect,1.); | |
| vec2 bO=dir*ab*0.8/vec2(uAspect,1.); | |
| vec4 colR=texture2D(tDiffuse,dUv+rO); | |
| vec4 colG=texture2D(tDiffuse,dUv); | |
| vec4 colB=texture2D(tDiffuse,dUv-bO); | |
| vec4 ref1=texture2D(tDiffuse,vUv+dir*0.08*df); | |
| vec4 ref2=texture2D(tDiffuse,vUv+dir*0.15*df); | |
| vec4 ref=mix(ref1,ref2,.6); | |
| vec4 col=vec4(colR.r,colG.g,colB.b,1.); | |
| col=mix(col,ref,uReflectionIntensity*df); | |
| float bl=uLensBlur*df*(1.+nd*.5); | |
| vec4 blr=blur(tDiffuse,dUv,vec2(1./uAspect,1.),vec2(1.),bl); | |
| col=mix(col,blr,df*.7); | |
| float edge=smoothstep(rad-uEdgeWidth,rad,dist); | |
| vec3 eCol=mix(vec3(1.),vec3(.8,.9,1.),nd); | |
| col=mix(col,vec4(eCol,1.),edge*uEdgeOpacity); | |
| float rimD=rad-uRimLightWidth; | |
| float rim=smoothstep(rimD-0.02,rimD+0.02,dist); | |
| rim*=(1.-smoothstep(rad-0.01,rad,dist)); | |
| col=mix(col,vec4(1.),rim*uRimLightIntensity); | |
| float br=1.+sin(nd*6.-uTime*2.)*.1*df; | |
| col.rgb*=br; | |
| // Replace hard cutoff with ultra-tight smoothstep to fix jagged edges | |
| float effectMask = 1.0 - smoothstep(rad - 0.001, rad + 0.001, dist); | |
| gl_FragColor=mix(orig, mix(orig,col,uOverallIntensity), effectMask); | |
| }`; | |
| this.customPass = new ShaderPass({ | |
| uniforms: { | |
| tDiffuse: { value: null }, | |
| uMouse: { value: new THREE.Vector2(0.5, 0.5) }, | |
| uRadius: { value: this.PARAMS.distortion.radius }, | |
| uSize: { value: this.PARAMS.distortion.size }, | |
| uStrength: { value: this.PARAMS.distortion.strength }, | |
| uEdgeWidth: { value: this.PARAMS.distortion.edgeWidth }, | |
| uEdgeOpacity: { value: this.PARAMS.distortion.edgeOpacity }, | |
| uRimLightIntensity: { | |
| value: this.PARAMS.distortion.rimLightIntensity | |
| }, | |
| uRimLightWidth: { value: this.PARAMS.distortion.rimLightWidth }, | |
| uChromaticAberration: { | |
| value: this.PARAMS.distortion.chromaticAberration | |
| }, | |
| uReflectionIntensity: { | |
| value: this.PARAMS.distortion.reflectionIntensity | |
| }, | |
| uWaveDistortion: { value: this.PARAMS.distortion.waveDistortion }, | |
| uWaveSpeed: { value: this.PARAMS.distortion.waveSpeed }, | |
| uLensBlur: { value: this.PARAMS.distortion.lensBlur }, | |
| uClearCenterSize: { value: this.PARAMS.distortion.clearCenterSize }, | |
| uOverallIntensity: { value: this.PARAMS.distortion.overallIntensity }, | |
| uAspect: { value: this.aspect }, | |
| uTime: { value: 0 } | |
| }, | |
| vertexShader: v, | |
| fragmentShader: f | |
| }); | |
| this.customPass.renderToScreen = true; | |
| this.composer.addPass(this.customPass); | |
| }, | |
| setupNavHoverSounds() { | |
| document.querySelectorAll(".nav-links a").forEach((a) => { | |
| a.addEventListener("mouseenter", () => { | |
| if (this.hoverSound && this.isBackgroundPlaying) { | |
| this.hoverSound.currentTime = 0; | |
| this.hoverSound.volume = 0.4; | |
| this.hoverSound.play().catch(() => {}); | |
| } | |
| }); | |
| }); | |
| }, | |
| setupKeyboardControls() { | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key.toLowerCase() === "h") { | |
| e.preventDefault(); | |
| this.togglePane(); | |
| } | |
| }); | |
| }, | |
| togglePane() { | |
| if (!this.paneInitialized) this.setupPane(); | |
| if (this.pane) { | |
| this.paneVisible = !this.paneVisible; | |
| this.pane.hidden = !this.paneVisible; | |
| } | |
| }, | |
| setupPane() { | |
| if (this.paneInitialized) return; | |
| const p = (this.pane = new Pane({ | |
| title: "Glass Refraction Controls", | |
| expanded: true | |
| })); | |
| p.addBinding(this.PARAMS.distortion, "preset", { | |
| label: "Presets", | |
| options: { | |
| Minimal: "Minimal", | |
| Subtle: "Subtle", | |
| "Classic Glass": "Classic Glass", | |
| Dramatic: "Dramatic", | |
| "Chromatic Focus": "Chromatic Focus", | |
| "Liquid Wave": "Liquid Wave", | |
| Gigantic: "Gigantic" | |
| } | |
| }).on("change", (ev) => this.loadPreset(ev.value)); | |
| p.addButton({ title: "Reload Preset" }).on("click", () => | |
| this.loadPreset(this.PARAMS.distortion.preset) | |
| ); | |
| const addBinding = (prop, opts) => | |
| p.addBinding(this.PARAMS.distortion, prop, opts).on("change", (ev) => { | |
| const uniformName = | |
| "u" + prop.charAt(0).toUpperCase() + prop.slice(1); | |
| if (this.customPass?.uniforms[uniformName]) | |
| this.customPass.uniforms[uniformName].value = ev.value; | |
| }); | |
| addBinding("overallIntensity", { | |
| min: 0, | |
| max: 2, | |
| step: 0.01, | |
| label: "Overall Intensity" | |
| }); | |
| p.addBinding(this.PARAMS.distortion, "followMouse", { | |
| label: "Follow Mouse" | |
| }).on("change", (ev) => { | |
| if (!ev.value) this.staticMousePosition = { x: 0.5, y: 0.5 }; | |
| }); | |
| addBinding("animationSpeed", { | |
| min: 0, | |
| max: 3, | |
| step: 0.1, | |
| label: "Animation Speed" | |
| }); | |
| const f1 = p.addFolder({ title: "Size Controls" }); | |
| f1.addBinding(this.PARAMS.distortion, "size", { | |
| min: 0.2, | |
| max: 3, | |
| step: 0.1, | |
| label: "Effect Size" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uSize.value = ev.value) | |
| ); | |
| f1.addBinding(this.PARAMS.distortion, "radius", { | |
| min: 0.05, | |
| max: 0.8, | |
| step: 0.01, | |
| label: "Base Radius" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uRadius.value = ev.value) | |
| ); | |
| const f2 = p.addFolder({ title: "Refraction Properties" }); | |
| f2.addBinding(this.PARAMS.distortion, "strength", { | |
| min: 0, | |
| max: 0.5, | |
| step: 0.01, | |
| label: "Refraction Strength" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uStrength.value = ev.value) | |
| ); | |
| f2.addBinding(this.PARAMS.distortion, "clearCenterSize", { | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| label: "Clear Center" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uClearCenterSize.value = ev.value) | |
| ); | |
| const f3 = p.addFolder({ title: "Visual Effects" }); | |
| f3.addBinding(this.PARAMS.distortion, "chromaticAberration", { | |
| min: 0, | |
| max: 0.15, | |
| step: 0.001, | |
| label: "Chromatic Aberration" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uChromaticAberration.value = ev.value) | |
| ); | |
| f3.addBinding(this.PARAMS.distortion, "reflectionIntensity", { | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| label: "Reflections" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uReflectionIntensity.value = ev.value) | |
| ); | |
| f3.addBinding(this.PARAMS.distortion, "lensBlur", { | |
| min: 0, | |
| max: 0.5, | |
| step: 0.01, | |
| label: "Lens Blur" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uLensBlur.value = ev.value) | |
| ); | |
| const f4 = p.addFolder({ title: "Wave Animation" }); | |
| f4.addBinding(this.PARAMS.distortion, "waveDistortion", { | |
| min: 0, | |
| max: 0.3, | |
| step: 0.01, | |
| label: "Wave Strength" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uWaveDistortion.value = ev.value) | |
| ); | |
| f4.addBinding(this.PARAMS.distortion, "waveSpeed", { | |
| min: 0, | |
| max: 5, | |
| step: 0.1, | |
| label: "Wave Speed" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uWaveSpeed.value = ev.value) | |
| ); | |
| const f5 = p.addFolder({ title: "Edge Effects" }); | |
| f5.addBinding(this.PARAMS.distortion, "edgeWidth", { | |
| min: 0, | |
| max: 0.2, | |
| step: 0.01, | |
| label: "Edge Width" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uEdgeWidth.value = ev.value) | |
| ); | |
| f5.addBinding(this.PARAMS.distortion, "edgeOpacity", { | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| label: "Edge Opacity" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uEdgeOpacity.value = ev.value) | |
| ); | |
| const f6 = p.addFolder({ title: "Rim Lighting" }); | |
| f6.addBinding(this.PARAMS.distortion, "rimLightIntensity", { | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| label: "Rim Light Intensity" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uRimLightIntensity.value = ev.value) | |
| ); | |
| f6.addBinding(this.PARAMS.distortion, "rimLightWidth", { | |
| min: 0, | |
| max: 0.3, | |
| step: 0.01, | |
| label: "Rim Light Width" | |
| }).on( | |
| "change", | |
| (ev) => (this.customPass.uniforms.uRimLightWidth.value = ev.value) | |
| ); | |
| Object.assign(p.element.style, { | |
| position: "fixed", | |
| top: "10px", | |
| right: "10px", | |
| zIndex: "3000" | |
| }); | |
| p.hidden = true; | |
| this.paneVisible = false; | |
| this.paneInitialized = true; | |
| this.loadPreset("Classic Glass"); | |
| }, | |
| loadPreset(name) { | |
| const preset = this.PARAMS.presets[name]; | |
| if (!preset) return; | |
| Object.entries(preset).forEach(([k, v]) => { | |
| if (k in this.PARAMS.distortion) { | |
| this.PARAMS.distortion[k] = v; | |
| const uniformName = "u" + k.charAt(0).toUpperCase() + k.slice(1); | |
| if (this.customPass?.uniforms[uniformName]) | |
| this.customPass.uniforms[uniformName].value = v; | |
| } | |
| }); | |
| this.PARAMS.distortion.preset = name; | |
| this.pane?.refresh(); | |
| }, | |
| onWindowResize() { | |
| this.aspect = window.innerWidth / window.innerHeight; | |
| if (this.camera) { | |
| this.camera.left = this.camera.right = this.backgroundCamera.left = this.backgroundCamera.right = null; | |
| [this.camera, this.backgroundCamera].forEach((cam) => { | |
| cam.left = -this.aspect; | |
| cam.right = this.aspect; | |
| cam.updateProjectionMatrix(); | |
| }); | |
| } | |
| this.renderer?.setSize(window.innerWidth, window.innerHeight); | |
| this.composer?.setSize(window.innerWidth, window.innerHeight); | |
| if (this.customPass) this.customPass.uniforms.uAspect.value = this.aspect; | |
| if (this.backgroundTexture) this.createBackgroundMesh(); | |
| }, | |
| animate(time = 0) { | |
| requestAnimationFrame((t) => this.animate(t)); | |
| if (!this.webglSupported || !this.renderer) return; | |
| this.performanceMonitor.frameCount++; | |
| if (time - this.performanceMonitor.lastTime >= 1000) { | |
| this.performanceMonitor.fps = this.performanceMonitor.frameCount; | |
| this.performanceMonitor.frameCount = 0; | |
| this.performanceMonitor.lastTime = time; | |
| const fpsElement = document.getElementById("fpsCounter"); | |
| if (fpsElement) | |
| fpsElement.textContent = String(this.performanceMonitor.fps); | |
| } | |
| const tgt = this.PARAMS.distortion.followMouse | |
| ? this.targetMousePosition | |
| : this.staticMousePosition; | |
| this.mousePosition.x += (tgt.x - this.mousePosition.x) * 0.1; | |
| this.mousePosition.y += (tgt.y - this.mousePosition.y) * 0.1; | |
| if (this.customPass) { | |
| this.customPass.uniforms.uMouse.value.set( | |
| this.mousePosition.x, | |
| this.mousePosition.y | |
| ); | |
| this.customPass.uniforms.uTime.value = | |
| time * 0.001 * this.PARAMS.distortion.animationSpeed; | |
| } | |
| this.composer | |
| ? this.composer.render() | |
| : (this.renderer.clear(), | |
| this.renderer.render(this.backgroundScene, this.backgroundCamera)); | |
| } | |
| }; | |
| window.addEventListener("error", (e) => { | |
| let m = "An error occurred"; | |
| if (e.error?.message) m += ": " + e.error.message; | |
| if (e.filename) m += " in " + e.filename; | |
| App.showError(m + ". Some features may not work properly."); | |
| }); | |
| window.addEventListener("unhandledrejection", (e) => { | |
| App.showError( | |
| "Loading failed: " + (e.reason || "Unknown error") + ". Retrying..." | |
| ); | |
| }); | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", () => App.init()); | |
| } else { | |
| App.init(); | |
| } | |
| window.App = App; | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/SplitText.min.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @font-face { | |
| font-family: "PPSupplyMono"; | |
| src: url("https://assets.codepen.io/7558/PPSupplyMono-Regular.ttf") | |
| format("truetype"); | |
| font-weight: normal; | |
| font-style: normal; | |
| font-display: swap; | |
| } | |
| *, | |
| *::after, | |
| *::before { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --color--background: rgb(60, 60, 60); | |
| --color--foreground: #c4d5bc; | |
| --color--accent: rgb(170, 170, 170); | |
| --font-primary: "PPSupplyMono", "Courier New", monospace; | |
| --font-secondary: "PPSupplyMono", "Courier New", monospace; | |
| --margin: 32px; | |
| --gutter: 16px; | |
| } | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| align-items: center; | |
| min-height: 100vh; | |
| margin: 0; | |
| padding: 0; | |
| font-family: var(--font-primary); | |
| background: radial-gradient( | |
| circle at 10% 20%, | |
| rgb(230, 230, 230) 0%, | |
| rgb(180, 180, 180) 45%, | |
| rgb(100, 100, 100) 90% | |
| ); | |
| color: var(--color--foreground); | |
| letter-spacing: -0.03em; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| body.loading-active { | |
| overflow: hidden !important; | |
| } | |
| body::before { | |
| content: ""; | |
| position: fixed; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: transparent url("https://assets.codepen.io/7558/noise.png") repeat | |
| 0 0; | |
| background-size: 300px 300px; | |
| animation: noise-animation 0.3s steps(5) infinite; | |
| opacity: 0.9; | |
| will-change: transform; | |
| z-index: 100; | |
| pointer-events: none; | |
| } | |
| @keyframes noise-animation { | |
| 0% { | |
| transform: translate(0, 0); | |
| } | |
| 10% { | |
| transform: translate(-2%, -3%); | |
| } | |
| 20% { | |
| transform: translate(-4%, 2%); | |
| } | |
| 30% { | |
| transform: translate(2%, -4%); | |
| } | |
| 40% { | |
| transform: translate(-2%, 5%); | |
| } | |
| 50% { | |
| transform: translate(-4%, 2%); | |
| } | |
| 60% { | |
| transform: translate(3%, 0); | |
| } | |
| 70% { | |
| transform: translate(0, 3%); | |
| } | |
| 80% { | |
| transform: translate(-3%, 0); | |
| } | |
| 90% { | |
| transform: translate(2%, 2%); | |
| } | |
| 100% { | |
| transform: translate(1%, 0); | |
| } | |
| } | |
| #canvas { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| background: transparent; | |
| z-index: 1; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.5s ease-out; | |
| } | |
| #canvas.ready { | |
| opacity: 1; | |
| } | |
| .fallback-bg { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: url("https://assets.codepen.io/7558/red-protocol-poster-03-bg.jpg"); | |
| background-size: cover; | |
| background-position: center; | |
| z-index: 0; | |
| opacity: 0; | |
| transition: opacity 1s ease-out; | |
| } | |
| .fallback-bg.active { | |
| opacity: 1; | |
| } | |
| .container { | |
| display: flex; | |
| width: 100%; | |
| height: 100vh; | |
| padding: var(--gutter); | |
| position: relative; | |
| z-index: 10; | |
| transition: filter 0.1s ease-out; | |
| } | |
| .text-element { | |
| position: fixed; | |
| font-family: var(--font-primary); | |
| color: var(--color--foreground); | |
| text-transform: uppercase; | |
| z-index: 10; | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| .audio-enable { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| background: #0a0a0a; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2000; | |
| font-family: var(--font-primary); | |
| font-size: 12px; | |
| color: #c4d5bc; | |
| text-transform: uppercase; | |
| gap: 2rem; | |
| text-align: center; | |
| padding: 1rem; | |
| } | |
| .enable-button { | |
| border: 1px solid #c4d5bc; | |
| background: transparent; | |
| color: #c4d5bc; | |
| padding: 1rem 2rem; | |
| font-family: var(--font-primary); | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| letter-spacing: 0.1em; | |
| transition: all 0.3s ease; | |
| } | |
| .enable-button:hover { | |
| background: #c4d5bc; | |
| color: #0a0a0a; | |
| } | |
| .preloader { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| background: #0a0a0a; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2000; | |
| font-family: var(--font-primary); | |
| font-size: 12px; | |
| color: #c4d5bc; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| opacity: 1; | |
| transition: opacity 0.8s ease-out; | |
| } | |
| .preloader.fade-out { | |
| opacity: 0; | |
| } | |
| .error-message { | |
| position: fixed; | |
| top: 20px; | |
| left: 20px; | |
| background: rgba(255, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px; | |
| font-size: 12px; | |
| display: none; | |
| z-index: 3000; | |
| } | |
| .description { | |
| top: 120px; | |
| left: 50px; | |
| width: 360px; | |
| font-size: 0.75rem; | |
| line-height: 1.2; | |
| } | |
| .nav-links { | |
| top: 50%; | |
| left: 50px; | |
| transform: translateY(-50%); | |
| } | |
| .nav-links a { | |
| position: relative; | |
| display: block; | |
| margin-bottom: 8px; | |
| color: var(--color--foreground); | |
| text-decoration: none; | |
| z-index: 1; | |
| transition: color 0.3s ease; | |
| padding: 4px 8px; | |
| } | |
| .nav-links a::after { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 0; | |
| height: 100%; | |
| background-color: var(--color--foreground); | |
| z-index: -1; | |
| transition: width 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| } | |
| .nav-links a:hover::after { | |
| width: 100%; | |
| } | |
| .nav-links a:hover { | |
| color: #1a1a1a; | |
| } | |
| .footer { | |
| bottom: 120px; | |
| right: 50px; | |
| font-size: 0.625rem; | |
| } | |
| .division { | |
| bottom: 120px; | |
| left: 50px; | |
| font-size: 0.625rem; | |
| } | |
| .signal { | |
| top: 50%; | |
| right: 100px; | |
| font-size: 0.625rem; | |
| transform: translateY(-50%); | |
| } | |
| .central-text { | |
| bottom: 30%; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(0); | |
| text-align: center; | |
| font-size: 1.25rem; | |
| line-height: 1.1; | |
| white-space: nowrap; | |
| width: max-content; | |
| } | |
| .dg.ac { | |
| z-index: 3000 !important; | |
| position: fixed !important; | |
| top: 10px !important; | |
| right: 10px !important; | |
| } | |
| /* Responsive design */ | |
| @media (max-width: 768px) { | |
| .description, | |
| .nav-links, | |
| .division, | |
| .performance { | |
| left: 20px; | |
| } | |
| .footer, | |
| .signal { | |
| right: 20px; | |
| } | |
| .central-text { | |
| font-size: 1rem; | |
| white-space: normal; | |
| max-width: 90%; | |
| } | |
| .description { | |
| width: calc(100vw - 40px); | |
| max-width: 300px; | |
| } | |
| /* Optimize GUI for mobile */ | |
| .dg.ac { | |
| transform: scale(0.8); | |
| transform-origin: top right; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .central-text { | |
| font-size: 0.9rem; | |
| } | |
| .description { | |
| font-size: 0.7rem; | |
| } | |
| /* Further scale down GUI on very small screens */ | |
| .dg.ac { | |
| transform: scale(0.7); | |
| } | |
| } | |
| /* Touch device optimizations */ | |
| @media (hover: none) and (pointer: coarse) { | |
| .nav-links a:hover::after { | |
| width: 0; | |
| } | |
| .nav-links a:active::after { | |
| width: 100%; | |
| } | |
| .enable-button:hover { | |
| background: transparent; | |
| color: #c4d5bc; | |
| } | |
| .enable-button:active { | |
| background: #c4d5bc; | |
| color: #0a0a0a; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment