fuzzyworld

explodorama / files / src / ui / padHold.ts

padHold.ts

2.9 KB · history · blame

1/**
2 * Hold to confirm (Q59, decisions.md 2026-11-02): a face button on the pad acts only if it is held
3 * until a ring has filled, and then at that moment, while it is still down. That button is then
4 * used up: it does nothing more, and starts no hold in whatever comes next, until it is let go.
5 * Pure, like padMap.ts: the caller feeds it the frame's clock and which button is down, and acts
6 * on the events that come back. One hold at a time: while a button is being held (or used up),
7 * another face button is ignored.
8 */
9
10/** How long a face button is held for the ring to fill. */
11export const HOLD_SECONDS = 0.5
12
13export type HoldEvent =
14 /** The ring, every frame while a button is held and its ring is filling: 0..1. */
15 | { kind: 'progress'; face: number; progress: number }
16 /** The ring just filled: the action, once. The button stays used up until it is let go (`release`). */
17 | { kind: 'fire'; face: number }
18 /** A used-up button was let go: the ring clears, nothing else happens. Ends the hold. */
19 | { kind: 'release'; face: number }
20 /** Released too early, or the pad went away: nothing happens. Ends the hold. */
21 | { kind: 'cancel'; face: number }
22
23/** MUTATED IN PLACE by the functions below. `face` is the button being held (or used up), or null. */
24export interface HoldState {
25 face: number | null
26 startMs: number
27 /** The ring filled and the action fired: the button is used up until it is let go. */
28 done: boolean
29}
30
31export function newHold(): HoldState {
32 return { face: null, startMs: 0, done: false }
33}
34
35/** A face button went down at `nowMs`. False (and nothing changes) when another one is being held or used up already. */
36export function startHold(h: HoldState, face: number, nowMs: number): boolean {
37 if (h.face !== null) return false
38 h.face = face
39 h.startMs = nowMs
40 h.done = false
41 return true
42}
43
44/** The ring's fill after `nowMs - startMs`, 0..1. */
45export function holdProgress(startMs: number, nowMs: number): number {
46 return Math.max(0, Math.min(1, (nowMs - startMs) / (HOLD_SECONDS * 1000)))
47}
48
49/** One frame: `isDown(face)` says whether the held button still is. */
50export function stepHold(h: HoldState, isDown: (face: number) => boolean, nowMs: number): HoldEvent[] {
51 if (h.face === null) return []
52 const face = h.face
53 if (!isDown(face)) {
54 h.face = null
55 const was = h.done
56 h.done = false
57 return [was ? { kind: 'release', face } : { kind: 'cancel', face }]
58 }
59 if (h.done) return []
60 const progress = holdProgress(h.startMs, nowMs)
61 if (progress < 1) return [{ kind: 'progress', face, progress }]
62 h.done = true
63 return [{ kind: 'progress', face, progress }, { kind: 'fire', face }]
64}
65
66/** The pad left, was swapped, or the window lost focus: the hold ends with no action, and a used-up button is free again. */
67export function cancelHold(h: HoldState): HoldEvent[] {
68 if (h.face === null) return []
69 const face = h.face
70 h.face = null
71 h.done = false
72 return [{ kind: 'cancel', face }]
73}