tfrere HF Staff Cursor commited on
Commit
f6acb84
Β·
1 Parent(s): bb90cc7

feat: welcome modal explaining the playground

Browse files

Blurred-backdrop dialog shown on first visit: what runs in the browser
(MuJoCo WASM + ONNX policies), controls, gamepad and multiplayer ghosts.
Dismiss via button, Escape or backdrop click; stored in localStorage,
reopenable by clicking the top-left brand.

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (1) hide show
  1. index.html +166 -0
index.html CHANGED
@@ -244,6 +244,125 @@
244
  #hud .keys { display: none; }
245
  #brand .sub { display: none; }
246
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  </style>
248
  </head>
249
  <body>
@@ -290,6 +409,53 @@
290
  </div>
291
  <div id="loading">Compiling physics&hellip;</div>
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  <script type="importmap">
294
  {
295
  "imports": {
 
244
  #hud .keys { display: none; }
245
  #brand .sub { display: none; }
246
  }
247
+ /* ── Welcome modal ─────────────────────────────────────────────── */
248
+ /* display:flex would override the hidden attribute's UA display:none,
249
+ so hidden must win explicitly. */
250
+ #welcome[hidden] { display: none; }
251
+ #welcome {
252
+ position: fixed;
253
+ inset: 0;
254
+ z-index: 30;
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ padding: 1.5rem;
259
+ background: rgba(8, 8, 12, 0.45);
260
+ backdrop-filter: blur(16px) saturate(1.3);
261
+ -webkit-backdrop-filter: blur(16px) saturate(1.3);
262
+ opacity: 1;
263
+ transition: opacity 0.35s ease;
264
+ }
265
+ #welcome.closing { opacity: 0; pointer-events: none; }
266
+ #welcome .card {
267
+ position: relative;
268
+ max-width: 430px;
269
+ width: 100%;
270
+ padding: 2.2rem 2.2rem 2rem;
271
+ border: 1px solid rgba(255, 255, 255, 0.12);
272
+ border-radius: 20px;
273
+ background:
274
+ radial-gradient(120% 90% at 50% 0%, rgba(255, 210, 63, 0.07), transparent 55%),
275
+ rgba(14, 14, 20, 0.92);
276
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
277
+ text-align: center;
278
+ animation: card-in 0.5s cubic-bezier(0.2, 0.9, 0.25, 1.15) both;
279
+ }
280
+ @keyframes card-in {
281
+ from { transform: translateY(18px) scale(0.94); opacity: 0; }
282
+ to { transform: none; opacity: 1; }
283
+ }
284
+ #welcome .badge {
285
+ width: 4rem;
286
+ height: 4rem;
287
+ margin: 0 auto 1.1rem;
288
+ display: flex;
289
+ align-items: center;
290
+ justify-content: center;
291
+ font-size: 2rem;
292
+ border-radius: 50%;
293
+ background: rgba(255, 210, 63, 0.12);
294
+ border: 1px solid rgba(255, 210, 63, 0.35);
295
+ animation: badge-bob 3s ease-in-out infinite;
296
+ }
297
+ @keyframes badge-bob {
298
+ 0%, 100% { transform: translateY(0) rotate(-3deg); }
299
+ 50% { transform: translateY(-6px) rotate(3deg); }
300
+ }
301
+ #welcome h1 {
302
+ font-size: 1.05rem;
303
+ font-weight: 700;
304
+ letter-spacing: 0.18em;
305
+ text-transform: uppercase;
306
+ color: #fff;
307
+ }
308
+ #welcome h1 b { color: var(--yellow); font-weight: 700; }
309
+ #welcome .tagline {
310
+ margin-top: 0.5rem;
311
+ font-size: 0.8rem;
312
+ line-height: 1.65;
313
+ color: rgba(255, 255, 255, 0.6);
314
+ }
315
+ #welcome ul {
316
+ list-style: none;
317
+ margin: 1.4rem 0 1.6rem;
318
+ text-align: left;
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: 0.55rem;
322
+ }
323
+ #welcome li {
324
+ display: flex;
325
+ align-items: baseline;
326
+ gap: 0.7rem;
327
+ font-size: 0.78rem;
328
+ line-height: 1.5;
329
+ color: rgba(255, 255, 255, 0.72);
330
+ }
331
+ #welcome li b {
332
+ flex: 0 0 4.6rem;
333
+ font-size: 0.62rem;
334
+ font-weight: 700;
335
+ letter-spacing: 0.16em;
336
+ text-transform: uppercase;
337
+ color: rgba(255, 210, 63, 0.85);
338
+ }
339
+ #welcome-go {
340
+ appearance: none;
341
+ border: none;
342
+ background: var(--yellow);
343
+ color: var(--ink);
344
+ font: inherit;
345
+ font-size: 0.75rem;
346
+ font-weight: 700;
347
+ letter-spacing: 0.14em;
348
+ text-transform: uppercase;
349
+ padding: 0.75rem 2.2rem;
350
+ border-radius: 999px;
351
+ cursor: pointer;
352
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
353
+ }
354
+ #welcome-go:hover {
355
+ transform: translateY(-1px) scale(1.03);
356
+ box-shadow: 0 6px 24px rgba(255, 210, 63, 0.35);
357
+ }
358
+ #welcome .fineprint {
359
+ margin-top: 0.9rem;
360
+ font-size: 0.6rem;
361
+ letter-spacing: 0.1em;
362
+ text-transform: uppercase;
363
+ color: rgba(255, 255, 255, 0.3);
364
+ }
365
+ #brand { cursor: pointer; }
366
  </style>
