| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover">
|
| <title>K1RL QUASAR — Quantitative Intelligence Observatory</title>
|
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| <style>
|
|
|
| * {
|
| margin: 0;
|
| padding: 0;
|
| box-sizing: border-box;
|
| -webkit-tap-highlight-color: transparent;
|
| }
|
|
|
| :root {
|
|
|
| font-size: 16px;
|
|
|
|
|
| --text-xs: 0.75rem;
|
| --text-sm: 0.875rem;
|
| --text-base: 1rem;
|
| --text-md: 1.25rem;
|
| --text-lg: 1.5rem;
|
| --text-xl: 2rem;
|
| --text-2xl: 2.5rem;
|
| --text-3xl: 3.5rem;
|
| --text-4xl: 5rem;
|
| --text-hero: 6rem;
|
|
|
|
|
| --weight-light: 200;
|
| --weight-regular: 400;
|
| --weight-medium: 500;
|
| --weight-semibold: 600;
|
| --weight-bold: 700;
|
|
|
|
|
| --tracking-tight: -0.02em;
|
| --tracking-normal: 0;
|
| --tracking-wide: 0.05em;
|
| --tracking-wider: 0.1em;
|
| --tracking-widest: 0.2em;
|
| --tracking-ultra: 0.4em;
|
|
|
|
|
| --leading-none: 1;
|
| --leading-tight: 1.1;
|
| --leading-snug: 1.2;
|
| --leading-normal: 1.4;
|
| --leading-relaxed: 1.6;
|
|
|
|
|
| --bg-deep: #000810;
|
| --bg-space: #001020;
|
| --accent-cyan: #00d4ff;
|
| --accent-cyan-dim: rgba(0, 212, 255, 0.3);
|
| --accent-cyan-glow: rgba(0, 212, 255, 0.15);
|
| --success: #00ff88;
|
| --danger: #ff4466;
|
| --warning: #ffaa00;
|
| --text-primary: #ffffff;
|
| --text-secondary: rgba(255, 255, 255, 0.7);
|
| --text-muted: rgba(255, 255, 255, 0.5);
|
| --text-dim: rgba(255, 255, 255, 0.35);
|
| --glass-bg: rgba(0, 20, 40, 0.4);
|
| --glass-border: rgba(0, 212, 255, 0.2);
|
| }
|
|
|
| body {
|
| font-family: 'Outfit', sans-serif;
|
| background: var(--bg-deep);
|
| color: var(--text-primary);
|
| line-height: var(--leading-normal);
|
| -webkit-font-smoothing: antialiased;
|
| -moz-osx-font-smoothing: grayscale;
|
| width: 100%;
|
| max-width: 100vw;
|
| overflow-x: hidden;
|
| }
|
|
|
|
|
| .font-mono { font-family: 'Space Mono', monospace; }
|
| .tracking-tight { letter-spacing: var(--tracking-tight); }
|
| .tracking-normal { letter-spacing: var(--tracking-normal); }
|
| .tracking-wide { letter-spacing: var(--tracking-wide); }
|
| .tracking-wider { letter-spacing: var(--tracking-wider); }
|
| .tracking-widest { letter-spacing: var(--tracking-widest); }
|
| .tracking-ultra { letter-spacing: var(--tracking-ultra); }
|
|
|
| .text-xs { font-size: var(--text-xs); }
|
| .text-sm { font-size: var(--text-sm); }
|
| .text-base { font-size: var(--text-base); }
|
| .text-md { font-size: var(--text-md); }
|
| .text-lg { font-size: var(--text-lg); }
|
| .text-xl { font-size: var(--text-xl); }
|
| .text-2xl { font-size: var(--text-2xl); }
|
| .text-3xl { font-size: var(--text-3xl); }
|
| .text-4xl { font-size: var(--text-4xl); }
|
| .text-hero { font-size: var(--text-hero); }
|
|
|
| .weight-light { font-weight: var(--weight-light); }
|
| .weight-regular { font-weight: var(--weight-regular); }
|
| .weight-medium { font-weight: var(--weight-medium); }
|
| .weight-semibold { font-weight: var(--weight-semibold); }
|
| .weight-bold { font-weight: var(--weight-bold); }
|
|
|
|
|
| #canvas-container {
|
| position: fixed;
|
| top: 0;
|
| left: 0;
|
| width: 100%;
|
| height: 100%;
|
| z-index: 0;
|
| pointer-events: none;
|
| }
|
|
|
| .gradient-overlay {
|
| position: fixed;
|
| top: 0;
|
| left: 0;
|
| width: 100%;
|
| height: 100%;
|
| background: radial-gradient(ellipse at 50% 100%, transparent 0%, rgba(0,8,16,0.7) 50%, rgba(0,8,16,0.95) 100%);
|
| z-index: 1;
|
| pointer-events: none;
|
| }
|
|
|
|
|
| .main-content {
|
| position: relative;
|
| z-index: 10;
|
| max-width: 1400px;
|
| margin: 0 auto;
|
| padding: 100px 40px 40px;
|
| width: 100%;
|
| }
|
|
|
|
|
| .nav-bar {
|
| position: fixed;
|
| top: 0;
|
| left: 0;
|
| right: 0;
|
| z-index: 100;
|
| display: flex;
|
| justify-content: space-between;
|
| align-items: center;
|
| padding: 20px 40px;
|
| background: rgba(0,8,16,0.85);
|
| backdrop-filter: blur(12px);
|
| border-bottom: 1px solid rgba(0,212,255,0.1);
|
| }
|
|
|
| .nav-logo {
|
| font-size: var(--text-lg);
|
| font-weight: var(--weight-light);
|
| letter-spacing: var(--tracking-widest);
|
| color: var(--text-primary);
|
| }
|
|
|
| .nav-logo span {
|
| color: var(--accent-cyan);
|
| font-weight: var(--weight-semibold);
|
| }
|
|
|
| .nav-links {
|
| display: flex;
|
| gap: 40px;
|
| list-style: none;
|
| }
|
|
|
| .nav-links a {
|
| color: var(--text-secondary);
|
| text-decoration: none;
|
| font-size: var(--text-sm);
|
| font-weight: var(--weight-medium);
|
| letter-spacing: var(--tracking-wider);
|
| transition: color 0.2s;
|
| }
|
|
|
| .nav-links a:hover {
|
| color: var(--accent-cyan);
|
| }
|
|
|
|
|
| .live-indicator {
|
| position: fixed;
|
| top: 20px;
|
| right: 40px;
|
| z-index: 101;
|
| display: flex;
|
| align-items: center;
|
| gap: 10px;
|
| padding: 10px 20px;
|
| background: rgba(0, 255, 136, 0.08);
|
| border: 1px solid rgba(0, 255, 136, 0.2);
|
| border-radius: 100px;
|
| backdrop-filter: blur(8px);
|
| }
|
|
|
| .live-dot {
|
| width: 8px;
|
| height: 8px;
|
| background: var(--success);
|
| border-radius: 50%;
|
| animation: pulse 1.5s infinite;
|
| }
|
|
|
| .live-text {
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-semibold);
|
| letter-spacing: var(--tracking-wider);
|
| color: var(--success);
|
| font-family: 'Space Mono', monospace;
|
| }
|
|
|
|
|
| .header {
|
| text-align: center;
|
| margin-bottom: 32px;
|
| }
|
|
|
| .logo-container {
|
| display: flex;
|
| align-items: center;
|
| justify-content: center;
|
| gap: 20px;
|
| margin-bottom: 16px;
|
| }
|
|
|
| .logo-icon {
|
| width: 56px;
|
| height: 56px;
|
| position: relative;
|
| }
|
|
|
| .logo-icon::before {
|
| content: '';
|
| position: absolute;
|
| inset: 0;
|
| border: 2px solid var(--accent-cyan);
|
| border-radius: 50%;
|
| animation: pulse-ring 2s infinite;
|
| }
|
|
|
| .logo-icon::after {
|
| content: '';
|
| position: absolute;
|
| inset: 14px;
|
| background: var(--accent-cyan);
|
| border-radius: 50%;
|
| box-shadow: 0 0 30px var(--accent-cyan);
|
| }
|
|
|
| .logo-text {
|
| font-size: var(--text-hero);
|
| font-weight: var(--weight-light);
|
| letter-spacing: var(--tracking-ultra);
|
| color: var(--text-primary);
|
| text-shadow: 0 0 40px var(--accent-cyan-dim);
|
| line-height: var(--leading-none);
|
| }
|
|
|
| .logo-text span {
|
| font-weight: var(--weight-bold);
|
| color: var(--accent-cyan);
|
| }
|
|
|
| .subtitle {
|
| font-size: var(--text-sm);
|
| font-weight: var(--weight-light);
|
| letter-spacing: var(--tracking-widest);
|
| text-transform: uppercase;
|
| color: var(--text-muted);
|
| margin-bottom: 8px;
|
| }
|
|
|
| .timestamp {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-xs);
|
| color: var(--text-dim);
|
| letter-spacing: var(--tracking-wide);
|
| }
|
|
|
| .inst-bar {
|
| text-align: center;
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-xs);
|
| letter-spacing: var(--tracking-wider);
|
| color: rgba(255,255,255,0.3);
|
| margin: 20px 0 32px;
|
| }
|
|
|
|
|
| .glass-card {
|
| background: var(--glass-bg);
|
| backdrop-filter: blur(16px);
|
| border: 1px solid var(--glass-border);
|
| border-radius: 24px;
|
| padding: 28px;
|
| transition: all 0.3s ease;
|
| display: flex;
|
| flex-direction: column;
|
| }
|
|
|
| .glass-card:hover {
|
| border-color: var(--accent-cyan-dim);
|
| box-shadow: 0 20px 40px rgba(0, 212, 255, 0.1);
|
| }
|
|
|
| .card-title {
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-semibold);
|
| letter-spacing: var(--tracking-wider);
|
| text-transform: uppercase;
|
| color: var(--text-muted);
|
| margin-bottom: 20px;
|
| display: flex;
|
| align-items: center;
|
| gap: 10px;
|
| }
|
|
|
| .card-title::before {
|
| content: '';
|
| width: 4px;
|
| height: 4px;
|
| background: var(--accent-cyan);
|
| border-radius: 50%;
|
| box-shadow: 0 0 10px var(--accent-cyan);
|
| }
|
|
|
| .card-content {
|
| flex: 1;
|
| display: flex;
|
| flex-direction: column;
|
| }
|
|
|
|
|
| .signal-panel {
|
| text-align: center;
|
| padding: 40px;
|
| margin-bottom: 32px;
|
| }
|
|
|
| .signal-label {
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-sm);
|
| font-weight: var(--weight-medium);
|
| letter-spacing: var(--tracking-widest);
|
| text-transform: uppercase;
|
| color: var(--text-muted);
|
| margin-bottom: 16px;
|
| }
|
|
|
| .signal-value {
|
| font-size: var(--text-4xl);
|
| font-weight: var(--weight-bold);
|
| letter-spacing: var(--tracking-wider);
|
| margin-bottom: 20px;
|
| line-height: var(--leading-tight);
|
| }
|
|
|
| .signal-buy {
|
| color: var(--success);
|
| text-shadow: 0 0 40px var(--success), 0 0 80px rgba(0, 255, 136, 0.2);
|
| }
|
|
|
| .signal-sell {
|
| color: var(--danger);
|
| text-shadow: 0 0 40px var(--danger), 0 0 80px rgba(255, 68, 102, 0.2);
|
| }
|
|
|
| .signal-neutral {
|
| color: var(--warning);
|
| text-shadow: 0 0 40px var(--warning), 0 0 80px rgba(255, 170, 0, 0.2);
|
| }
|
|
|
| .signal-stats {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-sm);
|
| color: var(--text-secondary);
|
| letter-spacing: var(--tracking-wide);
|
| display: flex;
|
| flex-wrap: wrap;
|
| justify-content: center;
|
| gap: 16px;
|
| margin-bottom: 20px;
|
| }
|
|
|
|
|
| .force-balance {
|
| margin-top: 32px;
|
| width: 100%;
|
| max-width: 600px;
|
| margin-left: auto;
|
| margin-right: auto;
|
| padding: 0 20px;
|
| }
|
|
|
|
|
| .pressure-labels {
|
| display: grid;
|
| grid-template-columns: 1fr 1fr;
|
| gap: 40px;
|
| margin-bottom: 16px;
|
| }
|
|
|
| .pressure-item {
|
| display: flex;
|
| flex-direction: column;
|
| }
|
|
|
| .pressure-item.buy {
|
| align-items: flex-start;
|
| }
|
|
|
| .pressure-item.sell {
|
| align-items: flex-end;
|
| text-align: right;
|
| }
|
|
|
| .pressure-label {
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-semibold);
|
| letter-spacing: var(--tracking-wider);
|
| text-transform: uppercase;
|
| color: var(--text-muted);
|
| margin-bottom: 6px;
|
| }
|
|
|
| .pressure-value {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-xl);
|
| font-weight: var(--weight-bold);
|
| letter-spacing: var(--tracking-wide);
|
| }
|
|
|
| .pressure-value.buy-value {
|
| color: #4fc3f7;
|
| text-shadow: 0 0 20px rgba(79, 195, 247, 0.3);
|
| }
|
|
|
| .pressure-value.sell-value {
|
| color: #ef5350;
|
| text-shadow: 0 0 20px rgba(239, 83, 80, 0.3);
|
| }
|
|
|
|
|
| .pressure-gauge {
|
| position: relative;
|
| margin: 24px 0;
|
| }
|
|
|
| .gauge-track {
|
| position: relative;
|
| height: 32px;
|
| background: linear-gradient(90deg,
|
| rgba(21, 101, 192, 0.15) 0%,
|
| rgba(0, 0, 0, 0.4) 50%,
|
| rgba(198, 40, 40, 0.15) 100%);
|
| border-radius: 8px;
|
| border: 1px solid rgba(255, 255, 255, 0.08);
|
| overflow: visible;
|
| }
|
|
|
| .gauge-fill-buy {
|
| position: absolute;
|
| top: 2px;
|
| bottom: 2px;
|
| left: 2px;
|
| right: 50%;
|
| background: linear-gradient(90deg, #1565C0, #42A5F5);
|
| border-radius: 6px 0 0 6px;
|
| transform-origin: right center;
|
| transform: scaleX(0);
|
| transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| }
|
|
|
| .gauge-fill-sell {
|
| position: absolute;
|
| top: 2px;
|
| bottom: 2px;
|
| left: 50%;
|
| right: 2px;
|
| background: linear-gradient(90deg, #ef5350, #c62828);
|
| border-radius: 0 6px 6px 0;
|
| transform-origin: left center;
|
| transform: scaleX(0);
|
| transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| }
|
|
|
|
|
| .gauge-center {
|
| position: absolute;
|
| left: 50%;
|
| top: -6px;
|
| bottom: -6px;
|
| width: 2px;
|
| background: rgba(255, 255, 255, 0.3);
|
| transform: translateX(-50%);
|
| z-index: 5;
|
| }
|
|
|
| .gauge-center::before {
|
| content: '';
|
| position: absolute;
|
| top: 0;
|
| left: 50%;
|
| transform: translateX(-50%);
|
| width: 8px;
|
| height: 8px;
|
| background: rgba(255, 255, 255, 0.4);
|
| border-radius: 50%;
|
| }
|
|
|
| .gauge-center::after {
|
| content: '';
|
| position: absolute;
|
| bottom: 0;
|
| left: 50%;
|
| transform: translateX(-50%);
|
| width: 8px;
|
| height: 8px;
|
| background: rgba(255, 255, 255, 0.4);
|
| border-radius: 50%;
|
| }
|
|
|
|
|
| .gauge-marker {
|
| position: absolute;
|
| top: 50%;
|
| left: 50%;
|
| transform: translate(-50%, -50%);
|
| width: 20px;
|
| height: 20px;
|
| background: white;
|
| border: 3px solid rgba(0, 0, 0, 0.8);
|
| border-radius: 50%;
|
| z-index: 10;
|
| box-shadow:
|
| 0 0 20px rgba(255, 255, 255, 0.5),
|
| 0 2px 8px rgba(0, 0, 0, 0.4);
|
| transition: left 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| }
|
|
|
| .gauge-marker.buy-dominant {
|
| background: #4fc3f7;
|
| border-color: #1565C0;
|
| }
|
|
|
| .gauge-marker.sell-dominant {
|
| background: #ef5350;
|
| border-color: #c62828;
|
| }
|
|
|
|
|
| .gauge-scale {
|
| display: flex;
|
| justify-content: space-between;
|
| margin-top: 10px;
|
| padding: 0 4px;
|
| }
|
|
|
| .gauge-scale span {
|
| font-family: 'Space Mono', monospace;
|
| font-size: 0.65rem;
|
| color: rgba(255, 255, 255, 0.25);
|
| letter-spacing: 0.05em;
|
| }
|
|
|
| .gauge-scale span:nth-child(3) {
|
| color: rgba(255, 255, 255, 0.4);
|
| font-weight: 700;
|
| }
|
|
|
|
|
| .net-position {
|
| display: flex;
|
| align-items: center;
|
| justify-content: center;
|
| gap: 12px;
|
| margin-top: 24px;
|
| padding-top: 20px;
|
| border-top: 1px solid rgba(255, 255, 255, 0.06);
|
| }
|
|
|
| .net-position-label {
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-semibold);
|
| letter-spacing: var(--tracking-wider);
|
| text-transform: uppercase;
|
| color: var(--text-dim);
|
| }
|
|
|
| .net-position-value {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-xl);
|
| font-weight: var(--weight-bold);
|
| letter-spacing: var(--tracking-wide);
|
| min-width: 80px;
|
| text-align: center;
|
| }
|
|
|
| .net-position-value.positive {
|
| color: #4fc3f7;
|
| text-shadow: 0 0 15px rgba(79, 195, 247, 0.3);
|
| }
|
|
|
| .net-position-value.negative {
|
| color: #ef5350;
|
| text-shadow: 0 0 15px rgba(239, 83, 80, 0.3);
|
| }
|
|
|
| .net-position-value.neutral {
|
| color: var(--text-secondary);
|
| }
|
|
|
|
|
| .grid-3 {
|
| display: grid;
|
| grid-template-columns: repeat(3, 1fr);
|
| gap: 28px;
|
| margin-bottom: 28px;
|
| }
|
|
|
|
|
| .metric-row {
|
| display: flex;
|
| justify-content: space-between;
|
| align-items: baseline;
|
| padding: 16px 0;
|
| border-bottom: 1px solid rgba(255,255,255,0.03);
|
| }
|
|
|
| .metric-row:last-child {
|
| border-bottom: none;
|
| padding-bottom: 0;
|
| }
|
|
|
| .metric-row:first-child {
|
| padding-top: 0;
|
| }
|
|
|
| .metric-label {
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-sm);
|
| font-weight: var(--weight-regular);
|
| color: var(--text-secondary);
|
| letter-spacing: var(--tracking-wide);
|
| }
|
|
|
| .metric-value {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-base);
|
| font-weight: var(--weight-semibold);
|
| color: var(--text-primary);
|
| letter-spacing: var(--tracking-normal);
|
| }
|
|
|
| .metric-value.positive { color: var(--success); }
|
| .metric-value.warning { color: var(--warning); }
|
| .metric-value.negative { color: var(--danger); }
|
|
|
|
|
| .big-number {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-2xl);
|
| font-weight: var(--weight-bold);
|
| text-align: center;
|
| color: var(--accent-cyan);
|
| text-shadow: 0 0 30px var(--accent-cyan-dim);
|
| margin: 20px 0;
|
| line-height: var(--leading-tight);
|
| letter-spacing: var(--tracking-wide);
|
| }
|
|
|
|
|
| .process-status {
|
| text-align: center;
|
| padding: 24px;
|
| margin-top: auto;
|
| border-radius: 16px;
|
| }
|
|
|
| .process-ok {
|
| background: rgba(0,255,136,0.08);
|
| border: 1px solid rgba(0,255,136,0.15);
|
| }
|
|
|
| .process-warning {
|
| background: rgba(255,68,102,0.08);
|
| border: 1px solid rgba(255,68,102,0.15);
|
| }
|
|
|
| .process-label {
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-medium);
|
| letter-spacing: var(--tracking-wider);
|
| text-transform: uppercase;
|
| color: var(--text-muted);
|
| margin-bottom: 10px;
|
| }
|
|
|
| .process-value {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-2xl);
|
| font-weight: var(--weight-bold);
|
| line-height: var(--leading-tight);
|
| }
|
|
|
| .process-ok .process-value { color: var(--success); }
|
| .process-warning .process-value { color: var(--danger); }
|
|
|
|
|
| .status-badge {
|
| display: inline-flex;
|
| align-items: center;
|
| gap: 8px;
|
| padding: 6px 14px;
|
| border-radius: 100px;
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-semibold);
|
| text-transform: uppercase;
|
| letter-spacing: var(--tracking-wider);
|
| }
|
|
|
| .status-active {
|
| background: rgba(0,255,136,0.1);
|
| color: var(--success);
|
| border: 1px solid rgba(0,255,136,0.2);
|
| }
|
| .status-unknown {
|
| background: rgba(255,170,0,0.1);
|
| color: #ffaa00;
|
| border: 1px solid rgba(255,170,0,0.3);
|
| }
|
|
|
| .status-inactive {
|
| background: rgba(255,68,102,0.1);
|
| color: var(--danger);
|
| border: 1px solid rgba(255,68,102,0.2);
|
| }
|
|
|
| .status-badge::before {
|
| content: '';
|
| width: 6px;
|
| height: 6px;
|
| border-radius: 50%;
|
| background: currentColor;
|
| }
|
|
|
|
|
| .progress-container {
|
| margin-top: 16px;
|
| }
|
|
|
| .progress-bar {
|
| width: 100%;
|
| height: 8px;
|
| background: rgba(255,255,255,0.06);
|
| border-radius: 4px;
|
| overflow: hidden;
|
| }
|
|
|
| .progress-fill {
|
| height: 100%;
|
| background: linear-gradient(90deg, var(--accent-cyan), var(--success));
|
| border-radius: 4px;
|
| transition: width 0.4s ease;
|
| }
|
|
|
|
|
| .log-container {
|
| background: rgba(0,0,0,0.25);
|
| border-radius: 16px;
|
| padding: 20px;
|
| height: 240px;
|
| overflow-y: auto;
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-xs);
|
| line-height: var(--leading-relaxed);
|
| border: 1px solid rgba(255,255,255,0.03);
|
| }
|
|
|
| .log-line {
|
| padding: 5px 0;
|
| word-break: break-all;
|
| color: var(--text-secondary);
|
| }
|
|
|
| .log-error { color: var(--danger); }
|
| .log-warning { color: var(--warning); }
|
| .log-info { color: var(--success); }
|
|
|
|
|
| .accuracy-header {
|
| display: flex;
|
| justify-content: space-between;
|
| align-items: flex-start;
|
| flex-wrap: wrap;
|
| gap: 20px;
|
| margin-bottom: 24px;
|
| }
|
|
|
| .accuracy-metrics {
|
| display: flex;
|
| gap: 32px;
|
| flex-wrap: wrap;
|
| }
|
|
|
| .accuracy-stat {
|
| text-align: center;
|
| }
|
|
|
| .accuracy-stat-label {
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-medium);
|
| letter-spacing: var(--tracking-wider);
|
| text-transform: uppercase;
|
| color: var(--text-muted);
|
| margin-bottom: 6px;
|
| }
|
|
|
| .accuracy-stat-value {
|
| font-family: 'Space Mono', monospace;
|
| font-size: var(--text-xl);
|
| font-weight: var(--weight-bold);
|
| }
|
|
|
| .accuracy-legend {
|
| display: flex;
|
| gap: 20px;
|
| align-items: center;
|
| }
|
|
|
| .legend-item {
|
| display: flex;
|
| align-items: center;
|
| gap: 8px;
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| color: var(--text-secondary);
|
| }
|
|
|
| .legend-dot {
|
| width: 8px;
|
| height: 8px;
|
| border-radius: 50%;
|
| }
|
|
|
| .legend-dot.current { background: var(--success); }
|
| .legend-dot.average { background: var(--accent-cyan); }
|
| .legend-dot.trend { background: var(--warning); }
|
|
|
| .accuracy-trend-badge {
|
| display: inline-flex;
|
| align-items: center;
|
| gap: 8px;
|
| padding: 8px 16px;
|
| border-radius: 100px;
|
| font-family: 'Outfit', sans-serif;
|
| font-size: var(--text-xs);
|
| font-weight: var(--weight-semibold);
|
| letter-spacing: var(--tracking-wider);
|
| text-transform: uppercase;
|
| }
|
|
|
| .accuracy-trend-badge.rising {
|
| background: rgba(0, 255, 136, 0.1);
|
| border: 1px solid rgba(0, 255, 136, 0.2);
|
| color: var(--success);
|
| }
|
|
|
| .accuracy-trend-badge.stable {
|
| background: rgba(0, 212, 255, 0.1);
|
| border: 1px solid rgba(0, 212, 255, 0.2);
|
| color: var(--accent-cyan);
|
| }
|
|
|
| .accuracy-trend-badge.declining {
|
| background: rgba(255, 68, 102, 0.1);
|
| border: 1px solid rgba(255, 68, 102, 0.2);
|
| color: var(--danger);
|
| }
|
|
|
| .trend-arrow {
|
| font-weight: var(--weight-bold);
|
| font-size: var(--text-sm);
|
| }
|
|
|
| .chart-container {
|
| height: 200px;
|
| width: 100%;
|
| margin-top: 16px;
|
| }
|
|
|
|
|
| .visitor-badge {
|
| position: fixed;
|
| bottom: 20px;
|
| left: 24px;
|
| background: rgba(0,20,30,0.9);
|
| border: 1px solid rgba(0,212,170,0.2);
|
| border-radius: 12px;
|
| padding: 12px 20px;
|
| display: flex;
|
| align-items: center;
|
| gap: 14px;
|
| z-index: 200;
|
| backdrop-filter: blur(12px);
|
| }
|
|
|
| .visitor-dot {
|
| width: 10px;
|
| height: 10px;
|
| background: #00d4aa;
|
| border-radius: 50%;
|
| animation: pulse 2s infinite;
|
| }
|
|
|
| .visitor-count {
|
| color: #00d4aa;
|
| font-size: var(--text-base);
|
| font-weight: var(--weight-bold);
|
| font-family: 'Space Mono', monospace;
|
| }
|
|
|
| .visitor-label {
|
| font-family: 'Outfit', sans-serif;
|
| color: rgba(255,255,255,0.5);
|
| font-size: var(--text-xs);
|
| text-transform: uppercase;
|
| letter-spacing: var(--tracking-wider);
|
| }
|
|
|
| .visitor-total {
|
| color: rgba(255,255,255,0.3);
|
| font-size: 0.7rem;
|
| display: block;
|
| font-family: 'Space Mono', monospace;
|
| }
|
|
|
|
|
| .connection-status {
|
| position: fixed;
|
| bottom: 20px;
|
| right: 24px;
|
| padding: 10px 20px;
|
| border-radius: 100px;
|
| font-size: var(--text-xs);
|
| font-family: 'Space Mono', monospace;
|
| backdrop-filter: blur(12px);
|
| z-index: 200;
|
| letter-spacing: var(--tracking-wide);
|
| }
|
|
|
| .connection-ok {
|
| background: rgba(0,255,136,0.1);
|
| border: 1px solid rgba(0,255,136,0.2);
|
| color: var(--success);
|
| }
|
|
|
| .connection-error {
|
| background: rgba(255,68,102,0.1);
|
| border: 1px solid rgba(255,68,102,0.2);
|
| color: var(--danger);
|
| }
|
|
|
|
|
| .loading-overlay {
|
| position: fixed;
|
| bottom: 90px;
|
| left: 24px;
|
| background: rgba(0,20,30,0.95);
|
| border: 1px solid rgba(0,212,255,0.2);
|
| border-radius: 12px;
|
| padding: 14px 24px;
|
| display: flex;
|
| align-items: center;
|
| gap: 14px;
|
| z-index: 300;
|
| backdrop-filter: blur(12px);
|
| font-size: var(--text-sm);
|
| }
|
|
|
| .loading-spinner {
|
| width: 18px;
|
| height: 18px;
|
| border: 2px solid rgba(0,212,255,0.2);
|
| border-top-color: var(--accent-cyan);
|
| border-radius: 50%;
|
| animation: spin 0.8s linear infinite;
|
| }
|
|
|
| .hidden {
|
| display: none !important;
|
| }
|
|
|
|
|
| .footer {
|
| text-align: center;
|
| padding: 50px 24px 24px;
|
| color: var(--text-dim);
|
| font-size: var(--text-xs);
|
| letter-spacing: var(--tracking-wider);
|
| }
|
|
|
| .footer p {
|
| margin-bottom: 6px;
|
| }
|
|
|
|
|
| @keyframes pulse {
|
| 0%, 100% { opacity: 1; transform: scale(1); }
|
| 50% { opacity: 0.5; transform: scale(0.8); }
|
| }
|
|
|
| @keyframes pulse-ring {
|
| 0% { transform: scale(1); opacity: 1; }
|
| 100% { transform: scale(1.5); opacity: 0; }
|
| }
|
|
|
| @keyframes spin {
|
| to { transform: rotate(360deg); }
|
| }
|
|
|
| @keyframes fadeInUp {
|
| from {
|
| opacity: 0;
|
| transform: translateY(20px);
|
| }
|
| to {
|
| opacity: 1;
|
| transform: translateY(0);
|
| }
|
| }
|
|
|
| .glass-card {
|
| animation: fadeInUp 0.5s ease forwards;
|
| }
|
|
|
| .grid-3 .glass-card:nth-child(1) { animation-delay: 0.1s; }
|
| .grid-3 .glass-card:nth-child(2) { animation-delay: 0.15s; }
|
| .grid-3 .glass-card:nth-child(3) { animation-delay: 0.2s; }
|
|
|
|
|
| @media (max-width: 1024px) {
|
| .grid-3 {
|
| grid-template-columns: repeat(2, 1fr);
|
| gap: 24px;
|
| }
|
| }
|
|
|
|
|
| @media (max-width: 768px) {
|
| :root {
|
| font-size: 14px;
|
| }
|
|
|
| .main-content {
|
| padding: 80px 16px 20px;
|
| }
|
|
|
| .nav-bar {
|
| padding: 14px 16px;
|
| }
|
|
|
| .nav-links {
|
| display: none;
|
| }
|
|
|
| .nav-logo {
|
| font-size: var(--text-base);
|
| letter-spacing: var(--tracking-wider);
|
| }
|
|
|
| .live-indicator {
|
| top: 14px;
|
| right: 16px;
|
| padding: 8px 14px;
|
| }
|
|
|
| .logo-icon {
|
| width: 40px;
|
| height: 40px;
|
| }
|
|
|
| .logo-icon::after {
|
| inset: 10px;
|
| }
|
|
|
| .logo-text {
|
| font-size: 3rem;
|
| letter-spacing: 0.25em;
|
| }
|
|
|
| .subtitle {
|
| font-size: 0.7rem;
|
| letter-spacing: 0.2em;
|
| }
|
|
|
| .grid-3 {
|
| grid-template-columns: 1fr;
|
| gap: 16px;
|
| }
|
|
|
| .glass-card {
|
| padding: 20px;
|
| border-radius: 20px;
|
| }
|
|
|
| .signal-panel {
|
| padding: 24px 16px;
|
| }
|
|
|
| .signal-value {
|
| font-size: var(--text-2xl);
|
| }
|
|
|
| .signal-stats {
|
| font-size: var(--text-xs);
|
| gap: 12px;
|
| }
|
|
|
|
|
| .force-balance {
|
| padding: 0 10px;
|
| }
|
|
|
| .pressure-labels {
|
| gap: 20px;
|
| }
|
|
|
| .pressure-value {
|
| font-size: var(--text-lg);
|
| }
|
|
|
| .gauge-track {
|
| height: 28px;
|
| }
|
|
|
| .gauge-marker {
|
| width: 16px;
|
| height: 16px;
|
| }
|
|
|
| .net-position-value {
|
| font-size: var(--text-lg);
|
| }
|
|
|
| .accuracy-header {
|
| flex-direction: column;
|
| align-items: flex-start;
|
| }
|
|
|
| .accuracy-metrics {
|
| gap: 24px;
|
| }
|
|
|
| .accuracy-stat-value {
|
| font-size: var(--text-lg);
|
| }
|
|
|
| .big-number {
|
| font-size: var(--text-xl);
|
| }
|
|
|
| .visitor-badge {
|
| left: 16px;
|
| bottom: 16px;
|
| padding: 10px 16px;
|
| }
|
|
|
| .connection-status {
|
| right: 16px;
|
| bottom: 16px;
|
| padding: 8px 16px;
|
| }
|
| }
|
|
|
|
|
| @media (max-width: 480px) {
|
| :root {
|
| font-size: 12px;
|
| }
|
|
|
| .logo-icon {
|
| width: 32px;
|
| height: 32px;
|
| }
|
|
|
| .logo-icon::after {
|
| inset: 8px;
|
| }
|
|
|
| .logo-text {
|
| font-size: 2.2rem;
|
| letter-spacing: 0.15em;
|
| }
|
|
|
| .subtitle {
|
| font-size: 0.6rem;
|
| letter-spacing: 0.15em;
|
| }
|
|
|
| .force-balance {
|
| margin-top: 20px;
|
| padding: 0 5px;
|
| }
|
|
|
| .pressure-labels {
|
| gap: 10px;
|
| }
|
|
|
| .pressure-label {
|
| font-size: 0.65rem;
|
| }
|
|
|
| .pressure-value {
|
| font-size: var(--text-md);
|
| }
|
|
|
| .gauge-track {
|
| height: 24px;
|
| }
|
|
|
| .gauge-scale span {
|
| font-size: 0.55rem;
|
| }
|
|
|
| .net-position {
|
| flex-direction: column;
|
| gap: 8px;
|
| }
|
|
|
| .accuracy-metrics {
|
| gap: 16px;
|
| }
|
|
|
| .accuracy-legend {
|
| flex-wrap: wrap;
|
| gap: 12px;
|
| }
|
| }
|
|
|
|
|
| @media (max-width: 360px) {
|
| :root {
|
| font-size: 11px;
|
| }
|
| }
|
|
|
|
|
| @media (hover: hover) and (pointer: fine) {
|
| .glass-card:hover {
|
| transform: translateY(-2px);
|
| box-shadow: 0 24px 48px rgba(0, 212, 255, 0.12);
|
| }
|
|
|
| .nav-links a:hover {
|
| color: var(--accent-cyan);
|
| }
|
| }
|
|
|
|
|
| @media (hover: none) {
|
| .glass-card:hover {
|
| transform: none;
|
| box-shadow: none;
|
| }
|
| }
|
| </style>
|
| </head>
|
| <body>
|
|
|
| <div id="canvas-container">
|
| <canvas id="three-canvas"></canvas>
|
| </div>
|
| <div class="gradient-overlay"></div>
|
|
|
|
|
| <div class="loading-overlay" id="loadingOverlay">
|
| <div class="loading-spinner"></div>
|
| <span id="loadingText">Initializing Observatory...</span>
|
| </div>
|
|
|
|
|
| <div class="connection-status connection-ok" id="connectionStatus">● Connected</div>
|
|
|
|
|
| <div class="visitor-badge">
|
| <div class="visitor-dot"></div>
|
| <div>
|
| <span class="visitor-count" id="activeVisitors">—</span>
|
| <span class="visitor-label">Online</span>
|
| <span class="visitor-total" id="totalVisitors">— total</span>
|
| </div>
|
| </div>
|
|
|
|
|
| <nav class="nav-bar">
|
| <div class="nav-logo">K1RL <span>QUASAR</span></div>
|
| <ul class="nav-links">
|
| <li><a href="#dashboard">Dashboard</a></li>
|
| <li><a href="#metrics">Metrics</a></li>
|
| <li><a href="#system">System</a></li>
|
| </ul>
|
| </nav>
|
|
|
|
|
| <div class="live-indicator">
|
| <div class="live-dot"></div>
|
| <span class="live-text">LIVE</span>
|
| </div>
|
|
|
|
|
| <div class="main-content">
|
|
|
| <header class="header" id="dashboard">
|
| <div class="logo-container">
|
| <div class="logo-icon"></div>
|
| <h1 class="logo-text">QU<span>A</span>SAR</h1>
|
| </div>
|
| <p class="subtitle">Quantitative Intelligence Observatory</p>
|
| <p class="timestamp" id="lastUpdate">Last Observation: Initializing...</p>
|
| <div class="inst-bar" id="instBar">VOLATILITY 75 INDEX (1s) │ DERIV</div>
|
| </header>
|
|
|
|
|
| <div class="glass-card signal-panel">
|
| <div class="card-title">Market Directive — 10M Agent — Last 5 Minutes</div>
|
| <div class="signal-label">Dominant Signal</div>
|
| <div class="signal-value signal-neutral" id="dominantSignal">NEUTRAL</div>
|
| <div class="signal-stats" id="signalStats">
|
| <span>BUY: 0 (0%)</span> • <span>SELL: 0 (0%)</span> • <span>TOTAL: 0</span>
|
| </div>
|
|
|
|
|
| <div class="force-balance">
|
|
|
| <div class="pressure-labels">
|
| <div class="pressure-item buy">
|
| <span class="pressure-label">Buy Pressure</span>
|
| <span class="pressure-value buy-value" id="forceBuyValue">0.0%</span>
|
| </div>
|
| <div class="pressure-item sell">
|
| <span class="pressure-label">Sell Pressure</span>
|
| <span class="pressure-value sell-value" id="forceSellValue">0.0%</span>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="pressure-gauge">
|
| <div class="gauge-track">
|
| <div class="gauge-fill-buy" id="gaugeFillBuy"></div>
|
| <div class="gauge-fill-sell" id="gaugeFillSell"></div>
|
| <div class="gauge-center"></div>
|
| <div class="gauge-marker" id="gaugeMarker"></div>
|
| </div>
|
| <div class="gauge-scale">
|
| <span>100</span>
|
| <span>50</span>
|
| <span>0</span>
|
| <span>50</span>
|
| <span>100</span>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="net-position">
|
| <span class="net-position-label">Net Position</span>
|
| <span class="net-position-value neutral" id="netPositionValue">0.0</span>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="grid-3" id="metrics">
|
|
|
| <div class="glass-card">
|
| <div class="card-title">Training Progression</div>
|
| <div class="card-content">
|
| <div class="metric-row">
|
| <span class="metric-label">Iterations Completed</span>
|
| <span class="metric-value" id="trainingSteps" style="color: #00ff88;">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Actor Loss</span>
|
| <span class="metric-value" id="actorLoss" style="color: #00ff88;">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Critic Loss</span>
|
| <span class="metric-value" id="criticLoss">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">AVN Loss</span>
|
| <span class="metric-value" id="avnLoss">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">AVN Accuracy</span>
|
| <span class="metric-value" id="avnAccuracy" style="color: #00d4aa;">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Buffer Capacity</span>
|
| <span class="metric-value" id="bufferSize">—</span>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card">
|
| <div class="card-title">Reward Mechanisms</div>
|
| <div class="card-content">
|
| <div class="metric-row">
|
| <span class="metric-label">Matched Signals</span>
|
| <span class="metric-value positive" id="matchedRewards">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Unmatched Signals</span>
|
| <span class="metric-value warning" id="unmatchedRewards">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Duplicate Entries</span>
|
| <span class="metric-value" id="duplicates">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Match Efficacy</span>
|
| <span class="metric-value" id="matchRate">—</span>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card" id="system">
|
| <div class="card-title">Service Registry</div>
|
| <div class="card-content">
|
| <div class="metric-row">
|
| <span class="metric-label">Quasar Engine</span>
|
| <span class="status-badge status-active" id="statusQuasar">Active</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Feature Pipeline</span>
|
| <span class="status-badge status-active" id="statusFeatures">Active</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Reward System</span>
|
| <span class="status-badge status-active" id="statusRewards">Active</span>
|
| </div>
|
| <div class="process-status process-ok" id="processCount">
|
| <div class="process-label">Quasar Processes</div>
|
| <div class="process-value" id="processCountValue">1</div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="grid-3">
|
|
|
| <div class="glass-card">
|
| <div class="card-title">Computational Load</div>
|
| <div class="card-content">
|
| <div class="big-number" id="cpuPercent">—%</div>
|
| <div class="progress-container">
|
| <div class="progress-bar">
|
| <div class="progress-fill" id="cpuBar" style="width:0%"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card">
|
| <div class="card-title">Memory Allocation</div>
|
| <div class="card-content">
|
| <div class="big-number" id="memoryPercent">—%</div>
|
| <div class="metric-row">
|
| <span class="metric-label">Used</span>
|
| <span class="metric-value" id="memoryUsed">—/— GB</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Quasar</span>
|
| <span class="metric-value positive" id="quasarMemory">— GB</span>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card">
|
| <div class="card-title">Latest Checkpoint</div>
|
| <div class="card-content">
|
| <div class="metric-row">
|
| <span class="metric-label">Filename</span>
|
| <span class="metric-value" id="checkpointFile" style="font-size:0.7rem;">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Training Step</span>
|
| <span class="metric-value" id="checkpointStep">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Size</span>
|
| <span class="metric-value" id="checkpointSize">—</span>
|
| </div>
|
| <div class="metric-row">
|
| <span class="metric-label">Modified</span>
|
| <span class="metric-value" id="checkpointModified" style="font-size:0.75rem;">—</span>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card">
|
| <div class="card-title">Signal Precision Analytics</div>
|
| <div class="accuracy-header">
|
| <div class="accuracy-metrics">
|
| <div class="accuracy-stat">
|
| <div class="accuracy-stat-label">Current</div>
|
| <div class="accuracy-stat-value" id="accuracy-current" style="color: var(--success);">—</div>
|
| </div>
|
| <div class="accuracy-stat">
|
| <div class="accuracy-stat-label">Average</div>
|
| <div class="accuracy-stat-value" id="accuracy-avg" style="color: var(--accent-cyan);">—</div>
|
| </div>
|
| <div class="accuracy-stat">
|
| <div class="accuracy-stat-label">Last 10</div>
|
| <div class="accuracy-stat-value" id="accuracy-last10" style="color: var(--warning);">—</div>
|
| </div>
|
| </div>
|
| <div class="accuracy-trend-badge stable" id="accuracy-trend">
|
| <span class="trend-arrow">―</span>
|
| <span>STABLE</span>
|
| </div>
|
| </div>
|
| <div class="chart-container">
|
| <canvas id="accuracyChart"></canvas>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card">
|
| <div class="card-title">Quasar Intelligence Stream</div>
|
| <div class="log-container" id="logContainer">
|
| <div class="log-line">Awaiting signal intelligence...</div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div style="position:fixed;bottom:20px;right:20px;z-index:101;padding:8px 16px;background:rgba(255,136,0,0.1);border:1px solid rgba(255,136,0,0.3);border-radius:20px;backdrop-filter:blur(8px);font-size:0.75rem;font-weight:500;color:#ffaa00;letter-spacing:0.05em;">🤗 HuggingFace Spaces</div>
|
|
|
|
|
| <footer class="footer">
|
| <p>K1RL QUASAR Observatory v9.0 — HF Spaces Edition</p>
|
| <p>Reinforcement Learning • Quantitative Intelligence • V75 Index • Redis-Backed</p>
|
| </footer>
|
| </div>
|
|
|
| <script>
|
|
|
| (function() {
|
| const canvas = document.getElementById('three-canvas');
|
| const isMobile = window.innerWidth <= 768;
|
|
|
| const renderer = new THREE.WebGLRenderer({
|
| canvas,
|
| antialias: !isMobile,
|
| alpha: true,
|
| powerPreference: 'high-performance'
|
| });
|
| renderer.setSize(window.innerWidth, window.innerHeight);
|
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2));
|
| renderer.setClearColor(0x000810, 1);
|
|
|
| const scene = new THREE.Scene();
|
| scene.fog = new THREE.FogExp2(0x000810, 0.0006);
|
|
|
| const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 3000);
|
| camera.position.set(0, 50, 500);
|
| camera.lookAt(0, -50, 0);
|
|
|
| const textureLoader = new THREE.TextureLoader();
|
|
|
|
|
| const starCount = isMobile ? 800 : 2000;
|
| const starGeo = new THREE.BufferGeometry();
|
| const starPos = new Float32Array(starCount * 3);
|
| for (let i = 0; i < starCount * 3; i += 3) {
|
| starPos[i] = (Math.random() - 0.5) * 2000;
|
| starPos[i+1] = (Math.random() - 0.5) * 2000;
|
| starPos[i+2] = (Math.random() - 0.5) * 2000;
|
| }
|
| starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
|
| const stars = new THREE.Points(starGeo, new THREE.PointsMaterial({
|
| color: 0xffffff,
|
| size: 1.2,
|
| transparent: true,
|
| opacity: 0.8,
|
| sizeAttenuation: true
|
| }));
|
| scene.add(stars);
|
|
|
|
|
| const nodeCount = isMobile ? 60 : 100;
|
| const nodes = [];
|
|
|
|
|
| const PHYSICS = {
|
|
|
| centralGravity: 0.00008,
|
| repulsion: 800,
|
| repulsionDistance: 120,
|
| springStrength: 0.0003,
|
| springRestLength: 100,
|
|
|
|
|
| minRadius: 200,
|
| maxRadius: 550,
|
|
|
|
|
| damping: 0.985,
|
| maxVelocity: 2.5,
|
|
|
|
|
| mouseRadius: 200,
|
| mouseForce: 0.15,
|
| mouseAttract: false,
|
|
|
|
|
| connectionDistance: 140,
|
| maxConnections: 6
|
| };
|
|
|
|
|
| const mouse = { x: 0, y: 0, z: 0, active: false };
|
| const mouseVector = new THREE.Vector3();
|
| const raycaster = new THREE.Raycaster();
|
| const mouse2D = new THREE.Vector2();
|
|
|
|
|
| const nodeGeometry = new THREE.BufferGeometry();
|
| const nodePositions = new Float32Array(nodeCount * 3);
|
| const nodeSizes = new Float32Array(nodeCount);
|
| const nodeColors = new Float32Array(nodeCount * 3);
|
|
|
| for (let i = 0; i < nodeCount; i++) {
|
| const i3 = i * 3;
|
| const theta = Math.random() * Math.PI * 2;
|
| const phi = Math.acos(2 * Math.random() - 1);
|
| const radius = PHYSICS.minRadius + Math.random() * (PHYSICS.maxRadius - PHYSICS.minRadius);
|
|
|
| const x = radius * Math.sin(phi) * Math.cos(theta);
|
| const y = radius * Math.sin(phi) * Math.sin(theta);
|
| const z = radius * Math.cos(phi);
|
|
|
| nodePositions[i3] = x;
|
| nodePositions[i3 + 1] = y;
|
| nodePositions[i3 + 2] = z;
|
|
|
|
|
| const mass = 0.5 + Math.random() * 1.5;
|
| nodeSizes[i] = isMobile ? mass * 3 : mass * 4;
|
|
|
|
|
| const colorFactor = Math.random();
|
| nodeColors[i3] = 0.0 + colorFactor * 0.2;
|
| nodeColors[i3 + 1] = 0.7 + colorFactor * 0.3;
|
| nodeColors[i3 + 2] = 1.0;
|
|
|
| nodes.push({
|
| x, y, z,
|
| vx: (Math.random() - 0.5) * 0.5,
|
| vy: (Math.random() - 0.5) * 0.5,
|
| vz: (Math.random() - 0.5) * 0.5,
|
| mass: mass,
|
| connections: [],
|
| energy: 0.5 + Math.random() * 0.5
|
| });
|
| }
|
|
|
| nodeGeometry.setAttribute('position', new THREE.BufferAttribute(nodePositions, 3));
|
| nodeGeometry.setAttribute('size', new THREE.BufferAttribute(nodeSizes, 1));
|
| nodeGeometry.setAttribute('color', new THREE.BufferAttribute(nodeColors, 3));
|
|
|
|
|
| const nodeShaderMaterial = new THREE.ShaderMaterial({
|
| uniforms: {
|
| time: { value: 0 },
|
| pixelRatio: { value: renderer.getPixelRatio() }
|
| },
|
| vertexShader: `
|
| attribute float size;
|
| attribute vec3 color;
|
| varying vec3 vColor;
|
| varying float vSize;
|
| uniform float time;
|
| uniform float pixelRatio;
|
|
|
| void main() {
|
| vColor = color;
|
| vSize = size;
|
|
|
| vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
|
| // Subtle pulse based on position
|
| float pulse = 1.0 + 0.15 * sin(time * 2.0 + position.x * 0.01 + position.y * 0.01);
|
|
|
| gl_PointSize = size * pulse * pixelRatio * (300.0 / -mvPosition.z);
|
| gl_Position = projectionMatrix * mvPosition;
|
| }
|
| `,
|
| fragmentShader: `
|
| varying vec3 vColor;
|
| varying float vSize;
|
|
|
| void main() {
|
| vec2 center = gl_PointCoord - vec2(0.5);
|
| float dist = length(center);
|
|
|
| if (dist > 0.5) discard;
|
|
|
| // Soft glow falloff
|
| float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
|
| float glow = exp(-dist * 3.0);
|
|
|
| vec3 finalColor = vColor + vec3(0.3, 0.5, 0.8) * glow * 0.5;
|
| gl_FragColor = vec4(finalColor, alpha * 0.9);
|
| }
|
| `,
|
| transparent: true,
|
| depthWrite: false,
|
| blending: THREE.AdditiveBlending
|
| });
|
|
|
| const nodePoints = new THREE.Points(nodeGeometry, nodeShaderMaterial);
|
| scene.add(nodePoints);
|
|
|
|
|
| const maxLines = nodeCount * PHYSICS.maxConnections;
|
| const linePositions = new Float32Array(maxLines * 6);
|
| const lineColors = new Float32Array(maxLines * 6);
|
|
|
| const linesGeometry = new THREE.BufferGeometry();
|
| linesGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3));
|
| linesGeometry.setAttribute('color', new THREE.BufferAttribute(lineColors, 3));
|
|
|
| const linesMaterial = new THREE.LineBasicMaterial({
|
| vertexColors: true,
|
| transparent: true,
|
| opacity: 0.6,
|
| blending: THREE.AdditiveBlending
|
| });
|
|
|
| const lines = new THREE.LineSegments(linesGeometry, linesMaterial);
|
| scene.add(lines);
|
|
|
|
|
| function buildSpatialHash() {
|
| const cellSize = PHYSICS.repulsionDistance;
|
| const hash = {};
|
|
|
| for (let i = 0; i < nodeCount; i++) {
|
| const node = nodes[i];
|
| const cellX = Math.floor(node.x / cellSize);
|
| const cellY = Math.floor(node.y / cellSize);
|
| const cellZ = Math.floor(node.z / cellSize);
|
| const key = `${cellX},${cellY},${cellZ}`;
|
|
|
| if (!hash[key]) hash[key] = [];
|
| hash[key].push(i);
|
| }
|
|
|
| return hash;
|
| }
|
|
|
|
|
| function getNeighborCells(x, y, z, cellSize) {
|
| const cellX = Math.floor(x / cellSize);
|
| const cellY = Math.floor(y / cellSize);
|
| const cellZ = Math.floor(z / cellSize);
|
| const neighbors = [];
|
|
|
| for (let dx = -1; dx <= 1; dx++) {
|
| for (let dy = -1; dy <= 1; dy++) {
|
| for (let dz = -1; dz <= 1; dz++) {
|
| neighbors.push(`${cellX + dx},${cellY + dy},${cellZ + dz}`);
|
| }
|
| }
|
| }
|
|
|
| return neighbors;
|
| }
|
|
|
|
|
| function simulatePhysics(deltaTime) {
|
| const dt = Math.min(deltaTime, 0.033);
|
| const spatialHash = buildSpatialHash();
|
|
|
|
|
| for (let i = 0; i < nodeCount; i++) {
|
| nodes[i].connections = [];
|
| }
|
|
|
|
|
| for (let i = 0; i < nodeCount; i++) {
|
| const node = nodes[i];
|
| let fx = 0, fy = 0, fz = 0;
|
|
|
|
|
| const distFromCenter = Math.sqrt(node.x * node.x + node.y * node.y + node.z * node.z);
|
| if (distFromCenter > 0.01) {
|
| const gravityStrength = PHYSICS.centralGravity * node.mass;
|
| fx -= (node.x / distFromCenter) * gravityStrength * distFromCenter;
|
| fy -= (node.y / distFromCenter) * gravityStrength * distFromCenter;
|
| fz -= (node.z / distFromCenter) * gravityStrength * distFromCenter;
|
| }
|
|
|
|
|
| if (distFromCenter > PHYSICS.maxRadius) {
|
| const overflow = distFromCenter - PHYSICS.maxRadius;
|
| const boundaryForce = overflow * 0.01;
|
| fx -= (node.x / distFromCenter) * boundaryForce;
|
| fy -= (node.y / distFromCenter) * boundaryForce;
|
| fz -= (node.z / distFromCenter) * boundaryForce;
|
| } else if (distFromCenter < PHYSICS.minRadius) {
|
| const underflow = PHYSICS.minRadius - distFromCenter;
|
| const boundaryForce = underflow * 0.01;
|
| fx += (node.x / distFromCenter) * boundaryForce;
|
| fy += (node.y / distFromCenter) * boundaryForce;
|
| fz += (node.z / distFromCenter) * boundaryForce;
|
| }
|
|
|
|
|
| const neighborCells = getNeighborCells(node.x, node.y, node.z, PHYSICS.repulsionDistance);
|
|
|
| for (const cellKey of neighborCells) {
|
| const cellNodes = spatialHash[cellKey];
|
| if (!cellNodes) continue;
|
|
|
| for (const j of cellNodes) {
|
| if (i === j) continue;
|
|
|
| const other = nodes[j];
|
| const dx = node.x - other.x;
|
| const dy = node.y - other.y;
|
| const dz = node.z - other.z;
|
| const distSq = dx * dx + dy * dy + dz * dz;
|
| const dist = Math.sqrt(distSq);
|
|
|
| if (dist < PHYSICS.repulsionDistance && dist > 0.1) {
|
|
|
| const force = PHYSICS.repulsion / (distSq + 100);
|
| fx += (dx / dist) * force / node.mass;
|
| fy += (dy / dist) * force / node.mass;
|
| fz += (dz / dist) * force / node.mass;
|
|
|
|
|
| if (dist < PHYSICS.connectionDistance && node.connections.length < PHYSICS.maxConnections) {
|
| if (!node.connections.includes(j)) {
|
| node.connections.push(j);
|
| }
|
| }
|
| }
|
| }
|
| }
|
|
|
|
|
| for (const j of node.connections) {
|
| const other = nodes[j];
|
| const dx = other.x - node.x;
|
| const dy = other.y - node.y;
|
| const dz = other.z - node.z;
|
| const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
| if (dist > 0.1) {
|
| const displacement = dist - PHYSICS.springRestLength;
|
| const springForce = displacement * PHYSICS.springStrength;
|
| fx += (dx / dist) * springForce;
|
| fy += (dy / dist) * springForce;
|
| fz += (dz / dist) * springForce;
|
| }
|
| }
|
|
|
|
|
| if (mouse.active) {
|
| const mdx = node.x - mouse.x;
|
| const mdy = node.y - mouse.y;
|
| const mdz = node.z - mouse.z;
|
| const mouseDist = Math.sqrt(mdx * mdx + mdy * mdy + mdz * mdz);
|
|
|
| if (mouseDist < PHYSICS.mouseRadius && mouseDist > 1) {
|
| const mouseStrength = (1 - mouseDist / PHYSICS.mouseRadius) * PHYSICS.mouseForce;
|
| const direction = PHYSICS.mouseAttract ? -1 : 1;
|
| fx += direction * (mdx / mouseDist) * mouseStrength;
|
| fy += direction * (mdy / mouseDist) * mouseStrength;
|
| fz += direction * (mdz / mouseDist) * mouseStrength;
|
| }
|
| }
|
|
|
|
|
| node.vx += fx * dt;
|
| node.vy += fy * dt;
|
| node.vz += fz * dt;
|
|
|
|
|
| node.vx *= PHYSICS.damping;
|
| node.vy *= PHYSICS.damping;
|
| node.vz *= PHYSICS.damping;
|
|
|
|
|
| const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy + node.vz * node.vz);
|
| if (speed > PHYSICS.maxVelocity) {
|
| const scale = PHYSICS.maxVelocity / speed;
|
| node.vx *= scale;
|
| node.vy *= scale;
|
| node.vz *= scale;
|
| }
|
| }
|
|
|
|
|
| const positions = nodeGeometry.attributes.position.array;
|
| const sizes = nodeGeometry.attributes.size.array;
|
|
|
| for (let i = 0; i < nodeCount; i++) {
|
| const node = nodes[i];
|
| const i3 = i * 3;
|
|
|
| node.x += node.vx;
|
| node.y += node.vy;
|
| node.z += node.vz;
|
|
|
| positions[i3] = node.x;
|
| positions[i3 + 1] = node.y;
|
| positions[i3 + 2] = node.z;
|
|
|
|
|
| const connectionBonus = node.connections.length * 0.3;
|
| sizes[i] = (isMobile ? 3 : 4) * node.mass + connectionBonus;
|
| }
|
|
|
| nodeGeometry.attributes.position.needsUpdate = true;
|
| nodeGeometry.attributes.size.needsUpdate = true;
|
| }
|
|
|
|
|
| function updateConnections() {
|
| const linePos = linesGeometry.attributes.position.array;
|
| const lineCol = linesGeometry.attributes.color.array;
|
| let lineIndex = 0;
|
|
|
| for (let i = 0; i < nodeCount; i++) {
|
| const node = nodes[i];
|
|
|
| for (const j of node.connections) {
|
| if (j > i) continue;
|
|
|
| const other = nodes[j];
|
| const dx = other.x - node.x;
|
| const dy = other.y - node.y;
|
| const dz = other.z - node.z;
|
| const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
|
| const idx = lineIndex * 6;
|
| linePos[idx] = node.x;
|
| linePos[idx + 1] = node.y;
|
| linePos[idx + 2] = node.z;
|
| linePos[idx + 3] = other.x;
|
| linePos[idx + 4] = other.y;
|
| linePos[idx + 5] = other.z;
|
|
|
|
|
| const intensity = 1 - (dist / PHYSICS.connectionDistance);
|
| const r = 0.0 * intensity;
|
| const g = 0.83 * intensity;
|
| const b = 1.0 * intensity;
|
|
|
| lineCol[idx] = r;
|
| lineCol[idx + 1] = g;
|
| lineCol[idx + 2] = b;
|
| lineCol[idx + 3] = r;
|
| lineCol[idx + 4] = g;
|
| lineCol[idx + 5] = b;
|
|
|
| lineIndex++;
|
| }
|
| }
|
|
|
|
|
| for (let i = lineIndex * 6; i < linePos.length; i++) {
|
| linePos[i] = 0;
|
| lineCol[i] = 0;
|
| }
|
|
|
| linesGeometry.attributes.position.needsUpdate = true;
|
| linesGeometry.attributes.color.needsUpdate = true;
|
| linesGeometry.setDrawRange(0, lineIndex * 2);
|
| }
|
|
|
|
|
| function onMouseMove(event) {
|
| mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
|
| mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
|
| raycaster.setFromCamera(mouse2D, camera);
|
|
|
|
|
| const planeZ = -100;
|
| const planeNormal = new THREE.Vector3(0, 0, 1);
|
| const planePoint = new THREE.Vector3(0, 0, planeZ);
|
| const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(planeNormal, planePoint);
|
|
|
| const intersectPoint = new THREE.Vector3();
|
| raycaster.ray.intersectPlane(plane, intersectPoint);
|
|
|
| if (intersectPoint) {
|
| mouse.x = intersectPoint.x;
|
| mouse.y = intersectPoint.y;
|
| mouse.z = intersectPoint.z;
|
| mouse.active = true;
|
| }
|
| }
|
|
|
| function onMouseLeave() {
|
| mouse.active = false;
|
| }
|
|
|
|
|
| function onTouchMove(event) {
|
| if (event.touches.length > 0) {
|
| const touch = event.touches[0];
|
| onMouseMove({ clientX: touch.clientX, clientY: touch.clientY });
|
| }
|
| }
|
|
|
| function onTouchEnd() {
|
| mouse.active = false;
|
| }
|
|
|
|
|
| document.addEventListener('mousemove', onMouseMove);
|
| document.addEventListener('mouseleave', onMouseLeave);
|
| document.addEventListener('touchmove', onTouchMove, { passive: true });
|
| document.addEventListener('touchend', onTouchEnd);
|
|
|
|
|
| const earthRadius = isMobile ? 160 : 180;
|
| const earthGroup = new THREE.Group();
|
| earthGroup.position.set(0, isMobile ? -350 : -380, -100);
|
| scene.add(earthGroup);
|
|
|
|
|
| const textureURLs = {
|
| dayMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-day.jpg',
|
| nightMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-night.jpg',
|
| bumpMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-topology.png',
|
| cloudsMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-clouds.png'
|
| };
|
|
|
|
|
| const earthDayTexture = textureLoader.load(textureURLs.dayMap);
|
| const earthNightTexture = textureLoader.load(textureURLs.nightMap);
|
| const earthBumpTexture = textureLoader.load(textureURLs.bumpMap);
|
| const earthCloudsTexture = textureLoader.load(textureURLs.cloudsMap);
|
|
|
|
|
| const earthGeometry = new THREE.SphereGeometry(earthRadius, isMobile ? 48 : 64, isMobile ? 48 : 64);
|
|
|
|
|
| const earthMaterial = new THREE.ShaderMaterial({
|
| uniforms: {
|
| dayTexture: { value: earthDayTexture },
|
| nightTexture: { value: earthNightTexture },
|
| sunDirection: { value: new THREE.Vector3(1, 0.5, 1).normalize() }
|
| },
|
| vertexShader: `
|
| varying vec2 vUv;
|
| varying vec3 vNormal;
|
| varying vec3 vPosition;
|
|
|
| void main() {
|
| vUv = uv;
|
| vNormal = normalize(normalMatrix * normal);
|
| vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
| }
|
| `,
|
| fragmentShader: `
|
| uniform sampler2D dayTexture;
|
| uniform sampler2D nightTexture;
|
| uniform vec3 sunDirection;
|
|
|
| varying vec2 vUv;
|
| varying vec3 vNormal;
|
| varying vec3 vPosition;
|
|
|
| void main() {
|
| vec3 dayColor = texture2D(dayTexture, vUv).rgb;
|
| vec3 nightColor = texture2D(nightTexture, vUv).rgb;
|
|
|
| float sunIntensity = dot(vNormal, sunDirection);
|
| float dayFactor = smoothstep(-0.2, 0.4, sunIntensity);
|
|
|
| nightColor *= 1.5;
|
| vec3 color = mix(nightColor, dayColor, dayFactor);
|
| color += vec3(0.02, 0.03, 0.05);
|
|
|
| float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0))), 3.0);
|
| vec3 atmosphereColor = vec3(0.3, 0.6, 1.0);
|
| color = mix(color, atmosphereColor, fresnel * 0.3);
|
|
|
| gl_FragColor = vec4(color, 1.0);
|
| }
|
| `
|
| });
|
|
|
| const earth = new THREE.Mesh(earthGeometry, earthMaterial);
|
| earthGroup.add(earth);
|
|
|
|
|
| const cloudGeometry = new THREE.SphereGeometry(earthRadius * 1.02, isMobile ? 48 : 64, isMobile ? 48 : 64);
|
| const cloudMaterial = new THREE.MeshPhongMaterial({
|
| map: earthCloudsTexture,
|
| transparent: true,
|
| opacity: 0.4,
|
| depthWrite: false,
|
| side: THREE.DoubleSide
|
| });
|
|
|
| const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial);
|
| earthGroup.add(clouds);
|
|
|
|
|
| const atmosphereGeometry = new THREE.SphereGeometry(earthRadius * 1.15, isMobile ? 48 : 64, isMobile ? 48 : 64);
|
| const atmosphereMaterial = new THREE.ShaderMaterial({
|
| uniforms: {
|
| glowColor: { value: new THREE.Color(0x3388ff) },
|
| viewVector: { value: camera.position }
|
| },
|
| vertexShader: `
|
| uniform vec3 viewVector;
|
| varying float intensity;
|
|
|
| void main() {
|
| vec3 vNormal = normalize(normalMatrix * normal);
|
| vec3 vNormel = normalize(normalMatrix * viewVector);
|
| intensity = pow(0.7 - dot(vNormal, vNormel), 2.0);
|
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
| }
|
| `,
|
| fragmentShader: `
|
| uniform vec3 glowColor;
|
| varying float intensity;
|
|
|
| void main() {
|
| vec3 glow = glowColor * intensity;
|
| gl_FragColor = vec4(glow, intensity * 0.6);
|
| }
|
| `,
|
| side: THREE.BackSide,
|
| blending: THREE.AdditiveBlending,
|
| transparent: true
|
| });
|
|
|
| const atmosphereOuter = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
| earthGroup.add(atmosphereOuter);
|
|
|
|
|
| const ambientLight = new THREE.AmbientLight(isMobile ? 0x555555 : 0x333333);
|
| scene.add(ambientLight);
|
|
|
| const sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
| sunLight.position.set(5, 3, 5);
|
| scene.add(sunLight);
|
|
|
|
|
| let updateAstronaut = null;
|
| const astronautEnabled = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
| if (astronautEnabled) {
|
| const astronautGroup = new THREE.Group();
|
|
|
|
|
|
|
|
|
|
|
|
|
| const mobileEmissiveBoost = isMobile ? 1.8 : 1.0;
|
|
|
| const mat = {
|
|
|
| hull: new THREE.MeshStandardMaterial({
|
| color: 0x5a7a9a, roughness: 0.3, metalness: 0.7,
|
| emissive: 0x2a4a6a, emissiveIntensity: 0.4 * mobileEmissiveBoost
|
| }),
|
|
|
| hullAccent: new THREE.MeshStandardMaterial({
|
| color: 0x6a8aaa, roughness: 0.4, metalness: 0.6,
|
| emissive: 0x3a5a7a, emissiveIntensity: 0.35 * mobileEmissiveBoost
|
| }),
|
|
|
| chrome: new THREE.MeshStandardMaterial({
|
| color: 0xddddee, roughness: 0.1, metalness: 1.0,
|
| emissive: 0x556677, emissiveIntensity: 0.4 * mobileEmissiveBoost
|
| }),
|
|
|
| domeGlass: new THREE.MeshStandardMaterial({
|
| color: 0x55bbdd, roughness: 0.05, metalness: 0.3,
|
| transparent: true, opacity: 0.6,
|
| emissive: 0x338899, emissiveIntensity: 0.7 * mobileEmissiveBoost
|
| }),
|
|
|
| domeInner: new THREE.MeshStandardMaterial({
|
| color: 0x00ffcc, emissive: 0x00ffaa, emissiveIntensity: 1.5 * mobileEmissiveBoost,
|
| transparent: true, opacity: 0.7
|
| }),
|
|
|
| lightCyan: new THREE.MeshStandardMaterial({
|
| color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 4.0 * mobileEmissiveBoost,
|
| transparent: true, opacity: 1.0
|
| }),
|
|
|
| lightGreen: new THREE.MeshStandardMaterial({
|
| color: 0x00ff88, emissive: 0x00ff88, emissiveIntensity: 3.5 * mobileEmissiveBoost
|
| }),
|
|
|
| thrustCore: new THREE.MeshStandardMaterial({
|
| color: 0x00ccff, emissive: 0x00aaff, emissiveIntensity: 5.0 * mobileEmissiveBoost,
|
| transparent: true, opacity: 0.95
|
| }),
|
|
|
| thrustMid: new THREE.MeshStandardMaterial({
|
| color: 0x00aaff, emissive: 0x0088ee, emissiveIntensity: 2.5 * mobileEmissiveBoost,
|
| transparent: true, opacity: 0.55, side: THREE.DoubleSide
|
| }),
|
|
|
| thrustOuter: new THREE.MeshStandardMaterial({
|
| color: 0x0088dd, emissive: 0x0066bb, emissiveIntensity: 1.2 * mobileEmissiveBoost,
|
| transparent: true, opacity: 0.3, side: THREE.DoubleSide
|
| }),
|
|
|
| panel: new THREE.MeshStandardMaterial({
|
| color: 0x3a5a7a, roughness: 0.7, metalness: 0.4,
|
| emissive: 0x2a3a4a, emissiveIntensity: 0.3 * mobileEmissiveBoost
|
| })
|
| };
|
|
|
|
|
|
|
|
|
| const upperDome = new THREE.Mesh(
|
| new THREE.SphereGeometry(8, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.5),
|
| mat.hull
|
| );
|
| upperDome.scale.set(1, 0.5, 1);
|
| upperDome.position.y = 2;
|
| astronautGroup.add(upperDome);
|
|
|
|
|
| const glassDome = new THREE.Mesh(
|
| new THREE.SphereGeometry(5, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.55),
|
| mat.domeGlass
|
| );
|
| glassDome.scale.set(1, 0.7, 1);
|
| glassDome.position.y = 3.5;
|
| astronautGroup.add(glassDome);
|
|
|
|
|
| const innerGlow = new THREE.Mesh(
|
| new THREE.SphereGeometry(4.5, 24, 12, 0, Math.PI * 2, 0, Math.PI * 0.5),
|
| mat.domeInner
|
| );
|
| innerGlow.scale.set(1, 0.6, 1);
|
| innerGlow.position.y = 3.5;
|
| astronautGroup.add(innerGlow);
|
|
|
|
|
| const discProfile = [];
|
| for (let i = 0; i <= 20; i++) {
|
| const t = i / 20;
|
| const angle = t * Math.PI;
|
| const r = 8 + Math.sin(angle) * 7;
|
| const y = Math.cos(angle) * 2.5;
|
| discProfile.push(new THREE.Vector2(r, y));
|
| }
|
| const discGeo = new THREE.LatheGeometry(discProfile, 48);
|
| const mainDisc = new THREE.Mesh(discGeo, mat.hull);
|
| mainDisc.position.y = 0;
|
| astronautGroup.add(mainDisc);
|
|
|
|
|
| const chromeRing = new THREE.Mesh(
|
| new THREE.TorusGeometry(15, 0.4, 16, 64),
|
| mat.chrome
|
| );
|
| chromeRing.rotation.x = Math.PI / 2;
|
| chromeRing.position.y = 0;
|
| astronautGroup.add(chromeRing);
|
|
|
|
|
| const upperRing = new THREE.Mesh(
|
| new THREE.TorusGeometry(8, 0.25, 12, 48),
|
| mat.chrome
|
| );
|
| upperRing.rotation.x = Math.PI / 2;
|
| upperRing.position.y = 2;
|
| astronautGroup.add(upperRing);
|
|
|
|
|
|
|
|
|
| const bottomDome = new THREE.Mesh(
|
| new THREE.SphereGeometry(10, 32, 16, 0, Math.PI * 2, Math.PI * 0.5, Math.PI * 0.5),
|
| mat.hullAccent
|
| );
|
| bottomDome.scale.set(1, 0.35, 1);
|
| bottomDome.position.y = -2.5;
|
| astronautGroup.add(bottomDome);
|
|
|
|
|
| const thrustHousing = new THREE.Mesh(
|
| new THREE.CylinderGeometry(4, 5, 2, 24),
|
| mat.hull
|
| );
|
| thrustHousing.position.y = -4;
|
| astronautGroup.add(thrustHousing);
|
|
|
|
|
| const thrustEmitterRing = new THREE.Mesh(
|
| new THREE.TorusGeometry(4.5, 0.3, 12, 32),
|
| mat.chrome
|
| );
|
| thrustEmitterRing.rotation.x = Math.PI / 2;
|
| thrustEmitterRing.position.y = -5;
|
| astronautGroup.add(thrustEmitterRing);
|
|
|
|
|
| const rimLightCount = 8;
|
| const rimLights = [];
|
| const rimLightMats = [];
|
| for (let i = 0; i < rimLightCount; i++) {
|
| const angle = (i / rimLightCount) * Math.PI * 2;
|
| const lightGroup = new THREE.Group();
|
|
|
|
|
| const housing = new THREE.Mesh(
|
| new THREE.SphereGeometry(1.2, 16, 12),
|
| mat.hullAccent
|
| );
|
| housing.scale.set(1, 0.6, 1);
|
| lightGroup.add(housing);
|
|
|
|
|
| const lightMat = new THREE.MeshStandardMaterial({
|
| color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 3.5 * mobileEmissiveBoost,
|
| transparent: true, opacity: 1.0
|
| });
|
| const lightDome = new THREE.Mesh(
|
| new THREE.SphereGeometry(1, 16, 12, 0, Math.PI * 2, 0, Math.PI * 0.6),
|
| lightMat
|
| );
|
| lightDome.position.y = 0.3;
|
| lightGroup.add(lightDome);
|
|
|
| lightGroup.position.set(
|
| Math.cos(angle) * 14,
|
| 0.5,
|
| Math.sin(angle) * 14
|
| );
|
|
|
| astronautGroup.add(lightGroup);
|
| rimLights.push(lightDome);
|
| rimLightMats.push(lightMat);
|
| }
|
|
|
|
|
| const domeLightCount = 4;
|
| const domeLights = [];
|
| const domeLightMats = [];
|
| for (let i = 0; i < domeLightCount; i++) {
|
| const angle = (i / domeLightCount) * Math.PI * 2 + Math.PI / 4;
|
|
|
| const lightMat = new THREE.MeshStandardMaterial({
|
| color: 0x00ff88, emissive: 0x00ff88, emissiveIntensity: 3.0 * mobileEmissiveBoost
|
| });
|
| const domeLight = new THREE.Mesh(
|
| new THREE.SphereGeometry(0.6, 12, 8),
|
| lightMat
|
| );
|
| domeLight.position.set(
|
| Math.cos(angle) * 6,
|
| 4,
|
| Math.sin(angle) * 6
|
| );
|
| astronautGroup.add(domeLight);
|
| domeLights.push(domeLight);
|
| domeLightMats.push(lightMat);
|
| }
|
|
|
|
|
| for (let i = 0; i < 12; i++) {
|
| const angle = (i / 12) * Math.PI * 2;
|
| const panelLine = new THREE.Mesh(
|
| new THREE.BoxGeometry(0.1, 0.3, 5),
|
| mat.panel
|
| );
|
| panelLine.position.set(
|
| Math.cos(angle) * 11,
|
| 1,
|
| Math.sin(angle) * 11
|
| );
|
| panelLine.rotation.y = -angle;
|
| astronautGroup.add(panelLine);
|
| }
|
|
|
|
|
|
|
|
|
|
|
| const mobileLightBoost = isMobile ? 2.0 : 1.0;
|
|
|
| const thrustGroup = new THREE.Group();
|
| thrustGroup.position.y = -5;
|
| astronautGroup.add(thrustGroup);
|
|
|
|
|
| const beamCoreGeo = new THREE.CylinderGeometry(1, 5, 30, 24, 1, true);
|
| const beamCore = new THREE.Mesh(beamCoreGeo, mat.thrustCore);
|
| beamCore.position.y = -15;
|
| thrustGroup.add(beamCore);
|
|
|
|
|
| const beamMidGeo = new THREE.CylinderGeometry(2.5, 8, 35, 24, 1, true);
|
| const beamMid = new THREE.Mesh(beamMidGeo, mat.thrustMid);
|
| beamMid.position.y = -17;
|
| thrustGroup.add(beamMid);
|
|
|
|
|
| const beamOuterGeo = new THREE.CylinderGeometry(5, 14, 40, 24, 1, true);
|
| const beamOuter = new THREE.Mesh(beamOuterGeo, mat.thrustOuter);
|
| beamOuter.position.y = -20;
|
| thrustGroup.add(beamOuter);
|
|
|
|
|
| const thrustParticleCount = isMobile ? 80 : 200;
|
| const thrustParticleGeo = new THREE.BufferGeometry();
|
| const thrustParticlePos = new Float32Array(thrustParticleCount * 3);
|
| const thrustParticleSpeeds = [];
|
|
|
| for (let i = 0; i < thrustParticleCount; i++) {
|
| const angle = Math.random() * Math.PI * 2;
|
| const radius = Math.random() * 5;
|
| thrustParticlePos[i * 3] = Math.cos(angle) * radius;
|
| thrustParticlePos[i * 3 + 1] = -Math.random() * 35;
|
| thrustParticlePos[i * 3 + 2] = Math.sin(angle) * radius;
|
| thrustParticleSpeeds.push(0.6 + Math.random() * 1.8);
|
| }
|
| thrustParticleGeo.setAttribute('position', new THREE.BufferAttribute(thrustParticlePos, 3));
|
|
|
| const thrustParticleMat = new THREE.PointsMaterial({
|
| color: 0x00eeff, size: isMobile ? 1.0 : 0.7, transparent: true, opacity: 1.0,
|
| blending: THREE.AdditiveBlending
|
| });
|
| const thrustParticles = new THREE.Points(thrustParticleGeo, thrustParticleMat);
|
| thrustGroup.add(thrustParticles);
|
|
|
|
|
| const thrustLight = new THREE.PointLight(0x00aaff, 5 * mobileLightBoost, 100);
|
| thrustLight.position.y = -15;
|
| thrustGroup.add(thrustLight);
|
|
|
|
|
| const ufoMainLight = new THREE.PointLight(0x00d4ff, 1.5 * mobileLightBoost, 150);
|
| ufoMainLight.position.set(0, 8, 0);
|
| astronautGroup.add(ufoMainLight);
|
|
|
| const ufoRimLight = new THREE.PointLight(0x00ffaa, 1.0 * mobileLightBoost, 120);
|
| ufoRimLight.position.set(0, -5, 0);
|
| astronautGroup.add(ufoRimLight);
|
|
|
|
|
| const ufoTopLight = new THREE.PointLight(0x88aacc, 0.8 * mobileLightBoost, 80);
|
| ufoTopLight.position.set(0, 15, 0);
|
| astronautGroup.add(ufoTopLight);
|
|
|
| const ufoFrontLight = new THREE.PointLight(0x00ccff, 0.6 * mobileLightBoost, 60);
|
| ufoFrontLight.position.set(0, 0, 20);
|
| astronautGroup.add(ufoFrontLight);
|
|
|
|
|
| if (isMobile) {
|
| const ufoBottomLight = new THREE.PointLight(0x00aaff, 1.5, 100);
|
| ufoBottomLight.position.set(0, -15, 0);
|
| astronautGroup.add(ufoBottomLight);
|
| }
|
|
|
|
|
| const ufoScale = isMobile ? 0.7 : 0.8;
|
| astronautGroup.scale.setScalar(ufoScale);
|
| scene.add(astronautGroup);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function smoothstep(edge0, edge1, x) {
|
| const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
|
| return t * t * (3 - 2 * t);
|
| }
|
|
|
| function smootherstep(edge0, edge1, x) {
|
| const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
|
| return t * t * t * (t * (t * 6 - 15) + 10);
|
| }
|
|
|
| function catmullRom(p0, p1, p2, p3, t) {
|
| const t2 = t * t;
|
| const t3 = t2 * t;
|
| return new THREE.Vector3(
|
| 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
|
| 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
|
| 0.5 * ((2 * p1.z) + (-p0.z + p2.z) * t + (2 * p0.z - 5 * p1.z + 4 * p2.z - p3.z) * t2 + (-p0.z + 3 * p1.z - 3 * p2.z + p3.z) * t3)
|
| );
|
| }
|
|
|
| function catmullRomTangent(p0, p1, p2, p3, t) {
|
| const t2 = t * t;
|
| return new THREE.Vector3(
|
| 0.5 * ((-p0.x + p2.x) + 2 * (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t + 3 * (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t2),
|
| 0.5 * ((-p0.y + p2.y) + 2 * (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t + 3 * (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t2),
|
| 0.5 * ((-p0.z + p2.z) + 2 * (2 * p0.z - 5 * p1.z + 4 * p2.z - p3.z) * t + 3 * (-p0.z + 3 * p1.z - 3 * p2.z + p3.z) * t2)
|
| ).normalize();
|
| }
|
|
|
| function quaternionFromDirection(direction, up) {
|
| up = up || new THREE.Vector3(0, 1, 0);
|
| const matrix = new THREE.Matrix4();
|
| matrix.lookAt(new THREE.Vector3(0, 0, 0), direction, up);
|
| const quat = new THREE.Quaternion();
|
| quat.setFromRotationMatrix(matrix);
|
| return quat;
|
| }
|
|
|
|
|
| const flightBounds = {
|
| xMin: -180, xMax: 180,
|
| yMin: 60, yMax: 220,
|
| zMin: -120, zMax: 80
|
| };
|
|
|
|
|
| function generateWaypoint(currentPos, currentDirection) {
|
| const minDist = 100;
|
| const maxDist = 200;
|
| const dist = minDist + Math.random() * (maxDist - minDist);
|
| const forwardBias = 0.6;
|
|
|
| let newDir = new THREE.Vector3(
|
| currentDirection.x * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 2,
|
| currentDirection.y * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 1.5,
|
| currentDirection.z * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 2
|
| ).normalize();
|
|
|
| let target = new THREE.Vector3(
|
| currentPos.x + newDir.x * dist,
|
| currentPos.y + newDir.y * dist,
|
| currentPos.z + newDir.z * dist
|
| );
|
|
|
| const margin = 30;
|
| if (target.x < flightBounds.xMin + margin) target.x = flightBounds.xMin + margin + Math.random() * 40;
|
| if (target.x > flightBounds.xMax - margin) target.x = flightBounds.xMax - margin - Math.random() * 40;
|
| if (target.y < flightBounds.yMin + margin) target.y = flightBounds.yMin + margin + Math.random() * 30;
|
| if (target.y > flightBounds.yMax - margin) target.y = flightBounds.yMax - margin - Math.random() * 30;
|
| if (target.z < flightBounds.zMin + margin) target.z = flightBounds.zMin + margin + Math.random() * 40;
|
| if (target.z > flightBounds.zMax - 50) target.z = flightBounds.zMax - 50 - Math.random() * 30;
|
|
|
| return target;
|
| }
|
|
|
|
|
| const ec = earthGroup.position;
|
| const startPos = new THREE.Vector3(ec.x + 50, ec.y + 130, ec.z - 30);
|
|
|
| const st = {
|
| waypoints: [
|
| startPos.clone().add(new THREE.Vector3(-100, 20, -50)),
|
| startPos.clone(),
|
| startPos.clone().add(new THREE.Vector3(80, -10, 30)),
|
| startPos.clone().add(new THREE.Vector3(160, 30, -20))
|
| ],
|
| t: 0,
|
| segmentDuration: 4,
|
| cruiseSpeed: 1.2,
|
| currentSpeed: 0,
|
| currentQuat: new THREE.Quaternion(),
|
| targetQuat: new THREE.Quaternion(),
|
| bankAngle: 0,
|
| targetBankAngle: 0,
|
| verticalWavePhase: Math.random() * Math.PI * 2,
|
| verticalWaveAmp: 1.5,
|
| verticalWaveFreq: 0.08,
|
| prevVelocity: new THREE.Vector3(1, 0, 0),
|
| totalTime: 0,
|
| thrusterIntensity: 1.0,
|
| opacity: 1.0,
|
| targetOp: 1.0,
|
|
|
| teleportCooldown: 15 + Math.random() * 20,
|
| teleportTimer: 0,
|
| isTeleporting: false,
|
| teleportPhase: 0,
|
| teleportDuration: 3.5,
|
| teleportFlashIntensity: 0,
|
| preTeleportPos: null,
|
| preTeleportQuat: null
|
| };
|
|
|
| const initialDir = new THREE.Vector3().subVectors(st.waypoints[2], st.waypoints[1]).normalize();
|
| st.currentQuat = quaternionFromDirection(initialDir);
|
| st.targetQuat = st.currentQuat.clone();
|
| astronautGroup.quaternion.copy(st.currentQuat);
|
| astronautGroup.position.copy(st.waypoints[1]);
|
|
|
| function advanceWaypoint() {
|
| st.waypoints[0] = st.waypoints[1].clone();
|
| st.waypoints[1] = st.waypoints[2].clone();
|
| st.waypoints[2] = st.waypoints[3].clone();
|
| const currentDir = new THREE.Vector3().subVectors(st.waypoints[2], st.waypoints[1]).normalize();
|
| st.waypoints[3] = generateWaypoint(st.waypoints[2], currentDir);
|
| st.t = 0;
|
| st.segmentDuration = 3 + Math.random() * 3;
|
| st.cruiseSpeed = 1.0 + Math.random() * 0.5;
|
| }
|
|
|
|
|
| updateAstronaut = function(time, deltaTime) {
|
| const dt = Math.min(deltaTime || 0.016, 0.05);
|
| st.totalTime += dt;
|
|
|
|
|
|
|
|
|
|
|
| const accelZone = 0.15;
|
| const decelZone = 0.85;
|
|
|
| let speedMultiplier;
|
| if (st.t < accelZone) {
|
| speedMultiplier = smootherstep(0, accelZone, st.t);
|
| } else if (st.t > decelZone) {
|
| speedMultiplier = smootherstep(1, decelZone, st.t);
|
| } else {
|
| speedMultiplier = 1.0;
|
| }
|
|
|
| st.currentSpeed = speedMultiplier * st.cruiseSpeed;
|
|
|
| const baseProgress = dt / st.segmentDuration;
|
| st.t += baseProgress * (0.5 + st.currentSpeed * 1.2);
|
|
|
| if (st.t >= 1.0) {
|
| st.t = 0;
|
| advanceWaypoint();
|
| }
|
|
|
| const splinePos = catmullRom(
|
| st.waypoints[0], st.waypoints[1],
|
| st.waypoints[2], st.waypoints[3],
|
| st.t
|
| );
|
|
|
| st.verticalWavePhase += dt * st.verticalWaveFreq * Math.PI * 2;
|
| const verticalOffset = Math.sin(st.verticalWavePhase) * st.verticalWaveAmp;
|
| splinePos.y += verticalOffset;
|
|
|
|
|
|
|
|
|
|
|
| st.teleportTimer += dt;
|
|
|
|
|
| if (!st.isTeleporting && st.teleportTimer >= st.teleportCooldown) {
|
|
|
| st.isTeleporting = true;
|
| st.teleportPhase = 0;
|
| st.teleportFlashIntensity = 1.0;
|
| st.preTeleportPos = astronautGroup.position.clone();
|
| st.preTeleportQuat = astronautGroup.quaternion.clone();
|
|
|
|
|
| const directiveEl = document.querySelector('.signal-panel');
|
| if (directiveEl) {
|
| const rect = directiveEl.getBoundingClientRect();
|
|
|
| const screenX = (rect.left + rect.width / 2) / window.innerWidth * 2 - 1;
|
| const screenY = -((rect.top + rect.height / 2) / window.innerHeight * 2 - 1);
|
|
|
|
|
| const teleportZ = 150;
|
| const fov = camera.fov * Math.PI / 180;
|
| const height = 2 * Math.tan(fov / 2) * teleportZ;
|
| const width = height * camera.aspect;
|
|
|
| st.teleportTarget = new THREE.Vector3(
|
| screenX * width / 2,
|
| screenY * height / 2 + 20,
|
| camera.position.z - teleportZ
|
| );
|
| }
|
| }
|
|
|
|
|
| if (st.isTeleporting) {
|
| st.teleportPhase += dt;
|
|
|
|
|
| st.teleportFlashIntensity = Math.max(0, st.teleportFlashIntensity - dt * 2);
|
|
|
| if (st.teleportPhase < st.teleportDuration) {
|
|
|
| if (st.teleportTarget) {
|
| const hoverOffset = new THREE.Vector3(
|
| Math.sin(st.teleportPhase * 2) * 8,
|
| Math.sin(st.teleportPhase * 1.5) * 5,
|
| Math.sin(st.teleportPhase * 1.8) * 4
|
| );
|
| astronautGroup.position.copy(st.teleportTarget).add(hoverOffset);
|
|
|
|
|
| const lookDir = new THREE.Vector3(0, 0, 1);
|
| const tiltAngle = Math.sin(st.teleportPhase * 2) * 0.15;
|
| st.targetQuat = quaternionFromDirection(lookDir);
|
| const tiltQuat = new THREE.Quaternion().setFromAxisAngle(
|
| new THREE.Vector3(1, 0, 0), tiltAngle
|
| );
|
| st.targetQuat.multiply(tiltQuat);
|
| st.currentQuat.slerp(st.targetQuat, 0.1);
|
| astronautGroup.quaternion.copy(st.currentQuat);
|
| }
|
| } else {
|
|
|
| if (st.teleportPhase < st.teleportDuration + 0.5) {
|
| st.teleportFlashIntensity = 1.0 - (st.teleportPhase - st.teleportDuration) * 2;
|
| } else {
|
|
|
| st.isTeleporting = false;
|
| st.teleportTimer = 0;
|
| st.teleportCooldown = 20 + Math.random() * 25;
|
| st.teleportFlashIntensity = 0;
|
|
|
|
|
| st.waypoints[1] = splinePos.clone();
|
| const dir = st.prevVelocity.clone().normalize();
|
| st.waypoints[0] = splinePos.clone().sub(dir.clone().multiplyScalar(50));
|
| st.waypoints[2] = generateWaypoint(splinePos, dir);
|
| st.waypoints[3] = generateWaypoint(st.waypoints[2], dir);
|
| st.t = 0;
|
| }
|
| }
|
| } else {
|
|
|
| astronautGroup.position.copy(splinePos);
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| if (!st.isTeleporting) {
|
| const velocity = catmullRomTangent(
|
| st.waypoints[0], st.waypoints[1],
|
| st.waypoints[2], st.waypoints[3],
|
| st.t
|
| );
|
|
|
| const turnAxis = new THREE.Vector3().crossVectors(st.prevVelocity, velocity);
|
| const turnAmount = turnAxis.y;
|
|
|
| st.targetBankAngle = -turnAmount * 35 * (Math.PI / 180);
|
| st.bankAngle += (st.targetBankAngle - st.bankAngle) * 0.08;
|
| st.prevVelocity.copy(velocity);
|
|
|
| st.targetQuat = quaternionFromDirection(velocity);
|
| const bankQuat = new THREE.Quaternion();
|
| bankQuat.setFromAxisAngle(velocity, st.bankAngle);
|
| st.targetQuat.multiply(bankQuat);
|
|
|
| st.currentQuat.slerp(st.targetQuat, 0.12);
|
| astronautGroup.quaternion.copy(st.currentQuat);
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| for (let i = 0; i < rimLightCount; i++) {
|
| const phase = time * 2 + (i / rimLightCount) * Math.PI * 2;
|
| rimLightMats[i].emissiveIntensity = 2.5 + Math.sin(phase) * 1.0;
|
| rimLightMats[i].opacity = 0.85 + Math.sin(phase) * 0.15;
|
| }
|
|
|
|
|
| for (let i = 0; i < domeLightCount; i++) {
|
| const phase = time * 3 + (i / domeLightCount) * Math.PI * 2;
|
| domeLightMats[i].emissiveIntensity = 2.0 + Math.sin(phase) * 1.0;
|
| }
|
|
|
|
|
| mat.domeInner.emissiveIntensity = 0.8 + Math.sin(time * 1.5) * 0.4;
|
| mat.domeInner.opacity = 0.5 + Math.sin(time * 1.5) * 0.15;
|
|
|
|
|
|
|
|
|
|
|
|
|
| const signalEl = document.getElementById('dominantSignal');
|
| const dominantSignal = signalEl ? signalEl.textContent.trim().toUpperCase() : 'NEUTRAL';
|
| const isSellSignal = dominantSignal === 'SELL';
|
| const isBuySignal = dominantSignal === 'BUY';
|
| const isNeutral = !isSellSignal && !isBuySignal;
|
|
|
|
|
|
|
| let targetCoreColor, targetCoreEmissive, targetMidColor, targetMidEmissive;
|
| let targetOuterColor, targetOuterEmissive, targetParticleColor, targetLightColor;
|
|
|
| if (isSellSignal) {
|
|
|
| targetCoreColor = new THREE.Color(0xff4444);
|
| targetCoreEmissive = new THREE.Color(0xff2222);
|
| targetMidColor = new THREE.Color(0xff6644);
|
| targetMidEmissive = new THREE.Color(0xee4422);
|
| targetOuterColor = new THREE.Color(0xcc4422);
|
| targetOuterEmissive = new THREE.Color(0xaa3311);
|
| targetParticleColor = new THREE.Color(0xff6655);
|
| targetLightColor = 0xff4433;
|
| } else if (isBuySignal) {
|
|
|
| targetCoreColor = new THREE.Color(0x00ccff);
|
| targetCoreEmissive = new THREE.Color(0x00aaff);
|
| targetMidColor = new THREE.Color(0x00aaff);
|
| targetMidEmissive = new THREE.Color(0x0088ee);
|
| targetOuterColor = new THREE.Color(0x0088dd);
|
| targetOuterEmissive = new THREE.Color(0x0066bb);
|
| targetParticleColor = new THREE.Color(0x00eeff);
|
| targetLightColor = 0x00aaff;
|
| } else {
|
|
|
| targetCoreColor = new THREE.Color(0xffcc44);
|
| targetCoreEmissive = new THREE.Color(0xffaa22);
|
| targetMidColor = new THREE.Color(0xddaa33);
|
| targetMidEmissive = new THREE.Color(0xcc9922);
|
| targetOuterColor = new THREE.Color(0xbb8822);
|
| targetOuterEmissive = new THREE.Color(0x996611);
|
| targetParticleColor = new THREE.Color(0xffdd66);
|
| targetLightColor = 0xffaa33;
|
| }
|
|
|
|
|
| mat.thrustCore.color.lerp(targetCoreColor, 0.08);
|
| mat.thrustCore.emissive.lerp(targetCoreEmissive, 0.08);
|
| mat.thrustMid.color.lerp(targetMidColor, 0.08);
|
| mat.thrustMid.emissive.lerp(targetMidEmissive, 0.08);
|
| mat.thrustOuter.color.lerp(targetOuterColor, 0.08);
|
| mat.thrustOuter.emissive.lerp(targetOuterEmissive, 0.08);
|
| thrustParticleMat.color.lerp(targetParticleColor, 0.08);
|
|
|
|
|
| const currentLightColor = new THREE.Color(thrustLight.color);
|
| currentLightColor.lerp(new THREE.Color(targetLightColor), 0.08);
|
| thrustLight.color.copy(currentLightColor);
|
|
|
|
|
| const isAccelerating = st.t < accelZone && st.currentSpeed > 0.3;
|
| const isTurning = Math.abs(st.targetBankAngle) > 0.05;
|
| const thrustActive = isAccelerating || isTurning || st.currentSpeed > 0.5 || st.isTeleporting;
|
|
|
| if (thrustActive) {
|
| st.thrusterIntensity = Math.min(st.thrusterIntensity + dt * 3, st.isTeleporting ? 1.2 : 1);
|
| } else {
|
| st.thrusterIntensity = Math.max(st.thrusterIntensity - dt * 2, 0.3);
|
| }
|
|
|
|
|
| mat.thrustCore.emissiveIntensity = (3.5 + Math.sin(time * 8) * 0.8) * st.thrusterIntensity;
|
| mat.thrustCore.opacity = (0.9 + Math.sin(time * 6) * 0.1) * st.thrusterIntensity;
|
|
|
|
|
| beamCore.rotation.y = time * 0.5;
|
| beamMid.rotation.y = -time * 0.3;
|
| beamOuter.rotation.y = time * 0.2;
|
|
|
|
|
| const thrustScale = 0.5 + st.thrusterIntensity * 0.5;
|
| thrustGroup.scale.set(thrustScale, thrustScale, thrustScale);
|
|
|
|
|
| thrustLight.intensity = (4.0 + Math.sin(time * 10) * 1.0 + Math.sin(time * 17) * 0.5) * st.thrusterIntensity;
|
|
|
|
|
| const positions = thrustParticleGeo.attributes.position.array;
|
| for (let i = 0; i < thrustParticleCount; i++) {
|
| positions[i * 3 + 1] -= thrustParticleSpeeds[i] * 0.6 * st.thrusterIntensity;
|
|
|
| if (positions[i * 3 + 1] < -38) {
|
| const angle = Math.random() * Math.PI * 2;
|
| const radius = Math.random() * 5;
|
| positions[i * 3] = Math.cos(angle) * radius;
|
| positions[i * 3 + 1] = 0;
|
| positions[i * 3 + 2] = Math.sin(angle) * radius;
|
| }
|
|
|
| positions[i * 3] *= 1.003;
|
| positions[i * 3 + 2] *= 1.003;
|
| }
|
| thrustParticleGeo.attributes.position.needsUpdate = true;
|
| thrustParticleMat.opacity = 0.85 * st.thrusterIntensity;
|
|
|
|
|
| ufoMainLight.intensity = 1.2 + Math.sin(time * 1.5) * 0.4 + st.teleportFlashIntensity * 3;
|
| ufoRimLight.intensity = 0.8 + st.thrusterIntensity * 0.5 + st.teleportFlashIntensity * 2;
|
|
|
|
|
| if (st.teleportFlashIntensity > 0) {
|
| for (let i = 0; i < rimLightCount; i++) {
|
| rimLightMats[i].emissiveIntensity += st.teleportFlashIntensity * 5;
|
| }
|
| for (let i = 0; i < domeLightCount; i++) {
|
| domeLightMats[i].emissiveIntensity += st.teleportFlashIntensity * 4;
|
| }
|
| mat.domeInner.emissiveIntensity += st.teleportFlashIntensity * 3;
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| if (st.isTeleporting || isMobile) {
|
| st.targetOp = 1.0;
|
| st.opacity = 1.0;
|
| } else {
|
| const distCam = astronautGroup.position.distanceTo(camera.position);
|
| st.targetOp = distCam < 250 ? 0.4 : distCam < 450 ? 0.7 : 1.0;
|
| st.opacity += (st.targetOp - st.opacity) * 0.015;
|
| }
|
| };
|
| }
|
|
|
|
|
| let time = 0;
|
| let lastTime = performance.now();
|
|
|
| function animate() {
|
| requestAnimationFrame(animate);
|
|
|
| const currentTime = performance.now();
|
| const deltaTime = (currentTime - lastTime) / 1000;
|
| lastTime = currentTime;
|
|
|
| time += 0.016;
|
|
|
|
|
| stars.rotation.y += 0.0001;
|
| stars.rotation.x += 0.00005;
|
|
|
|
|
| simulatePhysics(deltaTime);
|
| updateConnections();
|
|
|
|
|
| nodeShaderMaterial.uniforms.time.value = time;
|
|
|
|
|
| nodePoints.rotation.y += 0.0002;
|
| lines.rotation.y += 0.0002;
|
|
|
|
|
| earth.rotation.y += 0.0005;
|
| clouds.rotation.y += 0.0006;
|
| clouds.rotation.x += 0.0001;
|
|
|
|
|
| const sunAngle = time * 0.05;
|
| const sunDir = new THREE.Vector3(
|
| Math.cos(sunAngle),
|
| 0.3,
|
| Math.sin(sunAngle)
|
| ).normalize();
|
| earthMaterial.uniforms.sunDirection.value = sunDir;
|
| sunLight.position.set(sunDir.x * 5, sunDir.y * 5, sunDir.z * 5);
|
|
|
|
|
| atmosphereMaterial.uniforms.viewVector.value = new THREE.Vector3().subVectors(
|
| camera.position,
|
| earthGroup.position
|
| );
|
|
|
|
|
| if (updateAstronaut) updateAstronaut(time, deltaTime);
|
|
|
| renderer.render(scene, camera);
|
| }
|
|
|
|
|
| window.addEventListener('resize', () => {
|
| camera.aspect = window.innerWidth / window.innerHeight;
|
| camera.updateProjectionMatrix();
|
| renderer.setSize(window.innerWidth, window.innerHeight);
|
| });
|
|
|
|
|
| updateConnections();
|
| animate();
|
|
|
|
|
| setTimeout(() => {
|
| document.getElementById('loadingOverlay')?.classList.add('hidden');
|
| }, 1500);
|
| })();
|
|
|
|
|
| const signalHistory = [];
|
| const SIGNAL_WINDOW = 5 * 60 * 1000;
|
| const TARGET_AGENT = '10m';
|
| let consecutiveErrors = 0;
|
|
|
|
|
|
|
|
|
|
|
| function extractAgentName(signalKey) {
|
| if (!signalKey) return 'unknown';
|
| signalKey = signalKey.toString().trim().toLowerCase();
|
| if (signalKey.includes('_')) {
|
| return signalKey.split('_')[0];
|
| }
|
| const match = signalKey.match(/\d+[mhdw]/);
|
| return match ? match[0] : 'unknown';
|
| }
|
|
|
|
|
| function updateForceBalance(buyPercent, sellPercent, dominant) {
|
| const buyFill = document.getElementById('gaugeFillBuy');
|
| const sellFill = document.getElementById('gaugeFillSell');
|
| const marker = document.getElementById('gaugeMarker');
|
| const buyVal = document.getElementById('forceBuyValue');
|
| const sellVal = document.getElementById('forceSellValue');
|
| const netVal = document.getElementById('netPositionValue');
|
|
|
|
|
| buyPercent = Math.min(100, Math.max(0, buyPercent || 0));
|
| sellPercent = Math.min(100, Math.max(0, sellPercent || 0));
|
|
|
|
|
| if (buyFill) {
|
| buyFill.style.transform = `scaleX(${buyPercent / 100})`;
|
| }
|
| if (sellFill) {
|
| sellFill.style.transform = `scaleX(${sellPercent / 100})`;
|
| }
|
|
|
|
|
| if (buyVal) buyVal.textContent = buyPercent.toFixed(1) + '%';
|
| if (sellVal) sellVal.textContent = sellPercent.toFixed(1) + '%';
|
|
|
|
|
| const net = buyPercent - sellPercent;
|
| if (netVal) {
|
| netVal.textContent = (net >= 0 ? '+' : '') + net.toFixed(1);
|
| netVal.classList.remove('positive', 'negative', 'neutral');
|
| if (net > 0) {
|
| netVal.classList.add('positive');
|
| } else if (net < 0) {
|
| netVal.classList.add('negative');
|
| } else {
|
| netVal.classList.add('neutral');
|
| }
|
| }
|
|
|
|
|
|
|
| const markerPos = 50 + ((sellPercent - buyPercent) / 2);
|
| if (marker) {
|
| marker.style.left = markerPos + '%';
|
| marker.classList.remove('buy-dominant', 'sell-dominant');
|
| if (dominant === 'BUY') {
|
| marker.classList.add('buy-dominant');
|
| } else if (dominant === 'SELL') {
|
| marker.classList.add('sell-dominant');
|
| }
|
| }
|
| }
|
|
|
| function updateDominantSignal() {
|
| const now = Date.now();
|
| const recentSignals = signalHistory.filter(s => now - s.timestamp < SIGNAL_WINDOW);
|
|
|
| const buyCount = recentSignals.filter(s => s.action === 'BUY').length;
|
| const sellCount = recentSignals.filter(s => s.action === 'SELL').length;
|
| const total = buyCount + sellCount;
|
|
|
| const el = document.getElementById('dominantSignal');
|
| const statsEl = document.getElementById('signalStats');
|
|
|
| let dominant = 'NEUTRAL';
|
| if (total > 0) {
|
| if (buyCount > sellCount) dominant = 'BUY';
|
| else if (sellCount > buyCount) dominant = 'SELL';
|
| }
|
|
|
| if (el) {
|
| el.textContent = dominant;
|
| el.className = 'signal-value signal-' + dominant.toLowerCase();
|
| }
|
|
|
| if (statsEl && total > 0) {
|
| const buyPct = ((buyCount / total) * 100).toFixed(0);
|
| const sellPct = ((sellCount / total) * 100).toFixed(0);
|
| statsEl.innerHTML = `<span>BUY: ${buyCount} (${buyPct}%)</span> • <span>SELL: ${sellCount} (${sellPct}%)</span> • <span>TOTAL: ${total}</span>`;
|
|
|
| updateForceBalance(parseFloat(buyPct), parseFloat(sellPct), dominant);
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| function setText(id, text) {
|
| const el = document.getElementById(id);
|
| if (el) {
|
| if (text === null || text === undefined || String(text).includes('undefined') || String(text).includes('NaN')) {
|
| el.textContent = '—';
|
| } else {
|
| el.textContent = text;
|
| }
|
| }
|
| }
|
|
|
|
|
| function formatNumber(num) {
|
| if (num === undefined || num === null || isNaN(Number(num))) return '—';
|
| num = Number(num);
|
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
| if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
| return num.toLocaleString();
|
| }
|
|
|
|
|
| function safeNum(val, decimals = 1) {
|
| if (val === undefined || val === null || val === '' || isNaN(Number(val))) return null;
|
| return Number(val).toFixed(decimals);
|
| }
|
|
|
|
|
| function formatNum(val, decimals) {
|
| if (val === undefined || val === null || val === '' || isNaN(Number(val))) return '—';
|
| return Number(val).toFixed(decimals);
|
| }
|
|
|
| function updateConnectionStatus(connected) {
|
| const el = document.getElementById('connectionStatus');
|
| if (el) {
|
| el.textContent = connected ? '● Connected' : '● Disconnected';
|
| el.className = 'connection-status ' + (connected ? 'connection-ok' : 'connection-error');
|
| }
|
| }
|
|
|
|
|
| function updateMetrics() {
|
| fetch('/api/metrics')
|
| .then(r => r.json())
|
| .then(data => {
|
| consecutiveErrors = 0;
|
| updateConnectionStatus(true);
|
|
|
| const now = new Date();
|
| setText('lastUpdate', 'Last Observation: ' + now.toLocaleTimeString('en-US', {
|
| hour: '2-digit',
|
| minute: '2-digit',
|
| second: '2-digit',
|
| hour12: false
|
| }));
|
|
|
|
|
| if (data.asset) {
|
| const instBar = document.getElementById('instBar');
|
| if (instBar) instBar.textContent = data.asset;
|
| }
|
|
|
|
|
| if (data.metrics) {
|
| setText('trainingSteps', formatNumber(data.metrics.training_steps));
|
| setText('actorLoss', formatNum(data.metrics.actor_loss, 2));
|
| setText('criticLoss', formatNum(data.metrics.critic_loss, 4));
|
| setText('avnLoss', formatNum(data.metrics.avn_loss, 4));
|
|
|
|
|
| const accEl = document.getElementById('avnAccuracy');
|
| if (accEl) {
|
| const accRaw = data.metrics.avn_accuracy;
|
| const accValid = accRaw !== undefined && accRaw !== null && !isNaN(Number(accRaw));
|
| if (accValid) {
|
| const accVal = Number(accRaw).toFixed(1);
|
| accEl.textContent = accVal + '%';
|
| accEl.style.color = parseFloat(accVal) >= 55 ? '#00d4aa' : '#ffa500';
|
| } else {
|
| accEl.textContent = '—';
|
| }
|
| }
|
|
|
| setText('bufferSize', formatNumber(data.metrics.buffer_size));
|
|
|
|
|
| const matched = data.metrics.matched_rewards || 0;
|
| const unmatched = data.metrics.unmatched_rewards || 0;
|
| const total = matched + unmatched;
|
| const matchRate = total > 0 ? (matched / total * 100).toFixed(1) + '%' : '—';
|
|
|
| setText('matchedRewards', matched > 0 ? formatNumber(matched) : '—');
|
| setText('unmatchedRewards', unmatched > 0 ? formatNumber(unmatched) : '—');
|
| setText('duplicates', formatNumber(data.metrics.duplicates));
|
| setText('matchRate', matchRate);
|
| }
|
|
|
|
|
| if (data.services) {
|
| updateServiceStatus('statusQuasar', data.services['quasar_engine'] || data.services['quasar'] || 'unknown');
|
| updateServiceStatus('statusFeatures', data.services['features'] || data.services['feature_pipeline'] || 'active');
|
| updateServiceStatus('statusRewards', data.services['rewards'] || data.services['reward_system'] || 'active');
|
| }
|
|
|
|
|
| const processCount = data.process_count || data.resources?.process_count || 1;
|
| const procEl = document.getElementById('processCount');
|
| const procVal = document.getElementById('processCountValue');
|
|
|
| if (procVal) procVal.textContent = processCount;
|
| if (procEl) {
|
| procEl.classList.remove('process-ok', 'process-warning');
|
| procEl.classList.add(processCount >= 1 ? 'process-ok' : 'process-warning');
|
| }
|
|
|
|
|
| if (data.resources) {
|
| const cpuVal = safeNum(data.resources.cpu_percent);
|
| setText('cpuPercent', cpuVal !== null ? cpuVal + '%' : '—');
|
| const cpuBar = document.getElementById('cpuBar');
|
| if (cpuBar) {
|
| cpuBar.style.width = (cpuVal !== null ? Math.min(parseFloat(cpuVal), 100) : 0) + '%';
|
| cpuBar.className = 'progress-fill' + (parseFloat(cpuVal) > 80 ? ' warning' : '');
|
| }
|
|
|
| const memPct = safeNum(data.resources.memory_percent);
|
| setText('memoryPercent', memPct !== null ? memPct + '%' : '—');
|
|
|
| const memUsed = data.resources.memory_used_gb;
|
| const memTotal = data.resources.memory_total_gb;
|
| const memUsedValid = memUsed !== undefined && memUsed !== null && memUsed !== '';
|
| const memTotalValid = memTotal !== undefined && memTotal !== null && memTotal !== '';
|
| setText('memoryUsed', (memUsedValid && memTotalValid)
|
| ? memUsed + ' / ' + memTotal + ' GB'
|
| : '—');
|
|
|
| const quasarMem = data.resources.quasar_memory_gb;
|
| const quasarValid = quasarMem !== undefined && quasarMem !== null && quasarMem !== '';
|
| setText('quasarMemory', quasarValid ? quasarMem + ' GB' : '—');
|
| } else {
|
| setText('cpuPercent', '—');
|
| setText('memoryPercent', '—');
|
| setText('memoryUsed', '—');
|
| setText('quasarMemory', '—');
|
| }
|
|
|
|
|
| if (data.checkpoint) {
|
| setText('checkpointFile', data.checkpoint.filename ?? '—');
|
| setText('checkpointStep', data.checkpoint.step?.toLocaleString() ?? '—');
|
| setText('checkpointSize', data.checkpoint.size_mb !== undefined ? data.checkpoint.size_mb + ' MB' : '—');
|
| setText('checkpointModified', data.checkpoint.modified ?? '—');
|
| }
|
| })
|
| .catch(error => {
|
| console.error('Error fetching metrics:', error);
|
| consecutiveErrors++;
|
| if (consecutiveErrors >= 3) updateConnectionStatus(false);
|
| });
|
| }
|
|
|
| function updateServiceStatus(elementId, status) {
|
| const elem = document.getElementById(elementId);
|
| if (!elem) return;
|
| elem.textContent = (status || 'unknown').toUpperCase();
|
|
|
| if (status === 'active') {
|
| elem.className = 'status-badge status-active';
|
| } else if (status === 'unknown' || status === undefined || status === null) {
|
|
|
| elem.className = 'status-badge status-unknown';
|
| elem.textContent = 'PENDING';
|
| } else {
|
| elem.className = 'status-badge status-inactive';
|
| }
|
| }
|
|
|
|
|
|
|
| function updateLogs() {
|
| fetch('/logs/quasar_engine')
|
| .then(response => {
|
| if (!response.ok) {
|
|
|
| return fetch('/api/logs/quasar');
|
| }
|
| return response;
|
| })
|
| .then(response => {
|
| const contentType = response.headers.get('content-type') || '';
|
| if (contentType.includes('application/json')) {
|
| return response.json().then(data => (data.lines || []).join('\n'));
|
| }
|
| return response.text();
|
| })
|
| .then(logText => {
|
| const cont = document.getElementById('logContainer');
|
| if (!cont) return;
|
|
|
| cont.innerHTML = '';
|
|
|
| const lines = logText.split('\n').filter(line => line.trim());
|
| const recentLines = lines.slice(-50);
|
|
|
| recentLines.forEach(line => {
|
| if (!line.trim()) return;
|
|
|
| const div = document.createElement('div');
|
| div.className = 'log-line';
|
|
|
|
|
| if (line.includes('ERROR') || line.includes('error') || line.includes('Error') || line.includes('❌')) {
|
| div.classList.add('log-error');
|
| } else if (line.includes('WARNING') || line.includes('warning') || line.includes('Warning') || line.includes('⚠️')) {
|
| div.classList.add('log-warning');
|
| } else if (line.includes('INFO') || line.includes('✅') || line.includes('SUCCESS')) {
|
| div.classList.add('log-info');
|
| }
|
|
|
|
|
| let agent = null;
|
| const agentMatch1 = line.match(/(\d+[mhdw])_\d+/i);
|
| if (agentMatch1) agent = agentMatch1[1].toLowerCase();
|
| if (!agent) {
|
| const agentMatch2 = line.match(/[\[\(](\d+[mhdw])[\]\)]/i);
|
| if (agentMatch2) agent = agentMatch2[1].toLowerCase();
|
| }
|
| if (!agent) {
|
| const agentMatch3 = line.match(/AVN[\s\-_]?(\d+[mhdw])[:\s]/i);
|
| if (agentMatch3) agent = agentMatch3[1].toLowerCase();
|
| }
|
|
|
|
|
| if (agent && agent !== TARGET_AGENT) {
|
| return;
|
| }
|
|
|
| if (line.includes('AVN') && (line.includes('BUY') || line.includes('SELL'))) {
|
| const buyMatch = line.match(/BUY.*conf[idence]*[=:]\s*([\d.]+)/i);
|
| const sellMatch = line.match(/SELL.*conf[idence]*[=:]\s*([\d.]+)/i);
|
|
|
| if (buyMatch || sellMatch) {
|
| const signal = {
|
| action: buyMatch ? 'BUY' : 'SELL',
|
| confidence: buyMatch ? parseFloat(buyMatch[1]) : parseFloat(sellMatch[1]),
|
| timestamp: Date.now()
|
| };
|
| signalHistory.push(signal);
|
|
|
| if (signalHistory.length > 100) {
|
| signalHistory.shift();
|
| }
|
| }
|
| }
|
|
|
| div.textContent = line;
|
| cont.appendChild(div);
|
| });
|
|
|
| cont.scrollTop = cont.scrollHeight;
|
| updateDominantSignal();
|
| })
|
| .catch(error => {
|
| console.error('Error fetching logs:', error);
|
| });
|
| }
|
|
|
|
|
| function updateVisitors() {
|
| fetch('/api/visitors')
|
| .then(r => r.json())
|
| .then(v => {
|
| setText('activeVisitors', v.active_now || 0);
|
| const tel = document.getElementById('totalVisitors');
|
| if (tel) tel.textContent = (v.all_time_unique || 0) + ' total';
|
| })
|
| .catch(() => {});
|
| }
|
|
|
|
|
| let accuracyChart;
|
|
|
| function initChart() {
|
| const ctx = document.getElementById('accuracyChart')?.getContext('2d');
|
| if (!ctx) return;
|
|
|
|
|
| const gradient = ctx.createLinearGradient(0, 0, 0, 200);
|
| gradient.addColorStop(0, 'rgba(0, 255, 136, 0.25)');
|
| gradient.addColorStop(0.5, 'rgba(64, 192, 160, 0.1)');
|
| gradient.addColorStop(1, 'rgba(0, 255, 136, 0)');
|
|
|
| accuracyChart = new Chart(ctx, {
|
| type: 'line',
|
| data: {
|
| labels: [],
|
| datasets: [{
|
| label: 'AVN Accuracy',
|
| borderColor: '#00ff88',
|
| backgroundColor: gradient,
|
| borderWidth: 2.5,
|
| fill: true,
|
| tension: 0.4,
|
| pointRadius: 0,
|
| pointHoverRadius: 6,
|
| pointHoverBackgroundColor: '#00ff88',
|
| pointHoverBorderColor: '#fff',
|
| pointHoverBorderWidth: 2,
|
| data: []
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| interaction: {
|
| intersect: false,
|
| mode: 'index'
|
| },
|
| plugins: {
|
| legend: { display: false },
|
| tooltip: {
|
| backgroundColor: 'rgba(0, 20, 40, 0.95)',
|
| titleColor: '#fff',
|
| titleFont: { size: 12, weight: '600', family: 'Outfit' },
|
| bodyColor: '#00ff88',
|
| bodyFont: { size: 14, weight: '700', family: 'Space Mono' },
|
| borderColor: 'rgba(0, 255, 136, 0.3)',
|
| borderWidth: 1,
|
| padding: 12,
|
| cornerRadius: 8,
|
| displayColors: false,
|
| callbacks: {
|
| title: (items) => `Iteration ${items[0].label}`,
|
| label: (item) => `Accuracy: ${item.raw.toFixed(1)}%`
|
| }
|
| }
|
| },
|
| scales: {
|
| y: {
|
| min: 0,
|
| max: 100,
|
| grid: {
|
| color: 'rgba(255, 255, 255, 0.04)',
|
| drawBorder: false
|
| },
|
| border: { display: false },
|
| ticks: {
|
| color: 'rgba(255, 255, 255, 0.4)',
|
| callback: v => v + '%',
|
| font: { size: 11, family: 'Space Mono' },
|
| padding: 10,
|
| stepSize: 20
|
| }
|
| },
|
| x: {
|
| grid: { display: false },
|
| border: { display: false },
|
| ticks: {
|
| color: 'rgba(255, 255, 255, 0.4)',
|
| maxTicksLimit: window.innerWidth <= 768 ? 5 : 8,
|
| font: { size: 10, family: 'Space Mono' },
|
| padding: 8
|
| }
|
| }
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| async function updateAccuracy() {
|
| try {
|
|
|
| let stats = null;
|
| let history = [];
|
|
|
| try {
|
| stats = await fetch('/api/accuracy/stats').then(r => r.json());
|
| } catch (e) {
|
|
|
| try {
|
| const metricsData = await fetch('/api/metrics').then(r => r.json());
|
| if (metricsData.metrics && metricsData.metrics.avn_accuracy !== undefined) {
|
| stats = {
|
| current: metricsData.metrics.avn_accuracy,
|
| average: metricsData.metrics.avn_accuracy,
|
| last_10_avg: metricsData.metrics.avn_accuracy,
|
| trend: 'stable'
|
| };
|
| }
|
| } catch (e2) {
|
| console.warn('Accuracy fallback also failed:', e2);
|
| }
|
| }
|
|
|
| if (stats) {
|
| setText('accuracy-current', (stats.current?.toFixed(1) || '—') + (stats.current !== undefined ? '%' : ''));
|
| setText('accuracy-avg', (stats.average?.toFixed(1) || '—') + (stats.average !== undefined ? '%' : ''));
|
| setText('accuracy-last10', (stats.last_10_avg?.toFixed(1) || '—') + (stats.last_10_avg !== undefined ? '%' : ''));
|
|
|
| const trend = document.getElementById('accuracy-trend');
|
| if (trend && stats.trend) {
|
| trend.classList.remove('rising', 'stable', 'declining');
|
|
|
| if (stats.trend === 'up') {
|
| trend.classList.add('rising');
|
| trend.innerHTML = '<span class="trend-arrow">▲</span><span>RISING</span>';
|
| } else if (stats.trend === 'down') {
|
| trend.classList.add('declining');
|
| trend.innerHTML = '<span class="trend-arrow">▼</span><span>DECLINING</span>';
|
| } else {
|
| trend.classList.add('stable');
|
| trend.innerHTML = '<span class="trend-arrow">―</span><span>STABLE</span>';
|
| }
|
| }
|
| }
|
|
|
|
|
| try {
|
| history = await fetch('/api/accuracy/history').then(r => r.json());
|
| } catch (e) {
|
| console.warn('Accuracy history endpoint unavailable');
|
| }
|
|
|
|
|
| if (history.length > 0 && accuracyChart) {
|
| const recent = history.slice(-(window.innerWidth <= 768 ? 30 : 50));
|
| accuracyChart.data.labels = recent.map((_, i) => i + 1);
|
| accuracyChart.data.datasets[0].data = recent.map(d => d.accuracy);
|
| accuracyChart.update('none');
|
| }
|
| } catch(e) {
|
| console.warn('Accuracy fetch failed:', e);
|
| }
|
| }
|
|
|
|
|
| updateMetrics();
|
| updateLogs();
|
| updateVisitors();
|
| initChart();
|
| setTimeout(updateAccuracy, 500);
|
|
|
|
|
| setInterval(updateMetrics, 5000);
|
| setInterval(updateLogs, 8000);
|
| setInterval(updateVisitors, 30000);
|
| setInterval(updateAccuracy, 15000);
|
|
|
|
|
| console.log('🌌 K1RL QUASAR Full-Featured Dashboard initialized');
|
| console.log('Platform: HuggingFace Spaces (Redis-backed)');
|
| console.log('Signal Tracking: Log-based extraction (replaced Ably WebSocket)');
|
| console.log('Target Agent:', TARGET_AGENT);
|
| console.log('API Endpoints: /api/metrics, /logs/quasar_engine, /api/visitors, /api/accuracy/*');
|
| </script>
|
| </body>
|
| </html> |