Spaces:
Running on Zero
Running on Zero
Revert look/pan to keyboard IJKL (replacing mouse-look), keep WASD movement
Browse files- index.html +23 -66
index.html
CHANGED
|
@@ -126,6 +126,11 @@
|
|
| 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)}
|
|
@@ -145,7 +150,7 @@
|
|
| 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 +
|
| 149 |
</div>
|
| 150 |
</div>
|
| 151 |
<div class="links">
|
|
@@ -164,7 +169,7 @@
|
|
| 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 ·
|
| 168 |
<div class="overlay" id="overlay">
|
| 169 |
<div>
|
| 170 |
<div class="spinner" id="spinner" style="display:none"></div>
|
|
@@ -188,16 +193,18 @@
|
|
| 188 |
</div>
|
| 189 |
<div>
|
| 190 |
<div class="label" style="margin-bottom:8px">Look / pan</div>
|
| 191 |
-
<div class="look-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
| 194 |
</div>
|
| 195 |
-
<div class="look-badge" id="look-badge"><span class="d"></span><span id="look-badge-text">
|
| 196 |
</div>
|
| 197 |
</div>
|
| 198 |
<div class="hint-line">
|
| 199 |
<b>W A S D</b> — move & turn (hold to steer the next block) ·
|
| 200 |
-
<b>
|
| 201 |
</div>
|
| 202 |
<div class="active-tags" id="active-tags"></div>
|
| 203 |
</div>
|
|
@@ -243,12 +250,13 @@ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.m
|
|
| 243 |
const SESSION_ID = (crypto.randomUUID && crypto.randomUUID()) ||
|
| 244 |
(`s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`);
|
| 245 |
|
| 246 |
-
// Movement keys are driven by the keyboard (WASD); look/pan
|
| 247 |
-
//
|
| 248 |
const MOVE_KEYS = new Set(["W","A","S","D"]);
|
| 249 |
const LOOK_KEYS = new Set(["I","J","K","L"]);
|
| 250 |
const KEY_FROM_CODE = {
|
| 251 |
KeyW:"W", KeyA:"A", KeyS:"S", KeyD:"D",
|
|
|
|
| 252 |
};
|
| 253 |
|
| 254 |
const state = {
|
|
@@ -533,29 +541,25 @@ function startCtrlLoop(){
|
|
| 533 |
function stopCtrlLoop(){ if(ctrlTimer){ clearInterval(ctrlTimer); ctrlTimer = null; } }
|
| 534 |
|
| 535 |
function renderKeys(){
|
| 536 |
-
// Highlight the on-screen WASD keys that are currently held.
|
| 537 |
keycaps.forEach(c=>c.classList.toggle("active", state.pressed.has(c.dataset.k)));
|
| 538 |
-
// Look/pan status badge reflects
|
| 539 |
const look = Array.from(state.pressed).filter(k=>LOOK_KEYS.has(k));
|
| 540 |
const looking = look.length > 0;
|
| 541 |
lookBadge.classList.toggle("active", looking);
|
| 542 |
const LOOK_LABEL = { I:"up", K:"down", J:"left", L:"right" };
|
| 543 |
lookBadgeText.textContent = looking
|
| 544 |
? "Looking " + look.map(k=>LOOK_LABEL[k]).join(" + ")
|
| 545 |
-
: (state.capturing ? "
|
| 546 |
const held = Array.from(state.pressed);
|
| 547 |
activeTags.textContent = held.length ? "Active: " + held.join(" + ") : "";
|
| 548 |
}
|
| 549 |
|
| 550 |
-
// ---
|
| 551 |
-
// Clicking the stage captures input
|
| 552 |
-
// movement drives look/pan (I J K L) instead of the old IJKL keys.
|
| 553 |
function requestCapture(){
|
| 554 |
state.capturing = true;
|
| 555 |
stage.focus();
|
| 556 |
-
if(stage.requestPointerLock){
|
| 557 |
-
try{ stage.requestPointerLock(); }catch{}
|
| 558 |
-
}
|
| 559 |
renderKeys();
|
| 560 |
}
|
| 561 |
stage.addEventListener("click", requestCapture);
|
|
@@ -564,63 +568,16 @@ document.addEventListener("keydown",(e)=>{
|
|
| 564 |
if(!state.capturing) return;
|
| 565 |
if(e.code === "Escape"){
|
| 566 |
state.capturing=false; state.pressed.clear();
|
| 567 |
-
if(document.exitPointerLock) try{ document.exitPointerLock(); }catch{}
|
| 568 |
renderKeys(); return;
|
| 569 |
}
|
| 570 |
const k = KEY_FROM_CODE[e.code];
|
| 571 |
-
if(k && MOVE_KEYS.has(k)){ e.preventDefault(); state.pressed.add(k); renderKeys(); }
|
| 572 |
});
|
| 573 |
document.addEventListener("keyup",(e)=>{
|
| 574 |
const k = KEY_FROM_CODE[e.code];
|
| 575 |
if(k){ state.pressed.delete(k); renderKeys(); }
|
| 576 |
});
|
| 577 |
|
| 578 |
-
// --- Mouse-look: convert pointer movement into look/pan keys (I J K L) ---
|
| 579 |
-
// I = look up, K = look down, J = look left, L = look right. A small deadzone
|
| 580 |
-
// avoids jitter; each look direction is held briefly after motion then released
|
| 581 |
-
// so a continuous drag/move keeps looking while a still mouse stops looking.
|
| 582 |
-
const LOOK_DEADZONE = 2; // px of movement before a look direction fires
|
| 583 |
-
let lookReleaseTimer = null;
|
| 584 |
-
function applyMouseLook(dx, dy){
|
| 585 |
-
if(!state.capturing) return;
|
| 586 |
-
// Clear previous look keys; movement (WASD) is untouched.
|
| 587 |
-
let changed = false;
|
| 588 |
-
for(const k of LOOK_KEYS){ if(state.pressed.delete(k)) changed = true; }
|
| 589 |
-
|
| 590 |
-
if(Math.abs(dx) > LOOK_DEADZONE){
|
| 591 |
-
state.pressed.add(dx > 0 ? "L" : "J"); changed = true; // right / left
|
| 592 |
-
}
|
| 593 |
-
if(Math.abs(dy) > LOOK_DEADZONE){
|
| 594 |
-
state.pressed.add(dy > 0 ? "K" : "I"); changed = true; // down / up
|
| 595 |
-
}
|
| 596 |
-
if(changed) renderKeys();
|
| 597 |
-
|
| 598 |
-
// Auto-release look keys shortly after the mouse stops moving.
|
| 599 |
-
if(lookReleaseTimer) clearTimeout(lookReleaseTimer);
|
| 600 |
-
lookReleaseTimer = setTimeout(()=>{
|
| 601 |
-
let cleared = false;
|
| 602 |
-
for(const k of LOOK_KEYS){ if(state.pressed.delete(k)) cleared = true; }
|
| 603 |
-
if(cleared) renderKeys();
|
| 604 |
-
}, 120);
|
| 605 |
-
}
|
| 606 |
-
document.addEventListener("mousemove",(e)=>{
|
| 607 |
-
if(!state.capturing) return;
|
| 608 |
-
// Prefer raw movement deltas (available under pointer lock); fall back to
|
| 609 |
-
// client-space deltas for browsers that deny/lack pointer lock.
|
| 610 |
-
const dx = (e.movementX !== undefined) ? e.movementX : 0;
|
| 611 |
-
const dy = (e.movementY !== undefined) ? e.movementY : 0;
|
| 612 |
-
applyMouseLook(dx, dy);
|
| 613 |
-
});
|
| 614 |
-
// Track pointer-lock state so the badge reflects capture accurately.
|
| 615 |
-
document.addEventListener("pointerlockchange", ()=>{
|
| 616 |
-
if(document.pointerLockElement !== stage){
|
| 617 |
-
// Pointer released (e.g. Esc pressed by the browser): drop look keys.
|
| 618 |
-
let cleared = false;
|
| 619 |
-
for(const k of LOOK_KEYS){ if(state.pressed.delete(k)) cleared = true; }
|
| 620 |
-
if(cleared) renderKeys();
|
| 621 |
-
}
|
| 622 |
-
});
|
| 623 |
-
|
| 624 |
promptEl.addEventListener("input", ()=>{ state.prompt = promptEl.value; });
|
| 625 |
|
| 626 |
// --- Start / Stop ---
|
|
|
|
| 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 |
+
/* IJKL look cross placement inside its own 3x2 grid */
|
| 130 |
+
.keycap[data-k="I"]{grid-column:2;grid-row:1}
|
| 131 |
+
.keycap[data-k="J"]{grid-column:1;grid-row:2}
|
| 132 |
+
.keycap[data-k="K"]{grid-column:2;grid-row:2}
|
| 133 |
+
.keycap[data-k="L"]{grid-column:3;grid-row:2}
|
| 134 |
|
| 135 |
.look-info{font-size:12.5px;color:var(--muted);line-height:1.6}
|
| 136 |
.look-info b{color:var(--text)}
|
|
|
|
| 150 |
<div class="logo">🌍</div>
|
| 151 |
<div>
|
| 152 |
<h1>ABot-World — Interactive World Rollout</h1>
|
| 153 |
+
<p>Upload a starting frame, then steer a live action-conditioned world with WASD + IJKL.</p>
|
| 154 |
</div>
|
| 155 |
</div>
|
| 156 |
<div class="links">
|
|
|
|
| 169 |
<div class="chip"><span class="dot" id="status-dot"></span><span id="status-label">Idle</span></div>
|
| 170 |
<div class="chip"><span id="fps-value">0</span> fps · <span id="frame-value">0</span> blk</div>
|
| 171 |
</div>
|
| 172 |
+
<div class="capture-hint" id="capture-hint">Click to capture · WASD to move · IJKL to look · Esc to release</div>
|
| 173 |
<div class="overlay" id="overlay">
|
| 174 |
<div>
|
| 175 |
<div class="spinner" id="spinner" style="display:none"></div>
|
|
|
|
| 193 |
</div>
|
| 194 |
<div>
|
| 195 |
<div class="label" style="margin-bottom:8px">Look / pan</div>
|
| 196 |
+
<div class="keyboard" id="look-keys">
|
| 197 |
+
<span class="keycap" data-k="I">I</span>
|
| 198 |
+
<span class="keycap" data-k="J">J</span>
|
| 199 |
+
<span class="keycap" data-k="K">K</span>
|
| 200 |
+
<span class="keycap" data-k="L">L</span>
|
| 201 |
</div>
|
| 202 |
+
<div class="look-badge" id="look-badge"><span class="d"></span><span id="look-badge-text">Look idle</span></div>
|
| 203 |
</div>
|
| 204 |
</div>
|
| 205 |
<div class="hint-line">
|
| 206 |
<b>W A S D</b> — move & turn (hold to steer the next block) ·
|
| 207 |
+
<b>I J K L</b> — look / pan. Click the video to capture input.
|
| 208 |
</div>
|
| 209 |
<div class="active-tags" id="active-tags"></div>
|
| 210 |
</div>
|
|
|
|
| 250 |
const SESSION_ID = (crypto.randomUUID && crypto.randomUUID()) ||
|
| 251 |
(`s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`);
|
| 252 |
|
| 253 |
+
// Movement keys are driven by the keyboard (WASD); look/pan is driven by the
|
| 254 |
+
// keyboard too (I J K L) — both are handled as held-key controls.
|
| 255 |
const MOVE_KEYS = new Set(["W","A","S","D"]);
|
| 256 |
const LOOK_KEYS = new Set(["I","J","K","L"]);
|
| 257 |
const KEY_FROM_CODE = {
|
| 258 |
KeyW:"W", KeyA:"A", KeyS:"S", KeyD:"D",
|
| 259 |
+
KeyI:"I", KeyJ:"J", KeyK:"K", KeyL:"L",
|
| 260 |
};
|
| 261 |
|
| 262 |
const state = {
|
|
|
|
| 541 |
function stopCtrlLoop(){ if(ctrlTimer){ clearInterval(ctrlTimer); ctrlTimer = null; } }
|
| 542 |
|
| 543 |
function renderKeys(){
|
| 544 |
+
// Highlight the on-screen WASD + IJKL keys that are currently held.
|
| 545 |
keycaps.forEach(c=>c.classList.toggle("active", state.pressed.has(c.dataset.k)));
|
| 546 |
+
// Look/pan status badge reflects the held look keys (I J K L).
|
| 547 |
const look = Array.from(state.pressed).filter(k=>LOOK_KEYS.has(k));
|
| 548 |
const looking = look.length > 0;
|
| 549 |
lookBadge.classList.toggle("active", looking);
|
| 550 |
const LOOK_LABEL = { I:"up", K:"down", J:"left", L:"right" };
|
| 551 |
lookBadgeText.textContent = looking
|
| 552 |
? "Looking " + look.map(k=>LOOK_LABEL[k]).join(" + ")
|
| 553 |
+
: (state.capturing ? "Look ready" : "Look idle");
|
| 554 |
const held = Array.from(state.pressed);
|
| 555 |
activeTags.textContent = held.length ? "Active: " + held.join(" + ") : "";
|
| 556 |
}
|
| 557 |
|
| 558 |
+
// --- Keyboard capture: WASD (move) + IJKL (look/pan) ---
|
| 559 |
+
// Clicking the stage captures input; held keys are sent ~10x/s to the server.
|
|
|
|
| 560 |
function requestCapture(){
|
| 561 |
state.capturing = true;
|
| 562 |
stage.focus();
|
|
|
|
|
|
|
|
|
|
| 563 |
renderKeys();
|
| 564 |
}
|
| 565 |
stage.addEventListener("click", requestCapture);
|
|
|
|
| 568 |
if(!state.capturing) return;
|
| 569 |
if(e.code === "Escape"){
|
| 570 |
state.capturing=false; state.pressed.clear();
|
|
|
|
| 571 |
renderKeys(); return;
|
| 572 |
}
|
| 573 |
const k = KEY_FROM_CODE[e.code];
|
| 574 |
+
if(k && (MOVE_KEYS.has(k) || LOOK_KEYS.has(k))){ e.preventDefault(); state.pressed.add(k); renderKeys(); }
|
| 575 |
});
|
| 576 |
document.addEventListener("keyup",(e)=>{
|
| 577 |
const k = KEY_FROM_CODE[e.code];
|
| 578 |
if(k){ state.pressed.delete(k); renderKeys(); }
|
| 579 |
});
|
| 580 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
promptEl.addEventListener("input", ()=>{ state.prompt = promptEl.value; });
|
| 582 |
|
| 583 |
// --- Start / Stop ---
|