← Mike Higgins Home
Custom Code Experiment •

Metamorphosis 1

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>Metamorphosis 1</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #111;
            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(20, 20, 20, 0.85);
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
            backdrop-filter: blur(10px);
            text-align: center;
            border: 1px solid rgba(255, 255, 255, 0.1);
            user-select: none;
        }
        h1 {
            margin: 0 0 10px 0;
            font-size: 1.2rem;
        }
        p {
            margin: 0 0 20px 0;
            font-size: 0.9rem;
            color: #aaa;
        }
        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: #333;
            border-radius: 4px;
        }
        input[type=range]::-webkit-slider-thumb {
            height: 24px;
            width: 24px;
            border-radius: 50%;
            background: #e0ca45;
            cursor: pointer;
            -webkit-appearance: none;
            margin-top: -8px;
            box-shadow: 0 0 10px rgba(224, 202, 69, 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>
        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x111111, 0.015);

        const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 30, 45);
        camera.lookAt(0, 0, 0);

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

        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;

        scene.add(new THREE.AmbientLight(0xffffff, 0.4));

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

        const fillLight = new THREE.DirectionalLight(0xaaccff, 0.4);
        fillLight.position.set(-20, 20, -20);
        scene.add(fillLight);

        const canvas = document.createElement('canvas');
        canvas.width = 512;
        canvas.height = 512;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = '#dfdfdf';
        ctx.fillRect(0, 0, 512, 512);
        ctx.fillStyle = '#222222';
        ctx.fillRect(0, 0, 256, 256);
        ctx.fillRect(256, 256, 256, 256);

        const texture = new THREE.CanvasTexture(canvas);
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;

        const gridSize = 14;
        const resolution = 12;
        const segments = gridSize * resolution;
        const planeSize = 60;

        texture.repeat.set(gridSize / 2, gridSize / 2);

        const geometry = new THREE.PlaneGeometry(planeSize, planeSize, segments, segments);
        geometry.rotateX(-Math.PI / 2);

        const basePositions = new Float32Array(geometry.attributes.position.array);

        const material = new THREE.MeshStandardMaterial({
            map: texture,
            roughness: 0.5,
            metalness: 0.1,
            side: THREE.DoubleSide
        });

        const mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);

        const slider = document.getElementById('morphSlider');
        let targetShift = 0;
        let currentShift = 0;

        slider.addEventListener('input', (e) => {
            targetShift = parseFloat(e.target.value);
        });

        function animate() {
            requestAnimationFrame(animate);

            currentShift += (targetShift - currentShift) * 0.1;

            const pos = geometry.attributes.position.array;
            const tileSize = planeSize / gridSize;

            for (let i = 0; i < pos.length; i += 3) {
                let x = basePositions[i];
                let y = basePositions[i + 1];
                let z = basePositions[i + 2];

                let gx = (x + planeSize / 2) / tileSize;
                let gz = (z + planeSize / 2) / tileSize;

                let morphProgress = (x + planeSize / 2) / planeSize;
                morphProgress += currentShift;
                let morph = Math.max(0, Math.min(1, morphProgress));
                morph = morph * morph * (3 - 2 * morph);

                let dx = Math.sin(gz * Math.PI) * 0.4 + Math.sin(gz * Math.PI * 2.0) * 0.15;
                let dz = -Math.sin(gx * Math.PI) * 0.4 - Math.sin(gx * Math.PI * 2.0) * 0.15;

                pos[i] = x + (dx * tileSize * morph);
                pos[i + 2] = z + (dz * tileSize * morph);

                let cx = Math.abs((gx % 1.0) - 0.5);
                let cz = Math.abs((gz % 1.0) - 0.5);
                let bulge = Math.max(0, 1.0 - (cx + cz) * 2.0);

                pos[i + 1] = y + (bulge * tileSize * 0.4 * morph);
            }

            geometry.attributes.position.needsUpdate = true;
            geometry.computeVertexNormals();

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

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

        animate();
    </script>
</body>
</html>