Change the turn on the lift, not the touch
Android's swipe-up-from-the-bottom is how you leave an app, and the bottom panel owns that edge — .app is 1fr auto 1fr with the bar in the middle, so both panels run to the screen edges. Acting on pointerdown meant starting a swipe home was a finger landing on the clock, and the turn changed before the gesture had even been recognised. The status bar over the top panel has the same problem. Android won't let an app opt out either; setSystemGestureExclusionRects is ignored for the home gesture. So a tap is now a press and a lift in the same place, within 12px. A swipe travels further; a gesture the system claims arrives as pointercancel and never becomes a lift. The clock is still stamped at the press. tap() takes the pointerdown timestamp, which meant dropping elapsed() for now - st.turnAt — elapsed() always measures to the current instant, the one thing that can't be backdated. With the delay forced to 0, holding a finger down 600ms longer charges the outgoing player 1033ms against 1037ms for an instant lift: the same thinking time, not the extra 600. The fullscreen request moved to the lift too, so a swipe can't force it. touch-action:none on .panel means a slow drag ends in a lift we reject rather than a cancel the browser issues on its own account; it changes no pixels. Falls out of it: a hand dragged across a live panel no longer flips the turn, which the README only promised of the waiting player's hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,11 @@ except installation and the service worker.
|
|||||||
- **Tap your own half** to end your turn and start your opponent's clock.
|
- **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.
|
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.
|
- 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
|
- 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.
|
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
|
- **Reset** starts a whole new match: clocks back to full *and* the score back to
|
||||||
|
|||||||
+42
-6
@@ -62,6 +62,9 @@
|
|||||||
color:var(--idle-ink);
|
color:var(--idle-ink);
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
transition:background-color .18s ease;
|
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="active"]{background:var(--accent);color:var(--accent-ink)}
|
||||||
.panel[data-state="flagged"]{background:var(--flag);color:#fff}
|
.panel[data-state="flagged"]{background:var(--flag);color:#fff}
|
||||||
@@ -775,13 +778,17 @@
|
|||||||
saveGame();
|
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 === FLAG) return;
|
||||||
if(st.phase === PAUSE) return;
|
if(st.phase === PAUSE) return;
|
||||||
|
var now = at || performance.now();
|
||||||
|
|
||||||
if(st.phase === IDLE){
|
if(st.phase === IDLE){
|
||||||
st.active = 1 - side; // you tap your own side to start your opponent
|
st.active = 1 - side; // you tap your own side to start your opponent
|
||||||
st.turnAt = performance.now();
|
st.turnAt = now;
|
||||||
st.ticked = false;
|
st.ticked = false;
|
||||||
st.phase = RUN;
|
st.phase = RUN;
|
||||||
paintPlay(); // the bar has to show what the clock is doing
|
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
|
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.reserve[side] = Math.max(0, st.reserve[side] - Math.max(0, e - cfg.delay));
|
||||||
st.moves[side]++;
|
st.moves[side]++;
|
||||||
st.active = 1 - side;
|
st.active = 1 - side;
|
||||||
st.turnAt = performance.now();
|
st.turnAt = now;
|
||||||
st.ticked = false;
|
st.ticked = false;
|
||||||
sndSwap(); buzz(14);
|
sndSwap(); buzz(14);
|
||||||
saveGame();
|
saveGame();
|
||||||
@@ -1155,18 +1163,46 @@
|
|||||||
btnPlay.addEventListener("click", togglePause);
|
btnPlay.addEventListener("click", togglePause);
|
||||||
|
|
||||||
/* ============ panel taps ============ */
|
/* ============ 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;
|
var wantFullscreen = true;
|
||||||
panels.forEach(function(p){
|
panels.forEach(function(p){
|
||||||
|
var down = null;
|
||||||
|
var side = parseInt(p.dataset.side, 10);
|
||||||
|
|
||||||
p.addEventListener("pointerdown", function(ev){
|
p.addEventListener("pointerdown", function(ev){
|
||||||
ev.preventDefault();
|
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){
|
if(wantFullscreen){
|
||||||
wantFullscreen = false;
|
wantFullscreen = false;
|
||||||
if(!window.matchMedia("(display-mode: fullscreen)").matches && document.documentElement.requestFullscreen){
|
if(!window.matchMedia("(display-mode: fullscreen)").matches && document.documentElement.requestFullscreen){
|
||||||
document.documentElement.requestFullscreen().catch(function(){});
|
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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/* Cache-first: once installed the clock never touches the network again. */
|
/* Cache-first: once installed the clock never touches the network again. */
|
||||||
const CACHE = "bgclock-v10";
|
const CACHE = "bgclock-v11";
|
||||||
const FILES = [
|
const FILES = [
|
||||||
"./",
|
"./",
|
||||||
"./index.html",
|
"./index.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user