skip to Main Content

Building a Cinematic, Interactive 3D WebGL Header with Three.js and GLSL

Modern web headers are rarely tasked with more than displaying static brand marks, navigation menus, and hero typography. However, when the mission is to “visualize the intangible,” the header transforms into an active spatial medium: a living, responsive canvas where physical computing principles, fluid dynamics, and digital apparitions converge.

In this deep-dive walkthrough, we will unpack the complete architectural blueprint for building an interactive 600px-tall, panoramic WebGL header from scratch. We will combine Three.js with custom GLSL fragment shaders, three-tier particle physics, cursor lerping, and a procedural eye apparition that reacts to visitor gaze and clicks… all running at a silky 60 FPS.

Multi-Layer Sensor Dynamics: Active

Making the
Invisible Visible

Interactive art, physical computing, and digital apparitions woven through living sensor topologies.

Hover gently · Click to summon watching iris

1. The Layered Stacking Context and DOM Architecture

Before writing a single line of WebGL, we need an unbreakable layout structure. A common pitfall in full-bleed 3D web experiences is event trapping: overlay text and navigation either swallow mouse gestures or fail to allow interactions to reach the canvas underneath.

To solve this, we create an isolated stacking context where:

  1. The WebGL canvas is absolute-positioned at z-index: 1 with full hardware acceleration.
  2. The UI overlay sits at z-index: 10 with pointer-events: none, allowing all mouse, hover, and click events to pass directly through to the 3D plane.
  3. Interactive UI elements (buttons, links) selectively restore input by setting pointer-events: auto.

CSS Stacking Architecture

 .webgl-header-root { width: 100%; height: 600px; position: relative; background-color: #050400; overflow: hidden; font-family: ‘Inter’, sans-serif; color: #ffffff; }/* Layer 1: Hardware-accelerated canvas plane */ .webgl-canvas { position: absolute; top: 0; left: 0; width: 100% !important; height: 100% !important; z-index: 1; cursor: crosshair; display: block; }/* Layer 2: UI Overlay allowing clicks to pass through / .header-ui-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; pointer-events: none; / Mouse events pass directly to 3D canvas */ display: flex; flex-direction: column; justify-content: space-between; padding: 3rem 4rem; background: linear-gradient(180deg, rgba(5,4,0,0.85) 0%, rgba(5,4,0,0) 25%, rgba(5,4,0,0) 70%, rgba(5,4,0,0.95) 100%); }/* Layer 3: Interactive UI elements selectively restored / .interactive-link, .header-ui-overlay a, .header-ui-overlay button { pointer-events: auto; / Restores clickable links & buttons */ } 

HTML Markup Structure

 <div class=”webgl-header-root” id=”canvas-container”> <!– The WebGL canvas will be dynamically injected here at z-index: 1 –>
&lt;div class="header-ui-overlay"&gt;
    &lt;nav class="nav-bar"&gt;
        &lt;span class="brand-monogram"&gt;Intangible_Medium&lt;/span&gt;
        &lt;a href="#explore" class="interactive-link"&gt;Explore Archive&lt;/a&gt;
    &lt;/nav&gt;

    &lt;div class="hero-text-block"&gt;
        &lt;h1&gt;Making the &lt;span&gt;Invisible&lt;/span&gt; Visible&lt;/h1&gt;
        &lt;p&gt;Interactive art and digital apparitions woven through living sensor topologies.&lt;/p&gt;
    &lt;/div&gt;
&lt;/div&gt;

</div>

2. Initializing the Three.js Viewport and Raycaster

To prevent high-resolution displays (such as Retina screens) from choking GPU fill rates on a 1920×600 viewport, we clamp the pixel ratio to a maximum of 2.0: Math.min(window.devicePixelRatio, 2).

We also instantiate a THREE.Raycaster and project screen coordinates onto a mathematical plane at Z = 0. Instead of directly assigning mouse coordinates, we track both a target vector and an interpolated vector to achieve organic, physical deceleration.

 import * as THREE from ‘https://unpkg.com/three@0.160.0/build/three.module.js’;

const container = document.getElementById(‘canvas-container’); let width = container.clientWidth; let height = container.clientHeight;

// 1. Scene, Camera, and Atmospheric Fog const scene = new THREE.Scene(); scene.background = new THREE.Color(0x050400); scene.fog = new THREE.Fog(0x050400, 12, 45);

const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100); camera.position.set(0, -9, 21); camera.lookAt(0, 0, 0);

// 2. High-Performance Renderer const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: “high-performance” }); renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.domElement.className = ‘webgl-canvas’; container.insertBefore(renderer.domElement, container.firstChild);

// 3. Mathematical Unprojection Plane at Z = 0 const raycaster = new THREE.Raycaster(); const planeZ0 = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); const targetMouseWorld = new THREE.Vector3(-999, -999, 0); const currentMouseWorld = new THREE.Vector3(-999, -999, 0); let isMouseActive = false;

container.addEventListener(‘mousemove’, (e) => { const rect = container.getBoundingClientRect(); const ndcX = ((e.clientX – rect.left) / width) * 2 – 1; const ndcY = -((e.clientY – rect.top) / height) * 2 + 1;

raycaster.setFromCamera(new THREE.Vector2(ndcX, ndcY), camera); raycaster.ray.intersectPlane(planeZ0, targetMouseWorld); isMouseActive = true; });

3. Undulating Cloth Surface and Dynamic Vertex Deformation

 const planeWidth = 62; const planeHeight = 30; const segmentsX = 140; const segmentsY = 70; const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight, segmentsX, segmentsY);

const posAttribute = geometry.attributes.position; const originalPositions = new Float32Array(posAttribute.array.length); originalPositions.set(posAttribute.array);

// In the animation loop: function updateFabric(time) { const positions = geometry.attributes.position; const count = positions.count;

for (let i = 0; i < count; i++) { const i3 = i * 3; const ox = originalPositions[i3]; const oy = originalPositions[i3 + 1];

// Base harmonic undulation
let z = Math.sin(ox * 0.09 + time * 0.32) * 0.65;
z += Math.cos(oy * 0.11 + time * 0.28) * 0.55;
z += Math.sin((ox + oy) * 0.07 + time * 0.22) * 0.35;

// Interactive mouse depression
if (isMouseActive) {
  const dx = currentMouseWorld.x - ox;
  const dy = currentMouseWorld.y - oy;
  const dist = Math.sqrt(dx * dx + dy * dy);
  const radius = 7.5;
  
  if (dist &lt; radius) {
    const norm = dist / radius;
    const force = 1.0 - (norm * norm * (3.0 - 2.0 * norm)); // Smooth cubic curve
    z -= force * 3.0;
  }
}
positions.array[i3 + 2] = z;

} positions.needsUpdate = true; geometry.computeVertexNormals(); }

