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 {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        overflow: hidden;
        cursor: pointer;
        user-select: none;
    }

    #canvas {
        background: rgba(255, 255, 255, 0.05);
        border-radius: 20px;
        box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
        transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
    }

    #canvas.rotating {
        transform: rotate(180deg);
    }

    .info {
        position: absolute;
        bottom: 30px;
        left: 50%;
        transform: translateX(-50%);
        color: white;
        text-align: center;
        font-size: 18px;
        font-weight: 500;
        text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        pointer-events: none;
    }

    .controls {
        position: absolute;
        top: 30px;
        right: 30px;
        display: flex;
        gap: 15px;
    }

    button {
        background: rgba(255, 255, 255, 0.2);
        border: 2px solid rgba(255, 255, 255, 0.3);
        color: white;
        padding: 12px 24px;
        border-radius: 12px;
        font-size: 16px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        backdrop-filter: blur(10px);
    }

    button:hover {
        background: rgba(255, 255, 255, 0.3);
        transform: translateY(-2px);
        box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
    }

    button:active {
        transform: translateY(0);
    }
</style>
</head> <body> <canvas id="canvas"></canvas> <div class="info">Click to flip the hourglass</div> <div class="controls"> <button onclick="resetHourglass()">Reset</button> <button onclick="togglePause()">Pause</button> </div>
<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    
    // Set canvas size
    const width = Math.min(window.innerWidth * 0.8, 800);
    const height = Math.min(window.innerHeight * 0.8, 900);
    canvas.width = width;
    canvas.height = height;

    // Hourglass parameters
    const centerX = width / 2;
    const centerY = height / 2;
    const bulbRadius = width * 0.3;
    const neckWidth = width * 0.03;
    const neckHeight = height * 0.05;
    const glassThickness = 3;

    // Physics parameters
    const gravity = 0.3;
    const friction = 0.98;
    const restitution = 0.3;
    const particleRadius = 2;
    const maxParticles = 2000;
    const flowRate = 3;

    // State
    let particles = [];
    let isRotating = false;
    let isPaused = false;
    let rotation = 0;
    let targetRotation = 0;
    let sandPileTop = 0;
    let sandPileBottom = 0;

    class Particle {
        constructor(x, y) {
            this.x = x;
            this.y = y;
            this.vx = (Math.random() - 0.5) * 0.5;
            this.vy = 0;
            this.radius = particleRadius + Math.random() * 0.5;
            this.color = `hsl(${35 + Math.random() * 10}, ${70 + Math.random() * 20}%, ${50 + Math.random() * 20}%)`;
            this.settled = false;
        }

        update() {
            if (this.settled) return;

            // Apply gravity
            this.vy += gravity * (rotation % Math.PI < Math.PI ? 1 : -1);
            
            // Apply friction
            this.vx *= friction;
            this.vy *= friction;

            // Update position
            this.x += this.vx;
            this.y += this.vy;

            // Check hourglass boundaries
            this.checkBoundaries();

            // Check collision with other particles
            this.checkCollisions();

            // Check if particle has settled
            if (Math.abs(this.vx) < 0.01 && Math.abs(this.vy) < 0.01) {
                this.settled = true;
            }
        }

        checkBoundaries() {
            const dx = this.x - centerX;
            const dy = this.y - centerY;
            const dist = Math.sqrt(dx * dx + dy * dy);

            // Transform coordinates based on rotation
            const rot = rotation;
            const localX = dx * Math.cos(-rot) - dy * Math.sin(-rot);
            const localY = dx * Math.sin(-rot) + dy * Math.cos(-rot);

            // Check if in neck region
            if (Math.abs(localY) < neckHeight / 2) {
                // Neck constraints
                if (Math.abs(localX) > neckWidth / 2 - this.radius) {
                    // Bounce off neck walls
                    localX > 0 ? this.x = centerX + (neckWidth / 2 - this.radius) * Math.cos(rot) : 
                                 this.x = centerX - (neckWidth / 2 - this.radius) * Math.cos(rot);
                    this.vx *= -restitution;
                }
            } else {
                // Bulb constraints (hourglass shape)
                const bulbY = localY > 0 ? height / 2 - bulbRadius : -height / 2 + bulbRadius;
                const maxRadius = bulbRadius * (1 - Math.abs(localY - (localY > 0 ? height / 2 - bulbRadius : -height / 2 + bulbRadius)) / bulbRadius);
                
                if (Math.abs(localX) > maxRadius - this.radius) {
                    // Bounce off bulb walls
                    const angle = Math.atan2(localY, localX);
                    this.x = centerX + (maxRadius - this.radius) * Math.cos(angle + rot);
                    this.y = centerY + (maxRadius - this.radius) * Math.sin(angle + rot);
                    
                    // Reflect velocity
                    const normal = { x: Math.cos(angle + rot), y: Math.sin(angle + rot) };
                    const dot = this.vx * normal.x + this.vy * normal.y;
                    this.vx -= 2 * dot * normal.x * restitution;
                    this.vy -= 2 * dot * normal.y * restitution;
                }

                // Bottom of bulb
                if (localY > 0 && localY > height / 2 - bulbRadius * 0.9) {
                    this.y = centerY + (height / 2 - bulbRadius * 0.9 - this.radius) * Math.sin(rot) + localX * Math.sin(rot);
                    this.vy *= -restitution;
                    this.settled = true;
                }
                if (localY < 0 && localY < -height / 2 + bulbRadius * 0.9) {
                    this.y = centerY + (-height / 2 + bulbRadius * 0.9 + this.radius) * Math.sin(rot) + localX * Math.sin(rot);
                    this.vy *= -restitution;
                    this.settled = true;
                }
            }
        }

        checkCollisions() {
            for (let other of particles) {
                if (other === this) continue;
                
                const dx = other.x - this.x;
                const dy = other.y - this.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                const minDist = this.radius + other.radius;

                if (dist < minDist && dist > 0) {
                    // Collision detected
                    const overlap = minDist - dist;
                    const separationX = (dx / dist) * overlap * 0.5;
                    const separationY = (dy / dist) * overlap * 0.5;

                    this.x -= separationX;
                    this.y -= separationY;
                    other.x += separationX;
                    other.y += separationY;

                    // Exchange velocities (simplified)
                    const tempVx = this.vx;
                    const tempVy = this.vy;
                    this.vx = other.vx * restitution;
                    this.vy = other.vy * restitution;
                    other.vx = tempVx * restitution;
                    other.vy = tempVy * restitution;

                    this.settled = false;
                    other.settled = false;
                }
            }
        }

        draw() {
            ctx.save();
            ctx.globalAlpha = 0.9;
            ctx.fillStyle = this.color;
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
            ctx.fill();
            
            // Add subtle shadow
            ctx.globalAlpha = 0.3;
            ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
            ctx.beginPath();
            ctx.arc(this.x + 1, this.y + 1, this.radius, 0, Math.PI * 2);
            ctx.fill();
            ctx.restore();
        }
    }

    function drawHourglass() {
        ctx.save();
        ctx.translate(centerX, centerY);
        ctx.rotate(rotation);
        ctx.translate(-centerX, -centerY);

        // Draw glass outline
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
        ctx.lineWidth = glassThickness;
        ctx.shadowBlur = 20;
        ctx.shadowColor = 'rgba(255, 255, 255, 0.5)';

        // Top bulb
        ctx.beginPath();
        ctx.arc(centerX, centerY - height/2 + bulbRadius, bulbRadius, Math.PI, 0, false);
        
        // Neck
        ctx.lineTo(centerX + neckWidth/2, centerY - neckHeight/2);
        ctx.lineTo(centerX + neckWidth/2, centerY + neckHeight/2);
        
        // Bottom bulb
        ctx.arc(centerX, centerY + height/2 - bulbRadius, bulbRadius, 0, Math.PI, false);
        
        // Close neck
        ctx.lineTo(centerX - neckWidth/2, centerY - neckHeight/2);
        ctx.lineTo(centerX - neckWidth/2, centerY + neckHeight/2);
        
        ctx.closePath();
        ctx.stroke();

        // Add glass shine effect
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(centerX - bulbRadius/3, centerY - height/2 + bulbRadius/2, bulbRadius/4, -Math.PI/2, Math.PI/2);
        ctx.stroke();

        ctx.restore();
    }

    function generateParticles() {
        if (particles.length >= maxParticles) return;
        
        for (let i = 0; i < flowRate; i++) {
            const angle = Math.random() * Math.PI * 2;
            const r = Math.random() * (bulbRadius - particleRadius * 10);
            const x = centerX + Math.cos(angle) * r;
            const y = centerY - height/2 + bulbRadius * 0.3 + Math.random() * bulbRadius * 0.4;
            
            // Check if position is inside hourglass
            const dx = x - centerX;
            const dy = y - centerY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            
            if (dist < bulbRadius - particleRadius * 5) {
                particles.push(new Particle(x, y));
            }
        }
    }

    function animate() {
        if (!isPaused) {
            ctx.clearRect(0, 0, width, height);

            // Smooth rotation
            if (Math.abs(rotation - targetRotation) > 0.01) {
                rotation += (targetRotation - rotation) * 0.1;
            }

            drawHourglass();

            // Generate new particles
            if (particles.length < maxParticles && Math.random() < 0.8) {
                generateParticles();
            }

            // Update and draw particles
            for (let i = particles.length - 1; i >= 0; i--) {
                const particle = particles[i];
                particle.update();
                particle.draw();

                // Remove particles that fall out of bounds
                if (particle.y > height + 100 || particle.y < -100) {
                    particles.splice(i, 1);
                }
            }
        }

        requestAnimationFrame(animate);
    }

    function resetHourglass() {
        particles = [];
        rotation = 0;
        targetRotation = 0;
        canvas.classList.remove('rotating');
    }

    function togglePause() {
        isPaused = !isPaused;
        event.target.textContent = isPaused ? 'Resume' : 'Pause';
    }

    // Event listeners
    canvas.addEventListener('click', (e) => {
        if (e.target === canvas) {
            targetRotation += Math.PI;
            canvas.classList.toggle('rotating');
            
            // Reset settled state when flipping
            particles.forEach(p => p.settled = false);
        }
    });

    // Start animation
    animate();
</script>
</body> </html>
Updated files
v1