367
  </head>
368
  <body>
 
409
  </div>
410
  <div id="loading">Compiling physics&hellip;</div>
411
 
412
+ <div id="welcome" hidden>
413
+ <div class="card" role="dialog" aria-modal="true" aria-labelledby="welcome-title">
414
+ <div class="badge">🐀</div>
415
+ <h1 id="welcome-title"><b>Microduck</b> playground</h1>
416
+ <p class="tagline">
417
+ A real robot brain in your browser: MuJoCo physics compiled to
418
+ WebAssembly, driven by the duck's actual trained neural policies
419
+ at 50&nbsp;Hz. No server, no pre-baked animation.
420
+ </p>
421
+ <ul>
422
+ <li><b>Move</b><span>Arrow keys to run and turn, A / E to strafe, drag to orbit.</span></li>
423
+ <li><b>Roll</b><span>Space triggers a barrel roll, then it gets back up.</span></li>
424
+ <li><b>Gamepad</b><span>Plug one in - same mapping as the real robot.</span></li>
425
+ <li><b>Ghosts</b><span>Other visitors right now appear as translucent ducks, live.</span></li>
426
+ </ul>
427
+ <button id="welcome-go">Waddle in</button>
428
+ <div class="fineprint">click the title anytime to see this again</div>
429
+ </div>
430
+ </div>
431
+
432
+ <script>
433
+ // Welcome modal: shown on first visit, dismissible by button, Escape
434
+ // or clicking the blurred backdrop. The top-left brand reopens it.
435
+ (() => {
436
+ const SEEN_KEY = "md-welcome-seen";
437
+ const modal = document.getElementById("welcome");
438
+ const open = () => {
439
+ modal.hidden = false;
440
+ modal.classList.remove("closing");
441
+ };
442
+ const close = () => {
443
+ try { localStorage.setItem(SEEN_KEY, "1"); } catch {}
444
+ modal.classList.add("closing");
445
+ setTimeout(() => { modal.hidden = true; }, 380);
446
+ };
447
+ let seen = false;
448
+ try { seen = localStorage.getItem(SEEN_KEY) === "1"; } catch {}
449
+ if (!seen) open();
450
+ document.getElementById("welcome-go").addEventListener("click", close);
451
+ modal.addEventListener("click", (e) => { if (e.target === modal) close(); });
452
+ window.addEventListener("keydown", (e) => {
453
+ if (e.code === "Escape" && !modal.hidden) close();
454
+ });
455
+ document.getElementById("brand").addEventListener("click", open);
456
+ })();
457
+ </script>
458
+
459
  <script type="importmap">
460
  {
461
  "imports": {