4. GPU Proximity and Sonar Shaders (GLSL)

Rather than relying on heavy CPU recalculations or standard lights to illuminate thousands of vertices, we share the cloth geometry across a solid base mesh, a THREE.Points node lattice, and a wireframe mesh. We shade the points and wireframes using a custom GLSL fragment shader.

This shader calculates two real-time effects:

  1. Cursor Proximity Glow: A localized radial intensity calculated via smoothstep.
  2. Sonar Shockwave Ring: An expanding circular pulse that animates outward across the field upon a user click.
 // Vertex Shader varying vec3 vWorldPos;

void main() { vec4 worldPosition = modelMatrix * vec4(position, 1.0); vWorldPos = worldPosition.xyz; gl_Position = projectionMatrix * viewMatrix * worldPosition; gl_PointSize = 2.4; }

// Fragment Shader uniform vec3 uMouse; uniform vec3 uPulseCenter; uniform float uPulseRadius; uniform vec3 uColor; varying vec3 vWorldPos;

void main() { // 1. Proximity Glow float distToMouse = distance(vWorldPos, uMouse); float mouseGlow = 1.0 – smoothstep(0.0, 7.5, distToMouse);

// 2. Sonar Pulse Ring float distToPulse = distance(vWorldPos, uPulseCenter); float pulseGlow = 0.0; if (uPulseRadius > 0.0) { float ringWidth = 2.8; float diff = abs(distToPulse – uPulseRadius); pulseGlow = 1.0 – smoothstep(0.0, ringWidth, diff); pulseGlow *= (1.0 – smoothstep(0.0, 45.0, uPulseRadius)); // Dissolve over distance }

float alpha = max(mouseGlow, pulseGlow) + 0.035; if (alpha < 0.02) discard;

vec3 finalColor = mix(uColor, vec3(1.0, 0.95, 0.7), alpha * 0.55); gl_FragColor = vec4(finalColor, alpha * 0.9); }

5. Multi-Tier Dynamic Particle Architecture

A single particle system across an ultra-wide panoramic viewport (1920×600) can feel flat. To construct volumetric depth and kinetic dynamism, we build three distinct depth tiers:

  • Tier 1: Ambient Micro-Dust (850 particles): Drifts slowly across the background with harmonic Brownian oscillations.
  • Tier 2: Floating Embers (260 particles): Midground orbs rendered with custom radial GLSL billboard shaders, exhibiting upward thermal convection and subtle cursor repulsion.
  • Tier 3: Foreground Swarm Motes (380 particles): High-energy, reactive motes equipped with Hooke’s Law spring physics, viscous drag, and angular vortex acceleration.
 // Foreground Swarm Physics Step (inside requestAnimationFrame) const springK = 0.022; const damping = 0.91; const attRad = 11.0;

for (let i = 0; i < swarmCount; i++) { const i3 = i * 3;

// 1. Elastic Hooke’s Law recovery toward home anchor vx[i3] += (homeX[i3] – swarmPos[i3]) * springK; vy[i3] += (homeY[i3] – swarmPos[i3 + 1]) * springK; vz[i3] += (homeZ[i3] – swarmPos[i3 + 2]) * springK;

// 2. Cursor Magnetism & Tangential Vortex Swirl if (isMouseActive) { const dx = currentMouseWorld.x – swarmPos[i3]; const dy = currentMouseWorld.y – swarmPos[i3 + 1]; const dz = currentMouseWorld.z – swarmPos[i3 + 2]; const d = Math.sqrt(dx * dx + dy * dy + dz * dz);

if (d &lt; attRad &amp;&amp; d &gt; 0.2) {
  const pull = (1.0 - d / attRad) * 0.11;
  vx[i3] += (dx / d) * pull;
  vy[i3] += (dy / d) * pull;
  vz[i3] += (dz / d) * pull;

  // Perpendicular angular velocity (orbital spin)
  const swirl = (1.0 - d / attRad) * 0.07;
  vx[i3] += (-dy / d) * swirl;
  vy[i3] += (dx / d) * swirl;
}

}

// 3. Viscous Friction Damping vx[i3] *= damping; vy[i3] *= damping; vz[i3] *= damping;

swarmPos[i3] += vx[i3]; swarmPos[i3 + 1] += vy[i3]; swarmPos[i3 + 2] += vz[i3]; }

6. The Click Manifestation: Procedural Abstract Iris

To reinforce the concept of an intelligent, watching environment, clicking anywhere on the header triggers an ethereal eye iris directly at the intersection point.

Rather than sampling a heavy raster image, the entire iris is rendered through mathematical procedures inside a single quad fragment shader:

  1. Polar Coordinates: Convert UV space into radius r and angle theta = atan2(y, x).
  2. Ciliary Fibrils: Generate organic radial striations using high-frequency sine waves combined with pseudo-random hash noise.
  3. Contraction Crypts: Modulate concentric rings using time-dependent oscillation.
  4. Dynamic Pupil Tracking: Displace the pupil center toward the mouse vector to create an active gaze effect.
 // Procedural Abstract Iris Fragment Shader uniform float uTime; uniform float uOpacity; uniform vec2 uPupilOffset; uniform float uDilation; uniform vec3 uColorCore; uniform vec3 uColorIris; uniform vec3 uColorDeep; varying vec2 vUv;

float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }

void main() { vec2 centered = vUv – vec2(0.5); float r = length(centered) * 2.0; if (r > 1.0) discard;

float theta = atan(centered.y, centered.x);

// Radial muscle fibrils float fibrils = sin(theta * 32.0 + sin(theta * 8.0 + uTime * 1.2) * 2.5); fibrils = fibrils * 0.5 + 0.5; float noiseSpokes = hash(vec2(floor((theta + 3.14159) * 28.0), 1.0)); fibrils = mix(fibrils, noiseSpokes, 0.3);

// Responsive Pupil Center Offset vec2 pupilCenter = uPupilOffset * 0.16; float pupilDist = length(centered – pupilCenter) * 2.0; float pupilEdge = smoothstep(uDilation – 0.08, uDilation + 0.04, pupilDist);

// Concentric iris contraction rings & limbal edge float limbalEdge = 1.0 – smoothstep(0.72, 0.98, r); float contractionRings = sin(r * 26.0 – uTime * 1.5) * 0.15 + 0.85;

// Golden gradient ramp vec3 irisGrad = mix(uColorDeep, uColorIris, smoothstep(uDilation, 0.75, r)); irisGrad = mix(irisGrad, uColorCore, fibrils * (1.0 – pupilEdge * 0.4) * contractionRings * 0.6);

float alpha = pupilEdge * limbalEdge * (0.35 + fibrils * 0.65) * contractionRings * uOpacity; if (alpha < 0.01) discard;

gl_FragColor = vec4(irisGrad, clamp(alpha, 0.0, 0.95)); }

