<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive 3D Hypercube Projection</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="info">Drag to rotate, Scroll to zoom, Right-click to pan</div>
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>
    <script type="module" src="script.js"></script>
</body>
</html>
body {
    margin: 0;
    overflow: hidden;
    background-color: #050505; /* Darker background */
    color: #ccc;
    font-family: Arial, sans-serif;
}

canvas {
    display: block; /* Prevent scrollbars */
}

#info {
    position: absolute;
    top: 10px;
    left: 10px;
    padding: 5px 10px;
    background-color: rgba(0, 0, 0, 0.5);
    border-radius: 5px;
    font-size: 0.9em;
}
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// --- Three.js Setup ---
let scene, camera, renderer, controls;
let hypercubeLines, hypercubeGeometry;
let ballMeshes = [];
const clock = new THREE.Clock();

// --- Hypercube Configuration ---
const DIMENSIONS = 9;
const CUBE_SIZE = 3; // Size in 3D space units
const NUM_BALLS = 20; // Increase balls?
const BALL_RADIUS = 0.08; // Radius in 3D space units
const BALL_SPEED_SCALE = 1.5; // Speed relative to cube size

// Rotation speeds (radians per second) for 9D planes
const rotationSpeeds = [
    { dims: [0, 1], speed: 0.25 }, { dims: [2, 3], speed: 0.28 },
    { dims: [4, 5], speed: 0.31 }, { dims: [6, 7], speed: 0.34 },
    { dims: [8, 0], speed: 0.37 }, { dims: [1, 3], speed: -0.29 },
    { dims: [4, 6], speed: -0.32 }, { dims: [5, 8], speed: -0.35 },
    // Add more planes for more complex rotation
    { dims: [0, 2], speed: 0.15 }, { dims: [1, 4], speed: -0.18 },
    { dims: [3, 5], speed: 0.21 }, { dims: [6, 8], speed: -0.24 },
];
const rotationAngles = new Array(rotationSpeeds.length).fill(0);

// --- Hypercube Data ---
let vertices9D = []; // Store original 9D vertex positions
let edges = [];    // Store indices for edges [v1_idx, v2_idx]
let balls = [];    // Store ball data { position: [9D], velocity: [9D] }

// --- Initialization ---

function init() {
    // Scene
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x050505);

    // Camera
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = CUBE_SIZE * 2.5; // Adjust camera distance based on size

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

    // Controls
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; // Smooth camera movement
    controls.dampingFactor = 0.05;
    controls.screenSpacePanning = false; // Panning relative to camera orientation
    controls.minDistance = 1;
    controls.maxDistance = CUBE_SIZE * 10;

    // Lighting (optional, but good for non-basic materials)
    const ambientLight = new THREE.AmbientLight(0x404040); // Soft white light
    scene.add(ambientLight);
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(1, 1, 1).normalize();
    scene.add(directionalLight);

    // Generate Hypercube Structure (9D data)
    generateHypercubeData();

    // Create Three.js Geometry for Hypercube Lines
    createHypercubeGeometry();

    // Initialize Balls (9D data and 3D meshes)
    initializeBalls();

    // Handle Window Resize
    window.addEventListener('resize', onWindowResize);

    // Start Animation Loop
    animate();
}

// --- Hypercube Data Generation (9D) ---

function generateHypercubeData() {
    const numVertices = 1 << DIMENSIONS;
    const halfSize = CUBE_SIZE / 2; // Use 3D CUBE_SIZE for scale reference

    // Generate 9D Vertices
    vertices9D = [];
    for (let i = 0; i < numVertices; i++) {
        const vertex = new Array(DIMENSIONS);
        for (let d = 0; d < DIMENSIONS; d++) {
            vertex[d] = ((i >> d) & 1) ? halfSize : -halfSize;
        }
        vertices9D.push(vertex);
    }

    // Generate Edges (Indices)
    edges = [];
    for (let i = 0; i < numVertices; i++) {
        for (let j = i + 1; j < numVertices; j++) {
            let diffCount = 0;
            for (let d = 0; d < DIMENSIONS; d++) {
                if (vertices9D[i][d] !== vertices9D[j][d]) {
                    diffCount++;
                }
            }
            if (diffCount === 1) {
                edges.push([i, j]);
            }
        }
    }
    console.log(`Generated ${DIMENSIONS}D Hypercube Data: ${vertices9D.length} vertices, ${edges.length} edges`);
}

