Charge the hold, and drop the two buttons that have nothing to do
Three things, plus the README moves up to the repo root where a README belongs — it was being served as part of the site. The clock now runs until the finger lifts. The previous commit registered a tap on the lift but backdated the turn to the press, which showed up as the display counting down during a hold and then jumping back. Worse than cosmetic: the hold was refunded, so a thumb resting on your own half bought unlimited thinking time for nothing, and holding past zero couldn't flag you either, because render()'s flag check watches the live display while tap() charged from an earlier instant. One performance.now() at the lift now serves both the outgoing charge and the incoming turnAt, so nothing falls between them and goes to neither player. With the delay at 0 and a turn 1000ms old, an instant lift costs 1001ms and a 600ms hold costs 1602ms — the inverse of what the last commit was verified against. Swipe rejection is untouched; it lives in the pointer handler, not here. Reset and pause step out when they have nothing to do: pause before each game's first tap, reset while the match is still pristine. matchPristine() already meant exactly that, so both toggles sit beside the settings one in render(), where they can't go stale. Reset and settings turn out to be exact opposites. btn--vacant like the gear, so the space is kept — a fresh-match bar differs from the old build only inside those two slots, every other pixel in the frame identical, gear and speaker unmoved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
# Backgammon Clock
|
||||
|
||||
A two-player match clock with **simple delay you can actually see**. Reserve time
|
||||
plus a fixed number of free seconds each turn that never carry over — the
|
||||
Backgammon Galaxy control. Defaults to 3:00 + 12s.
|
||||
|
||||
Plain HTML, CSS and JavaScript. No frameworks, no fonts, no images, no network.
|
||||
|
||||
## Getting it onto your phone
|
||||
|
||||
Chrome will only offer a real install over HTTPS, so the file needs a host.
|
||||
GitHub Pages is free and takes about two minutes:
|
||||
|
||||
1. Create a new public repository, e.g. `bgclock`.
|
||||
2. Upload all six files to the root: `index.html`, `manifest.webmanifest`,
|
||||
`sw.js`, `icon-192.png`, `icon-512.png`, `icon-maskable-512.png`.
|
||||
3. **Settings → Pages → Source: Deploy from a branch → `main` / `(root)` → Save.**
|
||||
4. Wait a minute, then open `https://<you>.github.io/bgclock/` on your phone.
|
||||
5. **Android, Chrome:** menu (⋮) → **Add to Home screen** → **Install**.
|
||||
**iOS, Safari:** Share (□↑) → **Add to Home Screen** → **Add**. It has to be
|
||||
Safari — Chrome and Firefox on iOS can't install a web app.
|
||||
|
||||
Open it from the home-screen icon and it runs fullscreen with no browser
|
||||
chrome, offline, with the screen kept awake while a game is on.
|
||||
|
||||
On iOS a few things behave differently, none of them fatal:
|
||||
|
||||
- **Install from Safari, and play from the home-screen icon.** In a Safari tab
|
||||
you lose a strip of the screen to the toolbar, and iOS clears the saved
|
||||
settings of a site you haven't visited for a week. The installed app keeps
|
||||
its own storage and doesn't get pruned that way.
|
||||
- **There's no vibration** — iOS gives web pages no haptics at all, so the
|
||||
turn-change buzz is silent there. The sounds still play.
|
||||
- **The ringer switch doesn't mute the clock.** The page claims a playback
|
||||
audio session so you can hear the delay tick with the phone silenced, which
|
||||
in exchange pauses any music you had going. If you'd rather keep the music,
|
||||
delete the `navigator.audioSession` line in `index.html`.
|
||||
- **Screen wake needs iOS 16.4 or newer.** Below that the screen dims on its
|
||||
usual schedule mid-game.
|
||||
|
||||
If you're using a different path than `/bgclock/`, edit the `"id"` field in
|
||||
`manifest.webmanifest` to match, or just delete that line.
|
||||
|
||||
To try it before hosting, open `index.html` in any browser — everything works
|
||||
except installation and the service worker.
|
||||
|
||||
## Using it
|
||||
|
||||
- **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
|
||||
0–0. It takes two taps: the first arms the button — it lights up and a ring
|
||||
unwinds around it — and a second tap within those two seconds does it. One
|
||||
stray thumb can't wipe a live match.
|
||||
- **Pause** freezes mid-turn and resumes exactly where it stopped, delay
|
||||
included. Opening either sheet pauses automatically. The settings sheet leaves
|
||||
it paused — everything in there resets the clocks anyway — while the score
|
||||
sheet picks the turn back up when you close it, so glancing at the score
|
||||
doesn't cost you a tap on play.
|
||||
- **Settings are only reachable before a match starts.** Every control in that
|
||||
sheet resets both clocks, so the gear disappears the moment the first tap
|
||||
starts the clock and comes back after a reset. Its space in the bar is kept
|
||||
empty, so nothing else shifts. That means a mistake in the time or delay can
|
||||
only be fixed by resetting and starting the match again — deliberately, since
|
||||
the alternative is wiping two live clocks by accident. Correcting a clock
|
||||
mid-match is what the score sheet is for.
|
||||
|
||||
Backgammon clocks pause between games, and after a cocked die the delay is
|
||||
normally restarted — use pause and reset for those.
|
||||
|
||||
## Matches
|
||||
|
||||
Set **Match** in the settings to the number of points you're playing to — 1, 3,
|
||||
5 and up to 29. The time you set is *per point*, so a 5-point match at 2:00 a
|
||||
point puts 10:00 on each clock, and the settings sheet shows you that sum.
|
||||
|
||||
Each half reads the state from that player's own side of the board:
|
||||
|
||||
```
|
||||
You: 1 out of 5
|
||||
Them: 3
|
||||
Moves: 7
|
||||
```
|
||||
|
||||
So "You" always means whoever is sitting at that end — both players read their
|
||||
own score on the top line, and the two halves show mirrored numbers.
|
||||
|
||||
At **1 point** there's no match to keep score of, so the score lines and the
|
||||
score-sheet icon all disappear and you get the plain single-game clock with just
|
||||
its move count. Set the match to 3 or more and they come back.
|
||||
|
||||
The score sheet is the middle icon in the bar. It's filled in from the phone
|
||||
holder's side: **You** is the bottom half, **Them** the far end.
|
||||
|
||||
Nothing in that sheet takes effect until you press **Done**:
|
||||
|
||||
- **Done with nothing changed** leaves everything exactly as it was — same turn,
|
||||
same delay part-spent, same move counts — and the clock picks up where it left
|
||||
off. Tapping outside the sheet is the same as changing nothing, so a stray tap
|
||||
can't restart a game. A pause you set yourself before opening the sheet
|
||||
survives; only the sheet's own pause is undone.
|
||||
- **Change a score** and the game restarts when you press Done: move counters
|
||||
back to zero and the turn back to the first tap, waiting on whoever rolls the
|
||||
lower die. The clocks keep whatever they were showing.
|
||||
- **Change only a clock** — the sheet also lets you correct either player's time
|
||||
to the second — and the game carries on undisturbed and running: same turn,
|
||||
same delay position, same moves.
|
||||
|
||||
Shortening the match to fewer points than someone has already won pulls their
|
||||
score down to fit.
|
||||
|
||||
## Settings
|
||||
|
||||
At the top of the sheet are three presets — **2m / 12s**, **3m / 12s** and
|
||||
**3m / 15s**. Tapping one jumps both timers there and lights it up. Change the
|
||||
time or delay by hand and the highlight goes out; land back on a preset's
|
||||
numbers and it comes back on.
|
||||
|
||||
| Setting | Range | Default |
|
||||
| --- | --- | --- |
|
||||
| Match | 1 – 29 points, odd | 1 |
|
||||
| Time | 0:15 – 99:00 per point | 3:00 |
|
||||
| Delay | 0 – 60 s | 12 s |
|
||||
| Colour | Sage, Brass, Slate, Teal, Plum | Sage |
|
||||
| Sound | on / off | on |
|
||||
|
||||
Brass is the colour in the chess.com app, if you want the exact original look.
|
||||
Changing match, time or delay resets the clocks — the score is left alone, so you
|
||||
can fix the delay mid-match without losing where you are.
|
||||
|
||||
Settings **and the match in progress** are kept in `localStorage`, so they
|
||||
survive closing the tab, force-quitting the installed app and rebooting the
|
||||
phone — the app also asks for persistent storage so they aren't evicted when the
|
||||
device runs low on space (Chrome honours that request; Safari ignores it).
|
||||
Reopening restores the score, both clocks, the move counts, whose turn it is and
|
||||
how much delay was left. The one thing it can't restore is time passing while
|
||||
the app was closed, so it comes back paused rather than charging anyone for the
|
||||
gap: a crash shouldn't decide a game.
|
||||
|
||||
## Sounds
|
||||
|
||||
All three are generated with the Web Audio API, so there are no audio files to
|
||||
download and nothing to attribute.
|
||||
|
||||
- **Turn change** — a soft rising fifth with an octave partial, marimba-like.
|
||||
- **Delay expiry** — a single quiet 300 Hz tick, deliberately easy to miss unless
|
||||
you're listening for it.
|
||||
- **Time out** — a falling triad, loud and unambiguous.
|
||||
|
||||
## Licence
|
||||
|
||||
Do whatever you like with it.
|
||||
+15
-10
@@ -761,8 +761,13 @@
|
||||
}
|
||||
|
||||
// here rather than in the mutators, so it can never go stale; the
|
||||
// two-argument toggle leaves the DOM alone when the state already matches
|
||||
btnSettings.classList.toggle("btn--vacant", !matchPristine());
|
||||
// two-argument toggle leaves the DOM alone when the state already matches.
|
||||
// Settings and Reset are exact opposites: before the match you can change
|
||||
// the setup and have nothing to reset, and once it's running, the reverse.
|
||||
var pristine = matchPristine();
|
||||
btnSettings.classList.toggle("btn--vacant", !pristine);
|
||||
btnReset.classList.toggle("btn--vacant", pristine); // nothing played to reset
|
||||
btnPlay.classList.toggle("btn--vacant", st.phase === IDLE); // nothing running to pause
|
||||
}
|
||||
|
||||
function loop(){ render(); requestAnimationFrame(loop); }
|
||||
@@ -778,13 +783,12 @@
|
||||
saveGame();
|
||||
}
|
||||
|
||||
// `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){
|
||||
function tap(side){
|
||||
if(st.phase === FLAG) return;
|
||||
if(st.phase === PAUSE) return;
|
||||
var now = at || performance.now();
|
||||
// the lift, not the press: your clock runs for as long as your finger is
|
||||
// down, so a held thumb costs you exactly the time it was seen counting off
|
||||
var now = performance.now();
|
||||
|
||||
if(st.phase === IDLE){
|
||||
st.active = 1 - side; // you tap your own side to start your opponent
|
||||
@@ -798,7 +802,8 @@
|
||||
}
|
||||
if(side !== st.active) return; // the waiting player can't stop the clock
|
||||
|
||||
// not elapsed(): that measures to *now*, and this has to measure to the press
|
||||
// the same instant charges the outgoing turn and starts the incoming one,
|
||||
// so nothing falls between the two and goes to neither player
|
||||
var e = now - st.turnAt;
|
||||
st.reserve[side] = Math.max(0, st.reserve[side] - Math.max(0, e - cfg.delay));
|
||||
st.moves[side]++;
|
||||
@@ -1177,7 +1182,7 @@
|
||||
p.addEventListener("pointerdown", function(ev){
|
||||
ev.preventDefault();
|
||||
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 };
|
||||
down = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
|
||||
// so the lift still lands here if the finger drifts onto the other panel
|
||||
try{ p.setPointerCapture(ev.pointerId); }catch(e){}
|
||||
});
|
||||
@@ -1197,7 +1202,7 @@
|
||||
document.documentElement.requestFullscreen().catch(function(){});
|
||||
}
|
||||
}
|
||||
tap(side, d.t);
|
||||
tap(side);
|
||||
});
|
||||
|
||||
// the branch Android's home gesture takes
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* Cache-first: once installed the clock never touches the network again. */
|
||||
const CACHE = "bgclock-v11";
|
||||
const CACHE = "bgclock-v12";
|
||||
const FILES = [
|
||||
"./",
|
||||
"./index.html",
|
||||
|
||||
Reference in New Issue
Block a user