feat: infinite shader grid, edge-anchored UI, drop hero title
Browse files- index.html +42 -34
- rl.js +54 -34
index.html
CHANGED
|
@@ -28,30 +28,20 @@
|
|
| 28 |
mask-image: linear-gradient(to bottom, transparent 30%, black 100%);
|
| 29 |
-webkit-mask-image: linear-gradient(to bottom, transparent 30%, black 100%);
|
| 30 |
}
|
| 31 |
-
|
|
|
|
| 32 |
position: fixed;
|
| 33 |
-
top:
|
| 34 |
-
left:
|
| 35 |
-
|
| 36 |
-
text-align: center;
|
| 37 |
-
pointer-events: none;
|
| 38 |
-
}
|
| 39 |
-
header .facts {
|
| 40 |
font-size: 0.72rem;
|
| 41 |
font-weight: 600;
|
| 42 |
letter-spacing: 0.14em;
|
| 43 |
text-transform: uppercase;
|
| 44 |
color: rgba(255, 255, 255, 0.55);
|
|
|
|
| 45 |
}
|
| 46 |
-
|
| 47 |
-
header h1 {
|
| 48 |
-
margin-top: 0.5rem;
|
| 49 |
-
font-size: clamp(1.6rem, 4vw, 2.6rem);
|
| 50 |
-
font-weight: 700;
|
| 51 |
-
letter-spacing: -0.03em;
|
| 52 |
-
color: #fff;
|
| 53 |
-
}
|
| 54 |
-
header h1 #verb { color: var(--yellow); }
|
| 55 |
#loading {
|
| 56 |
position: fixed;
|
| 57 |
inset: 0;
|
|
@@ -66,17 +56,25 @@
|
|
| 66 |
padding: 0 2rem;
|
| 67 |
z-index: 20;
|
| 68 |
}
|
| 69 |
-
/* Bottom
|
| 70 |
#hud {
|
| 71 |
position: fixed;
|
| 72 |
bottom: 1.4rem;
|
| 73 |
-
left:
|
| 74 |
-
right:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
display: flex;
|
| 76 |
flex-direction: column;
|
| 77 |
-
align-items:
|
| 78 |
gap: 0.7rem;
|
| 79 |
-
|
| 80 |
}
|
| 81 |
#hud .row {
|
| 82 |
display: flex;
|
|
@@ -122,6 +120,9 @@
|
|
| 122 |
font-size: 0.7rem;
|
| 123 |
letter-spacing: 0.08em;
|
| 124 |
color: rgba(255, 255, 255, 0.4);
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
#hud .keys b { color: rgba(255, 255, 255, 0.75); font-weight: 600; }
|
| 127 |
#stats {
|
|
@@ -137,27 +138,34 @@
|
|
| 137 |
font-variant-numeric: tabular-nums;
|
| 138 |
}
|
| 139 |
#stats b { color: var(--yellow); font-weight: 600; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
</style>
|
| 141 |
</head>
|
| 142 |
<body>
|
| 143 |
<div id="scene"></div>
|
| 144 |
<div class="halftone"></div>
|
| 145 |
-
<
|
| 146 |
-
<
|
| 147 |
-
<
|
| 148 |
-
</
|
| 149 |
<div id="stats"></div>
|
| 150 |
<div id="hud" hidden>
|
| 151 |
-
<div class="
|
| 152 |
-
<
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
| 156 |
</div>
|
| 157 |
-
<div class="row" id="swatches"></div>
|
| 158 |
<div class="keys">
|
| 159 |
-
<b>↑/↓</b> forward / back <b>←/→</b> turn
|
| 160 |
-
<b>
|
| 161 |
</div>
|
| 162 |
</div>
|
| 163 |
<div id="loading">Compiling physics…</div>
|
|
|
|
| 28 |
mask-image: linear-gradient(to bottom, transparent 30%, black 100%);
|
| 29 |
-webkit-mask-image: linear-gradient(to bottom, transparent 30%, black 100%);
|
| 30 |
}
|
| 31 |
+
/* Top-left corner: what this is */
|
| 32 |
+
#brand {
|
| 33 |
position: fixed;
|
| 34 |
+
top: 1.4rem;
|
| 35 |
+
left: 1.6rem;
|
| 36 |
+
z-index: 10;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
font-size: 0.72rem;
|
| 38 |
font-weight: 600;
|
| 39 |
letter-spacing: 0.14em;
|
| 40 |
text-transform: uppercase;
|
| 41 |
color: rgba(255, 255, 255, 0.55);
|
| 42 |
+
line-height: 1.8;
|
| 43 |
}
|
| 44 |
+
#brand b { color: var(--yellow); font-weight: 600; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
#loading {
|
| 46 |
position: fixed;
|
| 47 |
inset: 0;
|
|
|
|
| 56 |
padding: 0 2rem;
|
| 57 |
z-index: 20;
|
| 58 |
}
|
| 59 |
+
/* Bottom edges: controls left, key hints right */
|
| 60 |
#hud {
|
| 61 |
position: fixed;
|
| 62 |
bottom: 1.4rem;
|
| 63 |
+
left: 1.6rem;
|
| 64 |
+
right: 1.6rem;
|
| 65 |
+
display: flex;
|
| 66 |
+
justify-content: space-between;
|
| 67 |
+
align-items: flex-end;
|
| 68 |
+
gap: 1rem;
|
| 69 |
+
z-index: 10;
|
| 70 |
+
pointer-events: none;
|
| 71 |
+
}
|
| 72 |
+
#hud .stack {
|
| 73 |
display: flex;
|
| 74 |
flex-direction: column;
|
| 75 |
+
align-items: flex-start;
|
| 76 |
gap: 0.7rem;
|
| 77 |
+
pointer-events: auto;
|
| 78 |
}
|
| 79 |
#hud .row {
|
| 80 |
display: flex;
|
|
|
|
| 120 |
font-size: 0.7rem;
|
| 121 |
letter-spacing: 0.08em;
|
| 122 |
color: rgba(255, 255, 255, 0.4);
|
| 123 |
+
text-align: right;
|
| 124 |
+
line-height: 1.9;
|
| 125 |
+
pointer-events: auto;
|
| 126 |
}
|
| 127 |
#hud .keys b { color: rgba(255, 255, 255, 0.75); font-weight: 600; }
|
| 128 |
#stats {
|
|
|
|
| 138 |
font-variant-numeric: tabular-nums;
|
| 139 |
}
|
| 140 |
#stats b { color: var(--yellow); font-weight: 600; }
|
| 141 |
+
@media (max-width: 700px) {
|
| 142 |
+
#hud { flex-direction: column; align-items: flex-start; }
|
| 143 |
+
#hud .keys { display: none; }
|
| 144 |
+
#brand .sub { display: none; }
|
| 145 |
+
}
|
| 146 |
</style>
|
| 147 |
</head>
|
| 148 |
<body>
|
| 149 |
<div id="scene"></div>
|
| 150 |
<div class="halftone"></div>
|
| 151 |
+
<div id="brand">
|
| 152 |
+
<b>Microduck</b> · real trained policies<br />
|
| 153 |
+
<span class="sub">MuJoCo + ONNX, live in your browser</span>
|
| 154 |
+
</div>
|
| 155 |
<div id="stats"></div>
|
| 156 |
<div id="hud" hidden>
|
| 157 |
+
<div class="stack">
|
| 158 |
+
<div class="row">
|
| 159 |
+
<button id="btn-walk" class="on">Run</button>
|
| 160 |
+
<button id="btn-sit">Sit</button>
|
| 161 |
+
<button id="btn-roulade">Roulade</button>
|
| 162 |
+
<button id="btn-reset">Reset</button>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="row" id="swatches"></div>
|
| 165 |
</div>
|
|
|
|
| 166 |
<div class="keys">
|
| 167 |
+
<b>↑/↓</b> forward / back <b>←/→</b> turn <b>A/E</b> strafe<br />
|
| 168 |
+
<b>R</b> reset drag to orbit scroll to zoom
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
<div id="loading">Compiling physics…</div>
|
rl.js
CHANGED
|
@@ -60,7 +60,6 @@ const mount = document.getElementById("scene");
|
|
| 60 |
const loadingEl = document.getElementById("loading");
|
| 61 |
const hudEl = document.getElementById("hud");
|
| 62 |
const statsEl = document.getElementById("stats");
|
| 63 |
-
const verbEl = document.getElementById("verb");
|
| 64 |
const setLoading = (msg) => { loadingEl.textContent = msg; };
|
| 65 |
|
| 66 |
// Surface boot failures on the page itself: a rejected top-level await
|
|
@@ -302,36 +301,58 @@ const rim = new THREE.DirectionalLight(0xffb366, 0.7);
|
|
| 302 |
rim.position.set(0, 3, -2);
|
| 303 |
scene.add(rim);
|
| 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 |
-
const
|
| 330 |
-
|
| 331 |
-
new THREE.MeshStandardMaterial({ map: makeFloorTexture(), roughness: 0.9, metalness: 0.0 }),
|
| 332 |
-
);
|
| 333 |
-
floor.rotation.x = -Math.PI / 2;
|
| 334 |
-
scene.add(floor);
|
| 335 |
|
| 336 |
const rig = await rigPromise;
|
| 337 |
scene.add(rig.placer);
|
|
@@ -370,6 +391,9 @@ function syncRig() {
|
|
| 370 |
_follow.copy(_target).sub(controls.target).multiplyScalar(0.06);
|
| 371 |
controls.target.add(_follow);
|
| 372 |
camera.position.add(_follow);
|
|
|
|
|
|
|
|
|
|
| 373 |
}
|
| 374 |
|
| 375 |
// Quack: a quick jaw flap on every mode/colour change. The jaw isn't a
|
|
@@ -444,9 +468,6 @@ const btnSit = document.getElementById("btn-sit");
|
|
| 444 |
const btnRoulade = document.getElementById("btn-roulade");
|
| 445 |
document.getElementById("btn-reset").addEventListener("click", resetSim);
|
| 446 |
|
| 447 |
-
// The headline verb tracks the active policy.
|
| 448 |
-
const VERBS = { walk: "run", sitstand: "sit", roulade: "roll over" };
|
| 449 |
-
|
| 450 |
function setMode(next) {
|
| 451 |
quack();
|
| 452 |
if (next !== "sit") {
|
|
@@ -477,7 +498,6 @@ function syncButtons() {
|
|
| 477 |
btnWalk.classList.toggle("on", mode === "walk" || (mode === "sitstand" && !sitting));
|
| 478 |
btnSit.classList.toggle("on", sitting);
|
| 479 |
btnRoulade.classList.toggle("on", mode === "roulade");
|
| 480 |
-
verbEl.textContent = VERBS[mode];
|
| 481 |
}
|
| 482 |
btnWalk.addEventListener("click", () => setMode("walk"));
|
| 483 |
btnSit.addEventListener("click", () => setMode("sit"));
|
|
|
|
| 60 |
const loadingEl = document.getElementById("loading");
|
| 61 |
const hudEl = document.getElementById("hud");
|
| 62 |
const statsEl = document.getElementById("stats");
|
|
|
|
| 63 |
const setLoading = (msg) => { loadingEl.textContent = msg; };
|
| 64 |
|
| 65 |
// Surface boot failures on the page itself: a rejected top-level await
|
|
|
|
| 301 |
rim.position.set(0, 3, -2);
|
| 302 |
scene.add(rim);
|
| 303 |
|
| 304 |
+
// Infinite shader grid, ported from drei's <Grid> (we're in vanilla three,
|
| 305 |
+
// not R3F): anti-aliased world-space lines at cell/section frequencies with
|
| 306 |
+
// a radial fade around the duck. Lines derive from world coordinates, so
|
| 307 |
+
// re-centering the mesh under the camera target every frame makes the grid
|
| 308 |
+
// effectively infinite without any visible swimming.
|
| 309 |
+
function makeInfiniteGrid() {
|
| 310 |
+
const material = new THREE.ShaderMaterial({
|
| 311 |
+
transparent: true,
|
| 312 |
+
depthWrite: false,
|
| 313 |
+
uniforms: {
|
| 314 |
+
uCell: { value: 0.1 },
|
| 315 |
+
uSection: { value: 0.5 },
|
| 316 |
+
uCellColor: { value: new THREE.Color(0x7d7360) },
|
| 317 |
+
uSectionColor: { value: new THREE.Color(0xffb366) },
|
| 318 |
+
uFadeDist: { value: 3.0 },
|
| 319 |
+
uFocus: { value: new THREE.Vector3() },
|
| 320 |
+
},
|
| 321 |
+
vertexShader: /* glsl */ `
|
| 322 |
+
varying vec3 vWorld;
|
| 323 |
+
void main() {
|
| 324 |
+
vec4 w = modelMatrix * vec4(position, 1.0);
|
| 325 |
+
vWorld = w.xyz;
|
| 326 |
+
gl_Position = projectionMatrix * viewMatrix * w;
|
| 327 |
+
}
|
| 328 |
+
`,
|
| 329 |
+
fragmentShader: /* glsl */ `
|
| 330 |
+
varying vec3 vWorld;
|
| 331 |
+
uniform float uCell, uSection, uFadeDist;
|
| 332 |
+
uniform vec3 uCellColor, uSectionColor, uFocus;
|
| 333 |
+
float gridLine(vec2 p, float size) {
|
| 334 |
+
vec2 r = p / size;
|
| 335 |
+
vec2 g = abs(fract(r - 0.5) - 0.5) / fwidth(r);
|
| 336 |
+
return 1.0 - min(min(g.x, g.y), 1.0);
|
| 337 |
+
}
|
| 338 |
+
void main() {
|
| 339 |
+
float cell = gridLine(vWorld.xz, uCell);
|
| 340 |
+
float section = gridLine(vWorld.xz, uSection);
|
| 341 |
+
float d = distance(vWorld.xz, uFocus.xz);
|
| 342 |
+
float fade = pow(clamp(1.0 - d / uFadeDist, 0.0, 1.0), 1.6);
|
| 343 |
+
vec3 col = mix(uCellColor, uSectionColor, section);
|
| 344 |
+
float alpha = max(section * 0.45, cell * 0.3) * fade;
|
| 345 |
+
if (alpha < 0.004) discard;
|
| 346 |
+
gl_FragColor = vec4(col, alpha);
|
| 347 |
+
}
|
| 348 |
+
`,
|
| 349 |
+
});
|
| 350 |
+
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(30, 30), material);
|
| 351 |
+
mesh.rotation.x = -Math.PI / 2;
|
| 352 |
+
return mesh;
|
| 353 |
}
|
| 354 |
+
const grid = makeInfiniteGrid();
|
| 355 |
+
scene.add(grid);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
const rig = await rigPromise;
|
| 358 |
scene.add(rig.placer);
|
|
|
|
| 391 |
_follow.copy(_target).sub(controls.target).multiplyScalar(0.06);
|
| 392 |
controls.target.add(_follow);
|
| 393 |
camera.position.add(_follow);
|
| 394 |
+
// Keep the grid plane (and its fade center) under the action.
|
| 395 |
+
grid.position.set(controls.target.x, 0, controls.target.z);
|
| 396 |
+
grid.material.uniforms.uFocus.value.copy(controls.target);
|
| 397 |
}
|
| 398 |
|
| 399 |
// Quack: a quick jaw flap on every mode/colour change. The jaw isn't a
|
|
|
|
| 468 |
const btnRoulade = document.getElementById("btn-roulade");
|
| 469 |
document.getElementById("btn-reset").addEventListener("click", resetSim);
|
| 470 |
|
|
|
|
|
|
|
|
|
|
| 471 |
function setMode(next) {
|
| 472 |
quack();
|
| 473 |
if (next !== "sit") {
|
|
|
|
| 498 |
btnWalk.classList.toggle("on", mode === "walk" || (mode === "sitstand" && !sitting));
|
| 499 |
btnSit.classList.toggle("on", sitting);
|
| 500 |
btnRoulade.classList.toggle("on", mode === "roulade");
|
|
|
|
| 501 |
}
|
| 502 |
btnWalk.addEventListener("click", () => setMode("walk"));
|
| 503 |
btnSit.addEventListener("click", () => setMode("sit"));
|