← Mike Higgins Home
Custom Code Experiment •

Simple Grid

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Grid</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #1a1a1a;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            color: white;
        }
        canvas {
            display: block;
        }
        #ui-container {
            position: absolute;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%);
            width: 80%;
            max-width: 600px;
            background: rgba(30, 30, 30, 0.85);
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(10px);
            text-align: center;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        h1 {
            margin: 0 0 10px 0;
            font-size: 1.2rem;
            letter-spacing: 1px;
        }
        p {
            margin: 0 0 20px 0;
            font-size: 0.9rem;
            color: #aaa;
        }
        /* Stylish, responsive slider */
        input[type=range] {
            -webkit-appearance: none;
            width: 100%;
            background: transparent;
        }
        input[type=range]:focus {
            outline: none;
        }
        input[type=range]::-webkit-slider-runnable-track {
            width: 100%;
            height: 8px;
            cursor: pointer;
            background: #4a4a4a;
            border-radius: 4px;
        }
        input[type=range]::-webkit-slider-thumb {
            height: 24px;
            width: 24px;
            border-radius: 50%;
            background: #4CAF50;
            cursor: pointer;
            -webkit-appearance: none;
            margin-top: -8px;
            box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
            transition: transform 0.1s;
        }
        input[type=range]::-webkit-slider-thumb:hover {
            transform: scale(1.2);
        }
    </style>
</head>
<body>

    <div id="ui-container">
        <input type="range" id="morphSlider" min="-1" max="1" step="0.001" value="0">
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

    <script>
        // 1. Scene Setup
        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x1a1a1a, 0.015);

        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 15, 25);
        camera.lookAt(0, 0, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Performance optimization
        document.body.appendChild(renderer.domElement);

        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.maxPolarAngle = Math.PI / 2 + 0.1; // Don't let camera go too far below ground

        // 2. Lighting
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
        scene.add(ambientLight);

        const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
        dirLight.position.set(10, 20, 10);
        scene.add(dirLight);

        const backLight = new THREE.DirectionalLight(0x4CAF50, 0.5); // Greenish rim light
        backLight.position.set(-10, 5, -10);
        scene.add(backLight);

        // 3. Custom Morphing Geometry Construction
        // We build a single geometry that has two "states" (Base Polygon and Target Bird)
        const geometry = new THREE.BufferGeometry();

        // State 1: The Base Polygon (A simple Hexagon)
        // 7 Vertices: 1 center point, 6 perimeter points
        const baseVertices = new Float32Array([
            0, 0, 0,          // 0: Center
            0, 1, 0,          // 1: Top
            0.866, 0.5, 0,    // 2: Top Right
            0.866, -0.5, 0,   // 3: Bottom Right
            0, -1, 0,         // 4: Bottom
            -0.866, -0.5, 0,  // 5: Bottom Left
            -0.866, 0.5, 0    // 6: Top Left
        ]);

        // State 2: The Target Figure (A stylized Bird / Chevron)
        const birdVertices = new Float32Array([
            0, -0.1, 0.2,     // 0: Center (Body lifted slightly in 3D)
            0, 0.9, 0.5,      // 1: Head/Beak protruding forward
            1.4, 0.4, -0.2,   // 2: Right Wingtip swept back
            0.3, -0.2, 0,     // 3: Right Wing root
            0, -1.2, -0.3,    // 4: Tail swept back
            -0.3, -0.2, 0,    // 5: Left Wing root
            -1.4, 0.4, -0.2   // 6: Left Wingtip swept back
        ]);

        // Define the triangles (faces) connecting the vertices
        const indices = [
            0, 1, 2,
            0, 2, 3,
            0, 3, 4,
            0, 4, 5,
            0, 5, 6,
            0, 6, 1
        ];

        geometry.setIndex(indices);
        geometry.setAttribute('position', new THREE.BufferAttribute(baseVertices, 3));
        geometry.computeVertexNormals(); // Lighting needs normals

        // Add the Bird shape as a Morph Target processed by the GPU
        geometry.morphAttributes.position = [];
        geometry.morphAttributes.position[0] = new THREE.BufferAttribute(birdVertices, 3);

        // We also need morph normals for correct lighting during the transition
        const birdGeometry = new THREE.BufferGeometry();
        birdGeometry.setIndex(indices);
        birdGeometry.setAttribute('position', new THREE.BufferAttribute(birdVertices, 3));
        birdGeometry.computeVertexNormals();
        geometry.morphAttributes.normal = [];
        geometry.morphAttributes.normal[0] = birdGeometry.attributes.normal;

        // 4. Create the Grid of Meshes
        const material = new THREE.MeshStandardMaterial({
            color: 0x88ccff,
            roughness: 0.4,
            metalness: 0.1,
            side: THREE.DoubleSide
        });

        const gridSize = 25;
        const spacing = 2.2;
        const meshes = [];

        // Create an invisible group to hold the grid so we can center it
        const gridGroup = new THREE.Group();
        scene.add(gridGroup);

        const offset = (gridSize * spacing) / 2;

        for (let x = 0; x < gridSize; x++) {
            for (let z = 0; z < gridSize; z++) {
                const mesh = new THREE.Mesh(geometry, material);

                // Position in grid
                mesh.position.x = (x * spacing) - offset;
                mesh.position.z = (z * spacing) - offset;

                // Lay them flat
                mesh.rotation.x = -Math.PI / 2;

                // Store their normalized X position (-1 to 1) to calculate morph gradient
                mesh.userData.normalizedX = (mesh.position.x / offset);

                gridGroup.add(mesh);
                meshes.push(mesh);
            }
        }

        // 5. Responsive UI and Animation Logic
        const slider = document.getElementById('morphSlider');

        // Target shift represents where the user wants the wave to be.
        // Current shift is where it actually is (interpolates for smoothness).
        let targetShift = parseFloat(slider.value);
        let currentShift = targetShift;

        // Extremely lightweight event listener
        slider.addEventListener('input', (e) => {
            targetShift = parseFloat(e.target.value);
        });

        // 6. Render Loop
        function animate() {
            requestAnimationFrame(animate);

            // Smoothly ease currentShift towards targetShift (Decouples rendering from UI events)
            currentShift += (targetShift - currentShift) * 0.1;

            // Update the morph influence for every mesh based on its position and the global shift
            meshes.forEach(mesh => {
                // Gradient logic: calculate how "bird-like" this specific mesh should be
                // mesh.userData.normalizedX goes from -1 (left) to 1 (right)
                // currentShift pushes this wave back and forth
                let morphValue = (mesh.userData.normalizedX + 1.0) / 2.0; // Base gradient 0 to 1
                morphValue += currentShift; // Apply user slider shift

                // Clamp the value strictly between 0 (Hexagon) and 1 (Bird)
                morphValue = Math.max(0, Math.min(1, morphValue));

                // Apply the value to the GPU Morph Target
                mesh.morphTargetInfluences[0] = morphValue;

                // Optional: Add a gentle bobbing motion to the "birds"
                if (morphValue > 0.1) {
                    mesh.position.y = Math.sin(Date.now() * 0.002 + mesh.position.x) * (morphValue * 0.5);
                } else {
                    mesh.position.y = 0;
                }
            });

            controls.update();
            renderer.render(scene, camera);
        }

        // Handle Window Resizing smoothly
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        // Start the loop
        animate();
    </script>
</body>
</html>