@vibedgames/gamepad 0.1.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/dist/dom.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { VirtualGamepad } from "./core";
2
+ import type { StickState, VirtualGamepadOptions, VisibilityPolicy } from "./types";
3
+ export type DomGamepadRenderOptions = {
4
+ /** z-index of the overlay container (default 40 — above the canvas, below
5
+ * modal HUD). */
6
+ zIndex?: number;
7
+ /** Knob + button accent as a CSS color (default "#fff"). Change at runtime
8
+ * with `setTint`. */
9
+ tint?: string;
10
+ };
11
+ export type DomGamepadOptions = VirtualGamepadOptions & {
12
+ /** Element the overlay is appended to (default `document.body`). */
13
+ root?: HTMLElement;
14
+ /** When the overlay renders: "touch" after the first touch (default),
15
+ * "coarse" pre-shown on touch-capable devices so fixed buttons are
16
+ * discoverable, or "always". */
17
+ visible?: VisibilityPolicy;
18
+ /** Built-in DOM renderer config, or `false` to render it yourself from the
19
+ * exposed `pad`. */
20
+ render?: false | DomGamepadRenderOptions;
21
+ /** Fired the first time a touch is seen — e.g. swap a control hint. */
22
+ onFirstTouch?: () => void;
23
+ /**
24
+ * Touches whose event target matches are left to the page (they never reach
25
+ * the pad). Defaults to interactive elements — `button`, `a`, `input`,
26
+ * `select`, `textarea`, `[data-gamepad-ignore]` — so a rest button doesn't
27
+ * swallow taps on HUD controls.
28
+ */
29
+ ignore?: (target: EventTarget | null) => boolean;
30
+ };
31
+ export type DomGamepad = {
32
+ /** The underlying framework-agnostic controller. */
33
+ readonly pad: VirtualGamepad;
34
+ /** True once any touch has been seen this session (stays true). */
35
+ readonly isTouch: boolean;
36
+ getStick(): StickState;
37
+ isButtonDown(id: string): boolean;
38
+ /** Edge-triggered press since last `update()` (tap = one action). */
39
+ justPressed(id: string): boolean;
40
+ justReleased(id: string): boolean;
41
+ /** Recolor the knob + buttons (CSS color). */
42
+ setTint(color: string): void;
43
+ /** Call once per frame from your game loop: publishes press edges and
44
+ * redraws the overlay. */
45
+ update(): void;
46
+ destroy(): void;
47
+ };
48
+ /**
49
+ * Wire a {@link VirtualGamepad} to a plain DOM page — the adapter for
50
+ * non-Phaser games (Three.js, canvas, anything). Listens for touch pointer
51
+ * events on `window` (mouse is ignored so desktop keeps its own controls) and
52
+ * renders the floating stick + fixed buttons as positioned DOM elements in a
53
+ * `pointer-events:none` overlay, so it never steals taps from the page.
54
+ *
55
+ * The page is responsible for `touch-action: none` on its game surface —
56
+ * without it the browser may scroll/zoom instead of delivering moves.
57
+ *
58
+ * ```ts
59
+ * const gamepad = attachDomGamepad({
60
+ * visible: "coarse",
61
+ * buttons: [{ id: "jump", label: "JUMP", position: (v) => ({
62
+ * x: v.width - 72 - v.inset.right, y: v.height - 72 - v.inset.bottom,
63
+ * }) }],
64
+ * });
65
+ * // in your game loop:
66
+ * gamepad.update();
67
+ * const stick = gamepad.getStick();
68
+ * if (gamepad.justPressed("jump")) jump();
69
+ * ```
70
+ */
71
+ export declare function attachDomGamepad(options?: DomGamepadOptions): DomGamepad;
72
+ export { VirtualGamepad, stickDirection4, stickDirection8 } from "./core";
73
+ export { PhysicalGamepad, isPadConnected } from "./physical";
74
+ export { safeAreaInset } from "./safe-area";
75
+ export type { PadButton, PhysicalGamepadOptions } from "./physical";
76
+ export type { ButtonLayout, ButtonOptions, Dir4, Dir8, Inset, StickGeometry, StickOptions, StickState, Vec2, VirtualGamepadOptions, Viewport, VisibilityPolicy, } from "./types";
77
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEnF,MAAM,MAAM,uBAAuB,GAAG;IACpC;sBACkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;0BACsB;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG;IACtD,oEAAoE;IACpE,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;qCAEiC;IACjC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B;yBACqB;IACrB,MAAM,CAAC,EAAE,KAAK,GAAG,uBAAuB,CAAC;IACzC,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,oDAAoD;IACpD,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,IAAI,UAAU,CAAC;IACvB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,8CAA8C;IAC9C,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;+BAC2B;IAC3B,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAYF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CA2E5E;AA4GD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,qBAAqB,EACrB,QAAQ,EACR,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
package/dist/dom.js ADDED
@@ -0,0 +1,202 @@
1
+ import { VirtualGamepad } from "./core";
2
+ import { preShow, safeAreaInset } from "./safe-area";
3
+ const defaultIgnore = (target) => target instanceof Element &&
4
+ target.closest("button, a, input, select, textarea, [data-gamepad-ignore]") !== null;
5
+ const placement = (x, y, r) => ({
6
+ transform: `translate(${x - r}px, ${y - r}px)`,
7
+ width: `${r * 2}px`,
8
+ height: `${r * 2}px`,
9
+ });
10
+ /**
11
+ * Wire a {@link VirtualGamepad} to a plain DOM page — the adapter for
12
+ * non-Phaser games (Three.js, canvas, anything). Listens for touch pointer
13
+ * events on `window` (mouse is ignored so desktop keeps its own controls) and
14
+ * renders the floating stick + fixed buttons as positioned DOM elements in a
15
+ * `pointer-events:none` overlay, so it never steals taps from the page.
16
+ *
17
+ * The page is responsible for `touch-action: none` on its game surface —
18
+ * without it the browser may scroll/zoom instead of delivering moves.
19
+ *
20
+ * ```ts
21
+ * const gamepad = attachDomGamepad({
22
+ * visible: "coarse",
23
+ * buttons: [{ id: "jump", label: "JUMP", position: (v) => ({
24
+ * x: v.width - 72 - v.inset.right, y: v.height - 72 - v.inset.bottom,
25
+ * }) }],
26
+ * });
27
+ * // in your game loop:
28
+ * gamepad.update();
29
+ * const stick = gamepad.getStick();
30
+ * if (gamepad.justPressed("jump")) jump();
31
+ * ```
32
+ */
33
+ export function attachDomGamepad(options = {}) {
34
+ const pad = new VirtualGamepad(options);
35
+ const ignore = options.ignore ?? defaultIgnore;
36
+ const policy = options.visible ?? "touch";
37
+ const renderOpts = options.render === false ? null : (options.render ?? {});
38
+ let isTouch = false;
39
+ let tint = renderOpts?.tint ?? "#fff";
40
+ let destroyed = false;
41
+ const live = new Set();
42
+ const syncViewport = () => pad.setViewport(window.innerWidth, window.innerHeight, safeAreaInset());
43
+ syncViewport();
44
+ const onDown = (e) => {
45
+ if (e.pointerType !== "touch")
46
+ return; // mouse stays on the game's own controls
47
+ if (!isTouch) {
48
+ isTouch = true;
49
+ options.onFirstTouch?.();
50
+ }
51
+ if (ignore(e.target))
52
+ return;
53
+ live.add(e.pointerId);
54
+ pad.pointerDown(e.pointerId, e.clientX, e.clientY);
55
+ };
56
+ const onMove = (e) => {
57
+ if (e.pointerType === "touch")
58
+ pad.pointerMove(e.pointerId, e.clientX, e.clientY);
59
+ };
60
+ const onUp = (e) => {
61
+ live.delete(e.pointerId);
62
+ pad.pointerUp(e.pointerId);
63
+ };
64
+ const onGone = () => {
65
+ live.clear();
66
+ pad.reset();
67
+ };
68
+ window.addEventListener("pointerdown", onDown);
69
+ window.addEventListener("pointermove", onMove);
70
+ window.addEventListener("pointerup", onUp);
71
+ window.addEventListener("pointercancel", onUp);
72
+ window.addEventListener("blur", onGone);
73
+ window.addEventListener("resize", syncViewport);
74
+ const view = renderOpts ? createOverlay(options.root ?? document.body, renderOpts) : null;
75
+ return {
76
+ pad,
77
+ get isTouch() {
78
+ return isTouch;
79
+ },
80
+ getStick: () => pad.getStick(),
81
+ isButtonDown: (id) => pad.isButtonDown(id),
82
+ justPressed: (id) => pad.justPressed(id),
83
+ justReleased: (id) => pad.justReleased(id),
84
+ setTint(color) {
85
+ tint = color;
86
+ },
87
+ update() {
88
+ pad.reconcile(live);
89
+ pad.nextFrame();
90
+ view?.draw(pad, tint, isTouch || preShow(policy));
91
+ },
92
+ destroy() {
93
+ if (destroyed)
94
+ return;
95
+ destroyed = true;
96
+ window.removeEventListener("pointerdown", onDown);
97
+ window.removeEventListener("pointermove", onMove);
98
+ window.removeEventListener("pointerup", onUp);
99
+ window.removeEventListener("pointercancel", onUp);
100
+ window.removeEventListener("blur", onGone);
101
+ window.removeEventListener("resize", syncViewport);
102
+ view?.root.remove();
103
+ },
104
+ };
105
+ }
106
+ // All chrome is drawn with white at fixed alphas; only the accent (knob fill,
107
+ // button border) takes the tint — mirrors the Phaser renderer's look.
108
+ function createOverlay(parent, opts) {
109
+ const root = document.createElement("div");
110
+ root.className = "vg-gamepad";
111
+ root.style.cssText =
112
+ `position:fixed;inset:0;pointer-events:none;z-index:${opts.zIndex ?? 40};` +
113
+ "font-family:system-ui,sans-serif;user-select:none;-webkit-user-select:none;";
114
+ parent.appendChild(root);
115
+ // Nodes are positioned with transform (top-left stays 0,0) and every style
116
+ // write is diffed through this cache, so an idle overlay costs zero style
117
+ // recalcs and a moving knob is compositor-only.
118
+ const applied = new Map();
119
+ const set = (node, styles) => {
120
+ let prev = applied.get(node);
121
+ if (!prev) {
122
+ prev = {};
123
+ applied.set(node, prev);
124
+ }
125
+ for (const [key, value] of Object.entries(styles)) {
126
+ if (prev[key] === value)
127
+ continue;
128
+ prev[key] = value;
129
+ if (key === "text")
130
+ node.textContent = value;
131
+ else
132
+ node.style.setProperty(key, value);
133
+ }
134
+ };
135
+ const el = (cls, css) => {
136
+ const d = document.createElement("div");
137
+ d.className = cls;
138
+ d.style.cssText = css;
139
+ root.appendChild(d);
140
+ return d;
141
+ };
142
+ const circle = "position:absolute;left:0;top:0;border-radius:50%;box-sizing:border-box;display:none;will-change:transform;";
143
+ const base = el("vg-stick-base", `${circle}background:rgba(255,255,255,.05);border:2px solid rgba(255,255,255,.2);`);
144
+ const knob = el("vg-stick-knob", `${circle}border:2px solid;`);
145
+ const buttons = new Map();
146
+ return {
147
+ root,
148
+ draw(pad, tint, show) {
149
+ const stick = pad.getStick();
150
+ const geom = pad.getStickGeometry();
151
+ const stickOn = show && stick.active && geom !== null;
152
+ set(base, { display: stickOn ? "block" : "none" });
153
+ set(knob, { display: stickOn ? "block" : "none" });
154
+ if (stickOn && geom) {
155
+ // Puck clamps to the ring edge so it never escapes the base.
156
+ const clamped = Math.min(stick.distance, geom.radius);
157
+ const kx = stick.distance > 0.001
158
+ ? stick.anchorX + (stick.dx / stick.distance) * clamped
159
+ : stick.anchorX;
160
+ const ky = stick.distance > 0.001
161
+ ? stick.anchorY + (stick.dy / stick.distance) * clamped
162
+ : stick.anchorY;
163
+ set(base, placement(stick.anchorX, stick.anchorY, geom.radius));
164
+ set(knob, {
165
+ ...placement(kx, ky, geom.knobRadius),
166
+ "border-color": tint,
167
+ background: `color-mix(in srgb, ${tint} 25%, transparent)`,
168
+ });
169
+ }
170
+ for (const b of pad.getButtonLayout()) {
171
+ if (b.rest)
172
+ continue; // rest buttons have no on-screen position
173
+ let node = buttons.get(b.id);
174
+ if (!node) {
175
+ node = el("vg-btn", `${circle}border:2px solid;align-items:center;justify-content:center;` +
176
+ "font-weight:600;letter-spacing:.04em;color:rgba(255,255,255,.92);");
177
+ node.dataset["id"] = b.id;
178
+ buttons.set(b.id, node);
179
+ }
180
+ if (!show) {
181
+ set(node, { display: "none" });
182
+ continue;
183
+ }
184
+ set(node, {
185
+ ...placement(b.x, b.y, b.radius),
186
+ display: "flex",
187
+ text: b.label ?? "",
188
+ "font-size": `${Math.round(b.radius * 0.42)}px`,
189
+ "border-color": tint,
190
+ opacity: b.pressed ? "1" : "0.55",
191
+ background: b.pressed
192
+ ? `color-mix(in srgb, ${tint} 34%, transparent)`
193
+ : "rgba(255,255,255,.08)",
194
+ });
195
+ }
196
+ },
197
+ };
198
+ }
199
+ export { VirtualGamepad, stickDirection4, stickDirection8 } from "./core";
200
+ export { PhysicalGamepad, isPadConnected } from "./physical";
201
+ export { safeAreaInset } from "./safe-area";
202
+ //# sourceMappingURL=dom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { VirtualGamepad, stickDirection4, stickDirection8 } from "./core";
2
+ export { PhysicalGamepad, isPadConnected } from "./physical";
3
+ export { safeAreaInset } from "./safe-area";
4
+ export type { PadButton, PhysicalGamepadOptions } from "./physical";
5
+ export type { ButtonLayout, ButtonOptions, Dir4, Dir8, Inset, StickGeometry, StickOptions, StickState, Vec2, VirtualGamepadOptions, Viewport, VisibilityPolicy, } from "./types";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,qBAAqB,EACrB,QAAQ,EACR,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { VirtualGamepad, stickDirection4, stickDirection8 } from "./core";
2
+ export { PhysicalGamepad, isPadConnected } from "./physical";
3
+ export { safeAreaInset } from "./safe-area";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,70 @@
1
+ import Phaser from "phaser";
2
+ import { VirtualGamepad } from "./core";
3
+ import type { StickState, VirtualGamepadOptions, VisibilityPolicy } from "./types";
4
+ export type PhaserGamepadRenderOptions = {
5
+ /** Render depth of the overlay Graphics (default 95 — above the world,
6
+ * below a DOM HUD). */
7
+ depth?: number;
8
+ /** Knob + button color (default white). Change at runtime with `setTint`. */
9
+ tint?: number;
10
+ /** Blend mode for the Graphics (default `Phaser.BlendModes.ADD` so it glows
11
+ * over a dark scene). Button labels always render NORMAL for legibility. */
12
+ blendMode?: Phaser.BlendModes;
13
+ };
14
+ export type PhaserGamepadOptions = VirtualGamepadOptions & {
15
+ /** Extra simultaneous pointers to register. Defaults to one per button plus
16
+ * two for the stick + a spare, so simultaneous holds are never dropped. */
17
+ extraPointers?: number;
18
+ /** When the overlay renders: "touch" after the first touch (default),
19
+ * "coarse" pre-shown on touch-capable devices so fixed buttons are
20
+ * discoverable, or "always". */
21
+ visible?: VisibilityPolicy;
22
+ /** Built-in Graphics renderer config, or `false` to render it yourself from
23
+ * the exposed `pad`. */
24
+ render?: false | PhaserGamepadRenderOptions;
25
+ /** Fired the first time a touch is seen — e.g. swap an on-screen control
26
+ * hint, or hide a desktop cursor prompt. */
27
+ onFirstTouch?: () => void;
28
+ };
29
+ export type PhaserGamepad = {
30
+ /** The underlying framework-agnostic controller. */
31
+ readonly pad: VirtualGamepad;
32
+ /** True once any touch has been seen this session (stays true). */
33
+ readonly isTouch: boolean;
34
+ /** Current stick reading. */
35
+ getStick(): StickState;
36
+ isButtonDown(id: string): boolean;
37
+ /** Edge-triggered press since last `update()` (tap = one action). */
38
+ justPressed(id: string): boolean;
39
+ justReleased(id: string): boolean;
40
+ /** Recolor the knob + buttons (e.g. to the local player's color). */
41
+ setTint(color: number): void;
42
+ /** Call once per frame from your scene's `update()`: reconciles stale
43
+ * pointers, publishes press edges, and redraws the overlay. */
44
+ update(): void;
45
+ destroy(): void;
46
+ };
47
+ /**
48
+ * Wire a {@link VirtualGamepad} to a Phaser scene: registers extra pointers,
49
+ * forwards touch events (mouse is ignored so a desktop game keeps its own
50
+ * cursor controls), and draws a screen-fixed joystick + buttons overlay.
51
+ *
52
+ * ```ts
53
+ * const gamepad = attachVirtualGamepad(this, {
54
+ * buttons: [{ id: "fire" }], // a "rest" button: any 2nd finger fires
55
+ * onFirstTouch: () => hideHint(),
56
+ * });
57
+ * // in update():
58
+ * gamepad.update();
59
+ * const stick = gamepad.getStick();
60
+ * if (stick.active && !stick.inDeadZone) steer(stick.angle, stick.magnitude);
61
+ * if (gamepad.isButtonDown("fire")) shoot();
62
+ * ```
63
+ */
64
+ export declare function attachVirtualGamepad(scene: Phaser.Scene, options?: PhaserGamepadOptions): PhaserGamepad;
65
+ export { VirtualGamepad, stickDirection4, stickDirection8 } from "./core";
66
+ export { PhysicalGamepad, isPadConnected } from "./physical";
67
+ export { safeAreaInset } from "./safe-area";
68
+ export type { PadButton, PhysicalGamepadOptions } from "./physical";
69
+ export type { ButtonLayout, ButtonOptions, Dir4, Dir8, Inset, StickGeometry, StickOptions, StickState, Vec2, VirtualGamepadOptions, Viewport, VisibilityPolicy, } from "./types";
70
+ //# sourceMappingURL=phaser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phaser.d.ts","sourceRoot":"","sources":["../src/phaser.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEnF,MAAM,MAAM,0BAA0B,GAAG;IACvC;4BACwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;iFAC6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,GAAG;IACzD;gFAC4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;qCAEiC;IACjC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B;6BACyB;IACzB,MAAM,CAAC,EAAE,KAAK,GAAG,0BAA0B,CAAC;IAC5C;iDAC6C;IAC7C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,6BAA6B;IAC7B,QAAQ,IAAI,UAAU,CAAC;IACvB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;oEACgE;IAChE,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,KAAK,EACnB,OAAO,GAAE,oBAAyB,GACjC,aAAa,CA2Ff;AA4DD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,qBAAqB,EACrB,QAAQ,EACR,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
package/dist/phaser.js ADDED
@@ -0,0 +1,169 @@
1
+ import Phaser from "phaser";
2
+ import { VirtualGamepad } from "./core";
3
+ import { preShow, safeAreaInset } from "./safe-area";
4
+ /**
5
+ * Wire a {@link VirtualGamepad} to a Phaser scene: registers extra pointers,
6
+ * forwards touch events (mouse is ignored so a desktop game keeps its own
7
+ * cursor controls), and draws a screen-fixed joystick + buttons overlay.
8
+ *
9
+ * ```ts
10
+ * const gamepad = attachVirtualGamepad(this, {
11
+ * buttons: [{ id: "fire" }], // a "rest" button: any 2nd finger fires
12
+ * onFirstTouch: () => hideHint(),
13
+ * });
14
+ * // in update():
15
+ * gamepad.update();
16
+ * const stick = gamepad.getStick();
17
+ * if (stick.active && !stick.inDeadZone) steer(stick.angle, stick.magnitude);
18
+ * if (gamepad.isButtonDown("fire")) shoot();
19
+ * ```
20
+ */
21
+ export function attachVirtualGamepad(scene, options = {}) {
22
+ const pad = new VirtualGamepad(options);
23
+ let isTouch = false;
24
+ const policy = options.visible ?? "touch";
25
+ const renderOpts = options.render === false ? null : (options.render ?? {});
26
+ let tint = renderOpts?.tint ?? 0xffffff;
27
+ scene.input.addPointer(options.extraPointers ?? Math.max(2, (options.buttons?.length ?? 0) + 2));
28
+ const syncViewport = () => {
29
+ const { width, height } = scene.scale;
30
+ // Phaser's logical size can differ from CSS pixels (FIT/zoom modes) —
31
+ // scale the CSS-px insets into scene units so anchored buttons don't
32
+ // overshoot on notched phones.
33
+ const raw = safeAreaInset();
34
+ const sx = window.innerWidth > 0 ? width / window.innerWidth : 1;
35
+ const sy = window.innerHeight > 0 ? height / window.innerHeight : 1;
36
+ pad.setViewport(width, height, {
37
+ top: raw.top * sy,
38
+ right: raw.right * sx,
39
+ bottom: raw.bottom * sy,
40
+ left: raw.left * sx,
41
+ });
42
+ };
43
+ syncViewport();
44
+ const onDown = (p) => {
45
+ if (!p.wasTouch)
46
+ return; // mouse stays on the game's own control model
47
+ if (!isTouch) {
48
+ isTouch = true;
49
+ options.onFirstTouch?.();
50
+ }
51
+ syncViewport();
52
+ pad.pointerDown(p.id, p.x, p.y);
53
+ };
54
+ const onMove = (p) => {
55
+ if (p.wasTouch)
56
+ pad.pointerMove(p.id, p.x, p.y);
57
+ };
58
+ const onUp = (p) => pad.pointerUp(p.id);
59
+ scene.input.on(Phaser.Input.Events.POINTER_DOWN, onDown);
60
+ scene.input.on(Phaser.Input.Events.POINTER_MOVE, onMove);
61
+ scene.input.on(Phaser.Input.Events.POINTER_UP, onUp);
62
+ let gfx = null;
63
+ const labels = new Map();
64
+ if (renderOpts) {
65
+ gfx = scene.add
66
+ .graphics()
67
+ .setScrollFactor(0)
68
+ .setDepth(renderOpts.depth ?? 95)
69
+ .setBlendMode(renderOpts.blendMode ?? Phaser.BlendModes.ADD);
70
+ }
71
+ const draw = () => {
72
+ if (!gfx)
73
+ return;
74
+ gfx.clear();
75
+ const show = isTouch || preShow(policy);
76
+ if (show)
77
+ drawGamepad(gfx, pad, tint);
78
+ syncLabels(scene, gfx, pad, labels, show);
79
+ };
80
+ return {
81
+ pad,
82
+ get isTouch() {
83
+ return isTouch;
84
+ },
85
+ getStick: () => pad.getStick(),
86
+ isButtonDown: (id) => pad.isButtonDown(id),
87
+ justPressed: (id) => pad.justPressed(id),
88
+ justReleased: (id) => pad.justReleased(id),
89
+ setTint: (color) => {
90
+ tint = color;
91
+ },
92
+ update() {
93
+ const live = [];
94
+ for (const ptr of scene.input.manager.pointers)
95
+ if (ptr.isDown)
96
+ live.push(ptr.id);
97
+ pad.reconcile(live);
98
+ pad.nextFrame();
99
+ syncViewport();
100
+ draw();
101
+ },
102
+ destroy() {
103
+ scene.input.off(Phaser.Input.Events.POINTER_DOWN, onDown);
104
+ scene.input.off(Phaser.Input.Events.POINTER_MOVE, onMove);
105
+ scene.input.off(Phaser.Input.Events.POINTER_UP, onUp);
106
+ gfx?.destroy();
107
+ for (const t of labels.values())
108
+ t.destroy();
109
+ labels.clear();
110
+ },
111
+ };
112
+ }
113
+ /** Keep a Text object per labeled fixed button (Graphics can't draw text). */
114
+ function syncLabels(scene, gfx, pad, labels, show) {
115
+ for (const b of pad.getButtonLayout()) {
116
+ if (b.rest || !b.label)
117
+ continue;
118
+ let text = labels.get(b.id);
119
+ if (!text) {
120
+ text = scene.add
121
+ .text(0, 0, b.label, {
122
+ fontFamily: "system-ui, sans-serif",
123
+ fontStyle: "600",
124
+ color: "#ffffff",
125
+ })
126
+ .setOrigin(0.5)
127
+ .setScrollFactor(0)
128
+ .setDepth(gfx.depth + 1);
129
+ labels.set(b.id, text);
130
+ }
131
+ text.setVisible(show);
132
+ if (!show)
133
+ continue;
134
+ text.setPosition(b.x, b.y);
135
+ text.setFontSize(Math.round(b.radius * 0.42));
136
+ text.setAlpha(b.pressed ? 1 : 0.75);
137
+ }
138
+ }
139
+ /** Draw the joystick + fixed buttons into a Graphics object (screen-space). */
140
+ function drawGamepad(g, pad, tint) {
141
+ const geom = pad.getStickGeometry();
142
+ const stick = pad.getStick();
143
+ if (geom && stick.active) {
144
+ const { radius, deadZone, knobRadius } = geom;
145
+ const ax = stick.anchorX;
146
+ const ay = stick.anchorY;
147
+ // Puck clamps to the ring edge so it never escapes the base.
148
+ const clamped = Math.min(stick.distance, radius);
149
+ const kx = stick.distance > 0.001 ? ax + (stick.dx / stick.distance) * clamped : ax;
150
+ const ky = stick.distance > 0.001 ? ay + (stick.dy / stick.distance) * clamped : ay;
151
+ g.fillStyle(0xffffff, 0.05).fillCircle(ax, ay, radius);
152
+ g.lineStyle(2, 0xffffff, 0.2).strokeCircle(ax, ay, radius);
153
+ g.fillStyle(0xffffff, 0.12).fillCircle(ax, ay, deadZone);
154
+ g.fillStyle(tint, 0.22).fillCircle(kx, ky, knobRadius);
155
+ g.lineStyle(2, tint, 0.85).strokeCircle(kx, ky, knobRadius);
156
+ }
157
+ for (const b of pad.getButtonLayout()) {
158
+ if (b.rest)
159
+ continue; // rest buttons have no on-screen position
160
+ g.fillStyle(tint, b.pressed ? 0.34 : 0.12);
161
+ g.fillCircle(b.x, b.y, b.radius);
162
+ g.lineStyle(2, tint, b.pressed ? 0.95 : 0.45);
163
+ g.strokeCircle(b.x, b.y, b.radius);
164
+ }
165
+ }
166
+ export { VirtualGamepad, stickDirection4, stickDirection8 } from "./core";
167
+ export { PhysicalGamepad, isPadConnected } from "./physical";
168
+ export { safeAreaInset } from "./safe-area";
169
+ //# sourceMappingURL=phaser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phaser.js","sourceRoot":"","sources":["../src/phaser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { StickState } from "./types";
2
+ /** Standard-mapping button names (https://w3c.github.io/gamepad/#remapping).
3
+ * Non-standard pads are read with the same indices — best effort. */
4
+ export type PadButton = "a" | "b" | "x" | "y" | "lb" | "rb" | "lt" | "rt" | "select" | "start" | "ls" | "rs" | "up" | "down" | "left" | "right";
5
+ export type PhysicalGamepadOptions = {
6
+ /**
7
+ * Game action id → physical buttons, so a game reads the SAME ids from this
8
+ * pad as from its {@link VirtualGamepad} buttons (e.g. `"jump"`, `"fire"`).
9
+ * Raw {@link PadButton} names always work without a binding.
10
+ */
11
+ bindings?: Readonly<Record<string, readonly PadButton[]>>;
12
+ /** Stick deflection (0–1) treated as noise. Default 0.15. */
13
+ stickDeadZone?: number;
14
+ /** Analog trigger value above which lt/rt count as pressed. Default 0.05. */
15
+ triggerThreshold?: number;
16
+ /** Fired when the first pad appears (polled — fires on the next update()). */
17
+ onConnect?: () => void;
18
+ /** Fired when the last pad disappears. */
19
+ onDisconnect?: () => void;
20
+ /** Pad source, injectable for tests. Default `navigator.getGamepads()`. */
21
+ poll?: () => ReadonlyArray<Gamepad | null>;
22
+ };
23
+ /** True if any physical pad is currently connected — e.g. to decide whether
24
+ * controller rows belong on an instructions screen. */
25
+ export declare function isPadConnected(): boolean;
26
+ /**
27
+ * Physical controller input, API-mirroring {@link VirtualGamepad} so a game's
28
+ * read path works for both: `isButtonDown(id)` / `justPressed(id)` /
29
+ * `getStick()`. Poll-based — call `update()` once per frame; edges are
30
+ * published per update, matching the virtual pad's `nextFrame()` semantics.
31
+ * The first connected pad wins. No DOM, no engine, no listeners.
32
+ *
33
+ * ```ts
34
+ * const pad = new PhysicalGamepad({ bindings: { jump: ["a"], dash: ["b", "rb"] } });
35
+ * // in your game loop:
36
+ * pad.update();
37
+ * const dir = stickDirection4(pad.getStick());
38
+ * if (pad.justPressed("jump")) jump();
39
+ * ```
40
+ */
41
+ export declare class PhysicalGamepad {
42
+ private readonly bindings;
43
+ private readonly stickDeadZone;
44
+ private readonly triggerThreshold;
45
+ private readonly onConnect?;
46
+ private readonly onDisconnect?;
47
+ private readonly poll;
48
+ private wasConnected;
49
+ private axes;
50
+ private down;
51
+ private prevDown;
52
+ /** Raw analog values (triggers), by button name. */
53
+ private values;
54
+ constructor(options?: PhysicalGamepadOptions);
55
+ /** Poll the first connected pad and publish press edges for this frame. */
56
+ update(): void;
57
+ /** A pad was connected as of the last `update()`. */
58
+ get connected(): boolean;
59
+ /** `id` is a binding action id or a raw {@link PadButton} name. */
60
+ isButtonDown(id: string): boolean;
61
+ /** Action went up→down since the previous `update()` (any bound button). */
62
+ justPressed(id: string): boolean;
63
+ /** Action went down→up since the previous `update()` (all bound buttons up). */
64
+ justReleased(id: string): boolean;
65
+ /** Raw analog value 0–1 (triggers are the interesting case). 0 when absent. */
66
+ buttonValue(button: PadButton): number;
67
+ /**
68
+ * A stick reading in {@link StickState} form (axes 0/1 left, 2/3 right;
69
+ * y-down matches screen space), so `stickDirection4/8` work unchanged.
70
+ * `active` while a pad is connected; dead-zoned like the virtual stick.
71
+ */
72
+ getStick(side?: "left" | "right"): StickState;
73
+ /** Release all state (the class holds no listeners — this just clears). */
74
+ destroy(): void;
75
+ private resolve;
76
+ }
77
+ //# sourceMappingURL=physical.d.ts.map