← Mike Higgins Home
Custom Code Experiment •

Slinky (Three,JS)

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>Wireframe Wormhole Tunnel</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background: #000;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        const scene = new THREE.Scene();
        scene.fog = new THREE.Fog(0x000000, 10, 150);

        const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 0;

        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        const tunnelSegments = 150;
        const ringsPerSegment = 1;
        const tunnelLength = 300;
        const rings = [];

        function getCurvedPosition(progress) {
            const z = -progress * tunnelLength;
            // Create a winding path that loops seamlessly - much gentler curves
            const x = Math.sin(progress * Math.PI * 4) * 3;
            const y = Math.cos(progress * Math.PI * 4) * 3;
            return { x, y, z };
        }

        for (let i = 0; i < tunnelSegments; i++) {
            const progress = i / tunnelSegments;
            const pos = getCurvedPosition(progress);

            const radius = 15 + Math.sin(progress * Math.PI * 8) * 2;
            const geometry = new THREE.TorusGeometry(radius, 0.15, 8, 32);

            const hue = (progress * 0.6 + 0.5) % 1;
            const color = new THREE.Color().setHSL(hue, 1, 0.5);
            const material = new THREE.MeshBasicMaterial({
                color: color,
                wireframe: true,
                transparent: true,
                opacity: 0.8
            });

            const ring = new THREE.Mesh(geometry, material);
            ring.position.set(pos.x, pos.y, pos.z);

            if (i < tunnelSegments - 1) {
                const nextPos = getCurvedPosition((i + 1) / tunnelSegments);
                const direction = new THREE.Vector3(
                    nextPos.x - pos.x,
                    nextPos.y - pos.y,
                    nextPos.z - pos.z
                ).normalize();

                const up = new THREE.Vector3(0, 1, 0);
                const right = new THREE.Vector3().crossVectors(up, direction).normalize();
                const newUp = new THREE.Vector3().crossVectors(direction, right);

                const matrix = new THREE.Matrix4();
                matrix.makeBasis(right, newUp, direction);
                ring.rotation.setFromRotationMatrix(matrix);
            }

            scene.add(ring);
            rings.push({ mesh: ring, index: i });
        }

        const starGeometry = new THREE.BufferGeometry();
        const starVertices = [];
        for (let i = 0; i < 1000; i++) {
            const x = (Math.random() - 0.5) * 200;
            const y = (Math.random() - 0.5) * 200;
            const z = (Math.random() - 0.5) * 200;
            starVertices.push(x, y, z);
        }
        starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3));
        const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.1 });
        const stars = new THREE.Points(starGeometry, starMaterial);
        scene.add(stars);

        let speed = 0.004; // Increased speed for better flying sensation
        let pathProgress = 0;
        let rotationSpeed = 0.002;
        let cameraRoll = 0;

        function animate() {
            requestAnimationFrame(animate);

            pathProgress += speed;
            if (pathProgress > 1) pathProgress -= 1;

            const currentPos = getCurvedPosition(pathProgress);
            camera.position.set(currentPos.x, currentPos.y, currentPos.z);

            const lookAheadProgress = (pathProgress + 0.02) % 1;
            const lookAtPos = getCurvedPosition(lookAheadProgress);

            const direction = new THREE.Vector3(
                lookAtPos.x - currentPos.x,
                lookAtPos.y - currentPos.y,
                lookAtPos.z - currentPos.z
            ).normalize();

            const nextProgress = (pathProgress + 0.01) % 1;
            const nextPos = getCurvedPosition(nextProgress);
            const lateralChange = nextPos.x - currentPos.x;
            cameraRoll = -lateralChange * 0.5; // Bank into turns

            camera.lookAt(lookAtPos.x, lookAtPos.y, lookAtPos.z);

            camera.rotateZ(cameraRoll);

            rings.forEach((ringData, idx) => {
                const ring = ringData.mesh;
                ring.rotation.z += rotationSpeed * (1 + idx * 0.005);

                const ringProgress = ringData.index / tunnelSegments;
                const relativeProgress = (ringProgress - pathProgress + 1) % 1;

                if (relativeProgress > 0.95 || relativeProgress < 0.05) {
                    const newIndex = Math.floor((pathProgress * tunnelSegments + tunnelSegments * 0.5)) % tunnelSegments;
                    ringData.index = newIndex;

                    const newProgress = newIndex / tunnelSegments;
                    const newPos = getCurvedPosition(newProgress);
                    const newRadius = 15 + Math.sin(newProgress * Math.PI * 8) * 2;

                    const newGeometry = new THREE.TorusGeometry(newRadius, 0.15, 8, 32);
                    ring.geometry.dispose();
                    ring.geometry = newGeometry;

                    ring.position.set(newPos.x, newPos.y, newPos.z);

                    const nextIndex = (newIndex + 1) % tunnelSegments;
                    const nextPos = getCurvedPosition(nextIndex / tunnelSegments);
                    const direction = new THREE.Vector3(
                        nextPos.x - newPos.x,
                        nextPos.y - newPos.y,
                        nextPos.z - newPos.z
                    ).normalize();

                    const up = new THREE.Vector3(0, 1, 0);
                    const right = new THREE.Vector3().crossVectors(up, direction).normalize();
                    const newUp = new THREE.Vector3().crossVectors(direction, right);

                    const matrix = new THREE.Matrix4();
                    matrix.makeBasis(right, newUp, direction);
                    ring.rotation.setFromRotationMatrix(matrix);

                    const hue = (newProgress * 0.6 + 0.5) % 1;
                    ring.material.color.setHSL(hue, 1, 0.5);
                }
            });

            stars.rotation.z += 0.0001;

            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>