Spawns multiple bouncy 3D objects with shape selection, corrected collision, and a settings UI.
// ==UserScript== // @name Bouncy Shaded Cube Final Release // @namespace http://tampermonkey.net/ // @version 3.0 // @description Spawns multiple bouncy 3D objects with shape selection, corrected collision, and a settings UI. // @author alisteveman12 // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @require https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js // @run-at document-idle // @icon data:image/jpeg;base64,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 // ==/UserScript== (function( ) { 'use strict'; // Check if THREE is loaded. If not, wait and check again. if (typeof window.THREE === 'undefined') { window.setTimeout(arguments.callee, 100); return; } // --- Configuration (Stored in GM_values) --- const DEFAULT_SETTINGS = { SHAPE: 'Cube', // New setting CUBE_SIZE: 50, SPEED: 2, BOUNCE_HEIGHT: 10, BOUNCE_SPEED: 0.05, MAX_CUBES: 10, COLLISION_ENABLED: true, CUBE_COLOR: 0x00ff00 // Default green color }; let settings = {}; // --- Global State Variables --- let cubeIsRunning = false; let animationFrameId = null; let canvas = null; let renderer = null; let scene = null; let camera = null; let cubes = []; let cubeButton = null; let spawnMoreButton = null; let deleteCubeButton = null; let settingsButton = null; let settingsPanel = null; let width = window.innerWidth; let height = window.innerHeight; // --- Cube Class (to manage multiple cubes) --- class BouncyCube { constructor(color) { let geometry; const size = settings.CUBE_SIZE; switch (settings.SHAPE) { case 'Sphere': geometry = new THREE.SphereGeometry(size / 2, 32, 32); break; case 'Tetrahedron': geometry = new THREE.TetrahedronGeometry(size / 2); break; case 'Custom': // --- Placeholder for Custom Model Loading --- console.warn("Custom Model selected, but not yet implemented. Defaulting to Cube."); geometry = new THREE.BoxGeometry(size, size, size); break; case 'Cube': default: geometry = new THREE.BoxGeometry(size, size, size); break; } this.geometry = geometry; const objectColor = color || Math.random() * 0xffffff; this.material = new THREE.MeshPhongMaterial({ color: objectColor, shininess: 30 }); this.mesh = new THREE.Mesh(this.geometry, this.material); scene.add(this.mesh); // Initial random position and velocity this.cubeX = Math.random() * width; this.cubeY = Math.random() * height; this.velocityX = (Math.random() > 0.5 ? 1 : -1) * (settings.SPEED + Math.random() * 1); this.velocityY = (Math.random() > 0.5 ? 1 : -1) * (settings.SPEED + Math.random() * 1); this.timeOffset = Math.random() * 100; // Set initial world position const worldPos = screenToWorld(this.cubeX, this.cubeY); this.mesh.position.x = worldPos.x; this.mesh.position.y = worldPos.y; } update() { // 1. Update position based on screen coordinates this.cubeX += this.velocityX; this.cubeY += this.velocityY; this.timeOffset += settings.BOUNCE_SPEED; // 2. Collision detection (on screen edges) const halfSize = settings.CUBE_SIZE / 2; if (this.cubeX + halfSize > width || this.cubeX - halfSize < 0) { this.velocityX = -this.velocityX; this.cubeX = Math.max(halfSize, Math.min(width - halfSize, this.cubeX)); } if (this.cubeY + halfSize > height || this.cubeY - halfSize < 0) { this.velocityY = -this.velocityY; this.cubeY = Math.max(halfSize, Math.min(height - halfSize, this.cubeY)); } // 3. Convert screen position to world position for rendering const worldPos = screenToWorld(this.cubeX, this.cubeY); this.mesh.position.x = worldPos.x; this.mesh.position.y = worldPos.y; // 4. "Bounce" effect (up and down along the Z-axis) this.mesh.position.z = Math.sin(this.timeOffset) * settings.BOUNCE_HEIGHT; // 5. Rotation for visual interest this.mesh.rotation.x += 0.01; this.mesh.rotation.y += 0.01; } dispose() { scene.remove(this.mesh); this.geometry.dispose(); this.material.dispose(); } } // --- Utility Functions --- function loadSettings() { settings = GM_getValue('bouncyCubeSettings', DEFAULT_SETTINGS); // Ensure all default keys exist in case of new settings settings = { ...DEFAULT_SETTINGS, ...settings }; } function saveSettings() { GM_setValue('bouncyCubeSettings', settings); } // Function to convert screen coordinates to Three.js world coordinates function screenToWorld(x, y) { const vector = new THREE.Vector3(); vector.set( (x / width) * 2 - 1, -(y / height) * 2 + 1, 0.5 ); vector.unproject(camera); const dir = vector.sub(camera.position).normalize(); const distance = -camera.position.z / dir.z; const pos = camera.position.clone().add(dir.multiplyScalar(distance)); return pos; } function onWindowResize() { if (!cubeIsRunning) return; width = window.innerWidth; height = window.innerHeight; // Update canvas size canvas.width = width; canvas.height = height; // Update camera aspect ratio camera.aspect = width / height; camera.updateProjectionMatrix(); // Update renderer size renderer.setSize(width, height); } function checkCollisions() { if (!settings.COLLISION_ENABLED) return; for (let i = 0; i < cubes.length; i++) { for (let j = i + 1; j < cubes.length; j++) { const cubeA = cubes[i]; const cubeB = cubes[j]; const dx = cubeA.cubeX - cubeB.cubeX; const dy = cubeA.cubeY - cubeB.cubeY; const distance = Math.sqrt(dx * dx + dy * dy); const minDistance = settings.CUBE_SIZE; if (distance < minDistance) { // Collision detected const angle = Math.atan2(dy, dx); const sin = Math.sin(angle); const cos = Math.cos(angle); // Rotate velocities to the collision axis const vA = { x: cubeA.velocityX, y: cubeA.velocityY }; const vB = { x: cubeB.velocityX, y: cubeB.velocityY }; const uA = { x: vA.x * cos + vA.y * sin, y: vA.y * cos - vA.x * sin }; const uB = { x: vB.x * cos + vB.y * sin, y: vB.y * cos - vB.x * sin }; // Swap x velocities (perfect elastic collision) const vFinalA = { x: uB.x, y: uA.y }; const vFinalB = { x: uA.x, y: uB.y }; // Rotate back to original coordinate system cubeA.velocityX = vFinalA.x * cos - vFinalA.y * sin; cubeA.velocityY = vFinalA.y * cos + vFinalA.x * sin; cubeB.velocityX = vFinalB.x * cos - vFinalB.y * sin; cubeB.velocityY = vFinalB.y * cos + vFinalB.x * sin; // Separation to prevent sticking (crucial fix) const overlap = minDistance - distance; const separationX = overlap * cos; const separationY = overlap * sin; // Move both cubes apart by half the overlap cubeA.cubeX += separationX * 0.5; cubeA.cubeY += separationY * 0.5; cubeB.cubeX -= separationX * 0.5; cubeB.cubeY -= separationY * 0.5; } } } } function animate() { animationFrameId = requestAnimationFrame(animate); // Update all cubes cubes.forEach(cube => cube.update()); // Check for collisions checkCollisions(); // Render the scene renderer.render(scene, camera); } function updateButtonVisibility() { const hasCubes = cubes.length > 0; if (spawnMoreButton) { spawnMoreButton.style.display = hasCubes ? 'block' : 'none'; spawnMoreButton.textContent = `Spawn More (${cubes.length}/${settings.MAX_CUBES})`; } if (deleteCubeButton) { deleteCubeButton.style.display = hasCubes ? 'block' : 'none'; deleteCubeButton.textContent = `Delete (${cubes.length})`; } if (settingsButton) { settingsButton.style.display = hasCubes ? 'block' : 'none'; // Also hide the settings panel if the button is hidden if (!hasCubes && settingsPanel) { settingsPanel.style.display = 'none'; } } // Update main toggle button text cubeButton.textContent = hasCubes ? 'Stop Objects' : 'Start Objects'; } function spawnCube() { if (cubes.length >= settings.MAX_CUBES) { alert(`Maximum number of objects (${settings.MAX_CUBES}) reached!`); return; } cubes.push(new BouncyCube(settings.CUBE_COLOR)); updateButtonVisibility(); } function deleteCube() { if (cubes.length === 0) { return; } // Remove the last spawned cube const cubeToRemove = cubes.pop(); cubeToRemove.dispose(); updateButtonVisibility(); // If all cubes are gone, stop the whole animation if (cubes.length === 0) { stopCube(); } } // --- Main Control Functions --- function startCube() { if (cubeIsRunning) return; // 1. Setup Canvas canvas = document.createElement('canvas'); canvas.id = 'bouncy-cube-canvas'; canvas.style.cssText = 'position: fixed; top: 0; left: 0; pointer-events: none; z-index: 99999;'; document.body.appendChild(canvas); width = window.innerWidth; height = window.innerHeight; canvas.width = width; canvas.height = height; // 2. Three.js Setup scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true }); renderer.setSize(width, height); camera.position.z = 100; // 3. Lighting Setup const ambientLight = new THREE.AmbientLight(0x404040, 2); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(1, 1, 1).normalize(); scene.add(directionalLight); // 4. Spawn the initial cube cubes = []; spawnCube(); // 5. Start Animation and Listeners window.addEventListener('resize', onWindowResize, false); animate(); cubeIsRunning = true; updateButtonVisibility(); } function stopCube() { if (!cubeIsRunning) return; // 1. Stop Animation cancelAnimationFrame(animationFrameId); // 2. Remove Canvas and Listeners if (canvas && canvas.parentNode) { canvas.parentNode.removeChild(canvas); } window.removeEventListener('resize', onWindowResize, false); // 3. Dispose of all cubes cubes.forEach(cube => cube.dispose()); cubes = []; // 4. Cleanup Three.js objects renderer.dispose(); scene = null; camera = null; renderer = null; canvas = null; cubeIsRunning = false; updateButtonVisibility(); } function toggleCube() { if (cubeIsRunning) { stopCube(); } else { startCube(); } } function toggleSettingsPanel() { if (settingsPanel.style.display === 'block') { settingsPanel.style.display = 'none'; } else { settingsPanel.style.display = 'block'; populateSettingsPanel(); } } function handleSettingChange(event) { const key = event.target.id.replace('setting-', ''); let value; if (event.target.type === 'checkbox') { value = event.target.checked; } else if (event.target.type === 'color') { // Convert hex color string to number value = parseInt(event.target.value.substring(1), 16); } else if (event.target.tagName === 'SELECT') { value = event.target.value; } else { value = parseFloat(event.target.value); if (key === 'MAX_CUBES') { value = parseInt(event.target.value); } } settings[key] = value; saveSettings(); // Special case: If a geometry-affecting setting changes, re-initialize cubes if (cubeIsRunning && (key === 'CUBE_SIZE' || key === 'CUBE_COLOR' || key === 'SHAPE')) { // Stop and restart to apply new geometry/material stopCube(); startCube(); } updateButtonVisibility(); } function populateSettingsPanel() { const shapeOptions = [ { value: 'Cube', text: 'Cube' }, { value: 'Sphere', text: 'Sphere' }, { value: 'Tetrahedron', text: 'Tetrahedron (Triangle)' }, { value: 'Custom', text: 'Custom Model (Coming Soon)', disabled: true } ]; let shapeSelectOptions = shapeOptions.map(opt => `<option value="${opt.value}" ${settings.SHAPE === opt.value ? 'selected' : ''} ${opt.disabled ? 'disabled' : ''}>${opt.text}</option>` ).join(''); settingsPanel.innerHTML = ` <h3>Object Settings</h3> <div class="setting-group"> <label for="setting-SHAPE">Shape:</label> <select id="setting-SHAPE"> ${shapeSelectOptions} </select> </div> <div class="setting-group"> <label for="setting-CUBE_SIZE">Size:</label> <input type="range" id="setting-CUBE_SIZE" min="10" max="100" step="5" value="${settings.CUBE_SIZE}"> <span>${settings.CUBE_SIZE}</span> </div> <div class="setting-group"> <label for="setting-SPEED">Speed:</label> <input type="range" id="setting-SPEED" min="0.5" max="5" step="0.5" value="${settings.SPEED}"> <span>${settings.SPEED}</span> </div> <div class="setting-group"> <label for="setting-BOUNCE_HEIGHT">Bounce Height:</label> <input type="range" id="setting-BOUNCE_HEIGHT" min="0" max="50" step="5" value="${settings.BOUNCE_HEIGHT}"> <span>${settings.BOUNCE_HEIGHT}</span> </div> <div class="setting-group"> <label for="setting-BOUNCE_SPEED">Bounce Speed:</label> <input type="range" id="setting-BOUNCE_SPEED" min="0.01" max="0.2" step="0.01" value="${settings.BOUNCE_SPEED}"> <span>${settings.BOUNCE_SPEED}</span> </div> <div class="setting-group"> <label for="setting-MAX_CUBES">Max Objects:</label> <input type="number" id="setting-MAX_CUBES" min="1" max="50" value="${settings.MAX_CUBES}"> </div> <div class="setting-group"> <label for="setting-COLLISION_ENABLED">Collisions:</label> <input type="checkbox" id="setting-COLLISION_ENABLED" ${settings.COLLISION_ENABLED ? 'checked' : ''}> </div> <div class="setting-group"> <label for="setting-CUBE_COLOR">Color:</label> <input type="color" id="setting-CUBE_COLOR" value="#${settings.CUBE_COLOR.toString(16).padStart(6, '0')}"> </div> <button id="reset-settings">Reset to Default</button> `; // Add event listeners to all inputs settingsPanel.querySelectorAll('input, select').forEach(input => { input.addEventListener('input', handleSettingChange); // Update range span dynamically if (input.type === 'range') { input.addEventListener('input', (e) => { e.target.nextElementSibling.textContent = e.target.value; }); } }); // Add event listener for reset button settingsPanel.querySelector('#reset-settings').addEventListener('click', () => { if (confirm('Are you sure you want to reset all settings to default?')) { settings = DEFAULT_SETTINGS; saveSettings(); populateSettingsPanel(); // Re-populate with default values // Stop and restart to apply all defaults if (cubeIsRunning) { stopCube(); startCube(); } } }); } // --- Menu Button and Panel Setup --- function createMenuButtons() { // Consolidated CSS for all buttons and the settings panel GM_addStyle("#bouncy-cube-toggle, #spawn-more-cubes, #delete-cube, #settings-button { position: fixed; padding: 10px 15px; background-color: #333; color: white; border: none; border-radius: 5px; cursor: pointer; z-index: 100000; font-family: sans-serif; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.5); transition: background-color 0.3s; } #bouncy-cube-toggle:hover, #spawn-more-cubes:hover, #delete-cube:hover, #settings-button:hover { background-color: #555; } #bouncy-cube-toggle { bottom: 10px; right: 10px; } #spawn-more-cubes { bottom: 60px; right: 10px; } #delete-cube { bottom: 110px; right: 10px; background-color: #d9534f; } #delete-cube:hover { background-color: #c9302c; } #settings-button { bottom: 160px; right: 10px; background-color: #f0ad4e; } #settings-button:hover { background-color: #ec971f; } #bouncy-cube-settings-panel { position: fixed; bottom: 10px; right: 180px; width: 300px; background-color: #222; color: #fff; border: 1px solid #555; border-radius: 8px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); z-index: 99999; display: none; font-family: sans-serif; } #bouncy-cube-settings-panel h3 { margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 10px; } #bouncy-cube-settings-panel .setting-group { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } #bouncy-cube-settings-panel input[type=\"range\"] { flex-grow: 1; margin: 0 10px; } #bouncy-cube-settings-panel input[type=\"number\"], #bouncy-cube-settings-panel input[type=\"color\"], #bouncy-cube-settings-panel select { width: 60px; color: #000; } #bouncy-cube-settings-panel button { width: 100%; margin-top: 10px; background-color: #5bc0de; color: white; } #bouncy-cube-settings-panel button:hover { background-color: #31b0d5; }"); // 1. Start/Stop Cube Button (Bottom) cubeButton = document.createElement('button'); cubeButton.id = 'bouncy-cube-toggle'; cubeButton.textContent = 'Start Objects'; cubeButton.addEventListener('click', toggleCube); document.body.appendChild(cubeButton); // 2. Spawn More Button (Middle-Bottom) spawnMoreButton = document.createElement('button'); spawnMoreButton.id = 'spawn-more-cubes'; spawnMoreButton.textContent = 'Spawn More'; spawnMoreButton.style.display = 'none'; spawnMoreButton.addEventListener('click', spawnCube); document.body.appendChild(spawnMoreButton); // 3. Delete Cube Button (Middle-Top) deleteCubeButton = document.createElement('button'); deleteCubeButton.id = 'delete-cube'; deleteCubeButton.textContent = 'Delete'; deleteCubeButton.style.display = 'none'; deleteCubeButton.addEventListener('click', deleteCube); document.body.appendChild(deleteCubeButton); // 4. Settings Button (Top) settingsButton = document.createElement('button'); settingsButton.id = 'settings-button'; settingsButton.textContent = 'Settings'; settingsButton.style.display = 'none'; // Initially hidden settingsButton.addEventListener('click', toggleSettingsPanel); document.body.appendChild(settingsButton); // 5. Settings Panel settingsPanel = document.createElement('div'); settingsPanel.id = 'bouncy-cube-settings-panel'; document.body.appendChild(settingsPanel); } // --- Initialization --- loadSettings(); createMenuButtons(); })();