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> body { margin: 0; padding: 0; width: 100vw; height: 100vh; background-color: #1a1a1d; color: #e0e0e0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; }
    #container {
        position: relative;
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    canvas {
        background: transparent;
        cursor: pointer;
        /* Ensure it fits but maintains aspect */
        max-width: 90vw;
        max-height: 90vh;
        touch-action: none;
    }

    .ui-layer {
        position: absolute;
        top: 5%;
        text-align: center;
        pointer-events: none;
        z-index: 10;
        opacity: 0.8;
        transition: opacity 1s ease;
    }

    h1 {
        font-size: 1.5rem;
        margin: 0 0 0.5rem 0;
        font-weight: 300;
        letter-spacing: 2px;
        text-transform: uppercase;
        color: #d4af37;
    }

    p {
        font-size: 0.9rem;
        color: #888;
    }

    .instruction {
        margin-top: 8px;
        font-size: 0.8rem;
        color: #aaa;
        background: rgba(0,0,0,0.3);
        padding: 6px 12px;
        border-radius: 20px;
        display: inline-block;
    }
</style>
</head> <body>
<div class="ui-layer" id="ui">
    <h1>Hourglass</h1>
    <div class="instruction">Click anywhere to rotate</div>
</div>

<div id="container">
    <canvas id="simCanvas"></canvas>
</div>
<script> /** * Physics Configuration */ const CONFIG = { particleCount: 900, particleRadius: 3.5, gravity: 0.15, friction: 0.99, // Velocity damping wallFriction: 0.8, subSteps: 8, // Physics iterations per frame for stability glassWidthBase: 15, // Neck width glassWidthMax: 130, // Bulb width glassHeight: 180, // Half-height of the glass rotationSpeed: 0.05 // Lerp speed }; // Setup Canvas const canvas = document.getElementById('simCanvas'); const ctx = canvas.getContext('2d', { alpha: true }); let width, height, cx, cy; let currentRotation = 0; let targetRotation = 0; // Particle System class Particle { constructor(x, y) { this.x = x; this.y = y; this.oldx = x; this.oldy = y; this.r = CONFIG.particleRadius; // Give slight color variation const hue = 35 + Math.random() * 10; const sat = 70 + Math.random() * 20; const lig = 50 + Math.random() * 20; this.color = `hsl(${hue}, ${sat}%, ${lig}%)`; } update() { const vx = (this.x - this.oldx) * CONFIG.friction; const vy = (this.y - this.oldy) * CONFIG.friction; this.oldx = this.x; this.oldy = this.y; this.x += vx; this.y += vy; this.y += CONFIG.gravity; } } let particles = []; // Spatial Hash Grid let grid = {}; const cellSize = CONFIG.particleRadius * 2.2; // Slightly larger than diameter function resize() { // Design reference size const refWidth = 1512; const refHeight = 982; // Determine scale width = window.innerWidth; height = window.innerHeight; canvas.width = width; canvas.height = height; cx = width / 2; cy = height / 2; // If first run, init particles if (particles.length === 0) initParticles(); } function initParticles() { particles = []; const startY = -CONFIG.glassHeight + 20; // Pack particles into the top bulb let pCount = 0; let py = startY; while (pCount < CONFIG.particleCount) { // Calculate width at this height to ensure we spawn inside const allowedWidth = getGlassWidth(py) - 10; const startX = -allowedWidth; const endX = allowedWidth; for (let px = startX; px <= endX; px += CONFIG.particleRadius * 2.2) { // Random jitter to prevent perfect lattice stacking (looks unnatural) const jx = px + (Math.random() - 0.5) * 2; const jy = py + (Math.random() - 0.5) * 2; // Initial rotation is 0, so world cords match local cords // Transform local (0, y) to screen space (cx, cy + y) // But we store simulation coordinates relative to center (0,0) in physics particles.push(new Particle(jx, jy)); pCount++; if (pCount >= CONFIG.particleCount) break; } py += CONFIG.particleRadius * 2.2; // Safety break if (py > CONFIG.glassHeight) break; } } /** * Mathematical definition of the Hourglass walls * Returns the half-width (radius) of the glass at a given local Y */ function getGlassWidth(localY) { const y = Math.abs(localY); if (y > CONFIG.glassHeight) return 0; // Cap ends // Shape function: A neck connected to round bulbs // Using a power curve for the neck and a circle/ellipse for the body // Normalized height 0 to 1 const h = y / CONFIG.glassHeight; // Width equation: Base + Curve // Using Cosine for smooth S-curve shape // 1.0 at ends, 0.0 at center const curve = 1 - Math.cos(h * Math.PI / 2); // Simple interpolated shape // x = neck + (max - neck) * curve^power return CONFIG.glassWidthBase + (CONFIG.glassWidthMax - CONFIG.glassWidthBase) * Math.pow(h, 2.5); } /** * Constraints & Collision Logic */ // 1. Rotate particle into local space of the hourglass // 2. Check bounds // 3. Push out if needed // 4. Rotate back to world space function constrainParticles() { const cos = Math.cos(-currentRotation); const sin = Math.sin(-currentRotation); for (let p of particles) { // Transform World -> Local // We assume the hourglass center is (0,0) in world simulation space let lx = p.x * cos - p.y * sin; let ly = p.x * sin + p.y * cos; // 1. Boundary Check (Walls) // Cap Top/Bottom const floorLimit = CONFIG.glassHeight - p.r; // Check Y bounds (caps) if (ly > floorLimit) { ly = floorLimit; // Simple friction on cap // Transform velocity logic would go here, but Verlet handles it via position adjust } else if (ly < -floorLimit) { ly = -floorLimit; } // Check X bounds (Side walls) const maxW = getGlassWidth(ly) - p.r; if (lx > maxW) { lx = maxW; } else if (lx < -maxW) { lx = -maxW; } // Transform Local -> World // Use inverse rotation (positive currentRotation) const iCos = Math.cos(currentRotation); const iSin = Math.sin(currentRotation); p.x = lx * iCos - ly * iSin; p.y = lx * iSin + ly * iCos; } } // Spatial Hash for Circle Collisions function solveCollisions() { grid = {}; // Populate Grid for (let p of particles) { // Hash key: "x:y" // We use Math.floor to bucket them const gx = Math.floor(p.x / cellSize); const gy = Math.floor(p.y / cellSize); const key = `${gx}:${gy}`; if (!grid[key]) grid[key] = []; grid[key].push(p); } // Check Collisions for (let p of particles) { const gx = Math.floor(p.x / cellSize); const gy = Math.floor(p.y / cellSize); // Check surrounding cells (3x3 grid) for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { const key = `${gx+x}:${gy+y}`; const cell = grid[key]; if (cell) { for (let other of cell) { if (p === other) continue; const dx = p.x - other.x; const dy = p.y - other.y; const distSq = dx*dx + dy*dy; const minDist = p.r + other.r; if (distSq < minDist * minDist && distSq > 0) { const dist = Math.sqrt(distSq); const overlap = minDist - dist; // Push apart relative to mass (equal mass here) const nx = dx / dist; const ny = dy / dist; const separateX = nx * overlap * 0.5; const separateY = ny * overlap * 0.5; p.x += separateX; p.y += separateY; other.x -= separateX; other.y -= separateY; } } } } } } } /** * Rendering */ function drawHourglass() { ctx.save(); ctx.translate(cx, cy); ctx.rotate(currentRotation); // Styling the glass ctx.strokeStyle = "rgba(255, 255, 255, 0.4)"; ctx.lineWidth = 4; ctx.lineCap = "round"; ctx.lineJoin = "round"; // Create path ctx.beginPath(); // Resolution of drawing const steps = 40; // Right Side for (let i = 0; i <= steps; i++) { const t = i / steps; // 0 to 1 // Map t to y range [-glassHeight, glassHeight] const y = -CONFIG.glassHeight + t * (CONFIG.glassHeight * 2); const w = getGlassWidth(y); if (i === 0) ctx.moveTo(w, y); else ctx.lineTo(w, y); } // Bottom Cap (draw across) const wBottom = getGlassWidth(CONFIG.glassHeight); ctx.lineTo(-wBottom, CONFIG.glassHeight); // Left Side (upwards) for (let i = steps; i >= 0; i--) { const t = i / steps; const y = -CONFIG.glassHeight + t * (CONFIG.glassHeight * 2); const w = getGlassWidth(y); ctx.lineTo(-w, y); } // Top Cap const wTop = getGlassWidth(-CONFIG.glassHeight); ctx.lineTo(wTop, -CONFIG.glassHeight); // Fill Glass Background ctx.fillStyle = "rgba(255, 255, 255, 0.05)"; ctx.fill(); ctx.stroke(); // Add a shiny reflection ctx.beginPath(); ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"; ctx.lineWidth = 2; ctx.moveTo(CONFIG.glassWidthMax * 0.5, -CONFIG.glassHeight * 0.5); ctx.quadraticCurveTo(CONFIG.glassWidthBase + 10, 0, CONFIG.glassWidthMax * 0.5, CONFIG.glassHeight * 0.5); ctx.stroke(); ctx.restore(); } function drawParticles() { ctx.save(); ctx.translate(cx, cy); for (let p of particles) { ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = p.color; ctx.fill(); } ctx.restore(); } /** * Main Loop */ function loop() { // Handle Rotation Animation // Smooth ease towards target const diff = targetRotation - currentRotation; if (Math.abs(diff) > 0.001) { currentRotation += diff * CONFIG.rotationSpeed; } else { currentRotation = targetRotation; } // Physics Sub-stepping // We run physics multiple times per frame for stability // especially for stacking (piling) behavior for (let i = 0; i < CONFIG.subSteps; i++) { particles.forEach(p => p.update()); constrainParticles(); solveCollisions(); } // Draw ctx.clearRect(0, 0, width, height); drawHourglass(); drawParticles(); requestAnimationFrame(loop); } // Events window.addEventListener('resize', resize); window.addEventListener('mousedown', () => { targetRotation += Math.PI; document.getElementById('ui').style.opacity = '0'; // Hide UI on first interaction }); window.addEventListener('touchstart', () => { targetRotation += Math.PI; document.getElementById('ui').style.opacity = '0'; }); // Init resize(); requestAnimationFrame(loop); </script> </body> </html>*
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.