7. Performance Best Practices for Panoramic Viewports

When rendering a high-density 1920×600 canvas alongside active web content, memory footprint and draw call counts dictate performance. Keep these three core principles in mind:

  1. Geometry Instancing and Sharing: Instead of creating separate geometries for the solid mesh, point grid, and wireframe, link all three objects to the same underlying PlaneGeometry instance. Mutating vertex Z data once automatically updates all three visual layers with zero memory overhead.
  2. Buffer Typed Arrays: Keep particle physics arrays as flat Float32Array structures. Avoid allocating temporary THREE.Vector3 objects inside the requestAnimationFrame loop to prevent garbage collection pauses.
  3. Adaptive Resize Observers: Monitor the container with a modern ResizeObserver rather than just window.onresize. This guarantees that if the browser layout reflows or parent element boundaries shift, your projection matrix and camera aspect ratio update seamlessly without distortion.

The Full Code


<style>
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=JetBrains+Mono:wght@100;400&display=swap');

    /* Scoped container to prevent theme CSS bleeding */
    #im-header-root {
        width: 100%;
        position: relative;
        background-color: #050400;
        overflow: hidden;
        border-bottom: 1px solid rgba(255, 215, 0, 0.15);
        font-family: 'Inter', sans-serif;
        color: #ffffff;
        box-sizing: border-box;
        /* Dynamic gutter variables matching the site's logo & navigation menu */
        --im-gutter-left: clamp(1.75rem, 5.2vw, 6.5rem);
        --im-gutter-right: clamp(1.75rem, 5.2vw, 6.5rem);
    }

    #im-header-root * {
        box-sizing: border-box;
    }

    .im-header-container {
        width: 100%;
        height: 600px;
        position: relative;
        background-color: #050400;
        overflow: hidden;
    }

    .im-webgl-canvas {
        position: absolute;
        top: 0;
        left: 0;
        width: 100% !important;
        height: 100% !important;
        z-index: 1;
        cursor: crosshair;
        display: block;
    }

    .im-header-overlay {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 10;
        pointer-events: none;
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        padding-top: 3rem;
        padding-bottom: 2.75rem;
        padding-left: var(--im-gutter-left, clamp(1.75rem, 5.2vw, 6.5rem));
        padding-right: var(--im-gutter-right, clamp(1.75rem, 5.2vw, 6.5rem));
        background: linear-gradient(180deg, rgba(5,4,0,0.85) 0%, rgba(5,4,0,0) 25%, rgba(5,4,0,0) 70%, rgba(5,4,0,0.95) 100%);
        transition: padding-left 0.2s ease, padding-right 0.2s ease;
    }

    .im-nav-top {
        display: flex;
        justify-content: space-between;
        align-items: center;
        width: 100%;
    }

    .im-logo {
        font-family: 'JetBrains Mono', monospace;
        font-size: 1.15rem;
        letter-spacing: 0.12em;
        font-weight: 300;
        text-transform: uppercase;
        color: #ffd700;
    }

    .im-dynamic-layer-badge {
        font-family: 'JetBrains Mono', monospace;
        font-size: 0.72rem;
        letter-spacing: 0.12em;
        color: rgba(255, 215, 0, 0.65);
        text-transform: uppercase;
        pointer-events: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
    }

    .im-dynamic-layer-badge::before {
        content: '';
        display: inline-block;
        width: 6px;
        height: 6px;
        border-radius: 50%;
        background: #ffd700;
        box-shadow: 0 0 8px #ffd700;
    }

    .im-bottom-row {
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        width: 100%;
        gap: 2rem;
    }

    .im-header-content {
        margin-bottom: 0;
        max-width: 720px;
    }

    .im-header-content h1 {
        font-family: 'Inter', sans-serif !important;
        font-size: clamp(2.2rem, 4.5vw, 3.75rem) !important;
        font-weight: 300 !important;
        line-height: 1.12 !important;
        margin: 0 0 1rem 0 !important;
        letter-spacing: -0.025em !important;
        color: #ffffff !important;
    }

    .im-header-content h1 span {
        color: #ffd700 !important;
        font-weight: 400 !important;
    }

    .im-header-content p {
        color: rgba(255, 235, 170, 0.75) !important;
        font-size: clamp(0.95rem, 1.4vw, 1.15rem) !important;
        line-height: 1.6 !important;
        margin: 0 !important;
    }

    .im-instruction {
        font-family: 'JetBrains Mono', monospace;
        font-size: 0.75rem;
        color: rgba(255, 215, 0, 0.8);
        text-transform: uppercase;
        letter-spacing: 0.1em;
        display: flex;
        align-items: center;
        gap: 0.65rem;
        user-select: none;
        white-space: nowrap;
        margin-bottom: 0.35rem;
    }

    .im-instruction-dot {
        width: 7px;
        height: 7px;
        background-color: #ffd700;
        border-radius: 50%;
        animation: im-pulse 2.5s infinite cubic-bezier(0.4, 0, 0.2, 1);
    }

    @keyframes im-pulse {
        0% { box-shadow: 0 0 0 0 rgba(255, 215, 0, 0.7); }
        70% { box-shadow: 0 0 0 12px rgba(255, 215, 0, 0); }
        100% { box-shadow: 0 0 0 0 rgba(255, 215, 0, 0); }
    }

    @media (max-width: 768px) {
        .im-header-container { height: 500px; }
        .im-header-overlay { 
            padding-top: 1.75rem;
            padding-bottom: 1.75rem;
            padding-left: 1.5rem !important; 
            padding-right: 1.5rem !important; 
        }
        .im-dynamic-layer-badge { display: none; }
        .im-bottom-row { 
            flex-direction: column; 
            align-items: flex-start; 
            gap: 1.25rem; 
        }
        .im-instruction { font-size: 0.65rem; margin-bottom: 0; }
    }
