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>
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>
<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>
<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>
This physics-based hourglass simulation features:
Physics Implementation:
Visual Features:
Interactive Elements:
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.