Ready to Record
🏖️
💧
🗿
🧱
Brush Size:
3
`); newWindow.document.close(); } else { // Fallback: create download link if popup is blocked const link = document.createElement('a'); link.href = dataURL; link.download = 'particle-physics-creation-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.png'; link.click(); alert('Image generated! If a new window didn\'t open, the image has been downloaded to your device.'); } // Hide the modal hideShareModal(); // Update status document.getElementById('statusText').textContent = 'Image shared successfully!'; } // Get mouse/touch position relative to canvas function getMousePos(e) { const rect = canvas.getBoundingClientRect(); const clientX = e.clientX || (e.touches && e.touches[0] ? e.touches[0].clientX : 0); const clientY = e.clientY || (e.touches && e.touches[0] ? e.touches[0].clientY : 0); return { x: Math.floor((clientX - rect.left) / cellSize), y: Math.floor((clientY - rect.top) / cellSize) }; } // Place particles in a circular brush pattern function placeParticles(centerX, centerY) { if (centerX < 0 || centerX >= gridWidth || centerY < 0 || centerY >= gridHeight) return; if (isInPlaybackMode) return; // Don't allow drawing during playback for (let dy = -brushSize; dy <= brushSize; dy++) { for (let dx = -brushSize; dx <= brushSize; dx++) { const x = centerX + dx; const y = centerY + dy; // Check if within brush radius and grid bounds if (dx * dx + dy * dy <= brushSize * brushSize && x >= 0 && x < gridWidth && y >= 0 && y < gridHeight) { // Don't overwrite immovable particles unless placing immovable particles const currentParticle = grid[y][x]; if (currentParticle === PARTICLE_TYPES.STONE || currentParticle === PARTICLE_TYPES.WALL) { if (selectedParticle !== PARTICLE_TYPES.STONE && selectedParticle !== PARTICLE_TYPES.WALL) { continue; } } grid[y][x] = selectedParticle; } } } } // Physics simulation function updatePhysics() { if (isInPlaybackMode) return; // Don't simulate physics during playback // Create a copy of the grid to avoid conflicts during updates const newGrid = grid.map(row => [...row]); // Update particles from bottom to top, left to right for (let y = gridHeight - 2; y >= 0; y--) { for (let x = 0; x < gridWidth; x++) { const particle = grid[y][x]; if (particle === PARTICLE_TYPES.SAND) { updateSand(x, y, newGrid); } else if (particle === PARTICLE_TYPES.WATER) { updateWater(x, y, newGrid); } } } grid = newGrid; } // Update sand particle physics function updateSand(x, y, newGrid) { // Try to fall down if (y + 1 < gridHeight && grid[y + 1][x] === PARTICLE_TYPES.EMPTY) { newGrid[y][x] = PARTICLE_TYPES.EMPTY; newGrid[y + 1][x] = PARTICLE_TYPES.SAND; return; } // Try to fall diagonally (left or right randomly) const directions = Math.random() < 0.5 ? [-1, 1] : [1, -1]; for (const dir of directions) { const newX = x + dir; if (newX >= 0 && newX < gridWidth && y + 1 < gridHeight && grid[y + 1][newX] === PARTICLE_TYPES.EMPTY) { newGrid[y][x] = PARTICLE_TYPES.EMPTY; newGrid[y + 1][newX] = PARTICLE_TYPES.SAND; return; } } } // Update water particle physics function updateWater(x, y, newGrid) { // Try to fall down first if (y + 1 < gridHeight && grid[y + 1][x] === PARTICLE_TYPES.EMPTY) { newGrid[y][x] = PARTICLE_TYPES.EMPTY; newGrid[y + 1][x] = PARTICLE_TYPES.WATER; return; } // Try to displace sand below if (y + 1 < gridHeight && grid[y + 1][x] === PARTICLE_TYPES.SAND) { newGrid[y][x] = PARTICLE_TYPES.SAND; newGrid[y + 1][x] = PARTICLE_TYPES.WATER; return; } // Try to fall diagonally const directions = Math.random() < 0.5 ? [-1, 1] : [1, -1]; for (const dir of directions) { const newX = x + dir; if (newX >= 0 && newX < gridWidth && y + 1 < gridHeight) { if (grid[y + 1][newX] === PARTICLE_TYPES.EMPTY) { newGrid[y][x] = PARTICLE_TYPES.EMPTY; newGrid[y + 1][newX] = PARTICLE_TYPES.WATER; return; } // Displace sand diagonally if (grid[y + 1][newX] === PARTICLE_TYPES.SAND) { newGrid[y][x] = PARTICLE_TYPES.SAND; newGrid[y + 1][newX] = PARTICLE_TYPES.WATER; return; } } } // Spread horizontally for (const dir of directions) { const newX = x + dir; if (newX >= 0 && newX < gridWidth && grid[y][newX] === PARTICLE_TYPES.EMPTY) { newGrid[y][x] = PARTICLE_TYPES.EMPTY; newGrid[y][newX] = PARTICLE_TYPES.WATER; return; } } } // Render the grid with enhanced visuals function render() { // Clear canvas with background color ctx.fillStyle = '#1a1a1a'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw particles with slight glow effect for (let y = 0; y < gridHeight; y++) { for (let x = 0; x < gridWidth; x++) { const particle = grid[y][x]; if (particle !== PARTICLE_TYPES.EMPTY) { const color = PARTICLE_COLORS[particle]; // Add subtle glow for water if (particle === PARTICLE_TYPES.WATER) { ctx.shadowColor = color; ctx.shadowBlur = 2; } else { ctx.shadowBlur = 0; } ctx.fillStyle = color; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } } } // Reset shadow ctx.shadowBlur = 0; } // Main game loop function gameLoop(timestamp) { // Handle recording if (isRecording) { captureFrame(timestamp); } // Handle playback if (isPlaying) { updatePlayback(timestamp); } else { // Only update physics when not in playback mode updatePhysics(); } render(); requestAnimationFrame(gameLoop); } // Event handlers function setupEventHandlers() { // Particle selection document.querySelectorAll('.particle-btn').forEach(btn => { btn.addEventListener('click', () => { if (isInPlaybackMode) return; // Disable during playback document.querySelector('.particle-btn.active').classList.remove('active'); btn.classList.add('active'); selectedParticle = PARTICLE_TYPES[btn.dataset.particle.toUpperCase()]; }); }); // Brush size slider const brushSlider = document.getElementById('brushSlider'); const brushHandle = document.getElementById('brushHandle'); const brushSizeDisplay = document.getElementById('brushSizeDisplay'); let isDraggingSlider = false; function updateBrushSize(clientX) { const rect = brushSlider.getBoundingClientRect(); const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); brushSize = Math.max(1, Math.min(15, Math.round(percent * 14 + 1))); brushHandle.style.left = `${percent * (rect.width - 32)}px`; brushSizeDisplay.textContent = brushSize; // Update cursor size const cursorSize = brushSize * cellSize * 2; brushCursor.style.width = cursorSize + 'px'; brushCursor.style.height = cursorSize + 'px'; } brushSlider.addEventListener('mousedown', (e) => { isDraggingSlider = true; updateBrushSize(e.clientX); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (isDraggingSlider) { updateBrushSize(e.clientX); } }); document.addEventListener('mouseup', () => { isDraggingSlider = false; }); // Clear button document.getElementById('clearBtn').addEventListener('click', () => { if (isInPlaybackMode) return; // Disable during playback for (let y = 0; y < gridHeight; y++) { for (let x = 0; x < gridWidth; x++) { grid[y][x] = PARTICLE_TYPES.EMPTY; } } document.getElementById('statusText').textContent = 'Canvas cleared'; }); // Share button document.getElementById('shareBtn').addEventListener('click', () => { showShareModal(); }); // Share modal event handlers document.getElementById('generateImageBtn').addEventListener('click', () => { generateShareImage(); }); document.getElementById('cancelShareBtn').addEventListener('click', () => { hideShareModal(); }); // Close modal when clicking outside document.getElementById('shareModal').addEventListener('click', (e) => { if (e.target.id === 'shareModal') { hideShareModal(); } }); // Recording controls document.getElementById('recordBtn').addEventListener('click', () => { if (isInPlaybackMode) return; // Disable during playback if (isRecording) { stopRecording(); } else { startRecording(); } }); document.getElementById('saveBtn').addEventListener('click', () => { saveRecording(); }); // Playback controls document.getElementById('loadBtn').addEventListener('click', () => { document.getElementById('fileInput').click(); }); document.getElementById('fileInput').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { loadRecording(file); } }); document.getElementById('playBtn').addEventListener('click', () => { if (isPlaying) { stopPlayback(); } else { startPlayback(); } }); // Speed control document.querySelectorAll('.speed-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelector('.speed-btn.active').classList.remove('active'); btn.classList.add('active'); playbackSpeed = parseFloat(btn.dataset.speed); }); }); // Canvas drawing (disabled during playback) canvas.addEventListener('mousedown', (e) => { if (isInPlaybackMode) return; isDrawing = true; const pos = getMousePos(e); placeParticles(pos.x, pos.y); lastMouseX = pos.x; lastMouseY = pos.y; e.preventDefault(); }); canvas.addEventListener('mousemove', (e) => { const pos = getMousePos(e); // Update brush cursor (only show when not in playback mode) if (!isInPlaybackMode) { brushCursor.style.left = (e.clientX - brushSize * cellSize) + 'px'; brushCursor.style.top = (e.clientY - brushSize * cellSize) + 'px'; brushCursor.style.display = 'block'; } else { brushCursor.style.display = 'none'; } if (isDrawing && !isInPlaybackMode) { // Draw line between last position and current position const dx = pos.x - lastMouseX; const dy = pos.y - lastMouseY; const distance = Math.sqrt(dx * dx + dy * dy); for (let i = 0; i <= distance; i++) { const t = distance > 0 ? i / distance : 0; const x = Math.round(lastMouseX + dx * t); const y = Math.round(lastMouseY + dy * t); placeParticles(x, y); } lastMouseX = pos.x; lastMouseY = pos.y; } }); canvas.addEventListener('mouseup', () => { isDrawing = false; }); canvas.addEventListener('mouseleave', () => { isDrawing = false; brushCursor.style.display = 'none'; }); // Touch events for mobile support canvas.addEventListener('touchstart', (e) => { if (isInPlaybackMode) return; e.preventDefault(); isDrawing = true; const pos = getMousePos(e); placeParticles(pos.x, pos.y); lastMouseX = pos.x; lastMouseY = pos.y; }, { passive: false }); canvas.addEventListener('touchmove', (e) => { if (isInPlaybackMode) return; e.preventDefault(); if (isDrawing) { const pos = getMousePos(e); const dx = pos.x - lastMouseX; const dy = pos.y - lastMouseY; const distance = Math.sqrt(dx * dx + dy * dy); for (let i = 0; i <= distance; i++) { const t = distance > 0 ? i / distance : 0; const x = Math.round(lastMouseX + dx * t); const y = Math.round(lastMouseY + dy * t); placeParticles(x, y); } lastMouseX = pos.x; lastMouseY = pos.y; } }, { passive: false }); canvas.addEventListener('touchend', (e) => { e.preventDefault(); isDrawing = false; }, { passive: false }); // Window resize handler window.addEventListener('resize', () => { initCanvas(); }); } // Initialize the game function init() { initCanvas(); setupEventHandlers(); // Initialize brush size display const brushSizeDisplay = document.getElementById('brushSizeDisplay'); brushSizeDisplay.textContent = brushSize; // Set initial brush handle position const brushHandle = document.getElementById('brushHandle'); const percent = (brushSize - 1) / 14; brushHandle.style.left = `${percent * 148}px`; // Initialize cursor size const cursorSize = brushSize * cellSize * 2; brushCursor.style.width = cursorSize + 'px'; brushCursor.style.height = cursorSize + 'px'; // Start game loop requestAnimationFrame(gameLoop); } // Start the game when page loads window.addEventListener('load', init);