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",