JIT
Grid - Untitled
<!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 {
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        overflow: hidden;
    }

    #canvas {
        cursor: pointer;
        filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.3));
        transition: transform 0.3s ease;
    }

    #canvas:hover {
        transform: scale(1.02);
    }

    .info {
        position: absolute;
        top: 20px;
        left: 50%;
        transform: translateX(-50%);
        color: white;
        text-align: center;
        background: rgba(0, 0, 0, 0.3);
        padding: 15px 30px;
        border-radius: 10px;
        backdrop-filter: blur(10px);
    }

    .info h1 {
        font-size: 24px;
        margin-bottom: 5px;
        text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
    }

    .info p {
        font-size: 14px;
        opacity: 0.9;
    }
</style>
</head> <body> <div class="info"> <h1>Physics Hourglass</h1> <p>Click to flip the hourglass</p> </div> <canvas id="canvas"></canvas>
<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');

    // Responsive canvas sizing
    function resizeCanvas() {
        const maxWidth = window.innerWidth * 0.8;
        const maxHeight = window.innerHeight * 0.8;
        const size = Math.min(maxWidth, maxHeight, 600);
        canvas.width = size;
        canvas.height = size * 1.4;
    }
    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);

    // Hourglass dimensions
    const centerX = canvas.width / 2;
    const centerY = canvas.height / 2;
    const topWidth = canvas.width * 0.35;
    const bottomWidth = canvas.width * 0.35;
    const neckWidth = canvas.width * 0.025;
    const halfHeight = canvas.height * 0.35;
    const neckHeight = canvas.height * 0.05;

    // Particle class
    class Particle {
        constructor(x, y) {
            this.x = x;
            this.y = y;
            this.vx = (Math.random() - 0.5) * 0.5;
            this.vy = 0;
            this.radius = 2 + Math.random() * 1.5;
            this.color = `hsl(${38 + Math.random() * 10}, ${70 + Math.random() * 20}%, ${50 + Math.random() * 10}%)`;
            this.settled = false;
            this.friction = 0.98;
            this.restitution = 0.2;
        }

        update() {
            if (this.settled) {
                this.vx *= 0.95;
                this.vy *= 0.95;
                if (Math.abs(this.vx) < 0.01 && Math.abs(this.vy) < 0.01) {
                    this.vx = 0;
                    this.vy = 0;
                }
            }

            // Apply gravity
            this.vy += gravity * (isFlipped ? -1 : 1);
            
            // Apply velocity
            this.x += this.vx;
            this.y += this.vy;

            // Check hourglass boundaries
            this.checkBoundaries();

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

        checkBoundaries() {
            const relY = this.y - centerY;
            const absRelY = Math.abs(relY);
            
            // Determine which section we're in
            if (absRelY < halfHeight + neckHeight / 2) {
                // In the neck or transition area
                if (absRelY < neckHeight / 2) {
                    // In the neck
                    if (Math.abs(this.x - centerX) > neckWidth / 2 - this.radius) {
                        this.x = centerX + (this.x > centerX ? 1 : -1) * (neckWidth / 2 - this.radius);
                        this.vx *= -this.restitution;
                    }
                } else {
                    // In transition cone
                    const progress = (absRelY - neckHeight / 2) / halfHeight;
                    const width = neckWidth + (topWidth - neckWidth) * progress;
                    const halfWidth = width / 2;
                    if (Math.abs(this.x - centerX) > halfWidth - this.radius) {
                        const angle = Math.atan2(this.y - (isFlipped ? centerY - halfHeight : centerY + halfHeight), 
                                                this.x - centerX);
                        this.x = centerX + Math.cos(angle) * (halfWidth - this.radius);
                        this.vx *= -this.restitution;
                        this.vy *= this.friction;
                    }
                }
            } else {
                // In top or bottom bulb
                const width = topWidth;
                const halfWidth = width / 2;
                const bulbTop = isFlipped ? centerY - halfHeight - neckHeight : centerY + halfHeight + neckHeight;
                const bulbBottom = isFlipped ? centerY - canvas.height / 2 + 50 : centerY + canvas.height / 2 - 50;
                
                if (Math.abs(this.x - centerX) > halfWidth - this.radius) {
                    this.x = centerX + (this.x > centerX ? 1 : -1) * (halfWidth - this.radius);
                    this.vx *= -this.restitution;
                }
                
                if (!isFlipped && this.y > bulbBottom - this.radius) {
                    this.y = bulbBottom - this.radius;
                    this.vy *= -this.restitution;
                    this.vx *= this.friction;
                    if (Math.abs(this.vy) < 0.5) {
                        this.settled = true;
                    }
                } else if (isFlipped && this.y < bulbTop + this.radius) {
                    this.y = bulbTop + this.radius;
                    this.vy *= -this.restitution;
                    this.vx *= this.friction;
                    if (Math.abs(this.vy) < 0.5) {
                        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 distance = Math.sqrt(dx * dx + dy * dy);
                const minDistance = this.radius + other.radius;
                
                if (distance < minDistance) {
                    // Collision detected
                    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.vx -= ax;
                    this.vy -= ay;
                    other.vx += ax;
                    other.vy += ay;
                    
                    this.vx *= this.friction;
                    this.vy *= this.friction;
                    
                    if (Math.abs(this.vy) < 0.5 && !isFlipped && this.y > centerY) {
                        this.settled = true;
                    } else if (Math.abs(this.vy) < 0.5 && isFlipped && this.y < centerY) {
                        this.settled = true;
                    }
                }
            }
        }

        draw() {
            ctx.save();
            ctx.fillStyle = this.color;
            ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
            ctx.shadowBlur = 2;
            ctx.shadowOffsetY = 1;
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
            ctx.fill();
            ctx.restore();
        }
    }

    // Simulation variables
    let particles = [];
    let isFlipped = false;
    let rotation = 0;
    let targetRotation = 0;
    let gravity = 0.3;
    let particleSpawnTimer = 0;

    // Initialize particles
    function initParticles() {
        particles = [];
        const numParticles = 800;
        
        for (let i = 0; i < numParticles; i++) {
            const angle = Math.random() * Math.PI * 2;
            const radius = Math.random() * (topWidth / 2 - 10);
            const x = centerX + Math.cos(angle) * radius;
            const y = centerY - halfHeight - neckHeight / 2 + Math.random() * 50;
            particles.push(new Particle(x, y));
        }
    }

    // Draw hourglass
    function drawHourglass() {
        ctx.save();
        ctx.translate(centerX, centerY);
        ctx.rotate(rotation);
        ctx.translate(-centerX, -centerY);
        
        // Glass outline
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
        ctx.lineWidth = 3;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        
        ctx.beginPath();
        // Top bulb left
        ctx.moveTo(centerX - topWidth / 2, centerY - canvas.height / 2 + 50);
        ctx.lineTo(centerX - topWidth / 2, centerY - halfHeight - neckHeight / 2);
        // Left cone to neck
        ctx.lineTo(centerX - neckWidth / 2, centerY - neckHeight / 2);
        // Right cone from neck
        ctx.lineTo(centerX - neckWidth / 2, centerY + neckHeight / 2);
        // Bottom bulb left
        ctx.lineTo(centerX - bottomWidth / 2, centerY + halfHeight + neckHeight / 2);
        ctx.lineTo(centerX - bottomWidth / 2, centerY + canvas.height / 2 - 50);
        
        ctx.stroke();
        
        ctx.beginPath();
        // Top bulb right
        ctx.moveTo(centerX + topWidth / 2, centerY - canvas.height / 2 + 50);
        ctx.lineTo(centerX + topWidth / 2, centerY - halfHeight - neckHeight / 2);
        // Right cone to neck
        ctx.lineTo(centerX + neckWidth / 2, centerY - neckHeight / 2);
        // Right cone from neck
        ctx.lineTo(centerX + neckWidth / 2, centerY + neckHeight / 2);
        // Bottom bulb right
        ctx.lineTo(centerX + bottomWidth / 2, centerY + halfHeight + neckHeight / 2);
        ctx.lineTo(centerX + bottomWidth / 2, centerY + canvas.height / 2 - 50);
        
        ctx.stroke();
        
        // Top and bottom caps
        ctx.beginPath();
        ctx.moveTo(centerX - topWidth / 2, centerY - canvas.height / 2 + 50);
        ctx.lineTo(centerX + topWidth / 2, centerY - canvas.height / 2 + 50);
        ctx.stroke();
        
        ctx.beginPath();
        ctx.moveTo(centerX - bottomWidth / 2, centerY + canvas.height / 2 - 50);
        ctx.lineTo(centerX + bottomWidth / 2, centerY + canvas.height / 2 - 50);
        ctx.stroke();
        
        ctx.restore();
    }

    // Animation loop
    function animate() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        // Smooth rotation
        if (Math.abs(rotation - targetRotation) > 0.01) {
            rotation += (targetRotation - rotation) * 0.1;
        }
        
        // Draw hourglass
        drawHourglass();
        
        // Update and draw particles
        ctx.save();
        ctx.translate(centerX, centerY);
        ctx.rotate(rotation);
        ctx.translate(-centerX, -centerY);
        
        particles.forEach(particle => {
            particle.update();
            particle.draw();
        });
        
        ctx.restore();
        
        requestAnimationFrame(animate);
    }

    // Click handler
    canvas.addEventListener('click', () => {
        isFlipped = !isFlipped;
        targetRotation += Math.PI;
        
        // Unsettle particles when flipped
        particles.forEach(particle => {
            particle.settled = false;
        });
    });

    // Initialize and start
    initParticles();
    animate();
</script>
</body> </html> ```

This physics-based hourglass simulation features:

Physics Implementation:

  • Realistic gravity simulation affecting each sand particle
  • Collision detection between particles for natural stacking
  • Granular physics with friction and restitution coefficients
  • Particles settle when velocity drops below threshold
  • Flow restriction through the narrow neck

Visual Features:

  • Glass hourglass outline with transparency
  • Sand particles with varied golden colors and subtle shadows
  • Smooth 180-degree rotation animation on click
  • Responsive design that scales to viewport

Interactive Elements:

  • Click to flip the hourglass and reverse sand flow
  • Particles naturally pile up in cone shapes
  • Flow rate controlled by neck width
  • Hover effect on canvas for better interactivity

The simulation creates a realistic hourglass where sand flows naturally, accumulates in realistic piles, and responds to gravity changes when flipped. The granular physics ensure particles interact naturally with each other and the hourglass boundaries.

Updated files
v1
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.