feat: duck-head modal logo, input lockout mid-roll, quieter quack
Browse files- welcome modal uses the site's duck-head mark (open variant on hover)
instead of the wordmark sticker
- all movement/mode inputs are ignored while a roll is in flight; the
existing deterministic completion check (tipped then upright, 3 s cap)
releases the lock, no arbitrary timer
- quack.wav only plays on the gamepad right trigger; mode/colour changes
keep the silent jaw flap
Co-authored-by: Cursor <cursoragent@cursor.com>
- .gitattributes +1 -0
- assets/{logo-wordmark.png → duck-head-mark-open.webp} +2 -2
- assets/duck-head-mark.webp +3 -0
- index.html +16 -6
- rl.js +14 -2
.gitattributes
CHANGED
|
@@ -36,4 +36,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 36 |
*.stl filter=lfs diff=lfs merge=lfs -text
|
| 37 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
*.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 39 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 36 |
*.stl filter=lfs diff=lfs merge=lfs -text
|
| 37 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 40 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
assets/{logo-wordmark.png → duck-head-mark-open.webp}
RENAMED
|
File without changes
|
assets/duck-head-mark.webp
ADDED
|
Git LFS Details
|
index.html
CHANGED
|
@@ -281,12 +281,18 @@
|
|
| 281 |
from { transform: translateY(18px) scale(0.94); opacity: 0; }
|
| 282 |
to { transform: none; opacity: 1; }
|
| 283 |
}
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
| 287 |
margin: 0.3rem auto 0;
|
| 288 |
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.45));
|
| 289 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
#welcome .subtitle {
|
| 291 |
margin-top: 0.55rem;
|
| 292 |
font-size: 0.62rem;
|
|
@@ -400,7 +406,10 @@
|
|
| 400 |
|
| 401 |
<div id="welcome" hidden>
|
| 402 |
<div class="card" role="dialog" aria-modal="true" aria-label="Microduck playground">
|
| 403 |
-
<
|
|
|
|
|
|
|
|
|
|
| 404 |
<div class="subtitle">RL playground</div>
|
| 405 |
<p class="tagline">
|
| 406 |
A real robot brain in your browser: MuJoCo physics compiled to
|
|
@@ -427,7 +436,8 @@
|
|
| 427 |
// on the private Space (rl.js hasn't installed its signer yet).
|
| 428 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 429 |
const signedUrl = (u) => (sign ? `${u}?__sign=${encodeURIComponent(sign)}` : u);
|
| 430 |
-
document.getElementById("logo-
|
|
|
|
| 431 |
const open = () => {
|
| 432 |
modal.hidden = false;
|
| 433 |
modal.classList.remove("closing");
|
|
@@ -458,7 +468,7 @@
|
|
| 458 |
appended: a plain src attribute can 401 when the browser blocks the
|
| 459 |
*.static.hf.space auth cookie inside the hub iframe. -->
|
| 460 |
<script type="module">
|
| 461 |
-
const V = "
|
| 462 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 463 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 464 |
import(`./rl.js?v=${V}${suffix}`);
|
|
|
|
| 281 |
from { transform: translateY(18px) scale(0.94); opacity: 0; }
|
| 282 |
to { transform: none; opacity: 1; }
|
| 283 |
}
|
| 284 |
+
/* Duck-head mark from the site header: closed by default, open on
|
| 285 |
+
hover (same swap as the site). */
|
| 286 |
+
#welcome .logo-head {
|
| 287 |
+
position: relative;
|
| 288 |
+
width: 7.2rem;
|
| 289 |
margin: 0.3rem auto 0;
|
| 290 |
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.45));
|
| 291 |
}
|
| 292 |
+
#welcome .logo-head img { display: block; width: 100%; }
|
| 293 |
+
#welcome .logo-head .open { position: absolute; inset: 0; opacity: 0; }
|
| 294 |
+
#welcome .logo-head:hover .open { opacity: 1; }
|
| 295 |
+
#welcome .logo-head:hover .closed { opacity: 0; }
|
| 296 |
#welcome .subtitle {
|
| 297 |
margin-top: 0.55rem;
|
| 298 |
font-size: 0.62rem;
|
|
|
|
| 406 |
|
| 407 |
<div id="welcome" hidden>
|
| 408 |
<div class="card" role="dialog" aria-modal="true" aria-label="Microduck playground">
|
| 409 |
+
<div class="logo-head">
|
| 410 |
+
<img class="closed" id="logo-head-closed" alt="Microduck" />
|
| 411 |
+
<img class="open" id="logo-head-open" alt="" aria-hidden="true" />
|
| 412 |
+
</div>
|
| 413 |
<div class="subtitle">RL playground</div>
|
| 414 |
<p class="tagline">
|
| 415 |
A real robot brain in your browser: MuJoCo physics compiled to
|
|
|
|
| 436 |
// on the private Space (rl.js hasn't installed its signer yet).
|
| 437 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 438 |
const signedUrl = (u) => (sign ? `${u}?__sign=${encodeURIComponent(sign)}` : u);
|
| 439 |
+
document.getElementById("logo-head-closed").src = signedUrl("./assets/duck-head-mark.webp");
|
| 440 |
+
document.getElementById("logo-head-open").src = signedUrl("./assets/duck-head-mark-open.webp");
|
| 441 |
const open = () => {
|
| 442 |
modal.hidden = false;
|
| 443 |
modal.classList.remove("closing");
|
|
|
|
| 468 |
appended: a plain src attribute can 401 when the browser blocks the
|
| 469 |
*.static.hf.space auth cookie inside the hub iframe. -->
|
| 470 |
<script type="module">
|
| 471 |
+
const V = "23"; // bump on rl.js changes to defeat heuristic caching
|
| 472 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 473 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 474 |
import(`./rl.js?v=${V}${suffix}`);
|
rl.js
CHANGED
|
@@ -208,7 +208,11 @@ let sitFlag = 0;
|
|
| 208 |
// The twist the policy actually receives: live gamepad sticks win over the
|
| 209 |
// held keys. No input means zero command - the duck stands in place.
|
| 210 |
// Shared by buildObs and the HUD mini-sticks so they can never disagree.
|
|
|
|
|
|
|
|
|
|
| 211 |
function effectiveCmd() {
|
|
|
|
| 212 |
return padActive ? padCmd : velCmd;
|
| 213 |
}
|
| 214 |
// One-shot roll tracking: trigger time + whether the trunk actually
|
|
@@ -465,8 +469,13 @@ let padJaw = 0;
|
|
| 465 |
// is swallowed and sound simply starts working after the first click/key.
|
| 466 |
const quackSound = new Audio(signed("./assets/quack.wav"));
|
| 467 |
quackSound.volume = 0.7;
|
|
|
|
|
|
|
| 468 |
const quack = () => {
|
| 469 |
quackAt = performance.now();
|
|
|
|
|
|
|
|
|
|
| 470 |
quackSound.currentTime = 0;
|
| 471 |
quackSound.play().catch(() => {});
|
| 472 |
};
|
|
@@ -505,7 +514,7 @@ function renderStats() {
|
|
| 505 |
|
| 506 |
// Mini sticks: the dot mirrors the effective twist, lit yellow while the
|
| 507 |
// user is actually driving.
|
| 508 |
-
const manual = padActive || held.size > 0;
|
| 509 |
const yN = vx >= 0 ? vx / VEL_FWD : vx / -VEL_BACK;
|
| 510 |
dotMove.style.transform = `translate(${(-vy / VEL_LAT) * STICK_R}px, ${-yN * STICK_R}px)`;
|
| 511 |
dotTurn.style.transform = `translate(${(-wz / VEL_ANG) * STICK_R}px, 0px)`;
|
|
@@ -636,7 +645,7 @@ pollPad = function pollGamepad() {
|
|
| 636 |
const rt = gp.buttons[7]?.value ?? 0;
|
| 637 |
const lt = gp.buttons[6]?.value ?? 0;
|
| 638 |
padJaw = Math.max(rt, lt);
|
| 639 |
-
if (padPrev.rt < 0.3 && rt >= 0.3)
|
| 640 |
padPrev.rt = rt;
|
| 641 |
};
|
| 642 |
|
|
@@ -645,6 +654,9 @@ pollPad = function pollGamepad() {
|
|
| 645 |
const modeLabel = document.getElementById("mode-label");
|
| 646 |
|
| 647 |
function setMode(next) {
|
|
|
|
|
|
|
|
|
|
| 648 |
quack();
|
| 649 |
rollRun = null;
|
| 650 |
if (next !== "sit") {
|
|
|
|
| 208 |
// The twist the policy actually receives: live gamepad sticks win over the
|
| 209 |
// held keys. No input means zero command - the duck stands in place.
|
| 210 |
// Shared by buildObs and the HUD mini-sticks so they can never disagree.
|
| 211 |
+
// Mid-roll every movement input is ignored (zero twist) until the roll
|
| 212 |
+
// hands back to walk on its own - steering would only knock the roll over.
|
| 213 |
+
const ZERO_CMD = new Float32Array(3);
|
| 214 |
function effectiveCmd() {
|
| 215 |
+
if (mode === "roll") return ZERO_CMD;
|
| 216 |
return padActive ? padCmd : velCmd;
|
| 217 |
}
|
| 218 |
// One-shot roll tracking: trigger time + whether the trunk actually
|
|
|
|
| 469 |
// is swallowed and sound simply starts working after the first click/key.
|
| 470 |
const quackSound = new Audio(signed("./assets/quack.wav"));
|
| 471 |
quackSound.volume = 0.7;
|
| 472 |
+
// Silent jaw flap for mode/colour changes; the actual quack sound only
|
| 473 |
+
// plays on the gamepad right trigger (anything more gets noisy fast).
|
| 474 |
const quack = () => {
|
| 475 |
quackAt = performance.now();
|
| 476 |
+
};
|
| 477 |
+
const quackLoud = () => {
|
| 478 |
+
quack();
|
| 479 |
quackSound.currentTime = 0;
|
| 480 |
quackSound.play().catch(() => {});
|
| 481 |
};
|
|
|
|
| 514 |
|
| 515 |
// Mini sticks: the dot mirrors the effective twist, lit yellow while the
|
| 516 |
// user is actually driving.
|
| 517 |
+
const manual = (padActive || held.size > 0) && mode !== "roll";
|
| 518 |
const yN = vx >= 0 ? vx / VEL_FWD : vx / -VEL_BACK;
|
| 519 |
dotMove.style.transform = `translate(${(-vy / VEL_LAT) * STICK_R}px, ${-yN * STICK_R}px)`;
|
| 520 |
dotTurn.style.transform = `translate(${(-wz / VEL_ANG) * STICK_R}px, 0px)`;
|
|
|
|
| 645 |
const rt = gp.buttons[7]?.value ?? 0;
|
| 646 |
const lt = gp.buttons[6]?.value ?? 0;
|
| 647 |
padJaw = Math.max(rt, lt);
|
| 648 |
+
if (padPrev.rt < 0.3 && rt >= 0.3) quackLoud();
|
| 649 |
padPrev.rt = rt;
|
| 650 |
};
|
| 651 |
|
|
|
|
| 654 |
const modeLabel = document.getElementById("mode-label");
|
| 655 |
|
| 656 |
function setMode(next) {
|
| 657 |
+
// No policy switching mid-roll: the roll ends on its own (upright again
|
| 658 |
+
// or expired) and returns to walk - switching now would floor the duck.
|
| 659 |
+
if (mode === "roll" && rollRun) return;
|
| 660 |
quack();
|
| 661 |
rollRun = null;
|
| 662 |
if (next !== "sit") {
|