← Mike Higgins Home
Custom Code Experiment •

ASCII: Animated Shapes

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>ASCII Animations 1-5</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #000;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            overflow: hidden;
        }

        pre {
            font-family: 'Courier New', monospace;
            font-size: 18px;
            line-height: 1.1;
            color: #fff;
            text-align: center;
            margin: 0;
            padding: 0;
            filter: brightness(1.1);
            text-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
            white-space: pre;
            width: 100%;
            height: 484px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .nav-container {
            position: fixed;
            top: 30px;
            display: flex;
            align-items: center;
            gap: 20px;
            z-index: 10;
            background-color: rgba(0, 0, 0, 0.6);
            padding: 8px 16px;
            border-radius: 10px;
            border: 1px solid rgba(255, 255, 255, 0.2);
            backdrop-filter: blur(10px);
        }

        .nav-button {
            padding: 8px 16px;
            font-size: 16px;
            font-family: 'Courier New', monospace;
            background-color: rgba(34, 34, 34, 0.5);
            color: #fff;
            border: 1px solid rgba(255, 255, 255, 0.3);
            cursor: pointer;
            border-radius: 6px;
            transition: all 0.3s ease;
            min-width: 40px;
            text-align: center;
            user-select: none;
        }

        .nav-button:hover {
            background-color: rgba(68, 68, 68, 0.7);
            border-color: rgba(255, 255, 255, 0.6);
            box-shadow: 0 0 10px rgba(255, 255, 255, 0.4);
            transform: translateY(-1px);
        }

        .nav-button:active {
            transform: translateY(0);
            box-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
        }

        .animation-number {
            color: #fff;
            font-family: 'Courier New', monospace;
            font-size: 20px;
            font-weight: bold;
            min-width: 40px;
            text-align: center;
            padding: 6px 12px;
            border: 1px solid rgba(255, 255, 255, 0.3);
            border-radius: 6px;
            background-color: rgba(255, 255, 255, 0.05);
        }

        .arrow {
            font-size: 18px;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div class="nav-container">
        <button class="nav-button arrow" id="prevBtn">←</button>
        <div class="animation-number" id="animationNumber">1</div>
        <button class="nav-button arrow" id="nextBtn">→</button>
    </div>

    <pre id="display"></pre>

    <script>
        const pre = document.getElementById('display');
        const prevBtn = document.getElementById('prevBtn');
        const nextBtn = document.getElementById('nextBtn');
        const animationNumber = document.getElementById('animationNumber');

        const animations = ['donut', 'cube', 'octahedron', 'dodecahedron', 'tetrahedron'];
        let currentIndex = 0;
        let animationId = null;

        // Donut variables
        let x = 1760, z = 0, y = 0;

        // Shared variables for 3D shapes
        let A = 0, B = 0, C = 0;
        const width = 160;
        const height = 44;
        const zBuffer = new Array(width * height);
        const buffer = new Array(width * height);
        const backgroundASCIICode = ' ';
        const distanceFromCam = 100;
        const K1 = 40;

        // 3D rotation and projection functions
        const calculateX = (i, j, k) => {
            return j * Math.sin(A) * Math.sin(B) * Math.cos(C) - k * Math.cos(A) * Math.sin(B) * Math.cos(C) +
                j * Math.cos(A) * Math.sin(C) + k * Math.sin(A) * Math.sin(C) + i * Math.cos(B) * Math.cos(C);
        };

        const calculateY = (i, j, k) => {
            return j * Math.cos(A) * Math.cos(C) + k * Math.sin(A) * Math.cos(C) -
                j * Math.sin(A) * Math.sin(B) * Math.sin(C) + k * Math.cos(A) * Math.sin(B) * Math.sin(C) -
                i * Math.cos(B) * Math.sin(C);
        };

        const calculateZ = (i, j, k) => {
            return k * Math.cos(A) * Math.cos(B) - j * Math.sin(A) * Math.cos(B) + i * Math.sin(B);
        };

        const renderPoint = (px, py, pz, ch) => {
            const x = calculateX(px, py, pz);
            const y = calculateY(px, py, pz);
            const z = calculateZ(px, py, pz) + distanceFromCam;

            const ooz = 1 / z;

            const xp = Math.floor(width / 2 + K1 * ooz * x * 2);
            const yp = Math.floor(height / 2 + K1 * ooz * y);

            const idx = xp + yp * width;
            if (idx >= 0 && idx < width * height) {
                if (ooz > zBuffer[idx]) {
                    zBuffer[idx] = ooz;
                    buffer[idx] = ch;
                }
            }
        };

        // Donut animation
        const animateDonut = () => {
            z += 0.02;
            y += 0.01;

            const a = [...new Array(x)].map((a, r) => r % 80 === 79 ? "\n" : " ");
            const r = new Array(x).fill(0);

            const t = Math.cos(z);
            const e = Math.sin(z);
            const n = Math.cos(y);
            const o = Math.sin(y);

            for (let s = 0; s < 6.28; s += 0.07) {
                const c = Math.cos(s);
                const h = Math.sin(s);

                for (let s = 0; s < 6.28; s += 0.02) {
                    const v = Math.sin(s);
                    const M = Math.cos(s);
                    const i = c + 2;
                    const l = 1 / (v * i * e + h * t + 5);
                    const p = v * i * t - h * e;
                    const d = 0 | (40 + 30 * l * (M * i * n - p * o));
                    const m = 0 | (12 + 15 * l * (M * i * o + p * n));
                    const f = 0 | (8 * ((h * e - v * c * t) * n - v * c * e - h * t - M * c * o));
                    const yIndex = d + 80 * m;

                    if (m < 22 && m >= 0 && d >= 0 && d < 79 && l > r[yIndex]) {
                        r[yIndex] = l;
                        a[yIndex] = ".,-~:;=!*#$@"[f > 0 ? f : 0];
                    }
                }
            }

            pre.textContent = a.join("");

            if (animations[currentIndex] === 'donut') {
                setTimeout(() => {
                    animationId = requestAnimationFrame(animateDonut);
                }, 100);
            }
        };

        // Cube animation
        const animateCube = () => {
            buffer.fill(backgroundASCIICode);
            zBuffer.fill(0);

            const cubeSize = 20;

            for (let cubeX = -cubeSize; cubeX < cubeSize; cubeX += 0.5) {
                for (let cubeY = -cubeSize; cubeY < cubeSize; cubeY += 0.5) {
                    renderPoint(cubeX, cubeY, -cubeSize, '@');
                    renderPoint(cubeX, cubeY, cubeSize, '#');
                    renderPoint(-cubeSize, cubeY, cubeX, '$');
                    renderPoint(cubeSize, cubeY, cubeX, '~');
                    renderPoint(cubeX, cubeSize, cubeY, ';');
                    renderPoint(cubeX, -cubeSize, cubeY, '+');
                }
            }

            let output = '';
            for (let k = 0; k < width * height; k++) {
                output += k % width === 0 && k !== 0 ? '\n' : buffer[k];
            }

            pre.textContent = output;

            A += 0.05;
            B += 0.05;
            C += 0.01;

            if (animations[currentIndex] === 'cube') {
                setTimeout(() => {
                    animationId = requestAnimationFrame(animateCube);
                }, 50);
            }
        };

        // Octahedron animation
        const animateOctahedron = () => {
            buffer.fill(backgroundASCIICode);
            zBuffer.fill(0);

            const size = 20;

            for (let u = 0; u <= 1; u += 0.02) {
                for (let v = 0; v <= 1 - u; v += 0.02) {
                    const x1 = (u - 0.5) * size * 2;
                    const y1 = size - (u + v) * size;
                    const z1 = (v - 0.5) * size * 2;
                    renderPoint(x1, y1, z1, '*');

                    const x2 = (v - 0.5) * size * 2;
                    const y2 = size - (u + v) * size;
                    const z2 = -(u - 0.5) * size * 2;
                    renderPoint(x2, y2, z2, '@');

                    const x3 = -(u - 0.5) * size * 2;
                    const y3 = size - (u + v) * size;
                    const z3 = -(v - 0.5) * size * 2;
                    renderPoint(x3, y3, z3, '#');

                    const x4 = -(v - 0.5) * size * 2;
                    const y4 = size - (u + v) * size;
                    const z4 = (u - 0.5) * size * 2;
                    renderPoint(x4, y4, z4, '$');
                }
            }

            for (let u = 0; u <= 1; u += 0.02) {
                for (let v = 0; v <= 1 - u; v += 0.02) {
                    const x1 = (u - 0.5) * size * 2;
                    const y1 = -size + (u + v) * size;
                    const z1 = (v - 0.5) * size * 2;
                    renderPoint(x1, y1, z1, '+');

                    const x2 = (v - 0.5) * size * 2;
                    const y2 = -size + (u + v) * size;
                    const z2 = -(u - 0.5) * size * 2;
                    renderPoint(x2, y2, z2, '~');

                    const x3 = -(u - 0.5) * size * 2;
                    const y3 = -size + (u + v) * size;
                    const z3 = -(v - 0.5) * size * 2;
                    renderPoint(x3, y3, z3, '=');

                    const x4 = -(v - 0.5) * size * 2;
                    const y4 = -size + (u + v) * size;
                    const z4 = (u - 0.5) * size * 2;
                    renderPoint(x4, y4, z4, ':');
                }
            }

            let output = '';
            for (let k = 0; k < width * height; k++) {
                output += k % width === 0 && k !== 0 ? '\n' : buffer[k];
            }

            pre.textContent = output;

            A += 0.05;
            B += 0.05;
            C += 0.01;

            if (animations[currentIndex] === 'octahedron') {
                setTimeout(() => {
                    animationId = requestAnimationFrame(animateOctahedron);
                }, 50);
            }
        };

        // Dodecahedron animation
        const animateDodecahedron = () => {
            buffer.fill(backgroundASCIICode);
            zBuffer.fill(0);

            const size = 60;
            const phi = (1 + Math.sqrt(5)) / 2;
            const a = size / 2;
            const b = size / (2 * phi);

            const verts = [
                [0, b, -a], [b, a, 0], [-b, a, 0], [0, b, a],
                [0, -b, a], [-a, 0, b], [a, 0, b], [0, -b, -a],
                [a, 0, -b], [-a, 0, -b], [b, -a, 0], [-b, -a, 0]
            ];

            const faces = [
                [0,1,2],[3,2,1],[3,4,5],[3,1,6],[0,2,9],[0,8,1],[0,7,8],
                [0,9,7],[4,3,6],[4,10,5],[6,1,10],[7,11,8],[5,11,4],[10,11,5],
                [8,11,10],[8,10,1],[9,2,11],[9,11,7],[2,3,5],[2,5,11]
            ];

            const chars = '@#$~*+=:;.-%^&!?';

            faces.forEach((face, idx) => {
                for (let u = 0; u <= 1; u += 0.015) {
                    for (let v = 0; v <= 1 - u; v += 0.015) {
                        const v0 = verts[face[0]];
                        const v1 = verts[face[1]];
                        const v2 = verts[face[2]];

                        const px = v0[0] * (1 - u - v) + v1[0] * u + v2[0] * v;
                        const py = v0[1] * (1 - u - v) + v1[1] * u + v2[1] * v;
                        const pz = v0[2] * (1 - u - v) + v1[2] * u + v2[2] * v;

                        renderPoint(px, py, pz, chars[idx % chars.length]);
                    }
                }
            });

            let output = '';
            for (let k = 0; k < width * height; k++) {
                output += k % width === 0 && k !== 0 ? '\n' : buffer[k];
            }

            pre.textContent = output;

            A += 0.05;
            B += 0.05;
            C += 0.01;

            if (animations[currentIndex] === 'dodecahedron') {
                setTimeout(() => {
                    animationId = requestAnimationFrame(animateDodecahedron);
                }, 50);
            }
        };

        // Tetrahedron animation
        const animateTetrahedron = () => {
            buffer.fill(backgroundASCIICode);
            zBuffer.fill(0);

            const size = 35;

            const vertices = [
                [size, size, size],
                [-size, -size, size],
                [-size, size, -size],
                [size, -size, -size]
            ];

            const faces = [
                [0, 1, 2],
                [0, 1, 3],
                [0, 2, 3],
                [1, 2, 3]
            ];

            const chars = ['@', '#', '$', '*'];

            faces.forEach((face, faceIdx) => {
                for (let u = 0; u <= 1; u += 0.015) {
                    for (let v = 0; v <= 1 - u; v += 0.015) {
                        const v0 = vertices[face[0]];
                        const v1 = vertices[face[1]];
                        const v2 = vertices[face[2]];

                        const px = v0[0] * (1 - u - v) + v1[0] * u + v2[0] * v;
                        const py = v0[1] * (1 - u - v) + v1[1] * u + v2[1] * v;
                        const pz = v0[2] * (1 - u - v) + v1[2] * u + v2[2] * v;

                        renderPoint(px, py, pz, chars[faceIdx]);
                    }
                }
            });

            let output = '';
            for (let k = 0; k < width * height; k++) {
                output += k % width === 0 && k !== 0 ? '\n' : buffer[k];
            }

            pre.textContent = output;

            A += 0.05;
            B += 0.05;
            C += 0.01;

            if (animations[currentIndex] === 'tetrahedron') {
                setTimeout(() => {
                    animationId = requestAnimationFrame(animateTetrahedron);
                }, 50);
            }
        };

        // Stop current animation and start new one
        const switchAnimation = () => {
            if (animationId) {
                cancelAnimationFrame(animationId);
            }

            // Reset animation variables
            x = 1760; z = 0; y = 0;
            A = 0; B = 0; C = 0;

            // Update display
            animationNumber.textContent = (currentIndex + 1).toString();

            // Start new animation
            if (animations[currentIndex] === 'donut') {
                animateDonut();
            } else if (animations[currentIndex] === 'cube') {
                animateCube();
            } else if (animations[currentIndex] === 'octahedron') {
                animateOctahedron();
            } else if (animations[currentIndex] === 'dodecahedron') {
                animateDodecahedron();
            } else if (animations[currentIndex] === 'tetrahedron') {
                animateTetrahedron();
            }
        };

        // Navigation functions
        prevBtn.addEventListener('click', () => {
            currentIndex = (currentIndex - 1 + animations.length) % animations.length;
            switchAnimation();
        });

        nextBtn.addEventListener('click', () => {
            currentIndex = (currentIndex + 1) % animations.length;
            switchAnimation();
        });

        // Keyboard navigation
        document.addEventListener('keydown', (e) => {
            if (e.key === 'ArrowLeft') {
                currentIndex = (currentIndex - 1 + animations.length) % animations.length;
                switchAnimation();
            } else if (e.key === 'ArrowRight') {
                currentIndex = (currentIndex + 1) % animations.length;
                switchAnimation();
            }
        });

        // Start with first animation
        switchAnimation();
    </script>
</body>
</html>