// --- Three.js Hypercube Geometry ---

function createHypercubeGeometry() {
    const numVertices = vertices9D.length;
    const projectedVertices = new Float32Array(numVertices * 3); // x, y, z for each vertex
    const colors = new Float32Array(numVertices * 3); // r, g, b for each vertex

    // Initialize positions (can be anything, will be updated frame 1)
    for (let i = 0; i < numVertices; i++) {
         projectedVertices[i * 3] = 0;
         projectedVertices[i * 3 + 1] = 0;
         projectedVertices[i * 3 + 2] = 0;
         // Initial color (white)
         colors[i * 3] = 1;
         colors[i * 3 + 1] = 1;
         colors[i * 3 + 2] = 1;
    }

    hypercubeGeometry = new THREE.BufferGeometry();
    hypercubeGeometry.setAttribute('position', new THREE.BufferAttribute(projectedVertices, 3));
    hypercubeGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

    // Set indices for the lines
    const indices = [];
    edges.forEach(edge => {
        indices.push(edge[0], edge[1]);
    });
    hypercubeGeometry.setIndex(indices);

    // Material for the lines - Enable vertex colors!
    const lineMaterial = new THREE.LineBasicMaterial({
        vertexColors: true, // Use colors defined in the geometry attribute
        linewidth: 1.5 // Note: linewidth > 1 might not work on all platforms/drivers
    });

    hypercubeLines = new THREE.LineSegments(hypercubeGeometry, lineMaterial);
    scene.add(hypercubeLines);
}

// --- Ball Initialization (9D Data + 3D Mesh) ---

function initializeBalls() {
    const halfSize = CUBE_SIZE / 2;
    const ballGeometry = new THREE.SphereGeometry(BALL_RADIUS, 16, 8); // Simple sphere

    for (let i = 0; i < NUM_BALLS; i++) {
        // 9D Physics Data
        const position9D = new Array(DIMENSIONS);
        const velocity9D = new Array(DIMENSIONS);
        for (let d = 0; d < DIMENSIONS; d++) {
            position9D[d] = (Math.random() - 0.5) * halfSize * 0.8; // Start near center
            velocity9D[d] = (Math.random() - 0.5) * BALL_SPEED_SCALE;
        }
        balls.push({ position: position9D, velocity: velocity9D });

        // 3D Render Mesh
        // Use MeshBasicMaterial for bright colors without lighting influence
        const ballMaterial = new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff });
        const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
        ballMesh.position.set(0, 0, 0); // Initial position, will be updated
        ballMeshes.push(ballMesh);
        scene.add(ballMesh);
    }
}

// --- Rotation Logic (9D) ---

function rotatePoint9D(point, dim1, dim2, angle) {
    const d1 = point[dim1];
    const d2 = point[dim2];
    const cosA = Math.cos(angle);
    const sinA = Math.sin(angle);
    point[dim1] = d1 * cosA - d2 * sinA;
    point[dim2] = d1 * sinA + d2 * cosA;
}

function apply9DRotations(point9D, deltaTime) {
    // Apply incremental rotation based on delta time
    // Work on a copy to avoid modifying the original data structure if needed elsewhere
    const rotatedPoint = [...point9D];
    for (let i = 0; i < rotationSpeeds.length; i++) {
        const { dims, speed } = rotationSpeeds[i];
        const angleIncrement = speed * deltaTime;
        // Update total angle (can be useful)
        rotationAngles[i] += angleIncrement;
        // Rotate the point incrementally
        rotatePoint9D(rotatedPoint, dims[0], dims[1], angleIncrement);
    }
    return rotatedPoint;
     // Alternatively: If applying total rotation from original vertices:
     // const rotatedPoint = [...point9D]; // Start with original
     // for (let i = 0; i < rotationSpeeds.length; i++) {
     //     rotatePoint9D(rotatedPoint, rotationSpeeds[i].dims[0], rotationSpeeds[i].dims[1], rotationAngles[i]);
     // }
     // return rotatedPoint;
}

// --- Projection (9D -> 3D) ---

function project9Dto3D(point9D) {
    // Simple projection: take the first 3 coordinates
    // More complex projections could be used here
    return new THREE.Vector3(point9D[0], point9D[1], point9D[2]);
}

// --- Ball Physics (9D) ---

