<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BP Sand</title>
<style>
body, html {
margin: 0;
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
canvas {
display: block;
width: 100%;
background-color: beige;
}
.controls {
display: flex;
justify-content: space-around;
padding: 10px;
background-color: white;
box-shadow: 0 -2px 5px rgba(0,0,0,0.1);
}
input, button {
font-size: 1.5rem;
}
</style>
</head>
<body>
<canvas id="sandCanvas"></canvas>
<div class="controls">
<input type="number" id="bpmInput" value="60" min="10" max="300">
<div>
<button id="clearSand">Clear</button>
<button id="toggleButton">Start</button>
</div>
</div>
<script>
const canvas = document.getElementById('sandCanvas');
const ctx = canvas.getContext('2d');
const bpmInput = document.getElementById('bpmInput');
const toggleButton = document.getElementById('toggleButton');
const clearSandButton = document.getElementById('clearSand');
const controls = document.querySelector('.controls');
let bpm = 60;
let isRunning = false;
let sandParticles = [];
let intervalId;
// Set canvas size to avoid overlapping with controls
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - controls.offsetHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playTone() {
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); // 440 Hz tone
oscillator.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.1); // Short beep
}
function createSandParticle() {
return {
x: Math.random() * canvas.width,
y: 0,
size: 5 + Math.random() * 5, // Vary size a bit
velocityY: 0,
atRest: false // Track if the particle is at rest
};
}
// Update sand particle positions
function updateSand() {
for (let particle of sandParticles) {
if (!particle.atRest) {
particle.velocityY += 0.1; // Accelerate! Gravity!
particle.y += particle.velocityY;
// Check if sand particle has hit the bottom
if (particle.y + particle.size >= canvas.height) {
particle.y = canvas.height - particle.size;
particle.atRest = true; // Mark the particle as at rest
}
// Check if sand particle has landed on another particle
for (let otherParticle of sandParticles) {
if (otherParticle !== particle && otherParticle.atRest) {
let distY = otherParticle.y - (particle.y + particle.size);
let distX = Math.abs(otherParticle.x - particle.x);
if (distY <= 0 && distX < particle.size) {
particle.y = otherParticle.y - particle.size;
particle.atRest = true;
break;
}
}
}
}
}
}
// Draw sand particles on the canvas
function drawSand() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'teal';
for (let particle of sandParticles) {
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fill();
}
}
// Main loop
function gameLoop() {
updateSand();
drawSand();
}
// Control the BPM and start/stop the loop
function toggleBPM() {
isRunning = !isRunning;
if (isRunning) {
let interval = (60 / bpm) * 1000; // Convert BPM to interval in milliseconds
intervalId = setInterval(() => {
sandParticles.push(createSandParticle()); // Generate new sand
playTone(); // Play the tone
}, interval);
toggleButton.textContent = 'Stop';
} else {
clearInterval(intervalId);
toggleButton.textContent = 'Start';
}
}
// Event Listeners
toggleButton.addEventListener('click', toggleBPM);
clearSandButton.addEventListener('click', () => {
sandParticles = [];
});
bpmInput.addEventListener('input', (e) => {
bpm = parseInt(e.target.value);
if (isRunning) {
clearInterval(intervalId);
toggleBPM();
}
});
function animate() {
gameLoop();
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>