three-gamepad-controls 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,9 @@ Gamepad support for [Three.js](https://threejs.org) controls, built on top of [W
4
4
 
5
5
  ## Architecture
6
6
 
7
- ![Architecture diagram showing GamepadManager used by GamepadInput, GamepadInput used by GamepadControls, and specific wrappers extending GamepadControls.](./assets/architecture-diagram.webp "Three.js Gamepad Controls architecture")
7
+ ![Layered architecture showing the shared gamepad input foundation branching into direct application input and Three.js control integrations.](./assets/architecture-diagram.webp "Three.js Gamepad Controls layered architecture")
8
+
9
+ The library is organized around a shared input foundation that handles gamepad selection, polling, button states, axes, sticks, haptics, and standard mappings. Applications can consume this input directly for gameplay, menus, and custom interactions, or use `GamepadControls` and its ready-made or custom wrappers to integrate gamepads with Three.js controls.
8
10
 
9
11
  ## 📦 Installation
10
12
 
@@ -55,6 +57,7 @@ bun add -d @types/three # optional: for TypeScript projects
55
57
 
56
58
  - [Core](./docs/core.md) — The fundamental building blocks.
57
59
  - [GamepadInput](./docs/gamepad-input.md) - Low-level reader for gamepad buttons, axes, sticks, and transitions.
60
+ - [Gamepad Stick Processing](./docs/gamepad-stick-processing.md) - Stateless processors, pipelines, and action bindings.
58
61
  - [Haptic Feedback](./docs/haptic-feedback.md) - Optional gamepad vibration effects with graceful degradation.
59
62
  - [Multiple Gamepads](./docs/multiple-gamepads.md) - Assign different gamepads to controls, players, and gameplay inputs.
60
63
  - [GamepadControls](./docs/gamepad-controls.md) — Abstract base class for custom gamepad controls.
@@ -1,3 +1,4 @@
1
+ import { GamepadStickBindingOptions } from "./gamepad-stick-processing.js";
1
2
  import { GamepadControls, GamepadControlsOptions } from "./gamepad-controls.js";
2
3
  import { ArcballControls } from "three/addons/controls/ArcballControls.js";
3
4
  //#region src/gamepad-arcball-controls.d.ts
@@ -28,30 +29,20 @@ type GamepadArcballControlsOptions = GamepadControlsOptions & {
28
29
  */
29
30
  zRotateSpeed: number;
30
31
  /**
31
- * Axis dead zone threshold in the range `[0, 1]`.
32
- * @default 0.1
33
- */
34
- deadzone: number;
35
- /**
36
- * Axis index for **horizontal** arcball rotation.
37
- * @default 0 - Left stick X
32
+ * Stick binding used for arcball rotation.
33
+ * @default Left stick with the default stick pipeline
38
34
  */
39
- axisRotateX: number;
35
+ rotateStick: GamepadStickBindingOptions;
40
36
  /**
41
- * Axis index for **vertical** arcball rotation.
42
- * @default 1 - Left stick Y
37
+ * Stick binding used for panning.
38
+ * @default Right stick with the default stick pipeline
43
39
  */
44
- axisRotateY: number;
40
+ panStick: GamepadStickBindingOptions;
45
41
  /**
46
- * Axis index for **horizontal** panning.
47
- * @default 2 - Right stick X
48
- */
49
- axisPanX: number;
50
- /**
51
- * Axis index for **vertical** panning.
52
- * @default 3 - Right stick Y
42
+ * Dead zone threshold for analog button and trigger values.
43
+ * @default 0.1
53
44
  */
54
- axisPanY: number;
45
+ buttonDeadzone: number;
55
46
  /**
56
47
  * Button index for zooming **in** (analog trigger value used for proportional zoom).
57
48
  * @default 7 - Right trigger
@@ -1,4 +1,5 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON } from "./core.js";
2
+ import { DEFAULT_GAMEPAD_STICK_PIPELINE, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
2
3
  import { GamepadControls } from "./gamepad-controls.js";
3
4
  import { Vector2, Vector3 } from "three";
4
5
  //#region src/gamepad-arcball-controls.ts
@@ -7,11 +8,17 @@ const DEFAULT_ARCBALL_OPTIONS = {
7
8
  panSpeed: 1,
8
9
  zoomSpeed: 1,
9
10
  zRotateSpeed: 1,
10
- deadzone: .1,
11
- axisRotateX: GAMEPAD_AXIS.LeftX,
12
- axisRotateY: GAMEPAD_AXIS.LeftY,
13
- axisPanX: GAMEPAD_AXIS.RightX,
14
- axisPanY: GAMEPAD_AXIS.RightY,
11
+ rotateStick: {
12
+ xAxis: GAMEPAD_AXIS.LeftX,
13
+ yAxis: GAMEPAD_AXIS.LeftY,
14
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
15
+ },
16
+ panStick: {
17
+ xAxis: GAMEPAD_AXIS.RightX,
18
+ yAxis: GAMEPAD_AXIS.RightY,
19
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
20
+ },
21
+ buttonDeadzone: .1,
15
22
  buttonZoomIn: GAMEPAD_BUTTON.RightTrigger,
16
23
  buttonZoomOut: GAMEPAD_BUTTON.LeftTrigger,
17
24
  buttonZRotateLeft: GAMEPAD_BUTTON.LeftShoulder,
@@ -47,7 +54,9 @@ var GamepadArcballControls = class extends GamepadControls {
47
54
  this.#controls = controls;
48
55
  this.#options = {
49
56
  ...DEFAULT_ARCBALL_OPTIONS,
50
- ...options
57
+ ...options,
58
+ rotateStick: resolveGamepadStickBinding(DEFAULT_ARCBALL_OPTIONS.rotateStick, options?.rotateStick),
59
+ panStick: resolveGamepadStickBinding(DEFAULT_ARCBALL_OPTIONS.panStick, options?.panStick)
51
60
  };
52
61
  this.#centerNdc = new Vector2(0, 0);
53
62
  this.#panStart = new Vector3();
@@ -65,20 +74,30 @@ var GamepadArcballControls = class extends GamepadControls {
65
74
  */
66
75
  onUpdate(deltaTime) {
67
76
  const controls = this.#controls;
68
- const { rotateSpeed, panSpeed, zoomSpeed, zRotateSpeed, deadzone, axisRotateX, axisRotateY, axisPanX, axisPanY, buttonZoomIn, buttonZoomOut, buttonZRotateLeft, buttonZRotateRight, buttonFocus } = this.#options;
77
+ const { rotateSpeed, panSpeed, zoomSpeed, zRotateSpeed, rotateStick, panStick, buttonDeadzone, buttonZoomIn, buttonZoomOut, buttonZRotateLeft, buttonZRotateRight, buttonFocus } = this.#options;
69
78
  const input = this.gamepadInput;
70
79
  const focusPoint = this.#consumeFocusPoint(buttonFocus);
71
80
  if (!controls.enabled) {
72
81
  this.#endInteraction();
73
82
  return;
74
83
  }
75
- const rotateX = controls.enableRotate ? input.axis(axisRotateX, { deadzone }) : 0;
76
- const rotateY = controls.enableRotate ? input.axis(axisRotateY, { deadzone }) : 0;
77
- const panX = controls.enablePan ? input.axis(axisPanX, { deadzone }) : 0;
78
- const panY = controls.enablePan ? input.axis(axisPanY, { deadzone }) : 0;
84
+ let rotateX = 0;
85
+ let rotateY = 0;
86
+ if (controls.enableRotate) {
87
+ const rotate = input.stick(rotateStick.xAxis, rotateStick.yAxis, rotateStick.pipeline);
88
+ rotateX = rotate.x;
89
+ rotateY = rotate.y;
90
+ }
91
+ let panX = 0;
92
+ let panY = 0;
93
+ if (controls.enablePan) {
94
+ const pan = input.stick(panStick.xAxis, panStick.yAxis, panStick.pipeline);
95
+ panX = pan.x;
96
+ panY = pan.y;
97
+ }
79
98
  const zoom = controls.enableZoom ? input.buttonValue(buttonZoomIn) - input.buttonValue(buttonZoomOut) : 0;
80
99
  const zRotation = controls.enableRotate ? input.buttonValue(buttonZRotateLeft) - input.buttonValue(buttonZRotateRight) : 0;
81
- const activeInput = rotateX !== 0 || rotateY !== 0 || panX !== 0 || panY !== 0 || Math.abs(zoom) > deadzone || Math.abs(zRotation) > deadzone;
100
+ const activeInput = rotateX !== 0 || rotateY !== 0 || panX !== 0 || panY !== 0 || Math.abs(zoom) > buttonDeadzone || Math.abs(zRotation) > buttonDeadzone;
82
101
  if (!activeInput && focusPoint === null) {
83
102
  this.#endInteraction();
84
103
  return;
@@ -87,8 +106,8 @@ var GamepadArcballControls = class extends GamepadControls {
87
106
  let changed = false;
88
107
  changed = this.#applyRotation(deltaTime, rotateX, rotateY, rotateSpeed) || changed;
89
108
  changed = this.#applyPan(deltaTime, panX, panY, panSpeed) || changed;
90
- changed = this.#applyZoom(deltaTime, zoom, zoomSpeed, deadzone) || changed;
91
- changed = this.#applyZRotation(deltaTime, zRotation, zRotateSpeed, deadzone) || changed;
109
+ changed = this.#applyZoom(deltaTime, zoom, zoomSpeed, buttonDeadzone) || changed;
110
+ changed = this.#applyZRotation(deltaTime, zRotation, zRotateSpeed, buttonDeadzone) || changed;
92
111
  changed = this.#applyFocus(focusPoint) || changed;
93
112
  if (changed) {
94
113
  controls.update();
@@ -163,11 +182,11 @@ var GamepadArcballControls = class extends GamepadControls {
163
182
  * @param deltaTime - Seconds since the last frame.
164
183
  * @param zoom - Signed zoom input from the configured trigger pair.
165
184
  * @param zoomSpeed - User-configured zoom speed multiplier.
166
- * @param deadzone - Trigger dead zone threshold.
185
+ * @param buttonDeadzone - Trigger dead zone threshold.
167
186
  * @returns `true` when a zoom transform was applied.
168
187
  */
169
- #applyZoom(deltaTime, zoom, zoomSpeed, deadzone) {
170
- if (Math.abs(zoom) <= deadzone || this.#controls.scaleFactor <= 0) return false;
188
+ #applyZoom(deltaTime, zoom, zoomSpeed, buttonDeadzone) {
189
+ if (Math.abs(zoom) <= buttonDeadzone || this.#controls.scaleFactor <= 0) return false;
171
190
  const controls = this.#controls;
172
191
  const size = controls.scaleFactor ** (zoom * zoomSpeed * deltaTime * ZOOM_NOTCHES_PER_SECOND);
173
192
  if (!Number.isFinite(size) || size <= 0 || size === 1) return false;
@@ -180,11 +199,11 @@ var GamepadArcballControls = class extends GamepadControls {
180
199
  * @param deltaTime - Seconds since the last frame.
181
200
  * @param zRotation - Signed z-rotation input from the configured buttons.
182
201
  * @param zRotateSpeed - User-configured z-rotation speed multiplier.
183
- * @param deadzone - Button value dead zone threshold.
202
+ * @param buttonDeadzone - Button value dead zone threshold.
184
203
  * @returns `true` when a z-rotation transform was applied.
185
204
  */
186
- #applyZRotation(deltaTime, zRotation, zRotateSpeed, deadzone) {
187
- if (Math.abs(zRotation) <= deadzone) return false;
205
+ #applyZRotation(deltaTime, zRotation, zRotateSpeed, buttonDeadzone) {
206
+ if (Math.abs(zRotation) <= buttonDeadzone) return false;
188
207
  const controls = this.#controls;
189
208
  const angle = zRotation * zRotateSpeed * deltaTime * Math.PI;
190
209
  controls.updateMatrixState();
@@ -1,3 +1,4 @@
1
+ import { GamepadStickBindingOptions } from "./gamepad-stick-processing.js";
1
2
  import { GamepadControls, GamepadControlsOptions } from "./gamepad-controls.js";
2
3
  import { DragControls } from "three/addons/controls/DragControls.js";
3
4
  //#region src/gamepad-drag-controls.d.ts
@@ -18,30 +19,15 @@ type GamepadDragControlsOptions = GamepadControlsOptions & {
18
19
  */
19
20
  rotateSpeed: number;
20
21
  /**
21
- * Axis dead zone threshold in the range `[0, 1]`.
22
- * @default 0.1
22
+ * Stick binding used for screen-relative dragging.
23
+ * @default Left stick with the default stick pipeline
23
24
  */
24
- deadzone: number;
25
+ dragStick: GamepadStickBindingOptions;
25
26
  /**
26
- * Axis index for **horizontal** dragging.
27
- * @default 0 - Left stick X
27
+ * Stick binding used for object rotation.
28
+ * @default Right stick with the default stick pipeline
28
29
  */
29
- axisDragX: number;
30
- /**
31
- * Axis index for **vertical** dragging.
32
- * @default 1 - Left stick Y
33
- */
34
- axisDragY: number;
35
- /**
36
- * Axis index for **horizontal** object rotation.
37
- * @default 2 - Right stick X
38
- */
39
- axisRotateX: number;
40
- /**
41
- * Axis index for **vertical** object rotation.
42
- * @default 3 - Right stick Y
43
- */
44
- axisRotateY: number;
30
+ rotateStick: GamepadStickBindingOptions;
45
31
  /**
46
32
  * Button index for grabbing and dropping the object under the center reticle.
47
33
  * @default 0 - South face button
@@ -1,15 +1,21 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON } from "./core.js";
2
+ import { DEFAULT_GAMEPAD_STICK_PIPELINE, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
2
3
  import { GamepadControls } from "./gamepad-controls.js";
3
4
  import { Matrix4, Vector2, Vector3 } from "three";
4
5
  //#region src/gamepad-drag-controls.ts
5
6
  const DEFAULT_DRAG_OPTIONS = {
6
7
  dragSpeed: 1,
7
8
  rotateSpeed: 1,
8
- deadzone: .1,
9
- axisDragX: GAMEPAD_AXIS.LeftX,
10
- axisDragY: GAMEPAD_AXIS.LeftY,
11
- axisRotateX: GAMEPAD_AXIS.RightX,
12
- axisRotateY: GAMEPAD_AXIS.RightY,
9
+ dragStick: {
10
+ xAxis: GAMEPAD_AXIS.LeftX,
11
+ yAxis: GAMEPAD_AXIS.LeftY,
12
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
13
+ },
14
+ rotateStick: {
15
+ xAxis: GAMEPAD_AXIS.RightX,
16
+ yAxis: GAMEPAD_AXIS.RightY,
17
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
18
+ },
13
19
  buttonSelect: GAMEPAD_BUTTON.South
14
20
  };
15
21
  /**
@@ -44,7 +50,9 @@ var GamepadDragControls = class extends GamepadControls {
44
50
  this.#controls = controls;
45
51
  this.#options = {
46
52
  ...DEFAULT_DRAG_OPTIONS,
47
- ...options
53
+ ...options,
54
+ dragStick: resolveGamepadStickBinding(DEFAULT_DRAG_OPTIONS.dragStick, options?.dragStick),
55
+ rotateStick: resolveGamepadStickBinding(DEFAULT_DRAG_OPTIONS.rotateStick, options?.rotateStick)
48
56
  };
49
57
  this.#centerNdc = new Vector2(0, 0);
50
58
  this.#intersections = [];
@@ -111,14 +119,12 @@ var GamepadDragControls = class extends GamepadControls {
111
119
  #updateSelected(deltaTime) {
112
120
  const selected = this.#selected;
113
121
  if (selected === null) return;
114
- const { dragSpeed, rotateSpeed, deadzone, axisDragX, axisDragY, axisRotateX, axisRotateY } = this.#options;
122
+ const { dragSpeed, rotateSpeed, dragStick, rotateStick } = this.#options;
115
123
  const input = this.gamepadInput;
116
- const dragX = input.axis(axisDragX, { deadzone });
117
- const dragY = input.axis(axisDragY, { deadzone });
118
- const rotateX = input.axis(axisRotateX, { deadzone });
119
- const rotateY = input.axis(axisRotateY, { deadzone });
120
- const dragged = this.#applyDrag(deltaTime, dragX, dragY, dragSpeed);
121
- const rotated = this.#applyRotation(deltaTime, rotateX, rotateY, rotateSpeed);
124
+ const drag = input.stick(dragStick.xAxis, dragStick.yAxis, dragStick.pipeline);
125
+ const rotate = input.stick(rotateStick.xAxis, rotateStick.yAxis, rotateStick.pipeline);
126
+ const dragged = this.#applyDrag(deltaTime, drag.x, drag.y, dragSpeed);
127
+ const rotated = this.#applyRotation(deltaTime, rotate.x, rotate.y, rotateSpeed);
122
128
  if (dragged || rotated) this.#controls.dispatchEvent({
123
129
  type: "drag",
124
130
  object: selected
@@ -1,3 +1,4 @@
1
+ import { GamepadStickBindingOptions } from "./gamepad-stick-processing.js";
1
2
  import { GamepadControls, GamepadControlsOptions } from "./gamepad-controls.js";
2
3
  import { FirstPersonControls } from "three/addons/controls/FirstPersonControls.js";
3
4
  //#region src/gamepad-first-person-controls.d.ts
@@ -18,30 +19,18 @@ type GamepadFirstPersonControlsOptions = GamepadControlsOptions & {
18
19
  */
19
20
  lookSpeed: number;
20
21
  /**
21
- * Axis dead zone threshold in the range `[0, 1]`.
22
- * @default 0.1
23
- */
24
- deadzone: number;
25
- /**
26
- * Axis index for **forward / backward** movement.
27
- * @default 1 - Left stick Y
22
+ * Movement stick axes and processing pipeline.
28
23
  */
29
- axisMoveForward: number;
24
+ moveStick: GamepadStickBindingOptions;
30
25
  /**
31
- * Axis index for **right / left** strafe movement.
32
- * @default 0 - Left stick X
26
+ * Camera-look stick axes and processing pipeline.
33
27
  */
34
- axisMoveRight: number;
28
+ lookStick: GamepadStickBindingOptions;
35
29
  /**
36
- * Axis index for **horizontal** camera look (yaw).
37
- * @default 2 - Right stick X
38
- */
39
- axisLookX: number;
40
- /**
41
- * Axis index for **vertical** camera look (pitch).
42
- * @default 3 - Right stick Y
30
+ * Dead zone threshold for analog button values.
31
+ * @default 0.1
43
32
  */
44
- axisLookY: number;
33
+ buttonDeadzone: number;
45
34
  /**
46
35
  * Button index for **moving up** (analog trigger value used for proportional speed).
47
36
  * @default 6 - Left trigger
@@ -1,15 +1,22 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON } from "./core.js";
2
+ import { DEFAULT_GAMEPAD_STICK_PIPELINE, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
2
3
  import { GamepadControls } from "./gamepad-controls.js";
3
4
  import { MathUtils, Spherical, Vector3 } from "three";
4
5
  //#region src/gamepad-first-person-controls.ts
5
6
  const DEFAULT_FIRST_PERSON_OPTIONS = {
6
7
  moveSpeed: 1,
7
8
  lookSpeed: 1,
8
- deadzone: .1,
9
- axisMoveForward: GAMEPAD_AXIS.LeftY,
10
- axisMoveRight: GAMEPAD_AXIS.LeftX,
11
- axisLookX: GAMEPAD_AXIS.RightX,
12
- axisLookY: GAMEPAD_AXIS.RightY,
9
+ moveStick: {
10
+ xAxis: GAMEPAD_AXIS.LeftX,
11
+ yAxis: GAMEPAD_AXIS.LeftY,
12
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
13
+ },
14
+ lookStick: {
15
+ xAxis: GAMEPAD_AXIS.RightX,
16
+ yAxis: GAMEPAD_AXIS.RightY,
17
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
18
+ },
19
+ buttonDeadzone: .1,
13
20
  buttonMoveUp: GAMEPAD_BUTTON.LeftTrigger,
14
21
  buttonMoveDown: GAMEPAD_BUTTON.RightTrigger
15
22
  };
@@ -37,7 +44,9 @@ var GamepadFirstPersonControls = class extends GamepadControls {
37
44
  this.#controls = controls;
38
45
  this.#options = {
39
46
  ...DEFAULT_FIRST_PERSON_OPTIONS,
40
- ...options
47
+ ...options,
48
+ moveStick: resolveGamepadStickBinding(DEFAULT_FIRST_PERSON_OPTIONS.moveStick, options?.moveStick),
49
+ lookStick: resolveGamepadStickBinding(DEFAULT_FIRST_PERSON_OPTIONS.lookStick, options?.lookStick)
41
50
  };
42
51
  this.#lookDirection = new Vector3();
43
52
  this.#spherical = new Spherical();
@@ -49,26 +58,26 @@ var GamepadFirstPersonControls = class extends GamepadControls {
49
58
  * @param deltaTime - Seconds since the last frame.
50
59
  */
51
60
  onUpdate(deltaTime) {
52
- const { moveSpeed, lookSpeed, deadzone, axisMoveForward, axisMoveRight, axisLookX, axisLookY, buttonMoveUp, buttonMoveDown } = this.#options;
53
- this.#applyMovement(deltaTime, moveSpeed, deadzone, axisMoveForward, axisMoveRight, buttonMoveUp, buttonMoveDown);
54
- this.#applyLook(deltaTime, lookSpeed, deadzone, axisLookX, axisLookY);
61
+ const { moveSpeed, lookSpeed, moveStick, lookStick, buttonDeadzone, buttonMoveUp, buttonMoveDown } = this.#options;
62
+ this.#applyMovement(deltaTime, moveSpeed, moveStick, buttonDeadzone, buttonMoveUp, buttonMoveDown);
63
+ this.#applyLook(deltaTime, lookSpeed, lookStick);
55
64
  }
56
65
  /**
57
66
  * Applies local translation input to FirstPersonControls' object.
58
67
  *
59
68
  * @param deltaTime - Seconds since the last frame.
60
69
  * @param moveSpeed - User-configured movement speed multiplier.
61
- * @param deadzone - Axis and trigger dead zone threshold.
62
- * @param axisMoveForward - Axis index for forward and backward movement.
63
- * @param axisMoveRight - Axis index for right and left strafe movement.
70
+ * @param moveStick - Resolved movement stick binding.
71
+ * @param buttonDeadzone - Analog button dead zone threshold.
64
72
  * @param buttonMoveUp - Button index for upward movement.
65
73
  * @param buttonMoveDown - Button index for downward movement.
66
74
  */
67
- #applyMovement(deltaTime, moveSpeed, deadzone, axisMoveForward, axisMoveRight, buttonMoveUp, buttonMoveDown) {
75
+ #applyMovement(deltaTime, moveSpeed, moveStick, buttonDeadzone, buttonMoveUp, buttonMoveDown) {
68
76
  const controls = this.#controls;
69
77
  const input = this.gamepadInput;
70
78
  const moveMult = deltaTime * controls.movementSpeed * moveSpeed;
71
- const forward = input.axis(axisMoveForward, { deadzone });
79
+ const move = input.stick(moveStick.xAxis, moveStick.yAxis, moveStick.pipeline);
80
+ const forward = move.y;
72
81
  if (forward !== 0) {
73
82
  let distance = forward * moveMult;
74
83
  if (forward < 0 && controls.heightSpeed) {
@@ -77,26 +86,22 @@ var GamepadFirstPersonControls = class extends GamepadControls {
77
86
  }
78
87
  controls.object.translateZ(distance);
79
88
  }
80
- const strafe = input.axis(axisMoveRight, { deadzone });
89
+ const strafe = move.x;
81
90
  if (strafe !== 0) controls.object.translateX(strafe * moveMult);
82
91
  const up = input.buttonValue(buttonMoveUp);
83
92
  const down = input.buttonValue(buttonMoveDown);
84
- if (up > deadzone) controls.object.translateY(up * moveMult);
85
- if (down > deadzone) controls.object.translateY(-down * moveMult);
93
+ if (up > buttonDeadzone) controls.object.translateY(up * moveMult);
94
+ if (down > buttonDeadzone) controls.object.translateY(-down * moveMult);
86
95
  }
87
96
  /**
88
97
  * Applies camera look input while keeping FirstPersonControls state in sync.
89
98
  *
90
99
  * @param deltaTime - Seconds since the last frame.
91
100
  * @param lookSpeed - User-configured look speed multiplier.
92
- * @param deadzone - Axis dead zone threshold.
93
- * @param axisLookX - Axis index for yaw input.
94
- * @param axisLookY - Axis index for pitch input.
101
+ * @param lookStick - Resolved camera-look stick binding.
95
102
  */
96
- #applyLook(deltaTime, lookSpeed, deadzone, axisLookX, axisLookY) {
97
- const input = this.gamepadInput;
98
- const lookX = input.axis(axisLookX, { deadzone });
99
- const lookY = input.axis(axisLookY, { deadzone });
103
+ #applyLook(deltaTime, lookSpeed, lookStick) {
104
+ const { x: lookX, y: lookY } = this.gamepadInput.stick(lookStick.xAxis, lookStick.yAxis, lookStick.pipeline);
100
105
  if (lookX === 0 && lookY === 0) return;
101
106
  const controls = this.#controls;
102
107
  const actualLookSpeed = controls.lookSpeed * lookSpeed * deltaTime * LOOK_SPEED_SCALE;
@@ -1,3 +1,4 @@
1
+ import { GamepadStickBindingOptions } from "./gamepad-stick-processing.js";
1
2
  import { GamepadControls, GamepadControlsOptions } from "./gamepad-controls.js";
2
3
  import { FlyControls } from "three/addons/controls/FlyControls.js";
3
4
  //#region src/gamepad-fly-controls.d.ts
@@ -18,30 +19,18 @@ type GamepadFlyControlsOptions = GamepadControlsOptions & {
18
19
  */
19
20
  rotateSpeed: number;
20
21
  /**
21
- * Axis dead zone threshold in the range `[0, 1]`.
22
+ * Dead zone threshold for analog button values.
22
23
  * @default 0.1
23
24
  */
24
- deadzone: number;
25
+ buttonDeadzone: number;
25
26
  /**
26
- * Axis index for **forward / backward** movement.
27
- * @default 1 — Left stick Y
27
+ * Movement stick axes and processing pipeline.
28
28
  */
29
- axisMoveForward: number;
29
+ moveStick: GamepadStickBindingOptions;
30
30
  /**
31
- * Axis index for **right / left** strafe movement.
32
- * @default 0 — Left stick X
31
+ * Camera-look stick axes and processing pipeline.
33
32
  */
34
- axisMoveRight: number;
35
- /**
36
- * Axis index for **horizontal** camera look (yaw).
37
- * @default 2 — Right stick X
38
- */
39
- axisLookX: number;
40
- /**
41
- * Axis index for **vertical** camera look (pitch).
42
- * @default 3 — Right stick Y
43
- */
44
- axisLookY: number;
33
+ lookStick: GamepadStickBindingOptions;
45
34
  /**
46
35
  * Button index for **rolling left**.
47
36
  * @default 4 — Left shoulder
@@ -1,15 +1,22 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON } from "./core.js";
2
+ import { DEFAULT_GAMEPAD_STICK_PIPELINE, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
2
3
  import { GamepadControls } from "./gamepad-controls.js";
3
4
  import { Quaternion } from "three";
4
5
  //#region src/gamepad-fly-controls.ts
5
6
  const DEFAULT_FLY_OPTIONS = {
6
7
  moveSpeed: 1,
7
8
  rotateSpeed: 1,
8
- deadzone: .1,
9
- axisMoveForward: GAMEPAD_AXIS.LeftY,
10
- axisMoveRight: GAMEPAD_AXIS.LeftX,
11
- axisLookX: GAMEPAD_AXIS.RightX,
12
- axisLookY: GAMEPAD_AXIS.RightY,
9
+ buttonDeadzone: .1,
10
+ moveStick: {
11
+ xAxis: GAMEPAD_AXIS.LeftX,
12
+ yAxis: GAMEPAD_AXIS.LeftY,
13
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
14
+ },
15
+ lookStick: {
16
+ xAxis: GAMEPAD_AXIS.RightX,
17
+ yAxis: GAMEPAD_AXIS.RightY,
18
+ pipeline: DEFAULT_GAMEPAD_STICK_PIPELINE
19
+ },
13
20
  buttonRollLeft: GAMEPAD_BUTTON.LeftShoulder,
14
21
  buttonRollRight: GAMEPAD_BUTTON.RightShoulder,
15
22
  buttonMoveUp: GAMEPAD_BUTTON.LeftTrigger,
@@ -36,7 +43,9 @@ var GamepadFlyControls = class extends GamepadControls {
36
43
  this.#controls = controls;
37
44
  this.#options = {
38
45
  ...DEFAULT_FLY_OPTIONS,
39
- ...options
46
+ ...options,
47
+ moveStick: resolveGamepadStickBinding(DEFAULT_FLY_OPTIONS.moveStick, options?.moveStick),
48
+ lookStick: resolveGamepadStickBinding(DEFAULT_FLY_OPTIONS.lookStick, options?.lookStick)
40
49
  };
41
50
  this.#tmpQuaternion = new Quaternion();
42
51
  }
@@ -46,20 +55,22 @@ var GamepadFlyControls = class extends GamepadControls {
46
55
  * @param deltaTime - Seconds since the last frame.
47
56
  */
48
57
  onUpdate(deltaTime) {
49
- const { moveSpeed, rotateSpeed, deadzone, axisMoveForward, axisMoveRight, axisLookX, axisLookY, buttonRollLeft, buttonRollRight, buttonMoveUp, buttonMoveDown } = this.#options;
58
+ const { moveSpeed, rotateSpeed, buttonDeadzone, moveStick, lookStick, buttonRollLeft, buttonRollRight, buttonMoveUp, buttonMoveDown } = this.#options;
50
59
  const input = this.gamepadInput;
60
+ const move = input.stick(moveStick.xAxis, moveStick.yAxis, moveStick.pipeline);
61
+ const look = input.stick(lookStick.xAxis, lookStick.yAxis, lookStick.pipeline);
51
62
  const moveMult = deltaTime * this.#controls.movementSpeed * moveSpeed;
52
- const fwd = input.axis(axisMoveForward, { deadzone });
63
+ const fwd = move.y;
53
64
  if (fwd !== 0) this.#controls.object.translateZ(fwd * moveMult);
54
- const strafe = input.axis(axisMoveRight, { deadzone });
65
+ const strafe = move.x;
55
66
  if (strafe !== 0) this.#controls.object.translateX(strafe * moveMult);
56
67
  const up = input.buttonValue(buttonMoveUp);
57
68
  const down = input.buttonValue(buttonMoveDown);
58
- if (up > deadzone) this.#controls.object.translateY(up * moveMult);
59
- if (down > deadzone) this.#controls.object.translateY(-down * moveMult);
69
+ if (up > buttonDeadzone) this.#controls.object.translateY(up * moveMult);
70
+ if (down > buttonDeadzone) this.#controls.object.translateY(-down * moveMult);
60
71
  const rotMult = deltaTime * this.#controls.rollSpeed * rotateSpeed;
61
- const pitch = -input.axis(axisLookY, { deadzone });
62
- const yaw = -input.axis(axisLookX, { deadzone });
72
+ const pitch = -look.y;
73
+ const yaw = -look.x;
63
74
  const roll = (input.isPressed(buttonRollLeft) ? 1 : 0) - (input.isPressed(buttonRollRight) ? 1 : 0);
64
75
  if (pitch !== 0 || yaw !== 0 || roll !== 0) {
65
76
  this.#tmpQuaternion.set(pitch * rotMult, yaw * rotMult, roll * rotMult, 1).normalize();
@@ -1,3 +1,4 @@
1
+ import { GamepadStick, GamepadStickPipeline } from "./gamepad-stick-processing.js";
1
2
  import { EventDispatcher } from "three";
2
3
  //#region src/gamepad-input.d.ts
3
4
  /**
@@ -23,29 +24,20 @@ type GamepadInputEventMap = {
23
24
  gamepad: Gamepad;
24
25
  };
25
26
  };
26
- /**
27
- * Dead zone processing mode for two-dimensional stick reads.
28
- */
29
- type GamepadDeadzoneMode = "axial" | "radial";
30
27
  /**
31
28
  * Configuration for {@link GamepadInput}.
32
29
  */
33
30
  type GamepadInputOptions = {
34
31
  /**
35
- * Default axis dead zone threshold in the range `[0, 1]`.
32
+ * Default dead zone threshold for {@link GamepadInput.axis} reads.
36
33
  * @default 0.1
37
34
  */
38
- deadzone: number;
35
+ axisDeadzone: number;
39
36
  /**
40
- * Default dead zone mode for stick reads.
41
- * @default "axial"
37
+ * Default stateless processing pipeline for stick reads.
38
+ * @default DEFAULT_GAMEPAD_STICK_PIPELINE
42
39
  */
43
- deadzoneMode: GamepadDeadzoneMode;
44
- /**
45
- * Whether stick reads rescale values outside the dead zone by default.
46
- * @default false
47
- */
48
- rescale: boolean;
40
+ stickPipeline: GamepadStickPipeline;
49
41
  /**
50
42
  * Browser-assigned gamepad slot to use.
51
43
  *
@@ -66,37 +58,6 @@ type GamepadAxisOptions = {
66
58
  */
67
59
  deadzone?: number;
68
60
  };
69
- /**
70
- * Options for two-dimensional stick reads.
71
- */
72
- type GamepadStickOptions = GamepadAxisOptions & {
73
- /**
74
- * Dead zone shape for this read.
75
- *
76
- * `"axial"` processes each axis independently, while `"radial"` processes
77
- * the stick magnitude.
78
- * @default "axial"
79
- */
80
- deadzoneMode?: GamepadDeadzoneMode;
81
- /**
82
- * Whether to remap values outside the dead zone to the full output range.
83
- * @default false
84
- */
85
- rescale?: boolean;
86
- };
87
- /**
88
- * Two-dimensional stick input after dead zone processing.
89
- */
90
- type GamepadStick = {
91
- /**
92
- * Horizontal stick value.
93
- */
94
- x: number;
95
- /**
96
- * Vertical stick value.
97
- */
98
- y: number;
99
- };
100
61
  /**
101
62
  * Gamepad input state reader for gameplay, menus, and custom actions.
102
63
  *
@@ -194,14 +155,14 @@ declare class GamepadInput extends EventDispatcher<GamepadInputEventMap> {
194
155
  */
195
156
  axis(axis: number, options?: GamepadAxisOptions): number;
196
157
  /**
197
- * Returns a two-axis stick after dead zone processing.
158
+ * Returns a two-axis stick after applying a stateless processing pipeline.
198
159
  *
199
160
  * @param xAxis - Horizontal axis index.
200
161
  * @param yAxis - Vertical axis index.
201
- * @param options - Optional per-read stick options.
162
+ * @param pipeline - Optional pipeline replacing the instance default.
202
163
  * @returns Object containing processed `x` and `y` values.
203
164
  */
204
- stick(xAxis: number, yAxis: number, options?: GamepadStickOptions): GamepadStick;
165
+ stick(xAxis: number, yAxis: number, pipeline?: GamepadStickPipeline): GamepadStick;
205
166
  /**
206
167
  * Plays an effect through the active gamepad's primary vibration actuator.
207
168
  *
@@ -225,4 +186,4 @@ declare class GamepadInput extends EventDispatcher<GamepadInputEventMap> {
225
186
  resetVibration(): Promise<GamepadHapticsResult | null>;
226
187
  }
227
188
  //#endregion
228
- export { GamepadAxisOptions, GamepadDeadzoneMode, GamepadInput, GamepadInputEventMap, GamepadInputOptions, GamepadStick, GamepadStickOptions };
189
+ export { GamepadAxisOptions, GamepadInput, GamepadInputEventMap, GamepadInputOptions };