UNPKG

aframe-babia-components

Version:

A data visualization set of components for A-Frame.

1,276 lines (1,070 loc) โ€ข 41.5 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>๐ŸŒŒ VR Planetarium - Real Constellations</title> <!-- A-Frame --> <script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v6.1.1/dist/aframe-extras.min.js"></script> <!-- Base Component --> <script src="babia-dome.js"></script> <style> body { margin: 0; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #000000; } #info-panel { position: absolute; top: 20px; left: 20px; background: rgba(0, 0, 20, 0.95); color: white; padding: 25px; border-radius: 15px; max-width: 350px; z-index: 1000; backdrop-filter: blur(15px); border: 2px solid rgba(255, 215, 0, 0.4); box-shadow: 0 8px 32px rgba(255, 215, 0, 0.2); } #info-panel h2 { margin-top: 0; color: #FFD700; font-size: 22px; display: flex; align-items: center; gap: 12px; text-shadow: 0 0 10px rgba(255, 215, 0, 0.5); } #info-panel p { margin: 10px 0; font-size: 14px; line-height: 1.6; } .badge { display: inline-block; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; padding: 4px 12px; border-radius: 15px; font-size: 11px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; } .vr-badge { background: linear-gradient(135deg, #FF1493, #FF69B4); animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.8; transform: scale(1.05); } } #controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 20, 0.95); padding: 20px 30px; border-radius: 15px; z-index: 1000; backdrop-filter: blur(15px); display: flex; gap: 12px; align-items: center; flex-wrap: wrap; justify-content: center; border: 2px solid rgba(255, 215, 0, 0.4); max-width: 90vw; box-shadow: 0 8px 32px rgba(255, 215, 0, 0.2); } #controls button { background: linear-gradient(135deg, #FFD700, #FFA500); border: none; color: #000; padding: 12px 20px; margin: 0; border-radius: 10px; cursor: pointer; font-size: 14px; transition: all 0.3s; font-weight: 700; box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3); text-transform: uppercase; letter-spacing: 0.5px; } #controls button:hover { background: linear-gradient(135deg, #FFA500, #FF8C00); transform: translateY(-3px); box-shadow: 0 6px 20px rgba(255, 215, 0, 0.5); } #controls button.active { background: linear-gradient(135deg, #FF1493, #FF69B4); color: white; } .divider { width: 2px; height: 35px; background: linear-gradient(180deg, rgba(255, 215, 0, 0), rgba(255, 215, 0, 0.5), rgba(255, 215, 0, 0)); margin: 0 10px; } .slider-container { display: flex; align-items: center; gap: 10px; color: white; font-size: 13px; background: rgba(255, 215, 0, 0.1); padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255, 215, 0, 0.3); } input[type="range"] { width: 100px; height: 6px; background: rgba(255, 255, 255, 0.1); border-radius: 5px; outline: none; cursor: pointer; -webkit-appearance: none; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; background: linear-gradient(135deg, #FFD700, #FFA500); border-radius: 50%; cursor: pointer; box-shadow: 0 2px 8px rgba(255, 215, 0, 0.5); } input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; background: linear-gradient(135deg, #FFD700, #FFA500); border-radius: 50%; cursor: pointer; border: none; box-shadow: 0 2px 8px rgba(255, 215, 0, 0.5); } .legend { position: absolute; top: 20px; right: 20px; background: rgba(0, 0, 20, 0.95); padding: 20px; border-radius: 15px; color: white; z-index: 1000; backdrop-filter: blur(15px); border: 2px solid rgba(255, 215, 0, 0.4); max-width: 280px; box-shadow: 0 8px 32px rgba(255, 215, 0, 0.2); } .legend h3 { margin-top: 0; font-size: 18px; color: #FFD700; margin-bottom: 15px; text-shadow: 0 0 10px rgba(255, 215, 0, 0.5); } .legend-item { display: flex; align-items: center; margin: 10px 0; font-size: 13px; } .legend-color { width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; box-shadow: 0 0 15px currentColor; } .legend-section { margin-top: 20px; padding-top: 15px; border-top: 1px solid rgba(255, 215, 0, 0.3); } #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 28px; z-index: 2000; text-align: center; background: rgba(0, 0, 20, 0.95); padding: 50px; border-radius: 20px; backdrop-filter: blur(15px); border: 2px solid rgba(255, 215, 0, 0.5); } .spinner { border: 5px solid rgba(255, 215, 0, 0.2); border-top: 5px solid #FFD700; border-radius: 50%; width: 70px; height: 70px; animation: spin 1s linear infinite; margin: 25px auto; box-shadow: 0 0 20px rgba(255, 215, 0, 0.5); } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .stats { margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255, 215, 0, 0.3); font-size: 13px; } .stats p { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; } .stat-value { color: #FFD700; font-weight: bold; text-shadow: 0 0 5px rgba(255, 215, 0, 0.5); } .vr-info { background: rgba(255, 20, 147, 0.2); border: 1px solid rgba(255, 105, 180, 0.5); padding: 12px; border-radius: 10px; margin-top: 12px; font-size: 13px; } .instruction { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 13px; } </style> </head> <body> <div id="loading"> <div class="spinner"></div> <p>โœจ Initializing VR Planetarium...</p> <p style="font-size: 16px; color: #FFD700;">Loading real constellations</p> </div> <div id="info-panel"> <h2> ๐ŸŒŒ VR Planetarium <span class="badge">Use Case</span> <span class="badge vr-badge">VR</span> </h2> <p style="color: #FFD700; font-weight: bold; font-size: 16px;">โญ Real Constellations</p> <p><strong>๐Ÿ–ฅ๏ธ Desktop:</strong></p> <div class="instruction">๐Ÿ–ฑ๏ธ Drag to rotate view</div> <div class="instruction">๐Ÿ‘† Hover on stars</div> <div class="vr-info"> <p><strong>๐Ÿฅฝ VR Mode:</strong></p> <div class="instruction">๐ŸŽฎ Point with controllers</div> <div class="instruction">โญ View star info</div> <div class="instruction">๐Ÿ•น๏ธ Left joystick: Move</div> <div class="instruction">๐Ÿ•น๏ธ Right joystick: Rotate camera</div> </div> <p id="node-count">Loading stars...</p> <div class="stats"> <p> <span>โšก Physics:</span> <span id="physics-status" class="stat-value">Enabled</span> </p> <p> <span>๐Ÿ“Š FPS:</span> <span id="fps-counter" class="stat-value">--</span> </p> <p> <span>๐Ÿฅฝ VR:</span> <span id="vr-status" class="stat-value">Inactive</span> </p> </div> </div> <div class="legend"> <h3>๐ŸŒ  Legend</h3> <div class="legend-section"> <h3>๐Ÿ”— Lines</h3> <div class="legend-item"> <div class="legend-color" style="background: #ffffff; color: #4ECDC4;"></div> <span>Constellation</span> </div> <div class="legend-item"> <div class="legend-color" style="background: #2ce6ff; color: #FFFFFF;"></div> <span>Proximity</span> </div> </div> <div class="legend-section"> <h3>๐ŸŒŸ Famous Stars</h3> <p style="font-size: 12px; line-height: 1.8;"> โ€ข <strong>Sirius</strong> - Brightest star<br> โ€ข <strong>Betelgeuse</strong> - Red supergiant<br> โ€ข <strong>Polaris</strong> - North Star<br> โ€ข <strong>Rigel</strong> - Blue giant<br> โ€ข <strong>Vega</strong> - Summer star<br> โ€ข <strong>Alpha Centauri</strong> - Nearest </p> </div> </div> <div id="controls"> <button id="toggle-links" onclick="toggleLinks()">๐Ÿ”— Lines</button> <button onclick="toggleAnimation()">โฏ๏ธ Rotation</button> <div class="divider"></div> <div class="slider-container"> <span>๐Ÿ’ช Force:</span> <input type="range" id="physics-strength" min="0" max="1" step="0.05" value="0.6" oninput="updatePhysicsStrength(this.value)"> <span id="strength-value">0.6</span> </div> <div class="slider-container"> <span>โญ Size:</span> <input type="range" id="star-size" min="0.5" max="2.5" step="0.1" value="1" oninput="updateStarSize(this.value)"> <span id="size-value">1.0x</span> </div> <div class="divider"></div> <button onclick="resetView()">๐Ÿ”„ Reset</button> </div> <a-scene xr-mode-ui="XRMode: xr" background="color: #000008" renderer="antialias: true; colorManagement: true; physicallyCorrectLights: true; exposure: 1.5" > <a-assets> <img id="texture-sol" src="/public/libs/sol.png"> <img id="texture-neptuno" src="/public/libs/neptuno.png"> <img id="texture-luna" src="/public/libs/luna.png"> <img id="texture-tierra" src="/public/libs/tierra.png"> <img id="texture-venus" src="/public/libs/venus.png"> <img id="texture-ganim" src="/public/libs/ganimedes.png"> </a-assets> <!-- Raycaster for Mouse --> <a-entity id="mouse-cursor" cursor="rayOrigin: mouse" raycaster="objects: .interactive-node; far: 100; interval: 100"> </a-entity> <!-- Camera Rig --> <a-entity id="camera-rig" position="0 0 0" vr-thumbstick-locomotion="speed: 5; deadZone: 0.15"> <!-- Main Camera --> <a-camera id="main-camera" position="0 0 0" look-controls wasd-controls="acceleration: 40" vr-gamepad-controls></a-camera> <!-- LEFT VR Controller --> <a-entity id="left-hand" laser-controls="hand: left" raycaster="objects: .interactive-node; far: 50; lineColor: #FFD700; lineOpacity: 0.8" line="color: #FFD700; opacity: 0.8; visible: true"> </a-entity> <!-- RIGHT VR Controller --> <a-entity id="right-hand" laser-controls="hand: right" raycaster="objects: .interactive-node; far: 50; lineColor: #FF69B4; lineOpacity: 0.8" line="color: #FF69B4; opacity: 0.8; visible: true"> </a-entity> </a-entity> <!-- ENHANCED LIGHTING FOR STARS --> <a-entity light="type: ambient; color: #FFFFFF; intensity: 1.2"></a-entity> <a-entity light="type: directional; color: #FFFFFF; intensity: 0.8" position="5 10 5"></a-entity> <a-entity light="type: directional; color: #AACCFF; intensity: 0.5" position="-5 -5 -5"></a-entity> <a-entity light="type: point; color: #FFD700; intensity: 0.6; distance: 100" position="0 0 0"></a-entity> <!-- Visualizer Component (BASE - UNCHANGED) --> <a-entity id="visualizer" babia-dome=" dataUrl: data2.json; sphereRadius: 15; nodeSize: 0.18; showLabels: true; animateNodes: true; colorByValue: false; nodeOpacity: 1.0; enablePhysics: true; physicsStrength: 0.1; collisionRadius: 0; showLinks: true; linkSegments: 50; linkOpacity: 0.8; linkWidth: 0.02; linkStyle: line; "></a-entity> <a-entity id="distant-stars"></a-entity> <a-sky hide-on-enter-ar color="#000008" material="shader: flat"></a-sky> </a-scene> <script> AFRAME.registerComponent('vr-thumbstick-locomotion', { schema: { speed: { type: 'number', default: 4 }, deadZone: { type: 'number', default: 0.15 }, rotationSpeed: { type: 'number', default: 1.5 } }, init: function () { this.leftInput = { x: 0, y: 0 }; this.rightInput = { x: 0, y: 0 }; this.cameraEl = null; this.forward = new THREE.Vector3(); this.right = new THREE.Vector3(); this.moveDir = new THREE.Vector3(); this.up = new THREE.Vector3(0, 1, 0); this.onLeftThumbstickMoved = this.onLeftThumbstickMoved.bind(this); this.onLeftThumbstickEnded = this.onLeftThumbstickEnded.bind(this); this.onRightThumbstickMoved = this.onRightThumbstickMoved.bind(this); this.onRightThumbstickEnded = this.onRightThumbstickEnded.bind(this); this.el.sceneEl.addEventListener('loaded', () => { this.cameraEl = document.querySelector('#main-camera'); this.leftHandEl = document.querySelector('#left-hand'); this.rightHandEl = document.querySelector('#right-hand'); if (this.leftHandEl) { this.leftHandEl.addEventListener('thumbstickmoved', this.onLeftThumbstickMoved); this.leftHandEl.addEventListener('thumbstickend', this.onLeftThumbstickEnded); } if (this.rightHandEl) { this.rightHandEl.addEventListener('thumbstickmoved', this.onRightThumbstickMoved); this.rightHandEl.addEventListener('thumbstickend', this.onRightThumbstickEnded); } }); }, remove: function () { if (this.leftHandEl) { this.leftHandEl.removeEventListener('thumbstickmoved', this.onLeftThumbstickMoved); this.leftHandEl.removeEventListener('thumbstickend', this.onLeftThumbstickEnded); } if (this.rightHandEl) { this.rightHandEl.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved); this.rightHandEl.removeEventListener('thumbstickend', this.onRightThumbstickEnded); } }, onLeftThumbstickMoved: function (evt) { const x = evt.detail && typeof evt.detail.x === 'number' ? evt.detail.x : 0; const y = evt.detail && typeof evt.detail.y === 'number' ? evt.detail.y : 0; this.leftInput.x = Math.abs(x) > this.data.deadZone ? -x : 0; this.leftInput.y = Math.abs(y) > this.data.deadZone ? -y : 0; }, onLeftThumbstickEnded: function () { this.leftInput.x = 0; this.leftInput.y = 0; }, onRightThumbstickMoved: function (evt) { const x = evt.detail && typeof evt.detail.x === 'number' ? evt.detail.x : 0; const y = evt.detail && typeof evt.detail.y === 'number' ? evt.detail.y : 0; this.rightInput.x = Math.abs(x) > this.data.deadZone ? x : 0; this.rightInput.y = Math.abs(y) > this.data.deadZone ? y : 0; }, onRightThumbstickEnded: function () { this.rightInput.x = 0; this.rightInput.y = 0; }, tick: function (time, deltaTime) { if (!this.el.sceneEl || (!this.el.sceneEl.is('vr-mode') && !this.el.sceneEl.is('ar-mode'))) return; if (!this.cameraEl || !this.cameraEl.object3D) return; if (!deltaTime) return; const dt = deltaTime / 1000; // Handle movement with left thumbstick if (this.leftInput.x || this.leftInput.y) { const cameraObj = this.cameraEl.object3D; cameraObj.getWorldDirection(this.forward); this.forward.y = 0; this.forward.normalize(); this.right.crossVectors(this.forward, this.up).normalize(); this.moveDir.set(0, 0, 0); this.moveDir.addScaledVector(this.forward, -this.leftInput.y); this.moveDir.addScaledVector(this.right, this.leftInput.x); if (this.moveDir.lengthSq() > 0) { this.moveDir.normalize().multiplyScalar(this.data.speed * dt); this.el.object3D.position.add(this.moveDir); } } // Handle rotation with right thumbstick if (this.rightInput.x || this.rightInput.y) { // Yaw (left-right) this.el.object3D.rotation.y -= this.rightInput.x * this.data.rotationSpeed * dt; // Pitch (up-down), clamp to prevent flipping const currentPitch = this.cameraEl.object3D.rotation.x; const newPitch = currentPitch - this.rightInput.y * this.data.rotationSpeed * dt; this.cameraEl.object3D.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, newPitch)); } } }); // Create thousands of distant stars function createDistantStars() { const container = document.getElementById('distant-stars'); const starCount = 2000; const minRadius = 25; // Beyond visualization sphere const maxRadius = 60; for (let i = 0; i < starCount; i++) { const star = document.createElement('a-sphere'); // Random position in 3D space const phi = Math.random() * Math.PI * 2; const theta = Math.random() * Math.PI; const radius = minRadius + Math.random() * (maxRadius - minRadius); const x = radius * Math.sin(theta) * Math.cos(phi); const y = radius * Math.sin(theta) * Math.sin(phi); const z = radius * Math.cos(theta); // Varying star sizes (most small, some larger) const size = Math.random() < 0.9 ? 0.02 + Math.random() * 0.03 : 0.05 + Math.random() * 0.05; // Varying brightness/opacity const opacity = 0.3 + Math.random() * 0.7; // Slight color variation (white to blue-white) const colorOptions = ['#FFFFFF', '#F0F8FF', '#E6F2FF', '#FFFACD']; const color = colorOptions[Math.floor(Math.random() * colorOptions.length)]; star.setAttribute('radius', size); star.setAttribute('position', `${x} ${y} ${z}`); star.setAttribute('material', { shader: 'flat', color: color, opacity: opacity, transparent: true, }); if (Math.random() > 0.7) { star.setAttribute('animation', { property: 'components.material.material.opacity', from: opacity, to: opacity * 0.3, dur: 2000 + Math.random() * 3000, dir: 'alternate', loop: true, easing: 'easeInOutQuad' }); } container.appendChild(star); } console.log(`โœจ Created ${starCount} distant background stars`); } // Initialize space background when scene loads document.querySelector('a-scene').addEventListener('loaded', function() { createDistantStars(); }); </script> <script> let animationEnabled = true; let linksEnabled = true; let visualizerComponent = null; let isVRMode = false; let currentStarScale = 1.0; // FPS Counter let frameCount = 0; let lastFPSUpdate = Date.now(); function updateFPS() { frameCount++; const now = Date.now(); if (now - lastFPSUpdate >= 1000) { const fps = Math.round(frameCount * 1000 / (now - lastFPSUpdate)); const fpsEl = document.getElementById('fps-counter'); if (fpsEl) fpsEl.textContent = fps; frameCount = 0; lastFPSUpdate = now; } requestAnimationFrame(updateFPS); } document.addEventListener('DOMContentLoaded', function() { const scene = document.querySelector('a-scene'); const visualizer = document.querySelector('#visualizer'); const loading = document.getElementById('loading'); updateFPS(); console.log('๐Ÿš€ VR Planetarium initialized'); // Intentar sincronizar inmediatamente los sliders leyendo los atributos // del elemento `#visualizer` (atributo inline `babia-dome`) para reflejar // valores como `physicsStrength: 0.4;` sin esperar al componente. (function syncSlidersFromEntityAttrs() { try { if (!visualizer) return; const raw = visualizer.getAttribute('babia-dome'); if (!raw || typeof raw !== 'string') return; // Buscar physicsStrength const psMatch = raw.match(/physicsStrength\s*:\s*([0-9.]+)/); if (psMatch && psMatch[1]) { const val = parseFloat(psMatch[1]); const strengthSlider = document.getElementById('physics-strength'); const strengthValue = document.getElementById('strength-value'); if (strengthSlider) strengthSlider.value = val; if (strengthValue) strengthValue.textContent = val.toFixed(2); console.log('๐Ÿ”ง Initial physicsStrength from attribute:', val); } // Buscar nodeSize -> mapear a escala del slider (base 0.18) const nsMatch = raw.match(/nodeSize\s*:\s*([0-9.]+)/); if (nsMatch && nsMatch[1]) { const nodeSize = parseFloat(nsMatch[1]); const base = 0.18; const scale = nodeSize / base; const sizeSlider = document.getElementById('star-size'); const sizeValue = document.getElementById('size-value'); if (sizeSlider) sizeSlider.value = scale; if (sizeValue) sizeValue.textContent = scale.toFixed(1) + 'x'; currentStarScale = scale; console.log('โญ Initial nodeSize from attribute:', nodeSize, '-> scale', scale); } } catch (e) { console.warn('Could not parse babia-dome attrs for sliders', e); } })(); // Detect VR entry scene.addEventListener('enter-vr', function() { console.log('๐Ÿฅฝ VR MODE ACTIVATED'); isVRMode = true; const vrStatus = document.getElementById('vr-status'); if (vrStatus) { vrStatus.textContent = 'Active'; vrStatus.style.color = '#FF69B4'; } document.getElementById('info-panel').style.display = 'none'; document.getElementById('controls').style.display = 'none'; document.querySelector('.legend').style.display = 'none'; animationEnabled = false; }); // Detect VR exit scene.addEventListener('exit-vr', function() { console.log('๐Ÿ–ฅ๏ธ Desktop mode'); isVRMode = false; const vrStatus = document.getElementById('vr-status'); if (vrStatus) { vrStatus.textContent = 'Inactive'; vrStatus.style.color = '#FFD700'; } document.getElementById('info-panel').style.display = 'block'; document.getElementById('controls').style.display = 'flex'; document.querySelector('.legend').style.display = 'block'; }); // Scene loaded scene.addEventListener('loaded', function() { console.log('โœ… Scene loaded'); setTimeout(() => { if (loading) loading.style.display = 'none'; visualizerComponent = visualizer.components['babia-dome']; if (visualizerComponent) { const nodeCount = visualizerComponent.simulationNodes.length; const linksCount = visualizerComponent.simulationLinks.length; const constellationsCount = visualizerComponent.constellations.length; document.getElementById('node-count').textContent = `โญ ${nodeCount} real stars`; document.getElementById('stars-count').textContent = nodeCount; document.getElementById('lines-count').textContent = linksCount; document.getElementById('constellations-count').textContent = constellationsCount; // โœ… Sincronizar valores del componente con los sliders syncComponentValuesToUI(visualizerComponent); console.log(`โญ Planetarium: ${nodeCount} stars, ${linksCount} lines, ${constellationsCount} constellations`); } }, 1500); }); // Auto-rotation let rotation = 0; function autoRotate() { if (animationEnabled && visualizer && !isVRMode) { rotation += 0.0001; if (visualizer.object3D) { visualizer.object3D.rotation.y = rotation; } } requestAnimationFrame(autoRotate); } autoRotate(); }); // โœ… FUNCIร“N DE SINCRONIZACIร“N function syncComponentValuesToUI(component) { const physicsStrength = component.data.physicsStrength; const strengthSlider = document.getElementById('physics-strength'); const strengthValue = document.getElementById('strength-value'); if (strengthSlider && strengthValue) { strengthSlider.value = physicsStrength; strengthValue.textContent = physicsStrength.toFixed(2); console.log(`๐Ÿ”ง Physics Strength synced: ${physicsStrength}`); } const componentNodeSize = component.data.nodeSize; const baseNodeSize = 0.18; const currentScale = componentNodeSize / baseNodeSize; const sizeSlider = document.getElementById('star-size'); const sizeValue = document.getElementById('size-value'); if (sizeSlider && sizeValue) { currentStarScale = currentScale; sizeSlider.value = currentScale; sizeValue.textContent = currentScale.toFixed(1) + 'x'; console.log(`โญ Star Size synced: ${currentScale.toFixed(1)}x`); } } function toggleLinks() { linksEnabled = !linksEnabled; const button = document.getElementById('toggle-links'); if (visualizerComponent) { visualizerComponent.toggleLinks(linksEnabled); if (button) { button.classList.toggle('active', !linksEnabled); button.textContent = linksEnabled ? '๐Ÿ”— Lines' : '๐Ÿ”— Hidden'; } } } function toggleAnimation() { animationEnabled = !animationEnabled; console.log('โฏ๏ธ Auto-rotation:', animationEnabled ? 'Enabled' : 'Disabled'); } function updateStarSize(value) { currentStarScale = parseFloat(value); const sizeValueEl = document.getElementById('size-value'); if (sizeValueEl) sizeValueEl.textContent = currentStarScale.toFixed(1) + 'x'; const baseRadius = visualizerComponent ? visualizerComponent.data.nodeSize : 0.18; const stars = document.querySelectorAll('.interactive-node'); stars.forEach(star => { star.setAttribute('radius', baseRadius * currentStarScale); }); } function updatePhysicsStrength(value) { const strengthValue = parseFloat(value); const strengthValueEl = document.getElementById('strength-value'); if (strengthValueEl) strengthValueEl.textContent = strengthValue.toFixed(2); if (visualizerComponent) { visualizerComponent.updatePhysicsStrength(strengthValue); } } function resetView() { console.log('๐Ÿ”„ Full reset initiated'); const cameraRig = document.querySelector('#camera-rig'); if (cameraRig) { cameraRig.setAttribute('position', '0 0 0'); cameraRig.object3D.rotation.set(0, 0, 0); } const camera = document.querySelector('a-camera'); if (camera) { camera.setAttribute('position', '0 0 0'); camera.setAttribute('rotation', '0 0 0'); } if (visualizerComponent) { const originalPhysicsStrength = visualizerComponent.data.physicsStrength; updatePhysicsStrength(originalPhysicsStrength); const strengthSlider = document.getElementById('physics-strength'); if (strengthSlider) strengthSlider.value = originalPhysicsStrength; const componentNodeSize = visualizerComponent.data.nodeSize; const originalScale = componentNodeSize / 0.18; updateStarSize(originalScale); const sizeSlider = document.getElementById('star-size'); if (sizeSlider) sizeSlider.value = originalScale; if (visualizerComponent.simulation) { visualizerComponent.simulation.alpha(1).restart(); } } animationEnabled = true; if (!linksEnabled && visualizerComponent) { linksEnabled = true; visualizerComponent.toggleLinks(true); const button = document.getElementById('toggle-links'); if (button) { button.classList.remove('active'); button.textContent = '๐Ÿ”— Lines'; } } console.log('โœ… Reset completed'); } // Auto-rotation ONLY in desktop mode let rotation = 0; function autoRotate() { if (animationEnabled && visualizer && !isVRMode) { rotation += 0.0001; if (visualizer.object3D) { visualizer.object3D.rotation.y = rotation; } } requestAnimationFrame(autoRotate); } autoRotate(); // Debugging: Check VR controllers const leftHand = document.querySelector('#left-hand'); const rightHand = document.querySelector('#right-hand'); if (leftHand) { leftHand.addEventListener('controllerconnected', function(evt) { console.log('๐ŸŽฎ LEFT controller connected:', evt.detail.name); }); } if (rightHand) { rightHand.addEventListener('controllerconnected', function(evt) { console.log('๐ŸŽฎ RIGHT controller connected:', evt.detail.name); }); } ; AFRAME.registerComponent('vr-gamepad-controls', { init: function () { this.lastButtonState = {}; this.REPEAT_DELAY = 300; // ms antes de repetir al mantener pulsado this.REPEAT_INTERVAL = 150; // ms entre repeticiones this.repeatTimers = {}; // Mapeo de botones (estรกndar WebXR) // Y = รญndice 5 (mano izquierda), X = รญndice 4 (mano izquierda) // B = รญndice 5 (mano derecha), A = รญndice 4 (mano derecha) this.buttonMap = { left: { 4: 'X', 5: 'Y' }, right: { 4: 'A', 5: 'B' } }; this.STAR_SIZE_STEP = 0.1; this.STAR_SIZE_MIN = 0.5; this.STAR_SIZE_MAX = 2.5; this.PHYSICS_STEP = 0.05; this.PHYSICS_MIN = 0.0; this.PHYSICS_MAX = 1.0; this.onButtonDown = this.onButtonDown.bind(this); this.onButtonUp = this.onButtonUp.bind(this); this.el.sceneEl.addEventListener('loaded', () => { this.leftHandEl = document.querySelector('#left-hand'); this.rightHandEl = document.querySelector('#right-hand'); if (this.leftHandEl) { this.leftHandEl.addEventListener('buttondown', this.onButtonDown); this.leftHandEl.addEventListener('buttonup', this.onButtonUp); } if (this.rightHandEl) { this.rightHandEl.addEventListener('buttondown', this.onButtonDown); this.rightHandEl.addEventListener('buttonup', this.onButtonUp); } }); }, // โ”€โ”€ Helpers para leer sliders y componente โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ _getVisualizer: function () { const el = document.querySelector('#visualizer'); return el ? el.components['babia-dome'] : null; }, _getStarSize: function () { const slider = document.getElementById('star-size'); return slider ? parseFloat(slider.value) : (window.currentStarScale || 1.0); }, _getPhysicsStrength: function () { const slider = document.getElementById('physics-strength'); return slider ? parseFloat(slider.value) : 0.6; }, // โ”€โ”€ Acciones โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ _increaseStarSize: function () { let val = Math.min(this.STAR_SIZE_MAX, parseFloat((this._getStarSize() + this.STAR_SIZE_STEP).toFixed(2))); const slider = document.getElementById('star-size'); if (slider) slider.value = val; const sizeValueEl = document.getElementById('size-value'); if (sizeValueEl) sizeValueEl.textContent = val.toFixed(1) + 'x'; window.currentStarScale = val; const baseRadius = this._getVisualizer() ? this._getVisualizer().data.nodeSize : 0.18; const stars = document.querySelectorAll('.interactive-node'); stars.forEach(star => { star.setAttribute('radius', baseRadius * val); }); console.log(`โญ Star size โ†’ ${val}`); }, _decreaseStarSize: function () { let val = Math.max(this.STAR_SIZE_MIN, parseFloat((this._getStarSize() - this.STAR_SIZE_STEP).toFixed(2))); const slider = document.getElementById('star-size'); if (slider) slider.value = val; const sizeValueEl = document.getElementById('size-value'); if (sizeValueEl) sizeValueEl.textContent = val.toFixed(1) + 'x'; window.currentStarScale = val; const baseRadius = this._getVisualizer() ? this._getVisualizer().data.nodeSize : 0.18; const stars = document.querySelectorAll('.interactive-node'); stars.forEach(star => { star.setAttribute('radius', baseRadius * val); }); console.log(`โญ Star size โ†’ ${val}`); }, _increasePhysics: function () { let val = Math.min(this.PHYSICS_MAX, parseFloat((this._getPhysicsStrength() + this.PHYSICS_STEP).toFixed(2))); const slider = document.getElementById('physics-strength'); if (slider) slider.value = val; const strengthValueEl = document.getElementById('strength-value'); if (strengthValueEl) strengthValueEl.textContent = val.toFixed(2); const vis = this._getVisualizer(); if (vis) vis.updatePhysicsStrength(val); console.log(`๐Ÿ’ช Physics โ†’ ${val}`); }, _decreasePhysics: function () { let val = Math.max(this.PHYSICS_MIN, parseFloat((this._getPhysicsStrength() - this.PHYSICS_STEP).toFixed(2))); const slider = document.getElementById('physics-strength'); if (slider) slider.value = val; const strengthValueEl = document.getElementById('strength-value'); if (strengthValueEl) strengthValueEl.textContent = val.toFixed(2); const vis = this._getVisualizer(); if (vis) vis.updatePhysicsStrength(val); console.log(`๐Ÿ’ช Physics โ†’ ${val}`); }, // โ”€โ”€ Dispatch por botรณn โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ _handleButton: function (hand, index) { const name = this.buttonMap[hand] && this.buttonMap[hand][index]; if (!name) return; if (name === 'Y') this._increaseStarSize(); else if (name === 'X') this._decreaseStarSize(); else if (name === 'B') this._increasePhysics(); else if (name === 'A') this._decreasePhysics(); }, onButtonDown: function (evt) { const hand = evt.target.id === 'left-hand' ? 'left' : 'right'; const button = evt.detail.id; const key = `${hand}-${button}`; const wasPressed = this.lastButtonState[key] || false; if (!wasPressed) { // Pulsaciรณn inicial this._handleButton(hand, button); // Iniciar repeticiรณn al mantener this.repeatTimers[key] = { timeout: setTimeout(() => { this.repeatTimers[key].interval = setInterval(() => { this._handleButton(hand, button); }, this.REPEAT_INTERVAL); }, this.REPEAT_DELAY) }; } this.lastButtonState[key] = true; }, onButtonUp: function (evt) { const hand = evt.target.id === 'left-hand' ? 'left' : 'right'; const button = evt.detail.id; const key = `${hand}-${button}`; // Botรณn soltado: cancelar repeticiรณn if (this.repeatTimers[key]) { clearTimeout(this.repeatTimers[key].timeout); clearInterval(this.repeatTimers[key].interval); delete this.repeatTimers[key]; } this.lastButtonState[key] = false; }, // โ”€โ”€ Tick: no necesitamos leer gamepads โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tick: function () { // Eventos manejan todo }, remove: function () { if (this.leftHandEl) { this.leftHandEl.removeEventListener('buttondown', this.onButtonDown); this.leftHandEl.removeEventListener('buttonup', this.onButtonUp); } if (this.rightHandEl) { this.rightHandEl.removeEventListener('buttondown', this.onButtonDown); this.rightHandEl.removeEventListener('buttonup', this.onButtonUp); } for (const key of Object.keys(this.repeatTimers)) { clearTimeout(this.repeatTimers[key].timeout); clearInterval(this.repeatTimers[key].interval); } this.repeatTimers = {}; } }); // ========== VR INFO PANEL COMPONENT ========== AFRAME.registerComponent('vr-info-panel', { schema: { width: { type: 'number', default: 256 }, height: { type: 'number', default: 192 }, distance: { type: 'number', default: 1.2 }, offsetX: { type: 'number', default: -0.4 }, offsetY: { type: 'number', default: -0.5 } }, init: function() { this.scene = this.el.sceneEl; this.canvas = null; this.texture = null; this.mesh = null; this.panelMesh = null; this.isVR = false; // Create canvas with info text this.createPanel(); // Listen for VR events this.scene.addEventListener('enter-vr', () => this.onEnterVR()); this.scene.addEventListener('exit-vr', () => this.onExitVR()); }, createPanel: function() { // Create canvas this.canvas = document.createElement('canvas'); this.canvas.width = this.data.width; this.canvas.height = this.data.height; const ctx = this.canvas.getContext('2d'); // Draw semi-transparent background (black) ctx.fillStyle = 'rgba(0, 0, 0, 0.9)'; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Draw border ctx.strokeStyle = 'rgba(255, 215, 0, 0.6)'; ctx.lineWidth = 2; ctx.strokeRect(3, 3, this.canvas.width - 6, this.canvas.height - 6); // Draw glow effect ctx.shadowColor = 'rgba(255, 215, 0, 0.4)'; ctx.shadowBlur = 20; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; // Title ctx.fillStyle = '#FFD700'; ctx.font = 'bold 16px Arial, sans-serif'; ctx.textAlign = 'left'; ctx.fillText('๐Ÿฅฝ VR CONTROLS', 12, 25); // Reset shadow for text ctx.shadowColor = 'transparent'; // Instructions const instructions = [ '๐Ÿ•น๏ธ LEFT/RIGHT JOYSTICK: Move around', '๐Ÿ” POINT with controllers: Look at stars', 'โญ HOVER on nodes: View star info', '๐Ÿ’ก TIP: Change size with buttons' ]; ctx.fillStyle = '#FFFFFF'; ctx.font = '10px Arial, sans-serif'; ctx.textAlign = 'left'; let yPos = 48; const lineHeight = 22; instructions.forEach(instruction => { if (instruction === '') { yPos += lineHeight * 0.5; } else { ctx.fillText(instruction, 30, yPos); yPos += lineHeight; } }); // Create THREE texture from canvas this.texture = new THREE.CanvasTexture(this.canvas); this.texture.minFilter = THREE.LinearFilter; this.texture.magFilter = THREE.LinearFilter; // Create material and geometry const material = new THREE.MeshBasicMaterial({ map: this.texture, transparent: true, side: THREE.DoubleSide }); const width = 0.5; const height = width * (this.canvas.height / this.canvas.width); const geometry = new THREE.PlaneGeometry(width, height); this.mesh = new THREE.Mesh(geometry, material); this.mesh.position.x = this.data.offsetX; this.mesh.position.y = this.data.offsetY; this.mesh.position.z = -this.data.distance; // Initially hidden this.mesh.visible = false; // Add to camera const camera = this.scene.querySelector('a-camera'); if (camera && camera.object3D) { camera.object3D.add(this.mesh); } }, onEnterVR: function() { this.isVR = true; if (this.mesh) { this.mesh.visible = true; console.log('๐Ÿฅฝ VR Info Panel shown'); } }, onExitVR: function() { this.isVR = false; if (this.mesh) { this.mesh.visible = false; console.log('๐Ÿ“บ VR Info Panel hidden'); } }, remove: function() { if (this.mesh) { this.mesh.parent.remove(this.mesh); } if (this.texture) { this.texture.dispose(); } if (this.canvas) { this.canvas = null; } } }); // ========== END VR INFO PANEL ========== // Duplicate functions removed: use the single canonical implementations above. // The synchronization with component values is handled by `syncComponentValuesToUI`. </script> </body> </html>