Files
lhns--steam-frame-nix/modules/vr-keyboard-touch/tracker.js
T
Pierre Kisters 0699d0f532 VR keyboard touch typing: the touched key highlights (and long-presses)
Steam's UpdateTouchState counts the event's `touches` per key: that count
is the pressed highlight (rgLayoutTouchCount -> Touched) and where the long
press (Backspace repeat, accents) starts. The touch-start event left the
new touch out of `touches`, like a touchend; now it is in, as in a DOM
touchstart (tracker.js touchEvent, with a test against a model of Steam's
handlers).
2026-10-01 03:36:13 +02:00

79 lines
3.6 KiB
JavaScript

// tracker.js: when a controller's tip touches a key (vr-keyboard-touch.nix;
// argument of keyboard-patch.js, tested by tests/tracker.test.mjs).
// Evaluates to { VERSION, DEFAULTS, createTracker, touchEvent }.
//
// createTracker(opts): one hand. update(sample, tMs) with the tip relative
// to the keyboard ({ u, v, d } of vr-keyboard-controllers' geometry.js:
// keyboard widths from the top left corner, m behind the surface; null: no
// pose / keyboard unusable) returns null or an event { phase, u, v }:
// down the tip passed through the surface (at `depth` behind it) from
// the front between two samples, at u, v (interpolated) inside the
// keyboard, not faster than maxStep per sample, minIntervalMs
// after the last release;
// up it came back out: release m back from its deepest point, or in
// front of the surface by arm; u, v where it is now;
// cancel the sample was null while down.
// A press needs the tip in front by arm (re-armed) since the last one, so
// tracking jitter at the surface can't repeat it, and the tip coming from
// behind or around the keyboard never presses.
//
// touchEvent(t, others, starting): what Steam's HandleTouchStart /
// HandleTouchEnd read, shaped like a DOM TouchEvent: changedTouches [t];
// touches: the touches down, with t at its start, without it at its end.
// Steam's UpdateTouchState counts `touches` per key for the pressed
// highlight and starts the long press (Backspace repeat, accents) there.
(() => {
const VERSION = 2;
const DEFAULTS = {
depth: 0, // m behind the drawn surface where a touch registers
arm: 0.005, // m in front of it to re-arm
release: 0.01, // m back from the deepest point to release
maxV: 0.5, // keyboard height bound (widths; the page is ~0.33)
maxStep: 0.08, // m per sample; more is a tracking jump, not a touch
minIntervalMs: 60, // release -> next press
};
function createTracker(opts = {}) {
const O = { ...DEFAULTS, ...opts };
let prev = null, armed = false, pressed = false, deepest = 0, releasedAt = -Infinity;
const inside = (u, v) => u >= 0 && u <= 1 && v >= 0 && v <= O.maxV;
function update(s, t) {
if (!s) {
prev = null; armed = false;
if (!pressed) return null;
pressed = false; releasedAt = t;
return { phase: 'cancel' };
}
const d = s.d - O.depth;
let ev = null;
if (pressed) {
deepest = Math.max(deepest, d);
if (d < deepest - O.release || d < -O.arm) {
pressed = false; armed = false; releasedAt = t;
ev = { phase: 'up', u: s.u, v: s.v };
}
} else if (armed && prev && prev.d < 0 && d >= 0) {
armed = false;
const f = prev.d / (prev.d - d); // where the segment meets the surface
const u = prev.u + f * (s.u - prev.u), v = prev.v + f * (s.v - prev.v);
if (inside(u, v) && d - prev.d <= O.maxStep && t - releasedAt >= O.minIntervalMs) {
pressed = true; deepest = d;
ev = { phase: 'down', u, v };
}
} else if (d < -O.arm) armed = true;
else if (d >= 0) armed = false;
prev = { u: s.u, v: s.v, d };
return ev;
}
return { update, get pressed() { return pressed; } };
}
const touchEvent = (t, others, starting) => ({
type: starting ? 'touchstart' : 'touchend', target: t.target, changedTouches: [t],
touches: starting ? [...others, t] : [...others],
preventDefault() {}, stopPropagation() {},
});
return { VERSION, DEFAULTS, createTracker, touchEvent };
})()