Spaces:
Running
Running
Upload 10 files
Browse files- index.html +73 -1536
- src/config.js +4 -0
- src/db.js +6 -0
- src/firebase.js +19 -0
- src/gemini.js +3 -0
- src/main.js +52 -0
- src/scene.js +33 -0
- src/ui.js +10 -0
- src/utils.js +4 -0
- styles.css +1 -74
index.html
CHANGED
|
@@ -1,1551 +1,88 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="es">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0
|
| 6 |
-
<title>ECOTAGS
|
| 7 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
-
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;
|
| 9 |
-
<
|
| 10 |
-
:root {
|
| 11 |
-
--color-bg: #030508;
|
| 12 |
-
--color-primary: #00f3ff;
|
| 13 |
-
--color-secondary: #0066ff;
|
| 14 |
-
--color-danger: #ff003c;
|
| 15 |
-
--color-success: #00ff9d;
|
| 16 |
-
--color-text: #e0f7ff;
|
| 17 |
-
--color-panel: rgba(5, 10, 20, 0.85);
|
| 18 |
-
--border-glow: 0 0 10px rgba(0, 243, 255, 0.3);
|
| 19 |
-
--font-main: 'Orbitron', sans-serif;
|
| 20 |
-
--font-body: 'Rajdhani', sans-serif;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
* {
|
| 24 |
-
box-sizing: border-box;
|
| 25 |
-
user-select: none;
|
| 26 |
-
-webkit-user-drag: none;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
body {
|
| 30 |
-
margin: 0;
|
| 31 |
-
overflow: hidden;
|
| 32 |
-
background-color: var(--color-bg);
|
| 33 |
-
font-family: var(--font-main);
|
| 34 |
-
color: var(--color-text);
|
| 35 |
-
height: 100vh;
|
| 36 |
-
width: 100vw;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
#canvas-container {
|
| 40 |
-
position: fixed;
|
| 41 |
-
top: 0;
|
| 42 |
-
left: 0;
|
| 43 |
-
width: 100%;
|
| 44 |
-
height: 100%;
|
| 45 |
-
z-index: 0;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
.hud-panel {
|
| 49 |
-
background: var(--color-panel);
|
| 50 |
-
border: 1px solid rgba(0, 243, 255, 0.2);
|
| 51 |
-
backdrop-filter: blur(12px);
|
| 52 |
-
-webkit-backdrop-filter: blur(12px);
|
| 53 |
-
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
| 54 |
-
border-radius: 4px;
|
| 55 |
-
position: relative;
|
| 56 |
-
overflow: hidden;
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
.hud-panel::before {
|
| 60 |
-
content: '';
|
| 61 |
-
position: absolute;
|
| 62 |
-
top: 0;
|
| 63 |
-
left: 0;
|
| 64 |
-
width: 100%;
|
| 65 |
-
height: 2px;
|
| 66 |
-
background: linear-gradient(90deg, transparent, var(--color-primary), transparent);
|
| 67 |
-
opacity: 0.5;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
.hud-panel::after {
|
| 71 |
-
content: '';
|
| 72 |
-
position: absolute;
|
| 73 |
-
bottom: 0;
|
| 74 |
-
right: 0;
|
| 75 |
-
width: 10px;
|
| 76 |
-
height: 10px;
|
| 77 |
-
border-bottom: 2px solid var(--color-primary);
|
| 78 |
-
border-right: 2px solid var(--color-primary);
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
#ui-sidebar {
|
| 82 |
-
position: fixed;
|
| 83 |
-
top: 20px;
|
| 84 |
-
left: 20px;
|
| 85 |
-
width: 340px;
|
| 86 |
-
max-height: calc(100vh - 40px);
|
| 87 |
-
display: flex;
|
| 88 |
-
flex-direction: column;
|
| 89 |
-
z-index: 100;
|
| 90 |
-
padding: 24px;
|
| 91 |
-
gap: 20px;
|
| 92 |
-
transition: transform 0.3s ease;
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
.logo-section {
|
| 96 |
-
display: flex;
|
| 97 |
-
justify-content: space-between;
|
| 98 |
-
align-items: center;
|
| 99 |
-
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 100 |
-
padding-bottom: 15px;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
.logo-text {
|
| 104 |
-
font-size: 24px;
|
| 105 |
-
font-weight: 900;
|
| 106 |
-
letter-spacing: 2px;
|
| 107 |
-
background: linear-gradient(180deg, #fff, var(--color-primary));
|
| 108 |
-
-webkit-background-clip: text;
|
| 109 |
-
-webkit-text-fill-color: transparent;
|
| 110 |
-
text-shadow: 0 0 10px rgba(0, 243, 255, 0.5);
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
.version-tag {
|
| 114 |
-
font-family: var(--font-body);
|
| 115 |
-
font-size: 12px;
|
| 116 |
-
color: var(--color-secondary);
|
| 117 |
-
border: 1px solid var(--color-secondary);
|
| 118 |
-
padding: 2px 6px;
|
| 119 |
-
border-radius: 2px;
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
.input-group {
|
| 123 |
-
position: relative;
|
| 124 |
-
margin-bottom: 10px;
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
.cyber-input {
|
| 128 |
-
width: 100%;
|
| 129 |
-
background: rgba(0, 0, 0, 0.3);
|
| 130 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 131 |
-
color: var(--color-primary);
|
| 132 |
-
padding: 12px 15px;
|
| 133 |
-
font-family: var(--font-body);
|
| 134 |
-
font-size: 16px;
|
| 135 |
-
font-weight: 500;
|
| 136 |
-
outline: none;
|
| 137 |
-
transition: all 0.3s ease;
|
| 138 |
-
text-transform: uppercase;
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
.cyber-input:focus {
|
| 142 |
-
border-color: var(--color-primary);
|
| 143 |
-
box-shadow: inset 0 0 10px rgba(0, 243, 255, 0.1);
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
.cyber-input::placeholder {
|
| 147 |
-
color: rgba(255, 255, 255, 0.3);
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
.slider-container {
|
| 151 |
-
margin-bottom: 15px;
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
.slider-header {
|
| 155 |
-
display: flex;
|
| 156 |
-
justify-content: space-between;
|
| 157 |
-
font-size: 12px;
|
| 158 |
-
color: rgba(255, 255, 255, 0.6);
|
| 159 |
-
margin-bottom: 5px;
|
| 160 |
-
font-family: var(--font-body);
|
| 161 |
-
text-transform: uppercase;
|
| 162 |
-
letter-spacing: 1px;
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
.cyber-slider {
|
| 166 |
-
-webkit-appearance: none;
|
| 167 |
-
width: 100%;
|
| 168 |
-
height: 4px;
|
| 169 |
-
background: rgba(255, 255, 255, 0.1);
|
| 170 |
-
outline: none;
|
| 171 |
-
border-radius: 2px;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
.cyber-slider::-webkit-slider-thumb {
|
| 175 |
-
-webkit-appearance: none;
|
| 176 |
-
width: 12px;
|
| 177 |
-
height: 20px;
|
| 178 |
-
background: var(--color-primary);
|
| 179 |
-
border: 1px solid #fff;
|
| 180 |
-
cursor: pointer;
|
| 181 |
-
box-shadow: 0 0 10px var(--color-primary);
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
.cyber-button {
|
| 185 |
-
width: 100%;
|
| 186 |
-
background: linear-gradient(45deg, rgba(0, 102, 255, 0.2), rgba(0, 243, 255, 0.2));
|
| 187 |
-
border: 1px solid var(--color-secondary);
|
| 188 |
-
color: var(--color-primary);
|
| 189 |
-
padding: 15px;
|
| 190 |
-
font-family: var(--font-main);
|
| 191 |
-
font-weight: 700;
|
| 192 |
-
font-size: 14px;
|
| 193 |
-
letter-spacing: 2px;
|
| 194 |
-
cursor: pointer;
|
| 195 |
-
transition: all 0.3s ease;
|
| 196 |
-
text-transform: uppercase;
|
| 197 |
-
position: relative;
|
| 198 |
-
overflow: hidden;
|
| 199 |
-
display: flex;
|
| 200 |
-
align-items: center;
|
| 201 |
-
justify-content: center;
|
| 202 |
-
gap: 10px;
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
.cyber-button:hover {
|
| 206 |
-
background: linear-gradient(45deg, var(--color-secondary), var(--color-primary));
|
| 207 |
-
color: #000;
|
| 208 |
-
box-shadow: 0 0 20px rgba(0, 243, 255, 0.4);
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
.cyber-button:disabled {
|
| 212 |
-
opacity: 0.5;
|
| 213 |
-
cursor: not-allowed;
|
| 214 |
-
filter: grayscale(1);
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
.cyber-button-danger {
|
| 218 |
-
border-color: var(--color-danger);
|
| 219 |
-
color: var(--color-danger);
|
| 220 |
-
background: rgba(255, 0, 60, 0.1);
|
| 221 |
-
font-size: 10px;
|
| 222 |
-
padding: 5px 10px;
|
| 223 |
-
width: auto;
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
.cyber-button-danger:hover {
|
| 227 |
-
background: var(--color-danger);
|
| 228 |
-
color: #fff;
|
| 229 |
-
box-shadow: 0 0 15px rgba(255, 0, 60, 0.5);
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
#minimap-container {
|
| 233 |
-
margin-top: auto;
|
| 234 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 235 |
-
background: rgba(0, 0, 0, 0.5);
|
| 236 |
-
position: relative;
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
#minimap-header {
|
| 240 |
-
position: absolute;
|
| 241 |
-
top: 5px;
|
| 242 |
-
left: 5px;
|
| 243 |
-
font-size: 10px;
|
| 244 |
-
color: var(--color-primary);
|
| 245 |
-
pointer-events: none;
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
#minimap-controls {
|
| 249 |
-
position: absolute;
|
| 250 |
-
top: 5px;
|
| 251 |
-
right: 5px;
|
| 252 |
-
display: flex;
|
| 253 |
-
gap: 2px;
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
.mini-btn {
|
| 257 |
-
width: 16px;
|
| 258 |
-
height: 16px;
|
| 259 |
-
background: rgba(0, 0, 0, 0.8);
|
| 260 |
-
border: 1px solid var(--color-secondary);
|
| 261 |
-
color: #fff;
|
| 262 |
-
display: flex;
|
| 263 |
-
align-items: center;
|
| 264 |
-
justify-content: center;
|
| 265 |
-
font-size: 10px;
|
| 266 |
-
cursor: pointer;
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
.mini-btn:hover {
|
| 270 |
-
background: var(--color-secondary);
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
#users-list {
|
| 274 |
-
margin-top: 10px;
|
| 275 |
-
max-height: 150px;
|
| 276 |
-
overflow-y: auto;
|
| 277 |
-
font-family: var(--font-body);
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
.user-item {
|
| 281 |
-
display: flex;
|
| 282 |
-
justify-content: space-between;
|
| 283 |
-
padding: 8px;
|
| 284 |
-
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 285 |
-
cursor: pointer;
|
| 286 |
-
transition: background 0.2s;
|
| 287 |
-
font-size: 14px;
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
.user-item:hover {
|
| 291 |
-
background: rgba(0, 243, 255, 0.1);
|
| 292 |
-
color: var(--color-primary);
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
.user-count-badge {
|
| 296 |
-
background: rgba(255, 255, 255, 0.1);
|
| 297 |
-
padding: 0 6px;
|
| 298 |
-
border-radius: 10px;
|
| 299 |
-
font-size: 12px;
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
#progress-bar {
|
| 303 |
-
position: absolute;
|
| 304 |
-
bottom: 0;
|
| 305 |
-
left: 0;
|
| 306 |
-
height: 2px;
|
| 307 |
-
background: var(--color-success);
|
| 308 |
-
width: 0%;
|
| 309 |
-
transition: width 0.3s;
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
#login-overlay {
|
| 313 |
-
position: fixed;
|
| 314 |
-
top: 0;
|
| 315 |
-
left: 0;
|
| 316 |
-
width: 100vw;
|
| 317 |
-
height: 100vh;
|
| 318 |
-
background: rgba(0, 3, 10, 0.95);
|
| 319 |
-
z-index: 200;
|
| 320 |
-
display: flex;
|
| 321 |
-
align-items: center;
|
| 322 |
-
justify-content: center;
|
| 323 |
-
backdrop-filter: blur(20px);
|
| 324 |
-
transition: opacity 0.6s ease;
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
-
.login-card {
|
| 328 |
-
width: 100%;
|
| 329 |
-
max-width: 450px;
|
| 330 |
-
padding: 40px;
|
| 331 |
-
background: rgba(10, 15, 30, 0.6);
|
| 332 |
-
border: 1px solid var(--color-primary);
|
| 333 |
-
box-shadow: 0 0 50px rgba(0, 243, 255, 0.1);
|
| 334 |
-
position: relative;
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
.login-card::before {
|
| 338 |
-
content: '';
|
| 339 |
-
position: absolute;
|
| 340 |
-
top: -2px;
|
| 341 |
-
left: -2px;
|
| 342 |
-
width: 20px;
|
| 343 |
-
height: 20px;
|
| 344 |
-
border-top: 2px solid var(--color-primary);
|
| 345 |
-
border-left: 2px solid var(--color-primary);
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
.login-card::after {
|
| 349 |
-
content: '';
|
| 350 |
-
position: absolute;
|
| 351 |
-
bottom: -2px;
|
| 352 |
-
right: -2px;
|
| 353 |
-
width: 20px;
|
| 354 |
-
height: 20px;
|
| 355 |
-
border-bottom: 2px solid var(--color-primary);
|
| 356 |
-
border-right: 2px solid var(--color-primary);
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
.scan-line {
|
| 360 |
-
position: absolute;
|
| 361 |
-
top: 0;
|
| 362 |
-
left: 0;
|
| 363 |
-
width: 100%;
|
| 364 |
-
height: 2px;
|
| 365 |
-
background: rgba(0, 243, 255, 0.5);
|
| 366 |
-
animation: scan 3s linear infinite;
|
| 367 |
-
opacity: 0.3;
|
| 368 |
-
pointer-events: none;
|
| 369 |
-
}
|
| 370 |
-
|
| 371 |
-
@keyframes scan {
|
| 372 |
-
0% { top: 0%; }
|
| 373 |
-
100% { top: 100%; }
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
.tooltip-box {
|
| 377 |
-
position: absolute;
|
| 378 |
-
background: rgba(5, 10, 15, 0.95);
|
| 379 |
-
border: 1px solid var(--color-success);
|
| 380 |
-
padding: 10px 15px;
|
| 381 |
-
pointer-events: none;
|
| 382 |
-
display: none;
|
| 383 |
-
z-index: 150;
|
| 384 |
-
box-shadow: 0 0 15px rgba(0, 255, 157, 0.2);
|
| 385 |
-
max-width: 250px;
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
.tooltip-title {
|
| 389 |
-
color: var(--color-success);
|
| 390 |
-
font-weight: 700;
|
| 391 |
-
font-size: 14px;
|
| 392 |
-
margin-bottom: 4px;
|
| 393 |
-
border-bottom: 1px solid rgba(0, 255, 157, 0.3);
|
| 394 |
-
padding-bottom: 4px;
|
| 395 |
-
text-transform: uppercase;
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
.tooltip-meta {
|
| 399 |
-
color: rgba(255, 255, 255, 0.7);
|
| 400 |
-
font-size: 11px;
|
| 401 |
-
font-family: var(--font-body);
|
| 402 |
-
}
|
| 403 |
-
|
| 404 |
-
#error-console {
|
| 405 |
-
position: fixed;
|
| 406 |
-
bottom: 0;
|
| 407 |
-
left: 0;
|
| 408 |
-
width: 100%;
|
| 409 |
-
height: 150px;
|
| 410 |
-
background: rgba(20, 0, 0, 0.9);
|
| 411 |
-
border-top: 2px solid var(--color-danger);
|
| 412 |
-
color: var(--color-danger);
|
| 413 |
-
font-family: monospace;
|
| 414 |
-
padding: 10px;
|
| 415 |
-
z-index: 9999;
|
| 416 |
-
overflow-y: auto;
|
| 417 |
-
display: none;
|
| 418 |
-
font-size: 12px;
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
::-webkit-scrollbar {
|
| 422 |
-
width: 4px;
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
::-webkit-scrollbar-track {
|
| 426 |
-
background: rgba(0, 0, 0, 0.2);
|
| 427 |
-
}
|
| 428 |
-
|
| 429 |
-
::-webkit-scrollbar-thumb {
|
| 430 |
-
background: var(--color-secondary);
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
.hidden {
|
| 434 |
-
display: none !important;
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
.fade-out {
|
| 438 |
-
opacity: 0;
|
| 439 |
-
pointer-events: none;
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
.loading-pulse {
|
| 443 |
-
animation: pulse 1.5s infinite;
|
| 444 |
-
color: var(--color-primary);
|
| 445 |
-
font-weight: bold;
|
| 446 |
-
text-align: center;
|
| 447 |
-
margin-top: 20px;
|
| 448 |
-
font-family: var(--font-body);
|
| 449 |
-
letter-spacing: 2px;
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
@keyframes pulse {
|
| 453 |
-
0% { opacity: 0.5; }
|
| 454 |
-
50% { opacity: 1; text-shadow: 0 0 10px var(--color-primary); }
|
| 455 |
-
100% { opacity: 0.5; }
|
| 456 |
-
}
|
| 457 |
-
</style>
|
| 458 |
</head>
|
| 459 |
<body>
|
| 460 |
-
|
| 461 |
-
<div id="
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
<
|
| 465 |
-
|
| 466 |
-
<
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
<div class="loading-pulse" id="loading-text">
|
| 474 |
-
INICIALIZANDO SISTEMA...
|
| 475 |
-
</div>
|
| 476 |
-
<div style="text-align: center; margin-top: 10px; color: #666; font-size: 10px;">
|
| 477 |
-
ASIGNANDO IDENTIFICADOR AUTOMÁTICO
|
| 478 |
-
</div>
|
| 479 |
</div>
|
| 480 |
-
<
|
| 481 |
-
|
| 482 |
-
<div
|
| 483 |
-
<div id="
|
| 484 |
-
|
| 485 |
-
<
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
<div style="display: flex; gap: 10px; align-items: center;">
|
| 491 |
-
<div class="version-tag">V3.1</div>
|
| 492 |
-
<button id="btn-logout" class="cyber-button cyber-button-danger">REINICIAR</button>
|
| 493 |
</div>
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
<div class="input-group">
|
| 498 |
-
<input type="text" id="seed-input" class="cyber-input" placeholder="SEMILLA NEURONAL...">
|
| 499 |
</div>
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
<span>DENSIDAD PRIMARIA</span>
|
| 504 |
-
<span id="val-l1" style="color: var(--color-primary)">6</span>
|
| 505 |
-
</div>
|
| 506 |
-
<input type="range" id="range-l1" class="cyber-slider" min="1" max="15" value="6">
|
| 507 |
</div>
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
</div>
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
</div>
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
<span>SEMBRAR EN LA RED</span>
|
| 527 |
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 528 |
-
<circle cx="12" cy="12" r="10"></circle>
|
| 529 |
-
<line x1="12" y1="8" x2="12" y2="16"></line>
|
| 530 |
-
<line x1="8" y1="12" x2="16" y2="12"></line>
|
| 531 |
-
</svg>
|
| 532 |
-
</button>
|
| 533 |
</div>
|
|
|
|
| 534 |
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
<canvas id="minimap-canvas" width="290" height="180"></canvas>
|
| 540 |
-
<div id="minimap-header">RADAR</div>
|
| 541 |
-
<div id="minimap-controls">
|
| 542 |
-
<div class="mini-btn" id="map-zoom-out">-</div>
|
| 543 |
-
<div class="mini-btn" id="map-zoom-in">+</div>
|
| 544 |
-
</div>
|
| 545 |
-
</div>
|
| 546 |
-
|
| 547 |
-
<div id="users-list"></div>
|
| 548 |
-
</div>
|
| 549 |
-
</aside>
|
| 550 |
-
|
| 551 |
-
<script>
|
| 552 |
-
// Sistema global de diagnóstico de errores
|
| 553 |
-
window.onerror = function(msg, url, lineNo, columnNo, error) {
|
| 554 |
-
const consoleDiv = document.getElementById('error-console');
|
| 555 |
-
const list = document.getElementById('error-list');
|
| 556 |
-
if (consoleDiv && list) {
|
| 557 |
-
consoleDiv.style.display = 'block';
|
| 558 |
-
const item = document.createElement('div');
|
| 559 |
-
item.innerHTML = `[CRITICO] ${msg} <br><span style="color:#aaa; font-size:10px">En: ${url}:${lineNo}</span>`;
|
| 560 |
-
item.style.borderBottom = '1px solid #400';
|
| 561 |
-
item.style.padding = '5px 0';
|
| 562 |
-
list.appendChild(item);
|
| 563 |
-
}
|
| 564 |
-
return false;
|
| 565 |
-
};
|
| 566 |
-
|
| 567 |
-
window.addEventListener('unhandledrejection', function(event) {
|
| 568 |
-
const consoleDiv = document.getElementById('error-console');
|
| 569 |
-
const list = document.getElementById('error-list');
|
| 570 |
-
if (consoleDiv && list) {
|
| 571 |
-
consoleDiv.style.display = 'block';
|
| 572 |
-
const item = document.createElement('div');
|
| 573 |
-
// Detectar error común de Firebase Domain
|
| 574 |
-
let msg = event.reason ? event.reason.message : 'Error desconocido';
|
| 575 |
-
if (msg.includes('auth/unauthorized-domain')) {
|
| 576 |
-
msg = "ERROR DE DOMINIO: Este dominio no está autorizado en Firebase Console -> Authentication -> Settings.";
|
| 577 |
-
}
|
| 578 |
-
item.innerHTML = `[PROMESA] ${msg}`;
|
| 579 |
-
item.style.borderBottom = '1px solid #400';
|
| 580 |
-
item.style.padding = '5px 0';
|
| 581 |
-
list.appendChild(item);
|
| 582 |
-
}
|
| 583 |
-
});
|
| 584 |
-
</script>
|
| 585 |
-
|
| 586 |
-
<script type="module">
|
| 587 |
-
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
|
| 588 |
-
import { getAuth, signInAnonymously, signOut, onAuthStateChanged, setPersistence, browserSessionPersistence } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
|
| 589 |
-
import { getFirestore, doc, setDoc, getDoc, addDoc, collection, onSnapshot, query, orderBy, serverTimestamp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
|
| 590 |
-
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.module.js";
|
| 591 |
-
import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/controls/OrbitControls.js";
|
| 592 |
-
import { FontLoader } from "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/loaders/FontLoader.js";
|
| 593 |
-
import { TextGeometry } from "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/geometries/TextGeometry.js";
|
| 594 |
-
import TWEEN from "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@18.6.4/dist/tween.esm.js";
|
| 595 |
-
|
| 596 |
-
class Config {
|
| 597 |
-
static get FIREBASE() {
|
| 598 |
-
return {
|
| 599 |
-
apiKey: "AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",
|
| 600 |
-
authDomain: "neuronal-1f3b9.firebaseapp.com",
|
| 601 |
-
projectId: "neuronal-1f3b9",
|
| 602 |
-
storageBucket: "neuronal-1f3b9.firebasestorage.app",
|
| 603 |
-
messagingSenderId: "208887839866",
|
| 604 |
-
appId: "1:208887839866:web:adbb697dd0b63195b10fc3"
|
| 605 |
-
};
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
static get GEMINI_API_KEY() {
|
| 609 |
-
return localStorage.getItem('GEMINI_API_KEY') || 'AIzaSyDmoQNpzgzW21f_WFCU9YbaAeI1fdOJMlo';
|
| 610 |
-
}
|
| 611 |
-
|
| 612 |
-
static get COLORS() {
|
| 613 |
-
return {
|
| 614 |
-
bg: 0x030508,
|
| 615 |
-
primary: 0x00f3ff,
|
| 616 |
-
secondary: 0x0066ff,
|
| 617 |
-
text: 0xe0f7ff,
|
| 618 |
-
grid: 0x1a2b4a
|
| 619 |
-
};
|
| 620 |
-
}
|
| 621 |
-
|
| 622 |
-
static get CONSTANTS() {
|
| 623 |
-
return {
|
| 624 |
-
GALAXY_SPACING: 400,
|
| 625 |
-
USER_SPHERE_RADIUS: 5,
|
| 626 |
-
SPIRAL_TIGHTNESS: 0.15,
|
| 627 |
-
SPIRAL_VERTICAL_STEP: 2.5,
|
| 628 |
-
CAMERA_FOV: 60,
|
| 629 |
-
CAMERA_FAR: 5000
|
| 630 |
-
};
|
| 631 |
-
}
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
class Utils {
|
| 635 |
-
static normalizeString(str) {
|
| 636 |
-
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase();
|
| 637 |
-
}
|
| 638 |
-
|
| 639 |
-
static stringToColor(str) {
|
| 640 |
-
let hash = 0;
|
| 641 |
-
for (let i = 0; i < str.length; i++) {
|
| 642 |
-
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
| 643 |
-
}
|
| 644 |
-
const h = Math.abs(hash % 360);
|
| 645 |
-
return new THREE.Color(`hsl(${h}, 80%, 60%)`);
|
| 646 |
-
}
|
| 647 |
-
|
| 648 |
-
static stringToHexColor(str) {
|
| 649 |
-
let hash = 0;
|
| 650 |
-
for (let i = 0; i < str.length; i++) {
|
| 651 |
-
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
| 652 |
-
}
|
| 653 |
-
const h = Math.abs(hash % 360);
|
| 654 |
-
return `hsl(${h}, 80%, 60%)`;
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
-
static delay(ms) {
|
| 658 |
-
return new Promise(resolve => setTimeout(resolve, ms));
|
| 659 |
-
}
|
| 660 |
-
|
| 661 |
-
static randomRange(min, max) {
|
| 662 |
-
return Math.random() * (max - min) + min;
|
| 663 |
-
}
|
| 664 |
-
|
| 665 |
-
static generateCode() {
|
| 666 |
-
return Math.floor(1000 + Math.random() * 9000).toString();
|
| 667 |
-
}
|
| 668 |
-
}
|
| 669 |
-
|
| 670 |
-
class AuthManager {
|
| 671 |
-
constructor(appInstance) {
|
| 672 |
-
this.app = appInstance;
|
| 673 |
-
this.firebaseApp = initializeApp(Config.FIREBASE);
|
| 674 |
-
this.auth = getAuth(this.firebaseApp);
|
| 675 |
-
this.db = getFirestore(this.firebaseApp);
|
| 676 |
-
this.currentUser = null;
|
| 677 |
-
this.currentUsername = null;
|
| 678 |
-
}
|
| 679 |
-
|
| 680 |
-
async init() {
|
| 681 |
-
await setPersistence(this.auth, browserSessionPersistence);
|
| 682 |
-
|
| 683 |
-
onAuthStateChanged(this.auth, async (user) => {
|
| 684 |
-
if (user) {
|
| 685 |
-
this.currentUser = user;
|
| 686 |
-
const loader = document.getElementById('loading-text');
|
| 687 |
-
if (loader) loader.innerText = "CONECTANDO A BASE DE DATOS...";
|
| 688 |
-
await this.checkUserProfile();
|
| 689 |
-
} else {
|
| 690 |
-
const loader = document.getElementById('loading-text');
|
| 691 |
-
if (loader) loader.innerText = "GENERANDO CREDENCIALES...";
|
| 692 |
-
try {
|
| 693 |
-
await signInAnonymously(this.auth);
|
| 694 |
-
} catch (e) {
|
| 695 |
-
console.error("Auth Error:", e);
|
| 696 |
-
if (loader) loader.innerText = "ERROR DE AUTENTICACION (VER CONSOLA)";
|
| 697 |
-
}
|
| 698 |
-
}
|
| 699 |
-
});
|
| 700 |
-
|
| 701 |
-
const logoutBtn = document.getElementById('btn-logout');
|
| 702 |
-
if (logoutBtn) logoutBtn.addEventListener('click', () => this.handleLogout());
|
| 703 |
-
}
|
| 704 |
-
|
| 705 |
-
async handleLogout() {
|
| 706 |
-
try {
|
| 707 |
-
await signOut(this.auth);
|
| 708 |
-
window.location.reload();
|
| 709 |
-
} catch (error) {
|
| 710 |
-
console.error("Logout Error:", error);
|
| 711 |
-
}
|
| 712 |
-
}
|
| 713 |
-
|
| 714 |
-
async createUserProfile(username) {
|
| 715 |
-
if (!this.currentUser) return;
|
| 716 |
-
const uid = this.currentUser.uid;
|
| 717 |
-
this.currentUsername = username;
|
| 718 |
-
|
| 719 |
-
const userRef = doc(this.db, 'artifacts', 'neuronal-1f3b9', 'users', uid, 'user_data', 'profile');
|
| 720 |
-
await setDoc(userRef, {
|
| 721 |
-
username: username,
|
| 722 |
-
createdAt: serverTimestamp(),
|
| 723 |
-
lastLogin: serverTimestamp(),
|
| 724 |
-
platform: navigator.platform
|
| 725 |
-
}, { merge: true });
|
| 726 |
-
}
|
| 727 |
-
|
| 728 |
-
async checkUserProfile() {
|
| 729 |
-
if (!this.currentUser) return;
|
| 730 |
-
const uid = this.currentUser.uid;
|
| 731 |
-
|
| 732 |
-
try {
|
| 733 |
-
const userRef = doc(this.db, 'artifacts', 'neuronal-1f3b9', 'users', uid, 'user_data', 'profile');
|
| 734 |
-
const snap = await getDoc(userRef);
|
| 735 |
-
|
| 736 |
-
if (snap.exists()) {
|
| 737 |
-
this.currentUsername = snap.data().username;
|
| 738 |
-
} else {
|
| 739 |
-
const randomName = `PILOTO-${Utils.generateCode()}`;
|
| 740 |
-
await this.createUserProfile(randomName);
|
| 741 |
-
}
|
| 742 |
-
|
| 743 |
-
const loader = document.getElementById('loading-text');
|
| 744 |
-
if (loader) loader.innerText = "ACCESO CONCEDIDO";
|
| 745 |
-
setTimeout(() => {
|
| 746 |
-
this.app.ui.hideLogin();
|
| 747 |
-
this.app.data.startListening();
|
| 748 |
-
}, 800);
|
| 749 |
-
|
| 750 |
-
} catch (error) {
|
| 751 |
-
console.error("Profile Check Error:", error);
|
| 752 |
-
const loader = document.getElementById('loading-text');
|
| 753 |
-
if (loader) loader.innerText = "ERROR DE CONEXION";
|
| 754 |
-
}
|
| 755 |
-
}
|
| 756 |
-
}
|
| 757 |
-
|
| 758 |
-
class DataManager {
|
| 759 |
-
constructor(appInstance) {
|
| 760 |
-
this.app = appInstance;
|
| 761 |
-
this.db = appInstance.auth.db;
|
| 762 |
-
this.maps = [];
|
| 763 |
-
this.users = new Map();
|
| 764 |
-
this.listeners = [];
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
startListening() {
|
| 768 |
-
const mapsRef = collection(this.db, 'artifacts', 'neuronal-1f3b9', 'public', 'data', 'maps');
|
| 769 |
-
const q = query(mapsRef, orderBy('createdAt', 'asc'));
|
| 770 |
-
|
| 771 |
-
const unsubscribe = onSnapshot(q, (snapshot) => {
|
| 772 |
-
this.maps = [];
|
| 773 |
-
snapshot.forEach(doc => {
|
| 774 |
-
this.maps.push({
|
| 775 |
-
id: doc.id,
|
| 776 |
-
...doc.data()
|
| 777 |
-
});
|
| 778 |
-
});
|
| 779 |
-
this.processData();
|
| 780 |
-
}, (error) => {
|
| 781 |
-
console.error("Snapshot error:", error);
|
| 782 |
-
});
|
| 783 |
-
this.listeners.push(unsubscribe);
|
| 784 |
-
}
|
| 785 |
-
|
| 786 |
-
async processData() {
|
| 787 |
-
const uniqueUserIds = [...new Set(this.maps.map(m => m.userId))];
|
| 788 |
-
|
| 789 |
-
if (this.app.auth.currentUser && !uniqueUserIds.includes(this.app.auth.currentUser.uid)) {
|
| 790 |
-
uniqueUserIds.push(this.app.auth.currentUser.uid);
|
| 791 |
-
}
|
| 792 |
-
|
| 793 |
-
for (const uid of uniqueUserIds) {
|
| 794 |
-
if (!this.users.has(uid)) {
|
| 795 |
-
let username = "DESCONOCIDO";
|
| 796 |
-
if (uid === this.app.auth.currentUser?.uid) {
|
| 797 |
-
username = this.app.auth.currentUsername;
|
| 798 |
-
} else {
|
| 799 |
-
try {
|
| 800 |
-
const ref = doc(this.db, 'artifacts', 'neuronal-1f3b9', 'users', uid, 'user_data', 'profile');
|
| 801 |
-
const snap = await getDoc(ref);
|
| 802 |
-
if (snap.exists()) username = snap.data().username;
|
| 803 |
-
} catch (e) {
|
| 804 |
-
console.warn(`Error fetching user ${uid}`, e);
|
| 805 |
-
}
|
| 806 |
-
}
|
| 807 |
-
this.users.set(uid, { username, id: uid, maps: [] });
|
| 808 |
-
}
|
| 809 |
-
}
|
| 810 |
-
|
| 811 |
-
this.users.forEach(u => u.maps = []);
|
| 812 |
-
this.maps.forEach(map => {
|
| 813 |
-
if (this.users.has(map.userId)) {
|
| 814 |
-
this.users.get(map.userId).maps.push(map);
|
| 815 |
-
}
|
| 816 |
-
});
|
| 817 |
-
|
| 818 |
-
this.app.world.galaxySystem.rebuild(this.users);
|
| 819 |
-
this.app.ui.updateUserList(this.users);
|
| 820 |
-
}
|
| 821 |
-
|
| 822 |
-
async saveNeuron(topic, dataStructure) {
|
| 823 |
-
if (!this.app.auth.currentUser) return;
|
| 824 |
-
|
| 825 |
-
const colRef = collection(this.db, 'artifacts', 'neuronal-1f3b9', 'public', 'data', 'maps');
|
| 826 |
-
await addDoc(colRef, {
|
| 827 |
-
topic: topic,
|
| 828 |
-
data: JSON.stringify(dataStructure),
|
| 829 |
-
userId: this.app.auth.currentUser.uid,
|
| 830 |
-
createdAt: serverTimestamp(),
|
| 831 |
-
origin: { x: 0, y: 0, z: 0 }
|
| 832 |
-
});
|
| 833 |
-
}
|
| 834 |
-
}
|
| 835 |
-
|
| 836 |
-
class AIManager {
|
| 837 |
-
constructor(appInstance) {
|
| 838 |
-
this.app = appInstance;
|
| 839 |
-
}
|
| 840 |
-
|
| 841 |
-
async generate(topic, levels) {
|
| 842 |
-
const prompt = `
|
| 843 |
-
Genera un objeto JSON puro (sin markdown) basado en el tema: "${topic}".
|
| 844 |
-
Asegúrate de que no haya acentos ni caracteres especiales.
|
| 845 |
-
Estructura:
|
| 846 |
-
{
|
| 847 |
-
"analisis": "Breve descripción de 15 palabras",
|
| 848 |
-
"lista_palabras": [
|
| 849 |
-
{
|
| 850 |
-
"palabra_principal": "Concepto1",
|
| 851 |
-
"variantes": [
|
| 852 |
-
{
|
| 853 |
-
"palabra_variante": "Subconcepto1",
|
| 854 |
-
"sub_variantes": ["Detalle1", "Detalle2"]
|
| 855 |
-
}
|
| 856 |
-
]
|
| 857 |
-
}
|
| 858 |
-
]
|
| 859 |
-
}
|
| 860 |
-
Requisitos de cantidad:
|
| 861 |
-
- ${levels.l1} elementos principales en lista_palabras.
|
| 862 |
-
- ${levels.l2} variantes por cada principal.
|
| 863 |
-
- ${levels.l3} sub_variantes por cada variante.
|
| 864 |
-
IMPORTANTE: Responde SOLO con el JSON.
|
| 865 |
-
`;
|
| 866 |
-
|
| 867 |
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${Config.GEMINI_API_KEY}`;
|
| 868 |
-
|
| 869 |
-
try {
|
| 870 |
-
const response = await fetch(url, {
|
| 871 |
-
method: 'POST',
|
| 872 |
-
headers: { 'Content-Type': 'application/json' },
|
| 873 |
-
body: JSON.stringify({
|
| 874 |
-
contents: [{ parts: [{ text: prompt }] }],
|
| 875 |
-
generationConfig: { responseMimeType: "application/json" }
|
| 876 |
-
})
|
| 877 |
-
});
|
| 878 |
-
|
| 879 |
-
if (!response.ok) throw new Error("API Error");
|
| 880 |
-
|
| 881 |
-
const data = await response.json();
|
| 882 |
-
const text = data.candidates[0].content.parts[0].text;
|
| 883 |
-
return JSON.parse(text);
|
| 884 |
-
|
| 885 |
-
} catch (error) {
|
| 886 |
-
console.error("AI Error:", error);
|
| 887 |
-
throw error;
|
| 888 |
-
}
|
| 889 |
-
}
|
| 890 |
-
}
|
| 891 |
-
|
| 892 |
-
class Comet {
|
| 893 |
-
constructor(scene, font) {
|
| 894 |
-
this.scene = scene;
|
| 895 |
-
this.font = font;
|
| 896 |
-
this.angle = 0;
|
| 897 |
-
this.radius = 600;
|
| 898 |
-
this.speed = 0.003;
|
| 899 |
-
this.group = new THREE.Group();
|
| 900 |
-
this.tailPositions = [];
|
| 901 |
-
this.wordTimer = 0;
|
| 902 |
-
this.currentWordMesh = null;
|
| 903 |
-
this.words = ["DATOS", "FLUJO", "RED", "NODO", "ENLACE", "CORE", "SYSTEM", "LINK", "SIGNAL", "PULSE"];
|
| 904 |
-
|
| 905 |
-
this.initVisuals();
|
| 906 |
-
this.scene.add(this.group);
|
| 907 |
-
}
|
| 908 |
-
|
| 909 |
-
initVisuals() {
|
| 910 |
-
const headGeo = new THREE.SphereGeometry(2, 16, 16);
|
| 911 |
-
const headMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
| 912 |
-
this.head = new THREE.Mesh(headGeo, headMat);
|
| 913 |
-
this.group.add(this.head);
|
| 914 |
-
|
| 915 |
-
const glowGeo = new THREE.SpriteMaterial({
|
| 916 |
-
map: new THREE.CanvasTexture(this.generateGlowTexture()),
|
| 917 |
-
color: 0x00ffff,
|
| 918 |
-
transparent: true,
|
| 919 |
-
opacity: 0.8,
|
| 920 |
-
blending: THREE.AdditiveBlending
|
| 921 |
-
});
|
| 922 |
-
const glow = new THREE.Sprite(glowGeo);
|
| 923 |
-
glow.scale.set(15, 15, 1);
|
| 924 |
-
this.head.add(glow);
|
| 925 |
-
|
| 926 |
-
const lineGeo = new THREE.BufferGeometry();
|
| 927 |
-
const positions = new Float32Array(50 * 3);
|
| 928 |
-
lineGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
| 929 |
-
const lineMat = new THREE.LineBasicMaterial({
|
| 930 |
-
color: 0x00ffff,
|
| 931 |
-
transparent: true,
|
| 932 |
-
opacity: 0.4,
|
| 933 |
-
blending: THREE.AdditiveBlending
|
| 934 |
-
});
|
| 935 |
-
this.tail = new THREE.Line(lineGeo, lineMat);
|
| 936 |
-
this.scene.add(this.tail);
|
| 937 |
-
}
|
| 938 |
-
|
| 939 |
-
generateGlowTexture() {
|
| 940 |
-
const canvas = document.createElement('canvas');
|
| 941 |
-
canvas.width = 32;
|
| 942 |
-
canvas.height = 32;
|
| 943 |
-
const context = canvas.getContext('2d');
|
| 944 |
-
const gradient = context.createRadialGradient(16, 16, 0, 16, 16, 16);
|
| 945 |
-
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
| 946 |
-
gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
|
| 947 |
-
gradient.addColorStop(0.5, 'rgba(0,64,255,0.2)');
|
| 948 |
-
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
| 949 |
-
context.fillStyle = gradient;
|
| 950 |
-
context.fillRect(0, 0, 32, 32);
|
| 951 |
-
return canvas;
|
| 952 |
-
}
|
| 953 |
-
|
| 954 |
-
update(camera) {
|
| 955 |
-
this.angle += this.speed;
|
| 956 |
-
|
| 957 |
-
const x = Math.cos(this.angle) * this.radius;
|
| 958 |
-
const z = Math.sin(this.angle * 0.7) * this.radius;
|
| 959 |
-
const y = Math.sin(this.angle * 0.3) * 100;
|
| 960 |
-
|
| 961 |
-
this.group.position.set(x, y, z);
|
| 962 |
-
|
| 963 |
-
this.tailPositions.unshift(new THREE.Vector3(x, y, z));
|
| 964 |
-
if (this.tailPositions.length > 50) this.tailPositions.pop();
|
| 965 |
-
|
| 966 |
-
const positions = this.tail.geometry.attributes.position.array;
|
| 967 |
-
for (let i = 0; i < this.tailPositions.length; i++) {
|
| 968 |
-
const p = this.tailPositions[i];
|
| 969 |
-
positions[i * 3] = p.x;
|
| 970 |
-
positions[i * 3 + 1] = p.y;
|
| 971 |
-
positions[i * 3 + 2] = p.z;
|
| 972 |
-
}
|
| 973 |
-
this.tail.geometry.attributes.position.needsUpdate = true;
|
| 974 |
-
|
| 975 |
-
this.wordTimer++;
|
| 976 |
-
if (this.wordTimer > 300) {
|
| 977 |
-
this.spawnWord();
|
| 978 |
-
this.wordTimer = 0;
|
| 979 |
-
}
|
| 980 |
-
|
| 981 |
-
if (this.currentWordMesh) {
|
| 982 |
-
this.currentWordMesh.lookAt(camera.position);
|
| 983 |
-
}
|
| 984 |
-
}
|
| 985 |
-
|
| 986 |
-
spawnWord() {
|
| 987 |
-
if (this.currentWordMesh) {
|
| 988 |
-
this.group.remove(this.currentWordMesh);
|
| 989 |
-
this.currentWordMesh.geometry.dispose();
|
| 990 |
-
}
|
| 991 |
-
|
| 992 |
-
const word = this.words[Math.floor(Math.random() * this.words.length)];
|
| 993 |
-
const geo = new TextGeometry(word, {
|
| 994 |
-
font: this.font,
|
| 995 |
-
size: 4,
|
| 996 |
-
height: 0.2
|
| 997 |
-
});
|
| 998 |
-
geo.center();
|
| 999 |
-
const mat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
|
| 1000 |
-
this.currentWordMesh = new THREE.Mesh(geo, mat);
|
| 1001 |
-
this.currentWordMesh.position.y = 8;
|
| 1002 |
-
this.group.add(this.currentWordMesh);
|
| 1003 |
-
}
|
| 1004 |
-
}
|
| 1005 |
-
|
| 1006 |
-
class GalaxySystem {
|
| 1007 |
-
constructor(appInstance) {
|
| 1008 |
-
this.app = appInstance;
|
| 1009 |
-
this.scene = appInstance.world.scene;
|
| 1010 |
-
this.container = new THREE.Group();
|
| 1011 |
-
this.scene.add(this.container);
|
| 1012 |
-
this.userNodes = new Map();
|
| 1013 |
-
}
|
| 1014 |
-
|
| 1015 |
-
rebuild(usersMap) {
|
| 1016 |
-
this.clear();
|
| 1017 |
-
const users = Array.from(usersMap.values());
|
| 1018 |
-
|
| 1019 |
-
const userIndex = users.findIndex(u => u.id === this.app.auth.currentUser?.uid);
|
| 1020 |
-
if (userIndex === -1 && this.app.auth.currentUser) {
|
| 1021 |
-
// Si el usuario actual no tiene mapas, agrégalo virtualmente al final
|
| 1022 |
-
users.push({
|
| 1023 |
-
id: this.app.auth.currentUser.uid,
|
| 1024 |
-
username: this.app.auth.currentUsername,
|
| 1025 |
-
maps: []
|
| 1026 |
-
});
|
| 1027 |
-
}
|
| 1028 |
-
|
| 1029 |
-
users.forEach((user, index) => {
|
| 1030 |
-
const angle = index * 2.5;
|
| 1031 |
-
const radius = Math.sqrt(index) * Config.CONSTANTS.GALAXY_SPACING;
|
| 1032 |
-
const x = Math.cos(angle) * radius;
|
| 1033 |
-
const z = Math.sin(angle) * radius;
|
| 1034 |
-
const origin = new THREE.Vector3(x, 0, z);
|
| 1035 |
-
|
| 1036 |
-
this.createUserGalaxy(user, origin);
|
| 1037 |
-
});
|
| 1038 |
-
}
|
| 1039 |
-
|
| 1040 |
-
createUserGalaxy(user, origin) {
|
| 1041 |
-
const isMe = user.id === this.app.auth.currentUser?.uid;
|
| 1042 |
-
|
| 1043 |
-
const coreGeo = new THREE.IcosahedronGeometry(Config.CONSTANTS.USER_SPHERE_RADIUS, 1);
|
| 1044 |
-
const coreMat = new THREE.MeshStandardMaterial({
|
| 1045 |
-
color: isMe ? Config.COLORS.primary : Config.COLORS.secondary,
|
| 1046 |
-
emissive: isMe ? 0x0044aa : 0x001133,
|
| 1047 |
-
emissiveIntensity: 0.8,
|
| 1048 |
-
wireframe: true
|
| 1049 |
-
});
|
| 1050 |
-
const core = new THREE.Mesh(coreGeo, coreMat);
|
| 1051 |
-
core.position.copy(origin);
|
| 1052 |
-
core.userData = { type: 'user', id: user.id, username: user.username };
|
| 1053 |
-
this.container.add(core);
|
| 1054 |
-
|
| 1055 |
-
// Anillos rotatorios decorativos
|
| 1056 |
-
const ringGeo = new THREE.TorusGeometry(Config.CONSTANTS.USER_SPHERE_RADIUS * 1.5, 0.1, 8, 32);
|
| 1057 |
-
const ringMat = new THREE.MeshBasicMaterial({ color: isMe ? 0xffffff : 0x555555, transparent: true, opacity: 0.3 });
|
| 1058 |
-
const ring = new THREE.Mesh(ringGeo, ringMat);
|
| 1059 |
-
ring.position.copy(origin);
|
| 1060 |
-
ring.rotation.x = Math.PI / 2;
|
| 1061 |
-
this.container.add(ring);
|
| 1062 |
-
|
| 1063 |
-
// Animación del anillo guardada en userData para el loop
|
| 1064 |
-
ring.userData = { animate: true, speed: 0.01 };
|
| 1065 |
-
|
| 1066 |
-
if (this.app.world.font) {
|
| 1067 |
-
const textGeo = new TextGeometry(user.username, {
|
| 1068 |
-
font: this.app.world.font,
|
| 1069 |
-
size: 2.5,
|
| 1070 |
-
height: 0.1
|
| 1071 |
-
});
|
| 1072 |
-
textGeo.center();
|
| 1073 |
-
const textMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
| 1074 |
-
const text = new THREE.Mesh(textGeo, textMat);
|
| 1075 |
-
text.position.copy(origin);
|
| 1076 |
-
text.position.y += 10;
|
| 1077 |
-
text.userData = { isBillboard: true };
|
| 1078 |
-
this.container.add(text);
|
| 1079 |
-
}
|
| 1080 |
-
|
| 1081 |
-
this.buildNeurons(user.maps, origin);
|
| 1082 |
-
this.userNodes.set(user.id, { origin, mesh: core });
|
| 1083 |
-
}
|
| 1084 |
-
|
| 1085 |
-
buildNeurons(maps, origin) {
|
| 1086 |
-
maps.forEach((map, index) => {
|
| 1087 |
-
// Cálculo de espiral esférica
|
| 1088 |
-
const phi = index * 0.5; // Espaciado angular
|
| 1089 |
-
const height = (index * Config.CONSTANTS.SPIRAL_VERTICAL_STEP) - (maps.length * Config.CONSTANTS.SPIRAL_VERTICAL_STEP / 2);
|
| 1090 |
-
const r = 20 + (index * 1.2);
|
| 1091 |
-
|
| 1092 |
-
const x = r * Math.cos(phi);
|
| 1093 |
-
const z = r * Math.sin(phi);
|
| 1094 |
-
const y = height;
|
| 1095 |
-
|
| 1096 |
-
const pos = new THREE.Vector3(x, y, z).add(origin);
|
| 1097 |
-
const color = Utils.stringToColor(map.topic);
|
| 1098 |
-
|
| 1099 |
-
// Línea conector al centro
|
| 1100 |
-
const points = [origin, pos];
|
| 1101 |
-
const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
|
| 1102 |
-
const lineMat = new THREE.LineBasicMaterial({
|
| 1103 |
-
color: color,
|
| 1104 |
-
transparent: true,
|
| 1105 |
-
opacity: 0.2
|
| 1106 |
-
});
|
| 1107 |
-
this.container.add(new THREE.Line(lineGeo, lineMat));
|
| 1108 |
-
|
| 1109 |
-
try {
|
| 1110 |
-
const data = JSON.parse(map.data);
|
| 1111 |
-
this.buildTreeRecursive(data.lista_palabras, pos, 1, color);
|
| 1112 |
-
|
| 1113 |
-
// Título del mapa
|
| 1114 |
-
if (this.app.world.font) {
|
| 1115 |
-
const labelGeo = new TextGeometry(map.topic, {
|
| 1116 |
-
font: this.app.world.font,
|
| 1117 |
-
size: 1.2,
|
| 1118 |
-
height: 0.05
|
| 1119 |
-
});
|
| 1120 |
-
const labelMat = new THREE.MeshBasicMaterial({ color: color });
|
| 1121 |
-
const label = new THREE.Mesh(labelGeo, labelMat);
|
| 1122 |
-
label.position.copy(pos).add(new THREE.Vector3(0, 2, 0));
|
| 1123 |
-
label.userData = { isBillboard: true };
|
| 1124 |
-
this.container.add(label);
|
| 1125 |
-
}
|
| 1126 |
-
} catch (e) {
|
| 1127 |
-
console.error("Error parsing map data", e);
|
| 1128 |
-
}
|
| 1129 |
-
});
|
| 1130 |
-
}
|
| 1131 |
-
|
| 1132 |
-
buildTreeRecursive(items, parentPos, level, color) {
|
| 1133 |
-
if (!items || level > 3) return;
|
| 1134 |
-
|
| 1135 |
-
const radius = 10 / level;
|
| 1136 |
-
|
| 1137 |
-
items.forEach((item, i) => {
|
| 1138 |
-
let text = item.palabra_principal || item.palabra_variante || item;
|
| 1139 |
-
let children = item.variantes || item.sub_variantes || [];
|
| 1140 |
-
|
| 1141 |
-
// Distribución aleatoria esférica local
|
| 1142 |
-
const u = Math.random();
|
| 1143 |
-
const v = Math.random();
|
| 1144 |
-
const theta = 2 * Math.PI * u;
|
| 1145 |
-
const phi = Math.acos(2 * v - 1);
|
| 1146 |
-
|
| 1147 |
-
const lx = radius * Math.sin(phi) * Math.cos(theta);
|
| 1148 |
-
const ly = radius * Math.sin(phi) * Math.sin(theta);
|
| 1149 |
-
const lz = radius * Math.cos(phi);
|
| 1150 |
-
|
| 1151 |
-
const pos = new THREE.Vector3(lx, ly, lz).add(parentPos);
|
| 1152 |
-
|
| 1153 |
-
const nodeSize = 0.5 / level;
|
| 1154 |
-
const geo = new THREE.SphereGeometry(nodeSize, 8, 8);
|
| 1155 |
-
const mat = new THREE.MeshBasicMaterial({ color: color });
|
| 1156 |
-
const mesh = new THREE.Mesh(geo, mat);
|
| 1157 |
-
mesh.position.copy(pos);
|
| 1158 |
-
mesh.userData = { type: 'neuron', text: text, level: level };
|
| 1159 |
-
this.container.add(mesh);
|
| 1160 |
-
|
| 1161 |
-
const lineGeo = new THREE.BufferGeometry().setFromPoints([parentPos, pos]);
|
| 1162 |
-
const lineMat = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.15 });
|
| 1163 |
-
this.container.add(new THREE.Line(lineGeo, lineMat));
|
| 1164 |
-
|
| 1165 |
-
this.buildTreeRecursive(children, pos, level + 1, color);
|
| 1166 |
-
});
|
| 1167 |
-
}
|
| 1168 |
-
|
| 1169 |
-
clear() {
|
| 1170 |
-
while(this.container.children.length > 0){
|
| 1171 |
-
const obj = this.container.children[0];
|
| 1172 |
-
this.container.remove(obj);
|
| 1173 |
-
if(obj.geometry) obj.geometry.dispose();
|
| 1174 |
-
if(obj.material) obj.material.dispose();
|
| 1175 |
-
}
|
| 1176 |
-
this.userNodes.clear();
|
| 1177 |
-
}
|
| 1178 |
-
|
| 1179 |
-
update() {
|
| 1180 |
-
this.container.children.forEach(child => {
|
| 1181 |
-
if (child.userData.animate) {
|
| 1182 |
-
child.rotation.z += child.userData.speed;
|
| 1183 |
-
}
|
| 1184 |
-
if (child.userData.isBillboard) {
|
| 1185 |
-
child.lookAt(this.app.world.camera.position);
|
| 1186 |
-
}
|
| 1187 |
-
});
|
| 1188 |
-
}
|
| 1189 |
-
}
|
| 1190 |
-
|
| 1191 |
-
class Minimap {
|
| 1192 |
-
constructor(appInstance) {
|
| 1193 |
-
this.app = appInstance;
|
| 1194 |
-
this.canvas = document.getElementById('minimap-canvas');
|
| 1195 |
-
this.ctx = this.canvas.getContext('2d');
|
| 1196 |
-
this.scale = 0.05;
|
| 1197 |
-
this.width = this.canvas.width;
|
| 1198 |
-
this.height = this.canvas.height;
|
| 1199 |
-
this.cx = this.width / 2;
|
| 1200 |
-
this.cy = this.height / 2;
|
| 1201 |
-
|
| 1202 |
-
this.setupInteractions();
|
| 1203 |
-
}
|
| 1204 |
-
|
| 1205 |
-
setupInteractions() {
|
| 1206 |
-
document.getElementById('map-zoom-in').onclick = () => { this.scale *= 1.2; this.draw(); };
|
| 1207 |
-
document.getElementById('map-zoom-out').onclick = () => { this.scale /= 1.2; this.draw(); };
|
| 1208 |
-
|
| 1209 |
-
this.canvas.addEventListener('click', (e) => {
|
| 1210 |
-
const rect = this.canvas.getBoundingClientRect();
|
| 1211 |
-
const x = e.clientX - rect.left;
|
| 1212 |
-
const y = e.clientY - rect.top;
|
| 1213 |
-
this.handleClick(x, y);
|
| 1214 |
-
});
|
| 1215 |
-
}
|
| 1216 |
-
|
| 1217 |
-
draw() {
|
| 1218 |
-
this.ctx.fillStyle = '#000000';
|
| 1219 |
-
this.ctx.fillRect(0, 0, this.width, this.height);
|
| 1220 |
-
|
| 1221 |
-
// Grid
|
| 1222 |
-
this.ctx.strokeStyle = '#112233';
|
| 1223 |
-
this.ctx.lineWidth = 1;
|
| 1224 |
-
this.ctx.beginPath();
|
| 1225 |
-
const gridSize = 20;
|
| 1226 |
-
for(let x=0; x<this.width; x+=gridSize) { this.ctx.moveTo(x,0); this.ctx.lineTo(x,this.height); }
|
| 1227 |
-
for(let y=0; y<this.height; y+=gridSize) { this.ctx.moveTo(0,y); this.ctx.lineTo(this.width,y); }
|
| 1228 |
-
this.ctx.stroke();
|
| 1229 |
-
|
| 1230 |
-
const galaxy = this.app.world.galaxySystem;
|
| 1231 |
-
|
| 1232 |
-
galaxy.userNodes.forEach((data, uid) => {
|
| 1233 |
-
const mapX = this.cx + data.origin.x * this.scale;
|
| 1234 |
-
const mapY = this.cy + data.origin.z * this.scale;
|
| 1235 |
-
|
| 1236 |
-
const isMe = uid === this.app.auth.currentUser?.uid;
|
| 1237 |
-
|
| 1238 |
-
this.ctx.fillStyle = isMe ? '#00f3ff' : '#0066ff';
|
| 1239 |
-
this.ctx.beginPath();
|
| 1240 |
-
this.ctx.arc(mapX, mapY, isMe ? 4 : 2, 0, Math.PI * 2);
|
| 1241 |
-
this.ctx.fill();
|
| 1242 |
-
|
| 1243 |
-
// Nombre
|
| 1244 |
-
if (isMe) {
|
| 1245 |
-
this.ctx.fillStyle = '#ffffff';
|
| 1246 |
-
this.ctx.font = '10px Rajdhani';
|
| 1247 |
-
this.ctx.fillText("YO", mapX + 6, mapY + 3);
|
| 1248 |
-
}
|
| 1249 |
-
});
|
| 1250 |
-
}
|
| 1251 |
-
|
| 1252 |
-
handleClick(mx, my) {
|
| 1253 |
-
const worldX = (mx - this.cx) / this.scale;
|
| 1254 |
-
const worldZ = (my - this.cy) / this.scale;
|
| 1255 |
-
|
| 1256 |
-
let closest = null;
|
| 1257 |
-
let minDist = Infinity;
|
| 1258 |
-
|
| 1259 |
-
this.app.world.galaxySystem.userNodes.forEach((data) => {
|
| 1260 |
-
const dx = data.origin.x - worldX;
|
| 1261 |
-
const dz = data.origin.z - worldZ;
|
| 1262 |
-
const dist = Math.sqrt(dx*dx + dz*dz);
|
| 1263 |
-
if (dist < minDist) {
|
| 1264 |
-
minDist = dist;
|
| 1265 |
-
closest = data.origin;
|
| 1266 |
-
}
|
| 1267 |
-
});
|
| 1268 |
-
|
| 1269 |
-
if (closest && minDist < 1000) {
|
| 1270 |
-
this.app.world.cameraFlyTo(closest);
|
| 1271 |
-
}
|
| 1272 |
-
}
|
| 1273 |
-
}
|
| 1274 |
-
|
| 1275 |
-
class World3D {
|
| 1276 |
-
constructor(appInstance) {
|
| 1277 |
-
this.app = appInstance;
|
| 1278 |
-
this.container = document.getElementById('canvas-container');
|
| 1279 |
-
this.font = null;
|
| 1280 |
-
|
| 1281 |
-
this.initThree();
|
| 1282 |
-
this.initStars();
|
| 1283 |
-
|
| 1284 |
-
this.galaxySystem = new GalaxySystem(this.app);
|
| 1285 |
-
this.raycaster = new THREE.Raycaster();
|
| 1286 |
-
this.mouse = new THREE.Vector2();
|
| 1287 |
-
|
| 1288 |
-
window.addEventListener('resize', () => this.onResize());
|
| 1289 |
-
document.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
| 1290 |
-
}
|
| 1291 |
-
|
| 1292 |
-
initThree() {
|
| 1293 |
-
this.scene = new THREE.Scene();
|
| 1294 |
-
this.scene.fog = new THREE.FogExp2(Config.COLORS.bg, 0.0008);
|
| 1295 |
-
|
| 1296 |
-
this.camera = new THREE.PerspectiveCamera(Config.CONSTANTS.CAMERA_FOV, window.innerWidth / window.innerHeight, 0.1, Config.CONSTANTS.CAMERA_FAR);
|
| 1297 |
-
this.camera.position.set(0, 100, 200);
|
| 1298 |
-
|
| 1299 |
-
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
| 1300 |
-
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 1301 |
-
this.renderer.setPixelRatio(window.devicePixelRatio);
|
| 1302 |
-
this.renderer.setClearColor(Config.COLORS.bg);
|
| 1303 |
-
this.container.appendChild(this.renderer.domElement);
|
| 1304 |
-
|
| 1305 |
-
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
| 1306 |
-
this.controls.enableDamping = true;
|
| 1307 |
-
this.controls.dampingFactor = 0.05;
|
| 1308 |
-
this.controls.maxDistance = 2000;
|
| 1309 |
-
|
| 1310 |
-
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
|
| 1311 |
-
this.scene.add(ambient);
|
| 1312 |
-
const dir = new THREE.DirectionalLight(0xffffff, 1);
|
| 1313 |
-
dir.position.set(100, 200, 100);
|
| 1314 |
-
this.scene.add(dir);
|
| 1315 |
-
}
|
| 1316 |
-
|
| 1317 |
-
initStars() {
|
| 1318 |
-
const count = 8000;
|
| 1319 |
-
const geo = new THREE.BufferGeometry();
|
| 1320 |
-
const pos = new Float32Array(count * 3);
|
| 1321 |
-
const colors = new Float32Array(count * 3);
|
| 1322 |
-
|
| 1323 |
-
for(let i=0; i<count; i++) {
|
| 1324 |
-
pos[i*3] = (Math.random()-0.5) * 4000;
|
| 1325 |
-
pos[i*3+1] = (Math.random()-0.5) * 2000;
|
| 1326 |
-
pos[i*3+2] = (Math.random()-0.5) * 4000;
|
| 1327 |
-
|
| 1328 |
-
const starType = Math.random();
|
| 1329 |
-
if (starType > 0.9) {
|
| 1330 |
-
colors[i*3] = 0.5; colors[i*3+1] = 0.8; colors[i*3+2] = 1;
|
| 1331 |
-
} else if (starType > 0.7) {
|
| 1332 |
-
colors[i*3] = 1; colors[i*3+1] = 0.9; colors[i*3+2] = 0.6;
|
| 1333 |
-
} else {
|
| 1334 |
-
colors[i*3] = 1; colors[i*3+1] = 1; colors[i*3+2] = 1;
|
| 1335 |
-
}
|
| 1336 |
-
}
|
| 1337 |
-
|
| 1338 |
-
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
| 1339 |
-
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
| 1340 |
-
|
| 1341 |
-
const mat = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8 });
|
| 1342 |
-
this.stars = new THREE.Points(geo, mat);
|
| 1343 |
-
this.scene.add(this.stars);
|
| 1344 |
-
}
|
| 1345 |
-
|
| 1346 |
-
async loadResources() {
|
| 1347 |
-
return new Promise((resolve) => {
|
| 1348 |
-
const loader = new FontLoader();
|
| 1349 |
-
loader.load('https://cdn.jsdelivr.net/npm/three@0.128.0/examples/fonts/helvetiker_bold.typeface.json', (font) => {
|
| 1350 |
-
this.font = font;
|
| 1351 |
-
this.comet = new Comet(this.scene, font);
|
| 1352 |
-
resolve();
|
| 1353 |
-
}, undefined, (err) => {
|
| 1354 |
-
console.error("Font loading error:", err);
|
| 1355 |
-
resolve(); // Continue without font
|
| 1356 |
-
});
|
| 1357 |
-
});
|
| 1358 |
-
}
|
| 1359 |
-
|
| 1360 |
-
cameraFlyTo(target) {
|
| 1361 |
-
new TWEEN.Tween(this.camera.position)
|
| 1362 |
-
.to({ x: target.x, y: target.y + 60, z: target.z + 120 }, 2000)
|
| 1363 |
-
.easing(TWEEN.Easing.Cubic.InOut)
|
| 1364 |
-
.start();
|
| 1365 |
-
|
| 1366 |
-
new TWEEN.Tween(this.controls.target)
|
| 1367 |
-
.to({ x: target.x, y: target.y, z: target.z }, 2000)
|
| 1368 |
-
.easing(TWEEN.Easing.Cubic.InOut)
|
| 1369 |
-
.start();
|
| 1370 |
-
}
|
| 1371 |
-
|
| 1372 |
-
onResize() {
|
| 1373 |
-
this.camera.aspect = window.innerWidth / window.innerHeight;
|
| 1374 |
-
this.camera.updateProjectionMatrix();
|
| 1375 |
-
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 1376 |
-
}
|
| 1377 |
-
|
| 1378 |
-
onMouseMove(e) {
|
| 1379 |
-
this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
| 1380 |
-
this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
| 1381 |
-
|
| 1382 |
-
const tooltip = document.getElementById('tooltip');
|
| 1383 |
-
tooltip.style.left = (e.clientX + 20) + 'px';
|
| 1384 |
-
tooltip.style.top = (e.clientY + 20) + 'px';
|
| 1385 |
-
}
|
| 1386 |
-
|
| 1387 |
-
animate() {
|
| 1388 |
-
requestAnimationFrame(() => this.animate());
|
| 1389 |
-
TWEEN.update();
|
| 1390 |
-
this.controls.update();
|
| 1391 |
-
|
| 1392 |
-
if (this.comet) this.comet.update(this.camera);
|
| 1393 |
-
if (this.galaxySystem) this.galaxySystem.update();
|
| 1394 |
-
if (this.stars) this.stars.rotation.y += 0.0001;
|
| 1395 |
-
|
| 1396 |
-
// Raycasting logic
|
| 1397 |
-
this.raycaster.setFromCamera(this.mouse, this.camera);
|
| 1398 |
-
const intersects = this.raycaster.intersectObjects(this.galaxySystem.container.children);
|
| 1399 |
-
|
| 1400 |
-
const tooltip = document.getElementById('tooltip');
|
| 1401 |
-
if (intersects.length > 0) {
|
| 1402 |
-
const obj = intersects[0].object;
|
| 1403 |
-
if (obj.userData && (obj.userData.text || obj.userData.username)) {
|
| 1404 |
-
tooltip.style.display = 'block';
|
| 1405 |
-
const title = obj.userData.username || obj.userData.text;
|
| 1406 |
-
const type = obj.userData.type || 'NODO DE DATO';
|
| 1407 |
-
tooltip.innerHTML = `
|
| 1408 |
-
<div class="tooltip-title">${title}</div>
|
| 1409 |
-
<div class="tooltip-meta">TIPO: ${type.toUpperCase()}</div>
|
| 1410 |
-
`;
|
| 1411 |
-
document.body.style.cursor = 'pointer';
|
| 1412 |
-
} else {
|
| 1413 |
-
tooltip.style.display = 'none';
|
| 1414 |
-
document.body.style.cursor = 'default';
|
| 1415 |
-
}
|
| 1416 |
-
} else {
|
| 1417 |
-
tooltip.style.display = 'none';
|
| 1418 |
-
document.body.style.cursor = 'default';
|
| 1419 |
-
}
|
| 1420 |
-
|
| 1421 |
-
this.renderer.render(this.scene, this.camera);
|
| 1422 |
-
|
| 1423 |
-
if (this.app.minimap) this.app.minimap.draw();
|
| 1424 |
-
}
|
| 1425 |
-
}
|
| 1426 |
-
|
| 1427 |
-
class UIManager {
|
| 1428 |
-
constructor(appInstance) {
|
| 1429 |
-
this.app = appInstance;
|
| 1430 |
-
this.setupListeners();
|
| 1431 |
-
}
|
| 1432 |
-
|
| 1433 |
-
setupListeners() {
|
| 1434 |
-
const s1 = document.getElementById('range-l1');
|
| 1435 |
-
const s2 = document.getElementById('range-l2');
|
| 1436 |
-
const s3 = document.getElementById('range-l3');
|
| 1437 |
-
|
| 1438 |
-
if(s1) s1.oninput = () => document.getElementById('val-l1').innerText = s1.value;
|
| 1439 |
-
if(s2) s2.oninput = () => document.getElementById('val-l2').innerText = s2.value;
|
| 1440 |
-
if(s3) s3.oninput = () => document.getElementById('val-l3').innerText = s3.value;
|
| 1441 |
-
|
| 1442 |
-
const vizBtn = document.getElementById('btn-visualize');
|
| 1443 |
-
if(vizBtn) vizBtn.addEventListener('click', () => this.handleVisualize());
|
| 1444 |
-
}
|
| 1445 |
-
|
| 1446 |
-
async handleVisualize() {
|
| 1447 |
-
const input = document.getElementById('seed-input');
|
| 1448 |
-
const topic = input.value.trim();
|
| 1449 |
-
if (!topic) return;
|
| 1450 |
-
|
| 1451 |
-
const btn = document.getElementById('btn-visualize');
|
| 1452 |
-
btn.disabled = true;
|
| 1453 |
-
btn.innerHTML = 'PROCESANDO NEURONA...';
|
| 1454 |
-
|
| 1455 |
-
const bar = document.getElementById('progress-bar');
|
| 1456 |
-
bar.style.width = '20%';
|
| 1457 |
-
|
| 1458 |
-
try {
|
| 1459 |
-
const levels = {
|
| 1460 |
-
l1: document.getElementById('range-l1').value,
|
| 1461 |
-
l2: document.getElementById('range-l2').value,
|
| 1462 |
-
l3: document.getElementById('range-l3').value
|
| 1463 |
-
};
|
| 1464 |
-
|
| 1465 |
-
bar.style.width = '60%';
|
| 1466 |
-
const data = await this.app.ai.generate(topic, levels);
|
| 1467 |
-
|
| 1468 |
-
bar.style.width = '80%';
|
| 1469 |
-
await this.app.data.saveNeuron(topic, data);
|
| 1470 |
-
|
| 1471 |
-
bar.style.width = '100%';
|
| 1472 |
-
input.value = '';
|
| 1473 |
-
|
| 1474 |
-
} catch (error) {
|
| 1475 |
-
console.error(error);
|
| 1476 |
-
alert("ERROR EN LA GENERACIÓN DE LA NEURONA");
|
| 1477 |
-
} finally {
|
| 1478 |
-
setTimeout(() => {
|
| 1479 |
-
btn.disabled = false;
|
| 1480 |
-
btn.innerHTML = `
|
| 1481 |
-
<span>SEMBRAR EN LA RED</span>
|
| 1482 |
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1483 |
-
<circle cx="12" cy="12" r="10"></circle>
|
| 1484 |
-
<line x1="12" y1="8" x2="12" y2="16"></line>
|
| 1485 |
-
<line x1="8" y1="12" x2="16" y2="12"></line>
|
| 1486 |
-
</svg>
|
| 1487 |
-
`;
|
| 1488 |
-
bar.style.width = '0%';
|
| 1489 |
-
}, 1000);
|
| 1490 |
-
}
|
| 1491 |
-
}
|
| 1492 |
-
|
| 1493 |
-
updateUserList(usersMap) {
|
| 1494 |
-
const list = document.getElementById('users-list');
|
| 1495 |
-
list.innerHTML = '';
|
| 1496 |
-
|
| 1497 |
-
usersMap.forEach((user) => {
|
| 1498 |
-
const div = document.createElement('div');
|
| 1499 |
-
div.className = 'user-item';
|
| 1500 |
-
div.innerHTML = `
|
| 1501 |
-
<span>${user.username}</span>
|
| 1502 |
-
<span class="user-count-badge">${user.maps.length}</span>
|
| 1503 |
-
`;
|
| 1504 |
-
div.onclick = () => {
|
| 1505 |
-
const node = this.app.world.galaxySystem.userNodes.get(user.id);
|
| 1506 |
-
if (node) this.app.world.cameraFlyTo(node.origin);
|
| 1507 |
-
};
|
| 1508 |
-
list.appendChild(div);
|
| 1509 |
-
});
|
| 1510 |
-
}
|
| 1511 |
-
|
| 1512 |
-
showLogin() {
|
| 1513 |
-
const overlay = document.getElementById('login-overlay');
|
| 1514 |
-
if(overlay) overlay.classList.remove('fade-out');
|
| 1515 |
-
}
|
| 1516 |
-
|
| 1517 |
-
hideLogin() {
|
| 1518 |
-
const overlay = document.getElementById('login-overlay');
|
| 1519 |
-
if(overlay) overlay.classList.add('fade-out');
|
| 1520 |
-
}
|
| 1521 |
-
}
|
| 1522 |
-
|
| 1523 |
-
class App {
|
| 1524 |
-
constructor() {
|
| 1525 |
-
this.auth = new AuthManager(this);
|
| 1526 |
-
this.data = new DataManager(this);
|
| 1527 |
-
this.world = new World3D(this);
|
| 1528 |
-
this.ui = new UIManager(this);
|
| 1529 |
-
this.ai = new AIManager(this);
|
| 1530 |
-
this.minimap = new Minimap(this);
|
| 1531 |
-
}
|
| 1532 |
-
|
| 1533 |
-
async start() {
|
| 1534 |
-
try {
|
| 1535 |
-
await this.world.loadResources();
|
| 1536 |
-
await this.auth.init();
|
| 1537 |
-
this.world.animate();
|
| 1538 |
-
} catch (err) {
|
| 1539 |
-
console.error("FATAL ERROR IN APP START:", err);
|
| 1540 |
-
const loader = document.getElementById('loading-text');
|
| 1541 |
-
if(loader) loader.innerText = "ERROR FATAL: " + err.message;
|
| 1542 |
-
}
|
| 1543 |
-
}
|
| 1544 |
-
}
|
| 1545 |
-
|
| 1546 |
-
const app = new App();
|
| 1547 |
-
app.start();
|
| 1548 |
|
| 1549 |
-
</script>
|
| 1550 |
</body>
|
| 1551 |
-
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="es">
|
| 3 |
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>ECOTAGS · Galaxias 3D</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="./styles.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
+
<div id="container"></div>
|
| 13 |
+
<div id="tooltip"></div>
|
| 14 |
+
|
| 15 |
+
<div id="ui">
|
| 16 |
+
<div id="hdr">
|
| 17 |
+
<div id="title">
|
| 18 |
+
<svg class="w-6 h-6 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/></svg>
|
| 19 |
+
<span>ECOTAGS</span>
|
| 20 |
+
</div>
|
| 21 |
+
<div class="flex items-center gap-2">
|
| 22 |
+
<span id="statusPill" class="hidden">Conectado</span>
|
| 23 |
+
<button id="logoutBtn" class="btn bg-red-600 text-white px-3 py-1 rounded text-sm hidden">Cerrar sesión</button>
|
| 24 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</div>
|
| 26 |
+
<div id="panel">
|
| 27 |
+
<div class="mb-3">
|
| 28 |
+
<div class="text-xs text-gray-400 mb-1">Usuario</div>
|
| 29 |
+
<div id="usernameLabel" class="text-green-300 font-semibold">-</div>
|
| 30 |
+
</div>
|
| 31 |
+
<input id="topicInput" type="text" class="w-full p-3 rounded-lg bg-gray-800/70 text-white border border-gray-700 focus:outline-none focus:border-green-500 mb-3" placeholder="Escribe uno o más hashtags">
|
| 32 |
+
<div class="grid grid-cols-3 gap-3 mb-4 text-sm">
|
| 33 |
+
<div>
|
| 34 |
+
<label class="text-gray-300">Nivel 1: <span id="level1Value" class="text-blue-300">10</span></label>
|
| 35 |
+
<input id="level1Slider" type="range" min="1" max="15" value="10">
|
|
|
|
|
|
|
|
|
|
| 36 |
</div>
|
| 37 |
+
<div>
|
| 38 |
+
<label class="text-gray-300">Nivel 2: <span id="level2Value" class="text-blue-300">5</span></label>
|
| 39 |
+
<input id="level2Slider" type="range" min="3" max="8" value="5">
|
|
|
|
|
|
|
| 40 |
</div>
|
| 41 |
+
<div>
|
| 42 |
+
<label class="text-gray-300">Nivel 3: <span id="level3Value" class="text-blue-300">3</span></label>
|
| 43 |
+
<input id="level3Slider" type="range" min="1" max="3" value="3">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
</div>
|
| 45 |
+
</div>
|
| 46 |
+
<button id="seedBtn" class="btn w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-500 hover:to-blue-500 text-white font-bold py-3 rounded-lg flex items-center justify-center gap-2">
|
| 47 |
+
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/></svg>
|
| 48 |
+
<span>Sembrar</span>
|
| 49 |
+
</button>
|
| 50 |
+
<div id="progressBarContainer" class="mt-3"><div id="progressBar"></div></div>
|
| 51 |
+
<details class="mt-4 bg-gray-800/60 rounded-md p-3 border border-gray-700">
|
| 52 |
+
<summary class="cursor-pointer text-sm text-blue-300 font-semibold">Configuración</summary>
|
| 53 |
+
<div class="mt-3 space-y-2 text-sm">
|
| 54 |
+
<div>
|
| 55 |
+
<label class="block text-gray-300">Gemini API Key</label>
|
| 56 |
+
<input id="geminiKeyInput" type="password" class="w-full p-2 rounded bg-gray-800 text-white border border-gray-700 focus:outline-none focus:border-blue-500" placeholder="AIza...">
|
| 57 |
+
</div>
|
| 58 |
+
<div class="flex items-center gap-2">
|
| 59 |
+
<button id="saveGeminiKeyBtn" class="btn bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded">Guardar clave</button>
|
| 60 |
+
<span id="geminiKeyStatus" class="text-gray-400"></span>
|
| 61 |
+
</div>
|
| 62 |
</div>
|
| 63 |
+
</details>
|
| 64 |
+
<div class="mt-4">
|
| 65 |
+
<h2 class="font-bold text-lg text-blue-300 mb-2">Galaxias de Usuarios</h2>
|
| 66 |
+
<div id="userList" class="w-full max-h-[22vh] overflow-y-auto text-gray-300 pr-1 text-sm"></div>
|
| 67 |
+
</div>
|
| 68 |
+
<div id="minimapContainer">
|
| 69 |
+
<div class="flex justify-between items-center mb-2">
|
| 70 |
+
<h2 class="font-bold text-lg text-blue-300">Minimapa</h2>
|
| 71 |
+
<div class="flex gap-1">
|
| 72 |
+
<button id="zoomOutButton" class="btn w-7 h-7 bg-gray-700 hover:bg-gray-600 rounded text-lg font-bold flex items-center justify-center">-</button>
|
| 73 |
+
<button id="zoomInButton" class="btn w-7 h-7 bg-gray-700 hover:bg-gray-600 rounded text-lg font-bold flex items-center justify-center">+</button>
|
| 74 |
+
</div>
|
| 75 |
</div>
|
| 76 |
+
<canvas id="minimap" width="340" height="200"></canvas>
|
| 77 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
</div>
|
| 79 |
+
</div>
|
| 80 |
|
| 81 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
|
| 82 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/controls/OrbitControls.js"></script>
|
| 83 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/geometries/TextGeometry.js"></script>
|
| 84 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/loaders/FontLoader.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
<script type="module" src="./src/main.js"></script>
|
| 87 |
</body>
|
| 88 |
+
</html>
|
src/config.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const firebaseConfig={apiKey:"AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",authDomain:"neuronal-1f3b9.firebaseapp.com",projectId:"neuronal-1f3b9",storageBucket:"neuronal-1f3b9.firebasestorage.app",messagingSenderId:"208887839866",appId:"1:208887839866:web:adbb697dd0b63195b10fc3",measurementId:"G-102SEBLQFJ"};
|
| 2 |
+
export const appId="neuronal-1f3b9";
|
| 3 |
+
export const gemKeyGet=()=>localStorage.getItem('GEMINI_API_KEY')||'';
|
| 4 |
+
export const gemKeySet=k=>localStorage.setItem('GEMINI_API_KEY',k||'');
|
src/db.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {getFirestore, collection, addDoc, onSnapshot, query, serverTimestamp} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
|
| 2 |
+
import {getDB} from './firebase.js';
|
| 3 |
+
|
| 4 |
+
export async function saveNeuron(n){const db=getDB();const ref=collection(db,'artifacts',n.appId,'public','data','neurons');await addDoc(ref,{userId:n.userId,username:n.username,label:n.label,level:n.level,position:{x:n.position.x,y:n.position.y,z:n.position.z},topic:n.topic||null,createdAt:serverTimestamp()});}
|
| 5 |
+
export function subscribeNeurons(appId,cb){const db=getDB();const ref=query(collection(db,'artifacts',appId,'public','data','neurons'));return onSnapshot(ref,cb);
|
| 6 |
+
}
|
src/firebase.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {initializeApp} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
|
| 2 |
+
import {getAuth, onAuthStateChanged, signInAnonymously, signOut as fbSignOut} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
|
| 3 |
+
import {getFirestore, doc, setDoc, getDoc, serverTimestamp} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
|
| 4 |
+
import {firebaseConfig, appId} from './config.js';
|
| 5 |
+
import {autoUsername} from './utils.js';
|
| 6 |
+
|
| 7 |
+
let app, db, auth, userId=null, username=null, ready=false;
|
| 8 |
+
let profilesCache={};
|
| 9 |
+
|
| 10 |
+
async function fetchIP(){try{const r=await fetch('https://api.ipify.org?format=json');if(!r.ok)throw 0;const j=await r.json();return j.ip||'';}catch{return''}}
|
| 11 |
+
|
| 12 |
+
export async function initFirebase(){app=initializeApp(firebaseConfig);db=getFirestore(app);auth=getAuth(app);const ip=await fetchIP();const last=localStorage.getItem('last_ip')||'';if(ip&&ip!==last){localStorage.removeItem('local_username');localStorage.setItem('last_ip',ip);}onAuthStateChanged(auth,async u=>{if(u){userId=u.uid;ready=true;const p=await ensureProfile(userId);username=p.username;renderAuthUI(true);}else{try{await signInAnonymously(auth);}catch{userId=localStorage.getItem('local_uid')||crypto.randomUUID();localStorage.setItem('local_uid',userId);let uname=localStorage.getItem('local_username');if(!uname){uname=autoUsername();localStorage.setItem('local_username',uname);}username=uname;ready=true;renderAuthUI(false);}}});return new Promise(res=>{const t=setInterval(()=>{if(ready){clearInterval(t);res();}},50);});}
|
| 13 |
+
|
| 14 |
+
export function getDB(){return db;}
|
| 15 |
+
export function getAuthState(){return{userId,username,ready}};
|
| 16 |
+
export async function ensureProfile(uid){if(profilesCache[uid])return profilesCache[uid];try{const ref=doc(db,'artifacts',appId,'users',uid,'user_data','profile');const s=await getDoc(ref);if(s.exists()){const p=s.data();profilesCache[uid]=p;return p;}const uname=localStorage.getItem('local_username')||autoUsername();const p={username:uname,createdAt:serverTimestamp()};await setDoc(ref,p);profilesCache[uid]=p;return p;}catch{const uname=localStorage.getItem('local_username')||autoUsername();const p={username:uname};profilesCache[uid]=p;return p;}}
|
| 17 |
+
export function getProfile(uid){return profilesCache[uid]||null}
|
| 18 |
+
export async function signOut(){try{await fbSignOut(auth);}catch{} localStorage.removeItem('local_username');}
|
| 19 |
+
function renderAuthUI(logged){const pill=document.getElementById('statusPill');const lo=document.getElementById('logoutBtn');const ul=document.getElementById('usernameLabel');if(logged){pill.classList.remove('hidden');lo.classList.remove('hidden');}else{pill.classList.remove('hidden');lo.classList.remove('hidden');}ul.textContent=username||'-';}
|
src/gemini.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {gemKeyGet} from './config.js';
|
| 2 |
+
|
| 3 |
+
export async function callGemini(topic,n1,n2,n3){const modelId='gemini-2.0-flash-exp';const schema={type:'OBJECT',properties:{analisis:{type:'STRING'},lista_palabras:{type:'ARRAY',items:{type:'OBJECT',properties:{palabra_principal:{type:'STRING'},variantes:{type:'ARRAY',items:{type:'OBJECT',properties:{palabra_variante:{type:'STRING'},sub_variantes:{type:'ARRAY',items:{type:'STRING'}}},required:['palabra_variante','sub_variantes']}}},required:['palabra_principal','variantes']}}},required:['lista_palabras']};const sys='Eres un analista. Devuelve JSON sin acentos.';const up=`Tema: "${topic}". 1) ${n1} palabras clave. 2) ${n2} variantes por cada una. 3) ${n3} sub-variantes por variante.`;const payload={contents:[{parts:[{text:up}]}],systemInstruction:{parts:[{text:sys}]},generationConfig:{responseMimeType:'application/json',responseSchema:schema}};const key=gemKeyGet();if(!key)throw new Error('Configura tu Gemini API Key.');const url=`https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${key}`;const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});if(!r.ok){throw new Error(await r.text()||'Error Gemini');}return await r.json();}
|
src/main.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {initFirebase, getAuthState, ensureProfile, signOut, getProfile} from './firebase.js';
|
| 2 |
+
import {initScene, clearScene, usernameSphere, addNeuronMesh, drawMinimap, focusOnUser, teleportToUser, userCenter} from './scene.js';
|
| 3 |
+
import {initUI, handleSeed} from './ui.js';
|
| 4 |
+
import {subscribeNeurons} from './db.js';
|
| 5 |
+
import {appId} from './config.js';
|
| 6 |
+
import {hslFromString} from './utils.js';
|
| 7 |
+
|
| 8 |
+
const THREE=window.THREE;
|
| 9 |
+
|
| 10 |
+
await initFirebase();
|
| 11 |
+
initScene();
|
| 12 |
+
initUI();
|
| 13 |
+
|
| 14 |
+
document.getElementById('seedBtn').addEventListener('click',handleSeed);
|
| 15 |
+
|
| 16 |
+
document.getElementById('logoutBtn').addEventListener('click',async()=>{await signOut(); location.reload();});
|
| 17 |
+
|
| 18 |
+
let profiles={};
|
| 19 |
+
|
| 20 |
+
subscribeNeurons(appId, async snap=>{
|
| 21 |
+
clearScene();
|
| 22 |
+
const perUser={};
|
| 23 |
+
snap.forEach(d=>{const v=d.data(); if(!v.userId||!v.position||!v.label) return; if(!perUser[v.userId]) perUser[v.userId]=[]; perUser[v.userId].push(v); if(v.username && !profiles[v.userId]) profiles[v.userId]={username:v.username};});
|
| 24 |
+
const uids=Object.keys(perUser);
|
| 25 |
+
const userList=document.getElementById('userList');
|
| 26 |
+
if (userList) userList.innerHTML='';
|
| 27 |
+
for(const uid of uids){
|
| 28 |
+
if(!profiles[uid]){ try{ const p=await ensureProfile(uid); profiles[uid]=p; }catch{} }
|
| 29 |
+
const uname=(profiles[uid]?.username)||`Usuario ${uid.slice(0,4)}`;
|
| 30 |
+
usernameSphere(uid,uname);
|
| 31 |
+
const arr=perUser[uid].sort((a,b)=>(a.createdAt?.seconds||0)-(b.createdAt?.seconds||0));
|
| 32 |
+
for(const n of arr){
|
| 33 |
+
const col=hslFromString(n.label).color;
|
| 34 |
+
const pos=new THREE.Vector3(n.position.x,n.position.y,n.position.z);
|
| 35 |
+
addNeuronMesh(uid,n.label,n.level||1,pos,col);
|
| 36 |
+
}
|
| 37 |
+
if (userList){
|
| 38 |
+
const item=document.createElement('div');
|
| 39 |
+
item.className='p-2 mb-1 rounded-md hover:bg-gray-700 cursor-pointer';
|
| 40 |
+
item.textContent=uname;
|
| 41 |
+
item.addEventListener('click',()=> teleportToUser(uid));
|
| 42 |
+
userList.appendChild(item);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
const me=getAuthState().userId;
|
| 46 |
+
drawMinimap(uids,profiles,me);
|
| 47 |
+
focusOnUser(me);
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
window.addEventListener('teleport',e=>{teleportToUser(e.detail.uid)});
|
| 51 |
+
|
| 52 |
+
document.getElementById('usernameLabel').textContent=getAuthState().username||'-';
|
src/scene.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {hslFromString, randomWord} from './utils.js';
|
| 2 |
+
|
| 3 |
+
const THREE=window.THREE;const OrbitControls=THREE.OrbitControls;const TextGeometry=THREE.TextGeometry;const FontLoader=THREE.FontLoader;
|
| 4 |
+
let scene,camera,renderer,controls,raycaster,mouse,tooltip,font,rootGroup,starsGroup,comet,cometTrail=[],cometTimer=0,cometWordTimer=0,cometWord='';
|
| 5 |
+
let minimapCtx,minimapScale=0.025,minimapDots=[];const MINIMAP_DOT_SIZE=2;const fontLoader=new FontLoader();
|
| 6 |
+
const usersCenters={};
|
| 7 |
+
|
| 8 |
+
export function getUsersCenters(){return usersCenters;}
|
| 9 |
+
export function userCenter(uid){if(usersCenters[uid])return usersCenters[uid];let hash=0;for(let i=0;i<uid.length;i++)hash=uid.charCodeAt(i)+((hash<<5)-hash);const ang=Math.abs(hash)%360*Math.PI/180;const ring=600+(Math.abs(hash)%7)*180;const x=Math.cos(ang)*ring;const z=Math.sin(ang)*ring;const y=((hash%37)-18)*2;const v=new THREE.Vector3(x,y,z);usersCenters[uid]=v;return v;}
|
| 10 |
+
export function spiralPosition(uid,i){const c=userCenter(uid);const a=0.55;const step=0.9;const ang=i*a;const r=6+step*i*0.45;const y=(i%18-9)*0.22;const x=Math.cos(ang)*r;const z=Math.sin(ang)*r;return new THREE.Vector3(c.x+x,c.y+y,c.z+z);}
|
| 11 |
+
|
| 12 |
+
export function initScene(){scene=new THREE.Scene();camera=new THREE.PerspectiveCamera(70,window.innerWidth/window.innerHeight,0.1,4000);camera.position.set(0,8,28);renderer=new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance'});renderer.setSize(window.innerWidth,window.innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,2));document.getElementById('container').appendChild(renderer.domElement);controls=new OrbitControls(camera,renderer.domElement);controls.enableDamping=true;controls.dampingFactor=.06;controls.target.set(0,0,0);const amb=new THREE.AmbientLight(0xbfd4ff,.75);scene.add(amb);const dir=new THREE.DirectionalLight(0xffffff,.9);dir.position.set(6,10,7);scene.add(dir);const hemi=new THREE.HemisphereLight(0x4fc3f7,0x0b1020,.35);scene.add(hemi);raycaster=new THREE.Raycaster();mouse=new THREE.Vector2();rootGroup=new THREE.Group();scene.add(rootGroup);tooltip=document.getElementById('tooltip');addBackgroundStars();addComet();const minimap=document.getElementById('minimap');minimapCtx=minimap.getContext('2d');minimap.addEventListener('click',onMinimapClick);document.getElementById('zoomInButton').addEventListener('click',()=>{minimapScale*=1.4;drawMinimap([],{})});document.getElementById('zoomOutButton').addEventListener('click',()=>{minimapScale/=1.4;drawMinimap([],{})});window.addEventListener('resize',onResize);window.addEventListener('mousemove',onPointerMove);fontLoader.load('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/fonts/helvetiker_regular.typeface.json',f=>{font=f;});animate();}
|
| 13 |
+
|
| 14 |
+
export function clearScene(){rootGroup.clear();}
|
| 15 |
+
export function focusOnUser(uid){const c=userCenter(uid);controls.target.copy(c);camera.position.copy(c.clone().add(new THREE.Vector3(0,8,26)));controls.update();}
|
| 16 |
+
export function teleportToUser(uid){const c=userCenter(uid);controls.target.copy(c);camera.position.copy(c.clone().add(new THREE.Vector3(0,8,26)));controls.update();}
|
| 17 |
+
|
| 18 |
+
function animate(){requestAnimationFrame(animate);controls.update();updateRaycast();updateComet();renderer.render(scene,camera);}
|
| 19 |
+
function onResize(){camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);}
|
| 20 |
+
function onPointerMove(e){mouse.x=e.clientX/window.innerWidth*2-1;mouse.y=-(e.clientY/window.innerHeight)*2+1;tooltip.style.left=`${e.clientX+16}px`;tooltip.style.top=`${e.clientY}px`}
|
| 21 |
+
function updateRaycast(){raycaster.setFromCamera(mouse,camera);const meshes=[];rootGroup.traverse(o=>{if(o.isMesh&&!o.userData.ignoreHit)meshes.push(o)});const is=raycaster.intersectObjects(meshes,false);if(is.length>0){const o=is[0].object;const d=o.userData;let html=`<strong>${d.label||d.hashtag||'Nodo'}</strong>`;if(d.level!=null)html+=`<br>Nivel: ${d.level}`;tooltip.innerHTML=html;tooltip.style.display='block';}else{tooltip.style.display='none';}}
|
| 22 |
+
|
| 23 |
+
function addBackgroundStars(){starsGroup=new THREE.Group();const geo=new THREE.BufferGeometry();const count=2000;const positions=new Float32Array(count*3);for(let i=0;i<count;i++){const r=1200*Math.pow(Math.random(),.6)+200;const a=Math.random()*Math.PI*2;const e=(Math.random()-0.5)*0.6;const x=Math.cos(a)*r;const z=Math.sin(a)*r;const y=r*e*0.25;positions[i*3]=x;positions[i*3+1]=y;positions[i*3+2]=z;}geo.setAttribute('position',new THREE.BufferAttribute(positions,3));const mat=new THREE.PointsMaterial({color:0x88b4ff,sizeAttenuation:true,size:1.2,transparent:true,opacity:.8});const pts=new THREE.Points(geo,mat);starsGroup.add(pts);scene.add(starsGroup);}
|
| 24 |
+
function addComet(){const cometGeo=new THREE.SphereGeometry(0.35,16,16);const cometMat=new THREE.MeshStandardMaterial({color:0xffe08a,emissive:0xffb703,emissiveIntensity:1.2,metalness:.2,roughness:.4});comet=new THREE.Mesh(cometGeo,cometMat);scene.add(comet);comet.userData.radius=220;comet.userData.speed=0.0022;comet.userData.angle=Math.random()*Math.PI*2;comet.userData.incl=0.25;}
|
| 25 |
+
function updateComet(){if(!comet)return;comet.userData.angle+=comet.userData.speed;const a=comet.userData.angle;const r=comet.userData.radius;const inc=comet.userData.incl;comet.position.set(Math.cos(a)*r,Math.sin(a*inc)*25,Math.sin(a)*r);cometTimer+=1/60;cometWordTimer+=1/60;if(cometTimer>.045){cometTimer=0;leaveTrailWord();}if(cometWordTimer>3){cometWordTimer=0;cometWord=randomWord();}for(let i=cometTrail.length-1;i>=0;i--){const m=cometTrail[i];m.material.opacity-=.01;m.position.y+=.03;if(m.material.opacity<=0){m.geometry.dispose();m.material.dispose();scene.remove(m);cometTrail.splice(i,1);}}}
|
| 26 |
+
function leaveTrailWord(){if(!font)return;const s=cometWord||randomWord();const g=new TextGeometry(s.toUpperCase(),{font,size:.9,height:.02,curveSegments:4,bevelEnabled:false});g.computeBoundingBox();const m=new THREE.MeshBasicMaterial({color:0x4ade80,transparent:true,opacity:.8});const mesh=new THREE.Mesh(g,m);mesh.position.copy(comet.position);mesh.position.y+=.6;mesh.rotation.y=Math.random()*Math.PI;cometTrail.push(mesh);scene.add(mesh);}
|
| 27 |
+
|
| 28 |
+
export function usernameSphere(uid,uname){const center=userCenter(uid);const mat=new THREE.MeshPhysicalMaterial({color:0x60a5fa,emissive:0x1e293b,roughness:.25,metalness:.35,clearcoat:.6,clearcoatRoughness:.2});const geo=new THREE.SphereGeometry(2.2,32,32);const s=new THREE.Mesh(geo,mat);s.position.copy(center);s.userData={ignoreHit:false,label:uname};rootGroup.add(s);if(font){const tg=new TextGeometry(uname.toUpperCase(),{font,size:.9,height:.05,curveSegments:6,bevelEnabled:false});tg.computeBoundingBox();const tm=new THREE.MeshBasicMaterial({color:0x93c5fd,transparent:true,opacity:.9});const text=new THREE.Mesh(tg,tm);text.position.copy(center);text.position.y+=3.1;text.position.x-=(tg.boundingBox.max.x-tg.boundingBox.min.x)/2;text.userData={ignoreHit:false,label:uname};rootGroup.add(text);}}
|
| 29 |
+
|
| 30 |
+
export function addNeuronMesh(uid,label,level,pos,baseColor){const mat=new THREE.MeshStandardMaterial({color:new THREE.Color(baseColor),roughness:.4,metalness:.2,emissive:new THREE.Color(baseColor).multiplyScalar(.15)});const r=level===1?.45:level===2?.28:.18;const geo=new THREE.IcosahedronGeometry(r,1);const m=new THREE.Mesh(geo,mat);m.position.copy(pos);m.userData={label,level};const rimGeo=new THREE.RingGeometry(r*1.2,r*1.35,24);const rimMat=new THREE.MeshBasicMaterial({color:0x94ffa8,transparent:true,opacity:.18,side:THREE.DoubleSide});const rim=new THREE.Mesh(rimGeo,rimMat);rim.position.copy(pos);rim.rotation.x=Math.PI/2;rim.userData={ignoreHit:true};rootGroup.add(rim);rootGroup.add(m);if(font){const tgeo=new TextGeometry(label.toUpperCase(),{font,size:r*.9,height:.02,curveSegments:4});tgeo.computeBoundingBox();const tmat=new THREE.MeshBasicMaterial({color:new THREE.Color(baseColor),transparent:true,opacity:.85});const tm=new THREE.Mesh(tgeo,tmat);tm.position.copy(pos);tm.position.y+=r+.08;tm.position.x-=(tgeo.boundingBox.max.x-tgeo.boundingBox.min.x)/2;tm.userData={label,level};rootGroup.add(tm);}}
|
| 31 |
+
|
| 32 |
+
export function drawMinimap(uids,profiles,me){if(!minimapCtx)return;const c=minimapCtx.canvas;minimapCtx.clearRect(0,0,c.width,c.height);minimapCtx.fillStyle='#0b1321';minimapCtx.fillRect(0,0,c.width,c.height);minimapDots=[];let cx=0,cz=0;if(me){const cc=userCenter(me);cx=cc.x;cz=cc.z;}for(const uid of uids){const center=userCenter(uid);const relX=(center.x-cx)*minimapScale;const relZ=(center.z-cz)*minimapScale;const x=c.width/2+relX;const y=c.height/2+relZ;const self=uid===me;const col=self?'#fde047':'#06b6d4';const size=MINIMAP_DOT_SIZE+(self?1:0);minimapCtx.beginPath();minimapCtx.arc(x,y,size,0,Math.PI*2);minimapCtx.fillStyle=col;minimapCtx.fill();minimapCtx.font='10px Orbitron';minimapCtx.fillStyle='#cbd5e1';const nm=(profiles[uid]?.username)||`Usr ${uid.slice(0,4)}`;minimapCtx.fillText(nm,x+size+3,y+3);minimapDots.push({x,y,uid});}}
|
| 33 |
+
function onMinimapClick(e){const c=minimapCtx.canvas;const r=c.getBoundingClientRect();const x=e.clientX-r.left;const y=e.clientY-r.top;let pick=null,dmin=12;for(let i=minimapDots.length-1;i>=0;i--){const d=Math.hypot(x-minimapDots[i].x,y-minimapDots[i].y);if(d<dmin){dmin=d;pick=minimapDots[i].uid;}}if(pick)window.dispatchEvent(new CustomEvent('teleport',{detail:{uid:pick}}));}
|
src/ui.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {gemKeySet, gemKeyGet, appId} from './config.js';
|
| 2 |
+
import {normalizeString, hslFromString} from './utils.js';
|
| 3 |
+
import {callGemini} from './gemini.js';
|
| 4 |
+
import {saveNeuron} from './db.js';
|
| 5 |
+
import {spiralPosition, addNeuronMesh, userCenter} from './scene.js';
|
| 6 |
+
import {getAuthState} from './firebase.js';
|
| 7 |
+
|
| 8 |
+
export function initUI(){document.getElementById('level1Slider').addEventListener('input',e=>document.getElementById('level1Value').innerText=e.target.value);document.getElementById('level2Slider').addEventListener('input',e=>document.getElementById('level2Value').innerText=e.target.value);document.getElementById('level3Slider').addEventListener('input',e=>document.getElementById('level3Value').innerText=e.target.value);document.getElementById('saveGeminiKeyBtn').addEventListener('click',()=>{const k=document.getElementById('geminiKeyInput').value.trim();gemKeySet(k);const s=document.getElementById('geminiKeyStatus');s.textContent='Guardada';setTimeout(()=>s.textContent='',1200)});const egk=gemKeyGet();if(egk)document.getElementById('geminiKeyInput').value=egk;}
|
| 9 |
+
|
| 10 |
+
export async function handleSeed(){const {userId,username}=getAuthState();const topic=normalizeString(document.getElementById('topicInput').value.trim());if(!topic) return;const n1=parseInt(document.getElementById('level1Slider').value,10);const n2=parseInt(document.getElementById('level2Slider').value,10);const n3=parseInt(document.getElementById('level3Slider').value,10);const btn=document.getElementById('seedBtn');const pbc=document.getElementById('progressBarContainer');const pb=document.getElementById('progressBar');btn.disabled=true;const bak=btn.innerHTML;btn.innerHTML='Analizando...';pbc.style.display='block';pb.style.transition='none';pb.style.width='0%';void pb.offsetWidth;pb.style.transition='width 18s ease-out';pb.style.width='92%';try{const data=await callGemini(topic,n1,n2,n3);const cand=data.candidates?.[0];const text=cand?.content?.parts?.[0]?.text||'{}';const parsed=JSON.parse(text);const lista=parsed.lista_palabras||[];const center=userCenter(userId);const rootCol=hslFromString(topic).color;addNeuronMesh(userId,topic,0,center.clone().add(new THREE.Vector3(0,3.1,0)),rootCol);let baseIndex=0;for(const l1 of lista){const tag1=normalizeString(l1.palabra_principal);const c1=hslFromString(tag1).color;const p1=spiralPosition(userId,baseIndex++);await saveNeuron({appId,userId,username,label:tag1,level:1,position:p1,topic});addNeuronMesh(userId,tag1,1,p1,c1);const variantes=l1.variantes||[];for(const v of variantes){const tag2=normalizeString(v.palabra_variante);const p2=spiralPosition(userId,baseIndex++);await saveNeuron({appId,userId,username,label:tag2,level:2,position:p2,topic});addNeuronMesh(userId,tag2,2,p2,c1);const subs=v.sub_variantes||[];for(const s of subs){const tag3=normalizeString(s);const p3=spiralPosition(userId,baseIndex++);await saveNeuron({appId,userId,username,label:tag3,level:3,position:p3,topic});addNeuronMesh(userId,tag3,3,p3,c1);}}}document.getElementById('topicInput').value='';}catch(e){}finally{btn.disabled=false;btn.innerHTML=bak;pb.style.transition='width .25s ease-in';pb.style.width='100%';setTimeout(()=>{pbc.style.display='none';pb.style.width='0%';},400)}}
|
src/utils.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function normalizeString(s){if(!s)return'';return s.normalize('NFD').replace(/[\u0300-\u036f]/g,'');}
|
| 2 |
+
export function hslFromString(s){let h=0;for(let i=0;i<s.length;i++)h=s.charCodeAt(i)+((h<<5)-h);h=Math.abs(h%360);return{h,color:`hsl(${h},80%,60%)`};}
|
| 3 |
+
export function autoUsername(){const a=["Nova","Orion","Lyra","Vela","Aster","Cosmo","Draco","Vasto","Lumen","Aqua","Gaia","Terra","Nix","Aura","Flux","Vox"];const b=a[Math.floor(Math.random()*a.length)];const k=Math.random().toString(36).slice(2,6).toUpperCase();return`${b}-${k}`;}
|
| 4 |
+
export function randomWord(){const r=["eco","vida","bosque","agua","tierra","aire","luz","verde","azul","flora","fauna","clima","ciclo","sol","lunar","salud","campo","rio","mar","cielo"];return r[Math.floor(Math.random()*r.length)];}
|
styles.css
CHANGED
|
@@ -1,74 +1 @@
|
|
| 1 |
-
body {
|
| 2 |
-
margin: 0;
|
| 3 |
-
overflow: hidden;
|
| 4 |
-
background: #020617;
|
| 5 |
-
font-family: "Orbitron", sans-serif;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
#scene {
|
| 9 |
-
position: fixed;
|
| 10 |
-
inset: 0;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
#ui {
|
| 14 |
-
position: fixed;
|
| 15 |
-
top: 16px;
|
| 16 |
-
left: 16px;
|
| 17 |
-
z-index: 20;
|
| 18 |
-
width: 320px;
|
| 19 |
-
background: rgba(15, 23, 42, 0.92);
|
| 20 |
-
padding: 16px;
|
| 21 |
-
border-radius: 14px;
|
| 22 |
-
color: white;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
.ui-header {
|
| 26 |
-
display: flex;
|
| 27 |
-
justify-content: space-between;
|
| 28 |
-
align-items: center;
|
| 29 |
-
margin-bottom: 8px;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
#username {
|
| 33 |
-
color: #4ade80;
|
| 34 |
-
font-size: 0.85rem;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
#logoutBtn {
|
| 38 |
-
background: #dc2626;
|
| 39 |
-
border: none;
|
| 40 |
-
padding: 4px 8px;
|
| 41 |
-
font-size: 0.75rem;
|
| 42 |
-
border-radius: 4px;
|
| 43 |
-
color: white;
|
| 44 |
-
cursor: pointer;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
#topicInput {
|
| 48 |
-
width: 100%;
|
| 49 |
-
padding: 8px;
|
| 50 |
-
margin-bottom: 8px;
|
| 51 |
-
border-radius: 6px;
|
| 52 |
-
background: #020617;
|
| 53 |
-
border: 1px solid #334155;
|
| 54 |
-
color: white;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
#seedBtn {
|
| 58 |
-
width: 100%;
|
| 59 |
-
padding: 10px;
|
| 60 |
-
border-radius: 6px;
|
| 61 |
-
background: #16a34a;
|
| 62 |
-
color: white;
|
| 63 |
-
font-weight: bold;
|
| 64 |
-
cursor: pointer;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
#minimap {
|
| 68 |
-
width: 100%;
|
| 69 |
-
height: 180px;
|
| 70 |
-
margin-top: 12px;
|
| 71 |
-
background: #020617;
|
| 72 |
-
border-radius: 8px;
|
| 73 |
-
border: 1px solid #334155;
|
| 74 |
-
}
|
|
|
|
| 1 |
+
body{font-family:'Orbitron',sans-serif;margin:0;overflow:hidden;background:radial-gradient(1200px 800px at 70% 10%,#0b1220 0%,#0a0f1a 40%,#070b14 70%,#04080f 100%)}#container{position:fixed;inset:0}canvas{display:block}#ui{position:fixed;top:20px;left:20px;z-index:20;background:rgba(17,24,39,.92);border:1px solid rgba(59,130,246,.18);box-shadow:0 10px 30px rgba(0,0,0,.45);border-radius:16px;width:380px;color:#e5e7eb;display:flex;flex-direction:column;max-height:calc(100vh - 40px)}#hdr{padding:14px 16px;border-bottom:1px solid rgba(59,130,246,.18);display:flex;align-items:center;justify-content:space-between;gap:10px}#title{display:flex;align-items:center;gap:10px;color:#a7f3d0;font-weight:800;letter-spacing:1px}#panel{padding:14px 16px;overflow:auto}#tooltip{position:absolute;display:none;background:rgba(0,0,0,.85);color:#fff;padding:8px 12px;border-radius:8px;z-index:30;pointer-events:none;font-size:13px;border:1px solid rgba(255,255,255,.1)}input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:8px;background:linear-gradient(90deg,#374151,#1f2937);border-radius:6px;outline:none}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;background:#22c55e;border-radius:50%;border:2px solid #052e1a;box-shadow:0 0 0 3px rgba(34,197,94,.25);cursor:pointer}#progressBarContainer{width:100%;background:#1f2937;border-radius:6px;overflow:hidden;display:none;height:8px;margin-top:10px;border:1px solid rgba(59,130,246,.18)}#progressBar{width:0;height:8px;background:linear-gradient(90deg,#22c55e,#3b82f6);box-shadow:0 0 12px #22c55e inset}#minimapContainer{width:100%;height:240px;margin-top:12px}#minimap{width:100%;height:100%;background:linear-gradient(180deg,#0d1726,#0b1321);border-radius:10px;border:1px solid rgba(148,163,184,.25);cursor:pointer}.btn{transition:all .2s ease}.btn:hover{transform:translateY(-1px);filter:brightness(1.05)}#statusPill{font-size:11px;padding:4px 8px;border-radius:999px;background:rgba(20,184,166,.15);color:#5eead4;border:1px solid rgba(45,212,191,.25)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|