UNPKG

aframe-babia-components

Version:

A data visualization set of components for A-Frame.

1,492 lines (1,266 loc) 61.1 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>npm package dependencies</title> <script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v7.2.0/dist/aframe-extras.min.js"></script> <script src="/dist/aframe-babia-components.js"></script> <style> body { margin: 0; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #000000; } /* ── Info panel (top-left) ───────────────────────────────────────────── */ #info-panel { position: absolute; top: 20px; left: 20px; background: rgba(0, 10, 20, 0.95); color: white; padding: 25px; border-radius: 15px; max-width: 350px; z-index: 1000; backdrop-filter: blur(15px); border: 2px solid rgba(39, 238, 245, 0.4); box-shadow: 0 8px 32px rgba(39, 238, 245, 0.15); } #info-panel h2 { margin-top: 0; color: #27EEF5; font-size: 20px; display: flex; align-items: center; gap: 10px; text-shadow: 0 0 10px rgba(39, 238, 245, 0.5); } #info-panel p { margin: 10px 0; font-size: 14px; line-height: 1.6; } .badge { display: inline-block; background: linear-gradient(135deg, #27EEF5, #0099a8); color: #000; padding: 4px 10px; border-radius: 15px; font-size: 11px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; } .vr-badge { background: linear-gradient(135deg, #FF1493, #FF69B4); color: #fff; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.8; transform: scale(1.05); } } /* ── Bottom controls ─────────────────────────────────────────────────── */ #controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 10, 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(39, 238, 245, 0.4); max-width: 90vw; box-shadow: 0 8px 32px rgba(39, 238, 245, 0.15); } #controls button { background: linear-gradient(135deg, #27EEF5, #0099a8); 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(39, 238, 245, 0.25); text-transform: uppercase; letter-spacing: 0.5px; } #controls button:hover { background: linear-gradient(135deg, #0099a8, #006d78); transform: translateY(-3px); box-shadow: 0 6px 20px rgba(39, 238, 245, 0.4); } #controls button.active { background: linear-gradient(135deg, #FF1493, #FF69B4); color: white; } .divider { width: 2px; height: 35px; background: linear-gradient(180deg, rgba(39, 238, 245, 0), rgba(39, 238, 245, 0.5), rgba(39, 238, 245, 0)); margin: 0 10px; } .slider-container { display: flex; align-items: center; gap: 10px; color: white; font-size: 13px; background: rgba(39, 238, 245, 0.08); padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(39, 238, 245, 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; width: 18px; height: 18px; background: linear-gradient(135deg, #27EEF5, #0099a8); border-radius: 50%; cursor: pointer; box-shadow: 0 2px 8px rgba(39, 238, 245, 0.5); } input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; background: linear-gradient(135deg, #27EEF5, #0099a8); border-radius: 50%; cursor: pointer; border: none; } /* ── Legend (top-right) ──────────────────────────────────────────────── */ .legend { position: absolute; top: 20px; right: 20px; background: rgba(0, 10, 20, 0.95); padding: 20px; border-radius: 15px; color: white; z-index: 1000; backdrop-filter: blur(15px); border: 2px solid rgba(39, 238, 245, 0.4); max-width: 260px; box-shadow: 0 8px 32px rgba(39, 238, 245, 0.15); } .legend h3 { margin-top: 0; font-size: 16px; color: #27EEF5; margin-bottom: 12px; text-shadow: 0 0 8px rgba(39, 238, 245, 0.4); } .legend-item { display: flex; align-items: center; margin: 8px 0; font-size: 13px; } .legend-color { width: 20px; height: 20px; border-radius: 50%; margin-right: 10px; flex-shrink: 0; } .legend-section { margin-top: 16px; padding-top: 12px; border-top: 1px solid rgba(39, 238, 245, 0.25); } /* ── Loading screen ──────────────────────────────────────────────────── */ #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 22px; z-index: 2000; text-align: center; background: rgba(0, 10, 20, 0.95); padding: 50px; border-radius: 20px; backdrop-filter: blur(15px); border: 2px solid rgba(39, 238, 245, 0.5); } .spinner { border: 5px solid rgba(39, 238, 245, 0.2); border-top: 5px solid #27EEF5; border-radius: 50%; width: 70px; height: 70px; animation: spin 1s linear infinite; margin: 25px auto; box-shadow: 0 0 20px rgba(39, 238, 245, 0.4); } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .stats { margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(39, 238, 245, 0.25); font-size: 13px; } .stats p { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; } .stat-value { color: #27EEF5; font-weight: bold; text-shadow: 0 0 5px rgba(39, 238, 245, 0.4); } .vr-info { background: rgba(255, 20, 147, 0.15); border: 1px solid rgba(255, 105, 180, 0.4); 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> <!-- ── Loading ─────────────────────────────────────────────────────────── --> <div id="loading"> <div class="spinner"></div> <p>📦 npm package dependencies</p> <p style="font-size: 16px; color: #444b4b;">Resolving dependency tree…</p> </div> <!-- ── Info panel ──────────────────────────────────────────────────────── --> <div id="info-panel"> <h2> 📦 npm dependencies <span class="badge">Use Case</span> <span class="badge vr-badge">VR</span> </h2> <p style="color: #414546; font-weight: bold; font-size: 15px;"> express dependency tree </p> <p><strong>🖥️ Desktop:</strong></p> <div class="instruction">🖱️ Drag to rotate view</div> <div class="instruction">👆 Hover on a package node</div> <div class="vr-info"> <p><strong>🥽 VR Mode:</strong></p> <div class="instruction">🕹️ Left joystick to move</div> <div class="instruction">🎮 Point with controllers</div> <div class="instruction">📦 View package info</div> </div> <p id="node-count" style="color:#aaa;">Loading packages…</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> <!-- ── Legend ──────────────────────────────────────────────────────────── --> <div class="legend"> <h3>🎯 Node Color (CVE Severity)</h3> <div class="legend-item"> <div class="legend-color" style="background:#1a3a5c; box-shadow:0 0 8px #1a3a5c;"></div> <span>No CVE / No vulnerabilities</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#00ff00; box-shadow:0 0 8px #00ff00;"></div> <span>Low severity (0-3)</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#ffff00; box-shadow:0 0 8px #ffff00;"></div> <span>Medium severity (4-6)</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#ff0000; box-shadow:0 0 8px #ff0000;"></div> <span>High severity (7-10)</span> </div> <div class="legend-section"> <h3>📏 Node Size (Package Size)</h3> <p style="font-size:12px; line-height:1.7; color:#ccc;"> Sphere radius proportional<br> to npm package size (KB)<br> Small = lightweight<br> Large = heavy package </p> </div> <div class="legend-section"> <h3>🔗 Links (Dependency Type)</h3> <div class="legend-item"> <div class="legend-color" style="background:#3050F8; border-radius:3px; height:4px; width:24px; margin-right:10px;"></div> <span>Direct dependency</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#808080; border-radius:3px; height:4px; width:24px; margin-right:10px;"></div> <span>Transitive dependency</span> </div> </div> <div class="legend-section"> <h3>📜 Node Color (License)</h3> <div class="legend-item"> <div class="legend-color" style="background:#ff00ff; box-shadow:0 0 8px #ff00ff;"></div> <span>AGPL (strong copyleft)</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#ff2222; box-shadow:0 0 8px #ff2222;"></div> <span>GPL (restrictive)</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#ff8800; box-shadow:0 0 8px #ff8800;"></div> <span>LGPL</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#ffcc00; box-shadow:0 0 8px #ffcc00;"></div> <span>MPL</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#00aaff; box-shadow:0 0 8px #00aaff;"></div> <span>Apache</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#00ddaa; box-shadow:0 0 8px #00ddaa;"></div> <span>BSD</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#33cc66; box-shadow:0 0 8px #33cc66;"></div> <span>MIT / ISC (permissive)</span> </div> <div class="legend-item"> <div class="legend-color" style="background:#666666; box-shadow:0 0 8px #666666;"></div> <span>Unknown</span> </div> </div> <div class="legend-section"> <h3>💡 How to read it</h3> <p style="font-size:12px; line-height:1.7; color:#ccc;"> Red nodes = vulnerable<br> Large nodes = big packages<br> Blue links = direct deps<br> Grey links = indirect deps </p> </div> </div> <!-- ── Bottom controls ─────────────────────────────────────────────────── --> <div id="controls"> <button id="toggle-links" onclick="toggleLinks()">🔗 Links</button> <button onclick="toggleAnimation()">⏯️ Rotation</button> <div class="divider"></div> <div class="slider-container"> <span>🎨 Color:</span> <select id="color-mode" onchange="setColorMode(this.value)" style="background:#0a1a2a; color:#27EEF5; border:1px solid rgba(39,238,245,0.4); border-radius:6px; padding:4px 6px; font-size:12px;"> <option value="cve" selected>CVE severity</option> <option value="license">License</option> </select> </div> <div class="slider-container"> <span>📜 License:</span> <input type="text" id="license-filter" placeholder="GPL, AGPL…" oninput="setLicenseFilter(this.value)" style="background:#0a1a2a; color:#27EEF5; border:1px solid rgba(39,238,245,0.4); border-radius:6px; padding:4px 6px; font-size:12px; width:120px;"> </div> <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.4" oninput="updatePhysicsStrength(this.value)"> <span id="strength-value">0.4</span> </div> <div class="slider-container"> <span>📦 Size:</span> <input type="range" id="node-size-slider" min="0.5" max="2.5" step="0.1" value="1" oninput="updateStarSize(this.value)"> <span id="size-value">1.0</span> </div> <div class="divider"></div> <button onclick="resetView()">🔄 Reset</button> </div> <!-- ── A-Frame scene ───────────────────────────────────────────────────── --> <a-scene xr-mode-ui="XRMode: xr" background="color: #000008" renderer="antialias: true; colorManagement: true; physicallyCorrectLights: true; exposure: 1.5" > <!-- 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 --> <!-- En tu cámara --> <a-camera id="main-camera" position="0 0 0" look-controls wasd-controls="acceleration: 40; fly: true" vertical-controls="speed: 5" vr-info-panel vr-color-toggle 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: express-dome.json; sphereRadius: 18; nodeSize: 0.18; showLabels: false; animateNodes: true; colorByValue: false; nodeOpacity: 1.0; enablePhysics: true; physicsStrength: 0.1; collisionRadius: 0; showLinks: true; linkSegments: 50; linkOpacity: 0.4; linkWidth: 0.02; linkStyle: line; colorByCVE: true; sizeByPackageSize: true; linkColorByType: true; minNodeSize: 0.1; maxNodeSize: 0.4; "></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> // Vertical fly controls for desktop: Space / E = up, Shift / Q = down AFRAME.registerComponent('vertical-controls', { schema: { speed: { type: 'number', default: 5 } }, init: function () { this.keys = { up: false, down: false }; this.onKeyDown = (e) => { if (e.code === 'Space' || e.code === 'KeyE') this.keys.up = true; else if (e.code === 'ShiftLeft' || e.code === 'ShiftRight' || e.code === 'KeyQ') this.keys.down = true; }; this.onKeyUp = (e) => { if (e.code === 'Space' || e.code === 'KeyE') this.keys.up = false; else if (e.code === 'ShiftLeft' || e.code === 'ShiftRight' || e.code === 'KeyQ') this.keys.down = false; }; window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); }, remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); }, tick: function (time, deltaTime) { if (!deltaTime) return; // Skip in VR (handled by thumbstick locomotion) if (this.el.sceneEl && (this.el.sceneEl.is('vr-mode') || this.el.sceneEl.is('ar-mode'))) return; const dt = deltaTime / 1000; let dy = 0; if (this.keys.up) dy += this.data.speed * dt; if (this.keys.down) dy -= this.data.speed * dt; if (dy !== 0) { // Move the camera rig if exists, otherwise the camera itself const rig = document.getElementById('camera-rig'); const target = rig ? rig.object3D : this.el.object3D; target.position.y += dy; } } }); 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.normalize(); // 'right' debe permanecer horizontal para que el strafe no incline 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) { // ✅ Sincronizar valores del componente con los sliders syncComponentValuesToUI(visualizerComponent); updateLicenseStats(); } }, 1500); }); 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 = {}; } }); // 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 updateLicenseStats() { if (!visualizerComponent || !visualizerComponent.getLicenseStats) return; const stats = visualizerComponent.getLicenseStats(); let gplTotal = 0; for (const [lic, c] of Object.entries(stats)) { if (lic.toUpperCase().includes('GPL')) gplTotal += c; // includes LGPL/AGPL/GPL } const gplEl = document.getElementById('gpl-count'); if (gplEl) gplEl.textContent = gplTotal; refreshLicenseMatchCount(); } function refreshLicenseMatchCount() { if (!visualizerComponent || !visualizerComponent.getLicenseStats) return; const input = document.getElementById('license-filter'); const el = document.getElementById('license-match-count'); if (!el) return; const str = (input && input.value || '').trim(); if (!str) { el.textContent = 'all'; return; } const tokens = str.toUpperCase().split(',').map(s => s.trim()).filter(Boolean); const stats = visualizerComponent.getLicenseStats(); let n = 0; for (const [lic, c] of Object.entries(stats)) { if (tokens.some(t => lic.toUpperCase().includes(t))) n += c; } el.textContent = n; } function setColorMode(mode) { if (!visualizerComponent || !visualizerComponent.setColorMode) return; visualizerComponent.setColorMode(mode); } function setLicenseFilter(str) { if (!visualizerComponent || !visualizerComponent.setLicenseFilter) return; visualizerComponent.setLicenseFilter(str); refreshLicenseMatchCount(); } 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); }); } ; // ========== 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); } this.createCloseButton(); }, createCloseButton: function() { const camera = this.scene.querySelector('a-camera'); if (!camera) return; // Calcular posición esquina superior derecha del panel const panelWidth = 0.5; const panelHeight = panelWidth * (this.data.height / this.data.width); // Posición absoluta = posición del panel + offset esquina const btnX = this.data.offsetX + panelWidth / 2 - 0.03; const btnY = this.data.offsetY + panelHeight / 2 - 0.03; const btnZ = -(this.data.distance - 0.01); // ligeramente por delante // Crear como a-entity para que el raycaster lo detecte this.closeBtnEl = document.createElement('a-entity'); this.closeBtnEl.setAttribute('class', 'interactive-node'); // ← clave this.closeBtnEl.setAttribute('position', `${btnX} ${btnY} ${btnZ}`); this.closeBtnEl.setAttribute('visible', 'false'); this.closeBtnEl.setAttribute('geometry', 'primitive: plane; width: 0.06; height: 0.06'); this.closeBtnEl.setAttribute('material', 'color: #cc2222; opacity: 0.95; transparent: true; shader: flat'); // Canvas para dibujar la X encima const xCanvas = document.createElement('canvas'); xCanvas.width = 64; xCanvas.height = 64; const xCtx = xCanvas.getContext('2d'); xCtx.fillStyle = '#cc2222'; xCtx.fillRect(255, 255, 255, 0); xCtx.strokeStyle = '#ffffff'; xCtx.lineWidth = 8; xCtx.lineCap = 'round'; xCtx.beginPath(); xCtx.moveTo(14, 14); xCtx.lineTo(50, 50); xCtx.stroke(); xCtx.beginPath(); xCtx.moveTo(50, 14); xCtx.lineTo(14, 50); xCtx.stroke(); const xTexture = new THREE.CanvasTexture(xCanvas); // Aplicar textura cuando el elemento cargue this.closeBtnEl.addEventListener('loaded', () => { if (this.closeBtnEl.getObject3D('mesh')) { this.closeBtnEl.getObject3D('mesh').material.map = xTexture; this.closeBtnEl.getObject3D('mesh').material.needsUpdate = true; } }); // Escuchar el click/trigger sobre el propio botón this.closeBtnEl.addEventListener('click', () => { console.log('❌ Panel VR cerrado'); this.hidePanel(); }); // Añadir a la cámara (mismo padre que el panel) camera.appendChild(this.closeBtnEl); }, hidePanel: function() { if (this.mesh) this.mesh.visible = false; if (this.closeBtnEl) this.closeBtnEl.setAttribute('visible', 'false'); this._panelDismissed = true; }, onEnterVR: function() { this.isVR = true; if (!this._panelDismissed) { if (this.mesh) this.mesh.visible = true; if (this.closeBtnEl) this.closeBtnEl.setAttribute('visible', 'true'); } }, onExitVR: function() { this.isVR = false; if (this.mesh) this.mesh.visible = false; if (this.closeBtnEl) this.closeBtnEl.setAttribute('visible', 'false'); this._panelDismissed = false; // resetear para próxima sesión VR }, remove: function() { if (this._onTrigger) { const leftHand = document.querySelector('#left-hand'); const rightHand = document.querySelector('#right-hand'); if (leftHand) leftHand.removeEventListener('triggerdown', this._onTrigger); if (rightHand) rightHand.removeEventListener('triggerdown', this._onTrigger); } if (this.closeBtn) { if (this.closeBtn.geometry) this.closeBtn.geometry.dispose(); if (this.closeBtn.material) this.closeBtn.material.dispose(); } if (this.mesh) { this.mesh.parent && this.mesh.parent.remove(this.mesh); } if (this.texture) this.texture.dispose(); this.canvas = null; }, }); // ========== END VR INFO PANEL ========== // ========== VR COLOR MODE TOGGLE ========== AFRAME.registerComponent('vr-color-toggle', { schema: { width: { type: 'number', default: 0.22 }, height: { type: 'number', default: 0.09 }, distance: { type: 'number', default: 0.9 }, off