Upload index.html with huggingface_hub
Browse files- index.html +131 -27
index.html
CHANGED
|
@@ -107,12 +107,33 @@
|
|
| 107 |
.ghost:hover{color:var(--text);border-color:var(--accent)}
|
| 108 |
button:disabled{opacity:.45;cursor:not-allowed;filter:none}
|
| 109 |
|
| 110 |
-
|
| 111 |
-
.
|
| 112 |
-
.
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
.active-tags{font-size:12px;color:var(--good);min-height:16px;margin-top:8px}
|
| 117 |
.footnote{font-size:11px;color:var(--muted);margin-top:12px;line-height:1.5}
|
| 118 |
</style>
|
|
@@ -124,7 +145,7 @@
|
|
| 124 |
<div class="logo">🌍</div>
|
| 125 |
<div>
|
| 126 |
<h1>ABot-World — Interactive World Rollout</h1>
|
| 127 |
-
<p>Upload a starting frame, then steer a live action-conditioned world with WASD
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
<div class="links">
|
|
@@ -143,7 +164,7 @@
|
|
| 143 |
<div class="chip"><span class="dot" id="status-dot"></span><span id="status-label">Idle</span></div>
|
| 144 |
<div class="chip"><span id="fps-value">0</span> fps · <span id="frame-value">0</span> blk</div>
|
| 145 |
</div>
|
| 146 |
-
<div class="capture-hint" id="capture-hint">Click
|
| 147 |
<div class="overlay" id="overlay">
|
| 148 |
<div>
|
| 149 |
<div class="spinner" id="spinner" style="display:none"></div>
|
|
@@ -183,26 +204,36 @@
|
|
| 183 |
<button class="primary" id="start-btn" disabled>Start world</button>
|
| 184 |
<button class="ghost" id="stop-btn" disabled>Stop</button>
|
| 185 |
</div>
|
|
|
|
|
|
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
<span class="keycap" data-k="W">W</span>
|
| 191 |
<span class="keycap" data-k="A">A</span>
|
| 192 |
<span class="keycap" data-k="S">S</span>
|
| 193 |
<span class="keycap" data-k="D">D</span>
|
| 194 |
-
<span class="keycap" data-k="I">I</span>
|
| 195 |
-
<span class="keycap" data-k="J">J</span>
|
| 196 |
-
<span class="keycap" data-k="K">K</span>
|
| 197 |
-
<span class="keycap" data-k="L">L</span>
|
| 198 |
</div>
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
| 202 |
</div>
|
| 203 |
-
<div class="
|
| 204 |
</div>
|
| 205 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
</div>
|
| 207 |
</div>
|
| 208 |
|
|
@@ -213,10 +244,12 @@ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.m
|
|
| 213 |
const SESSION_ID = (crypto.randomUUID && crypto.randomUUID()) ||
|
| 214 |
(`s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`);
|
| 215 |
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
| 217 |
const KEY_FROM_CODE = {
|
| 218 |
KeyW:"W", KeyA:"A", KeyS:"S", KeyD:"D",
|
| 219 |
-
KeyI:"I", KeyJ:"J", KeyK:"K", KeyL:"L",
|
| 220 |
};
|
| 221 |
|
| 222 |
const state = {
|
|
@@ -242,7 +275,8 @@ const presetsEl = $("presets");
|
|
| 242 |
const promptEl = $("prompt"), seedEl = $("seed");
|
| 243 |
const startBtn = $("start-btn"), stopBtn = $("stop-btn");
|
| 244 |
const activeTags = $("active-tags");
|
| 245 |
-
const keycaps = Array.from(document.querySelectorAll(".keycap"));
|
|
|
|
| 246 |
|
| 247 |
// --- UI helpers ---
|
| 248 |
function setStatus(kind, label){
|
|
@@ -500,24 +534,94 @@ function startCtrlLoop(){
|
|
| 500 |
function stopCtrlLoop(){ if(ctrlTimer){ clearInterval(ctrlTimer); ctrlTimer = null; } }
|
| 501 |
|
| 502 |
function renderKeys(){
|
|
|
|
| 503 |
keycaps.forEach(c=>c.classList.toggle("active", state.pressed.has(c.dataset.k)));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
const held = Array.from(state.pressed);
|
| 505 |
-
activeTags.textContent = held.length ? "
|
| 506 |
}
|
| 507 |
|
| 508 |
-
// ---
|
| 509 |
-
stage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
document.addEventListener("keydown",(e)=>{
|
| 511 |
if(!state.capturing) return;
|
| 512 |
-
if(e.code === "Escape"){
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
const k = KEY_FROM_CODE[e.code];
|
| 514 |
-
if(k &&
|
| 515 |
});
|
| 516 |
document.addEventListener("keyup",(e)=>{
|
| 517 |
const k = KEY_FROM_CODE[e.code];
|
| 518 |
if(k){ state.pressed.delete(k); renderKeys(); }
|
| 519 |
});
|
| 520 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
promptEl.addEventListener("input", ()=>{ state.prompt = promptEl.value; });
|
| 522 |
|
| 523 |
// --- Start / Stop ---
|
|
|
|
| 107 |
.ghost:hover{color:var(--text);border-color:var(--accent)}
|
| 108 |
button:disabled{opacity:.45;cursor:not-allowed;filter:none}
|
| 109 |
|
| 110 |
+
/* Controls section (below the stage) */
|
| 111 |
+
.controls-section{margin-top:20px}
|
| 112 |
+
.controls-grid{display:grid;grid-template-columns:auto 1fr;gap:28px;align-items:center;margin-top:12px}
|
| 113 |
+
@media (max-width:640px){.controls-grid{grid-template-columns:1fr;gap:20px}}
|
| 114 |
+
|
| 115 |
+
/* Visual on-screen WASD keyboard */
|
| 116 |
+
.keyboard{display:grid;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(2,56px);
|
| 117 |
+
gap:8px;justify-content:start}
|
| 118 |
+
.keycap{display:grid;place-items:center;border-radius:12px;background:#0d1220;
|
| 119 |
+
border:1px solid var(--edge);font-size:18px;color:var(--muted);font-weight:700;
|
| 120 |
+
transition:.08s;user-select:none}
|
| 121 |
+
.keycap.spacer{background:transparent;border:none}
|
| 122 |
+
.keycap.active{background:linear-gradient(135deg,var(--accent),var(--accent-2));color:#fff;
|
| 123 |
+
border-color:transparent;box-shadow:0 4px 16px rgba(91,140,255,.45);transform:translateY(1px)}
|
| 124 |
+
/* WASD cross placement inside the 3x2 grid */
|
| 125 |
+
.keycap[data-k="W"]{grid-column:2;grid-row:1}
|
| 126 |
+
.keycap[data-k="A"]{grid-column:1;grid-row:2}
|
| 127 |
+
.keycap[data-k="S"]{grid-column:2;grid-row:2}
|
| 128 |
+
.keycap[data-k="D"]{grid-column:3;grid-row:2}
|
| 129 |
+
|
| 130 |
+
.look-info{font-size:12.5px;color:var(--muted);line-height:1.6}
|
| 131 |
+
.look-info b{color:var(--text)}
|
| 132 |
+
.look-badge{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--good);
|
| 133 |
+
border:1px solid var(--edge);border-radius:999px;padding:4px 10px;margin-top:8px}
|
| 134 |
+
.look-badge .d{width:7px;height:7px;border-radius:50%;background:var(--muted)}
|
| 135 |
+
.look-badge.active .d{background:var(--good);box-shadow:0 0 8px var(--good)}
|
| 136 |
+
.hint-line{font-size:11.5px;color:var(--muted);margin-top:14px;line-height:1.55}
|
| 137 |
.active-tags{font-size:12px;color:var(--good);min-height:16px;margin-top:8px}
|
| 138 |
.footnote{font-size:11px;color:var(--muted);margin-top:12px;line-height:1.5}
|
| 139 |
</style>
|
|
|
|
| 145 |
<div class="logo">🌍</div>
|
| 146 |
<div>
|
| 147 |
<h1>ABot-World — Interactive World Rollout</h1>
|
| 148 |
+
<p>Upload a starting frame, then steer a live action-conditioned world with WASD + mouse-look.</p>
|
| 149 |
</div>
|
| 150 |
</div>
|
| 151 |
<div class="links">
|
|
|
|
| 164 |
<div class="chip"><span class="dot" id="status-dot"></span><span id="status-label">Idle</span></div>
|
| 165 |
<div class="chip"><span id="fps-value">0</span> fps · <span id="frame-value">0</span> blk</div>
|
| 166 |
</div>
|
| 167 |
+
<div class="capture-hint" id="capture-hint">Click to capture · WASD to move · mouse to look · Esc to release</div>
|
| 168 |
<div class="overlay" id="overlay">
|
| 169 |
<div>
|
| 170 |
<div class="spinner" id="spinner" style="display:none"></div>
|
|
|
|
| 204 |
<button class="primary" id="start-btn" disabled>Start world</button>
|
| 205 |
<button class="ghost" id="stop-btn" disabled>Stop</button>
|
| 206 |
</div>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
|
| 210 |
+
<!-- Controls (below the video output) -->
|
| 211 |
+
<div class="card controls-section">
|
| 212 |
+
<div class="label">Live controls</div>
|
| 213 |
+
<div class="controls-grid">
|
| 214 |
+
<div>
|
| 215 |
+
<div class="label" style="margin-bottom:8px">Movement</div>
|
| 216 |
+
<div class="keyboard" id="keys">
|
| 217 |
<span class="keycap" data-k="W">W</span>
|
| 218 |
<span class="keycap" data-k="A">A</span>
|
| 219 |
<span class="keycap" data-k="S">S</span>
|
| 220 |
<span class="keycap" data-k="D">D</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
</div>
|
| 222 |
+
</div>
|
| 223 |
+
<div>
|
| 224 |
+
<div class="label" style="margin-bottom:8px">Look / pan</div>
|
| 225 |
+
<div class="look-info">
|
| 226 |
+
Move the world camera with your <b>mouse</b>. Click the view to capture
|
| 227 |
+
the pointer, then move the mouse to look & pan. Press <b>Esc</b> to release.
|
| 228 |
</div>
|
| 229 |
+
<div class="look-badge" id="look-badge"><span class="d"></span><span id="look-badge-text">Mouse-look idle</span></div>
|
| 230 |
</div>
|
| 231 |
</div>
|
| 232 |
+
<div class="hint-line">
|
| 233 |
+
<b>W A S D</b> — move & turn (hold to steer the next block) ·
|
| 234 |
+
<b>Mouse</b> — look / pan. Click the video to capture input.
|
| 235 |
+
</div>
|
| 236 |
+
<div class="active-tags" id="active-tags"></div>
|
| 237 |
</div>
|
| 238 |
</div>
|
| 239 |
|
|
|
|
| 244 |
const SESSION_ID = (crypto.randomUUID && crypto.randomUUID()) ||
|
| 245 |
(`s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`);
|
| 246 |
|
| 247 |
+
// Movement keys are driven by the keyboard (WASD); look/pan (I J K L) is now
|
| 248 |
+
// driven by mouse movement (pointer-lock mouse-look) — see mouse-look block.
|
| 249 |
+
const MOVE_KEYS = new Set(["W","A","S","D"]);
|
| 250 |
+
const LOOK_KEYS = new Set(["I","J","K","L"]);
|
| 251 |
const KEY_FROM_CODE = {
|
| 252 |
KeyW:"W", KeyA:"A", KeyS:"S", KeyD:"D",
|
|
|
|
| 253 |
};
|
| 254 |
|
| 255 |
const state = {
|
|
|
|
| 275 |
const promptEl = $("prompt"), seedEl = $("seed");
|
| 276 |
const startBtn = $("start-btn"), stopBtn = $("stop-btn");
|
| 277 |
const activeTags = $("active-tags");
|
| 278 |
+
const keycaps = Array.from(document.querySelectorAll(".keycap[data-k]"));
|
| 279 |
+
const lookBadge = $("look-badge"), lookBadgeText = $("look-badge-text");
|
| 280 |
|
| 281 |
// --- UI helpers ---
|
| 282 |
function setStatus(kind, label){
|
|
|
|
| 534 |
function stopCtrlLoop(){ if(ctrlTimer){ clearInterval(ctrlTimer); ctrlTimer = null; } }
|
| 535 |
|
| 536 |
function renderKeys(){
|
| 537 |
+
// Highlight the on-screen WASD keys that are currently held.
|
| 538 |
keycaps.forEach(c=>c.classList.toggle("active", state.pressed.has(c.dataset.k)));
|
| 539 |
+
// Look/pan status badge reflects live mouse-look direction(s).
|
| 540 |
+
const look = Array.from(state.pressed).filter(k=>LOOK_KEYS.has(k));
|
| 541 |
+
const looking = look.length > 0;
|
| 542 |
+
lookBadge.classList.toggle("active", looking);
|
| 543 |
+
const LOOK_LABEL = { I:"up", K:"down", J:"left", L:"right" };
|
| 544 |
+
lookBadgeText.textContent = looking
|
| 545 |
+
? "Looking " + look.map(k=>LOOK_LABEL[k]).join(" + ")
|
| 546 |
+
: (state.capturing ? "Mouse-look ready" : "Mouse-look idle");
|
| 547 |
const held = Array.from(state.pressed);
|
| 548 |
+
activeTags.textContent = held.length ? "Active: " + held.join(" + ") : "";
|
| 549 |
}
|
| 550 |
|
| 551 |
+
// --- Movement keyboard capture (WASD) + mouse-look pointer capture ---
|
| 552 |
+
// Clicking the stage captures input. We request Pointer Lock so raw mouse
|
| 553 |
+
// movement drives look/pan (I J K L) instead of the old IJKL keys.
|
| 554 |
+
function requestCapture(){
|
| 555 |
+
state.capturing = true;
|
| 556 |
+
stage.focus();
|
| 557 |
+
if(stage.requestPointerLock){
|
| 558 |
+
try{ stage.requestPointerLock(); }catch{}
|
| 559 |
+
}
|
| 560 |
+
renderKeys();
|
| 561 |
+
}
|
| 562 |
+
stage.addEventListener("click", requestCapture);
|
| 563 |
+
|
| 564 |
document.addEventListener("keydown",(e)=>{
|
| 565 |
if(!state.capturing) return;
|
| 566 |
+
if(e.code === "Escape"){
|
| 567 |
+
state.capturing=false; state.pressed.clear();
|
| 568 |
+
if(document.exitPointerLock) try{ document.exitPointerLock(); }catch{}
|
| 569 |
+
renderKeys(); return;
|
| 570 |
+
}
|
| 571 |
const k = KEY_FROM_CODE[e.code];
|
| 572 |
+
if(k && MOVE_KEYS.has(k)){ e.preventDefault(); state.pressed.add(k); renderKeys(); }
|
| 573 |
});
|
| 574 |
document.addEventListener("keyup",(e)=>{
|
| 575 |
const k = KEY_FROM_CODE[e.code];
|
| 576 |
if(k){ state.pressed.delete(k); renderKeys(); }
|
| 577 |
});
|
| 578 |
|
| 579 |
+
// --- Mouse-look: convert pointer movement into look/pan keys (I J K L) ---
|
| 580 |
+
// I = look up, K = look down, J = look left, L = look right. A small deadzone
|
| 581 |
+
// avoids jitter; each look direction is held briefly after motion then released
|
| 582 |
+
// so a continuous drag/move keeps looking while a still mouse stops looking.
|
| 583 |
+
const LOOK_DEADZONE = 2; // px of movement before a look direction fires
|
| 584 |
+
let lookReleaseTimer = null;
|
| 585 |
+
function applyMouseLook(dx, dy){
|
| 586 |
+
if(!state.capturing) return;
|
| 587 |
+
// Clear previous look keys; movement (WASD) is untouched.
|
| 588 |
+
let changed = false;
|
| 589 |
+
for(const k of LOOK_KEYS){ if(state.pressed.delete(k)) changed = true; }
|
| 590 |
+
|
| 591 |
+
if(Math.abs(dx) > LOOK_DEADZONE){
|
| 592 |
+
state.pressed.add(dx > 0 ? "L" : "J"); changed = true; // right / left
|
| 593 |
+
}
|
| 594 |
+
if(Math.abs(dy) > LOOK_DEADZONE){
|
| 595 |
+
state.pressed.add(dy > 0 ? "K" : "I"); changed = true; // down / up
|
| 596 |
+
}
|
| 597 |
+
if(changed) renderKeys();
|
| 598 |
+
|
| 599 |
+
// Auto-release look keys shortly after the mouse stops moving.
|
| 600 |
+
if(lookReleaseTimer) clearTimeout(lookReleaseTimer);
|
| 601 |
+
lookReleaseTimer = setTimeout(()=>{
|
| 602 |
+
let cleared = false;
|
| 603 |
+
for(const k of LOOK_KEYS){ if(state.pressed.delete(k)) cleared = true; }
|
| 604 |
+
if(cleared) renderKeys();
|
| 605 |
+
}, 120);
|
| 606 |
+
}
|
| 607 |
+
document.addEventListener("mousemove",(e)=>{
|
| 608 |
+
if(!state.capturing) return;
|
| 609 |
+
// Prefer raw movement deltas (available under pointer lock); fall back to
|
| 610 |
+
// client-space deltas for browsers that deny/lack pointer lock.
|
| 611 |
+
const dx = (e.movementX !== undefined) ? e.movementX : 0;
|
| 612 |
+
const dy = (e.movementY !== undefined) ? e.movementY : 0;
|
| 613 |
+
applyMouseLook(dx, dy);
|
| 614 |
+
});
|
| 615 |
+
// Track pointer-lock state so the badge reflects capture accurately.
|
| 616 |
+
document.addEventListener("pointerlockchange", ()=>{
|
| 617 |
+
if(document.pointerLockElement !== stage){
|
| 618 |
+
// Pointer released (e.g. Esc pressed by the browser): drop look keys.
|
| 619 |
+
let cleared = false;
|
| 620 |
+
for(const k of LOOK_KEYS){ if(state.pressed.delete(k)) cleared = true; }
|
| 621 |
+
if(cleared) renderKeys();
|
| 622 |
+
}
|
| 623 |
+
});
|
| 624 |
+
|
| 625 |
promptEl.addEventListener("input", ()=>{ state.prompt = promptEl.value; });
|
| 626 |
|
| 627 |
// --- Start / Stop ---
|