</style>

<div id="im-header-root">
    <div class="im-header-container" id="im-header-canvas-container">
        <div class="im-header-overlay">
            <div class="im-nav-top">
                <div class="im-logo">Intangible_Medium</div>
                <div class="im-dynamic-layer-badge">
                    Multi-Layer Sensor Dynamics: Active
                </div>
            </div>

            <div class="im-bottom-row">
                <div class="im-header-content">
                    <h1>Making the<br><span>Invisible</span> Visible</h1>
                    <p>Interactive art, physical computing, and digital apparitions woven through living sensor topologies.</p>
                </div>

                <div class="im-instruction">
                    <div class="im-instruction-dot"></div>
                    Hover gently  Click to summon watching iris
                </div>
            </div>
        </div>
    </div>
</div>

<script type="module">
    import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';

    const container = document.getElementById('im-header-canvas-container');
    if (container) {
        let width = container.clientWidth || window.innerWidth;
        let height = container.clientHeight || 600;

        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x050400);
        scene.fog = new THREE.Fog(0x050400, 12, 45);

        const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
        camera.position.set(0, -9, 21);
        camera.lookAt(0, 0, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
        renderer.setSize(width, height);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.domElement.className = 'im-webgl-canvas';
        container.insertBefore(renderer.domElement, container.firstChild);

        const clock = new THREE.Clock();

        const targetMouseWorld = new THREE.Vector3(-999, -999, 0);
        const currentMouseWorld = new THREE.Vector3(-999, -999, 0);
        const raycaster = new THREE.Raycaster();
        const planeZ0 = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
        let isMouseActive = false;

        // Sonar Shockwave
        let pulseActive = false;
        let pulseRadius = 0;
        const pulseCenter = new THREE.Vector3(0, 0, 0);
        const maxPulseRadius = 45.0;
        const pulseSpeed = 26.0;

        // Abstract Watching Eye Iris State
        const eyeState = {
            active: false,
            opacity: 0.0,
            targetOpacity: 0.0,
            life: 0.0,
            scale: 0.35,
            targetScale: 1.0,
            position: new THREE.Vector3(0, 0, 0),
            pupilLook: new THREE.Vector2(0, 0),
            rotation: 0
        };

        const ambientLight = new THREE.AmbientLight(0x2a1a00, 1.4);
        scene.add(ambientLight);

        const pointerLight = new THREE.PointLight(0xffb700, 240, 18);
        pointerLight.position.set(-999, -999, 5);
        scene.add(pointerLight);

        const pulseLight = new THREE.PointLight(0xffd700, 0, 40);
        scene.add(pulseLight);

        const planeWidth = 62;
        const planeHeight = 30;
        const segmentsX = 140;
        const segmentsY = 70;
        const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight, segmentsX, segmentsY);

        const posAttribute = geometry.attributes.position;
        const originalPositions = new Float32Array(posAttribute.array.length);
        originalPositions.set(posAttribute.array);

        const solidMaterial = new THREE.MeshStandardMaterial({
            color: 0x140d02,
            emissive: 0x0c0700,
            roughness: 0.45,
            metalness: 0.8,
            transparent: true,
            opacity: 0.88,
            side: THREE.DoubleSide
        });
        const solidMesh = new THREE.Mesh(geometry, solidMaterial);
        scene.add(solidMesh);

        // Sensor Field Proximity Shader
        const sensorShaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
                uMouse: { value: new THREE.Vector3(-999, -999, 0) },
                uPulseCenter: { value: new THREE.Vector3(0, 0, 0) },
                uPulseRadius: { value: 0.0 },
                uColor: { value: new THREE.Color(0xffd000) }
            },
            vertexShader: `
                varying vec3 vWorldPos;
                void main() {
                    vec4 worldPosition = modelMatrix * vec4(position, 1.0);
                    vWorldPos = worldPosition.xyz;
                    gl_Position = projectionMatrix * viewMatrix * worldPosition;
                    gl_PointSize = 2.4;
                }
            `,
            fragmentShader: `
                uniform vec3 uMouse;
                uniform vec3 uPulseCenter;
                uniform float uPulseRadius;
                uniform vec3 uColor;
                varying vec3 vWorldPos;

                void main() {
                    float distToMouse = distance(vWorldPos, uMouse);
                    float mouseGlow = 1.0 - smoothstep(0.0, 7.5, distToMouse);
                    
                    float distToPulse = distance(vWorldPos, uPulseCenter);
                    float pulseGlow = 0.0;
                    if (uPulseRadius > 0.0) {
                        float ringWidth = 2.8;
                        float diff = abs(distToPulse - uPulseRadius);
                        pulseGlow = 1.0 - smoothstep(0.0, ringWidth, diff);
                        pulseGlow *= (1.0 - smoothstep(0.0, 45.0, uPulseRadius));
                    }

                    float alpha = max(mouseGlow, pulseGlow) + 0.035;
                    if (alpha < 0.02) discard;

                    vec3 finalColor = mix(uColor, vec3(1.0, 0.95, 0.7), alpha * 0.55);
                    gl_FragColor = vec4(finalColor, alpha * 0.9);
                }
            `,
            transparent: true,
            depthWrite: false,
            blending: THREE.AdditiveBlending
        });

        const nodes = new THREE.Points(geometry, sensorShaderMaterial);
        scene.add(nodes);

        const wireframeMaterial = sensorShaderMaterial.clone();
        wireframeMaterial.wireframe = true;
        const wireframeMesh = new THREE.Mesh(geometry, wireframeMaterial);
        scene.add(wireframeMesh);

        const dustCount = 850;
        const dustGeometry = new THREE.BufferGeometry();
        const dustPositions = new Float32Array(dustCount * 3);
        const dustOriginalPos = new Float32Array(dustCount * 3);

        for (let i = 0; i < dustCount * 3; i += 3) {
            dustPositions[i] = (Math.random() - 0.5) * planeWidth;
            dustPositions[i + 1] = (Math.random() - 0.5) * planeHeight;
            dustPositions[i + 2] = (Math.random() - 0.5) * 8.0;

            dustOriginalPos[i] = dustPositions[i];
            dustOriginalPos[i + 1] = dustPositions[i + 1];
            dustOriginalPos[i + 2] = dustPositions[i + 2];
        }

        dustGeometry.setAttribute('position', new THREE.BufferAttribute(dustPositions, 3));
        const dustMaterial = new THREE.PointsMaterial({
            color: 0xffd700,
            size: 0.16,
            transparent: true,
            opacity: 0.45,
            blending: THREE.AdditiveBlending,
            depthWrite: false
        });
        const dustParticles = new THREE.Points(dustGeometry, dustMaterial);
        scene.add(dustParticles);

        const sparkCount = 260;
        const sparkGeometry = new THREE.BufferGeometry();
        const sparkPositions = new Float32Array(sparkCount * 3);
        const sparkVelocities = new Float32Array(sparkCount * 3);
        const sparkScales = new Float32Array(sparkCount);
        const sparkPhases = new Float32Array(sparkCount);

        for (let i = 0; i < sparkCount; i++) {
            const i3 = i * 3;
            sparkPositions[i3] = (Math.random() - 0.5) * (planeWidth * 1.15);
            sparkPositions[i3 + 1] = (Math.random() - 0.5) * (planeHeight * 1.1);
            sparkPositions[i3 + 2] = (Math.random() - 0.3) * 14.0;

            sparkVelocities[i3] = (Math.random() - 0.5) * 0.015;
            sparkVelocities[i3 + 1] = (Math.random() * 0.02 + 0.006);
            sparkVelocities[i3 + 2] = (Math.random() - 0.5) * 0.01;

            sparkScales[i] = 1.2 + Math.random() * 2.8;
            sparkPhases[i] = Math.random() * Math.PI * 2;
        }

        sparkGeometry.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
        sparkGeometry.setAttribute('aScale', new THREE.BufferAttribute(sparkScales, 1));
        sparkGeometry.setAttribute('aPhase', new THREE.BufferAttribute(sparkPhases, 1));

        const sparkShaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
                uTime: { value: 0 },
                uMouse: { value: new THREE.Vector3(-999, -999, 0) },
                uBaseColor: { value: new THREE.Color(0xffc200) },
                uCoreColor: { value: new THREE.Color(0xfff7cc) },
                uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }
            },
            vertexShader: `
                uniform float uTime;
                uniform vec3 uMouse;
                uniform float uPixelRatio;
                attribute float aScale;
                attribute float aPhase;
                varying float vAlpha;

                void main() {
                    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
                    float pulse = 0.55 + 0.45 * sin(uTime * 2.2 + aPhase);
                    float distMouse = distance(position, uMouse);
                    float proxBoost = 1.0 - smoothstep(0.0, 9.0, distMouse);
                    
                    gl_PointSize = (aScale * (pulse + proxBoost * 0.8) * 16.0 * uPixelRatio) / -mvPosition.z;
                    gl_Position = projectionMatrix * mvPosition;
                    vAlpha = clamp(pulse * 0.75 + proxBoost * 0.65, 0.1, 1.0);
                }
            `,
            fragmentShader: `
                uniform vec3 uBaseColor;
                uniform vec3 uCoreColor;
                varying float vAlpha;

                void main() {
                    vec2 coord = gl_PointCoord - vec2(0.5);
                    float dist = length(coord);
                    if (dist > 0.5) discard;

                    float core = 1.0 - smoothstep(0.0, 0.22, dist);
                    float halo = 1.0 - smoothstep(0.15, 0.5, dist);
                    float glow = core * 0.9 + halo * 0.45;

                    vec3 color = mix(uBaseColor, uCoreColor, core * 0.85);
                    gl_FragColor = vec4(color, glow * vAlpha);
                }
            `,
            transparent: true,
            depthWrite: false,
            blending: THREE.AdditiveBlending
        });

        const sparkParticles = new THREE.Points(sparkGeometry, sparkShaderMaterial);
        scene.add(sparkParticles);

        const swarmCount = 380;
        const swarmGeometry = new THREE.BufferGeometry();
        const swarmPositions = new Float32Array(swarmCount * 3);
        const swarmHomePos = new Float32Array(swarmCount * 3);
        const swarmVelocities = new Float32Array(swarmCount * 3);
        const swarmSizes = new Float32Array(swarmCount);
        const swarmColors = new Float32Array(swarmCount * 3);

        const goldPalette = [
            new THREE.Color(0xfff5aa),
            new THREE.Color(0xffd700),
            new THREE.Color(0xffaa00),
            new THREE.Color(0xff8400)
        ];

        for (let i = 0; i < swarmCount; i++) {
            const i3 = i * 3;
            const px = (Math.random() - 0.5) * (planeWidth * 1.05);
            const py = (Math.random() - 0.5) * (planeHeight * 0.95);
            const pz = 1.5 + Math.random() * 11.5;

            swarmPositions[i3] = px;
            swarmPositions[i3 + 1] = py;
            swarmPositions[i3 + 2] = pz;

            swarmHomePos[i3] = px;
            swarmHomePos[i3 + 1] = py;
            swarmHomePos[i3 + 2] = pz;

            swarmVelocities[i3] = 0;
            swarmVelocities[i3 + 1] = 0;
            swarmVelocities[i3 + 2] = 0;

            swarmSizes[i] = 2.0 + Math.random() * 4.2;

            const col = goldPalette[Math.floor(Math.random() * goldPalette.length)];
            swarmColors[i3] = col.r;
            swarmColors[i3 + 1] = col.g;
            swarmColors[i3 + 2] = col.b;
        }

        swarmGeometry.setAttribute('position', new THREE.BufferAttribute(swarmPositions, 3));
        swarmGeometry.setAttribute('aSize', new THREE.BufferAttribute(swarmSizes, 1));
        swarmGeometry.setAttribute('color', new THREE.BufferAttribute(swarmColors, 3));

        const swarmShaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
                uTime: { value: 0 },
                uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }
            },
            vertexShader: `
                uniform float uTime;
                uniform float uPixelRatio;
                attribute float aSize;
                attribute vec3 color;
                varying vec3 vColor;
                varying float vFlicker;

                void main() {
                    vColor = color;
                    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
                    vFlicker = 0.65 + 0.35 * sin(uTime * 5.0 + position.x * 0.5 + position.y * 0.8);
                    gl_PointSize = (aSize * vFlicker * 24.0 * uPixelRatio) / -mvPosition.z;
                    gl_Position = projectionMatrix * mvPosition;
                }
            `,
            fragmentShader: `
                varying vec3 vColor;
                varying float vFlicker;

                void main() {
                    vec2 coord = gl_PointCoord - vec2(0.5);
                    float d = length(coord);
                    if (d > 0.5) discard;

                    float core = 1.0 - smoothstep(0.0, 0.16, d);
                    float aura = 1.0 - smoothstep(0.12, 0.5, d);
                    float alpha = (core * 0.95 + aura * 0.5) * vFlicker;

                    vec3 finalRgb = mix(vColor, vec3(1.0, 0.98, 0.9), core * 0.7);
                    gl_FragColor = vec4(finalRgb, alpha);
                }
            `,
            transparent: true,
            depthWrite: false,
            blending: THREE.AdditiveBlending
        });

        const swarmParticles = new THREE.Points(swarmGeometry, swarmShaderMaterial);
        scene.add(swarmParticles);

        const eyePlaneSize = 13.5;
        const eyeGeometry = new THREE.PlaneGeometry(eyePlaneSize, eyePlaneSize, 1, 1);

        const eyeShaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
                uTime: { value: 0.0 },
                uOpacity: { value: 0.0 },
                uPupilOffset: { value: new THREE.Vector2(0.0, 0.0) },
                uDilation: { value: 0.32 },
                uColorCore: { value: new THREE.Color(0xfff7cc) },
                uColorIris: { value: new THREE.Color(0xffc200) },
                uColorDeep: { value: new THREE.Color(0x995200) }
            },
            vertexShader: `
                varying vec2 vUv;
                void main() {
                    vUv = uv;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                }
            `,
            fragmentShader: `
                uniform float uTime;
                uniform float uOpacity;
                uniform vec2 uPupilOffset;
                uniform float uDilation;
                uniform vec3 uColorCore;
                uniform vec3 uColorIris;
                uniform vec3 uColorDeep;
                varying vec2 vUv;

                float hash(vec2 p) {
                    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
                }

                void main() {
                    vec2 centered = vUv - vec2(0.5);
                    float r = length(centered) * 2.0;
                    if (r > 1.0) discard;

                    float theta = atan(centered.y, centered.x);
                    float fibrils = sin(theta * 32.0 + sin(theta * 8.0 + uTime * 1.2) * 2.5);
                    fibrils += cos(theta * 64.0 - uTime * 0.8) * 0.5;
                    fibrils = fibrils * 0.5 + 0.5;

                    float noiseSpokes = hash(vec2(floor((theta + 3.14159) * 28.0), 1.0));
                    fibrils = mix(fibrils, noiseSpokes, 0.3);

                    vec2 pupilCenter = uPupilOffset * 0.16;
                    float pupilDist = length(centered - pupilCenter) * 2.0;
                    float pupilRadius = uDilation;
                    float pupilEdge = smoothstep(pupilRadius - 0.08, pupilRadius + 0.04, pupilDist);

                    float limbalEdge = 1.0 - smoothstep(0.72, 0.98, r);
                    float contractionRings = sin(r * 26.0 - uTime * 1.5) * 0.15 + 0.85;

                    vec3 irisGrad = mix(uColorDeep, uColorIris, smoothstep(pupilRadius, 0.75, r));
                    irisGrad = mix(irisGrad, uColorCore, fibrils * (1.0 - pupilEdge * 0.4) * contractionRings * 0.6);

                    float outerHalo = smoothstep(0.55, 0.88, r) * (1.0 - smoothstep(0.88, 1.0, r)) * 1.4;
                    vec3 finalColor = irisGrad + vec3(1.0, 0.95, 0.7) * outerHalo * 0.5;

                    float alpha = pupilEdge * limbalEdge * (0.35 + fibrils * 0.65) * contractionRings;
                    alpha += outerHalo * 0.6;
                    alpha *= uOpacity;

                    if (alpha < 0.01) discard;
                    gl_FragColor = vec4(finalColor, clamp(alpha, 0.0, 0.95));
                }
            `,
            transparent: true,
            depthWrite: false,
            blending: THREE.AdditiveBlending,
            side: THREE.DoubleSide
        });

        const eyeMesh = new THREE.Mesh(eyeGeometry, eyeShaderMaterial);
        eyeMesh.visible = false;
        scene.add(eyeMesh);

        const alignWithSiteGutter = () => {
            const root = document.getElementById('im-header-root');
            if (!root) return;

            // Search for the site's top navigation logo or main content container
            const leftTargets = [
                document.querySelector('header img, header .logo, header [class*="logo"], .navbar-brand, .site-logo'),
                document.querySelector('header .container, header [class*="container"], header [class*="wrap"]'),
                document.querySelector('.vc_row:not([data-vc-full-width]) .vc_column-inner, .vc_row:not([data-vc-full-width])'),
                document.querySelector('.site-content .container, main .container, #content .container')
            ];

            let detectedLeft = null;
            for (const el of leftTargets) {
                if (el) {
                    const rect = el.getBoundingClientRect();
                    if (rect.left >= 12 && rect.left < window.innerWidth * 0.45) {
                        detectedLeft = rect.left;
                        break;
                    }
                }
            }

            // Search for the right-side menu, search icon, or container bounds
            const rightTargets = [
                document.querySelector('header [class*="search"], header .search, header [id*="search"]'),
                document.querySelector('header nav, header [class*="menu"], header [class*="nav"], .navbar-collapse'),
                document.querySelector('header .container, header [class*="container"], header [class*="wrap"]'),
                document.querySelector('.vc_row:not([data-vc-full-width]) .vc_column-inner, .vc_row:not([data-vc-full-width])')
            ];

            let detectedRight = null;
            for (const el of rightTargets) {
                if (el) {
                    const rect = el.getBoundingClientRect();
                    const rightOffset = window.innerWidth - rect.right;
                    if (rightOffset >= 12 && rightOffset < window.innerWidth * 0.45) {
                        detectedRight = rightOffset;
                        break;
                    }
                }
            }

            if (detectedLeft && !detectedRight) detectedRight = detectedLeft;
            if (detectedRight && !detectedLeft) detectedLeft = detectedRight;

            if (detectedLeft) {
                root.style.setProperty('--im-gutter-left', `${Math.round(detectedLeft)}px`);
                root.style.setProperty('--im-gutter-right', `${Math.round(detectedRight)}px`);
            }
        };

        const updateDimensions = () => {
            width = container.clientWidth || window.innerWidth;
            height = container.clientHeight || 600;
            renderer.setSize(width, height);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            sparkShaderMaterial.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2);
            swarmShaderMaterial.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2);
            camera.aspect = width / height;
            camera.updateProjectionMatrix();
            alignWithSiteGutter();
        };

        const resizeObserver = new ResizeObserver(() => updateDimensions());
        resizeObserver.observe(container);
        window.addEventListener('resize', updateDimensions);
        window.addEventListener('load', alignWithSiteGutter);
        setTimeout(alignWithSiteGutter, 150);
        setTimeout(alignWithSiteGutter, 600);

        container.addEventListener('mousemove', (event) => {
            const rect = container.getBoundingClientRect();
            const mouseX = ((event.clientX - rect.left) / width) * 2 - 1;
            const mouseY = -((event.clientY - rect.top) / height) * 2 + 1;

            raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);
            raycaster.ray.intersectPlane(planeZ0, targetMouseWorld);
            
            if (!isMouseActive) {
                currentMouseWorld.copy(targetMouseWorld);
                isMouseActive = true;
            }
        });

        container.addEventListener('mouseleave', () => {
            targetMouseWorld.set(-999, -999, 0);
            isMouseActive = false;
        });

        container.addEventListener('mousedown', () => {
            if (!isMouseActive) return;
            
            pulseActive = true;
            pulseRadius = 0;
            pulseCenter.copy(currentMouseWorld);
            
            pulseLight.position.copy(pulseCenter);
            pulseLight.position.z = 2.0;
            pulseLight.intensity = 1400;

            // Trigger Abstract Eye Iris
            eyeState.active = true;
            eyeState.position.copy(currentMouseWorld);
            eyeState.position.z = -0.5;
            eyeState.opacity = 0.0;
            eyeState.targetOpacity = 0.95;
            eyeState.scale = 0.35;
            eyeState.life = 4.2;
            eyeState.rotation = Math.random() * Math.PI * 2;

            eyeMesh.position.copy(eyeState.position);
            eyeMesh.rotation.z = eyeState.rotation;
            eyeMesh.scale.set(eyeState.scale, eyeState.scale, 1.0);
            eyeMesh.visible = true;
        });

        // Touch Interaction
        container.addEventListener('touchmove', (event) => {
            if (event.touches.length > 0) {
                const touch = event.touches[0];
                const rect = container.getBoundingClientRect();
                const mouseX = ((touch.clientX - rect.left) / width) * 2 - 1;
                const mouseY = -((touch.clientY - rect.top) / height) * 2 + 1;

                raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);
                raycaster.ray.intersectPlane(planeZ0, targetMouseWorld);
                if (!isMouseActive) {
                    currentMouseWorld.copy(targetMouseWorld);
                    isMouseActive = true;
                }
            }
        }, { passive: true });

        container.addEventListener('touchend', () => {
            targetMouseWorld.set(-999, -999, 0);
            isMouseActive = false;
        });

        function animate() {
            requestAnimationFrame(animate);
            const time = clock.getElapsedTime();

            // 1. Mouse Lerp
            if (isMouseActive) {
                currentMouseWorld.lerp(targetMouseWorld, 0.08);
                pointerLight.position.copy(currentMouseWorld);
                pointerLight.position.z = 2.2;
            } else {
                pointerLight.position.set(-999, -999, 5);
                currentMouseWorld.set(-999, -999, 0);
            }

            // 2. Pulse Dynamics
            if (pulseActive) {
                pulseRadius += pulseSpeed * 0.016;
                pulseLight.intensity = Math.max(0, pulseLight.intensity - 28);
                if (pulseRadius > maxPulseRadius) {
                    pulseActive = false;
                    pulseRadius = 0;
                }
            }

            // 3. Fluid Cloth Wave & Deformation
            const positions = geometry.attributes.position;
            const count = positions.count;
            for (let i = 0; i < count; i++) {
                const i3 = i * 3;
                const ox = originalPositions[i3];
                const oy = originalPositions[i3 + 1];

                let z = Math.sin(ox * 0.09 + time * 0.32) * 0.65;
                z += Math.cos(oy * 0.11 + time * 0.28) * 0.55;
                z += Math.sin((ox + oy) * 0.07 + time * 0.22) * 0.35;

                if (isMouseActive) {
                    const dx = currentMouseWorld.x - ox;
                    const dy = currentMouseWorld.y - oy;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    const influenceRadius = 7.5;
                    if (dist < influenceRadius) {
                        const norm = dist / influenceRadius;
                        const force = 1.0 - (norm * norm * (3 - 2 * norm));
                        z -= force * 3.0;
                    }
                }

                if (pulseActive) {
                    const distToPulse = Math.sqrt(Math.pow(pulseCenter.x - ox, 2) + Math.pow(pulseCenter.y - oy, 2));
                    const diff = Math.abs(distToPulse - pulseRadius);
                    if (diff < 2.5) {
                        const shockForce = 1.0 - (diff / 2.5);
                        const dampening = Math.max(0, 1.0 - (pulseRadius / maxPulseRadius));
                        z += shockForce * 1.8 * dampening;
                    }
                }

                positions.array[i3 + 2] = z;
            }
            positions.needsUpdate = true;
            geometry.computeVertexNormals();

            // 4. Layer 1 Ambient Dust Motion
            const dPositions = dustGeometry.attributes.position.array;
            for (let i = 0; i < dustCount * 3; i += 3) {
                dPositions[i + 1] = dustOriginalPos[i + 1] + Math.sin(time * 0.45 + dustOriginalPos[i]) * 0.4;
                if (isMouseActive) {
                    const dx = dPositions[i] - currentMouseWorld.x;
                    const dy = dPositions[i + 1] - currentMouseWorld.y;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    if (dist < 4.2) {
                        const push = (4.2 - dist) * 0.055;
                        dPositions[i] += (dx / dist) * push;
                        dPositions[i + 1] += (dy / dist) * push;
                    } else {
                        dPositions[i] += (dustOriginalPos[i] - dPositions[i]) * 0.02;
                    }
                }
            }
            dustGeometry.attributes.position.needsUpdate = true;

            // 5. Layer 2 Floating Sparks Motion & Swirl
            const sPos = sparkGeometry.attributes.position.array;
            for (let i = 0; i < sparkCount; i++) {
                const i3 = i * 3;
                const phase = sparkPhases[i];
                sPos[i3] += Math.sin(time * 0.7 + phase) * 0.022 + sparkVelocities[i3];
                sPos[i3 + 1] += Math.cos(time * 0.5 + phase) * 0.018 + sparkVelocities[i3 + 1];
                sPos[i3 + 2] += Math.sin(time * 0.9 + phase * 2.0) * 0.015 + sparkVelocities[i3 + 2];

                if (sPos[i3 + 1] > planeHeight * 0.6) sPos[i3 + 1] = -planeHeight * 0.6;
                if (sPos[i3] > planeWidth * 0.6) sPos[i3] = -planeWidth * 0.6;
                else if (sPos[i3] < -planeWidth * 0.6) sPos[i3] = planeWidth * 0.6;

                if (isMouseActive) {
                    const dx = sPos[i3] - currentMouseWorld.x;
                    const dy = sPos[i3 + 1] - currentMouseWorld.y;
                    const dz = sPos[i3 + 2] - currentMouseWorld.z;
                    const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
                    const interactiveRadius = 8.5;
                    if (dist < interactiveRadius && dist > 0.01) {
                        const normDist = 1.0 - (dist / interactiveRadius);
                        const force = normDist * normDist * 0.16;
                        sPos[i3] += (dx / dist) * force;
                        sPos[i3 + 1] += (dy / dist) * force;
                        sPos[i3 + 2] += (dz / dist) * force * 0.7;

                        const tangentX = -dy / dist;
                        const tangentY = dx / dist;
                        sPos[i3] += tangentX * force * 0.6;
                        sPos[i3 + 1] += tangentY * force * 0.6;
                    }
                }
            }
            sparkGeometry.attributes.position.needsUpdate = true;
            sparkShaderMaterial.uniforms.uTime.value = time;
            sparkShaderMaterial.uniforms.uMouse.value.copy(currentMouseWorld);

            // 6. Layer 3 Foreground Swarm Physics & Gravity
            const swarmPos = swarmGeometry.attributes.position.array;
            for (let i = 0; i < swarmCount; i++) {
                const i3 = i * 3;
                const homeX = swarmHomePos[i3];
                const homeY = swarmHomePos[i3 + 1];
                const homeZ = swarmHomePos[i3 + 2];

                let vx = swarmVelocities[i3];
                let vy = swarmVelocities[i3 + 1];
                let vz = swarmVelocities[i3 + 2];

                const springK = 0.022;
                vx += (homeX - swarmPos[i3]) * springK;
                vy += (homeY - swarmPos[i3 + 1]) * springK;
                vz += (homeZ - swarmPos[i3 + 2]) * springK;

                if (isMouseActive) {
                    const dx = currentMouseWorld.x - swarmPos[i3];
                    const dy = currentMouseWorld.y - swarmPos[i3 + 1];
                    const dz = currentMouseWorld.z - swarmPos[i3 + 2];
                    const dSq = dx * dx + dy * dy + dz * dz;
                    const d = Math.sqrt(dSq);
                    const attRad = 11.0;

                    if (d < attRad && d > 0.2) {
                        const pull = (1.0 - d / attRad) * 0.11;
                        vx += (dx / d) * pull;
                        vy += (dy / d) * pull;
                        vz += (dz / d) * pull;

                        const swirlFactor = (1.0 - d / attRad) * 0.07;
                        vx += (-dy / d) * swirlFactor;
                        vy += (dx / d) * swirlFactor;
                    }
                }

                if (pulseActive) {
                    const dx = swarmPos[i3] - pulseCenter.x;
                    const dy = swarmPos[i3 + 1] - pulseCenter.y;
                    const d = Math.sqrt(dx * dx + dy * dy);
                    const diff = Math.abs(d - pulseRadius);
                    if (diff < 3.2 && d > 0.1) {
                        const blast = (1.0 - diff / 3.2) * 0.38;
                        vx += (dx / d) * blast;
                        vy += (dy / d) * blast;
                        vz += blast * 0.8;
                    }
                }

                vx *= 0.91;
                vy *= 0.91;
                vz *= 0.91;

                swarmVelocities[i3] = vx;
                swarmVelocities[i3 + 1] = vy;
                swarmVelocities[i3 + 2] = vz;

                swarmPos[i3] += vx;
                swarmPos[i3 + 1] += vy;
                swarmPos[i3 + 2] += vz;
            }
            swarmGeometry.attributes.position.needsUpdate = true;
            swarmShaderMaterial.uniforms.uTime.value = time;

            // 7. Abstract Eye Iris Update
            if (eyeState.active) {
                if (eyeState.life > 1.2) {
                    eyeState.opacity += (eyeState.targetOpacity - eyeState.opacity) * 0.08;
                    eyeState.scale += (eyeState.targetScale - eyeState.scale) * 0.06;
                } else {
                    eyeState.opacity += (0.0 - eyeState.opacity) * 0.045;
                    eyeState.scale += (1.25 - eyeState.scale) * 0.02;
                }

                eyeState.life -= 0.016;
                if (eyeState.life <= 0 || eyeState.opacity <= 0.01) {
                    eyeState.active = false;
                    eyeState.opacity = 0;
                    eyeMesh.visible = false;
                }

                if (isMouseActive) {
                    const lookX = Math.max(-1.0, Math.min(1.0, (currentMouseWorld.x - eyeState.position.x) / 10.0));
                    const lookY = Math.max(-1.0, Math.min(1.0, (currentMouseWorld.y - eyeState.position.y) / 6.0));
                    eyeState.pupilLook.x += (lookX - eyeState.pupilLook.x) * 0.08;
                    eyeState.pupilLook.y += (lookY - eyeState.pupilLook.y) * 0.08;
                }

                const dilation = 0.3 + 0.06 * Math.sin(time * 2.8);

                eyeMesh.scale.set(eyeState.scale, eyeState.scale, 1.0);
                eyeMesh.rotation.z += 0.0015;
                eyeShaderMaterial.uniforms.uTime.value = time;
                eyeShaderMaterial.uniforms.uOpacity.value = eyeState.opacity;
                eyeShaderMaterial.uniforms.uPupilOffset.value.copy(eyeState.pupilLook);
                eyeShaderMaterial.uniforms.uDilation.value = dilation;
            }

            // 8. Uniform Updates
            sensorShaderMaterial.uniforms.uMouse.value.copy(currentMouseWorld);
            sensorShaderMaterial.uniforms.uPulseCenter.value.copy(pulseCenter);
            sensorShaderMaterial.uniforms.uPulseRadius.value = pulseRadius;

            wireframeMaterial.uniforms.uMouse.value.copy(currentMouseWorld);
            wireframeMaterial.uniforms.uPulseCenter.value.copy(pulseCenter);
            wireframeMaterial.uniforms.uPulseRadius.value = pulseRadius;

            renderer.render(scene, camera);
        }

        animate();
    }
</script>

This Post Has 0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top
×Close search
Search