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 Simulation</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
        color: #fff;
        min-height: 100vh;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 2rem;
        overflow: hidden;
    }
    
    .container {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 2rem;
        width: 100%;
        max-width: 1200px;
    }
    
    h1 {
        font-size: clamp(2rem, 5vw, 3.5rem);
        text-align: center;
        margin-bottom: 1rem;
        background: linear-gradient(45deg, #ffd166, #ef476f);
        -webkit-background-clip: text;
        background-clip: text;
        color: transparent;
        text-shadow: 0 2px 10px rgba(0,0,0,0.3);
    }
    
    .instructions {
        text-align: center;
        font-size: clamp(1rem, 2vw, 1.2rem);
        color: #a9b7c6;
        max-width: 600px;
        line-height: 1.6;
    }
    
    .simulation-area {
        position: relative;
        width: min(90vw, 600px);
        height: min(80vh, 700px);
        background: rgba(255, 255, 255, 0.05);
        border-radius: 20px;
        box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
        overflow: hidden;
        border: 1px solid rgba(255, 255, 255, 0.1);
    }
    
    canvas {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        cursor: pointer;
    }
    
    .stats {
        display: flex;
        gap: 2rem;
        font-size: 1.1rem;
        color: #ffd166;
        background: rgba(0, 0, 0, 0.3);
        padding: 1rem 2rem;
        border-radius: 10px;
        border: 1px solid rgba(255, 255, 255, 0.1);
    }
    
    .stat-item {
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    
    .stat-value {
        font-weight: bold;
        font-size: 1.3rem;
    }
    
    .stat-label {
        font-size: 0.9rem;
        color: #a9b7c6;
    }
    
    @media (max-width: 768px) {
        body {
            padding: 1rem;
        }
        
        .simulation-area {
            width: 95vw;
            height: 70vh;
        }
        
        .stats {
            flex-direction: column;
            gap: 1rem;
            padding: 1rem;
        }
    }
</style>
</head> <body> <div class="container"> <h1>Physics Hourglass</h1> <p class="instructions">Click the hourglass to flip it. Watch the sand particles flow realistically and form natural cone shapes.</p>
    <div class="simulation-area">
        <canvas id="hourglassCanvas"></canvas>
    </div>
    
    <div class="stats">
        <div class="stat-item">
            <span class="stat-value" id="particleCount">0</span>
            <span class="stat-label">Particles</span>
        </div>
        <div class="stat-item">
            <span class="stat-value" id="fpsCounter">0</span>
            <span class="stat-label">FPS</span>
        </div>
        <div class="stat-item">
            <span class="stat-value" id="stateDisplay">Ready</span>
            <span class="stat-label">Status</span>
        </div>
    </div>
</div>

<script>
    class HourglassSimulation {
        constructor() {
            this.canvas = document.getElementById('hourglassCanvas');
            this.ctx = this.canvas.getContext('2d');
            this.particles = [];
            this.isFlipping = false;
            this.flipProgress = 0;
            this.rotationAngle = 0;
            this.lastTime = 0;
            this.fps = 0;
            this.frameCount = 0;
            this.lastFpsUpdate = 0;
            
            this.particleCountElement = document.getElementById('particleCount');
            this.fpsCounterElement = document.getElementById('fpsCounter');
            this.stateDisplayElement = document.getElementById('stateDisplay');
            
            this.init();
            this.setupEventListeners();
            this.animate();
        }
        
        init() {
            this.resizeCanvas();
            this.createParticles();
        }
        
        resizeCanvas() {
            const container = this.canvas.parentElement;
            this.canvas.width = container.clientWidth;
            this.canvas.height = container.clientHeight;
        }
        
        createParticles() {
            this.particles = [];
            const particleCount = 800;
            const centerX = this.canvas.width / 2;
            const topChamberHeight = this.canvas.height * 0.4;
            
            for (let i = 0; i < particleCount; i++) {
                const radius = Math.random() * 3 + 2;
                const angle = Math.random() * Math.PI * 2;
                const distance = Math.random() * (this.canvas.width * 0.2);
                
                this.particles.push({
                    x: centerX + Math.cos(angle) * distance,
                    y: topChamberHeight * 0.3 + Math.random() * topChamberHeight * 0.4,
                    radius: radius,
                    vx: 0,
                    vy: 0,
                    color: this.getSandColor(),
                    inTop: true,
                    settled: false
                });
            }
        }
        
        getSandColor() {
            const colors = [
                '#d4a574', '#c19a6b', '#b08d5f', 
                '#e5b887', '#deab79', '#d6a066'
            ];
            return colors[Math.floor(Math.random() * colors.length)];
        }
        
        setupEventListeners() {
            this.canvas.addEventListener('click', () => this.flip());
            window.addEventListener('resize', () => this.resizeCanvas());
        }
        
        flip() {
            if (!this.isFlipping) {
                this.isFlipping = true;
                this.flipProgress = 0;
                this.stateDisplayElement.textContent = 'Flipping...';
            }
        }
        
        updateParticles(deltaTime) {
            const gravity = 0.2;
            const friction = 0.98;
            const centerX = this.canvas.width / 2;
            const neckWidth = this.canvas.width * 0.08;
            const topChamberHeight = this.canvas.height * 0.4;
            const bottomChamberHeight = this.canvas.height * 0.4;
            const neckY = this.canvas.height * 0.4;
            
            let activeParticles = 0;
            
            for (let particle of this.particles) {
                if (particle.settled) {
                    activeParticles++;
                    continue;
                }
                
                // Apply gravity (direction depends on orientation)
                const gravityDirection = this.rotationAngle < Math.PI ? 1 : -1;
                particle.vy += gravity * gravityDirection * deltaTime;
                
                // Update position
                particle.x += particle.vx * deltaTime;
                particle.y += particle.vy * deltaTime;
                
                // Boundary constraints for top chamber
                if (particle.inTop && this.rotationAngle < Math.PI) {
                    const topRadius = this.canvas.width * 0.3;
                    const distFromCenter = Math.abs(particle.x - centerX);
                    
                    // Chamber walls
                    if (distFromCenter + particle.radius > topRadius) {
                        particle.x = centerX + Math.sign(particle.x - centerX) * (topRadius - particle.radius);
                        particle.vx *= -0.5;
                    }
                    
                    // Chamber bottom (neck entrance)
                    if (particle.y > neckY - particle.radius) {
                        const neckDist = Math.abs(particle.x - centerX);
                        if (neckDist < neckWidth / 2) {
                            // Pass through neck
                            particle.inTop = false;
                        } else {
                            particle.y = neckY - particle.radius;
                            particle.vy *= -0.3;
                        }
                    }
                    
                    // Chamber top
                    if (particle.y < particle.radius) {
                        particle.y = particle.radius;
                        particle.vy *= -0.3;
                    }
                }
                // Boundary constraints for bottom chamber
                else if (!particle.inTop && this.rotationAngle < Math.PI) {
                    const bottomRadius = this.canvas.width * 0.3;
                    const bottomChamberTop = neckY;
                    const bottomChamberBottom = this.canvas.height - this.canvas.height * 0.1;
                    const distFromCenter = Math.abs(particle.x - centerX);
                    
                    // Chamber walls
                    if (distFromCenter + particle.radius > bottomRadius) {
                        particle.x = centerX + Math.sign(particle.x - centerX) * (bottomRadius - particle.radius);
                        particle.vx *= -0.5;
                    }
                    
                    // Chamber bottom
                    if (particle.y > bottomChamberBottom - particle.radius) {
                        particle.y = bottomChamberBottom - particle.radius;
                        particle.vy *= -0.2;
                        particle.vx *= 0.8;
                        
                        // Check if particle should settle
                        if (Math.abs(particle.vy) < 0.5 && Math.abs(particle.vx) < 0.5) {
                            particle.settled = true;
                        }
                    }
                    
                    // Chamber top (neck)
                    if (particle.y < bottomChamberTop + particle.radius) {
                        const neckDist = Math.abs(particle.x - centerX);
                        if (neckDist < neckWidth / 2) {
                            // Can go back up through neck
                            particle.inTop = true;
                        } else {
                            particle.y = bottomChamberTop + particle.radius;
                            particle.vy *= -0.3;
                        }
                    }
                }
                
                // Apply friction
                particle.vx *= friction;
                particle.vy *= friction;
                
                // Particle interactions (simple collision)
                for (let other of this.particles) {
                    if (particle === other || other.settled) continue;
                    
                    const dx = other.x - particle.x;
                    const dy = other.y - particle.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    const minDistance = particle.radius + other.radius;
                    
                    if (distance < minDistance && distance > 0) {
                        // Simple elastic collision response
                        const angle = Math.atan2(dy, dx);
                        const targetX = particle.x + Math.cos(angle) * minDistance;
                        const targetY = particle.y + Math.sin(angle) * minDistance;
                        
                        const ax = (targetX - other.x) * 0.05;
                        const ay = (targetY - other.y) * 0.05;
                        
                        particle.vx -= ax;
                        particle.vy -= ay;
                        other.vx += ax;
                        other.vy += ay;
                    }
                }
                
                activeParticles++;
            }
            
            this.particleCountElement.textContent = activeParticles;
        }
        
        drawHourglass() {
            const ctx = this.ctx;
            const centerX = this.canvas.width / 2;
            const centerY = this.canvas.height / 2;
            const chamberWidth = this.canvas.width * 0.3;
            const neckWidth = this.canvas.width * 0.08;
            const chamberHeight = this.canvas.height * 0.4;
            
            ctx.save();
            ctx.translate(centerX, centerY);
            ctx.rotate(this.rotationAngle);
            ctx.translate(-centerX, -centerY);
            
            // Draw hourglass frame
            ctx.strokeStyle = '#ffd166';
            ctx.lineWidth = 3;
            ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
            
            // Top chamber
            ctx.beginPath();
            ctx.moveTo(centerX - chamberWidth, centerY - chamberHeight);
            ctx.lineTo(centerX + chamberWidth, centerY - chamberHeight);
            ctx.lineTo(centerX + neckWidth, centerY);
            ctx.lineTo(centerX - neckWidth, centerY);
            ctx.closePath();
            ctx.stroke();
            ctx.fill();
            
            // Bottom chamber
            ctx.beginPath();
            ctx.moveTo(centerX - neckWidth, centerY);
            ctx.lineTo(centerX + neckWidth, centerY);
            ctx.lineTo(centerX + chamberWidth, centerY + chamberHeight);
            ctx.lineTo(centerX - chamberWidth, centerY + chamberHeight);
            ctx.closePath();
            ctx.stroke();
            ctx.fill();
            
            // Neck connector
            ctx.beginPath();
            ctx.moveTo(centerX - neckWidth, centerY);
            ctx.lineTo(centerX + neckWidth, centerY);
            ctx.stroke();
            
            ctx.restore();
        }
        
        drawParticles() {
            const ctx = this.ctx;
            const centerX = this.canvas.width / 2;
            const centerY = this.canvas.height / 2;
            
            ctx.save();
            ctx.translate(centerX, centerY);
            ctx.rotate(this.rotationAngle);
            ctx.translate(-centerX, -centerY);
            
            for (let particle of this.particles) {
                ctx.beginPath();
                ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
                ctx.fillStyle = particle.color;
                ctx.fill();
                
                // Add subtle shadow for depth
                ctx.beginPath();
                ctx.arc(particle.x - particle.radius * 0.3, particle.y - particle.radius * 0.3, 
                        particle.radius * 0.6, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
                ctx.fill();
            }
            
            ctx.restore();
        }
        
        update(deltaTime) {
            // Handle flip animation
            if (this.isFlipping) {
                this.flipProgress += deltaTime * 0.003;
                if (this.flipProgress >= 1) {
                    this.flipProgress = 1;
                    this.isFlipping = false;
                    this.rotationAngle += Math.PI;
                    if (this.rotationAngle >= Math.PI * 2) {
                        this.rotationAngle = 0;
                    }
                    this.stateDisplayElement.textContent = 'Flowing';
                } else {
                    this.rotationAngle += Math.PI * deltaTime * 0.003;
                }
            }
            
            this.updateParticles(deltaTime);
        }
        
        render() {
            // Clear canvas with gradient background
            const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
            gradient.addColorStop(0, '#1a1a2e');
            gradient.addColorStop(1, '#16213e');
            
            this.ctx.fillStyle = gradient;
            this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
            
            this.drawHourglass();
            this.drawParticles();
        }
        
        animate(currentTime = 0) {
            const deltaTime = Math.min(currentTime - this.lastTime, 100) / 16;
            this.lastTime = currentTime;
            
            this.frameCount++;
            if (currentTime - this.lastFpsUpdate >= 1000) {
                this.fps = Math.round((this.frameCount * 1000) / (currentTime - this.lastFpsUpdate));
                this.fpsCounterElement.textContent = this.fps;
                this.frameCount = 0;
                this.lastFpsUpdate = currentTime;
            }
            
            this.update(deltaTime);
            this.render();
            
            requestAnimationFrame((time) => this.animate(time));
        }
    }
    
    // Initialize simulation when page loads
    window.addEventListener('load', () => {
        new HourglassSimulation();
    });
</script>
</body> </html>*
Updated files
v1