aframe-babia-components
Version:
A data visualization set of components for A-Frame.
1,320 lines (1,138 loc) โข 45 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๐ข VR Corporate Network - Coca-Cola</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 โโ */
#info-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(10, 0, 0, 0.95);
color: white;
padding: 25px;
border-radius: 15px;
max-width: 340px;
z-index: 1000;
backdrop-filter: blur(15px);
border: 2px solid rgba(220, 30, 30, 0.5);
box-shadow: 0 8px 32px rgba(220, 30, 30, 0.2);
}
#info-panel h2 {
margin-top: 0;
color: #FF4444;
font-size: 20px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
text-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
}
#info-panel p {
margin: 10px 0;
font-size: 14px;
line-height: 1.6;
}
.badge {
display: inline-block;
background: linear-gradient(135deg, #FF4444, #CC0000);
color: #fff;
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); }
}
.stats {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 68, 68, 0.3);
font-size: 13px;
}
.stats p {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
}
.stat-value {
color: #FF4444;
font-weight: bold;
text-shadow: 0 0 5px rgba(255, 68, 68, 0.5);
}
.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;
}
/* โโ LEGEND โโ */
.legend {
position: absolute;
top: 20px;
right: 20px;
background: rgba(10, 0, 0, 0.95);
padding: 20px;
border-radius: 15px;
color: white;
z-index: 1000;
backdrop-filter: blur(15px);
border: 2px solid rgba(220, 30, 30, 0.5);
max-width: 240px;
box-shadow: 0 8px 32px rgba(220, 30, 30, 0.2);
}
.legend h3 {
margin-top: 0;
font-size: 16px;
color: #FF4444;
margin-bottom: 14px;
text-shadow: 0 0 8px rgba(255, 68, 68, 0.5);
}
.legend-item {
display: flex;
align-items: center;
margin: 9px 0;
font-size: 12px;
}
.legend-dot {
width: 22px;
height: 22px;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
box-shadow: 0 0 10px currentColor;
}
.legend-line {
width: 28px;
height: 3px;
margin-right: 10px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-section {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid rgba(255, 68, 68, 0.25);
}
/* โโ CONTROLS โโ */
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(10, 0, 0, 0.95);
padding: 18px 28px;
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(220, 30, 30, 0.5);
max-width: 90vw;
box-shadow: 0 8px 32px rgba(220, 30, 30, 0.2);
}
#controls button {
background: linear-gradient(135deg, #FF4444, #CC0000);
border: none;
color: #fff;
padding: 11px 18px;
border-radius: 10px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
font-weight: 700;
box-shadow: 0 4px 15px rgba(255, 68, 68, 0.3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
#controls button:hover {
background: linear-gradient(135deg, #FF6666, #FF2200);
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(255, 68, 68, 0.5);
}
#controls button.active {
background: linear-gradient(135deg, #FF1493, #FF69B4);
}
.divider {
width: 2px;
height: 35px;
background: linear-gradient(180deg, rgba(255,68,68,0), rgba(255,68,68,0.5), rgba(255,68,68,0));
margin: 0 8px;
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
color: white;
font-size: 13px;
background: rgba(255, 68, 68, 0.1);
padding: 10px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 68, 68, 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, #FF4444, #CC0000);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 8px rgba(255, 68, 68, 0.5);
}
input[type="range"]::-moz-range-thumb {
width: 18px; height: 18px;
background: linear-gradient(135deg, #FF4444, #CC0000);
border-radius: 50%; cursor: pointer; border: none;
box-shadow: 0 2px 8px rgba(255, 68, 68, 0.5);
}
/* โโ SELECT FILTER โโ */
.select-container {
display: flex;
align-items: center;
gap: 10px;
color: white;
font-size: 13px;
background: rgba(255, 68, 68, 0.1);
padding: 10px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 68, 68, 0.3);
}
select {
background: rgba(20, 0, 0, 0.9);
color: #fff;
border: 1px solid rgba(255, 68, 68, 0.5);
border-radius: 6px;
padding: 5px 10px;
font-size: 13px;
cursor: pointer;
outline: none;
}
select:focus { border-color: #FF4444; }
/* โโ LOADING โโ */
#loading {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 26px;
z-index: 2000;
text-align: center;
background: rgba(10, 0, 0, 0.95);
padding: 50px;
border-radius: 20px;
backdrop-filter: blur(15px);
border: 2px solid rgba(255, 68, 68, 0.5);
}
.spinner {
border: 5px solid rgba(255, 68, 68, 0.2);
border-top: 5px solid #FF4444;
border-radius: 50%;
width: 70px; height: 70px;
animation: spin 1s linear infinite;
margin: 25px auto;
box-shadow: 0 0 20px rgba(255, 68, 68, 0.4);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<!-- LOADING SCREEN -->
<div id="loading">
<div class="spinner"></div>
<p>๐ข Loading Corporate Network...</p>
<p style="font-size: 16px; color: #FF4444;">Mapping subsidiaries</p>
</div>
<!-- INFO PANEL -->
<div id="info-panel">
<h2>
๐ข Corporate Network
<span class="badge">Use Case</span>
<span class="badge vr-badge">VR</span>
</h2>
<p style="color: #FF4444; font-weight: bold; font-size: 15px;">๐ฅค The Coca-Cola Company</p>
<p><strong>๐ฅ๏ธ Desktop:</strong></p>
<div class="instruction">๐ฑ๏ธ Drag to rotate view</div>
<div class="instruction">๐ Hover on nodes to see info</div>
<div class="instruction">โจ๏ธ WASD to move</div>
<div class="vr-info">
<p><strong>๐ฅฝ VR Mode:</strong></p>
<div class="instruction">๐ฎ Left joystick to move</div>
<div class="instruction">๐ฎ Right joystick to rotate camera</div>
<div class="instruction">๐ Point at nodes for info</div>
</div>
<p id="node-count" style="color:#aaa; font-size:13px;">Loading network...</p>
<div class="stats">
<p><span>โก Physics:</span><span id="physics-status" class="stat-value">Active</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 Types</h3>
<div class="legend-item">
<div class="legend-dot" style="background:#FF0000; color:#FF0000;"></div>
<span>Parent Corp.</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background:#FF3333; color:#FF3333;"></div>
<span>Beverage</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background:#FFAA00; color:#FFAA00;"></div>
<span>Juice</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background:#CC6600; color:#CC6600;"></div>
<span>Coffee</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background:#0044CC; color:#0044CC;"></div>
<span>Sports</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background:#0099FF; color:#0099FF;"></div>
<span>Water</span>
</div>
<div class="legend-section">
<h3>๐ Link Types</h3>
<div class="legend-item">
<div class="legend-line" style="background:#ffffff;"></div>
<span>Direct ownership</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background:#27EEF5;"></div>
<span>Strategic stake</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background:#FF0D0D;"></div>
<span>Sub-brand</span>
</div>
</div>
</div>
<!-- CONTROLS -->
<div id="controls">
<button id="toggle-links" onclick="toggleLinks()">๐ Links</button>
<button onclick="toggleLabels()" id="toggle-labels-btn">๐ท๏ธ Labels</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.5"
oninput="updatePhysicsStrength(this.value)">
<span id="strength-value">0.50</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="updateNodeSize(this.value)">
<span id="size-value">1.0x</span>
</div>
<div class="divider"></div>
<div class="divider"></div>
<button onclick="resetView()">๐ Reset</button>
</div>
<!-- A-FRAME SCENE -->
<a-scene
xr-mode-ui="XRMode: xr"
background="color: #020005"
renderer="antialias: true; colorManagement: true; physicallyCorrectLights: true; exposure: 1.4">
<a-assets>
<!-- Add company logo textures here if desired -->
<!-- <img id="texture-cocacola" src="images/coca-cola-logo.png"> -->
</a-assets>
<!-- Mouse raycaster -->
<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">
<!-- 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-gamepad-controls></a-camera>
<!-- LEFT VR Controller -->
<a-entity
id="left-hand"
laser-controls="hand: left"
raycaster="objects: .interactive-node; far: 50; lineColor: #FF4444; lineOpacity: 0.8"
line="color: #FF4444; 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>
<!-- Lighting -->
<a-entity light="type: ambient; color: #FFE8E8; 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: #FF8888; intensity: 0.4" position="-5 -5 -5"></a-entity>
<a-entity light="type: point; color: #FF4444; intensity: 0.5; distance: 100" position="0 0 0"></a-entity>
<!-- Corporate Network Visualizer -->
<a-entity id="visualizer" babia-dome="
dataUrl: data.json;
sphereRadius: 18;
nodeSize: 0.18;
showLabels: true;
animateNodes: true;
colorByValue: false;
nodeOpacity: 1.0;
enablePhysics: true;
physicsStrength: 0.3;
collisionRadius: 0.1;
showLinks: true;
linkSegments: 50;
linkOpacity: 0.8;
linkWidth: 0.025;
linkStyle: line;
"></a-entity>
<!-- Background particle container -->
<a-entity id="bg-particles"></a-entity>
<a-sky hide-on-enter-ar color="#020005" material="shader: flat"></a-sky>
</a-scene>
<!-- โโ SCRIPTS โโ -->
<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;
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) {
const rig = document.getElementById('camera-rig');
const target = rig ? rig.object3D : this.el.object3D;
target.position.y += dy;
}
}
});
// โโ VR THUMBSTICK LOCOMOTION โโ
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));
}
}
});
// โโ BACKGROUND PARTICLES (subtle grid dots) โโ
function createBackgroundParticles() {
const container = document.getElementById('bg-particles');
const count = 800;
const minR = 22, maxR = 55;
for (let i = 0; i < count; i++) {
const dot = document.createElement('a-sphere');
const phi = Math.random() * Math.PI * 2;
const theta = Math.random() * Math.PI;
const radius = minR + Math.random() * (maxR - minR);
const x = radius * Math.sin(theta) * Math.cos(phi);
const y = radius * Math.sin(theta) * Math.sin(phi);
const z = radius * Math.cos(theta);
const size = 0.015 + Math.random() * 0.025;
const opacity = 0.15 + Math.random() * 0.4;
// Mostly white-grey, occasional red tint
const colors = ['#FFFFFF', '#FFDDDD', '#FFE8E8', '#CCCCCC', '#FFBBBB'];
const color = colors[Math.floor(Math.random() * colors.length)];
dot.setAttribute('radius', size);
dot.setAttribute('position', `${x} ${y} ${z}`);
dot.setAttribute('material', { shader: 'flat', color, opacity, transparent: true });
if (Math.random() > 0.75) {
dot.setAttribute('animation', {
property: 'components.material.material.opacity',
from: opacity,
to: opacity * 0.2,
dur: 2500 + Math.random() * 3500,
dir: 'alternate', loop: true, easing: 'easeInOutSine'
});
}
container.appendChild(dot);
}
}
document.querySelector('a-scene').addEventListener('loaded', createBackgroundParticles);
</script>
<script>
// โโ STATE โโ
let animationEnabled = true;
let linksEnabled = true;
let labelsEnabled = true;
let isVRMode = false;
let visualizerComponent = null;
let currentNodeScale = 1.0;
let frameCount = 0, lastFPSUpdate = Date.now();
let allNodeData = [];
// โโ FPS โโ
function updateFPS() {
frameCount++;
const now = Date.now();
if (now - lastFPSUpdate >= 1000) {
const fps = Math.round(frameCount * 1000 / (now - lastFPSUpdate));
const el = document.getElementById('fps-counter');
if (el) el.textContent = fps;
frameCount = 0;
lastFPSUpdate = now;
}
requestAnimationFrame(updateFPS);
}
// โโ SYNC SLIDERS FROM INLINE ATTRS โโ
function syncSlidersFromAttrs() {
const visualizer = document.querySelector('#visualizer');
if (!visualizer) return;
const raw = visualizer.getAttribute('babia-dome');
if (!raw || typeof raw !== 'string') return;
const psMatch = raw.match(/physicsStrength\s*:\s*([0-9.]+)/);
if (psMatch) {
const val = parseFloat(psMatch[1]);
const sl = document.getElementById('physics-strength');
const vl = document.getElementById('strength-value');
if (sl) sl.value = val;
if (vl) vl.textContent = val.toFixed(2);
}
const nsMatch = raw.match(/nodeSize\s*:\s*([0-9.]+)/);
if (nsMatch) {
const nodeSize = parseFloat(nsMatch[1]);
const scale = nodeSize / 0.18;
const sl = document.getElementById('node-size-slider');
const vl = document.getElementById('size-value');
if (sl) sl.value = scale;
if (vl) vl.textContent = scale.toFixed(1) + 'x';
currentNodeScale = scale;
}
}
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 = {};
}
});
// โโ SYNC FROM COMPONENT โโ
function syncComponentToUI(component) {
const ps = component.data.physicsStrength;
const sl = document.getElementById('physics-strength');
const vl = document.getElementById('strength-value');
if (sl) sl.value = ps;
if (vl) vl.textContent = ps.toFixed(2);
const ns = component.data.nodeSize;
const scale = ns / 0.18;
const sl2 = document.getElementById('node-size-slider');
const vl2 = document.getElementById('size-value');
if (sl2) sl2.value = scale;
if (vl2) vl2.textContent = scale.toFixed(1) + 'x';
currentNodeScale = scale;
}
// โโ INIT โโ
document.addEventListener('DOMContentLoaded', function () {
const scene = document.querySelector('a-scene');
const visualizer = document.querySelector('#visualizer');
const loading = document.getElementById('loading');
updateFPS();
syncSlidersFromAttrs();
scene.addEventListener('enter-vr', function () {
isVRMode = true;
const el = document.getElementById('vr-status');
if (el) { el.textContent = 'Active'; el.style.color = '#FF69B4'; }
document.getElementById('info-panel').style.display = 'none';
document.getElementById('controls').style.display = 'none';
document.querySelector('.legend').style.display = 'none';
animationEnabled = false;
});
scene.addEventListener('exit-vr', function () {
isVRMode = false;
const el = document.getElementById('vr-status');
if (el) { el.textContent = 'Inactive'; el.style.color = '#FF4444'; }
document.getElementById('info-panel').style.display = 'block';
document.getElementById('controls').style.display = 'flex';
document.querySelector('.legend').style.display = 'block';
});
scene.addEventListener('loaded', function () {
setTimeout(() => {
if (loading) loading.style.display = 'none';
visualizerComponent = visualizer.components['babia-dome'];
if (visualizerComponent) {
const nodes = visualizerComponent.simulationNodes.length;
const links = visualizerComponent.simulationLinks.length;
const groups = visualizerComponent.constellations ? visualizerComponent.constellations.length : 0;
document.getElementById('node-count').textContent = `๐ข ${nodes} companies mapped`;
document.getElementById('companies-count').textContent = nodes;
document.getElementById('links-count').textContent = links;
document.getElementById('groups-count').textContent = groups;
syncComponentToUI(visualizerComponent);
}
}, 1500);
});
// Auto-rotation
let rotation = 0;
function autoRotate() {
if (animationEnabled && visualizer && !isVRMode) {
rotation += 0.00008;
if (visualizer.object3D) visualizer.object3D.rotation.y = rotation;
}
requestAnimationFrame(autoRotate);
}
autoRotate();
// VR controller events
['#left-hand', '#right-hand'].forEach(sel => {
const el = document.querySelector(sel);
if (el) el.addEventListener('controllerconnected', e =>
console.log(`๐ฎ Controller connected: ${e.detail.name}`)
);
});
});
// โโ CONTROL FUNCTIONS โโ
function toggleLinks() {
linksEnabled = !linksEnabled;
const btn = document.getElementById('toggle-links');
if (visualizerComponent) {
visualizerComponent.toggleLinks(linksEnabled);
if (btn) {
btn.classList.toggle('active', !linksEnabled);
btn.textContent = linksEnabled ? '๐ Links' : '๐ Hidden';
}
}
}
function toggleLabels() {
labelsEnabled = !labelsEnabled;
const btn = document.getElementById('toggle-labels-btn');
if (visualizerComponent) {
visualizerComponent.toggleLabels(labelsEnabled);
if (btn) {
btn.classList.toggle('active', !labelsEnabled);
btn.textContent = labelsEnabled ? '๐ท๏ธ Labels' : '๐ท๏ธ Hidden';
}
}
}
function toggleAnimation() {
animationEnabled = !animationEnabled;
}
function updateNodeSize(value) {
currentNodeScale = parseFloat(value);
const vl = document.getElementById('size-value');
if (vl) vl.textContent = currentNodeScale.toFixed(1) + 'x';
const base = visualizerComponent ? visualizerComponent.data.nodeSize : 0.18;
document.querySelectorAll('.interactive-node').forEach(n =>
n.setAttribute('radius', base * currentNodeScale)
);
}
function updatePhysicsStrength(value) {
const val = parseFloat(value);
const vl = document.getElementById('strength-value');
if (vl) vl.textContent = val.toFixed(2);
if (visualizerComponent) visualizerComponent.updatePhysicsStrength(val);
}
function resetView() {
const rig = document.querySelector('#camera-rig');
if (rig) { rig.setAttribute('position', '0 0 0'); rig.object3D.rotation.set(0,0,0); }
const cam = document.querySelector('a-camera');
if (cam) { cam.setAttribute('position', '0 0 0'); cam.setAttribute('rotation', '0 0 0'); }
if (visualizerComponent) {
const origPS = visualizerComponent.data.physicsStrength;
updatePhysicsStrength(origPS);
const sl = document.getElementById('physics-strength');
if (sl) sl.value = origPS;
const origNS = visualizerComponent.data.nodeSize;
const origScale = origNS / 0.18;
updateNodeSize(origScale);
const sl2 = document.getElementById('node-size-slider');
if (sl2) sl2.value = origScale;
if (visualizerComponent.simulation) visualizerComponent.simulation.alpha(1).restart();
}
// Reset region filter
animationEnabled = true;
if (!linksEnabled && visualizerComponent) {
linksEnabled = true;
visualizerComponent.toggleLinks(true);
const btn = document.getElementById('toggle-links');
if (btn) { btn.classList.remove('active'); btn.textContent = '๐ Links'; }
}
if (!labelsEnabled && visualizerComponent) {
labelsEnabled = true;
visualizerComponent.toggleLabels(true);
const btn = document.getElementById('toggle-labels-btn');
if (btn) { btn.classList.remove('active'); btn.textContent = '๐ท๏ธ Labels'; }
}
}
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 ==========
// Duplicate functions removed: use the single canonical implementations above.
// The synchronization with component values is handled by `syncComponentValuesToUI`.
</script>
</body>
</html>