feat: latch touch mode on first real touch, add ?touch=1 test hook
Browse files(pointer: coarse) misses DevTools device modes and hybrid laptops, so any
real touchstart now arms the thumbs permanently (title page hands the
proof to controls/touch.js via __microduckTouched). ?touch=1 forces the
overlay on any device for desktop testing.
Co-authored-by: Cursor <cursoragent@cursor.com>
- controls/touch.js +13 -5
- index.html +14 -6
controls/touch.js
CHANGED
|
@@ -15,10 +15,11 @@
|
|
| 15 |
// holds the beak open (axes.jaw = 1) while held.
|
| 16 |
//
|
| 17 |
// The overlay DOM lives in index.html (#touch-ui); this module only wires
|
| 18 |
-
// pointer events to it. `connected`
|
| 19 |
-
//
|
| 20 |
-
//
|
| 21 |
-
//
|
|
|
|
| 22 |
|
| 23 |
const TOUCH_ALPHA = 0.18; // EMA smoothing toward the stick target
|
| 24 |
const TOUCH_DEADZONE = 0.12; // normalized deflection ignored around center
|
|
@@ -44,9 +45,16 @@ export class TouchSource {
|
|
| 44 |
|
| 45 |
init() {
|
| 46 |
this.#mq = window.matchMedia("(pointer: coarse)");
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
this.#mq.addEventListener("change", this.#onMq);
|
| 49 |
this.#onMq();
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
const zone = document.getElementById("touch-zone");
|
| 52 |
const stick = document.getElementById("touch-stick");
|
|
|
|
| 15 |
// holds the beak open (axes.jaw = 1) while held.
|
| 16 |
//
|
| 17 |
// The overlay DOM lives in index.html (#touch-ui); this module only wires
|
| 18 |
+
// pointer events to it. `connected` (mirrored onto body.touch-mode by
|
| 19 |
+
// rl.js, which is what actually shows the overlay) arms two ways:
|
| 20 |
+
// - (pointer: coarse) matches: phones/tablets, before any touch;
|
| 21 |
+
// - OR the first real touch anywhere: catches DevTools device modes and
|
| 22 |
+
// hybrid laptops the media query misses. Latching, like a gamepad.
|
| 23 |
|
| 24 |
const TOUCH_ALPHA = 0.18; // EMA smoothing toward the stick target
|
| 25 |
const TOUCH_DEADZONE = 0.12; // normalized deflection ignored around center
|
|
|
|
| 45 |
|
| 46 |
init() {
|
| 47 |
this.#mq = window.matchMedia("(pointer: coarse)");
|
| 48 |
+
// Latching: once a device has proven it can touch, keep the thumbs.
|
| 49 |
+
// __microduckTouched carries a touch seen by the title page before
|
| 50 |
+
// this module booted.
|
| 51 |
+
if (window.__microduckTouched) this.connected = true;
|
| 52 |
+
this.#onMq = () => { this.connected = this.connected || this.#mq.matches; };
|
| 53 |
this.#mq.addEventListener("change", this.#onMq);
|
| 54 |
this.#onMq();
|
| 55 |
+
const armTouch = () => { this.connected = true; };
|
| 56 |
+
window.addEventListener("touchstart", armTouch, { once: true, passive: true });
|
| 57 |
+
this.#disposers.push(() => window.removeEventListener("touchstart", armTouch));
|
| 58 |
|
| 59 |
const zone = document.getElementById("touch-zone");
|
| 60 |
const stick = document.getElementById("touch-stick");
|
index.html
CHANGED
|
@@ -1280,12 +1280,20 @@
|
|
| 1280 |
pollPad();
|
| 1281 |
|
| 1282 |
// Touch tutorial variant needs the class before rl.js boots; the
|
| 1283 |
-
// game loop keeps mirroring the
|
|
|
|
|
|
|
|
|
|
| 1284 |
const coarse = matchMedia("(pointer: coarse)");
|
| 1285 |
-
const
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
|
| 1290 |
// ββ Menu open / close ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1291 |
const open = () => {
|
|
@@ -1368,7 +1376,7 @@
|
|
| 1368 |
appended: a plain src attribute can 401 when the browser blocks the
|
| 1369 |
*.static.hf.space auth cookie inside the hub iframe. -->
|
| 1370 |
<script type="module">
|
| 1371 |
-
const V = "
|
| 1372 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 1373 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 1374 |
import(`./rl.js?v=${V}${suffix}`);
|
|
|
|
| 1280 |
pollPad();
|
| 1281 |
|
| 1282 |
// Touch tutorial variant needs the class before rl.js boots; the
|
| 1283 |
+
// game loop keeps mirroring the touch source afterwards. Latching:
|
| 1284 |
+
// (pointer: coarse) OR any real touch proves the device (DevTools
|
| 1285 |
+
// device modes and hybrid laptops often fail the media query), and
|
| 1286 |
+
// the __microduckTouched flag hands the proof to controls/touch.js.
|
| 1287 |
const coarse = matchMedia("(pointer: coarse)");
|
| 1288 |
+
const armTouch = () => {
|
| 1289 |
+
window.__microduckTouched = true;
|
| 1290 |
+
document.body.classList.add("touch-mode");
|
| 1291 |
+
};
|
| 1292 |
+
if (coarse.matches) armTouch();
|
| 1293 |
+
else coarse.addEventListener("change", () => coarse.matches && armTouch());
|
| 1294 |
+
window.addEventListener("touchstart", armTouch, { once: true, passive: true });
|
| 1295 |
+
// ?touch=1 forces the thumbs on any device - desktop testing hook.
|
| 1296 |
+
if (new URLSearchParams(location.search).get("touch") === "1") armTouch();
|
| 1297 |
|
| 1298 |
// ββ Menu open / close ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1299 |
const open = () => {
|
|
|
|
| 1376 |
appended: a plain src attribute can 401 when the browser blocks the
|
| 1377 |
*.static.hf.space auth cookie inside the hub iframe. -->
|
| 1378 |
<script type="module">
|
| 1379 |
+
const V = "68"; // bump on rl.js changes to defeat heuristic caching
|
| 1380 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 1381 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 1382 |
import(`./rl.js?v=${V}${suffix}`);
|