<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>
<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>