mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-19 09:01:13 +02:00
58 lines
2.3 KiB
JavaScript
58 lines
2.3 KiB
JavaScript
// Copyright (C) 2026 half144 and PS5 React contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution term: see LICENSE-ATTRIBUTION.
|
|
// The touchpad as a D-pad: a finger sliding across it steps focus once per stretch of travel, so a
|
|
// slow swipe walks card by card and a long one runs further. See docs/NAVIGATION.md.
|
|
|
|
// Travel per step, as a fraction of the pad's width or height: about nine cards across a full swipe,
|
|
// five rows down one (the pad is twice as wide as it is tall).
|
|
export const STEP_X = 0.11;
|
|
export const STEP_Y = 0.2;
|
|
// Travel before a gesture settles on an axis: a tap or a wobbling finger moves nothing, and a swipe
|
|
// stays on its axis to the end, so a sideways swipe never slips into the next row.
|
|
export const LOCK = 0.03;
|
|
// The pad is about twice as wide as it is tall: a fraction of its width is twice the distance of the
|
|
// same fraction of its height, which the axis lock has to weigh.
|
|
const ASPECT = 2;
|
|
|
|
export class TouchNavigator {
|
|
/** @param {(direction: import('./geometry.js').Direction) => void} step */
|
|
constructor(step) {
|
|
this.step = step;
|
|
this.release();
|
|
}
|
|
|
|
/** The finger at (x, y), each 0..1 across the pad; `null` x when it lifts. */
|
|
touch(x, y) {
|
|
if (x == null) return this.release();
|
|
if (this.last) this.slide(x - this.last.x, y - this.last.y);
|
|
this.last = {x, y};
|
|
return undefined;
|
|
}
|
|
|
|
release() {
|
|
this.last = null;
|
|
this.axis = null;
|
|
this.travel = {x: 0, y: 0};
|
|
this.taken = 0;
|
|
}
|
|
|
|
// Steps sit at fixed points along the swipe since it settled on an axis, like a dial's detents:
|
|
// going back over one steps back, so returning to where the swipe began returns focus too.
|
|
/** @param {number} dx @param {number} dy */
|
|
slide(dx, dy) {
|
|
const travel = this.travel;
|
|
travel.x += dx;
|
|
travel.y += dy;
|
|
if (!this.axis) {
|
|
const across = Math.abs(travel.x) * ASPECT, down = Math.abs(travel.y);
|
|
if (Math.max(across, down) < LOCK) return;
|
|
this.axis = across >= down ? 'x' : 'y';
|
|
}
|
|
const [size, back, forward] = this.axis === 'x' ? [STEP_X, 'left', 'right'] : [STEP_Y, 'up', 'down'];
|
|
const due = Math.trunc(travel[this.axis] / size);
|
|
for (; this.taken < due; this.taken++) this.step(forward);
|
|
for (; this.taken > due; this.taken--) this.step(back);
|
|
}
|
|
}
|