-<!DOCTYPE html>\r
-<html lang="en">\r
-<head>\r
- <meta charset="UTF-8">\r
- <meta name="viewport" content="width=device-width, initial-scale=1.0">\r
- <title>Circles Of Doom with Music by StarStabbedMoon</title>\r
- <style>\r
- /* Basic styling to fill the screen and show a black background */\r
- body, html {\r
- margin: 0;\r
- padding: 0;\r
- overflow: hidden;\r
- height: 100%;\r
- }\r
- canvas {\r
- display: block;\r
- background: black;\r
- width: 100%;\r
- height: 100%;\r
- }\r
- /* UI container styling */\r
- #ui {\r
- position: fixed;\r
- bottom: 0;\r
- left: 0;\r
- width: 100%;\r
- background: rgba(255, 255, 255, 0);\r
- padding: 10px;\r
- \r
- display: flex;\r
- flex-direction: column;\r
- align-items: flex-start;\r
- gap: 10px;\r
- transition: transform 0.3s ease-in-out;\r
- transform: translateY(100%); /* Hidden by default */\r
- pointer-events: none; /* Makes UI transparent to clicks */\r
- }\r
- \r
- #ui * {\r
- pointer-events: auto; /* Re-enable clicks for child elements */\r
- }\r
- /* When we add the class "visible", the UI slides up */\r
- #ui.visible {\r
- transform: translateY(0);\r
- }\r
- /* Container for sliders and labels */\r
- .slider-container {\r
- display: flex;\r
- align-items: center;\r
- gap: 10px;\r
- margin-left: 10px; /* Reduced from 20px */\r
- color: white;\r
- width: auto;\r
- pointer-events: none;\r
- }\r
- .slider-container > * {\r
- pointer-events: auto;\r
- }\r
- label {\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- min-width: 150px; /* Ensure labels have consistent width */\r
- color: white;\r
- flex-shrink: 0;\r
- }\r
- input[type="range"] {\r
- flex: 1; /* Take available space */\r
- max-width: 300px; /* Maximum width for larger screens */\r
- min-width: 100px; /* Minimum usable width */\r
- }\r
- \r
- \r
- /* Mobile adjustments - Untested */\r
- @media (max-width: 600px) {\r
- .slider-container {\r
- margin-left: 5px;\r
- gap: 8px;\r
- }\r
- \r
- label {\r
- min-width: 110px; /* Smaller min-width for mobile */\r
- font-size: 13px;\r
- }\r
- \r
- input[type="range"] {\r
- max-width: 100%; /* Allow full width of container */\r
- }\r
- \r
- #info, #options, #resetButton, #restoreDefaultsButton {\r
- margin-left: 5px;\r
- }\r
- }\r
- \r
- \r
- /* Information text styling */\r
- #info {\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- margin-top: 10px;\r
- text-align: left;\r
- width: auto;\r
- margin-left: 20px; /* Indent options text */\r
- color: white;\r
- }\r
- \r
- /* Options text styling */\r
- #options {\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- margin-top: 10px;\r
- text-align: left;\r
- width: auto;\r
- margin-left: 20px; /* Indent info text */\r
- color: white;\r
- }\r
- \r
- \r
- .button-container {\r
- display: flex;\r
- gap: 10px;\r
- margin-left: 20px;\r
- margin-top: 10px;\r
- }\r
- /* Reset button styling */\r
- #resetButton {\r
- padding: 5px 10px;\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- cursor: pointer;\r
- color: white;\r
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\r
- background: rgba(0, 0, 0, 255);\r
- }\r
- /* Restore Defaults button styling */\r
-#restoreDefaultsButton {\r
- padding: 5px 10px;\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- cursor: pointer;\r
- color: white;\r
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\r
- background: rgba(0, 0, 0, 255);\r
-}\r
- /* Button to toggle the UI visibility */\r
- #toggleUIButton {\r
- position: fixed;\r
- bottom: 10px;\r
- right: 10px;\r
- padding: 10px;\r
- background: rgba(0, 0, 0, 255);\r
- color: white;\r
- border: none;\r
- border-radius: 5px;\r
- cursor: pointer;\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\r
- }\r
- \r
- \r
- .music-credits {\r
- font-family: Arial, sans-serif;\r
- font-size: 12px;\r
- color: white;\r
- margin-left: 20px;\r
- margin-top: 10px;\r
- opacity: 0.8;\r
- text-align: center;\r
- }\r
-\r
- .music-credits a {\r
- color: #ff8888;\r
- text-decoration: none;\r
- margin: 0 5px;\r
- }\r
-\r
- .music-credits a:hover {\r
- text-decoration: underline;\r
- }\r
- \r
- #fullscreenButton {\r
- position: fixed;\r
- bottom: 60px;\r
- right: 10px;\r
- padding: 10px;\r
- background: rgba(0, 0, 0, 255);\r
- color: white;\r
- border: none;\r
- border-radius: 5px;\r
- cursor: pointer;\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\r
- z-index: 1000;\r
- }\r
- \r
- /* Mode toggle styling */\r
-.mode-toggle {\r
- margin-top: 10px;\r
-}\r
-\r
-.toggle-container {\r
- display: flex;\r
- gap: 5px;\r
- margin-left: 10px;\r
-}\r
-\r
-.mode-button {\r
- padding: 5px 12px;\r
- border: none;\r
- border-radius: 15px;\r
- cursor: pointer;\r
- background: rgba(255, 255, 255, 0.1);\r
- color: white;\r
- font-family: Arial, sans-serif;\r
- font-size: 14px;\r
- transition: all 0.2s ease;\r
-}\r
-\r
-.mode-button.active {\r
- background: #ff4444;\r
- color: white;\r
- box-shadow: 0 0 5px rgba(255, 68, 68, 0.5);\r
-}\r
- \r
-/* Modern slider styling with persistent red color */\r
-input[type="range"] {\r
- accent-color: #ff0000;\r
- background: transparent !important;\r
- border: none !important;\r
- box-shadow: none !important;\r
-}\r
-\r
-/* Chrome/Safari track */\r
-input[type="range"]::-webkit-slider-runnable-track {\r
- background: rgba(255, 255, 255, 0.2);\r
- height: 8px;\r
- border-radius: 5px;\r
- border: 1px solid #000000ff;\r
- box-sizing: border-box;\r
-}\r
-\r
-/* Chrome/Safari thumb - Border workaround */\r
-input[type="range"]::-webkit-slider-thumb {\r
- -webkit-appearance: none;\r
- width: 16px;\r
- height: 16px;\r
- background: #ff0000;\r
- border-radius: 50%;\r
- margin-top: -4px;\r
- box-shadow: 0 0 0 1.5px black; /* Simulated border using box-shadow */\r
- position: relative; /* Ensure layering */\r
- z-index: 1; /* Force border above track */\r
-}\r
-\r
-/* Firefox Track */\r
-input[type="range"]::-moz-range-track {\r
- background: #ffffff; /* Completely transparent */\r
- height: 8px;\r
- border: 1px solid #000000ff;\r
- border-radius: 5px;\r
- box-shadow: none;\r
-}\r
-\r
-/* Firefox Progress (filled portion) */\r
-input[type="range"]::-moz-range-progress {\r
- background: #ff0000; /* Red filled area */\r
- height: 8px;\r
- border-radius: 5px 0 0 5px;\r
- border: none;\r
-}\r
-\r
-/* Firefox Thumb */\r
-input[type="range"]::-moz-range-thumb {\r
- width: 16px;\r
- height: 16px;\r
- background: #ff0000 !important; /* Force red color */\r
- border: 2px solid black;\r
- border-radius: 50%;\r
- box-shadow: none;\r
- position: relative;\r
- z-index: 1;\r
-}\r
-\r
-/* Remove focus ring */\r
-input[type="range"]:focus {\r
- outline: none;\r
-}\r
-\r
-\r
-.stat {\r
- display: flex;\r
- align-items: center;\r
- gap: 8px;\r
- margin-bottom: 8px;\r
-}\r
-\r
-.stat-checkbox {\r
- margin: 0;\r
- accent-color: #ff4444; /* Red color for checkboxes */\r
-}\r
-\r
-#info {\r
- margin-left: 10px; /* Adjust as needed */\r
-}\r
-\r
-#options {\r
- margin-left: 10px; /* Adjust as needed */\r
-}\r
-\r
-\r
- </style>\r
-</head>\r
-<body>\r
- <!-- Main game canvas -->\r
- <canvas id="gameCanvas"></canvas>\r
-\r
- <!-- UI with sliders and info -->\r
- <div id="ui">\r
- <div class="slider-container">\r
- <label for="ballCountSlider">Number of Balls:</label>\r
- <input type="range" id="ballCountSlider" min="1" max="500" value="60" autocomplete="off">\r
- <span id="ballCountValue">60</span>\r
- </div>\r
- <div class="slider-container">\r
- <label for="ballSpeedSlider">Ball Speed:</label>\r
- <input type="range" id="ballSpeedSlider" min="0" max="10" step="0.25" value="2" autocomplete="off">\r
- <span id="ballSpeedValue">2</span>\r
- </div>\r
- <div class="slider-container">\r
- <label for="ballRadiusSlider">Ball Radius:</label>\r
- <input type="range" id="ballRadiusSlider" min="1" max="20" value="5" autocomplete="off">\r
- <span id="ballRadiusValue">5</span>\r
- </div>\r
- <div class="slider-container">\r
- <label for="explosionRadiusSlider">Explosion Radius:</label>\r
- <input type="range" id="explosionRadiusSlider" min="10" max="200" value="60" autocomplete="off">\r
- <span id="explosionRadiusValue">60</span>\r
- </div>\r
- <div class="slider-container">\r
- <label for="explosionSpeedSlider">Explosion Speed:</label>\r
- <input type="range" id="explosionSpeedSlider" min="0.25" max="4" step="0.25" value="1.0" autocomplete="off">\r
- <span id="explosionSpeedValue">1.0</span>\r
- </div>\r
- <div class="slider-container">\r
- <label for="explosionLingerSlider">Explosion Linger Time:</label>\r
- <input type="range" id="explosionLingerSlider" min="0" max="4" step="0.1" value="0" autocomplete="off">\r
- <span id="explosionLingerValue">0</span>\r
- </div>\r
- <div class="slider-container">\r
- <label for="explosionDragFreqSlider">Explosion Drag Freq:</label>\r
- <input type="range" id="explosionDragFreqSlider" min="10" max="1000" value="100" autocomplete="off">\r
- <span id="explosionDragFreqValue">100</span>\r
- </div>\r
-<div id="options"> \r
- <div class="stat">\r
- <input type="checkbox" class="stat-checkbox" id="autoStartCheckbox">\r
- <label for="autoStartCheckbox">Auto Start Reaction</label>\r
- </div>\r
- <div class="stat">\r
- <input type="checkbox" class="stat-checkbox" id="ballAfterExplosionCheckbox" checked>\r
- <label for="ballAfterExplosionCheckbox">Ball After Explosion</label>\r
- </div>\r
-</div>\r
-<div id="info">\r
- <div class="stat">\r
- <input type="checkbox" class="stat-checkbox" id="activeExplosionsCheckbox">\r
- <label for="activeExplosionsCheckbox">Active Explosions:</label>\r
- <span id="activeExplosions">0</span>\r
- </div>\r
- <div class="stat">\r
- <input type="checkbox" class="stat-checkbox" id="longestChainCheckbox">\r
- <label for="longestChainCheckbox">Longest Current Chain:</label>\r
- <span id="longestCurrentChain">0</span>\r
- </div>\r
- <div class="stat">\r
- <input type="checkbox" class="stat-checkbox" id="totalExplosionsCheckbox">\r
- <label for="totalExplosionsCheckbox">Total Explosions:</label>\r
- <span id="totalExplosions">0</span>\r
- </div>\r
-</div>\r
-<div class="button-container">\r
- <button id="resetButton">Reset</button>\r
- <button id="restoreDefaultsButton">Restore Defaults</button>\r
-</div>\r
-<div class="slider-container mode-toggle">\r
- <label>Mode:</label>\r
- <div class="toggle-container">\r
- <button class="mode-button active" data-mode="click">Click</button>\r
- <button class="mode-button" data-mode="drag">Drag</button>\r
- </div>\r
-</div>\r
- <div class="music-credits">\r
- Music by StarStabbedMoon:<br>\r
- <a href="mailto:StarStabbedMoon@gmail.com">Email</a> \r
- <a href="https://starstabbedmoon.bandcamp.com">Bandcamp</a><br>\r
- <a href="https://www.youtube.com/starstabbedmoon">YouTube</a>\r
- <a href="https://open.spotify.com/artist/71cCMapE4464Npz4TnTAT8?si=CpaYj-SUR0S_2Y6hg1B5bw">Spotify</a> \r
- <a href="https://www.soundclick.com/starstabbedmoon">SoundClick</a>\r
- </div>\r
- </div>\r
-\r
- <!-- Button to show or hide the UI -->\r
- <button id="toggleUIButton">Show Controls</button>\r
- <button id="fullscreenButton">Full Screen</button>\r
- \r
- <button id="musicButton" style="position: fixed; bottom: 110px; right: 10px; padding: 10px; background: rgba(0, 0, 0, 255); color: white; border: none; border-radius: 5px; cursor: pointer; font-family: Arial, sans-serif; font-size: 14px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); z-index: 1000; filter: grayscale(100%);">\r
- 🔇\r
- </button>\r
- \r
- <audio id="bgMusic" loop>\r
- <source src="Celestial Assault.mp3" type="audio/mpeg">\r
- Your browser does not support the audio element.\r
- </audio>\r
-\r
- <script>\r
- /*************************************************************\r
- * GLOBAL VARIABLES & INITIAL SETUP\r
- *************************************************************/\r
- const canvas = document.getElementById('gameCanvas');\r
- const ctx = canvas.getContext('2d');\r
-\r
- // Arrays to hold balls and explosions\r
- let balls = [];\r
- let expandingCircles = [];\r
-\r
- // Default values for game parameters\r
- let maxCircleSize = 60; // Explosion radius\r
- let ballCount = 60; // Number of balls\r
- let ballRadius = 6; // Radius of each ball\r
- let ballSpeed = 2; // Speed of each ball (base value)\r
- let explosionSpeed = 1.0; // Expansion speed of explosion\r
- let explosionLingerTime = 0; // Seconds to linger after reaching max size\r
- let explosionDragFreq = 100; // Milliseconds between explosions during drag\r
- let isDragging = false;\r
- let lastDragExplosionTime = 0;\r
- let currentDragPosition = { x: 0, y: 0 };\r
- let interactionMode = 'drag';\r
- let muted = true;\r
- let totalExplosions = 0;\r
- \r
- \r
- \r
-\r
- \r
-\r
- // Track the time of the previous frame for consistent animation\r
- let lastFrameTime = 0;\r
- \r
-\r
- /*************************************************************\r
- * UI ELEMENTS\r
- *************************************************************/\r
- // sliders\r
- const ballCountSlider = document.getElementById('ballCountSlider');\r
- const ballCountValue = document.getElementById('ballCountValue');\r
- const ballSpeedSlider = document.getElementById('ballSpeedSlider');\r
- const ballSpeedValue = document.getElementById('ballSpeedValue');\r
- const ballRadiusSlider = document.getElementById('ballRadiusSlider');\r
- const ballRadiusValue = document.getElementById('ballRadiusValue');\r
- const explosionRadiusSlider = document.getElementById('explosionRadiusSlider');\r
- const explosionRadiusValue = document.getElementById('explosionRadiusValue');\r
- const explosionSpeedSlider = document.getElementById('explosionSpeedSlider');\r
- const explosionSpeedValue = document.getElementById('explosionSpeedValue');\r
- const explosionLingerSlider = document.getElementById('explosionLingerSlider');\r
- const explosionLingerValue = document.getElementById('explosionLingerValue');\r
- const explosionDragFreqSlider = document.getElementById('explosionDragFreqSlider');\r
- const explosionDragFreqValue = document.getElementById('explosionDragFreqValue');\r
- // stats\r
- const activeExplosions = document.getElementById('activeExplosions');\r
- const longestCurrentChain = document.getElementById('longestCurrentChain');\r
- const totalExplosionsElement = document.getElementById('totalExplosions');\r
- // options\r
- const modeSelect = document.getElementById('modeSelect');\r
- const autoStartCheckbox = document.getElementById('autoStartCheckbox');\r
- const ballAfterExplosionCheckbox = document.getElementById('ballAfterExplosionCheckbox');\r
- // other\r
- const resetButton = document.getElementById('resetButton');\r
- const restoreDefaultsButton = document.getElementById('restoreDefaultsButton');\r
- const toggleUIButton = document.getElementById('toggleUIButton');\r
- const fullscreenButton = document.getElementById('fullscreenButton');\r
- const bgMusic = document.getElementById('bgMusic');\r
- const musicButton = document.getElementById('musicButton');\r
-\r
- const ui = document.getElementById('ui');\r
- \r
- \r
- /*************************************************************\r
- * LOAD/SAVE SETTINGS\r
- *************************************************************/\r
- function loadSettings() {\r
- // Sliders\r
- ballCountSlider.value = localStorage.getItem('ballCount') || ballCountSlider.value;\r
- ballSpeedSlider.value = localStorage.getItem('ballSpeed') || ballSpeedSlider.value;\r
- ballRadiusSlider.value = localStorage.getItem('ballRadius') || ballRadiusSlider.value;\r
- explosionRadiusSlider.value = localStorage.getItem('explosionRadius') || explosionRadiusSlider.value;\r
- explosionSpeedSlider.value = localStorage.getItem('explosionSpeed') || explosionSpeedSlider.value;\r
- explosionLingerSlider.value = localStorage.getItem('explosionLinger') || explosionLingerSlider.value;\r
- explosionDragFreqSlider.value = localStorage.getItem('explosionDragFreq') || explosionDragFreqSlider.value;\r
-\r
- // Checkboxes\r
- autoStartCheckbox.checked = localStorage.getItem('autoStart') === 'true';\r
- ballAfterExplosionCheckbox.checked = localStorage.getItem('ballAfterExplosion') === 'true';\r
- document.getElementById('activeExplosionsCheckbox').checked = localStorage.getItem('activeExplosionsCheckbox') === 'true';\r
- document.getElementById('longestChainCheckbox').checked = localStorage.getItem('longestChainCheckbox') === 'true';\r
- document.getElementById('totalExplosionsCheckbox').checked = localStorage.getItem('totalExplosionsCheckbox') === 'true';\r
-\r
- // Interaction Mode\r
- const savedMode = localStorage.getItem('interactionMode');\r
- if (savedMode) {\r
- interactionMode = savedMode;\r
- }\r
- document.querySelectorAll('.mode-button').forEach(btn => {\r
- btn.classList.toggle('active', btn.dataset.mode === interactionMode);\r
- });\r
- \r
-\r
- // UI Visibility\r
- const uiVisible = localStorage.getItem('uiVisible') === 'true';\r
- ui.classList.toggle('visible', uiVisible);\r
- toggleUIButton.textContent = uiVisible ? 'Hide Controls' : 'Show Controls';\r
-\r
- // Music Mute State\r
- muted = !(localStorage.getItem('muted') === 'false');\r
- musicButton.textContent = muted ? '🔇' : '🔊';\r
- if (!muted) bgMusic.play().catch(() => {});\r
- }\r
-\r
- function saveSetting(key, value) {\r
- localStorage.setItem(key, value);\r
- }\r
-\r
-\r
- /*************************************************************\r
- * FIREFOX SLIDER FIX\r
- *************************************************************/\r
- const sliders = [\r
- ballCountSlider,\r
- ballSpeedSlider,\r
- ballRadiusSlider,\r
- explosionRadiusSlider,\r
- explosionSpeedSlider,\r
- explosionLingerSlider\r
- ];\r
-\r
- function resetSliderVisuals() {\r
- sliders.forEach(slider => {\r
- const initialValue = slider.getAttribute('value');\r
- slider.value = initialValue;\r
- // Force DOM refresh\r
- const type = slider.type;\r
- slider.type = "text";\r
- slider.type = "range";\r
- slider.blur();\r
- });\r
- }\r
-\r
- /*************************************************************\r
- * INITIALIZATION\r
- *************************************************************/\r
- function init() {\r
- \r
- //restoreDefaultSettings(); // initialize defaults\r
- \r
- const screenArea = window.innerWidth * window.innerHeight;\r
- const baseArea = 1920 * 1080; // Reference area for default 60 balls\r
- let proportionalBallCount = (screenArea / baseArea) * 350;\r
- proportionalBallCount = Math.round(proportionalBallCount / 10) * 10; // Round to nearest 10\r
- proportionalBallCount = Math.min(proportionalBallCount, 500); // Clamp to slider max\r
- proportionalBallCount = Math.max(proportionalBallCount, 10); // Minimum 10 balls\r
- \r
- ballCountSlider.value = proportionalBallCount;\r
- \r
- \r
- loadSettings(); // Load before resetting the game\r
- //resetSliderVisuals();\r
- \r
- \r
-\r
- // Set parameters from HTML attributes\r
- maxCircleSize = parseInt(explosionRadiusSlider.value);\r
- ballCount = parseInt(ballCountSlider.value);\r
- ballRadius = parseInt(ballRadiusSlider.value);\r
- ballSpeed = parseFloat(ballSpeedSlider.value);\r
- explosionSpeed = parseFloat(explosionSpeedSlider.value);\r
- explosionLingerTime = parseFloat(explosionLingerSlider.value);\r
- explosionDragFreq = parseInt(explosionDragFreqSlider.value);\r
- \r
- // Update displayed values\r
- ballCountValue.textContent = ballCountSlider.value;\r
- ballSpeedValue.textContent = ballSpeedSlider.value;\r
- ballRadiusValue.textContent = ballRadiusSlider.value;\r
- explosionRadiusValue.textContent = explosionRadiusSlider.value;\r
- explosionSpeedValue.textContent = explosionSpeedSlider.value;\r
- explosionLingerValue.textContent = explosionLingerSlider.value;\r
- explosionDragFreqValue.textContent = explosionDragFreqSlider.value;\r
-\r
- resetGame();\r
- }\r
-\r
- /*************************************************************\r
- * UI EVENT LISTENERS\r
- *************************************************************/\r
- \r
- restoreDefaultsButton.addEventListener('click', restoreDefaultSettings);\r
- \r
-document.querySelectorAll('.mode-button').forEach(button => {\r
- button.addEventListener('click', function() {\r
- \r
- // Remove active class from all buttons\r
- document.querySelectorAll('.mode-button').forEach(b => b.classList.remove('active'));\r
- // Add active class to clicked button\r
- this.classList.add('active');\r
- // Update interaction mode\r
- interactionMode = this.dataset.mode;\r
- saveSetting('interactionMode', this.dataset.mode);\r
- endDrag(); // Cancel any ongoing drag operations\r
- });\r
-});\r
-/* \r
-// Attempt autoplay on page load\r
-document.addEventListener('DOMContentLoaded', () => {\r
- bgMusic.play()\r
- .then(() => musicButton.textContent = 'Pause Music')\r
- .catch(() => {\r
- musicButton.textContent = 'Play Music (Blocked)';\r
- console.log('Autoplay blocked - click button to start');\r
- });\r
-});\r
-*/\r
- \r
- // Toggle music playback\r
-musicButton.addEventListener('click', () => {\r
- muted = !muted;\r
- saveSetting('muted', muted);\r
- if (!muted) {\r
- bgMusic.play();\r
- musicButton.textContent = '🔊';\r
- } else {\r
- bgMusic.pause();\r
- musicButton.textContent = '🔇';\r
- }\r
-});\r
-\r
-\r
-\r
- // handle music start playing on first click\r
- document.addEventListener('click', handleFirstClick);\r
-\r
- // Show/hide the control panel\r
- toggleUIButton.addEventListener('click', () => {\r
- ui.classList.toggle('visible');\r
- saveSetting('uiVisible', ui.classList.contains('visible'));\r
- toggleUIButton.textContent = ui.classList.contains('visible') ? 'Hide Controls' : 'Show Controls';\r
- });\r
-\r
- // Adjust ball count and reset the game\r
- ballCountSlider.addEventListener('input', () => {\r
- //saveSetting('ballCount', ballCountSlider.value);\r
- ballCountValue.textContent = ballCountSlider.value;\r
- ballCount = parseInt(ballCountSlider.value);\r
- saveSetting('ballCount', ballCount);\r
- resetGame();\r
- });\r
-\r
- // Adjust ball speed (time-based movement will keep it consistent)\r
- ballSpeedSlider.addEventListener('input', () => {\r
- saveSetting('ballSpeed', ballSpeedSlider.value);\r
- ballSpeedValue.textContent = ballSpeedSlider.value;\r
- ballSpeed = parseFloat(ballSpeedSlider.value);\r
- updateBallSpeeds();\r
- });\r
-\r
- // Adjust ball radius and reset balls\r
- ballRadiusSlider.addEventListener('input', () => {\r
- saveSetting('ballRadius', ballRadiusSlider.value);\r
- ballRadiusValue.textContent = ballRadiusSlider.value;\r
- ballRadius = parseInt(ballRadiusSlider.value);\r
- resetBalls();\r
- });\r
-\r
- // Adjust explosion radius (max size)\r
- explosionRadiusSlider.addEventListener('input', () => {\r
- saveSetting('explosionRadius', explosionRadiusSlider.value);\r
- explosionRadiusValue.textContent = explosionRadiusSlider.value;\r
- maxCircleSize = parseInt(explosionRadiusSlider.value);\r
- });\r
-\r
- // Adjust explosion speed (for time-based growth)\r
- explosionSpeedSlider.addEventListener('input', () => {\r
- saveSetting('explosionSpeed', explosionSpeedSlider.value);\r
- explosionSpeed = parseFloat(explosionSpeedSlider.value);\r
- explosionSpeedValue.textContent = explosionSpeed.toFixed(2);\r
- });\r
-\r
- // Adjust explosion lingering time\r
- explosionLingerSlider.addEventListener('input', () => {\r
- saveSetting('explosionLinger', explosionLingerSlider.value);\r
- explosionLingerValue.textContent = explosionLingerSlider.value;\r
- explosionLingerTime = parseFloat(explosionLingerSlider.value);\r
- });\r
-\r
- // Reset the entire game\r
- resetButton.addEventListener('click', () => {\r
- resetGame();\r
- });\r
- \r
- fullscreenButton.addEventListener('click', toggleFullScreen);\r
- \r
- // Add to existing event listeners\r
- document.addEventListener('fullscreenchange', () => {\r
- if (!document.fullscreenElement) {\r
- fullscreenButton.textContent = 'Full Screen';\r
- }\r
- });\r
- \r
- explosionDragFreqSlider.addEventListener('input', () => {\r
- saveSetting('explosionDragFreq', explosionDragFreqSlider.value);\r
- explosionDragFreq = parseInt(explosionDragFreqSlider.value);\r
- explosionDragFreqValue.textContent = explosionDragFreq;\r
- });\r
- \r
- // Checkboxes\r
- autoStartCheckbox.addEventListener('change', () => {\r
- saveSetting('autoStart', autoStartCheckbox.checked);\r
- });\r
- ballAfterExplosionCheckbox.addEventListener('change', () => {\r
- saveSetting('ballAfterExplosion', ballAfterExplosionCheckbox.checked);\r
- });\r
- document.getElementById('activeExplosionsCheckbox').addEventListener('change', function() {\r
- saveSetting('activeExplosionsCheckbox', this.checked);\r
- });\r
- document.getElementById('longestChainCheckbox').addEventListener('change', function() {\r
- saveSetting('longestChainCheckbox', this.checked);\r
- });\r
- document.getElementById('totalExplosionsCheckbox').addEventListener('change', function() {\r
- saveSetting('totalExplosionsCheckbox', this.checked);\r
- });\r
- \r
- canvas.addEventListener('mousedown', startDrag);\r
- canvas.addEventListener('mousemove', handleDrag);\r
- canvas.addEventListener('mouseup', endDrag);\r
- canvas.addEventListener('mouseleave', endDrag);\r
-\r
- /*************************************************************\r
- * HELPER FUNCTIONS\r
- *************************************************************/\r
- \r
- // Add this function with other helper functions\r
-function restoreDefaultSettings() {\r
-\r
- const screenArea = window.innerWidth * window.innerHeight;\r
- const baseArea = 1920 * 1080; // Reference area for default 60 balls\r
- let proportionalBallCount = (screenArea / baseArea) * 350;\r
- proportionalBallCount = Math.round(proportionalBallCount / 10) * 10; // Round to nearest 10\r
- proportionalBallCount = Math.min(proportionalBallCount, 500); // Clamp to slider max\r
- proportionalBallCount = Math.max(proportionalBallCount, 10); // Minimum 10 balls\r
- \r
- // Sliders\r
- const sliders = [\r
- { slider: ballCountSlider, defaultValue: proportionalBallCount.toString() },\r
- { slider: ballSpeedSlider, defaultValue: '2' },\r
- { slider: ballRadiusSlider, defaultValue: '6' },\r
- { slider: explosionRadiusSlider, defaultValue: '60' },\r
- { slider: explosionSpeedSlider, defaultValue: '1.0' },\r
- { slider: explosionLingerSlider, defaultValue: '0' },\r
- { slider: explosionDragFreqSlider, defaultValue: '100' }\r
- ];\r
- \r
- sliders.forEach(({ slider, defaultValue }) => {\r
- slider.value = defaultValue;\r
- slider.dispatchEvent(new Event('input'));\r
- });\r
-\r
- // Checkboxes\r
- const checkboxes = [\r
- { element: autoStartCheckbox, defaultValue: false },\r
- { element: ballAfterExplosionCheckbox, defaultValue: true },\r
- { element: document.getElementById('activeExplosionsCheckbox'), defaultValue: false },\r
- { element: document.getElementById('longestChainCheckbox'), defaultValue: false },\r
- { element: document.getElementById('totalExplosionsCheckbox'), defaultValue: false }\r
- ];\r
- \r
- checkboxes.forEach(({ element, defaultValue }) => {\r
- element.checked = defaultValue;\r
- element.dispatchEvent(new Event('change'));\r
- });\r
-\r
- // Interaction Mode\r
- interactionMode = 'drag';\r
- document.querySelectorAll('.mode-button').forEach(btn => {\r
- btn.classList.toggle('active', btn.dataset.mode === 'drag');\r
- });\r
- saveSetting('interactionMode', 'drag');\r
-\r
- // UI Visibility\r
-// ui.classList.remove('visible');\r
-// saveSetting('uiVisible', false);\r
-// toggleUIButton.textContent = 'Show Controls';\r
-\r
-// // Music\r
-// muted = false;\r
-// musicButton.textContent = '🔊';\r
-// saveSetting('muted', false);\r
-// bgMusic.play().catch(() => {});\r
-\r
- resetGame();\r
-}\r
- \r
- function handleFirstClick(event) {\r
- \r
- if (event.target === musicButton)\r
- return;\r
- \r
- if (!muted)\r
- bgMusic.play();\r
- \r
- document.removeEventListener('click', handleFirstClick);\r
- \r
- }\r
- \r
- function toggleFullScreen() {\r
- if (!document.fullscreenElement) {\r
- if (canvas.requestFullscreen) {\r
- canvas.requestFullscreen();\r
- } else if (canvas.webkitRequestFullscreen) { /* Safari */\r
- canvas.webkitRequestFullscreen();\r
- } else if (canvas.msRequestFullscreen) { /* IE11 */\r
- canvas.msRequestFullscreen();\r
- }\r
- fullscreenButton.textContent = 'Exit Full Screen';\r
- } else {\r
- if (document.exitFullscreen) {\r
- document.exitFullscreen();\r
- } else if (document.webkitExitFullscreen) { /* Safari */\r
- document.webkitExitFullscreen();\r
- } else if (document.msExitFullscreen) { /* IE11 */\r
- document.msExitFullscreen();\r
- }\r
- fullscreenButton.textContent = 'Full Screen';\r
- }\r
- }\r
- /**\r
- * Resets all balls based on the current ballCount and ballRadius.\r
- * Clears the balls array and spawns fresh balls.\r
- */\r
- function resetBalls() {\r
- balls = [];\r
- for (let i = 0; i < ballCount; i++) {\r
- spawnBall();\r
- }\r
- }\r
-\r
- /**\r
- * Updates the speed (dx, dy) of each ball to match the new ballSpeed\r
- * but preserves their direction. If speed was zero, assigns a random direction.\r
- */\r
- function updateBallSpeeds() {\r
- balls.forEach(ball => {\r
- const speedCurrent = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);\r
- if (speedCurrent === 0) {\r
- // If speed was zero but now set to non-zero, give random direction\r
- if (ballSpeed === 0) return;\r
- const angle = Math.random() * 2 * Math.PI;\r
- ball.dx = Math.cos(angle) * ballSpeed;\r
- ball.dy = Math.sin(angle) * ballSpeed;\r
- } else {\r
- const scale = ballSpeed / speedCurrent; \r
- ball.dx *= scale;\r
- ball.dy *= scale;\r
- }\r
- });\r
- }\r
-\r
- /**\r
- * Resets the game state:\r
- * - Clears all existing explosions\r
- * - Respawns balls\r
- * - Resets the highest chain reaction display\r
- */\r
- function resetGame() {\r
- expandingCircles = [];\r
- totalExplosions = 0;\r
- totalExplosionsElement.textContent = 0;\r
- resetBalls();\r
- longestCurrentChain.textContent = 0;\r
- }\r
-\r
- /**\r
- * Generates a random color in RGB format.\r
- * @returns {string} A string like "rgb(r, g, b)"\r
- */\r
- function getRandomColor() {\r
- const r = Math.floor(Math.random() * 256);\r
- const g = Math.floor(Math.random() * 256);\r
- const b = Math.floor(Math.random() * 256);\r
- return `rgb(${r}, ${g}, ${b})`;\r
- }\r
-\r
- /**\r
- * Spawns a single ball at a random position within the canvas,\r
- * given the current ballRadius and ballSpeed.\r
- */\r
- function spawnBall() {\r
- const radius = ballRadius;\r
- const x = Math.random() * (canvas.width - radius * 2) + radius;\r
- const y = Math.random() * (canvas.height - radius * 2) + radius;\r
- const angle = Math.random() * 2 * Math.PI;\r
- const dx = Math.cos(angle) * ballSpeed;\r
- const dy = Math.sin(angle) * ballSpeed;\r
- const color = getRandomColor();\r
- balls.push(new Ball(x, y, dx, dy, radius, color));\r
- }\r
-\r
- /**\r
- * Resize the canvas to match the window size.\r
- */\r
- function resizeCanvas() {\r
- canvas.width = window.innerWidth;\r
- canvas.height = window.innerHeight;\r
- }\r
- \r
- function startDrag(event) {\r
- if (interactionMode !== 'drag') return;\r
- isDragging = true;\r
- updateDragPosition(event);\r
- triggerDragExplosion(); // Trigger immediately on click\r
- }\r
-\r
- function handleDrag(event) {\r
- if (interactionMode !== 'drag' || !isDragging) return;\r
- \r
- updateDragPosition(event);\r
- \r
- const now = Date.now();\r
- if (now - lastDragExplosionTime >= explosionDragFreq) {\r
- triggerDragExplosion();\r
- lastDragExplosionTime = now;\r
- }\r
- }\r
-\r
- function endDrag() {\r
- isDragging = false;\r
- }\r
-\r
- function updateDragPosition(event) {\r
- const rect = canvas.getBoundingClientRect();\r
- const scaleX = canvas.width / rect.width;\r
- const scaleY = canvas.height / rect.height;\r
- currentDragPosition.x = (event.clientX - rect.left) * scaleX;\r
- currentDragPosition.y = (event.clientY - rect.top) * scaleY;\r
- }\r
-\r
- function triggerDragExplosion() {\r
- const initialIndices = {\r
- r: 300,\r
- g: 600,\r
- b: 900,\r
- };\r
- expandingCircles.push(new ExpandingCircle(\r
- currentDragPosition.x,\r
- currentDragPosition.y,\r
- 1,\r
- true, // isMouseClickExplosion\r
- initialIndices\r
- ));\r
- }\r
-\r
- /*************************************************************\r
- * CLASSES\r
- *************************************************************/\r
- class Ball {\r
- constructor(x, y, dx, dy, radius, color) {\r
- this.x = x;\r
- this.y = y;\r
- this.dx = dx;\r
- this.dy = dy;\r
- this.radius = radius;\r
- this.color = color;\r
- }\r
-\r
- draw() {\r
- ctx.beginPath();\r
- ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);\r
- ctx.fillStyle = this.color;\r
- ctx.fill();\r
- ctx.closePath();\r
- \r
- ctx.beginPath();\r
- ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);\r
- ctx.strokeStyle = 'black';\r
- ctx.lineWidth = 2;\r
- ctx.stroke();\r
- ctx.closePath();\r
- }\r
-\r
- update(frameFactor) {\r
- if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {\r
- this.dx = -this.dx;\r
- }\r
- if (this.y + this.radius > canvas.height || this.y - this.radius < 0) {\r
- this.dy = -this.dy;\r
- }\r
- this.x += this.dx * frameFactor;\r
- this.y += this.dy * frameFactor;\r
- this.draw();\r
- }\r
- }\r
-\r
- class ExpandingCircle {\r
- constructor(x, y, chainReactionCount = 1, isMouseClickExplosion = false, parentIndices = { r: 100, g: 200, b: 300 }) {\r
- this.x = x;\r
- this.y = y;\r
- this.radius = 0;\r
- this.growing = true;\r
- this.lingerStartTime = null;\r
- this.chainReactionCount = chainReactionCount;\r
- this.isMouseClickExplosion = isMouseClickExplosion;\r
- this.indices = {\r
- r: parentIndices.r + 18,\r
- g: parentIndices.g + 16,\r
- b: parentIndices.b + 14,\r
- };\r
- this.rgb = this.calculateRGB();\r
- totalExplosions++;\r
- }\r
-\r
- calculateRGBNeo() {\r
- const r = Math.floor(100 + 155 * ((Math.sin(this.indices.r * Math.PI / 180) + 1) / 2));\r
- const g = Math.floor(100 + 155 * ((Math.sin(this.indices.g * Math.PI / 180) + 1) / 2));\r
- const b = Math.floor(100 + 155 * ((Math.sin(this.indices.b * Math.PI / 180) + 1) / 2));\r
- return { r, g, b };\r
- }\r
- \r
- calculateRGB() {\r
- const r = 126*(Math.sin(0.02*(this.chainReactionCount+150)*15)+1);\r
- const g = 126*(Math.sin(0.03*(this.chainReactionCount+150)*15)+1);\r
- const b = 126*(Math.sin(0.01*(this.chainReactionCount+150)*15)+1);\r
- return { r, g, b };\r
- }\r
-\r
- draw() {\r
- ctx.beginPath();\r
- ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);\r
- ctx.fillStyle = `rgba(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}, 255)`;\r
- ctx.fill();\r
- ctx.closePath();\r
-\r
- ctx.beginPath();\r
- ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);\r
- ctx.strokeStyle = 'black';\r
- ctx.lineWidth = 4;\r
- ctx.stroke();\r
- ctx.closePath();\r
- }\r
-\r
- update(frameFactor) {\r
- if (this.growing) {\r
- this.radius += explosionSpeed * frameFactor;\r
- if (this.radius >= maxCircleSize) {\r
- this.growing = false;\r
- this.lingerStartTime = Date.now();\r
- }\r
- }\r
- this.draw();\r
- }\r
-\r
- isColliding(ball) {\r
- const dx = this.x - ball.x;\r
- const dy = this.y - ball.y;\r
- const distance = Math.sqrt(dx * dx + dy * dy);\r
- return distance < this.radius + ball.radius;\r
- }\r
-\r
- shouldDisappear() {\r
- if (!this.lingerStartTime) return false;\r
- const elapsedTime = (Date.now() - this.lingerStartTime) / 1000;\r
- return elapsedTime >= explosionLingerTime;\r
- }\r
- }\r
-\r
- /*************************************************************\r
- * MAIN GAME LOOP FUNCTIONS\r
- *************************************************************/\r
- function updateGame(frameFactor) {\r
- balls.forEach(ball => {\r
- ball.update(frameFactor);\r
- });\r
- \r
- // Auto-start reaction check\r
- if (autoStartCheckbox.checked && expandingCircles.length === 0) {\r
- const initialIndices = { r: 300, g: 600, b: 900 };\r
- const x = Math.random() * canvas.width;\r
- const y = Math.random() * canvas.height;\r
- expandingCircles.push(new ExpandingCircle(x, y, 1, true, initialIndices));\r
- }\r
-\r
- const explosionsToRemove = [];\r
- let longestCurrentChainCount = 0;\r
-\r
- expandingCircles.forEach((circle, circleIndex) => {\r
- circle.update(frameFactor);\r
-\r
- balls.forEach((ball, ballIndex) => {\r
- if (circle.isColliding(ball)) {\r
- const newIndices = {\r
- r: circle.indices.r + 9,\r
- g: circle.indices.g + 8,\r
- b: circle.indices.b + 7,\r
- };\r
- expandingCircles.push(\r
- new ExpandingCircle(\r
- ball.x,\r
- ball.y,\r
- circle.chainReactionCount + 1,\r
- false,\r
- newIndices\r
- )\r
- );\r
- balls.splice(ballIndex, 1);\r
- }\r
- });\r
-\r
- if (circle.chainReactionCount > longestCurrentChainCount) {\r
- longestCurrentChainCount = circle.chainReactionCount;\r
- }\r
-\r
- if (!circle.growing && circle.shouldDisappear()) {\r
- explosionsToRemove.push(circleIndex);\r
- }\r
- });\r
-\r
- for (let i = explosionsToRemove.length - 1; i >= 0; i--) {\r
- const circleIndex = explosionsToRemove[i];\r
- const circle = expandingCircles[circleIndex];\r
- if (!circle.isMouseClickExplosion && ballAfterExplosionCheckbox.checked) {\r
- spawnBall();\r
- }\r
- expandingCircles.splice(circleIndex, 1);\r
- }\r
- \r
- totalExplosionsElement.textContent = totalExplosions;\r
- activeExplosions.textContent = expandingCircles.length;\r
- longestCurrentChain.textContent = longestCurrentChainCount;\r
- }\r
-\r
- function renderGame() {\r
- ctx.clearRect(0, 0, canvas.width, canvas.height);\r
- balls.forEach(ball => ball.draw());\r
- expandingCircles.forEach(circle => circle.draw());\r
- \r
- // Draw stats on canvas if checkboxes are checked\r
- const activeExplosionsCheckbox = document.getElementById('activeExplosionsCheckbox');\r
- const longestChainCheckbox = document.getElementById('longestChainCheckbox');\r
- \r
- ctx.fillStyle = 'white';\r
- ctx.font = '16px Arial';\r
- const padding = 20;\r
- let yPos = padding;\r
-\r
- if (activeExplosionsCheckbox.checked) {\r
- ctx.fillText(`Active Explosions: ${document.getElementById('activeExplosions').textContent}`, padding, yPos);\r
- yPos += 24;\r
- }\r
- if (longestChainCheckbox.checked) {\r
- ctx.fillText(`Longest Current Chain: ${document.getElementById('longestCurrentChain').textContent}`, padding, yPos);\r
- yPos += 24;\r
- }\r
- \r
- if (document.getElementById('totalExplosionsCheckbox').checked) {\r
- ctx.fillText(`Total Explosions: ${totalExplosions}`, padding, yPos);\r
- yPos += 24;\r
-}\r
- }\r
-\r
- function animate(timestamp) {\r
- requestAnimationFrame(animate);\r
-\r
- if (!lastFrameTime) {\r
- lastFrameTime = timestamp;\r
- }\r
-\r
- const dt = timestamp - lastFrameTime;\r
- lastFrameTime = timestamp;\r
- const frameFactor = dt / (1000 / 60);\r
-\r
- updateGame(frameFactor);\r
- renderGame();\r
- }\r
-\r
- /*************************************************************\r
- * EVENT LISTENERS\r
- *************************************************************/\r
- canvas.addEventListener('click', (event) => {\r
- if (interactionMode !== 'click') return;\r
- \r
- const rect = canvas.getBoundingClientRect();\r
- const scaleX = canvas.width / rect.width;\r
- const scaleY = canvas.height / rect.height;\r
- const x = (event.clientX - rect.left) * scaleX;\r
- const y = (event.clientY - rect.top) * scaleY;\r
-\r
- if (y < canvas.height) {\r
- const initialIndices = {\r
- r: 300,\r
- g: 600,\r
- b: 900,\r
- };\r
- expandingCircles.push(new ExpandingCircle(x, y, 1, true, initialIndices));\r
- }\r
- });\r
-\r
- window.addEventListener('resize', resizeCanvas);\r
- \r
- \r
- \r
- canvas.addEventListener('touchstart', handleTouchStart);\r
-canvas.addEventListener('touchmove', handleTouchMove);\r
-canvas.addEventListener('touchend', handleTouchEnd);\r
-canvas.addEventListener('touchcancel', handleTouchEnd);\r
-\r
- \r
- document.body.addEventListener('touchstart', function(event) {\r
- if (event.target === canvas) {\r
- event.preventDefault();\r
- }\r
-}, { passive: false });\r
-\r
-document.body.addEventListener('touchmove', function(event) {\r
- if (event.target === canvas) {\r
- event.preventDefault();\r
- }\r
-}, { passive: false });\r
-\r
-function handleTouchStart(event) {\r
- event.preventDefault(); // Prevent default touch behavior (e.g., scrolling)\r
- // if (interactionMode !== 'drag') return;\r
-\r
- const touches = event.touches;\r
- for (let i = 0; i < touches.length; i++) {\r
- const touch = touches[i];\r
- const rect = canvas.getBoundingClientRect();\r
- const scaleX = canvas.width / rect.width;\r
- const scaleY = canvas.height / rect.height;\r
- const x = (touch.clientX - rect.left) * scaleX;\r
- const y = (touch.clientY - rect.top) * scaleY;\r
-\r
- const initialIndices = {\r
- r: 300,\r
- g: 600,\r
- b: 900,\r
- };\r
- expandingCircles.push(new ExpandingCircle(x, y, 1, true, initialIndices));\r
- }\r
-}\r
-\r
-function handleTouchMove(event) {\r
- event.preventDefault(); // Prevent default touch behavior (e.g., scrolling)\r
- if (interactionMode !== 'drag') return;\r
-\r
- const touches = event.touches;\r
- for (let i = 0; i < touches.length; i++) {\r
- const touch = touches[i];\r
- const rect = canvas.getBoundingClientRect();\r
- const scaleX = canvas.width / rect.width;\r
- const scaleY = canvas.height / rect.height;\r
- const x = (touch.clientX - rect.left) * scaleX;\r
- const y = (touch.clientY - rect.top) * scaleY;\r
-\r
- const initialIndices = {\r
- r: 300,\r
- g: 600,\r
- b: 900,\r
- };\r
- expandingCircles.push(new ExpandingCircle(x, y, 1, true, initialIndices));\r
- }\r
-}\r
-\r
-function handleTouchEnd(event) {\r
- // You can add cleanup logic here if needed\r
-}\r
- \r
-\r
- /*************************************************************\r
- * START THE GAME\r
- *************************************************************/\r
- resizeCanvas();\r
- init();\r
- requestAnimationFrame(animate);\r
- </script>\r
-</body>\r
-</html>\r
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Circles Of Doom with Music by StarStabbedMoon</title>
+ <style>
+ /* Basic styling to fill the screen and show a black background */
+ body, html {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ height: 100%;
+ }
+ canvas {
+ display: block;
+ background: black;
+ width: 100%;
+ height: 100%;
+ }
+ /* UI container styling */
+ #ui {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ background: rgba(255, 255, 255, 0);
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ transition: transform 0.3s ease-in-out;
+ transform: translateY(100%); /* Hidden by default */
+ pointer-events: none; /* Makes UI transparent to clicks */
+ }
+
+ #ui * {
+ pointer-events: auto; /* Re-enable clicks for child elements */
+ }
+ /* When we add the class "visible", the UI slides up */
+ #ui.visible {
+ transform: translateY(0);
+ }
+ /* Container for sliders and labels */
+ .slider-container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-left: 10px;
+ color: white;
+ width: auto;
+ pointer-events: none;
+ }
+ .slider-container > * {
+ pointer-events: auto;
+ }
+ label {
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ min-width: 150px; /* Ensure labels have consistent width */
+ color: white;
+ flex-shrink: 0;
+ }
+ input[type="range"] {
+ flex: 1; /* Take available space */
+ max-width: 300px; /* Maximum width for larger screens */
+ min-width: 100px; /* Minimum usable width */
+ }
+
+ /* Mobile adjustments */
+ @media (max-width: 600px) {
+ .slider-container {
+ margin-left: 5px;
+ gap: 8px;
+ }
+ label {
+ min-width: 110px; /* Smaller min-width for mobile */
+ font-size: 13px;
+ }
+ input[type="range"] {
+ max-width: 100%; /* Allow full width of container */
+ }
+ #info, #options, #resetButton, #restoreDefaultsButton {
+ margin-left: 5px;
+ }
+ }
+
+ /* Information text styling */
+ #info {
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ margin-top: 10px;
+ text-align: left;
+ width: auto;
+ margin-left: 10px;
+ color: white;
+ }
+
+ /* Options text styling */
+ #options {
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ margin-top: 10px;
+ text-align: left;
+ width: auto;
+ margin-left: 10px;
+ color: white;
+ }
+
+ .button-container {
+ display: flex;
+ gap: 10px;
+ margin-left: 20px;
+ margin-top: 10px;
+ }
+
+ /* Button styling */
+ #resetButton,
+ #restoreDefaultsButton {
+ padding: 5px 10px;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ cursor: pointer;
+ color: white;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ background: rgba(0, 0, 0, 255);
+ }
+
+ /* Style for the audio toggle button */
+ #audio-toggle {
+ position: fixed;
+ top: 16px;
+ left: 16px;
+ width: 48px;
+ height: 48px;
+ background: rgba(255, 255, 255, 0.2);
+ border: 2px solid rgba(255, 255, 255, 0.5);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 1000;
+ transition: background 0.3s, border 0.3s;
+ }
+
+ /* Button to toggle the UI visibility */
+ #toggleUIButton {
+ position: fixed;
+ bottom: 10px;
+ right: 10px;
+ padding: 10px;
+ background: rgba(0, 0, 0, 255);
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ }
+
+ .music-credits {
+ font-family: Arial, sans-serif;
+ font-size: 12px;
+ color: white;
+ margin-left: 20px;
+ margin-top: 10px;
+ opacity: 0.8;
+ text-align: center;
+ }
+
+ .music-credits a {
+ color: #ff8888;
+ text-decoration: none;
+ margin: 0 5px;
+ }
+
+ .music-credits a:hover {
+ text-decoration: underline;
+ }
+
+ #fullscreenButton {
+ position: fixed;
+ bottom: 60px;
+ right: 10px;
+ padding: 10px;
+ background: rgba(0, 0, 0, 255);
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ z-index: 1000;
+ }
+
+ /* Mode toggle styling */
+ .mode-toggle {
+ margin-top: 10px;
+ }
+
+ .toggle-container {
+ display: flex;
+ gap: 5px;
+ margin-left: 10px;
+ }
+
+ .mode-button {
+ padding: 5px 12px;
+ border: none;
+ border-radius: 15px;
+ cursor: pointer;
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ transition: all 0.2s ease;
+ }
+
+ .mode-button.active {
+ background: #ff4444;
+ color: white;
+ box-shadow: 0 0 5px rgba(255, 68, 68, 0.5);
+ }
+
+ /* Modern slider styling with persistent red color */
+ input[type="range"] {
+ accent-color: #ff0000;
+ background: transparent !important;
+ border: none !important;
+ box-shadow: none !important;
+ }
+
+ /* Chrome/Safari track */
+ input[type="range"]::-webkit-slider-runnable-track {
+ background: rgba(255, 255, 255, 0.2);
+ height: 8px;
+ border-radius: 5px;
+ border: 1px solid #000000ff;
+ box-sizing: border-box;
+ }
+
+ /* Chrome/Safari thumb */
+ input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ background: #ff0000;
+ border-radius: 50%;
+ margin-top: -4px;
+ box-shadow: 0 0 0 1.5px black; /* Simulated border using box-shadow */
+ position: relative;
+ z-index: 1;
+ }
+
+ /* Firefox Track */
+ input[type="range"]::-moz-range-track {
+ background: #ffffff;
+ height: 8px;
+ border: 1px solid #000000ff;
+ border-radius: 5px;
+ box-shadow: none;
+ }
+
+ /* Firefox Progress (filled portion) */
+ input[type="range"]::-moz-range-progress {
+ background: #ff0000;
+ height: 8px;
+ border-radius: 5px 0 0 5px;
+ border: none;
+ }
+
+ /* Firefox Thumb */
+ input[type="range"]::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+ background: #ff0000 !important;
+ border: 2px solid black;
+ border-radius: 50%;
+ box-shadow: none;
+ position: relative;
+ z-index: 1;
+ }
+
+ /* Remove focus ring */
+ input[type="range"]:focus {
+ outline: none;
+ }
+
+ .stat {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ }
+
+ .stat-checkbox {
+ margin: 0;
+ accent-color: #ff4444; /* Red color for checkboxes */
+ }
+ </style>
+</head>
+<body>
+ <div id="audio-toggle"><span id="musicButton">🔊</span></div>
+ <!-- Main game canvas -->
+ <canvas id="gameCanvas"></canvas>
+
+ <!-- UI with sliders and info -->
+ <div id="ui">
+ <div class="slider-container">
+ <label for="ballCountSlider">Number of Balls:</label>
+ <input type="range" id="ballCountSlider" min="1" max="500" value="60" autocomplete="off">
+ <span id="ballCountValue">60</span>
+ </div>
+ <div class="slider-container">
+ <label for="ballSpeedSlider">Ball Speed:</label>
+ <input type="range" id="ballSpeedSlider" min="0" max="10" step="0.25" value="2" autocomplete="off">
+ <span id="ballSpeedValue">2</span>
+ </div>
+ <div class="slider-container">
+ <label for="ballRadiusSlider">Ball Radius:</label>
+ <input type="range" id="ballRadiusSlider" min="1" max="20" value="5" autocomplete="off">
+ <span id="ballRadiusValue">5</span>
+ </div>
+ <div class="slider-container">
+ <label for="explosionRadiusSlider">Explosion Radius:</label>
+ <input type="range" id="explosionRadiusSlider" min="10" max="200" value="60" autocomplete="off">
+ <span id="explosionRadiusValue">60</span>
+ </div>
+ <div class="slider-container">
+ <label for="explosionSpeedSlider">Explosion Speed:</label>
+ <input type="range" id="explosionSpeedSlider" min="0.25" max="4" step="0.25" value="1.0" autocomplete="off">
+ <span id="explosionSpeedValue">1.0</span>
+ </div>
+ <div class="slider-container">
+ <label for="explosionLingerSlider">Explosion Linger Time:</label>
+ <input type="range" id="explosionLingerSlider" min="0" max="4" step="0.1" value="0" autocomplete="off">
+ <span id="explosionLingerValue">0</span>
+ </div>
+ <div class="slider-container">
+ <label for="explosionDragFreqSlider">Explosion Drag Freq:</label>
+ <input type="range" id="explosionDragFreqSlider" min="10" max="1000" value="100" autocomplete="off">
+ <span id="explosionDragFreqValue">10</span>
+ </div>
+ <div id="options">
+ <div class="stat">
+ <input type="checkbox" class="stat-checkbox" id="autoStartCheckbox">
+ <label for="autoStartCheckbox">Auto Start Reaction</label>
+ </div>
+ <div class="stat">
+ <input type="checkbox" class="stat-checkbox" id="ballAfterExplosionCheckbox" checked>
+ <label for="ballAfterExplosionCheckbox">Ball After Explosion</label>
+ </div>
+ </div>
+ <div id="info">
+ <div class="stat">
+ <input type="checkbox" class="stat-checkbox" id="activeExplosionsCheckbox">
+ <label for="activeExplosionsCheckbox">Active Explosions:</label>
+ <span id="activeExplosions">0</span>
+ </div>
+ <div class="stat">
+ <input type="checkbox" class="stat-checkbox" id="longestChainCheckbox">
+ <label for="longestChainCheckbox">Longest Current Chain:</label>
+ <span id="longestCurrentChain">0</span>
+ </div>
+ <div class="stat">
+ <input type="checkbox" class="stat-checkbox" id="totalExplosionsCheckbox">
+ <label for="totalExplosionsCheckbox">Total Explosions:</label>
+ <span id="totalExplosions">0</span>
+ </div>
+ </div>
+ <div class="button-container">
+ <button id="resetButton">Reset</button>
+ <button id="restoreDefaultsButton">Restore Defaults</button>
+ </div>
+ <div class="slider-container mode-toggle">
+ <label>Mode:</label>
+ <div class="toggle-container">
+ <button class="mode-button active" data-mode="click">Click</button>
+ <button class="mode-button" data-mode="drag">Drag</button>
+ </div>
+ </div>
+ <div class="music-credits">
+ Music by StarStabbedMoon:<br>
+ <a href="mailto:StarStabbedMoon@gmail.com">Email</a>
+ <a href="https://starstabbedmoon.bandcamp.com">Bandcamp</a><br>
+ <a href="https://www.youtube.com/starstabbedmoon">YouTube</a>
+ <a href="https://open.spotify.com/artist/71cCMapE4464Npz4TnTAT8">Spotify</a>
+ <a href="https://www.soundclick.com/starstabbedmoon">SoundClick</a>
+ </div>
+ </div>
+
+ <!-- Button to show or hide the UI -->
+ <button id="toggleUIButton">Show Controls</button>
+ <button id="fullscreenButton">Full Screen</button>
+
+ <audio id="bgMusic" loop>
+ <source src="Celestial Assault.mp3" type="audio/mpeg">
+ Your browser does not support the audio element.
+ </audio>
+
+ <script>
+ /*************************************************************
+ * GLOBAL VARIABLES & INITIAL SETUP
+ *************************************************************/
+ const canvas = document.getElementById('gameCanvas');
+ const ctx = canvas.getContext('2d');
+
+ // Arrays to hold balls and explosions
+ let balls = [];
+ let expandingCircles = [];
+
+ // Default values for game parameters
+ let maxCircleSize = 60; // Explosion radius
+ let ballCount = 60; // Number of balls
+ let ballRadius = 6; // Radius of each ball
+ let ballSpeed = 2; // Speed of each ball (base value)
+ let explosionSpeed = 1.0; // Expansion speed of explosion
+ let explosionLingerTime = 0; // Seconds to linger after reaching max size
+ let explosionDragFreq = 10; // Milliseconds between explosions during drag
+ let isDragging = false;
+ let lastDragExplosionTime = 0;
+ let currentDragPosition = { x: 0, y: 0 };
+ let mode = 'drag';
+ let muted = false;
+ let totalExplosions = 0;
+
+ // Track the time of the previous frame for consistent animation
+ let lastFrameTime = 0;
+
+ /*************************************************************
+ * UI ELEMENTS
+ *************************************************************/
+ // Sliders
+ const ballCountSlider = document.getElementById('ballCountSlider');
+ const ballCountValue = document.getElementById('ballCountValue');
+ const ballSpeedSlider = document.getElementById('ballSpeedSlider');
+ const ballSpeedValue = document.getElementById('ballSpeedValue');
+ const ballRadiusSlider = document.getElementById('ballRadiusSlider');
+ const ballRadiusValue = document.getElementById('ballRadiusValue');
+ const explosionRadiusSlider = document.getElementById('explosionRadiusSlider');
+ const explosionRadiusValue = document.getElementById('explosionRadiusValue');
+ const explosionSpeedSlider = document.getElementById('explosionSpeedSlider');
+ const explosionSpeedValue = document.getElementById('explosionSpeedValue');
+ const explosionLingerSlider = document.getElementById('explosionLingerSlider');
+ const explosionLingerValue = document.getElementById('explosionLingerValue');
+ const explosionDragFreqSlider = document.getElementById('explosionDragFreqSlider');
+ const explosionDragFreqValue = document.getElementById('explosionDragFreqValue');
+
+ // Stats
+ const activeExplosions = document.getElementById('activeExplosions');
+ const longestCurrentChain = document.getElementById('longestCurrentChain');
+ const totalExplosionsElement = document.getElementById('totalExplosions');
+
+ // Checkboxes
+ const activeExplosionsCheckbox = document.getElementById('activeExplosionsCheckbox');
+ const longestChainCheckbox = document.getElementById('longestChainCheckbox');
+ const totalExplosionsCheckbox = document.getElementById('totalExplosionsCheckbox');
+ const autoStartCheckbox = document.getElementById('autoStartCheckbox');
+ const ballAfterExplosionCheckbox = document.getElementById('ballAfterExplosionCheckbox');
+
+ // Other UI elements
+ const resetButton = document.getElementById('resetButton');
+ const restoreDefaultsButton = document.getElementById('restoreDefaultsButton');
+ const toggleUIButton = document.getElementById('toggleUIButton');
+ const fullscreenButton = document.getElementById('fullscreenButton');
+ const bgMusic = document.getElementById('bgMusic');
+ const musicButton = document.getElementById('musicButton');
+ const ui = document.getElementById('ui');
+
+ /*************************************************************
+ * CLASSES
+ *************************************************************/
+ class Ball {
+ constructor(x, y, dx, dy, radius, color) {
+ this.x = x;
+ this.y = y;
+ this.dx = dx;
+ this.dy = dy;
+ this.radius = radius;
+ this.color = color;
+ }
+
+ draw() {
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
+ ctx.fillStyle = this.color;
+ ctx.fill();
+ ctx.closePath();
+
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
+ ctx.strokeStyle = 'black';
+ ctx.lineWidth = 2;
+ ctx.stroke();
+ ctx.closePath();
+ }
+
+ update(frameFactor) {
+ // Handle boundary collisions
+ if (this.x + this.radius > canvas.width) {
+ this.x = canvas.width - this.radius;
+ this.dx = -Math.abs(this.dx);
+ }
+ if (this.x - this.radius < 0) {
+ this.x = this.radius;
+ this.dx = Math.abs(this.dx);
+ }
+ if (this.y + this.radius > canvas.height) {
+ this.y = canvas.height - this.radius;
+ this.dy = -Math.abs(this.dy);
+ }
+ if (this.y - this.radius < 0) {
+ this.y = this.radius;
+ this.dy = Math.abs(this.dy);
+ }
+
+ // Update position
+ this.x += this.dx * frameFactor;
+ this.y += this.dy * frameFactor;
+ this.draw();
+ }
+ }
+
+ class ExpandingCircle {
+ constructor(x, y, chainReactionCount = 1, isMouseClickExplosion = false, parentIndices = { r: 100, g: 200, b: 300 }) {
+ this.x = x;
+ this.y = y;
+ this.radius = 0;
+ this.growing = true;
+ this.lingerStartTime = null;
+ this.chainReactionCount = chainReactionCount;
+ this.isMouseClickExplosion = isMouseClickExplosion;
+ this.indices = {
+ r: parentIndices.r + 18,
+ g: parentIndices.g + 16,
+ b: parentIndices.b + 14,
+ };
+ this.rgb = this.calculateRGB();
+ totalExplosions++;
+ }
+
+ calculateRGB() {
+ const r = 126 * (Math.sin(0.02 * (this.chainReactionCount + 150) * 15) + 1);
+ const g = 126 * (Math.sin(0.03 * (this.chainReactionCount + 150) * 15) + 1);
+ const b = 126 * (Math.sin(0.01 * (this.chainReactionCount + 150) * 15) + 1);
+ return { r, g, b };
+ }
+
+ draw() {
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
+ ctx.fillStyle = `rgba(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}, 255)`;
+ ctx.fill();
+ ctx.closePath();
+
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
+ ctx.strokeStyle = 'black';
+ ctx.lineWidth = 4;
+ ctx.stroke();
+ ctx.closePath();
+ }
+
+ update(frameFactor) {
+ if (this.growing) {
+ this.radius += explosionSpeed * frameFactor;
+ if (this.radius >= maxCircleSize) {
+ this.growing = false;
+ this.lingerStartTime = Date.now();
+ }
+ }
+ this.draw();
+ }
+
+ isColliding(ball) {
+ const dx = this.x - ball.x;
+ const dy = this.y - ball.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+ return distance < this.radius + ball.radius;
+ }
+
+ shouldDisappear() {
+ if (!this.lingerStartTime) return false;
+ const elapsedTime = (Date.now() - this.lingerStartTime) / 1000;
+ return elapsedTime >= explosionLingerTime;
+ }
+ }
+
+ /*************************************************************
+ * HELPER FUNCTIONS
+ *************************************************************/
+ function getProportionalBallCount() {
+ const screenArea = window.innerWidth * window.innerHeight;
+ const baseArea = 1920 * 1080; // Reference area for default 60 balls
+ let proportionalBallCount = (screenArea / baseArea) * 350;
+ proportionalBallCount = Math.round(proportionalBallCount / 10) * 10; // Round to nearest 10
+ proportionalBallCount = Math.min(proportionalBallCount, 500); // Clamp to slider max
+ proportionalBallCount = Math.max(proportionalBallCount, 10); // Minimum 10 balls
+ return proportionalBallCount;
+ }
+
+ function getRandomColor() {
+ const r = Math.floor(Math.random() * 256);
+ const g = Math.floor(Math.random() * 256);
+ const b = Math.floor(Math.random() * 256);
+ return `rgb(${r}, ${g}, ${b})`;
+ }
+
+ function spawnBall() {
+ const radius = ballRadius;
+ const x = Math.random() * (canvas.width - (2 * radius)) + radius;
+ const y = Math.random() * (canvas.height - (2 * radius)) + radius;
+ const angle = Math.random() * 2 * Math.PI;
+ const dx = Math.cos(angle) * ballSpeed;
+ const dy = Math.sin(angle) * ballSpeed;
+ const color = getRandomColor();
+ balls.push(new Ball(x, y, dx, dy, radius, color));
+ }
+
+ function createExplosion(x, y, chainReactionCount = 1, isMouseClickExplosion = false) {
+ const initialIndices = {
+ r: 300,
+ g: 600,
+ b: 900,
+ };
+ expandingCircles.push(new ExpandingCircle(
+ x,
+ y,
+ chainReactionCount,
+ isMouseClickExplosion,
+ initialIndices
+ ));
+ }
+
+ /*************************************************************
+ * SETTINGS MANAGEMENT
+ *************************************************************/
+ function loadSettings() {
+ // Sliders
+ const sliders = [
+ { element: ballCountSlider, key: 'ballCount', defaultValue: getProportionalBallCount() },
+ { element: ballSpeedSlider, key: 'ballSpeed', defaultValue: 2 },
+ { element: ballRadiusSlider, key: 'ballRadius', defaultValue: 6 },
+ { element: explosionRadiusSlider, key: 'explosionRadius', defaultValue: 60 },
+ { element: explosionSpeedSlider, key: 'explosionSpeed', defaultValue: 1.0 },
+ { element: explosionLingerSlider, key: 'explosionLinger', defaultValue: 0 },
+ { element: explosionDragFreqSlider, key: 'explosionDragFreq', defaultValue: 10 }
+ ];
+
+ sliders.forEach(({ element, key, defaultValue }) => {
+ const savedValue = localStorage.getItem(key);
+ element.value = savedValue != null ? savedValue : defaultValue;
+ element.dispatchEvent(new Event('input'));
+ });
+
+ // Checkboxes
+ const checkboxes = [
+ { element: autoStartCheckbox, key: 'autoStart', defaultValue: false },
+ { element: ballAfterExplosionCheckbox, key: 'ballAfterExplosion', defaultValue: true },
+ { element: activeExplosionsCheckbox, key: 'activeExplosionsCheckbox', defaultValue: false },
+ { element: longestChainCheckbox, key: 'longestChainCheckbox', defaultValue: false },
+ { element: totalExplosionsCheckbox, key: 'totalExplosionsCheckbox', defaultValue: false }
+ ];
+
+ checkboxes.forEach(({ element, key, defaultValue }) => {
+ const savedValue = localStorage.getItem(key);
+ element.checked = savedValue != null ? savedValue === 'true' : defaultValue;
+ element.dispatchEvent(new Event('change'));
+ });
+
+ // Mode
+ const savedMode = localStorage.getItem('mode');
+ if (savedMode) {
+ mode = savedMode;
+ }
+ document.querySelectorAll('.mode-button').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.mode === mode);
+ });
+
+ // UI Visibility
+ const uiVisible = localStorage.getItem('uiVisible') === 'true';
+ ui.classList.toggle('visible', uiVisible);
+ toggleUIButton.textContent = uiVisible ? 'Hide Controls' : 'Show Controls';
+
+ // Music Mute State
+ muted = localStorage.getItem('muted') === 'true';
+ musicButton.textContent = muted ? '🔇' : '🔊';
+
+ // Set parameters from UI elements
+ updateGameParameters();
+ }
+
+ function saveSetting(key, value) {
+ localStorage.setItem(key, value);
+ }
+
+ function updateGameParameters() {
+ maxCircleSize = parseInt(explosionRadiusSlider.value);
+ ballCount = parseInt(ballCountSlider.value);
+ ballRadius = parseInt(ballRadiusSlider.value);
+ ballSpeed = parseFloat(ballSpeedSlider.value);
+ explosionSpeed = parseFloat(explosionSpeedSlider.value);
+ explosionLingerTime = parseFloat(explosionLingerSlider.value);
+ explosionDragFreq = parseInt(explosionDragFreqSlider.value);
+
+ // Update displayed values
+ ballCountValue.textContent = ballCount;
+ ballSpeedValue.textContent = ballSpeed;
+ ballRadiusValue.textContent = ballRadius;
+ explosionRadiusValue.textContent = maxCircleSize;
+ explosionSpeedValue.textContent = explosionSpeed;
+ explosionLingerValue.textContent = explosionLingerTime;
+ explosionDragFreqValue.textContent = explosionDragFreq;
+ }
+
+ function restoreDefaultSettings() {
+ // Sliders
+ const sliders = [
+ { element: ballCountSlider, value: getProportionalBallCount() },
+ { element: ballSpeedSlider, value: 2 },
+ { element: ballRadiusSlider, value: 6 },
+ { element: explosionRadiusSlider, value: 60 },
+ { element: explosionSpeedSlider, value: 1.0 },
+ { element: explosionLingerSlider, value: 0 },
+ { element: explosionDragFreqSlider, value: 10 }
+ ];
+
+ sliders.forEach(({ element, value }) => {
+ element.value = value;
+ element.dispatchEvent(new Event('input'));
+ });
+
+ // Checkboxes
+ const checkboxes = [
+ { element: autoStartCheckbox, value: false },
+ { element: ballAfterExplosionCheckbox, value: true },
+ { element: activeExplosionsCheckbox, value: false },
+ { element: longestChainCheckbox, value: false },
+ { element: totalExplosionsCheckbox, value: false }
+ ];
+
+ checkboxes.forEach(({ element, value }) => {
+ element.checked = value;
+ element.dispatchEvent(new Event('change'));
+ });
+
+ // Mode
+ mode = 'drag';
+ document.querySelectorAll('.mode-button').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.mode === 'drag');
+ });
+ saveSetting('mode', 'drag');
+
+ resetGame();
+ }
+
+ /*************************************************************
+ * GAME MANAGEMENT FUNCTIONS
+ *************************************************************/
+ function resetBalls() {
+ balls = [];
+ for (let i = 0; i < ballCount; i++) {
+ spawnBall();
+ }
+ }
+
+ function updateBallSpeeds() {
+ balls.forEach(ball => {
+ const speedCurrent = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
+ if (speedCurrent === 0) {
+ // If speed was zero but now set to non-zero, give random direction
+ if (ballSpeed === 0) return;
+ const angle = Math.random() * 2 * Math.PI;
+ ball.dx = Math.cos(angle) * ballSpeed;
+ ball.dy = Math.sin(angle) * ballSpeed;
+ } else {
+ const scale = ballSpeed / speedCurrent;
+ ball.dx *= scale;
+ ball.dy *= scale;
+ }
+ });
+ }
+
+ function resetGame() {
+ expandingCircles = [];
+ totalExplosions = 0;
+ totalExplosionsElement.textContent = '0';
+ longestCurrentChain.textContent = '0';
+ resetBalls();
+ }
+
+ function resizeCanvas() {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+
+ // relocate all balls to be within the screen bounds
+ balls.forEach(ball => {
+ if(ball.x-ball.radius < 0) {
+ ball.x = Math.min(ball.radius, canvas.width);
+ }
+ if(ball.y-ball.radius < 0) {
+ ball.y = Math.min(ball.radius, canvas.height);
+ }
+ if(ball.x+ball.radius >= canvas.width) {
+ ball.x = Math.max(canvas.width - ball.radius, 0);
+ }
+ if(ball.y+ball.radius >= canvas.height) {
+ ball.y = Math.max(canvas.height - ball.radius, 0);
+ }
+ });
+ }
+
+ function toggleFullScreen() {
+ if (!document.fullscreenElement) {
+ if (canvas.requestFullscreen) {
+ canvas.requestFullscreen();
+ } else if (canvas.webkitRequestFullscreen) {
+ canvas.webkitRequestFullscreen();
+ } else if (canvas.msRequestFullscreen) {
+ canvas.msRequestFullscreen();
+ }
+ fullscreenButton.textContent = 'Exit Full Screen';
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen();
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen();
+ }
+ fullscreenButton.textContent = 'Full Screen';
+ }
+ }
+
+ /*************************************************************
+ * INTERACTION FUNCTIONS
+ *************************************************************/
+ function handleFirstClick(event) {
+ if (event.target === musicButton) {
+ return;
+ }
+
+ if (!muted) {
+ bgMusic.play();
+ }
+
+ canvas.removeEventListener('click', handleFirstClick);
+ canvas.removeEventListener('touchstart', handleFirstClick);
+ }
+
+ function startDrag(event) {
+ if (mode !== 'drag') return;
+ isDragging = true;
+ updateDragPosition(event);
+ triggerDragExplosion(); // Trigger immediately on click
+ }
+
+ function handleDrag(event) {
+ if (mode !== 'drag' || !isDragging) return;
+ updateDragPosition(event);
+
+ const now = Date.now();
+ if (now - lastDragExplosionTime >= explosionDragFreq) {
+ triggerDragExplosion();
+ lastDragExplosionTime = now;
+ }
+ }
+
+ function endDrag() {
+ isDragging = false;
+ }
+
+ function updateDragPosition(event) {
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+ currentDragPosition.x = (event.clientX - rect.left) * scaleX;
+ currentDragPosition.y = (event.clientY - rect.top) * scaleY;
+ }
+
+ function triggerDragExplosion() {
+ createExplosion(currentDragPosition.x, currentDragPosition.y, 1, true);
+ }
+
+ function handleTouchStart(event) {
+ event.preventDefault();
+ const touches = event.touches;
+ for (let i = 0; i < touches.length; i++) {
+ const touch = touches[i];
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+ const x = (touch.clientX - rect.left) * scaleX;
+ const y = (touch.clientY - rect.top) * scaleY;
+
+ createExplosion(x, y, 1, true);
+ }
+ }
+
+ function handleTouchMove(event) {
+ event.preventDefault();
+ if (mode !== 'drag') return;
+
+ const touches = event.touches;
+ for (let i = 0; i < touches.length; i++) {
+ const touch = touches[i];
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+ const x = (touch.clientX - rect.left) * scaleX;
+ const y = (touch.clientY - rect.top) * scaleY;
+
+ createExplosion(x, y, 1, true);
+ }
+ }
+
+ function handleTouchEnd() {
+ // Placeholder for future touch end handling
+ }
+
+ /*************************************************************
+ * GAME LOOP FUNCTIONS
+ *************************************************************/
+ function updateGame(frameFactor) {
+ // Update all balls
+ balls.forEach(ball => {
+ ball.update(frameFactor);
+ });
+
+ // Auto-start reaction check
+ if (autoStartCheckbox.checked && expandingCircles.length === 0) {
+ const x = Math.random() * canvas.width;
+ const y = Math.random() * canvas.height;
+ createExplosion(x, y, 1, true);
+ }
+
+ const explosionsToRemove = [];
+ let longestCurrentChainCount = 0;
+
+ expandingCircles.forEach((circle, circleIndex) => {
+ circle.update(frameFactor);
+
+ // Check for collision with balls
+ balls.forEach((ball, ballIndex) => {
+ if (circle.isColliding(ball)) {
+ // Create new explosion at ball position
+ const newIndices = {
+ r: circle.indices.r + 9,
+ g: circle.indices.g + 8,
+ b: circle.indices.b + 7,
+ };
+ expandingCircles.push(
+ new ExpandingCircle(
+ ball.x,
+ ball.y,
+ circle.chainReactionCount + 1,
+ false,
+ newIndices
+ )
+ );
+ // Remove the ball
+ balls.splice(ballIndex, 1);
+ }
+ });
+
+ // Track longest chain
+ if (circle.chainReactionCount > longestCurrentChainCount) {
+ longestCurrentChainCount = circle.chainReactionCount;
+ }
+
+ // Check if explosion should be removed
+ if (!circle.growing && circle.shouldDisappear()) {
+ explosionsToRemove.push(circleIndex);
+ }
+ });
+
+ // Remove finished explosions and spawn new balls if needed
+ for (let i = explosionsToRemove.length - 1; i >= 0; i--) {
+ const circleIndex = explosionsToRemove[i];
+ const circle = expandingCircles[circleIndex];
+ if (!circle.isMouseClickExplosion && ballAfterExplosionCheckbox.checked) {
+ spawnBall();
+ }
+ expandingCircles.splice(circleIndex, 1);
+ }
+
+ // Update UI stats
+ totalExplosionsElement.textContent = totalExplosions;
+ activeExplosions.textContent = expandingCircles.length;
+ longestCurrentChain.textContent = longestCurrentChainCount;
+ }
+
+ function renderGame() {
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Draw balls and explosions
+ balls.forEach(ball => ball.draw());
+ expandingCircles.forEach(circle => circle.draw());
+
+ // Draw stats on canvas if checkboxes are checked
+ ctx.fillStyle = 'white';
+ ctx.font = '16px Arial';
+ const padding = 84;
+ let yPos = 24;
+
+ if (activeExplosionsCheckbox.checked) {
+ ctx.fillText(`Active Explosions: ${activeExplosions.textContent}`, padding, yPos);
+ yPos += 24;
+ }
+ if (longestChainCheckbox.checked) {
+ ctx.fillText(`Longest Current Chain: ${longestCurrentChain.textContent}`, padding, yPos);
+ yPos += 24;
+ }
+ if (totalExplosionsCheckbox.checked) {
+ ctx.fillText(`Total Explosions: ${totalExplosions}`, padding, yPos);
+ }
+ }
+
+ function animate(timestamp) {
+ requestAnimationFrame(animate);
+
+ if (!lastFrameTime) {
+ lastFrameTime = timestamp;
+ }
+
+ const dt = timestamp - lastFrameTime;
+ lastFrameTime = timestamp;
+ const frameFactor = dt / (1000 / 60); // Normalize to 60fps
+
+ updateGame(frameFactor);
+ renderGame();
+ }
+
+ /*************************************************************
+ * EVENT LISTENERS
+ *************************************************************/
+ function setupEventListeners() {
+ // Slider event listeners
+ ballCountSlider.addEventListener('input', () => {
+ ballCount = parseInt(ballCountSlider.value);
+ ballCountValue.textContent = ballCount;
+ saveSetting('ballCount', ballCount);
+ resetGame();
+ });
+
+ ballSpeedSlider.addEventListener('input', () => {
+ ballSpeed = parseFloat(ballSpeedSlider.value);
+ ballSpeedValue.textContent = ballSpeed;
+ saveSetting('ballSpeed', ballSpeed);
+ updateBallSpeeds();
+ });
+
+ ballRadiusSlider.addEventListener('input', () => {
+ ballRadius = parseInt(ballRadiusSlider.value);
+ ballRadiusValue.textContent = ballRadius;
+ saveSetting('ballRadius', ballRadius);
+ resetBalls();
+ });
+
+ explosionRadiusSlider.addEventListener('input', () => {
+ maxCircleSize = parseInt(explosionRadiusSlider.value);
+ explosionRadiusValue.textContent = maxCircleSize;
+ saveSetting('explosionRadius', maxCircleSize);
+ });
+
+ explosionSpeedSlider.addEventListener('input', () => {
+ explosionSpeed = parseFloat(explosionSpeedSlider.value);
+ explosionSpeedValue.textContent = explosionSpeed.toFixed(2);
+ saveSetting('explosionSpeed', explosionSpeed);
+ });
+
+ explosionLingerSlider.addEventListener('input', () => {
+ explosionLingerTime = parseFloat(explosionLingerSlider.value);
+ explosionLingerValue.textContent = explosionLingerTime;
+ saveSetting('explosionLinger', explosionLingerTime);
+ });
+
+ explosionDragFreqSlider.addEventListener('input', () => {
+ explosionDragFreq = parseInt(explosionDragFreqSlider.value);
+ explosionDragFreqValue.textContent = explosionDragFreq;
+ saveSetting('explosionDragFreq', explosionDragFreq);
+ });
+
+ // Checkbox event listeners
+ autoStartCheckbox.addEventListener('change', () => {
+ saveSetting('autoStart', autoStartCheckbox.checked);
+ });
+
+ ballAfterExplosionCheckbox.addEventListener('change', () => {
+ saveSetting('ballAfterExplosion', ballAfterExplosionCheckbox.checked);
+ });
+
+ activeExplosionsCheckbox.addEventListener('change', function() {
+ saveSetting('activeExplosionsCheckbox', this.checked);
+ });
+
+ longestChainCheckbox.addEventListener('change', function() {
+ saveSetting('longestChainCheckbox', this.checked);
+ });
+
+ totalExplosionsCheckbox.addEventListener('change', function() {
+ saveSetting('totalExplosionsCheckbox', this.checked);
+ });
+
+ // Button event listeners
+ resetButton.addEventListener('click', resetGame);
+ restoreDefaultsButton.addEventListener('click', restoreDefaultSettings);
+ toggleUIButton.addEventListener('click', () => {
+ ui.classList.toggle('visible');
+ saveSetting('uiVisible', ui.classList.contains('visible'));
+ toggleUIButton.textContent = ui.classList.contains('visible') ? 'Hide Controls' : 'Show Controls';
+ });
+ fullscreenButton.addEventListener('click', toggleFullScreen);
+
+ // Mode button event listeners
+ document.querySelectorAll('.mode-button').forEach(button => {
+ button.addEventListener('click', function() {
+ // Remove active class from all buttons
+ document.querySelectorAll('.mode-button').forEach(b => b.classList.remove('active'));
+ // Add active class to clicked button
+ this.classList.add('active');
+ // Update mode
+ mode = this.dataset.mode;
+ saveSetting('mode', mode);
+ endDrag(); // Cancel any ongoing drag operations
+ });
+ });
+
+ // Music button event listener
+ musicButton.addEventListener('click', () => {
+ muted = !muted;
+ saveSetting('muted', muted);
+ if (!muted) {
+ bgMusic.play();
+ musicButton.textContent = '🔊';
+ } else {
+ bgMusic.pause();
+ musicButton.textContent = '🔇';
+ }
+ });
+
+ // Canvas event listeners
+ canvas.addEventListener('click', (event) => {
+ if (mode !== 'click') return;
+
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+ const x = (event.clientX - rect.left) * scaleX;
+ const y = (event.clientY - rect.top) * scaleY;
+
+ if (y < canvas.height) {
+ createExplosion(x, y, 1, true);
+ }
+ });
+
+ canvas.addEventListener('mousedown', startDrag);
+ canvas.addEventListener('mousemove', handleDrag);
+ canvas.addEventListener('mouseup', endDrag);
+ canvas.addEventListener('mouseleave', endDrag);
+
+ // Touch event listeners
+ canvas.addEventListener('touchstart', handleTouchStart);
+ canvas.addEventListener('touchmove', handleTouchMove);
+ canvas.addEventListener('touchend', handleTouchEnd);
+ canvas.addEventListener('touchcancel', handleTouchEnd);
+
+ // Handle touch events on the document body
+ document.body.addEventListener('touchstart', function(event) {
+ if (event.target === canvas) {
+ event.preventDefault();
+ }
+ }, { passive: false });
+
+ document.body.addEventListener('touchmove', function(event) {
+ if (event.target === canvas) {
+ event.preventDefault();
+ }
+ }, { passive: false });
+
+ // Fullscreen change event listener
+ document.addEventListener('fullscreenchange', () => {
+ if (!document.fullscreenElement) {
+ fullscreenButton.textContent = 'Full Screen';
+ }
+ });
+
+ // Resize event listener
+ window.addEventListener('resize', resizeCanvas);
+
+ // First interaction handlers for audio
+ canvas.addEventListener('click', handleFirstClick);
+ canvas.addEventListener('touchstart', handleFirstClick);
+ }
+
+ /*************************************************************
+ * INITIALIZATION AND STARTUP
+ *************************************************************/
+ function init() {
+ loadSettings();
+ setupEventListeners();
+ resetGame();
+ }
+
+ // Start the application
+ resizeCanvas();
+ init();
+ requestAnimationFrame(animate);
+ </script>
+</body>
+</html>