1873 lines
82 KiB
HTML
1873 lines
82 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>RuVector VWM - Super Bowl LX: Patriots vs Seahawks (4D Gaussian Splatting)</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&family=Roboto+Condensed:wght@400;700&display=swap');
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; color: #e0e0e8; font-family: 'Roboto Condensed', 'SF Mono', monospace; }
|
|
canvas { display: block; width: 100%; height: 100%; }
|
|
#overlay { position: fixed; inset: 0; pointer-events: none; z-index: 10; }
|
|
#overlay > * { pointer-events: auto; }
|
|
|
|
/* === ESPN-STYLE SCOREBUG === */
|
|
#scorebug {
|
|
position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
|
|
display: flex; flex-direction: column; align-items: center; gap: 0;
|
|
filter: drop-shadow(0 2px 8px rgba(0,0,0,0.6));
|
|
}
|
|
#sb-event-tag {
|
|
background: linear-gradient(90deg, #b8860b, #daa520, #b8860b);
|
|
color: #1a1a1a; font-family: 'Oswald', sans-serif; font-weight: 700;
|
|
font-size: 0.6rem; letter-spacing: 0.25em; text-transform: uppercase;
|
|
padding: 2px 20px; border-radius: 3px 3px 0 0;
|
|
}
|
|
#sb-teams {
|
|
display: flex; align-items: stretch; background: #111; border-radius: 0 0 6px 6px;
|
|
overflow: hidden; min-width: 440px;
|
|
}
|
|
.sb-team-row {
|
|
display: flex; align-items: center; padding: 3px 0;
|
|
}
|
|
.sb-team-color { width: 5px; height: 100%; }
|
|
.sb-team-logo-box { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; margin: 0 6px; }
|
|
.sb-team-logo-box canvas { width: 26px; height: 26px; }
|
|
.sb-team-abbr { font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 0.95rem; width: 40px; }
|
|
.sb-team-record { font-size: 0.5rem; color: #666; margin-left: 2px; width: 30px; }
|
|
.sb-team-score {
|
|
font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 1.3rem;
|
|
width: 42px; text-align: center; background: #1a1a1a;
|
|
padding: 2px 0; margin-left: auto;
|
|
}
|
|
.sb-poss-indicator { width: 8px; display: flex; align-items: center; justify-content: center; }
|
|
.sb-poss-dot { width: 5px; height: 5px; border-radius: 50%; }
|
|
.sb-center-col {
|
|
display: flex; flex-direction: column; background: #1a1a1a;
|
|
min-width: 110px; align-items: center; justify-content: center; padding: 2px 10px;
|
|
border-left: 1px solid #333; border-right: 1px solid #333;
|
|
}
|
|
.sb-quarter { font-family: 'Oswald', sans-serif; font-weight: 600; font-size: 0.65rem; color: #daa520; letter-spacing: 0.1em; }
|
|
.sb-clock { font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 1.1rem; color: #fff; }
|
|
.sb-down-dist { font-size: 0.55rem; color: #8f8; font-weight: 700; }
|
|
.sb-yard-line { font-size: 0.5rem; color: #aaa; }
|
|
.sb-right-col { display: flex; flex-direction: column; align-items: center; }
|
|
.ne-color { color: #c60c30; }
|
|
.sea-color { color: #69be28; }
|
|
|
|
/* Play clock badge */
|
|
#play-clock-badge {
|
|
position: absolute; top: 82px; left: 50%; transform: translateX(-50%);
|
|
background: rgba(0,0,0,0.8); border: 1px solid #444; border-radius: 4px;
|
|
padding: 2px 10px; font-family: 'Oswald', sans-serif; font-size: 0.7rem;
|
|
color: #fff; display: flex; gap: 6px; align-items: center;
|
|
}
|
|
.pc-label { color: #888; font-size: 0.55rem; }
|
|
.pc-value { font-weight: 700; font-size: 0.85rem; }
|
|
.pc-warn { color: #f87171; }
|
|
|
|
/* Stats panel - minimal */
|
|
#stats-panel {
|
|
position: absolute; bottom: 52px; left: 12px;
|
|
background: rgba(0,0,0,0.75); backdrop-filter: blur(6px);
|
|
border: 1px solid rgba(255,255,255,0.1); border-radius: 6px;
|
|
padding: 6px 10px; font-size: 0.6rem; line-height: 1.6; color: #888;
|
|
}
|
|
.st-val { color: #daa520; font-weight: 600; }
|
|
|
|
/* Play-by-play ticker - bottom bar style */
|
|
#play-ticker {
|
|
position: absolute; bottom: 52px; right: 12px;
|
|
background: rgba(0,0,0,0.8); backdrop-filter: blur(6px);
|
|
border: 1px solid rgba(255,255,255,0.08); border-radius: 6px;
|
|
padding: 6px 10px; font-size: 0.6rem; width: 300px; max-height: 140px;
|
|
overflow-y: auto; line-height: 1.5;
|
|
}
|
|
.tk-entry { padding: 2px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
|
|
.tk-entry:last-child { border-bottom: none; }
|
|
.tk-time { color: #daa520; font-weight: 600; }
|
|
.tk-text { color: #bbb; }
|
|
.tk-highlight { color: #facc15; font-weight: 700; }
|
|
.tk-gain { color: #4ade80; }
|
|
|
|
/* Transport bar */
|
|
#transport {
|
|
position: absolute; bottom: 0; left: 0; right: 0;
|
|
background: linear-gradient(180deg, rgba(20,20,20,0.95), rgba(10,10,10,0.98));
|
|
border-top: 1px solid rgba(255,255,255,0.08);
|
|
display: flex; align-items: center; gap: 8px; padding: 8px 14px; font-size: 0.65rem;
|
|
}
|
|
.tb {
|
|
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12);
|
|
color: #aaa; border-radius: 3px; padding: 4px 10px; cursor: pointer;
|
|
font-family: 'Roboto Condensed', sans-serif; font-size: 0.65rem; transition: all 0.15s;
|
|
}
|
|
.tb:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
|
.tb.active { background: rgba(218,165,32,0.3); border-color: #daa520; color: #daa520; }
|
|
#time-slider { flex: 1; accent-color: #daa520; height: 3px; }
|
|
#time-label { color: #555; min-width: 54px; font-size: 0.6rem; }
|
|
.sep { width: 1px; height: 18px; background: rgba(255,255,255,0.08); }
|
|
|
|
/* Big play overlay */
|
|
#big-play {
|
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 3rem;
|
|
text-transform: uppercase; letter-spacing: 0.15em; opacity: 0;
|
|
text-shadow: 0 0 30px rgba(218,165,32,0.8), 0 0 60px rgba(218,165,32,0.4);
|
|
transition: opacity 0.3s; pointer-events: none; color: #daa520;
|
|
}
|
|
#big-play.show { opacity: 1; }
|
|
|
|
/* Replay badge */
|
|
#replay-badge {
|
|
position: absolute; top: 110px; left: 50%; transform: translateX(-50%);
|
|
font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 0.7rem;
|
|
color: #f87171; letter-spacing: 0.2em; text-transform: uppercase;
|
|
opacity: 0; transition: opacity 0.3s; border: 1px solid #f87171;
|
|
padding: 2px 14px; border-radius: 3px; background: rgba(0,0,0,0.7);
|
|
}
|
|
#replay-badge.show { opacity: 1; }
|
|
|
|
/* Network bug */
|
|
#network-bug {
|
|
position: absolute; top: 16px; right: 16px;
|
|
font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 0.6rem;
|
|
color: rgba(255,255,255,0.25); letter-spacing: 0.15em;
|
|
}
|
|
|
|
/* Tech description modal */
|
|
#tech-modal-backdrop {
|
|
position: fixed; inset: 0; background: rgba(0,0,0,0.85);
|
|
backdrop-filter: blur(8px); z-index: 100; display: none;
|
|
align-items: center; justify-content: center;
|
|
pointer-events: auto;
|
|
}
|
|
#tech-modal-backdrop.open { display: flex; }
|
|
#tech-modal {
|
|
background: linear-gradient(160deg, #141418 0%, #1a1a22 100%);
|
|
border: 1px solid rgba(218,165,32,0.3); border-radius: 10px;
|
|
max-width: 680px; width: 90%; max-height: 80vh; overflow-y: auto;
|
|
padding: 28px 32px; position: relative;
|
|
box-shadow: 0 0 60px rgba(218,165,32,0.12), 0 20px 60px rgba(0,0,0,0.6);
|
|
}
|
|
#tech-modal::-webkit-scrollbar { width: 4px; }
|
|
#tech-modal::-webkit-scrollbar-track { background: transparent; }
|
|
#tech-modal::-webkit-scrollbar-thumb { background: rgba(218,165,32,0.3); border-radius: 2px; }
|
|
#tech-close {
|
|
position: absolute; top: 12px; right: 16px; background: none; border: none;
|
|
color: #666; font-size: 1.4rem; cursor: pointer; line-height: 1;
|
|
transition: color 0.2s;
|
|
}
|
|
#tech-close:hover { color: #daa520; }
|
|
.tech-badge {
|
|
display: inline-block; font-family: 'Oswald', sans-serif; font-weight: 600;
|
|
font-size: 0.55rem; letter-spacing: 0.2em; text-transform: uppercase;
|
|
color: #daa520; border: 1px solid rgba(218,165,32,0.4); border-radius: 3px;
|
|
padding: 2px 10px; margin-bottom: 10px;
|
|
}
|
|
.tech-title {
|
|
font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 1.4rem;
|
|
color: #fff; margin-bottom: 18px; line-height: 1.2;
|
|
}
|
|
.tech-section {
|
|
margin-bottom: 16px;
|
|
}
|
|
.tech-section-head {
|
|
font-family: 'Oswald', sans-serif; font-weight: 600; font-size: 0.8rem;
|
|
color: #daa520; letter-spacing: 0.1em; text-transform: uppercase;
|
|
margin-bottom: 6px; display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.tech-section-head::before {
|
|
content: ''; display: inline-block; width: 3px; height: 12px;
|
|
background: #daa520; border-radius: 1px;
|
|
}
|
|
.tech-body {
|
|
font-family: 'Roboto Condensed', sans-serif; font-size: 0.75rem;
|
|
color: #aaa; line-height: 1.7;
|
|
}
|
|
.tech-body strong { color: #ccc; font-weight: 700; }
|
|
.tech-body code {
|
|
background: rgba(218,165,32,0.1); color: #daa520; padding: 1px 5px;
|
|
border-radius: 2px; font-size: 0.7rem; font-family: 'SF Mono', monospace;
|
|
}
|
|
.tech-diagram {
|
|
background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.06);
|
|
border-radius: 6px; padding: 12px 16px; margin: 10px 0;
|
|
font-family: 'SF Mono', monospace; font-size: 0.6rem; color: #888;
|
|
line-height: 1.6; white-space: pre; overflow-x: auto;
|
|
}
|
|
.tech-footer {
|
|
margin-top: 18px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.06);
|
|
font-size: 0.6rem; color: #555; text-align: center;
|
|
font-family: 'Roboto Condensed', sans-serif;
|
|
}
|
|
|
|
/* Settings modal */
|
|
#settings-backdrop {
|
|
position: fixed; inset: 0; background: rgba(0,0,0,0.85);
|
|
backdrop-filter: blur(8px); z-index: 100; display: none;
|
|
align-items: center; justify-content: center; pointer-events: auto;
|
|
}
|
|
#settings-backdrop.open { display: flex; }
|
|
#settings-panel-modal {
|
|
background: linear-gradient(160deg, #141418 0%, #1a1a22 100%);
|
|
border: 1px solid rgba(218,165,32,0.3); border-radius: 10px;
|
|
max-width: 580px; width: 92%; max-height: 82vh; overflow-y: auto;
|
|
padding: 0; position: relative;
|
|
box-shadow: 0 0 60px rgba(218,165,32,0.12), 0 20px 60px rgba(0,0,0,0.6);
|
|
}
|
|
#settings-panel-modal::-webkit-scrollbar { width: 4px; }
|
|
#settings-panel-modal::-webkit-scrollbar-track { background: transparent; }
|
|
#settings-panel-modal::-webkit-scrollbar-thumb { background: rgba(218,165,32,0.3); border-radius: 2px; }
|
|
#settings-close {
|
|
position: absolute; top: 10px; right: 14px; background: none; border: none;
|
|
color: #666; font-size: 1.4rem; cursor: pointer; line-height: 1; z-index: 2;
|
|
transition: color 0.2s;
|
|
}
|
|
#settings-close:hover { color: #daa520; }
|
|
.st-header {
|
|
padding: 18px 24px 0; display: flex; align-items: center; gap: 10px;
|
|
}
|
|
.st-header-badge {
|
|
font-family: 'Oswald', sans-serif; font-weight: 600; font-size: 0.55rem;
|
|
letter-spacing: 0.2em; text-transform: uppercase; color: #daa520;
|
|
border: 1px solid rgba(218,165,32,0.4); border-radius: 3px; padding: 2px 10px;
|
|
}
|
|
.st-header-title {
|
|
font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 1.15rem; color: #fff;
|
|
}
|
|
/* Tabs */
|
|
.st-tabs {
|
|
display: flex; gap: 0; margin: 14px 24px 0; border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
}
|
|
.st-tab {
|
|
font-family: 'Oswald', sans-serif; font-weight: 600; font-size: 0.7rem;
|
|
letter-spacing: 0.08em; text-transform: uppercase; color: #555;
|
|
padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent;
|
|
transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none;
|
|
}
|
|
.st-tab:hover { color: #aaa; }
|
|
.st-tab.active { color: #daa520; border-bottom-color: #daa520; }
|
|
.st-tab-content { display: none; padding: 16px 24px 22px; }
|
|
.st-tab-content.active { display: block; }
|
|
/* Setting rows */
|
|
.st-row {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
}
|
|
.st-row:last-child { border-bottom: none; }
|
|
.st-row-label {
|
|
font-family: 'Roboto Condensed', sans-serif; font-size: 0.72rem; color: #aaa;
|
|
}
|
|
.st-row-sub { font-size: 0.58rem; color: #555; margin-top: 1px; }
|
|
.st-row-ctrl { display: flex; align-items: center; gap: 8px; }
|
|
/* Toggle switch */
|
|
.st-toggle { position: relative; width: 36px; height: 20px; cursor: pointer; }
|
|
.st-toggle input { display: none; }
|
|
.st-toggle-track {
|
|
position: absolute; inset: 0; background: #333; border-radius: 10px;
|
|
transition: background 0.2s;
|
|
}
|
|
.st-toggle input:checked + .st-toggle-track { background: rgba(218,165,32,0.6); }
|
|
.st-toggle-thumb {
|
|
position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
|
|
background: #888; border-radius: 50%; transition: all 0.2s;
|
|
}
|
|
.st-toggle input:checked ~ .st-toggle-thumb { left: 18px; background: #daa520; }
|
|
/* Range slider */
|
|
.st-range {
|
|
width: 100px; accent-color: #daa520; height: 3px;
|
|
}
|
|
.st-range-val {
|
|
font-family: 'SF Mono', monospace; font-size: 0.6rem; color: #daa520;
|
|
min-width: 32px; text-align: right;
|
|
}
|
|
/* Select */
|
|
.st-select {
|
|
background: #222; border: 1px solid #444; color: #ccc; border-radius: 4px;
|
|
padding: 3px 8px; font-family: 'Roboto Condensed', sans-serif; font-size: 0.65rem;
|
|
cursor: pointer; outline: none;
|
|
}
|
|
.st-select:focus { border-color: #daa520; }
|
|
/* AI section styles */
|
|
.st-ai-status {
|
|
display: flex; align-items: center; gap: 6px; padding: 6px 10px;
|
|
background: rgba(218,165,32,0.06); border: 1px solid rgba(218,165,32,0.15);
|
|
border-radius: 6px; margin-bottom: 12px;
|
|
}
|
|
.st-ai-dot { width: 6px; height: 6px; border-radius: 50%; background: #4ade80; animation: st-pulse 2s infinite; }
|
|
@keyframes st-pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
|
|
.st-ai-label { font-family: 'Roboto Condensed', sans-serif; font-size: 0.65rem; color: #aaa; }
|
|
.st-ai-label strong { color: #daa520; }
|
|
.st-metric-grid {
|
|
display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 10px 0;
|
|
}
|
|
.st-metric-card {
|
|
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.06);
|
|
border-radius: 6px; padding: 8px 10px;
|
|
}
|
|
.st-metric-label { font-size: 0.55rem; color: #666; text-transform: uppercase; letter-spacing: 0.1em; font-family: 'Oswald', sans-serif; }
|
|
.st-metric-value { font-family: 'Oswald', sans-serif; font-size: 1rem; font-weight: 700; color: #daa520; }
|
|
.st-metric-sub { font-size: 0.52rem; color: #555; }
|
|
.st-pipeline {
|
|
display: flex; align-items: center; gap: 4px; margin: 10px 0; flex-wrap: wrap;
|
|
}
|
|
.st-pipe-stage {
|
|
font-family: 'SF Mono', monospace; font-size: 0.55rem; padding: 3px 8px;
|
|
background: rgba(218,165,32,0.08); border: 1px solid rgba(218,165,32,0.2);
|
|
border-radius: 3px; color: #daa520;
|
|
}
|
|
.st-pipe-arrow { color: #444; font-size: 0.6rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="c"></canvas>
|
|
<div id="overlay">
|
|
<!-- ESPN-style scorebug -->
|
|
<div id="scorebug">
|
|
<div id="sb-event-tag">Super Bowl LX • Las Vegas</div>
|
|
<div id="sb-teams">
|
|
<!-- NE row -->
|
|
<div style="display:flex;flex-direction:column;flex:1;">
|
|
<div class="sb-team-row" style="border-bottom:1px solid #222;">
|
|
<div class="sb-team-color" style="background:#c60c30;"></div>
|
|
<div class="sb-poss-indicator"><div class="sb-poss-dot" id="ne-poss" style="background:transparent;"></div></div>
|
|
<div class="sb-team-logo-box"><canvas id="pat-logo" width="26" height="26"></canvas></div>
|
|
<div class="sb-team-abbr ne-color">NE</div>
|
|
<div class="sb-team-record">(14-5)</div>
|
|
<div class="sb-team-score ne-color" id="score-ne">0</div>
|
|
</div>
|
|
<div class="sb-team-row">
|
|
<div class="sb-team-color" style="background:#69be28;"></div>
|
|
<div class="sb-poss-indicator"><div class="sb-poss-dot" id="sea-poss" style="background:transparent;"></div></div>
|
|
<div class="sb-team-logo-box"><canvas id="sea-logo" width="26" height="26"></canvas></div>
|
|
<div class="sb-team-abbr sea-color">SEA</div>
|
|
<div class="sb-team-record">(13-6)</div>
|
|
<div class="sb-team-score sea-color" id="score-sea">0</div>
|
|
</div>
|
|
</div>
|
|
<!-- Center info -->
|
|
<div class="sb-center-col">
|
|
<div class="sb-quarter" id="quarter">1ST</div>
|
|
<div class="sb-clock" id="clock">15:00</div>
|
|
<div class="sb-down-dist" id="down-dist">1st & 10</div>
|
|
<div class="sb-yard-line" id="ball-yard">NE 25</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="play-clock-badge">
|
|
<span class="pc-label">PLAY CLOCK</span>
|
|
<span class="pc-value" id="play-clock">40</span>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div id="stats-panel">
|
|
<div>Gaussians: <span class="st-val" id="g-count">0</span></div>
|
|
<div>FPS: <span class="st-val" id="fps-value">0</span></div>
|
|
<div>Camera: <span class="st-val" id="cam-mode">Broadcast</span></div>
|
|
<div>Coherence: <span class="st-val" id="coh-val">1.000</span></div>
|
|
</div>
|
|
|
|
<!-- Play-by-play ticker -->
|
|
<div id="play-ticker">
|
|
<div class="tk-entry"><span class="tk-time">Q1 15:00</span> <span class="tk-highlight">Super Bowl LX is underway!</span></div>
|
|
</div>
|
|
|
|
<!-- Big play overlay -->
|
|
<div id="big-play"></div>
|
|
<div id="replay-badge">REPLAY</div>
|
|
|
|
<!-- Network bug -->
|
|
<div id="network-bug">RUVECTOR VWM</div>
|
|
|
|
<!-- Tech description modal -->
|
|
<div id="tech-modal-backdrop">
|
|
<div id="tech-modal">
|
|
<button id="tech-close">×</button>
|
|
<div class="tech-badge">How It Works</div>
|
|
<div class="tech-title">4D Gaussian Splatting Engine</div>
|
|
|
|
<div class="tech-section">
|
|
<div class="tech-section-head">What You're Seeing</div>
|
|
<div class="tech-body">
|
|
Every object on screen — players, the football, the field, the crowd — is built from
|
|
<strong>thousands of 3D Gaussians</strong>: soft, translucent ellipsoids defined by a position,
|
|
covariance (shape), color, and opacity. Each player alone is composed of <strong>30+ overlapping
|
|
Gaussians</strong> (helmet, visor, shoulder pads, jersey, numbers, legs, cleats, shadows). The full
|
|
scene renders <strong>4,000+ Gaussians per frame</strong> at 60 fps.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section">
|
|
<div class="tech-section-head">4D = 3D + Time</div>
|
|
<div class="tech-body">
|
|
Standard 3D Gaussian Splatting reconstructs static scenes. This engine adds a
|
|
<strong>temporal dimension</strong>: each Gaussian carries keyframes that define how its position,
|
|
shape, color, and opacity evolve over time. Arm swing during a sprint, the spiral rotation of a
|
|
thrown football, crowd wave motion — all are encoded as <strong>continuous temporal
|
|
interpolations</strong> across Gaussian parameters rather than traditional skeletal animation.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section">
|
|
<div class="tech-section-head">Rendering Pipeline</div>
|
|
<div class="tech-body">
|
|
Each frame follows a strict pipeline:
|
|
</div>
|
|
<div class="tech-diagram">Scene Graph → View-Projection Matrix → Perspective Divide
|
|
↓
|
|
Depth Sort (back-to-front) → Alpha Composite
|
|
↓
|
|
Post-Processing: Fog → Vignette → Film Grain → Color Grade</div>
|
|
<div class="tech-body">
|
|
Gaussians are projected from world space into screen space using a <strong>perspective projection
|
|
matrix</strong>, then <strong>depth-sorted back-to-front</strong> for correct transparency. Each
|
|
Gaussian is drawn as a <strong>radial gradient</strong> (Canvas2D's closest approximation to a
|
|
true splatted ellipsoid). A post-processing stack applies <strong>atmospheric depth fog</strong>,
|
|
cinematic <strong>vignette</strong>, photographic <strong>film grain</strong>, and
|
|
<strong>warm color grading</strong>.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section">
|
|
<div class="tech-section-head">Three-Loop Architecture</div>
|
|
<div class="tech-diagram">FAST LOOP (~60 Hz) Render + physics + particle dynamics
|
|
MEDIUM LOOP (~10 Hz) Coherence gate + play state machine
|
|
SLOW LOOP (~1 Hz) Game governance + AI play-calling</div>
|
|
<div class="tech-body">
|
|
The <strong>fast loop</strong> handles rendering, camera interpolation, and particle physics every
|
|
frame. The <strong>medium loop</strong> runs a coherence gate that validates world-state
|
|
self-consistency — are all 22 players accounted for? Is the ball on the correct trajectory?
|
|
The <strong>slow loop</strong> governs high-level game logic: selecting formations, calling plays,
|
|
managing clock and score state.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section">
|
|
<div class="tech-section-head">Play Simulation Engine</div>
|
|
<div class="tech-body">
|
|
Plays are not scripted animations. The engine defines <strong>offensive formations</strong>
|
|
(Shotgun, I-Form, Spread) and <strong>defensive coverages</strong> (Nickel, Cover 3), then
|
|
simulates route-running, QB dropbacks, handoffs, and tackle detection using
|
|
<strong>distance-based collision</strong> and probabilistic outcomes. Each play generates unique
|
|
player trajectories, yardage, and game-state transitions.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section">
|
|
<div class="tech-section-head">Broadcast Presentation</div>
|
|
<div class="tech-body">
|
|
The HUD replicates an ESPN-style scorebug with real-time <strong>quarter, clock, down &
|
|
distance, possession, and play clock</strong>. Five camera modes (Broadcast, End Zone, SkyCam,
|
|
Follow Ball, Cinematic Dolly) use <strong>lerped position smoothing</strong> for fluid transitions.
|
|
Touchdown confetti, turf spray particles, slow-motion replay at <code>0.25x</code>, and big-play
|
|
overlays complete the broadcast experience.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section">
|
|
<div class="tech-section-head">Why Gaussians?</div>
|
|
<div class="tech-body">
|
|
Gaussian Splatting has emerged as a breakthrough in neural scene representation. Unlike mesh-based
|
|
rendering, Gaussians <strong>naturally handle transparency, soft edges, and volumetric
|
|
effects</strong> (fog, glow, motion blur) without expensive ray-marching. They compose additively,
|
|
making it trivial to layer complex appearances from simple primitives. This engine demonstrates
|
|
that even without GPU shaders, the Gaussian paradigm produces <strong>visually rich, physically
|
|
plausible scenes</strong> in real time on a standard Canvas2D surface.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-footer">
|
|
RuVector Visual World Model • 4D Gaussian Splatting • Canvas2D Renderer
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings modal -->
|
|
<div id="settings-backdrop">
|
|
<div id="settings-panel-modal">
|
|
<button id="settings-close">×</button>
|
|
<div class="st-header">
|
|
<span class="st-header-badge">Configuration</span>
|
|
<span class="st-header-title">Settings</span>
|
|
</div>
|
|
<div class="st-tabs">
|
|
<button class="st-tab active" data-tab="tab-visual">Visual</button>
|
|
<button class="st-tab" data-tab="tab-game">Game</button>
|
|
<button class="st-tab" data-tab="tab-ai">AI & Learning</button>
|
|
</div>
|
|
|
|
<!-- VISUAL TAB -->
|
|
<div class="st-tab-content active" id="tab-visual">
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Atmospheric Fog</div><div class="st-row-sub">Distance-based depth fade</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-fog" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Vignette</div><div class="st-row-sub">Cinematic edge darkening</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-vignette" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Film Grain</div><div class="st-row-sub">Photographic noise overlay</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-grain" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Warm Color Grade</div><div class="st-row-sub">Broadcast-style tint</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-colorgrade" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Particle Effects</div><div class="st-row-sub">Confetti & turf spray</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-particles" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Crowd Density</div><div class="st-row-sub">Gaussian count for spectators</div></div>
|
|
<div class="st-row-ctrl">
|
|
<input type="range" class="st-range" id="set-crowd" min="0" max="100" value="100">
|
|
<span class="st-range-val" id="set-crowd-val">100%</span>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Camera Smoothing</div><div class="st-row-sub">Transition interpolation factor</div></div>
|
|
<div class="st-row-ctrl">
|
|
<input type="range" class="st-range" id="set-camsmooth" min="1" max="20" value="4">
|
|
<span class="st-range-val" id="set-camsmooth-val">0.04</span>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Gaussian Quality</div><div class="st-row-sub">Min render size threshold</div></div>
|
|
<div class="st-row-ctrl">
|
|
<select class="st-select" id="set-quality">
|
|
<option value="0.15">Ultra</option>
|
|
<option value="0.25" selected>High</option>
|
|
<option value="0.5">Medium</option>
|
|
<option value="1.0">Low</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- GAME TAB -->
|
|
<div class="st-tab-content" id="tab-game">
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Game Speed</div><div class="st-row-sub">Simulation time multiplier</div></div>
|
|
<div class="st-row-ctrl">
|
|
<input type="range" class="st-range" id="set-gamespeed" min="1" max="40" value="10">
|
|
<span class="st-range-val" id="set-gamespeed-val">1.0x</span>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Auto-Play</div><div class="st-row-sub">Automatically call next play</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-autoplay" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Auto-Replay</div><div class="st-row-sub">Slow-motion on big plays</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-autoreplay" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Replay Speed</div><div class="st-row-sub">Slow-motion multiplier</div></div>
|
|
<div class="st-row-ctrl">
|
|
<input type="range" class="st-range" id="set-replayspeed" min="5" max="50" value="25">
|
|
<span class="st-range-val" id="set-replayspeed-val">0.25x</span>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Play-by-Play Feed</div><div class="st-row-sub">Show commentary ticker</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-ticker" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Scorebug</div><div class="st-row-sub">ESPN-style HUD overlay</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-scorebug" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Stats Overlay</div><div class="st-row-sub">Gaussian count, FPS, coherence</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-stats" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI & LEARNING TAB -->
|
|
<div class="st-tab-content" id="tab-ai">
|
|
<div class="st-ai-status">
|
|
<div class="st-ai-dot"></div>
|
|
<div class="st-ai-label"><strong>RuVector VWM</strong> — Active • 3 loops online</div>
|
|
</div>
|
|
|
|
<div class="st-metric-grid">
|
|
<div class="st-metric-card">
|
|
<div class="st-metric-label">Coherence Score</div>
|
|
<div class="st-metric-value" id="ai-coherence">1.000</div>
|
|
<div class="st-metric-sub">World-state consistency</div>
|
|
</div>
|
|
<div class="st-metric-card">
|
|
<div class="st-metric-label">Entities Tracked</div>
|
|
<div class="st-metric-value" id="ai-entities">26</div>
|
|
<div class="st-metric-sub">22 players + 4 refs</div>
|
|
</div>
|
|
<div class="st-metric-card">
|
|
<div class="st-metric-label">Plays Simulated</div>
|
|
<div class="st-metric-value" id="ai-plays">0</div>
|
|
<div class="st-metric-sub">Total game actions</div>
|
|
</div>
|
|
<div class="st-metric-card">
|
|
<div class="st-metric-label">Gaussians / Frame</div>
|
|
<div class="st-metric-value" id="ai-gaussians">0</div>
|
|
<div class="st-metric-sub">Active splat count</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section-head" style="margin:14px 0 8px;">Intelligence Pipeline</div>
|
|
<div class="st-pipeline">
|
|
<span class="st-pipe-stage">RETRIEVE</span><span class="st-pipe-arrow">→</span>
|
|
<span class="st-pipe-stage">JUDGE</span><span class="st-pipe-arrow">→</span>
|
|
<span class="st-pipe-stage">DISTILL</span><span class="st-pipe-arrow">→</span>
|
|
<span class="st-pipe-stage">CONSOLIDATE</span>
|
|
</div>
|
|
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Coherence Gate</div><div class="st-row-sub">Validates world-state self-consistency at ~10 Hz</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-coherence" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Coherence Threshold</div><div class="st-row-sub">Min acceptable consistency score</div></div>
|
|
<div class="st-row-ctrl">
|
|
<input type="range" class="st-range" id="set-cohthreshold" min="80" max="100" value="95">
|
|
<span class="st-range-val" id="set-cohthreshold-val">0.95</span>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Play-Calling AI</div><div class="st-row-sub">Formation and route selection strategy</div></div>
|
|
<div class="st-row-ctrl">
|
|
<select class="st-select" id="set-playcall">
|
|
<option value="balanced" selected>Balanced</option>
|
|
<option value="aggressive">Aggressive</option>
|
|
<option value="conservative">Conservative</option>
|
|
<option value="random">Random</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Attention Allocation</div><div class="st-row-sub">Compute budget per entity per frame</div></div>
|
|
<div class="st-row-ctrl">
|
|
<select class="st-select" id="set-attention">
|
|
<option value="adaptive" selected>Adaptive</option>
|
|
<option value="uniform">Uniform</option>
|
|
<option value="ball-focus">Ball Focus</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Temporal Learning</div><div class="st-row-sub">4D keyframe interpolation method</div></div>
|
|
<div class="st-row-ctrl">
|
|
<select class="st-select" id="set-temporal">
|
|
<option value="continuous" selected>Continuous</option>
|
|
<option value="discrete">Discrete</option>
|
|
<option value="predictive">Predictive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Neural Substrate</div><div class="st-row-sub">SONA self-optimizing adaptation</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-neural" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">EWC++ Memory Guard</div><div class="st-row-sub">Prevent catastrophic forgetting</div></div>
|
|
<label class="st-toggle"><input type="checkbox" id="set-ewc" checked><span class="st-toggle-track"></span><span class="st-toggle-thumb"></span></label>
|
|
</div>
|
|
|
|
<div class="tech-section-head" style="margin:14px 0 8px;">Three-Loop Architecture</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Fast Loop</div><div class="st-row-sub">Render + physics (~60 Hz)</div></div>
|
|
<div class="st-row-ctrl"><span class="st-range-val" id="ai-fast-hz">60 Hz</span></div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Medium Loop</div><div class="st-row-sub">Coherence gate (~10 Hz)</div></div>
|
|
<div class="st-row-ctrl"><span class="st-range-val" id="ai-med-hz">10 Hz</span></div>
|
|
</div>
|
|
<div class="st-row">
|
|
<div><div class="st-row-label">Slow Loop</div><div class="st-row-sub">Game governance (~1 Hz)</div></div>
|
|
<div class="st-row-ctrl"><span class="st-range-val" id="ai-slow-hz">1 Hz</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transport -->
|
|
<div id="transport">
|
|
<button class="tb" id="play-btn">▮▮</button>
|
|
<div class="sep"></div>
|
|
<button class="tb" id="cam-broadcast">Broadcast</button>
|
|
<button class="tb" id="cam-endzone">End Zone</button>
|
|
<button class="tb" id="cam-skycam">SkyCam</button>
|
|
<button class="tb" id="cam-follow">Follow</button>
|
|
<button class="tb" id="cam-cinematic">Cinematic</button>
|
|
<div class="sep"></div>
|
|
<button class="tb" id="btn-replay">Replay</button>
|
|
<div class="sep"></div>
|
|
<button class="tb" id="btn-tech">Tech</button>
|
|
<button class="tb" id="btn-settings">Settings</button>
|
|
<div class="sep"></div>
|
|
<input type="range" id="time-slider" min="0" max="1000" value="0" />
|
|
<span id="time-label">t=0.000</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ============================================================
|
|
// RuVector VWM - Super Bowl LX: Patriots vs Seahawks
|
|
// HYPER-REALISTIC 4D Gaussian Splatting (Canvas2D)
|
|
// ============================================================
|
|
|
|
const canvas = document.getElementById('c');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// === TEAMS ===
|
|
const TEAMS = {
|
|
NE: {
|
|
name:'Patriots', abbr:'NE',
|
|
helmet:'#b0b7bc', helmetStripe:'#c60c30', facemask:'#b0b7bc',
|
|
jersey:'#002244', jerseyAccent:'#c60c30', pants:'#002244',
|
|
numberColor:'#c60c30', socks:'#c60c30', visor:'#1a1a3a',
|
|
// Specular highlight color for helmet
|
|
specular:'#e8e8f0',
|
|
},
|
|
SEA: {
|
|
name:'Seahawks', abbr:'SEA',
|
|
helmet:'#002244', helmetStripe:'#69be28', facemask:'#b0b7bc',
|
|
jersey:'#002244', jerseyAccent:'#69be28', pants:'#a5acaf',
|
|
numberColor:'#69be28', socks:'#002244', visor:'#1a3a1a',
|
|
specular:'#4488aa',
|
|
}
|
|
};
|
|
|
|
const hex2rgb = h => { const n=parseInt(h.slice(1),16); return [(n>>16&255)/255,(n>>8&255)/255,(n&255)/255]; };
|
|
|
|
// === FIELD ===
|
|
const FIELD = { length:120, width:53.33, endZone:10 };
|
|
|
|
// === GAME STATE ===
|
|
const game = {
|
|
quarter:1, clock:900, playClock:40,
|
|
scoreNE:0, scoreSEA:0,
|
|
down:1, yardsToGo:10, lineOfScrimmage:25,
|
|
possession:'NE', playInProgress:false, playPhase:'huddle',
|
|
playTimer:0, driveYards:0, totalPlays:0,
|
|
};
|
|
|
|
// === CAMERA (smoothed) ===
|
|
const camera = {
|
|
mode:'broadcast',
|
|
theta:0, phi:0.35, radius:60,
|
|
target:[60,0,26.67],
|
|
fov:Math.PI/4.5, near:0.1, far:350,
|
|
// Smoothed actual values
|
|
eyeSmooth:[0,14,35], targetSmooth:[26.67,0,35],
|
|
cinematicAngle: 0,
|
|
};
|
|
|
|
// === RENDER STATE ===
|
|
let gaussians = [];
|
|
let totalGaussianCount = 0;
|
|
let playing = true;
|
|
let globalTime = 0;
|
|
let lastFrameTime = 0;
|
|
let frameTimes = [];
|
|
|
|
// === PARTICLES (confetti, turf spray, breath) ===
|
|
let particles = [];
|
|
|
|
// === REPLAY ===
|
|
let replayMode = false;
|
|
let replaySpeed = 0.25;
|
|
let replayTimer = 0;
|
|
|
|
// === SETTINGS ===
|
|
const settings = {
|
|
fog: true, vignette: true, grain: true, colorGrade: true, particles: true,
|
|
crowdDensity: 1.0, camSmooth: 0.04, qualityThreshold: 0.25,
|
|
gameSpeed: 1.0, autoPlay: true, autoReplay: true, replaySpeedMul: 0.25,
|
|
showTicker: true, showScorebug: true, showStats: true,
|
|
coherenceGate: true, coherenceThreshold: 0.95,
|
|
playCalling: 'balanced', attention: 'adaptive', temporal: 'continuous',
|
|
neural: true, ewc: true,
|
|
};
|
|
|
|
// === SEED FOR DETERMINISTIC CROWD ===
|
|
function seededRandom(seed) {
|
|
let s = seed;
|
|
return () => { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; };
|
|
}
|
|
const crowdRng = seededRandom(42);
|
|
|
|
// ============================================================
|
|
// LOGOS (mini canvas)
|
|
// ============================================================
|
|
function drawPatriotsLogo(cvs) {
|
|
const c=cvs.getContext('2d'); c.clearRect(0,0,26,26);
|
|
// Simplified shield
|
|
c.fillStyle='#002244';
|
|
c.beginPath(); c.moveTo(13,2); c.lineTo(24,8); c.lineTo(22,22); c.lineTo(13,24); c.lineTo(4,22); c.lineTo(2,8); c.closePath(); c.fill();
|
|
c.fillStyle='#c60c30'; c.fillRect(6,8,14,3);
|
|
c.fillStyle='#b0b7bc'; c.fillRect(6,12,14,3);
|
|
c.fillStyle='#fff'; c.fillRect(6,16,14,3);
|
|
c.fillStyle='#fff'; c.font='bold 5px Oswald,sans-serif'; c.textAlign='center'; c.fillText('NE',13,8);
|
|
}
|
|
function drawSeahawksLogo(cvs) {
|
|
const c=cvs.getContext('2d'); c.clearRect(0,0,26,26);
|
|
c.fillStyle='#002244';
|
|
c.beginPath(); c.moveTo(13,2); c.lineTo(24,8); c.lineTo(22,22); c.lineTo(13,24); c.lineTo(4,22); c.lineTo(2,8); c.closePath(); c.fill();
|
|
c.fillStyle='#69be28'; c.beginPath(); c.arc(13,12,6,0,Math.PI*2); c.fill();
|
|
c.fillStyle='#002244'; c.beginPath(); c.arc(13,12,3,0,Math.PI*2); c.fill();
|
|
c.fillStyle='#fff'; c.beginPath(); c.arc(12,11,1,0,Math.PI*2); c.fill();
|
|
c.fillStyle='#69be28'; c.font='bold 4px Oswald,sans-serif'; c.textAlign='center'; c.fillText('SEA',13,22);
|
|
}
|
|
|
|
// ============================================================
|
|
// FORMATIONS
|
|
// ============================================================
|
|
const FORMATIONS = {
|
|
offense: {
|
|
shotgun: [
|
|
{pos:'QB',x:0,z:-5,num:10},{pos:'RB',x:-2,z:-7,num:28},
|
|
{pos:'WR',x:-22,z:0,num:81},{pos:'WR',x:22,z:0,num:14},{pos:'WR',x:-18,z:-2,num:11},
|
|
{pos:'TE',x:6,z:0.5,num:87},
|
|
{pos:'LT',x:-4,z:0,num:77},{pos:'LG',x:-2,z:0,num:62},{pos:'C',x:0,z:0,num:60},
|
|
{pos:'RG',x:2,z:0,num:64},{pos:'RT',x:4,z:0,num:72},
|
|
],
|
|
spread: [
|
|
{pos:'QB',x:0,z:-5,num:10},{pos:'RB',x:2,z:-6,num:28},
|
|
{pos:'WR',x:-24,z:0,num:81},{pos:'WR',x:24,z:0,num:14},
|
|
{pos:'WR',x:-16,z:-1,num:11},{pos:'WR',x:16,z:-1,num:15},
|
|
{pos:'LT',x:-4,z:0,num:77},{pos:'LG',x:-2,z:0,num:62},{pos:'C',x:0,z:0,num:60},
|
|
{pos:'RG',x:2,z:0,num:64},{pos:'RT',x:4,z:0,num:72},
|
|
],
|
|
iform: [
|
|
{pos:'QB',x:0,z:-2,num:10},{pos:'FB',x:0,z:-4,num:44},{pos:'RB',x:0,z:-6,num:28},
|
|
{pos:'WR',x:-22,z:0,num:81},{pos:'WR',x:22,z:0,num:14},
|
|
{pos:'TE',x:6,z:0.5,num:87},
|
|
{pos:'LT',x:-4,z:0,num:77},{pos:'LG',x:-2,z:0,num:62},{pos:'C',x:0,z:0,num:60},
|
|
{pos:'RG',x:2,z:0,num:64},{pos:'RT',x:4,z:0,num:72},
|
|
],
|
|
},
|
|
defense: {
|
|
nickel: [
|
|
{pos:'DE',x:-5,z:1,num:99},{pos:'DT',x:-1.5,z:1,num:93},
|
|
{pos:'DT',x:1.5,z:1,num:97},{pos:'DE',x:5,z:1,num:91},
|
|
{pos:'MLB',x:0,z:4,num:54},{pos:'OLB',x:-8,z:3,num:50},
|
|
{pos:'CB',x:-22,z:3,num:24},{pos:'CB',x:22,z:3,num:21},
|
|
{pos:'NCB',x:-14,z:5,num:26},{pos:'FS',x:-5,z:12,num:32},{pos:'SS',x:5,z:8,num:33},
|
|
],
|
|
cover3: [
|
|
{pos:'DE',x:-5,z:1,num:99},{pos:'DT',x:-1.5,z:1,num:93},
|
|
{pos:'DT',x:1.5,z:1,num:97},{pos:'DE',x:5,z:1,num:91},
|
|
{pos:'WLB',x:-10,z:4,num:50},{pos:'MLB',x:0,z:4,num:54},{pos:'SLB',x:10,z:4,num:55},
|
|
{pos:'CB',x:-22,z:6,num:24},{pos:'CB',x:22,z:6,num:21},
|
|
{pos:'FS',x:0,z:16,num:32},{pos:'SS',x:8,z:10,num:33},
|
|
],
|
|
}
|
|
};
|
|
|
|
// ============================================================
|
|
// GAUSSIAN BUILDERS
|
|
// ============================================================
|
|
|
|
function buildField() {
|
|
const gs = [];
|
|
// High-detail grass with mowing pattern
|
|
for (let z = 0; z < FIELD.length; z += 1.5) {
|
|
for (let x = 0; x < FIELD.width; x += 1.5) {
|
|
const onEZ = z < FIELD.endZone || z >= FIELD.length - FIELD.endZone;
|
|
let r, g, b;
|
|
if (onEZ) {
|
|
if (z < FIELD.endZone) {
|
|
// Patriots end zone - dark navy
|
|
r = 0.0; g = 0.08; b = 0.16;
|
|
} else {
|
|
// Seahawks end zone - dark navy
|
|
r = 0.0; g = 0.1; b = 0.16;
|
|
}
|
|
} else {
|
|
const yd = z - FIELD.endZone;
|
|
const stripe = Math.floor(yd / 5) % 2;
|
|
// More realistic grass with micro-variation
|
|
const noise = Math.sin(x * 3.7 + z * 2.1) * 0.015 + Math.sin(x * 7.3 - z * 4.9) * 0.008;
|
|
r = 0.06 + stripe * 0.025 + noise;
|
|
g = 0.32 + stripe * 0.05 + noise * 2;
|
|
b = 0.05 + stripe * 0.01 + noise * 0.5;
|
|
}
|
|
gs.push({ x, y:0, z, sx:1.0, sy:0.03, sz:1.0, r, g, b, a:1, type:'field' });
|
|
}
|
|
}
|
|
return gs;
|
|
}
|
|
|
|
function buildFieldMarkings() {
|
|
const gs = [];
|
|
const cx = FIELD.width / 2;
|
|
|
|
// Yard lines every 5 yards (crisp white)
|
|
for (let yd = 0; yd <= 100; yd += 5) {
|
|
const z = FIELD.endZone + yd;
|
|
for (let x = 1; x < FIELD.width - 1; x += 1) {
|
|
gs.push({ x, y:0.008, z, sx:0.55, sy:0.008, sz:0.06, r:1, g:1, b:1, a:0.92, type:'marking' });
|
|
}
|
|
// Yard numbers (both sides)
|
|
if (yd % 10 === 0 && yd > 0 && yd < 100) {
|
|
const dn = yd <= 50 ? yd : 100 - yd;
|
|
gs.push({ x:7, y:0.01, z, sx:1.8, sy:0.008, sz:1.5, r:1, g:1, b:1, a:0.45, type:'number', label:dn });
|
|
gs.push({ x:FIELD.width-7, y:0.01, z, sx:1.8, sy:0.008, sz:1.5, r:1, g:1, b:1, a:0.45, type:'number', label:dn });
|
|
}
|
|
}
|
|
|
|
// Hash marks every yard
|
|
for (let yd = 0; yd <= 100; yd++) {
|
|
const z = FIELD.endZone + yd;
|
|
gs.push({ x:cx-6, y:0.008, z, sx:0.25, sy:0.008, sz:0.04, r:1, g:1, b:1, a:0.55, type:'marking' });
|
|
gs.push({ x:cx+6, y:0.008, z, sx:0.25, sy:0.008, sz:0.04, r:1, g:1, b:1, a:0.55, type:'marking' });
|
|
// Sideline hashes
|
|
gs.push({ x:1.5, y:0.008, z, sx:0.2, sy:0.008, sz:0.04, r:1, g:1, b:1, a:0.4, type:'marking' });
|
|
gs.push({ x:FIELD.width-1.5, y:0.008, z, sx:0.2, sy:0.008, sz:0.04, r:1, g:1, b:1, a:0.4, type:'marking' });
|
|
}
|
|
|
|
// End zone text blocks
|
|
const patCol = hex2rgb('#c60c30');
|
|
const seaCol = hex2rgb('#69be28');
|
|
for (let i = 0; i < 10; i++) {
|
|
gs.push({ x:7+i*4, y:0.015, z:5, sx:1.6, sy:0.008, sz:1.8, r:patCol[0], g:patCol[1], b:patCol[2], a:0.65, type:'ez-text' });
|
|
gs.push({ x:7+i*4, y:0.015, z:115, sx:1.6, sy:0.008, sz:1.8, r:seaCol[0], g:seaCol[1], b:seaCol[2], a:0.65, type:'ez-text' });
|
|
}
|
|
|
|
// Sidelines
|
|
for (let z = 0; z < FIELD.length; z += 0.8) {
|
|
gs.push({ x:0.2, y:0.008, z, sx:0.12, sy:0.008, sz:0.45, r:1, g:1, b:1, a:0.95, type:'marking' });
|
|
gs.push({ x:FIELD.width-0.2, y:0.008, z, sx:0.12, sy:0.008, sz:0.45, r:1, g:1, b:1, a:0.95, type:'marking' });
|
|
}
|
|
|
|
// NFL Shield at midfield
|
|
for (let dx = -2; dx <= 2; dx += 0.8) {
|
|
for (let dz = -2; dz <= 2; dz += 0.8) {
|
|
const dist = Math.sqrt(dx*dx+dz*dz);
|
|
if (dist < 2.5) {
|
|
gs.push({ x:cx+dx, y:0.012, z:FIELD.length/2+dz, sx:0.5, sy:0.008, sz:0.5,
|
|
r:0.13, g:0.2, b:0.4, a:0.35*(1-dist/3), type:'logo' });
|
|
}
|
|
}
|
|
}
|
|
|
|
return gs;
|
|
}
|
|
|
|
function buildGoalposts() {
|
|
const gs = [];
|
|
const cx = FIELD.width / 2;
|
|
for (const z of [0, FIELD.length]) {
|
|
// Base
|
|
for (let h = 0; h < 3; h += 0.3) {
|
|
gs.push({ x:cx, y:h, z, sx:0.18, sy:0.2, sz:0.18, r:0.85, g:0.75, b:0.15, a:1, type:'post' });
|
|
}
|
|
// Crossbar
|
|
for (let xo = -2.8; xo <= 2.8; xo += 0.35) {
|
|
gs.push({ x:cx+xo, y:3, z, sx:0.2, sy:0.1, sz:0.1, r:0.9, g:0.8, b:0.18, a:1, type:'post' });
|
|
}
|
|
// Uprights
|
|
for (let h = 3; h < 11; h += 0.35) {
|
|
gs.push({ x:cx-2.8, y:h, z, sx:0.1, sy:0.22, sz:0.1, r:0.9, g:0.8, b:0.18, a:1, type:'post' });
|
|
gs.push({ x:cx+2.8, y:h, z, sx:0.1, sy:0.22, sz:0.1, r:0.9, g:0.8, b:0.18, a:1, type:'post' });
|
|
}
|
|
}
|
|
return gs;
|
|
}
|
|
|
|
function buildPlayer(bx, bz, team, num, pos, isOff, anim) {
|
|
const gs = [];
|
|
const t = TEAMS[team];
|
|
const [hr,hg,hb] = hex2rgb(t.helmet);
|
|
const [hsr,hsg,hsb] = hex2rgb(t.helmetStripe);
|
|
const [fmr,fmg,fmb] = hex2rgb(t.facemask);
|
|
const [jr,jg,jb] = hex2rgb(t.jersey);
|
|
const [jar,jag,jab] = hex2rgb(t.jerseyAccent);
|
|
const [pr,pg,pb] = hex2rgb(t.pants);
|
|
const [nr,ng,nb] = hex2rgb(t.numberColor);
|
|
const [spr,spg,spb] = hex2rgb(t.specular);
|
|
const [skr,skg,skb] = hex2rgb(t.socks);
|
|
|
|
const isLine = ['LT','LG','C','RG','RT','DE','DT'].includes(pos);
|
|
const isLB = ['MLB','WLB','SLB','OLB'].includes(pos);
|
|
const sc = isLine ? 1.35 : (isLB ? 1.15 : 1.0);
|
|
const ht = isLine ? 1.05 : (isLB ? 1.0 : 0.92);
|
|
|
|
const breathe = Math.sin(globalTime*3+num)*0.015;
|
|
const runCycle = anim?.moving ? Math.sin(globalTime*8+num*1.3)*0.08 : 0;
|
|
const sway = anim?.moving ? Math.sin(globalTime*6+num)*0.06 : Math.sin(globalTime*1.2+num*0.7)*0.02;
|
|
|
|
const px = bx + sway;
|
|
const pz = bz;
|
|
|
|
// === HELMET ===
|
|
// Main shell
|
|
gs.push({ x:px, y:ht*1.85+breathe, z:pz, sx:0.38*sc, sy:0.32, sz:0.36*sc, r:hr, g:hg, b:hb, a:1, type:'player' });
|
|
// Helmet stripe (center)
|
|
gs.push({ x:px, y:ht*1.92+breathe, z:pz, sx:0.06, sy:0.12, sz:0.38*sc, r:hsr, g:hsg, b:hsb, a:0.9, type:'player' });
|
|
// Specular highlight (glossy reflection)
|
|
gs.push({ x:px+0.08, y:ht*1.95+breathe, z:pz-0.05, sx:0.12, sy:0.08, sz:0.12, r:spr, g:spg, b:spb, a:0.35, type:'spec' });
|
|
// Facemask
|
|
gs.push({ x:px, y:ht*1.72+breathe, z:pz+0.22*(isOff?-1:1), sx:0.22, sy:0.18, sz:0.08, r:fmr, g:fmg, b:fmb, a:0.85, type:'player' });
|
|
// Visor
|
|
const [vr,vg,vb] = hex2rgb(t.visor);
|
|
gs.push({ x:px, y:ht*1.78+breathe, z:pz+0.18*(isOff?-1:1), sx:0.2, sy:0.06, sz:0.04, r:vr, g:vg, b:vb, a:0.5, type:'player' });
|
|
|
|
// === SHOULDER PADS ===
|
|
gs.push({ x:px-0.4*sc, y:ht*1.48, z:pz, sx:0.22, sy:0.18, sz:0.28, r:jr*1.15, g:jg*1.15, b:jb*1.15, a:0.95, type:'player' });
|
|
gs.push({ x:px+0.4*sc, y:ht*1.48, z:pz, sx:0.22, sy:0.18, sz:0.28, r:jr*1.15, g:jg*1.15, b:jb*1.15, a:0.95, type:'player' });
|
|
|
|
// === JERSEY (TORSO) ===
|
|
gs.push({ x:px, y:ht*1.22+breathe*0.3, z:pz, sx:0.52*sc, sy:0.48, sz:0.38*sc, r:jr, g:jg, b:jb, a:1, type:'player' });
|
|
// Jersey accent stripes (sleeves)
|
|
gs.push({ x:px-0.42*sc, y:ht*1.3, z:pz, sx:0.08, sy:0.15, sz:0.18, r:jar, g:jag, b:jab, a:0.8, type:'player' });
|
|
gs.push({ x:px+0.42*sc, y:ht*1.3, z:pz, sx:0.08, sy:0.15, sz:0.18, r:jar, g:jag, b:jab, a:0.8, type:'player' });
|
|
// Number (front)
|
|
gs.push({ x:px, y:ht*1.28, z:pz+0.18*(isOff?-1:1), sx:0.18, sy:0.18, sz:0.04, r:nr, g:ng, b:nb, a:0.85, type:'num' });
|
|
// Number (back)
|
|
gs.push({ x:px, y:ht*1.28, z:pz-0.18*(isOff?-1:1), sx:0.22, sy:0.22, sz:0.04, r:nr, g:ng, b:nb, a:0.7, type:'num' });
|
|
|
|
// === ARMS ===
|
|
const armSwing = runCycle * 0.5;
|
|
// Left arm
|
|
gs.push({ x:px-0.5*sc, y:ht*1.1+armSwing, z:pz, sx:0.1, sy:0.3, sz:0.1, r:jr, g:jg, b:jb, a:0.95, type:'player' });
|
|
// Left hand
|
|
gs.push({ x:px-0.52*sc, y:ht*0.82+armSwing, z:pz, sx:0.08, sy:0.08, sz:0.08, r:0.7, g:0.55, b:0.42, a:1, type:'player' });
|
|
// Right arm
|
|
gs.push({ x:px+0.5*sc, y:ht*1.1-armSwing, z:pz, sx:0.1, sy:0.3, sz:0.1, r:jr, g:jg, b:jb, a:0.95, type:'player' });
|
|
// Right hand
|
|
gs.push({ x:px+0.52*sc, y:ht*0.82-armSwing, z:pz, sx:0.08, sy:0.08, sz:0.08, r:0.7, g:0.55, b:0.42, a:1, type:'player' });
|
|
|
|
// === PANTS ===
|
|
gs.push({ x:px, y:ht*0.62, z:pz, sx:0.38*sc, sy:0.35, sz:0.3*sc, r:pr, g:pg, b:pb, a:1, type:'player' });
|
|
// Knee pads
|
|
gs.push({ x:px-0.12, y:ht*0.42, z:pz+0.08, sx:0.08, sy:0.06, sz:0.06, r:pr*1.2, g:pg*1.2, b:pb*1.2, a:0.8, type:'player' });
|
|
gs.push({ x:px+0.12, y:ht*0.42, z:pz+0.08, sx:0.08, sy:0.06, sz:0.06, r:pr*1.2, g:pg*1.2, b:pb*1.2, a:0.8, type:'player' });
|
|
|
|
// === LEGS (with run cycle) ===
|
|
const legFwd = runCycle;
|
|
gs.push({ x:px-0.14, y:ht*0.22+Math.abs(legFwd)*0.05, z:pz+legFwd*0.3, sx:0.1, sy:0.22, sz:0.1, r:pr, g:pg, b:pb, a:0.95, type:'player' });
|
|
gs.push({ x:px+0.14, y:ht*0.22+Math.abs(legFwd)*0.05, z:pz-legFwd*0.3, sx:0.1, sy:0.22, sz:0.1, r:pr, g:pg, b:pb, a:0.95, type:'player' });
|
|
|
|
// === SOCKS & CLEATS ===
|
|
gs.push({ x:px-0.14, y:ht*0.1, z:pz+legFwd*0.3, sx:0.08, sy:0.1, sz:0.08, r:skr, g:skg, b:skb, a:0.9, type:'player' });
|
|
gs.push({ x:px+0.14, y:ht*0.1, z:pz-legFwd*0.3, sx:0.08, sy:0.1, sz:0.08, r:skr, g:skg, b:skb, a:0.9, type:'player' });
|
|
gs.push({ x:px-0.14, y:0.04, z:pz+legFwd*0.3, sx:0.08, sy:0.04, sz:0.12, r:0.05, g:0.05, b:0.05, a:1, type:'player' });
|
|
gs.push({ x:px+0.14, y:0.04, z:pz-legFwd*0.3, sx:0.08, sy:0.04, sz:0.12, r:0.05, g:0.05, b:0.05, a:1, type:'player' });
|
|
|
|
// === SHADOW (contact + soft) ===
|
|
gs.push({ x:px, y:0.003, z:pz, sx:0.35*sc, sy:0.005, sz:0.3*sc, r:0, g:0, b:0, a:0.45, type:'shadow' });
|
|
gs.push({ x:px, y:0.002, z:pz, sx:0.6*sc, sy:0.005, sz:0.5*sc, r:0, g:0, b:0, a:0.15, type:'shadow' });
|
|
|
|
return gs;
|
|
}
|
|
|
|
function buildFootball(x, y, z, inFlight) {
|
|
const gs = [];
|
|
gs.push({ x, y, z, sx:0.16, sy:0.1, sz:0.32, r:0.42, g:0.23, b:0.08, a:1, type:'ball' });
|
|
gs.push({ x, y:y+0.055, z, sx:0.025, sy:0.015, sz:0.18, r:1, g:1, b:1, a:0.9, type:'ball' });
|
|
// Pointed tips
|
|
gs.push({ x, y, z:z-0.22, sx:0.07, sy:0.05, sz:0.1, r:0.38, g:0.2, b:0.06, a:1, type:'ball' });
|
|
gs.push({ x, y, z:z+0.22, sx:0.07, sy:0.05, sz:0.1, r:0.38, g:0.2, b:0.06, a:1, type:'ball' });
|
|
// Seam line
|
|
gs.push({ x, y, z, sx:0.02, sy:0.11, sz:0.3, r:1, g:1, b:1, a:0.15, type:'ball' });
|
|
if (inFlight) {
|
|
// Spiral glow
|
|
gs.push({ x, y, z, sx:0.4, sy:0.25, sz:0.6, r:1, g:0.92, b:0.55, a:0.12, type:'glow' });
|
|
// Motion trail
|
|
for (let i = 1; i <= 3; i++) {
|
|
const trail = i * 0.3;
|
|
gs.push({ x:x, y:y-trail*0.5, z:z-(ballState.targetZ>ballState.startZ?1:-1)*trail,
|
|
sx:0.1+i*0.02, sy:0.06, sz:0.2, r:0.42, g:0.23, b:0.08, a:0.15/i, type:'trail' });
|
|
}
|
|
}
|
|
// Shadow
|
|
const shadowScale = 1 + y * 0.04;
|
|
gs.push({ x, y:0.003, z, sx:0.15*shadowScale, sy:0.005, sz:0.25*shadowScale,
|
|
r:0, g:0, b:0, a:Math.max(0.05, 0.4-y*0.025), type:'shadow' });
|
|
return gs;
|
|
}
|
|
|
|
function buildStadium() {
|
|
const gs = [];
|
|
|
|
// === CROWD (seeded for consistency, animated) ===
|
|
const rng = seededRandom(42);
|
|
const crowdPalette = {
|
|
NE: [[0.78,0.05,0.19],[0,0.13,0.27],[0.69,0.72,0.74],[1,1,1],[0.85,0.8,0.15]],
|
|
SEA: [[0.41,0.74,0.16],[0,0.13,0.27],[0.65,0.67,0.69],[1,1,1],[0.85,0.8,0.15]],
|
|
};
|
|
|
|
// Side stands (deeper, 8 rows)
|
|
for (let side = 0; side < 2; side++) {
|
|
const palette = side === 0 ? crowdPalette.NE : crowdPalette.SEA;
|
|
for (let row = 0; row < 8; row++) {
|
|
for (let z = -2; z < FIELD.length + 2; z += 1.8) {
|
|
const cc = palette[Math.floor(rng() * palette.length)];
|
|
if (rng() > settings.crowdDensity) continue;
|
|
const wave = Math.sin(globalTime * 2.5 + z * 0.15 + row * 0.3) * 0.15;
|
|
const flash = rng() < 0.02 ? 0.3 : 0; // Phone flashlights
|
|
const bx = side === 0 ? -4 - row * 2.2 : FIELD.width + 4 + row * 2.2;
|
|
const by = 2.5 + row * 2.2;
|
|
gs.push({
|
|
x: bx, y: by + wave, z,
|
|
sx: 0.55, sy: 0.65, sz: 0.7,
|
|
r: Math.min(1, cc[0] + flash), g: Math.min(1, cc[1] + flash), b: Math.min(1, cc[2] + flash),
|
|
a: 0.65, type: 'crowd',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// End zone stands
|
|
for (const ezSide of [0, 1]) {
|
|
const palette = ezSide === 0 ? crowdPalette.NE : crowdPalette.SEA;
|
|
for (let row = 0; row < 5; row++) {
|
|
for (let x = -2; x < FIELD.width + 2; x += 2) {
|
|
const cc = palette[Math.floor(rng() * palette.length)];
|
|
if (rng() > settings.crowdDensity) continue;
|
|
const zBase = ezSide === 0 ? -4 - row * 2 : FIELD.length + 4 + row * 2;
|
|
gs.push({
|
|
x, y: 2.5 + row * 2.2, z: zBase,
|
|
sx: 0.55, sy: 0.65, sz: 0.55,
|
|
r: cc[0], g: cc[1], b: cc[2], a: 0.55, type: 'crowd',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// === STADIUM STRUCTURE (upper deck rim) ===
|
|
for (let side = 0; side < 2; side++) {
|
|
for (let z = -8; z < FIELD.length + 8; z += 3) {
|
|
const bx = side === 0 ? -24 : FIELD.width + 24;
|
|
gs.push({ x:bx, y:22, z, sx:1.5, sy:1.2, sz:1.8, r:0.12, g:0.12, b:0.14, a:0.7, type:'structure' });
|
|
}
|
|
}
|
|
|
|
// === STADIUM LIGHTS (8 clusters for even coverage) ===
|
|
const lightPos = [
|
|
[-18, 15], [FIELD.width+18, 15], [-18, FIELD.length-15], [FIELD.width+18, FIELD.length-15],
|
|
[-18, FIELD.length/2], [FIELD.width+18, FIELD.length/2],
|
|
[FIELD.width/2, -12], [FIELD.width/2, FIELD.length+12],
|
|
];
|
|
for (const [lx, lz] of lightPos) {
|
|
// Tower
|
|
for (let h = 0; h < 28; h += 1.5) {
|
|
gs.push({ x:lx, y:h, z:lz, sx:0.35, sy:0.9, sz:0.35, r:0.2, g:0.2, b:0.22, a:0.75, type:'structure' });
|
|
}
|
|
// Light cluster
|
|
gs.push({ x:lx, y:28, z:lz, sx:2.5, sy:1, sz:2.5, r:1, g:0.97, b:0.88, a:0.5, type:'light' });
|
|
// Light cone (volumetric feel)
|
|
gs.push({ x:(lx+FIELD.width/2)/2, y:14, z:(lz+FIELD.length/2)/2,
|
|
sx:12, sy:10, sz:12, r:1, g:0.98, b:0.93, a:0.018, type:'lightcone' });
|
|
}
|
|
|
|
// === SIDELINE PERSONNEL ===
|
|
// Coaches, cameramen, chain crew
|
|
const losZ = FIELD.endZone + game.lineOfScrimmage;
|
|
for (let z = losZ - 15; z < losZ + 15; z += 3) {
|
|
gs.push({ x:-2.5, y:0.9, z, sx:0.25, sy:0.45, sz:0.2, r:0.15, g:0.15, b:0.15, a:0.6, type:'sideline' });
|
|
gs.push({ x:FIELD.width+2.5, y:0.9, z, sx:0.25, sy:0.45, sz:0.2, r:0.15, g:0.15, b:0.15, a:0.6, type:'sideline' });
|
|
}
|
|
|
|
// Down marker
|
|
gs.push({ x:-2.5, y:1.2, z:losZ, sx:0.15, sy:0.7, sz:0.15, r:1, g:0.5, b:0, a:0.95, type:'marker' });
|
|
// First down marker
|
|
const fdZ = losZ + game.yardsToGo * (game.possession === 'NE' ? 1 : -1);
|
|
gs.push({ x:-2.5, y:1.2, z:fdZ, sx:0.15, sy:0.7, sz:0.15, r:1, g:0.85, b:0, a:0.95, type:'marker' });
|
|
|
|
return gs;
|
|
}
|
|
|
|
function buildOverlayLines() {
|
|
const gs = [];
|
|
const losZ = FIELD.endZone + game.lineOfScrimmage;
|
|
const fdZ = losZ + game.yardsToGo * (game.possession === 'NE' ? 1 : -1);
|
|
|
|
// LOS (blue) - thicker and more visible
|
|
for (let x = 0; x < FIELD.width; x += 0.8) {
|
|
gs.push({ x, y:0.015, z:losZ, sx:0.45, sy:0.008, sz:0.1, r:0.15, g:0.35, b:0.95, a:0.55, type:'overlay' });
|
|
}
|
|
// First down (yellow)
|
|
for (let x = 0; x < FIELD.width; x += 0.8) {
|
|
gs.push({ x, y:0.015, z:fdZ, sx:0.45, sy:0.008, sz:0.1, r:1, g:0.85, b:0, a:0.55, type:'overlay' });
|
|
}
|
|
return gs;
|
|
}
|
|
|
|
function buildParticles() {
|
|
const gs = [];
|
|
for (const p of particles) {
|
|
if (p.life <= 0) continue;
|
|
const fade = Math.min(1, p.life / p.maxLife);
|
|
gs.push({
|
|
x:p.x, y:p.y, z:p.z,
|
|
sx:p.size, sy:p.size, sz:p.size,
|
|
r:p.r, g:p.g, b:p.b, a:p.a * fade,
|
|
type:'particle',
|
|
});
|
|
}
|
|
return gs;
|
|
}
|
|
|
|
// ============================================================
|
|
// PARTICLE SYSTEM
|
|
// ============================================================
|
|
function spawnConfetti(x, y, z, count) {
|
|
const colors = [[1,0.85,0],[1,0,0],[0,0.4,1],[1,1,1],[0.41,0.74,0.16],[0.78,0.05,0.19]];
|
|
for (let i = 0; i < count; i++) {
|
|
const c = colors[Math.floor(Math.random()*colors.length)];
|
|
particles.push({
|
|
x: x + (Math.random()-0.5)*10,
|
|
y: y + Math.random()*5,
|
|
z: z + (Math.random()-0.5)*10,
|
|
vx: (Math.random()-0.5)*8,
|
|
vy: Math.random()*10 + 5,
|
|
vz: (Math.random()-0.5)*8,
|
|
r:c[0], g:c[1], b:c[2], a:0.9,
|
|
size: 0.1 + Math.random()*0.15,
|
|
life: 3 + Math.random()*2,
|
|
maxLife: 5,
|
|
gravity: -12,
|
|
});
|
|
}
|
|
}
|
|
|
|
function spawnTurfSpray(x, z) {
|
|
for (let i = 0; i < 8; i++) {
|
|
particles.push({
|
|
x: x + (Math.random()-0.5)*0.5,
|
|
y: 0.1,
|
|
z: z + (Math.random()-0.5)*0.5,
|
|
vx: (Math.random()-0.5)*3,
|
|
vy: Math.random()*3 + 1,
|
|
vz: (Math.random()-0.5)*3,
|
|
r:0.15, g:0.35, b:0.12, a:0.6,
|
|
size: 0.05 + Math.random()*0.05,
|
|
life: 0.5 + Math.random()*0.3,
|
|
maxLife: 0.8,
|
|
gravity: -8,
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateParticles(dt) {
|
|
for (const p of particles) {
|
|
p.x += p.vx * dt;
|
|
p.y += p.vy * dt;
|
|
p.z += p.vz * dt;
|
|
p.vy += p.gravity * dt;
|
|
p.life -= dt;
|
|
if (p.y < 0.02) { p.y = 0.02; p.vy *= -0.3; p.vx *= 0.8; p.vz *= 0.8; }
|
|
}
|
|
particles = particles.filter(p => p.life > 0);
|
|
}
|
|
|
|
// ============================================================
|
|
// PLAY SIMULATION
|
|
// ============================================================
|
|
let currentPlay = null;
|
|
let ballState = { x:0, y:0.8, z:0, inFlight:false, held:true, carrier:'QB', targetX:0, targetZ:0, startX:0, startZ:0, startY:0, flightTime:0, flightDuration:1 };
|
|
let playerPos = { off:[], def:[] };
|
|
let lastBigPlay = '';
|
|
|
|
function initPlayers() {
|
|
const losZ = FIELD.endZone + game.lineOfScrimmage;
|
|
const cx = FIELD.width / 2;
|
|
const dir = game.possession === 'NE' ? 1 : -1;
|
|
const offF = ['shotgun','spread','iform'][Math.floor(Math.random()*3)];
|
|
const defF = Math.random()>0.5 ? 'nickel' : 'cover3';
|
|
|
|
playerPos.off = FORMATIONS.offense[offF].map(p=>({
|
|
...p, wx:cx+p.x, wz:losZ+p.z*dir, tx:cx+p.x, tz:losZ+p.z*dir,
|
|
team:game.possession, moving:false,
|
|
}));
|
|
const defTeam = game.possession==='NE'?'SEA':'NE';
|
|
playerPos.def = FORMATIONS.defense[defF].map(p=>({
|
|
...p, wx:cx+p.x, wz:losZ+p.z*dir, tx:cx+p.x, tz:losZ+p.z*dir,
|
|
team:defTeam, moving:false,
|
|
}));
|
|
ballState = { x:cx, y:0.8, z:losZ, inFlight:false, held:true, carrier:'QB', targetX:0,targetZ:0,startX:0,startZ:0,startY:0,flightTime:0,flightDuration:1 };
|
|
}
|
|
|
|
function simulatePlay(dt) {
|
|
const effDt = replayMode ? dt * settings.replaySpeedMul : dt;
|
|
if (!game.playInProgress) {
|
|
game.playTimer += effDt;
|
|
if (game.playTimer > 2.5) {
|
|
game.playInProgress = true;
|
|
game.playPhase = 'pre-snap';
|
|
game.playTimer = 0;
|
|
game.playClock = 40;
|
|
initPlayers();
|
|
currentPlay = { type: Math.random()>0.42?'pass':'run', receiver: ['WR','WR','WR','TE'][Math.floor(Math.random()*4)], gain:0 };
|
|
}
|
|
return;
|
|
}
|
|
|
|
game.playTimer += effDt;
|
|
game.playClock = Math.max(0, 40 - game.playTimer);
|
|
const dir = game.possession==='NE'?1:-1;
|
|
const losZ = FIELD.endZone + game.lineOfScrimmage;
|
|
const cx = FIELD.width / 2;
|
|
|
|
if (game.playPhase === 'pre-snap') {
|
|
if (game.playTimer > 1.2) { game.playPhase = 'snap'; game.playTimer = 0; }
|
|
playerPos.off.forEach(p=>{ p.wx+=(Math.random()-0.5)*0.008; });
|
|
playerPos.def.forEach(p=>{ p.wx+=(Math.random()-0.5)*0.015; });
|
|
} else if (game.playPhase === 'snap') {
|
|
if (currentPlay.type === 'pass') simPass(effDt,dir,losZ,cx);
|
|
else simRun(effDt,dir,losZ,cx);
|
|
} else if (['complete','incomplete','tackle'].includes(game.playPhase)) {
|
|
if (game.playTimer > 1.8) endPlay();
|
|
}
|
|
|
|
// Lerp all players
|
|
[...playerPos.off,...playerPos.def].forEach(p=>{
|
|
const dx=p.tx-p.wx, dz=p.tz-p.wz;
|
|
const d=Math.sqrt(dx*dx+dz*dz);
|
|
p.moving = d > 0.1;
|
|
if (d>0.04) { p.wx+=dx*Math.min(1,effDt*3.5); p.wz+=dz*Math.min(1,effDt*3.5); }
|
|
});
|
|
}
|
|
|
|
function simPass(dt,dir,losZ,cx) {
|
|
const t=game.playTimer;
|
|
const qb=playerPos.off.find(p=>p.pos==='QB');
|
|
const tgt=playerPos.off.find(p=>p.pos===currentPlay.receiver);
|
|
|
|
if(qb) { qb.tx=cx+(Math.random()-0.5)*1.5; qb.tz=losZ-5*dir; ballState.x=qb.wx; ballState.z=qb.wz; ballState.y=1.5; }
|
|
|
|
playerPos.off.forEach(p=>{
|
|
if(['WR','TE'].includes(p.pos)) { p.tx=cx+p.x+(Math.random()-0.5)*5; p.tz=losZ+(8+Math.random()*14)*dir; p.moving=true; }
|
|
if(['LT','LG','C','RG','RT'].includes(p.pos)) { p.tx=cx+p.x; p.tz=losZ+0.5*dir; }
|
|
});
|
|
playerPos.def.forEach(p=>{
|
|
if(['CB','NCB'].includes(p.pos)) {
|
|
const wr=playerPos.off.filter(o=>['WR','TE'].includes(o.pos)).sort((a,b)=>Math.abs(a.wx-p.wx)-Math.abs(b.wx-p.wx))[0];
|
|
if(wr){p.tx=wr.wx+(Math.random()-0.5)*1.5;p.tz=wr.wz+dir;}
|
|
} else if(p.pos==='DE') { if(qb){p.tx=qb.wx+(Math.random()-0.5)*2;p.tz=qb.wz;} }
|
|
else if(['FS','SS'].includes(p.pos)) { p.tz+=dir*dt*4; }
|
|
else { p.tz=losZ+5*dir; }
|
|
p.moving=true;
|
|
});
|
|
|
|
if(t>2.0 && !ballState.inFlight && ballState.held) {
|
|
ballState.inFlight=true; ballState.held=false;
|
|
ballState.targetX=tgt?tgt.tx:cx+(Math.random()-0.5)*20;
|
|
ballState.targetZ=tgt?tgt.tz:losZ+15*dir;
|
|
ballState.startX=ballState.x; ballState.startZ=ballState.z; ballState.startY=1.8;
|
|
ballState.flightTime=0; ballState.flightDuration=0.9+Math.random()*0.5;
|
|
}
|
|
if(ballState.inFlight) {
|
|
ballState.flightTime+=dt;
|
|
const p=Math.min(1,ballState.flightTime/ballState.flightDuration);
|
|
ballState.x=ballState.startX+(ballState.targetX-ballState.startX)*p;
|
|
ballState.z=ballState.startZ+(ballState.targetZ-ballState.startZ)*p;
|
|
ballState.y=ballState.startY+7*Math.sin(p*Math.PI)*(1-p*0.25);
|
|
if(p>=1) {
|
|
ballState.inFlight=false;
|
|
const complete=Math.random()>0.32;
|
|
if(complete) {
|
|
game.playPhase='complete';
|
|
const gain=Math.round(Math.abs(ballState.z-losZ)+Math.random()*4);
|
|
currentPlay.gain=gain;
|
|
const passer=game.possession==='NE'?'Brady':'Wilson';
|
|
addTicker(`${passer} complete to #${tgt?.num||14} for ${gain} yds`,gain);
|
|
if(gain>20) { showBigPlay(`${gain}-YARD PASS!`); spawnConfetti(ballState.x,3,ballState.z,30); }
|
|
spawnTurfSpray(ballState.x,ballState.z);
|
|
ballState.y=1;
|
|
} else {
|
|
game.playPhase='incomplete'; currentPlay.gain=0;
|
|
addTicker(`Incomplete, intended #${tgt?.num||14}`,0);
|
|
ballState.y=0.08;
|
|
}
|
|
game.playTimer=0;
|
|
}
|
|
}
|
|
}
|
|
|
|
function simRun(dt,dir,losZ,cx) {
|
|
const t=game.playTimer;
|
|
const rb=playerPos.off.find(p=>['RB','FB'].includes(p.pos));
|
|
const qb=playerPos.off.find(p=>p.pos==='QB');
|
|
|
|
if(t<0.7) { if(qb){qb.tx=cx;qb.tz=losZ-dir;ballState.x=qb.wx;ballState.z=qb.wz;ballState.y=1;} }
|
|
else {
|
|
if(rb) {
|
|
rb.tx=cx+(Math.random()-0.5)*7; rb.tz=losZ+(3+Math.random()*9)*dir;
|
|
rb.moving=true; ballState.x=rb.wx; ballState.z=rb.wz; ballState.y=0.9;
|
|
}
|
|
playerPos.off.forEach(p=>{
|
|
if(['LT','LG','C','RG','RT'].includes(p.pos)) { p.tx=cx+p.x+(rb?(rb.tx-cx)*0.25:0); p.tz=losZ+2*dir; p.moving=true; }
|
|
});
|
|
playerPos.def.forEach(p=>{
|
|
if(rb) {
|
|
const dd=Math.sqrt((rb.wx-p.wx)**2+(rb.wz-p.wz)**2);
|
|
p.tx=rb.wx+(Math.random()-0.5)*1.5; p.tz=rb.wz+(Math.random()-0.5)*1.5; p.moving=true;
|
|
if(dd<1.3 && t>1.3) {
|
|
game.playPhase='tackle';
|
|
const gain=Math.round(Math.abs(rb.wz-losZ));
|
|
currentPlay.gain=gain;
|
|
addTicker(`#${rb.num} runs ${gain} yds, tackled by #${p.num}`,gain);
|
|
if(gain>15) { showBigPlay(`${gain}-YARD RUN!`); }
|
|
spawnTurfSpray(rb.wx,rb.wz);
|
|
game.playTimer=0;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if(t>5 && game.playPhase==='snap') {
|
|
game.playPhase='tackle';
|
|
const gain=rb?Math.round(Math.abs(rb.wz-losZ)):3;
|
|
currentPlay.gain=gain; addTicker(`#${rb?.num||28} runs ${gain} yds`,gain);
|
|
game.playTimer=0;
|
|
}
|
|
}
|
|
|
|
function endPlay() {
|
|
game.playInProgress=false; game.playTimer=0; game.totalPlays++;
|
|
const gain=currentPlay?.gain||0;
|
|
const dir=game.possession==='NE'?1:-1;
|
|
|
|
if(game.playPhase==='incomplete') { game.down++; }
|
|
else { game.lineOfScrimmage+=gain*dir; game.yardsToGo-=gain; game.driveYards+=gain; game.down++; }
|
|
|
|
if(game.yardsToGo<=0) { game.down=1; game.yardsToGo=10; addTicker('FIRST DOWN!',0,true); }
|
|
|
|
if(game.lineOfScrimmage>=100||game.lineOfScrimmage<=0) {
|
|
const team = game.possession;
|
|
if(team==='NE') { game.scoreNE+=7; showBigPlay('TOUCHDOWN!'); addTicker('TOUCHDOWN PATRIOTS!',0,true); }
|
|
else { game.scoreSEA+=7; showBigPlay('TOUCHDOWN!'); addTicker('TOUCHDOWN SEAHAWKS!',0,true); }
|
|
spawnConfetti(FIELD.width/2,5,FIELD.length/2,120);
|
|
game.possession=game.possession==='NE'?'SEA':'NE';
|
|
game.lineOfScrimmage=25; game.down=1; game.yardsToGo=10; game.driveYards=0;
|
|
}
|
|
if(game.down>4) {
|
|
addTicker('Turnover on downs!',0,true);
|
|
game.possession=game.possession==='NE'?'SEA':'NE';
|
|
game.lineOfScrimmage=Math.max(1,Math.min(99,game.lineOfScrimmage));
|
|
game.down=1; game.yardsToGo=10; game.driveYards=0;
|
|
}
|
|
game.clock-=Math.floor(Math.random()*28+12);
|
|
if(game.clock<=0) {
|
|
game.quarter++; game.clock=900;
|
|
if(game.quarter>4) { game.quarter=4; game.clock=0; showBigPlay('FINAL'); addTicker('FINAL! Super Bowl LX is over!',0,true); }
|
|
else { addTicker(`End of Q${game.quarter-1}`,0,true); if(game.quarter===3){game.possession=game.possession==='NE'?'SEA':'NE';game.lineOfScrimmage=25;} }
|
|
}
|
|
game.playPhase='huddle'; currentPlay=null;
|
|
}
|
|
|
|
// ============================================================
|
|
// UI
|
|
// ============================================================
|
|
function addTicker(text, yards, hl) {
|
|
const feed=document.getElementById('play-ticker');
|
|
const m=Math.floor(game.clock/60), s=game.clock%60;
|
|
const div=document.createElement('div'); div.className='tk-entry';
|
|
const cls=hl?'tk-highlight':(yards>0?'tk-gain':'tk-text');
|
|
div.innerHTML=`<span class="tk-time">Q${game.quarter} ${m}:${s.toString().padStart(2,'0')}</span> <span class="${cls}">${text}</span>`;
|
|
feed.appendChild(div); feed.scrollTop=feed.scrollHeight;
|
|
while(feed.children.length>25) feed.removeChild(feed.firstChild);
|
|
}
|
|
|
|
let bigPlayTimer = 0;
|
|
function showBigPlay(text) {
|
|
const el=document.getElementById('big-play');
|
|
el.textContent=text; el.classList.add('show');
|
|
bigPlayTimer=2.5;
|
|
lastBigPlay=text;
|
|
// Auto-replay on big plays
|
|
if(settings.autoReplay && !replayMode && (text.includes('TOUCHDOWN') || text.includes('YARD'))) {
|
|
triggerReplay();
|
|
}
|
|
}
|
|
|
|
function triggerReplay() {
|
|
replayMode=true; replayTimer=3;
|
|
document.getElementById('replay-badge').classList.add('show');
|
|
}
|
|
|
|
function updateUI() {
|
|
document.getElementById('g-count').textContent=totalGaussianCount.toLocaleString();
|
|
document.getElementById('score-ne').textContent=game.scoreNE;
|
|
document.getElementById('score-sea').textContent=game.scoreSEA;
|
|
const qs=['1ST','2ND','3RD','4TH','OT'];
|
|
document.getElementById('quarter').textContent=qs[Math.min(game.quarter-1,4)];
|
|
const m=Math.floor(game.clock/60), s=game.clock%60;
|
|
document.getElementById('clock').textContent=`${m}:${s.toString().padStart(2,'0')}`;
|
|
document.getElementById('down-dist').textContent=`${game.down}${['st','nd','rd','th'][Math.min(game.down-1,3)]} & ${game.yardsToGo}`;
|
|
document.getElementById('ne-poss').style.background=game.possession==='NE'?'#c60c30':'transparent';
|
|
document.getElementById('sea-poss').style.background=game.possession==='SEA'?'#69be28':'transparent';
|
|
const yd=game.lineOfScrimmage<=50?`${game.possession} ${game.lineOfScrimmage}`:`${game.possession==='NE'?'SEA':'NE'} ${100-game.lineOfScrimmage}`;
|
|
document.getElementById('ball-yard').textContent=yd;
|
|
const pc=document.getElementById('play-clock');
|
|
pc.textContent=Math.max(0,Math.round(game.playClock));
|
|
pc.classList.toggle('pc-warn',game.playClock<10);
|
|
document.getElementById('coh-val').textContent=(1.0-game.totalPlays*0.001).toFixed(3);
|
|
document.getElementById('cam-mode').textContent=camera.mode.charAt(0).toUpperCase()+camera.mode.slice(1);
|
|
}
|
|
|
|
// ============================================================
|
|
// 3D PROJECTION (same math, improved fog/post)
|
|
// ============================================================
|
|
function mat4LookAt(e,c,u){let fx=e[0]-c[0],fy=e[1]-c[1],fz=e[2]-c[2];let l=Math.sqrt(fx*fx+fy*fy+fz*fz);fx/=l;fy/=l;fz/=l;let rx=u[1]*fz-u[2]*fy,ry=u[2]*fx-u[0]*fz,rz=u[0]*fy-u[1]*fx;l=Math.sqrt(rx*rx+ry*ry+rz*rz);rx/=l;ry/=l;rz/=l;const ux=fy*rz-fz*ry,uy=fz*rx-fx*rz,uz=fx*ry-fy*rx;return[rx,ux,fx,0,ry,uy,fy,0,rz,uz,fz,0,-(rx*e[0]+ry*e[1]+rz*e[2]),-(ux*e[0]+uy*e[1]+uz*e[2]),-(fx*e[0]+fy*e[1]+fz*e[2]),1];}
|
|
function mat4Persp(f,a,n,r){const t=1/Math.tan(f/2),i=1/(n-r);return[t/a,0,0,0,0,t,0,0,0,0,r*i,-1,0,0,r*n*i,0];}
|
|
function mat4Mul(a,b){const o=[];for(let i=0;i<4;i++)for(let j=0;j<4;j++){o[j*4+i]=a[i]*b[j*4]+a[4+i]*b[j*4+1]+a[8+i]*b[j*4+2]+a[12+i]*b[j*4+3];}return o;}
|
|
function proj(vp,x,y,z,w,h){const cx=vp[0]*x+vp[4]*y+vp[8]*z+vp[12],cy=vp[1]*x+vp[5]*y+vp[9]*z+vp[13],cz=vp[2]*x+vp[6]*y+vp[10]*z+vp[14],cw=vp[3]*x+vp[7]*y+vp[11]*z+vp[15];if(cw<=0.01)return null;return{sx:(cx/cw*0.5+0.5)*w,sy:(1-(cy/cw*0.5+0.5))*h,depth:cz/cw,w:cw};}
|
|
|
|
function getEye() {
|
|
const cx=FIELD.width/2, losZ=FIELD.endZone+game.lineOfScrimmage;
|
|
switch(camera.mode) {
|
|
case 'broadcast': return [-20, 15, losZ+Math.sin(globalTime*0.12)*4];
|
|
case 'endzone': return [cx, 22, game.possession==='NE'?-18:FIELD.length+18];
|
|
case 'skycam': return [cx+Math.sin(globalTime*0.15)*8, 38, losZ];
|
|
case 'follow': return [ballState.x-10, 6, ballState.z-6];
|
|
case 'cinematic': {
|
|
camera.cinematicAngle += 0.003;
|
|
const r = 45;
|
|
return [cx+r*Math.cos(camera.cinematicAngle), 12+Math.sin(camera.cinematicAngle*0.7)*5,
|
|
FIELD.length/2+r*Math.sin(camera.cinematicAngle)];
|
|
}
|
|
default: return [cx+camera.radius*Math.cos(camera.phi)*Math.sin(camera.theta),
|
|
camera.radius*Math.sin(camera.phi),
|
|
FIELD.length/2+camera.radius*Math.cos(camera.phi)*Math.cos(camera.theta)];
|
|
}
|
|
}
|
|
function getTarget() {
|
|
const cx=FIELD.width/2, losZ=FIELD.endZone+game.lineOfScrimmage;
|
|
switch(camera.mode) {
|
|
case 'broadcast': case 'endzone': case 'skycam': return [cx,0,losZ];
|
|
case 'follow': return [ballState.x,ballState.y,ballState.z];
|
|
case 'cinematic': return [cx,0,FIELD.length/2];
|
|
default: return camera.target;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// MAIN RENDER LOOP
|
|
// ============================================================
|
|
function render(nowMs) {
|
|
if (!lastFrameTime) lastFrameTime = nowMs;
|
|
const dt = Math.min((nowMs - lastFrameTime) / 1000, 0.05);
|
|
lastFrameTime = nowMs;
|
|
|
|
const simDt = dt * settings.gameSpeed;
|
|
if (playing) {
|
|
globalTime += simDt;
|
|
simulatePlay(simDt);
|
|
if (settings.particles) updateParticles(simDt); else particles = [];
|
|
}
|
|
|
|
// Big play timer
|
|
if (bigPlayTimer > 0) {
|
|
bigPlayTimer -= dt;
|
|
if (bigPlayTimer <= 0) document.getElementById('big-play').classList.remove('show');
|
|
}
|
|
// Replay timer
|
|
if (replayMode) {
|
|
replayTimer -= dt;
|
|
if (replayTimer <= 0) {
|
|
replayMode = false;
|
|
document.getElementById('replay-badge').classList.remove('show');
|
|
}
|
|
}
|
|
|
|
// Canvas resize
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const w = canvas.clientWidth * dpr;
|
|
const h = canvas.clientHeight * dpr;
|
|
if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
|
|
|
|
// Build all gaussians
|
|
gaussians = [];
|
|
gaussians.push(...buildField());
|
|
gaussians.push(...buildFieldMarkings());
|
|
gaussians.push(...buildGoalposts());
|
|
gaussians.push(...buildOverlayLines());
|
|
playerPos.off.forEach(p=>{ gaussians.push(...buildPlayer(p.wx,p.wz,p.team,p.num,p.pos,true,p)); });
|
|
playerPos.def.forEach(p=>{ gaussians.push(...buildPlayer(p.wx,p.wz,p.team,p.num,p.pos,false,p)); });
|
|
|
|
// Refs
|
|
const losZ=FIELD.endZone+game.lineOfScrimmage;
|
|
[[FIELD.width/2-12,losZ+8],[FIELD.width/2+15,losZ-3],[FIELD.width/2,losZ+18],[FIELD.width/2-8,losZ-12]].forEach(([rx,rz])=>{
|
|
gaussians.push(
|
|
{x:rx,y:1.7,z:rz,sx:0.28,sy:0.24,sz:0.28,r:0.05,g:0.05,b:0.05,a:1,type:'ref'},
|
|
{x:rx,y:1.15,z:rz,sx:0.35,sy:0.38,sz:0.28,r:0,g:0,b:0,a:1,type:'ref'},
|
|
{x:rx,y:1.2,z:rz,sx:0.3,sy:0.3,sz:0.22,r:1,g:1,b:1,a:0.65,type:'ref'},
|
|
{x:rx,y:0.5,z:rz,sx:0.22,sy:0.32,sz:0.18,r:0.05,g:0.05,b:0.05,a:1,type:'ref'},
|
|
);
|
|
});
|
|
|
|
gaussians.push(...buildFootball(ballState.x,ballState.y,ballState.z,ballState.inFlight));
|
|
gaussians.push(...buildStadium());
|
|
gaussians.push(...buildParticles());
|
|
|
|
totalGaussianCount = gaussians.length;
|
|
|
|
// Camera smoothing
|
|
const eyeRaw = getEye(), tgtRaw = getTarget();
|
|
const sm = settings.camSmooth;
|
|
for(let i=0;i<3;i++) { camera.eyeSmooth[i]+=(eyeRaw[i]-camera.eyeSmooth[i])*sm; camera.targetSmooth[i]+=(tgtRaw[i]-camera.targetSmooth[i])*sm; }
|
|
|
|
const view = mat4LookAt(camera.eyeSmooth, camera.targetSmooth, [0,1,0]);
|
|
const pr = mat4Persp(camera.fov, w/h, camera.near, camera.far);
|
|
const vp = mat4Mul(pr, view);
|
|
|
|
// Project
|
|
const projected = [];
|
|
for (const g of gaussians) {
|
|
const p = proj(vp, g.x, g.y, g.z, w, h);
|
|
if (!p || p.depth<-1 || p.depth>1) continue;
|
|
if (p.sx<-w*0.3 || p.sx>w*1.3 || p.sy<-h*0.3 || p.sy>h*1.3) continue;
|
|
const scale = Math.max(g.sx, g.sy, g.sz);
|
|
const ss = (scale/p.w)*w*0.5;
|
|
if (ss<settings.qualityThreshold) continue;
|
|
projected.push({g,p,ss});
|
|
}
|
|
projected.sort((a,b)=>b.p.depth-a.p.depth);
|
|
|
|
// Clear & sky
|
|
ctx.clearRect(0,0,w,h);
|
|
const sky=ctx.createLinearGradient(0,0,0,h*0.45);
|
|
sky.addColorStop(0,'#050510');
|
|
sky.addColorStop(0.4,'#0a0f22');
|
|
sky.addColorStop(1,'#111830');
|
|
ctx.fillStyle=sky; ctx.fillRect(0,0,w,h);
|
|
|
|
// Draw gaussians
|
|
for (const {g,p,ss} of projected) {
|
|
let rx = ss*(g.sx/Math.max(g.sx,g.sy,g.sz))*1.15;
|
|
let ry = ss*(g.sy/Math.max(g.sx,g.sy,g.sz))*1.15;
|
|
if(rx<0.4&&ry<0.4) continue;
|
|
|
|
// Atmospheric fog (distance fade)
|
|
const fogFactor = settings.fog ? Math.max(0, Math.min(1, (p.depth - 0.3) * 1.2)) : 0;
|
|
const fogAlpha = g.a * (1 - fogFactor * 0.6);
|
|
|
|
ctx.save();
|
|
ctx.translate(p.sx, p.sy);
|
|
const r=Math.min(255,Math.round(g.r*255));
|
|
const gr=Math.min(255,Math.round(g.g*255));
|
|
const b=Math.min(255,Math.round(g.b*255));
|
|
const maxR=Math.max(rx,ry,1);
|
|
const grad=ctx.createRadialGradient(0,0,0,0,0,maxR);
|
|
grad.addColorStop(0,`rgba(${r},${gr},${b},${fogAlpha})`);
|
|
grad.addColorStop(0.45,`rgba(${r},${gr},${b},${fogAlpha*0.55})`);
|
|
grad.addColorStop(1,`rgba(${r},${gr},${b},0)`);
|
|
ctx.fillStyle=grad;
|
|
ctx.scale(rx/maxR, ry/maxR);
|
|
ctx.beginPath(); ctx.arc(0,0,maxR,0,Math.PI*2); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
// === POST-PROCESSING ===
|
|
|
|
// Vignette
|
|
if (settings.vignette) {
|
|
const vg = ctx.createRadialGradient(w/2,h/2,w*0.25,w/2,h/2,w*0.7);
|
|
vg.addColorStop(0,'rgba(0,0,0,0)');
|
|
vg.addColorStop(0.7,'rgba(0,0,0,0)');
|
|
vg.addColorStop(1,'rgba(0,0,0,0.45)');
|
|
ctx.fillStyle=vg; ctx.fillRect(0,0,w,h);
|
|
}
|
|
|
|
// Subtle film grain
|
|
if (settings.grain) {
|
|
ctx.globalAlpha = 0.015;
|
|
for (let i = 0; i < 30; i++) {
|
|
const gx = Math.random()*w, gy = Math.random()*h;
|
|
const gv = Math.random()*255;
|
|
ctx.fillStyle = `rgb(${gv},${gv},${gv})`;
|
|
ctx.fillRect(gx, gy, 3, 3);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// Slight warm color grade
|
|
if (settings.colorGrade) {
|
|
ctx.globalCompositeOperation = 'overlay';
|
|
ctx.fillStyle = 'rgba(255,235,200,0.03)';
|
|
ctx.fillRect(0,0,w,h);
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
}
|
|
|
|
// FPS
|
|
frameTimes.push(nowMs);
|
|
if(frameTimes.length>60) frameTimes.shift();
|
|
if(frameTimes.length>1) {
|
|
const fps=1000/((frameTimes[frameTimes.length-1]-frameTimes[0])/(frameTimes.length-1));
|
|
document.getElementById('fps-value').textContent=fps.toFixed(1);
|
|
}
|
|
|
|
updateUI();
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
// ============================================================
|
|
// CONTROLS
|
|
// ============================================================
|
|
document.getElementById('play-btn').addEventListener('click',()=>{
|
|
playing=!playing;
|
|
document.getElementById('play-btn').innerHTML=playing?'▮▮':'▶';
|
|
});
|
|
['broadcast','endzone','skycam','follow','cinematic'].forEach(m=>{
|
|
document.getElementById('cam-'+m)?.addEventListener('click',()=>{
|
|
camera.mode=m;
|
|
document.querySelectorAll('#transport .tb').forEach(b=>b.classList.remove('active'));
|
|
document.getElementById('cam-'+m).classList.add('active');
|
|
});
|
|
});
|
|
document.getElementById('btn-replay')?.addEventListener('click',()=>triggerReplay());
|
|
|
|
// Tech modal
|
|
const techBackdrop=document.getElementById('tech-modal-backdrop');
|
|
document.getElementById('btn-tech')?.addEventListener('click',()=>{
|
|
techBackdrop.classList.toggle('open');
|
|
});
|
|
document.getElementById('tech-close')?.addEventListener('click',()=>{
|
|
techBackdrop.classList.remove('open');
|
|
});
|
|
techBackdrop?.addEventListener('click',(e)=>{
|
|
if(e.target===techBackdrop) techBackdrop.classList.remove('open');
|
|
});
|
|
|
|
// Settings modal
|
|
const setBackdrop=document.getElementById('settings-backdrop');
|
|
document.getElementById('btn-settings')?.addEventListener('click',()=>{
|
|
setBackdrop.classList.toggle('open');
|
|
updateSettingsAI();
|
|
});
|
|
document.getElementById('settings-close')?.addEventListener('click',()=>{
|
|
setBackdrop.classList.remove('open');
|
|
});
|
|
setBackdrop?.addEventListener('click',(e)=>{
|
|
if(e.target===setBackdrop) setBackdrop.classList.remove('open');
|
|
});
|
|
|
|
// Tabs
|
|
document.querySelectorAll('.st-tab').forEach(tab=>{
|
|
tab.addEventListener('click',()=>{
|
|
document.querySelectorAll('.st-tab').forEach(t=>t.classList.remove('active'));
|
|
document.querySelectorAll('.st-tab-content').forEach(c=>c.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.tab)?.classList.add('active');
|
|
});
|
|
});
|
|
|
|
// Toggle bindings
|
|
function bindToggle(id,key){
|
|
document.getElementById(id)?.addEventListener('change',function(){ settings[key]=this.checked; });
|
|
}
|
|
bindToggle('set-fog','fog');
|
|
bindToggle('set-vignette','vignette');
|
|
bindToggle('set-grain','grain');
|
|
bindToggle('set-colorgrade','colorGrade');
|
|
bindToggle('set-particles','particles');
|
|
bindToggle('set-autoplay','autoPlay');
|
|
bindToggle('set-autoreplay','autoReplay');
|
|
bindToggle('set-coherence','coherenceGate');
|
|
bindToggle('set-neural','neural');
|
|
bindToggle('set-ewc','ewc');
|
|
|
|
// Ticker toggle
|
|
document.getElementById('set-ticker')?.addEventListener('change',function(){
|
|
settings.showTicker=this.checked;
|
|
document.getElementById('play-ticker').style.display=this.checked?'block':'none';
|
|
});
|
|
// Scorebug toggle
|
|
document.getElementById('set-scorebug')?.addEventListener('change',function(){
|
|
settings.showScorebug=this.checked;
|
|
document.getElementById('scorebug').style.display=this.checked?'flex':'none';
|
|
document.getElementById('play-clock-badge').style.display=this.checked?'flex':'none';
|
|
});
|
|
// Stats toggle
|
|
document.getElementById('set-stats')?.addEventListener('change',function(){
|
|
settings.showStats=this.checked;
|
|
document.getElementById('stats-panel').style.display=this.checked?'block':'none';
|
|
});
|
|
|
|
// Range bindings
|
|
document.getElementById('set-crowd')?.addEventListener('input',function(){
|
|
settings.crowdDensity=this.value/100;
|
|
document.getElementById('set-crowd-val').textContent=this.value+'%';
|
|
});
|
|
document.getElementById('set-camsmooth')?.addEventListener('input',function(){
|
|
settings.camSmooth=this.value/100;
|
|
document.getElementById('set-camsmooth-val').textContent=(this.value/100).toFixed(2);
|
|
});
|
|
document.getElementById('set-gamespeed')?.addEventListener('input',function(){
|
|
settings.gameSpeed=this.value/10;
|
|
document.getElementById('set-gamespeed-val').textContent=(this.value/10).toFixed(1)+'x';
|
|
});
|
|
document.getElementById('set-replayspeed')?.addEventListener('input',function(){
|
|
settings.replaySpeedMul=this.value/100;
|
|
document.getElementById('set-replayspeed-val').textContent=(this.value/100).toFixed(2)+'x';
|
|
});
|
|
document.getElementById('set-cohthreshold')?.addEventListener('input',function(){
|
|
settings.coherenceThreshold=this.value/100;
|
|
document.getElementById('set-cohthreshold-val').textContent=(this.value/100).toFixed(2);
|
|
});
|
|
|
|
// Select bindings
|
|
document.getElementById('set-quality')?.addEventListener('change',function(){
|
|
settings.qualityThreshold=parseFloat(this.value);
|
|
});
|
|
document.getElementById('set-playcall')?.addEventListener('change',function(){
|
|
settings.playCalling=this.value;
|
|
});
|
|
document.getElementById('set-attention')?.addEventListener('change',function(){
|
|
settings.attention=this.value;
|
|
});
|
|
document.getElementById('set-temporal')?.addEventListener('change',function(){
|
|
settings.temporal=this.value;
|
|
});
|
|
|
|
// Update AI metrics in settings modal
|
|
function updateSettingsAI(){
|
|
const coh=(1.0-game.totalPlays*0.001);
|
|
document.getElementById('ai-coherence').textContent=coh.toFixed(3);
|
|
document.getElementById('ai-entities').textContent='26';
|
|
document.getElementById('ai-plays').textContent=game.totalPlays;
|
|
document.getElementById('ai-gaussians').textContent=totalGaussianCount.toLocaleString();
|
|
if(frameTimes.length>1){
|
|
const fps=1000/((frameTimes[frameTimes.length-1]-frameTimes[0])/(frameTimes.length-1));
|
|
document.getElementById('ai-fast-hz').textContent=fps.toFixed(0)+' Hz';
|
|
}
|
|
}
|
|
|
|
let dragging=false, lmx=0, lmy=0;
|
|
canvas.addEventListener('mousedown',e=>{dragging=true;lmx=e.clientX;lmy=e.clientY;});
|
|
window.addEventListener('mouseup',()=>dragging=false);
|
|
window.addEventListener('mousemove',e=>{
|
|
if(!dragging)return;
|
|
camera.theta-=(e.clientX-lmx)*0.005;
|
|
camera.phi+=(e.clientY-lmy)*0.005;
|
|
camera.phi=Math.max(0.05,Math.min(Math.PI/2-0.05,camera.phi));
|
|
lmx=e.clientX;lmy=e.clientY; camera.mode='free';
|
|
});
|
|
canvas.addEventListener('wheel',e=>{
|
|
e.preventDefault();
|
|
camera.radius*=1+e.deltaY*0.001;
|
|
camera.radius=Math.max(5,Math.min(150,camera.radius));
|
|
},{passive:false});
|
|
|
|
document.getElementById('time-slider').addEventListener('input',function(){
|
|
document.getElementById('time-label').textContent=`t=${(parseInt(this.value)/1000).toFixed(3)}`;
|
|
});
|
|
|
|
// ============================================================
|
|
// INIT
|
|
// ============================================================
|
|
drawPatriotsLogo(document.getElementById('pat-logo'));
|
|
drawSeahawksLogo(document.getElementById('sea-logo'));
|
|
initPlayers();
|
|
document.getElementById('cam-broadcast').classList.add('active');
|
|
requestAnimationFrame(render);
|
|
</script>
|
|
</body>
|
|
</html>
|