File size: 16,686 Bytes
094478c
c8f39b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b065e9
 
c8f39b4
2b065e9
 
 
c8f39b4
 
 
 
 
2b065e9
c8f39b4
2b065e9
c8f39b4
 
 
 
 
 
 
 
 
 
 
 
 
 
2b065e9
c8f39b4
 
 
2b065e9
 
 
 
 
 
 
 
 
 
c8f39b4
 
2b065e9
68e86d4
2b065e9
c8f39b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68e86d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8f39b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b065e9
 
 
c8f39b4
68e86d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9cc1c
68e86d4
 
 
 
04f936c
 
68e86d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8f39b4
 
 
 
68e86d4
 
 
 
 
 
c8f39b4
 
 
 
 
 
 
2b065e9
 
 
 
 
f6acb84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53d6d58
 
 
 
 
0df9a77
 
f6acb84
53d6d58
 
 
 
1e9f6d1
 
 
f6acb84
1e9f6d1
f6acb84
1e9f6d1
f6acb84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8f39b4
 
 
 
 
2b065e9
 
 
 
68e86d4
 
 
 
c8f39b4
2b065e9
68e86d4
2b065e9
c8f39b4
 
68e86d4
 
 
 
 
 
 
 
 
 
 
9f9cc1c
 
 
 
 
68e86d4
 
9f9cc1c
 
 
 
68e86d4
c8f39b4
 
 
 
f6acb84
1e9f6d1
53d6d58
 
 
 
1e9f6d1
f6acb84
 
 
 
 
 
0df9a77
f6acb84
 
 
 
 
 
 
 
 
 
4348e2e
f6acb84
 
 
1e9f6d1
 
 
 
53d6d58
 
f6acb84
 
 
 
 
 
 
 
4348e2e
f6acb84
 
 
 
 
 
 
 
 
c8f39b4
 
 
 
 
 
 
 
35110ea
 
 
 
f402bd3
35110ea
68e86d4
 
