← Mike Higgins Home
Custom Code Experiment •

Tesseract 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>Tesseract 1</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #000000;
            overflow: hidden;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            touch-action: manipulation;
        }

        #canvas {
            display: block;
            background: transparent;
            cursor: grab;
        }

        #canvas:active {
            cursor: grabbing;
        }

        .mobile-toggle {
            display: none;
            position: fixed;
            top: 20px;
            right: 20px;
            width: 44px;
            height: 44px;
            border-radius: 50%;
            background: #ffffff;
            border: 1px solid rgba(255, 255, 255, 0.1);
            color: #000000;
            font-size: 20px;
            cursor: pointer;
            z-index: 1001;
            transition: all 0.3s ease;
            padding: 0;
            line-height: 44px;
            text-align: center;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
        }

        .mobile-toggle:hover {
            background: #f0f0f0;
            transform: scale(1.05);
        }

        .mobile-toggle:active {
            transform: scale(0.95);
        }

        .control-panel {
            position: fixed;
            top: 15px;
            right: 15px;
            background: rgba(0, 0, 0, 0.85);
            backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 12px;
            padding: 20px;
            color: white;
            font-size: 13px;
            max-width: 280px;
            z-index: 1000;
            box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
        }

        .control-panel.mobile-hidden {
            transform: translateX(120%);
            opacity: 0;
            pointer-events: none;
        }

        .control-panel.mobile-visible {
            transform: translateX(0);
            opacity: 1;
            pointer-events: auto;
        }

        .control-group {
            margin-bottom: 18px;
        }

        .control-group:last-child {
            margin-bottom: 0;
        }

        label {
            display: block;
            margin-bottom: 6px;
            font-weight: 500;
            color: #ffffff;
            font-size: 11px;
            letter-spacing: 0.5px;
            text-transform: uppercase;
        }

        input[type="range"] {
            width: 100%;
            height: 4px;
            border-radius: 2px;
            background: rgba(255, 255, 255, 0.1);
            outline: none;
            -webkit-appearance: none;
            appearance: none;
        }

        input[type="range"]::-webkit-slider-thumb {
            appearance: none;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: #ffffff;
            cursor: pointer;
            border: 2px solid #000000;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
        }

        input[type="range"]::-moz-range-thumb {
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: #ffffff;
            cursor: pointer;
            border: 2px solid #000000;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
        }

        .button-group {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }

        .button-group button:not(.mobile-toggle) {
            flex: 1;
            min-width: 60px;
            padding: 10px 14px;
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 8px;
            background: rgba(255, 255, 255, 0.1);
            color: white;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s ease;
            font-size: 12px;
            letter-spacing: 0.3px;
        }

        .button-group button:not(.mobile-toggle):hover {
            background: rgba(255, 255, 255, 0.15);
            border-color: rgba(255, 255, 255, 0.3);
        }

        .button-group button:not(.mobile-toggle):active {
            background: rgba(255, 255, 255, 0.2);
            transform: translateY(0);
        }

        .value-display {
            font-size: 11px;
            color: rgba(255, 255, 255, 0.7);
            text-align: right;
            margin-top: 4px;
            font-family: 'Monaco', 'Menlo', monospace;
        }

        .divider {
            height: 1px;
            background: rgba(255, 255, 255, 0.1);
            margin: 15px 0;
        }

        .instructions {
            position: fixed;
            bottom: 20px;
            left: 0;
            right: 0;
            text-align: center;
            color: rgba(255, 255, 255, 0.5);
            font-size: 11px;
            padding: 0 10px;
            z-index: 999;
            pointer-events: none;
            letter-spacing: 0.5px;
        }

        @media (min-width: 769px) {
            .instructions {
                display: none;
            }

            .control-panel {
                transition: none;
                transform: translateX(0);
                opacity: 1;
                pointer-events: auto;
            }
        }

        @media (max-width: 768px) {
            .mobile-toggle {
                display: block;
            }

            .control-panel {
                top: 10px;
                right: 10px;
                left: 10px;
                max-width: none;
                padding: 18px;
                font-size: 12px;
                max-height: 85vh;
                overflow-y: auto;
                transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
            }

            .control-group {
                margin-bottom: 16px;
            }

            .button-group button:not(.mobile-toggle) {
                font-size: 11px;
                padding: 9px 12px;
            }

            .instructions {
                display: block;
            }

            #closeButton {
                display: block !important;
            }
        }

        @media (max-height: 600px) {
            .control-panel {
                max-height: 75vh;
                overflow-y: auto;
            }
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>

    <button class="mobile-toggle" id="mobileToggle">⚙️</button>

    <div class="control-panel" id="controlPanel">
        <div class="control-group">
            <label>Rotation Speed</label>
            <input type="range" id="rotationSpeed" min="0.1" max="3" value="1" step="0.1">
            <div class="value-display" id="rotationValue">1.0</div>
        </div>

        <div class="control-group">
            <label>Pulse Intensity</label>
            <input type="range" id="pulseIntensity" min="0.1" max="2" value="2" step="0.1">
            <div class="value-display" id="pulseValue">2.0</div>
        </div>

        <div class="control-group">
            <label>Tessellation Density</label>
            <input type="range" id="tessellationDensity" min="4" max="20" value="8" step="2">
            <div class="value-display" id="tessellationValue">8</div>
        </div>

        <div class="divider"></div>

        <div class="control-group">
            <label>Trail Length</label>
            <input type="range" id="trailLength" min="0.8" max="0.99" value="0.95" step="0.01">
            <div class="value-display" id="trailValue">0.95</div>
        </div>

        <div class="control-group">
            <label>Color Shift Speed</label>
            <input type="range" id="colorSpeed" min="0.1" max="5" value="2" step="0.1">
            <div class="value-display" id="colorValue">2.0</div>
        </div>

        <div class="divider"></div>

        <div class="control-group">
            <div class="button-group">
                <button onclick="resetRotation()">Reset</button>
                <button onclick="randomizeColors()">Colors</button>
                <button onclick="togglePause()">Pause</button>
                <button onclick="closePanel()" id="closeButton" style="display: none;">Close</button>
            </div>
        </div>
    </div>

    <div class="instructions">
        Drag to rotate • Tap ⚙️ for controls
    </div>

    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        const mobileToggle = document.getElementById('mobileToggle');
        const controlPanel = document.getElementById('controlPanel');
        const closeButton = document.getElementById('closeButton');

        let isDragging = false;
        let lastTouchX = 0;
        let lastTouchY = 0;
        let animationId;
        let isPaused = false;
        let time = 0;
        let rotationSpeed = 1;
        let pulseIntensity = 2;
        let tessellationDensity = 8;
        let trailLength = 0.95;
        let colorSpeed = 2;

        let rotXY = 0, rotXZ = 0, rotXW = 0, rotYZ = 0, rotYW = 0, rotZW = 0;

        let colorOffset = 0;
        const colorPalettes = [
            ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57'],
            ['#e17055', '#00b894', '#0984e3', '#6c5ce7', '#fd79a8'],
            ['#ff7675', '#74b9ff', '#55a3ff', '#fd79a8', '#fdcb6e'],
            ['#a29bfe', '#fd79a8', '#fdcb6e', '#e17055', '#00b894']
        ];
        let currentPalette = 0;

        let isMobile = window.innerWidth <= 768;
        let panelOpen = false;

        function initUI() {
            isMobile = window.innerWidth <= 768;

            if (isMobile) {
                controlPanel.classList.add('mobile-hidden');
                controlPanel.classList.remove('mobile-visible');
                mobileToggle.style.display = 'block';
                panelOpen = false;
                mobileToggle.textContent = '⚙️';
                mobileToggle.style.background = '#ffffff';
                mobileToggle.style.color = '#000000';

                closeButton.style.display = 'block';
            } else {
                controlPanel.classList.remove('mobile-hidden');
                controlPanel.classList.remove('mobile-visible');
                mobileToggle.style.display = 'none';
                panelOpen = true;

                closeButton.style.display = 'none';
            }
        }

        function resizeCanvas() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        }

        function toggleControlPanel() {
            if (!isMobile) return;

            panelOpen = !panelOpen;
            if (panelOpen) {
                controlPanel.classList.remove('mobile-hidden');
                controlPanel.classList.add('mobile-visible');
                mobileToggle.textContent = '✕';
                mobileToggle.style.background = '#000000';
                mobileToggle.style.color = '#ffffff';
                mobileToggle.style.borderColor = 'rgba(255, 255, 255, 0.3)';
            } else {
                controlPanel.classList.remove('mobile-visible');
                controlPanel.classList.add('mobile-hidden');
                mobileToggle.textContent = '⚙️';
                mobileToggle.style.background = '#ffffff';
                mobileToggle.style.color = '#000000';
                mobileToggle.style.borderColor = 'rgba(255, 255, 255, 0.1)';
            }
        }

        function closePanel() {
            if (!isMobile) return;

            panelOpen = false;
            controlPanel.classList.remove('mobile-visible');
            controlPanel.classList.add('mobile-hidden');
            mobileToggle.textContent = '⚙️';
            mobileToggle.style.background = '#ffffff';
            mobileToggle.style.color = '#000000';
            mobileToggle.style.borderColor = 'rgba(255, 255, 255, 0.1)';
        }

        function point4D(x, y, z, w) {
            return { x, y, z, w };
        }

        function rotateXY(p, angle) {
            const cos = Math.cos(angle);
            const sin = Math.sin(angle);
            return point4D(
                cos * p.x - sin * p.y,
                sin * p.x + cos * p.y,
                p.z, p.w
            );
        }

        function rotateXZ(p, angle) {
            const cos = Math.cos(angle);
            const sin = Math.sin(angle);
            return point4D(
                cos * p.x - sin * p.z,
                p.y,
                sin * p.x + cos * p.z,
                p.w
            );
        }

        function rotateXW(p, angle) {
            const cos = Math.cos(angle);
            const sin = Math.sin(angle);
            return point4D(
                cos * p.x - sin * p.w,
                p.y, p.z,
                sin * p.x + cos * p.w
            );
        }

        function project4Dto3D(p) {
            const distance = 4;
            const w = 1 / (distance - p.w);
            return {
                x: p.x * w,
                y: p.y * w,
                z: p.z * w
            };
        }

        function project3Dto2D(p) {
            const distance = 1.8;
            const scale = Math.min(canvas.width, canvas.height) * 0.8;
            const z = 1 / (distance - p.z);
            return {
                x: canvas.width/2 + p.x * scale * z,
                y: canvas.height/2 + p.y * scale * z,
                depth: p.z
            };
        }

        function createTesseractVertices() {
            const vertices = [];
            for (let i = 0; i < 16; i++) {
                vertices.push(point4D(
                    (i & 1) ? 1 : -1,
                    (i & 2) ? 1 : -1,
                    (i & 4) ? 1 : -1,
                    (i & 8) ? 1 : -1
                ));
            }
            return vertices;
        }

        function getColorFromPalette(index, alpha = 1) {
            const colors = colorPalettes[currentPalette];
            const colorIndex = Math.floor((index + colorOffset) % colors.length);
            const color = colors[colorIndex];

            const r = parseInt(color.slice(1, 3), 16);
            const g = parseInt(color.slice(3, 5), 16);
            const b = parseInt(color.slice(5, 7), 16);

            return `rgba(${r}, ${g}, ${b}, ${alpha})`;
        }

        function drawEscherPattern(p1, p2, p3, p4, colorIndex) {
            const pulse = 1 + Math.sin(time * 3 + colorIndex) * 0.3 * pulseIntensity;

            ctx.save();

            const segments = tessellationDensity;
            for (let i = 0; i < segments; i++) {
                const t1 = i / segments;
                const t2 = (i + 1) / segments;

                const x1 = p1.x + (p2.x - p1.x) * t1;
                const y1 = p1.y + (p2.y - p1.y) * t1;
                const x2 = p3.x + (p4.x - p3.x) * t1;
                const y2 = p3.y + (p4.y - p3.y) * t1;

                const x3 = p1.x + (p2.x - p1.x) * t2;
                const y3 = p1.y + (p2.y - p1.y) * t2;
                const x4 = p3.x + (p4.x - p3.x) * t2;
                const y4 = p3.y + (p4.y - p3.y) * t2;

                ctx.beginPath();
                ctx.moveTo(x1, y1);

                const midX = (x1 + x2 + x3 + x4) / 4;
                const midY = (y1 + y2 + y3 + y4) / 4;

                const curve1X = midX + Math.sin(t1 * Math.PI * 4 + time) * 80 * pulse;
                const curve1Y = midY + Math.cos(t1 * Math.PI * 4 + time) * 80 * pulse;

                ctx.quadraticCurveTo(curve1X, curve1Y, x2, y2);
                ctx.lineTo(x4, y4);

                const curve2X = midX + Math.sin(t2 * Math.PI * 4 + time + Math.PI) * 20 * pulse;
                const curve2Y = midY + Math.cos(t2 * Math.PI * 4 + time + Math.PI) * 20 * pulse;

                ctx.quadraticCurveTo(curve2X, curve2Y, x3, y3);
                ctx.closePath();

                const alpha = Math.abs(Math.sin(t1 * Math.PI + time + colorIndex)) * 0.8 + 0.2;
                ctx.fillStyle = getColorFromPalette(colorIndex + i, alpha);
                ctx.fill();

                ctx.strokeStyle = getColorFromPalette(colorIndex + i + 1, 0.6);
                ctx.lineWidth = 1;
                ctx.stroke();
            }

            ctx.restore();
        }

        function animate() {
            if (isPaused) {
                animationId = requestAnimationFrame(animate);
                return;
            }

            time += 0.02 * rotationSpeed;
            colorOffset += 0.01 * colorSpeed;

            rotXY += 0.01 * rotationSpeed;
            rotXZ += 0.007 * rotationSpeed;
            rotXW += 0.013 * rotationSpeed;
            rotYZ += 0.011 * rotationSpeed;
            rotYW += 0.009 * rotationSpeed;
            rotZW += 0.005 * rotationSpeed;

            ctx.fillStyle = `rgba(0, 0, 0, ${1 - trailLength})`;
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            const vertices = createTesseractVertices();
            const transformedVertices = vertices.map(v => {
                let p = v;

                const pulse = 1 + Math.sin(time * 2) * 0.2 * pulseIntensity;
                p = point4D(p.x * pulse, p.y * pulse, p.z * pulse, p.w * pulse);

                p = rotateXY(p, rotXY);
                p = rotateXZ(p, rotXZ);
                p = rotateXW(p, rotXW);

                const p3d = project4Dto3D(p);
                return project3Dto2D(p3d);
            });

            const edges = [
                [0, 1], [1, 3], [3, 2], [2, 0],
                [4, 5], [5, 7], [7, 6], [6, 4],
                [0, 4], [1, 5], [2, 6], [3, 7],
                [8, 9], [9, 11], [11, 10], [10, 8],
                [12, 13], [13, 15], [15, 14], [14, 12],
                [8, 12], [9, 13], [10, 14], [11, 15],
                [0, 8], [1, 9], [2, 10], [3, 11],
                [4, 12], [5, 13], [6, 14], [7, 15]
            ];

            const edgesWithDepth = edges.map((edge, index) => ({
                edge,
                index,
                depth: (transformedVertices[edge[0]].depth + transformedVertices[edge[1]].depth) / 2
            })).sort((a, b) => a.depth - b.depth);

            edgesWithDepth.forEach(({ edge, index }) => {
                const v1 = transformedVertices[edge[0]];
                const v2 = transformedVertices[edge[1]];

                const dx = v2.x - v1.x;
                const dy = v2.y - v1.y;
                const length = Math.sqrt(dx * dx + dy * dy);

                if (length > 5) {
                    const perpX = -dy / length * 60;
                    const perpY = dx / length * 60;

                    const p1 = { x: v1.x + perpX, y: v1.y + perpY };
                    const p2 = { x: v2.x + perpX, y: v2.y + perpY };
                    const p3 = { x: v1.x - perpX, y: v1.y - perpY };
                    const p4 = { x: v2.x - perpX, y: v2.y - perpY };

                    drawEscherPattern(p1, p2, p3, p4, index);
                }
            });

            animationId = requestAnimationFrame(animate);
        }

        document.getElementById('rotationSpeed').addEventListener('input', (e) => {
            rotationSpeed = parseFloat(e.target.value);
            document.getElementById('rotationValue').textContent = rotationSpeed.toFixed(1);
        });

        document.getElementById('pulseIntensity').addEventListener('input', (e) => {
            pulseIntensity = parseFloat(e.target.value);
            document.getElementById('pulseValue').textContent = pulseIntensity.toFixed(1);
        });

        document.getElementById('tessellationDensity').addEventListener('input', (e) => {
            tessellationDensity = parseInt(e.target.value);
            document.getElementById('tessellationValue').textContent = tessellationDensity;
        });

        document.getElementById('trailLength').addEventListener('input', (e) => {
            trailLength = parseFloat(e.target.value);
            document.getElementById('trailValue').textContent = trailLength.toFixed(2);
        });

        document.getElementById('colorSpeed').addEventListener('input', (e) => {
            colorSpeed = parseFloat(e.target.value);
            document.getElementById('colorValue').textContent = colorSpeed.toFixed(1);
        });

        mobileToggle.addEventListener('click', toggleControlPanel);

        document.addEventListener('click', (e) => {
            if (isMobile && panelOpen &&
                !controlPanel.contains(e.target) &&
                e.target !== mobileToggle) {
                closePanel();
            }
        });

        canvas.addEventListener('touchstart', (e) => {
            e.preventDefault();
            isDragging = true;
            const touch = e.touches[0];
            lastTouchX = touch.clientX;
            lastTouchY = touch.clientY;

            if (isMobile && panelOpen) {
                closePanel();
            }
        });

        function resetRotation() {
            rotXY = rotXZ = rotXW = rotYZ = rotYW = rotZW = 0;
            time = 0;
        }

        function randomizeColors() {
            currentPalette = Math.floor(Math.random() * colorPalettes.length);
            colorOffset = Math.random() * 100;
        }

        function togglePause() {
            isPaused = !isPaused;
            const pauseButton = document.querySelector('button[onclick="togglePause()"]');
            if (pauseButton) {
                pauseButton.textContent = isPaused ? 'Play' : 'Pause';
            }
        }

        canvas.addEventListener('touchmove', (e) => {
            e.preventDefault();
            if (!isDragging) return;

            const touch = e.touches[0];
            const deltaX = (touch.clientX - lastTouchX) * 0.01;
            const deltaY = (touch.clientY - lastTouchY) * 0.01;

            rotXY += deltaX;
            rotXZ += deltaY;

            lastTouchX = touch.clientX;
            lastTouchY = touch.clientY;
        });

        canvas.addEventListener('touchend', (e) => {
            e.preventDefault();
            isDragging = false;
        });

        canvas.addEventListener('mousedown', (e) => {
            isDragging = true;
            lastTouchX = e.clientX;
            lastTouchY = e.clientY;
        });

        canvas.addEventListener('mousemove', (e) => {
            if (!isDragging) return;

            const deltaX = (e.clientX - lastTouchX) * 0.01;
            const deltaY = (e.clientY - lastTouchY) * 0.01;

            rotXY += deltaX;
            rotXZ += deltaY;

            lastTouchX = e.clientX;
            lastTouchY = e.clientY;
        });

        canvas.addEventListener('mouseup', () => {
            isDragging = false;
        });

        window.addEventListener('resize', () => {
            resizeCanvas();
            initUI();
        });

        resizeCanvas();
        initUI();
        animate();

        if (isMobile) {
            setTimeout(() => {
                if (panelOpen) {
                    closePanel();
                }
            }, 5000);
        }
    </script>
</body>
</html>