JIT
Grid - Untitled
Generate a single-file HTML simulation of a physics-based hourglass. Sand particles must fall through a narrow neck and pile up realistically in a cone shape (granular physics). Clicking the canvas triggers a smooth 180-degree rotation, reversing the flow of sand.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Physics Hourglass</title> <style> :root { --bg-color: #121212; --text-color: #e0e0e0; --accent-color: #f0c05a; }
    body {
        margin: 0;
        padding: 0;
        width: 100vw;
        height: 100vh;
        background-color: var(--bg-color);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        font-family: 'Courier New', Courier, monospace;
        overflow: hidden;
        user-select: none;
        -webkit-user-select: none;
    }

    h1 {
        color: var(--text-color);
        margin-bottom: 1rem;
        font-size: clamp(1rem, 3vw, 1.5rem);
        text-transform: uppercase;
        letter-spacing: 2px;
        opacity: 0.8;
    }

    .canvas-container {
        position: relative;
        width: auto;
        height: 75vh;
        aspect-ratio: 2 / 3;
        display: flex;
        justify-content: center;
        align-items: center;
        transition: transform 0.8s cubic-bezier(0.45, 0, 0.55, 1);
        cursor: pointer;
        filter: drop-shadow(0 0 20px rgba(0,0,0,0.5));
    }

    canvas {
        width: 100%;
        height: 100%;
        image-rendering: pixelated; /* Keeps the pixel art look crisp */
        border-radius: 4px;
    }

    /* Instructions overlay */
    .overlay {
        position: absolute;
        bottom: -40px;
        color: var(--text-color);
        font-size: 0.9rem;
        opacity: 0.5;
        animation: pulse 2s infinite;
    }

    @keyframes pulse {
        0%, 100% { opacity: 0.3; }
        50% { opacity: 0.7; }
    }

    /* Interactive highlight */
    .canvas-container:hover {
        transform: scale(1.02);
    }
    
    /* While rotating, don't scale on hover to avoid conflict */
    .canvas-container.rotating {
        pointer-events: none; 
    }

</style>
</head> <body>
<h1>Interactive Physics Hourglass</h1>

<div class="canvas-container" id="container">
    <canvas id="simCanvas"></canvas>
</div>
<div class="overlay">Click to Rotate</div>

<script>
    const canvas = document.getElementById('simCanvas');
    const ctx = canvas.getContext('2d', { alpha: false });
    const container = document.getElementById('container');

    // Simulation Resolution (Low res for performance and retro style)
    const SIM_WIDTH = 120;
    const SIM_HEIGHT = 180;
    
    // Set internal canvas size
    canvas.width = SIM_WIDTH;
    canvas.height = SIM_HEIGHT;

    // Particle Types
    const EMPTY = 0;
    const WALL = 1;
    const SAND = 2;

    // State Arrays
    // grid: stores the type of particle
    // colorGrid: stores a random variant for visual texture
    let grid = new Int8Array(SIM_WIDTH * SIM_HEIGHT);
    let colorGrid = new Float32Array(SIM_WIDTH * SIM_HEIGHT);

    // Physics State
    let gravity = 1; // 1 = down, -1 = up
    let isRotating = false;
    let rotationAngle = 0;

    // Image Buffer for rendering
    const imageData = ctx.createImageData(SIM_WIDTH, SIM_HEIGHT);
    const buf = new Uint32Array(imageData.data.buffer);

    // Colors (ABGR format for Little Endian 32-bit write)
    // Background: #121212 -> FF121212
    const COL_BG = 0xFF121212; 
    // Wall: #444444 -> FF444444
    const COL_WALL = 0xFF505050;
    
    // Sand Base Colors (We will vary these)
    // Creating a palette of sand colors for variety
    function makeColor(r, g, b) {
        return (255 << 24) | (b << 16) | (g << 8) | r;
    }

    // Initialization
    function init() {
        // Fill grid
        for (let y = 0; y < SIM_HEIGHT; y++) {
            for (let x = 0; x < SIM_WIDTH; x++) {
                const i = y * SIM_WIDTH + x;
                
                // Define Hourglass Shape
                // Normalize coords -1 to 1
                let ny = (y - SIM_HEIGHT / 2) / (SIM_HEIGHT / 2);
                let nx = (x - SIM_WIDTH / 2) / (SIM_WIDTH / 2);
                
                // Curve function: x = width at y
                // Wide at top/bottom, narrow at center
                // w = 0.1 + 0.9 * ny^2
                let maxW = 0.06 + 0.90 * (ny * ny);
                
                // Add walls
                if (Math.abs(nx) > maxW) {
                    grid[i] = WALL;
                } else {
                    grid[i] = EMPTY;
                    
                    // Fill bottom half with sand initially
                    // y > SIM_HEIGHT / 2 + margin
                    if (y > SIM_HEIGHT * 0.55 && y < SIM_HEIGHT - 5) {
                        // Random fill density for natural look start
                        if (Math.random() > 0.1) {
                            grid[i] = SAND;
                            colorGrid[i] = Math.random(); // Store random value for texture
                        }
                    }
                }

                // Cap top and bottom
                if (y === 0 || y === SIM_HEIGHT - 1) grid[i] = WALL;
            }
        }
        
        // Explicitly open the neck just in case calculation was too tight
        const cy = Math.floor(SIM_HEIGHT / 2);
        const cx = Math.floor(SIM_WIDTH / 2);
        for(let y = cy - 2; y <= cy + 2; y++) {
            for(let x = cx - 2; x <= cx + 2; x++) {
                grid[y * SIM_WIDTH + x] = (x === cx - 2 || x === cx + 2) ? WALL : EMPTY;
            }
        }
    }

    function update() {
        if (isRotating) return; // Pause physics during rotation animation

        // We iterate either bottom-up or top-down depending on gravity to prevent
        // particles from "teleporting" through the whole grid in one frame.
        
        let startY, endY, stepY;
        
        if (gravity === 1) {
            startY = SIM_HEIGHT - 2; // Start from one row above bottom
            endY = 0;
            stepY = -1;
        } else {
            startY = 1; // Start from one row below top
            endY = SIM_HEIGHT - 1;
            stepY = 1;
        }

        // To prevent bias (always falling left or right first), we can alternate X direction
        // or just use Math.random(). Alternating frames is cheaper.
        const randDir = Math.random() > 0.5 ? 1 : -1;

        for (let y = startY; y !== endY + stepY; y += stepY) {
            for (let x = 0; x < SIM_WIDTH; x++) {
                // Pick x based on even/odd row to distribute checks or just iterate linear
                // Standard linear is fine if we randomize fall direction check
                
                const i = y * SIM_WIDTH + x;
                
                if (grid[i] === SAND) {
                    const down = i + (SIM_WIDTH * gravity);
                    
                    // Check directly down
                    if (grid[down] === EMPTY) {
                        grid[down] = SAND;
                        grid[i] = EMPTY;
                        colorGrid[down] = colorGrid[i];
                    } 
                    // Pile Up Logic (Granular Physics)
                    // Check diagonals
                    else {
                        const dir = Math.random() > 0.5 ? 1 : -1;
                        const downA = down + dir;
                        const downB = down - dir;
                        
                        // Ensure we don't wrap around the grid (boundary check x)
                        const xA = x + dir;
                        const xB = x - dir;
                        
                        let moved = false;

                        // Try direction A
                        if (xA >= 0 && xA < SIM_WIDTH && grid[downA] === EMPTY) {
                            grid[downA] = SAND;
                            grid[i] = EMPTY;
                            colorGrid[downA] = colorGrid[i];
                            moved = true;
                        } 
                        // Try direction B
                        else if (xB >= 0 && xB < SIM_WIDTH && grid[downB] === EMPTY) {
                            grid[downB] = SAND;
                            grid[i] = EMPTY;
                            colorGrid[downB] = colorGrid[i];
                            moved = true;
                        }
                    }
                }
            }
        }
    }

    function draw() {
        // Clear buffer
        buf.fill(COL_BG);

        for (let i = 0; i < grid.length; i++) {
            const type = grid[i];
            if (type === WALL) {
                buf[i] = COL_WALL;
                // Add simple shading to wall
                // check if right pixel is empty for highlight
                if ((i+1) < grid.length && grid[i+1] === EMPTY) {
                     buf[i] = 0xFF888888;
                }
            } else if (type === SAND) {
                // Generate sand color based on stored variant
                // Base Gold: R=240, G=190, B=90
                // Darker Gold: R=180, G=130, B=40
                const variant = colorGrid[i];
                const r = 200 + (variant * 55);
                const g = 150 + (variant * 50);
                const b = 50 + (variant * 60);
                buf[i] = makeColor(r, g, b);
            }
        }

        ctx.putImageData(imageData, 0, 0);
    }

    function loop() {
        update();
        draw();
        requestAnimationFrame(loop);
    }

    // Input Handling
    container.addEventListener('click', () => {
        if (isRotating) return;
        
        isRotating = true;
        container.classList.add('rotating');
        rotationAngle += 180;
        
        // Apply visual rotation
        container.style.transform = `rotate(${rotationAngle}deg)`;

        // Wait for CSS transition (0.8s) to finish before flipping physics
        setTimeout(() => {
            gravity *= -1;
            isRotating = false;
            container.classList.remove('rotating');
            // Note: We do NOT flip the grid data array. 
            // When visual is 180deg, the top of the screen is the bottom of the grid array.
            // Physics Gravity -1 means falling towards index 0 (Grid Top).
            // Visually, falling towards Grid Top (index 0) when rotated 180deg means falling DOWN.
            // So simply flipping the gravity scalar works perfectly.
        }, 800);
    });

    // Start
    init();
    loop();

</script>
</body> </html>_
Updated files
v1