picker/README.md
2026-10-09 13:45:54 +02:00

6 KiB
Raw Permalink Blame History

Picker

A simple web app to select who pays the bills: everyone puts a finger on the phone screen, and after a suspenseful roulette one of you is chosen.

Plain HTML, CSS and vanilla JS modules. No build step and no dependencies. Works on any static host and installs as an offline PWA.

Modes

  • Pick one: one finger is chosen.
  • Pick several: N fingers are chosen.
  • Teams: everyone is split into 2–4 balanced teams.
  • Order: everyone gets a number, revealed one at a time.
  • Elimination: one finger is knocked out per round until one remains. Letting go early counts as being knocked out.

Settings (gear button on the idle screen) also cover the stakes text ("pay the bill" → "You pay the bill!"), theme (neon, pastel, or high contrast with a distinct shape per finger), language (English/Dutch), sound and vibration. Settings are saved in localStorage.

How a round works

  1. Put two or more fingers down. Once the set of fingers has been stable for 2.5 s, a three-beat countdown starts. Adding a finger restarts the wait. Lifting one shows a "chickened out" message.
  2. The outcome is drawn with crypto.getRandomValues before the animation starts. The roulette is only presentation and always lands on the pre-drawn result.
  3. The result stays on screen after fingers lift. Once everyone has let go, tap to play again (the tapping finger joins the next round).

Project layout

File What
js/main.js wiring + frame loop
js/input.js Pointer Events per pointerId, gesture blocking, debug fake fingers
js/game.js state machine (idle → gathering → countdown → suspense → result)
js/modes.js outcome planning and roulette schedules for each mode
js/random.js unbiased crypto randomness
js/renderer.js canvas rendering (pre-rendered glow sprites)
js/reveals.js reveal animations: flood, confetti, spotlight, shockwave, calm
js/audio.js Web Audio sound effects + vibration
js/ui.js overlay text, toolbar, settings sheet
js/settings.js persisted settings
js/i18n.js all strings (en, nl)
js/themes.js colour themes
js/device.js wake lock, fullscreen
sw.js offline caching (network-first, cache fallback)
tools/make_icons.py regenerates the PNG icons (Python stdlib only)

Testing on a desktop

Open http://localhost:8000/?debug=1:

  • Click to place a fake finger, click it again to remove it, drag to move it.
  • A adds a fake finger at a random spot, C clears them all.
  • ?debug=1&fingers=4 starts with 4 fake fingers in a circle.
  • ?debug=1&open=settings opens the settings sheet on load.
  • A HUD shows the phase, finger count and fps. The service worker is not registered in debug mode, so you always get fresh files.

Automated checks (serve the folder, then open in any browser):

  • tests/logic.html: state machine and fairness tests.
  • tests/sim.html?mode=order&n=5&t=11500: renders one simulated frame at a given time (mode, n, t, theme, style, k, teams, reduced).

Testing on your phone (local network)

Serve the folder from your PC:

python -m http.server 8000 --bind 0.0.0.0

Find your PC's LAN IP (ipconfig on Windows, ip addr on Linux), then open http://<that-ip>:8000/ on the phone. On Windows, allow Python through the firewall for private networks when asked.

Over plain http:// on a LAN address, the page is not a secure context. These features need HTTPS (or localhost):

Feature Plain http on LAN
Touch, canvas, crypto randomness, sound, vibration, fullscreen works
Service worker / offline mode needs HTTPS
Install as app (PWA) needs HTTPS
Screen wake lock needs HTTPS

To test the secure-only features:

  • Android + USB (easiest): enable USB debugging, open chrome://inspect in desktop Chrome or Edge, click Port forwarding and map 8000 → localhost:8000. Then open http://localhost:8000/ on the phone. localhost counts as secure, so everything works.
  • Android without USB: on the phone, open chrome://flags/#unsafely-treat-insecure-origin-as-secure and add http://<your-ip>:8000. Use this for testing only.
  • Anywhere, iOS included: put it behind HTTPS (see Docker below, behind your reverse proxy).

Browser notes:

  • Browsers only allow sound and vibration after a tap or a finger lift. A finger going down doesn't count. If the very first round after opening is silent, tap the screen once (or use any toolbar button) first.
  • iOS Safari has no vibration API and no fullscreen API on iPhone, so those controls are hidden there. Add it to the home screen for a full-screen experience.
  • Devices cap simultaneous touches (iPhone: 5, most Android phones: 10). The app handles whatever arrives.

Docker

The image is plain unprivileged nginx serving the static files on port 8080, with a /healthz endpoint.

docker build -t picker .
docker run --rm -p 8080:8080 picker

On every push to main, the GitHub Actions workflow (.github/workflows/docker.yml) publishes a multi-arch (amd64 + arm64) image to GHCR:

  • ghcr.io/<owner>/<repo>:latest on main
  • :1.2.3 and :1.2 when you push a v1.2.3 tag
  • :sha-abc1234 on every build

Example docker-compose.yml for a home server:

services:
  picker:
    image: ghcr.io/<owner>/<repo>:latest
    restart: unless-stopped
    ports:
      - "8080:8080"

Point your reverse proxy (Caddy, Traefik, nginx proxy manager, …) at port 8080 for your subdomain and terminate HTTPS there. HTTPS is required for offline mode, installing and wake lock. All paths are relative, so it also works under a sub-path.

GHCR packages are private by default. Either make the package public (GitHub → your profile → Packages → picker → Package settings), or run docker login ghcr.io on the server with a token that has read:packages.

Icons

icons/*.png are generated: python tools/make_icons.py.