35110ea
c8f39b4
094478c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Microduck RL playground</title>
    <style>
      :root {
        --ink: #08080c;
        --yellow: #ffd23f;
      }
      * { margin: 0; padding: 0; box-sizing: border-box; }
      html, body { height: 100%; }
      body {
        background: var(--ink);
        color: rgba(255, 255, 255, 0.85);
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        overflow: hidden;
      }
      #scene { position: fixed; inset: 0; }
      #scene canvas { display: block; }
      .halftone {
        position: fixed;
        inset: 0;
        pointer-events: none;
        background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1.5px);
        background-size: 21px 21px;
        mask-image: linear-gradient(to bottom, transparent 30%, black 100%);
        -webkit-mask-image: linear-gradient(to bottom, transparent 30%, black 100%);
      }
      /* Top-left corner: what this is */
      #brand {
        position: fixed;
        top: 1.4rem;
        left: 1.6rem;
        z-index: 10;
        font-size: 0.72rem;
        font-weight: 600;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.55);
        line-height: 1.8;
      }
      #brand b { color: var(--yellow); font-weight: 600; }
      #loading {
        position: fixed;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 0.8rem;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.45);
        text-align: center;
        padding: 0 2rem;
        z-index: 20;
      }
      /* Bottom edges: controls left, key hints right */
      #hud {
        position: fixed;
        bottom: 1.4rem;
        left: 1.6rem;
        right: 1.6rem;
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        gap: 1rem;
        z-index: 10;
        pointer-events: none;
      }
      #hud .stack {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: 0.45rem;
        pointer-events: auto;
      }
      #hud .row {
        display: flex;
        align-items: center;
        gap: 0.5rem;
      }
      #hud button {
        appearance: none;
        border: 1px solid rgba(255, 255, 255, 0.14);
        background: rgba(255, 255, 255, 0.04);
        color: rgba(255, 255, 255, 0.8);
        font: inherit;
        font-size: 0.72rem;
        font-weight: 600;
        letter-spacing: 0.1em;
        text-transform: uppercase;
        padding: 0.5rem 1rem;
        border-radius: 999px;
        cursor: pointer;
        transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease;
      }
      #hud button:hover { background: rgba(255, 255, 255, 0.1); }
      #hud button.on {
        background: #ffc61e;
        border-color: #ffc61e;
        color: #08080c;
      }
      /* Current-state label: quiet line above the telemetry, slot-machine
         roll on change */
      .mode {
        position: relative;
        overflow: hidden;
        height: 1.2em;
        min-width: 8ch;
        font-size: 0.72rem;
        font-weight: 600;
        letter-spacing: 0.2em;
        text-transform: uppercase;
        color: rgba(255, 210, 63, 0.8);
        text-align: right;
      }
      .mode .mode-text {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        line-height: 1.2;
        transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.3s ease;
      }
      .mode .mode-text.in { transform: translateY(130%); opacity: 0; }
      .mode .mode-text.out { transform: translateY(-130%); opacity: 0; }
      /* Colour swatches */
      #swatches { gap: 0.6rem; }
      #swatches button {
        width: 1.5rem;
        height: 1.5rem;
        padding: 0;
        border-radius: 50%;
        border: 2px solid rgba(255, 255, 255, 0.18);
      }
      #swatches button:hover { transform: scale(1.15); }
      #swatches button.on {
        border-color: #fff;
        transform: scale(1.15);
      }
      #hud .keys {
        font-size: 0.7rem;
        letter-spacing: 0.08em;
        color: rgba(255, 255, 255, 0.4);
        text-align: right;
        line-height: 1.9;
        pointer-events: auto;
      }
      /* Corner group captions */
      #hud .caption {
        display: block;
        font-size: 0.55rem;
        font-weight: 600;
        letter-spacing: 0.2em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.28);
      }
      #hud .keys .caption { margin-bottom: 0.45rem; }
      /* Keycap chips in the hints */
      #hud .keys .key {
        display: inline-block;
        min-width: 1.5em;
        padding: 0.12em 0.4em;
        border: 1px solid rgba(255, 255, 255, 0.2);
        border-bottom-width: 2px;
        border-radius: 5px;
        background: rgba(255, 255, 255, 0.05);
        color: rgba(255, 255, 255, 0.75);
        font-size: 0.62rem;
        font-weight: 600;
        line-height: 1.35;
        text-align: center;
        transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
      }
      #hud .keys .key.lit {
        background: var(--yellow);
        border-color: var(--yellow);
        color: var(--ink);
      }
      #pad-hint { display: none; }
      body.pad-connected #pad-hint { display: inline; }
      /* Mini command sticks: dot = the twist the policy actually receives */
      #sticks {
        display: flex;
        gap: 0.7rem;
        justify-content: flex-end;
        margin-bottom: 0.5rem;
      }
      .stickwrap {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 0.15rem;
      }
      .stickwrap span {
        font-size: 0.55rem;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.3);
        line-height: 1;
      }
      .stickbox {
        position: relative;
        width: 46px;
        height: 46px;
        border-radius: 50%;
        border: 1px solid rgba(255, 255, 255, 0.16);
        background: rgba(255, 255, 255, 0.03);
      }
      .stickdot {
        position: absolute;
        left: 50%;
        top: 50%;
        width: 12px;
        height: 12px;
        margin: -6px 0 0 -6px;
        border-radius: 50%;
        background: rgba(255, 255, 255, 0.35);
        transition: background 0.15s ease;
      }
      .stickbox.live .stickdot { background: var(--yellow); }
      #stats-wrap {
        position: fixed;
        top: 1.4rem;
        right: 1.6rem;
        z-index: 10;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 0.15rem;
      }
      #stats {
        font-size: 0.68rem;
        letter-spacing: 0.08em;
        text-align: right;
        color: rgba(255, 255, 255, 0.4);
        line-height: 1.7;
        font-variant-numeric: tabular-nums;
      }
      @media (max-width: 700px) {
        #hud { flex-direction: column; align-items: flex-start; }
        #hud .keys { display: none; }
        #brand .sub { display: none; }
      }
      /* ── Welcome modal ─────────────────────────────────────────────── */
      /* display:flex would override the hidden attribute's UA display:none,
         so hidden must win explicitly. */
      #welcome[hidden] { display: none; }
      #welcome {
        position: fixed;
        inset: 0;
        z-index: 30;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 1.5rem;
        background: rgba(8, 8, 12, 0.45);
        backdrop-filter: blur(16px) saturate(1.3);
        -webkit-backdrop-filter: blur(16px) saturate(1.3);
        opacity: 1;
        transition: opacity 0.35s ease;
      }
      #welcome.closing { opacity: 0; pointer-events: none; }
      #welcome .card {
        position: relative;
        max-width: 430px;
        width: 100%;
        padding: 2.2rem 2.2rem 2rem;
        border: 1px solid rgba(255, 255, 255, 0.12);
        border-radius: 20px;
        background:
          radial-gradient(120% 90% at 50% 0%, rgba(255, 210, 63, 0.07), transparent 55%),
          rgba(14, 14, 20, 0.92);
        box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
        text-align: center;
        animation: card-in 0.5s cubic-bezier(0.2, 0.9, 0.25, 1.15) both;
      }
      @keyframes card-in {
        from { transform: translateY(18px) scale(0.94); opacity: 0; }
        to { transform: none; opacity: 1; }
      }
      /* Duck-head mark from the site header: closed by default, open on
         hover (same swap as the site). */
      #welcome .logo-head {
        position: relative;
        width: 7.2rem;
        margin: 0.3rem auto 0;
        filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.45));
      }
      #welcome .logo-head img { display: block; width: 100%; }
      #welcome .logo-head .open { position: absolute; inset: 0; opacity: 0; }
      #welcome .logo-head:hover .open { opacity: 1; }
      #welcome .logo-head:hover .closed { opacity: 0; }
      #welcome .subtitle {
        margin-top: 0.55rem;
        font-size: 0.62rem;
        font-weight: 700;
        letter-spacing: 0.32em;
        text-transform: uppercase;
        color: rgba(255, 210, 63, 0.75);
      }
      #welcome .tagline {
        margin-top: 0.5rem;
        font-size: 0.8rem;
        line-height: 1.65;
        color: rgba(255, 255, 255, 0.6);
      }
      #welcome ul {
        list-style: none;
        margin: 1.4rem 0 1.6rem;
        text-align: left;
        display: flex;
        flex-direction: column;
        gap: 0.55rem;
      }
      #welcome li {
        display: flex;
        align-items: baseline;
        gap: 0.7rem;
        font-size: 0.78rem;
        line-height: 1.5;
        color: rgba(255, 255, 255, 0.72);
      }
      #welcome li b {
        flex: 0 0 4.6rem;
        font-size: 0.62rem;
        font-weight: 700;
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: rgba(255, 210, 63, 0.85);
      }
      #welcome-go {
        appearance: none;
        border: none;
        background: var(--yellow);
        color: var(--ink);
        font: inherit;
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        padding: 0.75rem 2.2rem;
        border-radius: 999px;
        cursor: pointer;
        transition: transform 0.15s ease, box-shadow 0.15s ease;
      }
      #welcome-go:hover {
        transform: translateY(-1px) scale(1.03);
        box-shadow: 0 6px 24px rgba(255, 210, 63, 0.35);
      }
      #welcome .fineprint {
        margin-top: 0.9rem;
        font-size: 0.6rem;
        letter-spacing: 0.1em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.3);
      }
      #brand { cursor: pointer; }
    </style>
  </head>
  <body>
    <div id="scene"></div>
    <div class="halftone"></div>
    <div id="brand">
      <b>Microduck</b> &middot; real trained policies<br />
      <span class="sub">MuJoCo + ONNX, live in your browser</span>
    </div>
    <div id="stats-wrap">
      <div class="mode" id="mode-label"><span class="mode-text">Run</span></div>
      <div id="stats"></div>
    </div>
    <div id="hud" hidden>
      <div class="stack">
        <span class="caption">Colour</span>
        <div class="row" id="swatches"></div>
      </div>
      <div class="keys">
        <span class="caption">Controls</span>
        <div id="sticks">
          <div class="stickwrap">
            <div class="stickbox"><div class="stickdot" id="dot-move"></div></div>
            <span>move</span>
          </div>
          <div class="stickwrap">
            <div class="stickbox"><div class="stickdot" id="dot-turn"></div></div>
            <span>turn</span>
          </div>
        </div>
        <span class="hint"><span class="key" id="key-fwd">&uarr;</span><span class="key" id="key-back">&darr;</span> move</span> &nbsp;
        <span class="hint"><span class="key" id="key-turnl">&larr;</span><span class="key" id="key-turnr">&rarr;</span> turn</span> &nbsp;
        <span class="hint"><span class="key" id="key-left">A</span><span class="key" id="key-right">E</span> strafe</span><br />
        <span class="hint"><span class="key" id="key-roll">Space</span> roll</span> &nbsp;
        <span class="hint"><span class="key" id="key-reset">R</span> reset</span> &nbsp;
        drag to orbit &middot; scroll to zoom<br />
        <span id="pad-hint">gamepad:
          <span class="hint"><span class="key" id="key-pad-x">X</span> roll</span> &nbsp;
          <span class="hint"><span class="key" id="key-pad-sit">&dArr;</span> sit</span> &nbsp;
          <span class="hint"><span class="key" id="key-pad-run">&uArr;</span> run</span> &nbsp;
          <span class="hint"><span class="key" id="key-pad-rt">RT</span> quack</span>
        </span>
      </div>
    </div>
    <div id="loading">Compiling physics&hellip;</div>

    <div id="welcome" hidden>
      <div class="card" role="dialog" aria-modal="true" aria-label="Microduck playground">
        <div class="logo-head">
          <img class="closed" id="logo-head-closed" alt="Microduck" />
          <img class="open" id="logo-head-open" alt="" aria-hidden="true" />
        </div>
        <div class="subtitle">RL playground</div>
        <p class="tagline">
          A real robot brain in your browser: MuJoCo physics compiled to
          WebAssembly, driven by the duck's actual trained neural policies
          at 50&nbsp;Hz. No server, no pre-baked animation.
        </p>
        <ul>
          <li><b>Move</b><span>Arrows or ZQSD to run and turn, A / E to strafe, drag to orbit.</span></li>
          <li><b>Roll</b><span>Space triggers a barrel roll, then it gets back up.</span></li>
          <li><b>Gamepad</b><span>Plug one in - same mapping as the real robot.</span></li>
          <li><b>Ghosts</b><span>Other visitors right now appear as translucent ducks, live.</span></li>
        </ul>
        <button id="welcome-go">Waddle in</button>
        <div class="fineprint">click the title anytime to see this again</div>
      </div>
    </div>

    <script>
      // Welcome modal: shown on every load, dismissible by button, Escape
      // or clicking the blurred backdrop. The top-left brand reopens it.
      (() => {
        const modal = document.getElementById("welcome");
        // Logo images need the same ?__sign JWT as every other subresource
        // on the private Space (rl.js hasn't installed its signer yet).
        const sign = new URLSearchParams(location.search).get("__sign");
        const signedUrl = (u) => (sign ? `${u}?__sign=${encodeURIComponent(sign)}` : u);
        document.getElementById("logo-head-closed").src = signedUrl("./assets/duck-head-mark.webp");
        document.getElementById("logo-head-open").src = signedUrl("./assets/duck-head-mark-open.webp");
        const open = () => {
          modal.hidden = false;
          modal.classList.remove("closing");
        };
        const close = () => {
          modal.classList.add("closing");
          setTimeout(() => { modal.hidden = true; }, 380);
        };
        open();
        document.getElementById("welcome-go").addEventListener("click", close);
        modal.addEventListener("click", (e) => { if (e.target === modal) close(); });
        window.addEventListener("keydown", (e) => {
          if (e.code === "Escape" && !modal.hidden) close();
        });
        document.getElementById("brand").addEventListener("click", open);
      })();
    </script>

    <script type="importmap">
      {
        "imports": {
          "three": "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js",
          "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/"
        }
      }
    </script>
    <!-- rl.js is imported dynamically with the private-space ?__sign JWT
         appended: a plain src attribute can 401 when the browser blocks the
         *.static.hf.space auth cookie inside the hub iframe. -->
    <script type="module">
      const V = "24"; // bump on rl.js changes to defeat heuristic caching
      const sign = new URLSearchParams(location.search).get("__sign");
      const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
      import(`./rl.js?v=${V}${suffix}`);
    </script>
  </body>
</html>