diff --git a/public_html/README.md b/public_html/README.md index 671d9b1..73cece9 100644 --- a/public_html/README.md +++ b/public_html/README.md @@ -49,6 +49,11 @@ except installation and the service worker. - **Tap your own half** to end your turn and start your opponent's clock. At the start, whoever taps first sets the *other* player going. - The waiting player's taps are ignored, so a stray hand won't stop the clock. +- A tap counts when you **lift** your finger, and only if it didn't move. A swipe + does nothing, so Android's swipe-up-from-the-bottom and the status-bar pull-down + work over either half without costing anyone their turn — and a hand dragged + across the panel won't change it either. The clock still switches at the moment + you pressed, not when you let go. - The bar under the big time is the delay draining. When it empties you get a soft tick and your reserve starts moving. That's the moment worth hearing. - **Reset** starts a whole new match: clocks back to full *and* the score back to diff --git a/public_html/index.html b/public_html/index.html index 4b5984a..e242191 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -62,6 +62,9 @@ color:var(--idle-ink); overflow:hidden; transition:background-color .18s ease; + /* there's nothing to pan in a 100dvh frame, and this keeps a slow drag from + being cancelled by the browser — it ends in a lift we can measure instead */ + touch-action:none; } .panel[data-state="active"]{background:var(--accent);color:var(--accent-ink)} .panel[data-state="flagged"]{background:var(--flag);color:#fff} @@ -775,13 +778,17 @@ saveGame(); } - function tap(side){ + // `at` is when the finger landed. A tap is only confirmed once it lifts, but + // the turn has to change at the press or every switch drifts by however long + // the finger rested on the panel. + function tap(side, at){ if(st.phase === FLAG) return; if(st.phase === PAUSE) return; + var now = at || performance.now(); if(st.phase === IDLE){ st.active = 1 - side; // you tap your own side to start your opponent - st.turnAt = performance.now(); + st.turnAt = now; st.ticked = false; st.phase = RUN; paintPlay(); // the bar has to show what the clock is doing @@ -791,11 +798,12 @@ } if(side !== st.active) return; // the waiting player can't stop the clock - var e = elapsed(); + // not elapsed(): that measures to *now*, and this has to measure to the press + var e = now - st.turnAt; st.reserve[side] = Math.max(0, st.reserve[side] - Math.max(0, e - cfg.delay)); st.moves[side]++; st.active = 1 - side; - st.turnAt = performance.now(); + st.turnAt = now; st.ticked = false; sndSwap(); buzz(14); saveGame(); @@ -1155,18 +1163,46 @@ btnPlay.addEventListener("click", togglePause); /* ============ panel taps ============ */ + // A tap is a press and a lift in the same place. Acting on the press alone + // would steal the turn from Android's swipe-up-from-the-bottom gesture: the + // bottom panel owns that edge, and starting a swipe home is a finger landing. + // A swipe travels past SLOP, or the system claims it and we get pointercancel + // instead of a lift. Either way there's no turn change. + var SLOP = 12; // px of travel a press may drift and still count var wantFullscreen = true; panels.forEach(function(p){ + var down = null; + var side = parseInt(p.dataset.side, 10); + p.addEventListener("pointerdown", function(ev){ ev.preventDefault(); - audio(); + audio(); // unlocking on the press is a valid gesture and earlier is better + down = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, t: ev.timeStamp }; + // so the lift still lands here if the finger drifts onto the other panel + try{ p.setPointerCapture(ev.pointerId); }catch(e){} + }); + + p.addEventListener("pointerup", function(ev){ + var d = down; + // only the pointer we're tracking clears it: with two fingers on one panel + // the second overwrites the first, and the first's lift mustn't wipe it + if(!d || ev.pointerId !== d.id) return; + down = null; + if(Math.abs(ev.clientX - d.x) > SLOP || Math.abs(ev.clientY - d.y) > SLOP) return; + + // on the lift, not the press, so a swipe can't force fullscreen either if(wantFullscreen){ wantFullscreen = false; if(!window.matchMedia("(display-mode: fullscreen)").matches && document.documentElement.requestFullscreen){ document.documentElement.requestFullscreen().catch(function(){}); } } - tap(parseInt(p.dataset.side, 10)); + tap(side, d.t); + }); + + // the branch Android's home gesture takes + p.addEventListener("pointercancel", function(ev){ + if(down && ev.pointerId === down.id) down = null; }); }); diff --git a/public_html/sw.js b/public_html/sw.js index 4495f4e..7bdb194 100644 --- a/public_html/sw.js +++ b/public_html/sw.js @@ -1,5 +1,5 @@ /* Cache-first: once installed the clock never touches the network again. */ -const CACHE = "bgclock-v10"; +const CACHE = "bgclock-v11"; const FILES = [ "./", "./index.html",