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> * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        overflow: hidden;
    }
    
    .container {
        position: relative;
        width: min(90vw, 600px);
        height: min(80vh, 800px);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }
    
    canvas {
        background: rgba(255, 255, 255, 0.05);
        border-radius: 20px;
        box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
        cursor: pointer;
        transition: transform 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
    }
    
    .controls {
        margin-top: 2rem;
        color: white;
        text-align: center;
    }
    
    h1 {
        color: #fff;
        margin-bottom: 1rem;
        font-weight: 300;
        font-size: clamp(1.5rem, 4vw, 2.5rem);
        text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
    }
    
    p {
        color: #a0a0c0;
        font-size: clamp(0.9rem, 2vw, 1.1rem);
        max-width: 500px;
        line-height: 1.6;
    }
    
    .hourglass-outline {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        pointer-events: none;
        z-index: -1;
    }
</style>
</head> <body> <div class="container"> <h1>Physics Hourglass</h1> <canvas id="hourglassCanvas"></canvas> <div class="controls"> <p>Click to flip the hourglass. Watch sand particles flow realistically and form natural piles.</p> </div> </div>
<script>
    class Particle {
        constructor(x, y, radius, color) {
            this.x = x;
            this.y = y;
            this.radius = radius;
            this.color = color;
            this.velocity = { x: 0, y: 0 };
            this.gravity = 0.2;
            this.friction = 0.95;
            this.mass = 1;
            this.stuck = false;
        }
        
        update(particles, neckX, neckY, neckWidth, neckHeight, isUpsideDown) {
            if (this.stuck) return;
            
            // Apply gravity (direction depends on orientation)
            this.velocity.y += isUpsideDown ? -this.gravity : this.gravity;
            
            // Apply friction
            this.velocity.x *= this.friction;
            this.velocity.y *= this.friction;
            
            // Update position
            this.x += this.velocity.x;
            this.y += this.velocity.y;
            
            // Hourglass boundary collision
            const hourglassWidth = 300;
            const hourglassHeight = 400;
            const centerX = canvas.width / 2;
            const centerY = canvas.height / 2;
            
            // Top chamber (when upright)
            if (!isUpsideDown && this.y < centerY - neckHeight/2) {
                const distFromCenter = Math.abs(this.x - centerX);
                const maxWidth = hourglassWidth * (1 - Math.pow((this.y - (centerY - hourglassHeight/2)) / (hourglassHeight/2 - neckHeight/2), 2));
                
                if (distFromCenter + this.radius > maxWidth/2) {
                    const angle = Math.atan2(this.y - (centerY - hourglassHeight/2), this.x - centerX);
                    this.x = centerX + Math.cos(angle) * (maxWidth/2 - this.radius);
                    this.velocity.x = -this.velocity.x * 0.8;
                }
            }
            // Bottom chamber (when upright)
            else if (!isUpsideDown && this.y > centerY + neckHeight/2) {
                const distFromCenter = Math.abs(this.x - centerX);
                const maxWidth = hourglassWidth * (1 - Math.pow((this.y - (centerY + hourglassHeight/2)) / (hourglassHeight/2 - neckHeight/2), 2));
                
                if (distFromCenter + this.radius > maxWidth/2) {
                    const angle = Math.atan2(this.y - (centerY + hourglassHeight/2), this.x - centerX);
                    this.x = centerX + Math.cos(angle) * (maxWidth/2 - this.radius);
                    this.velocity.x = -this.velocity.x * 0.8;
                }
            }
            // Top chamber (when upside down)
            else if (isUpsideDown && this.y > centerY + neckHeight/2) {
                const distFromCenter = Math.abs(this.x - centerX);
                const maxWidth = hourglassWidth * (1 - Math.pow((this.y - (centerY + hourglassHeight/2)) / (hourglassHeight/2 - neckHeight/2), 2));
                
                if (distFromCenter + this.radius > maxWidth/2) {
                    const angle = Math.atan2(this.y - (centerY + hourglassHeight/2), this.x - centerX);
                    this.x = centerX + Math.cos(angle) * (maxWidth/2 - this.radius);
                    this.velocity.x = -this.velocity.x * 0.8;
                }
            }
            // Bottom chamber (when upside down)
            else if (isUpsideDown && this.y < centerY - neckHeight/2) {
                const distFromCenter = Math.abs(this.x - centerX);
                const maxWidth = hourglassWidth * (1 - Math.pow((this.y - (centerY - hourglassHeight/2)) / (hourglassHeight/2 - neckHeight/2), 2));
                
                if (distFromCenter + this.radius > maxWidth/2) {
                    const angle = Math.atan2(this.y - (centerY - hourglassHeight/2), this.x - centerX);
                    this.x = centerX + Math.cos(angle) * (maxWidth/2 - this.radius);
                    this.velocity.x = -this.velocity.x * 0.8;
                }
            }
            
            // Neck passage
            const neckLeft = neckX - neckWidth/2;
            const neckRight = neckX + neckWidth/2;
            const neckTop = neckY - neckHeight/2;
            const neckBottom = neckY + neckHeight/2;
            
            if (this.x > neckLeft && this.x < neckRight && 
                this.y > neckTop && this.y < neckBottom) {
                // Allow particles to pass through neck
            } else if (Math.abs(this.x - neckX) < neckWidth/2 + this.radius && 
                      Math.abs(this.y - neckY) < neckHeight/2 + this.radius) {
                // Collision with neck edges
                if (this.x < neckX) {
                    this.x = neckLeft - this.radius;
                    this.velocity.x = Math.abs(this.velocity.x) * 0.5;
                } else {
                    this.x = neckRight + this.radius;
                    this.velocity.x = -Math.abs(this.velocity.x) * 0.5;
                }
            }
            
            // Particle-particle collisions
            for (let other of particles) {
                if (other === this || other.stuck) continue;
                
                const dx = other.x - this.x;
                const dy = other.y - this.y;
                const distance = Math.sqrt(dx * dx + dy * dy);
                const minDistance = this.radius + other.radius;
                
                if (distance < minDistance) {
                    // Collision response
                    const angle = Math.atan2(dy, dx);
                    const targetX = this.x + Math.cos(angle) * minDistance;
                    const targetY = this.y + Math.sin(angle) * minDistance;
                    
                    const ax = (targetX - other.x) * 0.05;
                    const ay = (targetY - other.y) * 0.05;
                    
                    this.velocity.x -= ax;
                    this.velocity.y -= ay;
                    other.velocity.x += ax;
                    other.velocity.y += ay;
                    
                    // Granular physics - particles stick when slow
                    if (Math.abs(this.velocity.x) < 0.1 && Math.abs(this.velocity.y) < 0.3) {
                        if (!isUpsideDown && this.y > centerY + 50) {
                            this.stuck = true;
                        } else if (isUpsideDown && this.y < centerY - 50) {
                            this.stuck = true;
                        }
                    }
                }
            }
        }
        
        draw(ctx) {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
            ctx.fillStyle = this.color;
            ctx.fill();
            ctx.closePath();
        }
    }

    class Hourglass {
        constructor() {
            this.canvas = document.getElementById('hourglassCanvas');
            this.ctx = this.canvas.getContext('2d');
            this.particles = [];
            this.isUpsideDown = false;
            this.isRotating = false;
            this.rotationProgress = 0;
            this.rotationDuration = 1500; // ms
            
            this.resize();
            window.addEventListener('resize', () => this.resize());
            this.canvas.addEventListener('click', () => this.flip());
            
            this.createParticles();
            this.animate();
        }
        
        resize() {
            this.canvas.width = Math.min(500, window.innerWidth * 0.8);
            this.canvas.height = Math.min(600, window.innerHeight * 0.7);
        }
        
        createParticles() {
            this.particles = [];
            const particleCount = 200;
            const centerX = this.canvas.width / 2;
            const centerY = this.canvas.height / 2;
            
            for (let i = 0; i < particleCount; i++) {
                const radius = 3 + Math.random() * 2;
                const angle = Math.random() * Math.PI * 2;
                const distance = Math.random() * 80;
                
                const x = centerX + Math.cos(angle) * distance;
                const y = centerY - 120 + Math.random() * 40;
                
                const color = `hsl(${40 + Math.random() * 10}, 70%, ${60 + Math.random() * 10}%)`;
                
                this.particles.push(new Particle(x, y, radius, color));
            }
        }
        
        flip() {
            if (!this.isRotating) {
                this.isRotating = true;
                this.rotationProgress = 0;
                this.isUpsideDown = !this.isUpsideDown;
                
                // Unstick all particles when flipping
                this.particles.forEach(particle => {
                    particle.stuck = false;
                    // Add some random velocity to break up piles
                    particle.velocity.x += (Math.random() - 0.5) * 2;
                    particle.velocity.y += (Math.random() - 0.5) * 2;
                });
            }
        }
        
        drawHourglass() {
            const ctx = this.ctx;
            const width = this.canvas.width;
            const height = this.canvas.height;
            const centerX = width / 2;
            const centerY = height / 2;
            const hourglassWidth = width * 0.6;
            const hourglassHeight = height * 0.7;
            const neckWidth = 15;
            const neckHeight = 20;
            
            ctx.save();
            
            // Apply rotation during flip animation
            if (this.isRotating) {
                const rotationAngle = (this.rotationProgress / this.rotationDuration) * Math.PI;
                ctx.translate(centerX, centerY);
                ctx.rotate(rotationAngle);
                ctx.translate(-centerX, -centerY);
            }
            
            // Draw hourglass outline
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
            ctx.lineWidth = 3;
            ctx.beginPath();
            
            // Top curve
            ctx.moveTo(centerX - hourglassWidth/2, centerY - hourglassHeight/2);
            ctx.bezierCurveTo(
                centerX - hourglassWidth/2, centerY - hourglassHeight/4,
                centerX - neckWidth/2, centerY - neckHeight/2,
                centerX - neckWidth/2, centerY - neckHeight/2
            );
            ctx.bezierCurveTo(
                centerX - neckWidth/2, centerY - neckHeight/2,
                centerX - neckWidth/2, centerY + neckHeight/2,
                centerX - hourglassWidth/2, centerY + hourglassHeight/2
            );
            
            // Bottom curve
            ctx.moveTo(centerX + hourglassWidth/2, centerY - hourglassHeight/2);
            ctx.bezierCurveTo(
                centerX + hourglassWidth/2, centerY - hourglassHeight/4,
                centerX + neckWidth/2, centerY - neckHeight/2,
                centerX + neckWidth/2, centerY - neckHeight/2
            );
            ctx.bezierCurveTo(
                centerX + neckWidth/2, centerY - neckHeight/2,
                centerX + neckWidth/2, centerY + neckHeight/2,
                centerX + hourglassWidth/2, centerY + hourglassHeight/2
            );
            
            ctx.stroke();
            
            ctx.restore();
            
            return {
                neckX: centerX,
                neckY: centerY,
                neckWidth,
                neckHeight
            };
        }
        
        animate() {
            requestAnimationFrame(() => this.animate());
            
            // Update rotation progress
            if (this.isRotating) {
                this.rotationProgress += 16; // ~60fps
                if (this.rotationProgress >= this.rotationDuration) {
                    this.isRotating = false;
                }
            }
            
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            
            const neck = this.drawHourglass();
            
            // Update and draw particles
            for (let particle of this.particles) {
                particle.update(this.particles, neck.neckX, neck.neckY, neck.neckWidth, neck.neckHeight, this.isUpsideDown);
                particle.draw(this.ctx);
            }
        }
    }

    // Initialize hourglass when page loads
    window.addEventListener('load', () => {
        new Hourglass();
    });
</script>
</body> </html>*
Updated files
v1