function updateBallsPhysics(deltaTime) {
    const halfSize = CUBE_SIZE / 2;
    balls.forEach(ball => {
        for (let d = 0; d < DIMENSIONS; d++) {
            // Update 9D position
            ball.position[d] += ball.velocity[d] * deltaTime;

            // 9D Boundary Check & Bounce
            const effectiveRadius = 0; // Treat as point for collision simplicity
            if (ball.position[d] < -halfSize + effectiveRadius) {
                ball.position[d] = -halfSize + effectiveRadius;
                ball.velocity[d] *= -0.9; // Dampen bounce slightly
            } else if (ball.position[d] > halfSize - effectiveRadius) {
                ball.position[d] = halfSize - effectiveRadius;
                ball.velocity[d] *= -0.9; // Dampen bounce slightly
            }
        }
    });
}

// --- Animation Loop ---

function animate() {
    requestAnimationFrame(animate);

    const deltaTime = clock.getDelta();
    const elapsedTime = clock.getElapsedTime();

    // 1. Update Controls (for mouse interaction)
    controls.update();

    // 2. Update Ball Physics (in 9D)
    updateBallsPhysics(deltaTime);

    // 3. Update Hypercube Geometry (Rotate 9D -> Project 3D -> Update Buffer)
    const positionAttribute = hypercubeGeometry.attributes.position;
    const colorAttribute = hypercubeGeometry.attributes.color;
    const numVertices = vertices9D.length;

    // -- Cool Animation: Color Pulsing --
    // Cycle hue over time
    const hue = (elapsedTime * 0.1) % 1; // Adjust speed with 0.1
    const pulseColor = new THREE.Color();

    for (let i = 0; i < numVertices; i++) {
        // Rotate original 9D vertex position incrementally
        // IMPORTANT: Rotate the *original* vertex data each frame for accuracy
        let rotatedVertex9D = [...vertices9D[i]]; // Start with original
        for(let r=0; r < rotationSpeeds.length; ++r){
             // Apply total accumulated angle for this frame to original vertex
             rotatePoint9D(rotatedVertex9D, rotationSpeeds[r].dims[0], rotationSpeeds[r].dims[1], rotationAngles[r]);
        }
        // Or apply incremental (less accurate over time):
        // rotatedVertex9D = apply9DRotations(vertices9D[i], deltaTime); // Incorrect if vertices9D is modified

        // Project rotated 9D vertex to 3D
        const projected3D = project9Dto3D(rotatedVertex9D);

        // Update position buffer
        positionAttribute.setXYZ(i, projected3D.x, projected3D.y, projected3D.z);

        // Update color buffer for pulsing effect
        // Use vertex position or index to vary the pulse phase slightly
        const phaseOffset = (projected3D.x + projected3D.y + projected3D.z) * 0.1; // Small offset based on position
        pulseColor.setHSL((hue + phaseOffset) % 1, 0.8, 0.6); // Saturation, Lightness

        colorAttribute.setXYZ(i, pulseColor.r, pulseColor.g, pulseColor.b);
    }
    // Update total rotation angles AFTER using them for this frame's rotation
     for (let i = 0; i < rotationSpeeds.length; i++) {
        rotationAngles[i] += rotationSpeeds[i].speed * deltaTime;
     }

    positionAttribute.needsUpdate = true; // Flag buffers for update
    colorAttribute.needsUpdate = true;

    // 4. Update Ball Meshes (Rotate 9D Pos -> Project 3D -> Update Mesh)
    balls.forEach((ball, index) => {
        // Rotate the ball's current 9D position
         let rotatedBallPos9D = [...ball.position]; // Start with current physics position
         for(let r=0; r < rotationSpeeds.length; ++r){
              rotatePoint9D(rotatedBallPos9D, rotationSpeeds[r].dims[0], rotationSpeeds[r].dims[1], rotationAngles[r]);
         }
        // Or apply incremental:
        // rotatedBallPos9D = apply9DRotations(ball.position, deltaTime); // Incorrect if ball.position modified by physics

        // Project to 3D
        const projectedBall3D = project9Dto3D(rotatedBallPos9D);

        // Update the 3D mesh position
        ballMeshes[index].position.copy(projectedBall3D);

        // Optional: Change ball color based on speed/position?
        // const speedMag = Math.sqrt(ball.velocity.reduce((sum, v) => sum + v*v, 0));
        // ballMeshes[index].material.color.setHSL(speedMag * 0.01 % 1, 1, 0.5);
    });

    // 5. Render the Scene
    renderer.render(scene, camera);
}

// --- Window Resize Handler ---

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

// --- Start ---
init();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.