@simpludify/web 0.1.3 → 0.1.4
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 +5 -3
- package/dist/game.d.ts +2 -0
- package/dist/gamepad-bindings.d.ts +100 -0
- package/dist/gamepad-layout.d.ts +18 -0
- package/dist/gamepads.d.ts +79 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +433 -6
- package/dist/index.js.map +1 -1
- package/dist/style.css +3 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./style.css";
|
|
2
|
-
import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, FixedStepRunner, GeometryTilemapLayer, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, cleanupAll, containsScreenPoint, createViewTransform, imageSize, intersectUI, isKeyCode, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
|
|
2
|
+
import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, EventBus, FixedStepRunner, GeometryTilemapLayer, Lifetime, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, cleanupAll, containsScreenPoint, createViewTransform, defineEvent, imageSize, intersectUI, isKeyCode, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
|
|
3
3
|
import { Application, CanvasTextMetrics, Container, Graphics, Rectangle, RendererType, Sprite, Text, TextStyle, Texture } from "pixi.js";
|
|
4
4
|
export * from "@simpludify/core";
|
|
5
5
|
// packages/web/src/canvas-node.ts
|
|
@@ -17,6 +17,415 @@ function CanvasButton(text, click, style = {}) {
|
|
|
17
17
|
return click(signal);
|
|
18
18
|
}, style);
|
|
19
19
|
}
|
|
20
|
+
// packages/web/src/gamepad-bindings.ts
|
|
21
|
+
/** Standard mapping physical positions, not product-specific printed labels. */
|
|
22
|
+
var GamepadButton = Object.freeze({
|
|
23
|
+
South: 0,
|
|
24
|
+
East: 1,
|
|
25
|
+
West: 2,
|
|
26
|
+
North: 3,
|
|
27
|
+
LeftShoulder: 4,
|
|
28
|
+
RightShoulder: 5,
|
|
29
|
+
LeftTrigger: 6,
|
|
30
|
+
RightTrigger: 7,
|
|
31
|
+
Select: 8,
|
|
32
|
+
Start: 9,
|
|
33
|
+
LeftStick: 10,
|
|
34
|
+
RightStick: 11,
|
|
35
|
+
DPadUp: 12,
|
|
36
|
+
DPadDown: 13,
|
|
37
|
+
DPadLeft: 14,
|
|
38
|
+
DPadRight: 15,
|
|
39
|
+
Home: 16
|
|
40
|
+
});
|
|
41
|
+
var GamepadAxis = Object.freeze({
|
|
42
|
+
LeftX: 0,
|
|
43
|
+
LeftY: 1,
|
|
44
|
+
RightX: 2,
|
|
45
|
+
RightY: 3
|
|
46
|
+
});
|
|
47
|
+
var axis = (index) => Object.freeze({ axis: index });
|
|
48
|
+
/** Ready-to-use data; game Actions retain application-defined names. */
|
|
49
|
+
var GamepadBindings = Object.freeze({
|
|
50
|
+
LeftStick: Object.freeze({
|
|
51
|
+
x: axis(0),
|
|
52
|
+
y: axis(1)
|
|
53
|
+
}),
|
|
54
|
+
RightStick: Object.freeze({
|
|
55
|
+
x: axis(2),
|
|
56
|
+
y: axis(3)
|
|
57
|
+
}),
|
|
58
|
+
DPad: Object.freeze({
|
|
59
|
+
x: Object.freeze({
|
|
60
|
+
negative: Object.freeze([14]),
|
|
61
|
+
positive: Object.freeze([15])
|
|
62
|
+
}),
|
|
63
|
+
y: Object.freeze({
|
|
64
|
+
negative: Object.freeze([12]),
|
|
65
|
+
positive: Object.freeze([13])
|
|
66
|
+
})
|
|
67
|
+
}),
|
|
68
|
+
Move: Object.freeze({
|
|
69
|
+
x: Object.freeze({
|
|
70
|
+
axis: 0,
|
|
71
|
+
negative: Object.freeze([14]),
|
|
72
|
+
positive: Object.freeze([15])
|
|
73
|
+
}),
|
|
74
|
+
y: Object.freeze({
|
|
75
|
+
axis: 1,
|
|
76
|
+
negative: Object.freeze([12]),
|
|
77
|
+
positive: Object.freeze([13])
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
var fail = () => {
|
|
82
|
+
throw new EngineError("INVALID_GAMEPAD_BINDING", "Invalid gamepad binding");
|
|
83
|
+
};
|
|
84
|
+
function gamepadIndex(value) {
|
|
85
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return fail();
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function gamepadDeadZone(value) {
|
|
89
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value >= 1) return fail();
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function record(value, keys) {
|
|
93
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !keys.includes(k))) return fail();
|
|
94
|
+
return Object.fromEntries(Object.entries(value));
|
|
95
|
+
}
|
|
96
|
+
function axisBinding(value) {
|
|
97
|
+
const v = record(value, [
|
|
98
|
+
"axis",
|
|
99
|
+
"invert",
|
|
100
|
+
"negative",
|
|
101
|
+
"positive",
|
|
102
|
+
"deadZone"
|
|
103
|
+
]);
|
|
104
|
+
const indices = (value) => {
|
|
105
|
+
if (value === void 0) return Object.freeze([]);
|
|
106
|
+
if (!Array.isArray(value)) return fail();
|
|
107
|
+
return Object.freeze([...new Set(value.map(gamepadIndex))]);
|
|
108
|
+
};
|
|
109
|
+
if (v.invert !== void 0 && typeof v.invert !== "boolean") return fail();
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
...v.axis === void 0 ? {} : { axis: gamepadIndex(v.axis) },
|
|
112
|
+
invert: v.invert ?? false,
|
|
113
|
+
negative: indices(v.negative),
|
|
114
|
+
positive: indices(v.positive),
|
|
115
|
+
...v.deadZone === void 0 ? {} : { deadZone: gamepadDeadZone(v.deadZone) }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function checkedGamepadBindings(actions, value) {
|
|
119
|
+
const v = record(value, Object.keys(actions));
|
|
120
|
+
return Object.freeze(Object.entries(v).map(([name, binding]) => {
|
|
121
|
+
if (!Object.hasOwn(actions, name)) return fail();
|
|
122
|
+
const action = actions[name];
|
|
123
|
+
if (!action) return fail();
|
|
124
|
+
let result;
|
|
125
|
+
if (action.kind === "button") {
|
|
126
|
+
const b = record(binding, ["buttons"]);
|
|
127
|
+
if (b.buttons !== void 0 && !Array.isArray(b.buttons)) return fail();
|
|
128
|
+
result = Object.freeze({ buttons: Object.freeze((b.buttons ?? []).map((item) => {
|
|
129
|
+
if (typeof item === "number") return gamepadIndex(item);
|
|
130
|
+
const b = record(item, ["button", "threshold"]), threshold = b.threshold ?? .5;
|
|
131
|
+
if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold <= 0 || threshold > 1) return fail();
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
button: gamepadIndex(b.button),
|
|
134
|
+
threshold
|
|
135
|
+
});
|
|
136
|
+
})) });
|
|
137
|
+
} else if (action.kind === "axis1d") result = axisBinding(binding);
|
|
138
|
+
else {
|
|
139
|
+
const keys = action.kind === "axis2d" ? ["x", "y"] : [
|
|
140
|
+
"x",
|
|
141
|
+
"y",
|
|
142
|
+
"z"
|
|
143
|
+
], b = record(binding, keys);
|
|
144
|
+
result = Object.freeze(Object.fromEntries(keys.map((k) => [k, axisBinding(b[k] ?? {})])));
|
|
145
|
+
}
|
|
146
|
+
return Object.freeze([action, result]);
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
// packages/web/src/gamepad-layout.ts
|
|
150
|
+
var positions = Object.fromEntries(Object.entries(GamepadButton).map(([name, index]) => [index, name]));
|
|
151
|
+
var faces = Object.freeze({
|
|
152
|
+
generic: Object.freeze([
|
|
153
|
+
"South",
|
|
154
|
+
"East",
|
|
155
|
+
"West",
|
|
156
|
+
"North"
|
|
157
|
+
]),
|
|
158
|
+
xbox: Object.freeze([
|
|
159
|
+
"A",
|
|
160
|
+
"B",
|
|
161
|
+
"X",
|
|
162
|
+
"Y"
|
|
163
|
+
]),
|
|
164
|
+
playstation: Object.freeze([
|
|
165
|
+
"×",
|
|
166
|
+
"○",
|
|
167
|
+
"□",
|
|
168
|
+
"△"
|
|
169
|
+
]),
|
|
170
|
+
nintendo: Object.freeze([
|
|
171
|
+
"B",
|
|
172
|
+
"A",
|
|
173
|
+
"Y",
|
|
174
|
+
"X"
|
|
175
|
+
])
|
|
176
|
+
});
|
|
177
|
+
function gamepadButtonLabel(button, options = {}) {
|
|
178
|
+
gamepadIndex(button);
|
|
179
|
+
const layout = options.layout ?? "generic";
|
|
180
|
+
if (!Object.hasOwn(faces, layout)) throw new EngineError("INVALID_GAMEPAD_BINDING", "Unknown gamepad display layout");
|
|
181
|
+
if (options.overrides && Object.hasOwn(options.overrides, button)) {
|
|
182
|
+
const label = options.overrides[button];
|
|
183
|
+
if (typeof label !== "string") throw new EngineError("INVALID_GAMEPAD_BINDING", "Button label must be a string");
|
|
184
|
+
return label;
|
|
185
|
+
}
|
|
186
|
+
return faces[layout][button] ?? positions[button] ?? `Button ${button}`;
|
|
187
|
+
}
|
|
188
|
+
function inferredGamepadLayout(id) {
|
|
189
|
+
if (/dualsense|dualshock|playstation|054c/i.test(id)) return "playstation";
|
|
190
|
+
if (/nintendo|switch|057e/i.test(id)) return "nintendo";
|
|
191
|
+
if (/xbox|xinput|045e/i.test(id)) return "xbox";
|
|
192
|
+
return "generic";
|
|
193
|
+
}
|
|
194
|
+
// packages/web/src/gamepads.ts
|
|
195
|
+
var finite = (v, min, max) => typeof v === "number" && Number.isFinite(v) ? Math.max(min, Math.min(max, v)) : 0;
|
|
196
|
+
var buttonValue = (pad, index) => finite(pad.buttons[index]?.value, 0, 1);
|
|
197
|
+
function scalar(pad, binding, deadZone) {
|
|
198
|
+
const group = (indices) => (indices ?? []).reduce((maximum, index) => Math.max(maximum, buttonValue(pad, index)), 0);
|
|
199
|
+
const raw = finite((binding.axis === void 0 ? 0 : finite(pad.axes[binding.axis], -1, 1)) + group(binding.positive) - group(binding.negative), -1, 1) * (binding.invert ? -1 : 1);
|
|
200
|
+
const zone = binding.deadZone ?? deadZone;
|
|
201
|
+
return Math.abs(raw) <= zone ? 0 : Math.sign(raw) * (Math.abs(raw) - zone) / (1 - zone);
|
|
202
|
+
}
|
|
203
|
+
function values(pad, bindings, zone) {
|
|
204
|
+
return bindings.map(([action, binding]) => {
|
|
205
|
+
if (action.kind === "button") return [action, binding.buttons?.some((b) => buttonValue(pad, typeof b === "number" ? b : b.button) >= (typeof b === "number" ? .5 : b.threshold ?? .5)) ?? false];
|
|
206
|
+
if (action.kind === "axis1d") return [action, scalar(pad, binding, zone)];
|
|
207
|
+
const b = binding, x = scalar(pad, b.x ?? {}, zone), y = scalar(pad, b.y ?? {}, zone);
|
|
208
|
+
return [action, action.kind === "axis2d" ? {
|
|
209
|
+
x,
|
|
210
|
+
y
|
|
211
|
+
} : {
|
|
212
|
+
x,
|
|
213
|
+
y,
|
|
214
|
+
z: scalar(pad, b.z ?? {}, zone)
|
|
215
|
+
}];
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function neutral(value) {
|
|
219
|
+
return typeof value === "boolean" ? !value : typeof value === "number" ? value === 0 : value.x === 0 && value.y === 0 && (value.z ?? 0) === 0;
|
|
220
|
+
}
|
|
221
|
+
/** A controller-owned device contribution. Reassignment does not replace its readers/Actors. */
|
|
222
|
+
var GamepadSource = class {
|
|
223
|
+
input;
|
|
224
|
+
controller;
|
|
225
|
+
life = new Lifetime();
|
|
226
|
+
source;
|
|
227
|
+
bindings;
|
|
228
|
+
assigned;
|
|
229
|
+
armed = false;
|
|
230
|
+
signature;
|
|
231
|
+
zone;
|
|
232
|
+
mapping;
|
|
233
|
+
constructor(input, controller, options) {
|
|
234
|
+
this.input = input;
|
|
235
|
+
this.controller = controller;
|
|
236
|
+
input.assertController(controller);
|
|
237
|
+
this.assigned = gamepadIndex(options.index);
|
|
238
|
+
this.zone = gamepadDeadZone(options.deadZone ?? .15);
|
|
239
|
+
this.mapping = options.mapping ?? "standard";
|
|
240
|
+
if (this.mapping !== "standard" && this.mapping !== "raw") throw new EngineError("INVALID_GAMEPAD_BINDING", "Unknown gamepad mapping policy");
|
|
241
|
+
this.bindings = checkedGamepadBindings(controller.actions, options.bindings);
|
|
242
|
+
this.source = controller.createSource();
|
|
243
|
+
this.life.onDispose(() => this.source.dispose());
|
|
244
|
+
this.life.onDispose(controller.onDispose(() => this.dispose()));
|
|
245
|
+
}
|
|
246
|
+
get disposed() {
|
|
247
|
+
return this.life.disposed;
|
|
248
|
+
}
|
|
249
|
+
get enabled() {
|
|
250
|
+
return this.source.enabled;
|
|
251
|
+
}
|
|
252
|
+
set enabled(value) {
|
|
253
|
+
this.alive();
|
|
254
|
+
if (value === this.source.enabled) return;
|
|
255
|
+
this.source.enabled = value;
|
|
256
|
+
this.clear();
|
|
257
|
+
}
|
|
258
|
+
get index() {
|
|
259
|
+
return this.assigned;
|
|
260
|
+
}
|
|
261
|
+
get info() {
|
|
262
|
+
return this.input.devices.find((d) => d.index === this.assigned);
|
|
263
|
+
}
|
|
264
|
+
get connected() {
|
|
265
|
+
return this.info?.connected ?? false;
|
|
266
|
+
}
|
|
267
|
+
get usable() {
|
|
268
|
+
return this.connected && (this.mapping === "raw" || this.info?.standardMapping === true);
|
|
269
|
+
}
|
|
270
|
+
assign(index) {
|
|
271
|
+
this.alive();
|
|
272
|
+
const checked = gamepadIndex(index);
|
|
273
|
+
if (checked === this.assigned) return;
|
|
274
|
+
this.clear();
|
|
275
|
+
this.signature = void 0;
|
|
276
|
+
this.assigned = checked;
|
|
277
|
+
}
|
|
278
|
+
rebind(bindings) {
|
|
279
|
+
this.alive();
|
|
280
|
+
const checked = checkedGamepadBindings(this.controller.actions, bindings);
|
|
281
|
+
this.clear();
|
|
282
|
+
this.bindings = checked;
|
|
283
|
+
}
|
|
284
|
+
clear() {
|
|
285
|
+
this.source.clear();
|
|
286
|
+
this.armed = false;
|
|
287
|
+
}
|
|
288
|
+
alive() {
|
|
289
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Gamepad source has been disposed");
|
|
290
|
+
}
|
|
291
|
+
/** @internal */ sample(pad, allowed) {
|
|
292
|
+
if (this.disposed) return;
|
|
293
|
+
if (!allowed || !this.enabled || !this.controller.active || !pad || this.mapping === "standard" && pad.mapping !== "standard") {
|
|
294
|
+
this.clear();
|
|
295
|
+
this.signature = void 0;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const signature = JSON.stringify([
|
|
299
|
+
pad.id,
|
|
300
|
+
pad.mapping,
|
|
301
|
+
pad.buttons.length,
|
|
302
|
+
pad.axes.length
|
|
303
|
+
]);
|
|
304
|
+
if (signature !== this.signature) {
|
|
305
|
+
this.clear();
|
|
306
|
+
this.signature = signature;
|
|
307
|
+
}
|
|
308
|
+
const next = values(pad, this.bindings, this.zone);
|
|
309
|
+
if (!this.armed) {
|
|
310
|
+
if (next.every(([, value]) => neutral(value))) this.armed = true;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
for (const [action, value] of next) this.source.set(action, value);
|
|
314
|
+
}
|
|
315
|
+
/** @internal */ onDispose(callback) {
|
|
316
|
+
return this.life.onDispose(callback);
|
|
317
|
+
}
|
|
318
|
+
dispose() {
|
|
319
|
+
this.life.dispose();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var devicesChanged = defineEvent("GamepadsChanged");
|
|
323
|
+
/** One Web host adapter; multiple sources share a single snapshot per Engine input boundary. */
|
|
324
|
+
var GamepadInput = class {
|
|
325
|
+
input;
|
|
326
|
+
life = new Lifetime();
|
|
327
|
+
changes = new EventBus();
|
|
328
|
+
sources = /* @__PURE__ */ new Set();
|
|
329
|
+
disconnected = /* @__PURE__ */ new Set();
|
|
330
|
+
listed = Object.freeze([]);
|
|
331
|
+
readable = false;
|
|
332
|
+
poll;
|
|
333
|
+
active;
|
|
334
|
+
constructor(input, options = {}) {
|
|
335
|
+
this.input = input;
|
|
336
|
+
this.life.onDispose(() => this.changes.dispose());
|
|
337
|
+
this.poll = options.poll ?? (() => typeof navigator !== "undefined" && typeof navigator.getGamepads === "function" ? navigator.getGamepads() : (() => {
|
|
338
|
+
throw new Error("Gamepad API unavailable");
|
|
339
|
+
})());
|
|
340
|
+
this.active = options.active ?? (() => true);
|
|
341
|
+
this.life.onDispose(input.addAdapter(this));
|
|
342
|
+
const events = options.events ?? (typeof window === "undefined" ? void 0 : window), abort = new AbortController();
|
|
343
|
+
this.life.onDispose(() => abort.abort());
|
|
344
|
+
events?.addEventListener("gamepaddisconnected", (event) => {
|
|
345
|
+
const index = event.gamepad.index;
|
|
346
|
+
this.disconnected.add(index);
|
|
347
|
+
for (const source of this.sources) if (source.index === index) source.clear();
|
|
348
|
+
this.publish(this.listed.filter((d) => d.index !== index));
|
|
349
|
+
}, { signal: abort.signal });
|
|
350
|
+
events?.addEventListener("gamepadconnected", (event) => {
|
|
351
|
+
const index = event.gamepad.index;
|
|
352
|
+
this.disconnected.delete(index);
|
|
353
|
+
for (const source of this.sources) if (source.index === index) source.clear();
|
|
354
|
+
}, { signal: abort.signal });
|
|
355
|
+
}
|
|
356
|
+
get disposed() {
|
|
357
|
+
return this.life.disposed;
|
|
358
|
+
}
|
|
359
|
+
/** Whether the most recent polling attempt completed; an empty list may still need a user gesture. */
|
|
360
|
+
get available() {
|
|
361
|
+
return this.readable;
|
|
362
|
+
}
|
|
363
|
+
get devices() {
|
|
364
|
+
return this.listed;
|
|
365
|
+
}
|
|
366
|
+
onChange(handler, owner) {
|
|
367
|
+
return this.changes.on(devicesChanged, handler, owner);
|
|
368
|
+
}
|
|
369
|
+
publish(devices, force = false) {
|
|
370
|
+
const changed = JSON.stringify(this.listed) !== JSON.stringify(devices);
|
|
371
|
+
this.listed = Object.freeze(devices);
|
|
372
|
+
if (changed || force) this.changes.emit(devicesChanged, void 0);
|
|
373
|
+
}
|
|
374
|
+
/** @internal */ assertController(controller) {
|
|
375
|
+
if (!this.input.hasController(controller)) throw new EngineError("INVALID_ACTION", "Gamepad controller must belong to this InputSystem");
|
|
376
|
+
}
|
|
377
|
+
bind(controller, options) {
|
|
378
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Gamepad input has been disposed");
|
|
379
|
+
const source = new GamepadSource(this, controller, options);
|
|
380
|
+
const entry = source;
|
|
381
|
+
this.sources.add(entry);
|
|
382
|
+
const remove = this.life.onDispose(() => source.dispose());
|
|
383
|
+
source.onDispose(() => {
|
|
384
|
+
this.sources.delete(entry);
|
|
385
|
+
remove();
|
|
386
|
+
});
|
|
387
|
+
return source;
|
|
388
|
+
}
|
|
389
|
+
sample() {
|
|
390
|
+
if (this.disposed) return;
|
|
391
|
+
const wasReadable = this.readable;
|
|
392
|
+
let snapshots;
|
|
393
|
+
try {
|
|
394
|
+
snapshots = this.poll();
|
|
395
|
+
if (!Array.isArray(snapshots)) throw new Error("Invalid gamepad snapshot");
|
|
396
|
+
this.readable = true;
|
|
397
|
+
} catch {
|
|
398
|
+
snapshots = [];
|
|
399
|
+
this.readable = false;
|
|
400
|
+
}
|
|
401
|
+
const pads = /* @__PURE__ */ new Map();
|
|
402
|
+
for (const pad of snapshots) if (pad?.connected && typeof pad.id === "string" && typeof pad.mapping === "string" && Array.isArray(pad.axes) && Array.isArray(pad.buttons) && Number.isSafeInteger(pad.index) && pad.index >= 0) pads.set(pad.index, pad);
|
|
403
|
+
for (const index of this.disconnected) if (!pads.has(index)) this.disconnected.delete(index);
|
|
404
|
+
for (const index of this.disconnected) pads.delete(index);
|
|
405
|
+
const listed = Object.freeze([...pads.values()].sort((a, b) => a.index - b.index).map((p) => Object.freeze({
|
|
406
|
+
id: p.id,
|
|
407
|
+
index: p.index,
|
|
408
|
+
connected: p.connected,
|
|
409
|
+
mapping: p.mapping,
|
|
410
|
+
standardMapping: p.mapping === "standard",
|
|
411
|
+
buttonCount: p.buttons.length,
|
|
412
|
+
axisCount: p.axes.length,
|
|
413
|
+
inferredLayout: inferredGamepadLayout(p.id)
|
|
414
|
+
})));
|
|
415
|
+
const allowed = this.active();
|
|
416
|
+
for (const source of this.sources) if (source.disposed) this.sources.delete(source);
|
|
417
|
+
else source.sample(pads.get(source.index), allowed);
|
|
418
|
+
this.publish(listed, wasReadable !== this.readable);
|
|
419
|
+
}
|
|
420
|
+
reset() {
|
|
421
|
+
for (const source of this.sources) source.clear();
|
|
422
|
+
}
|
|
423
|
+
dispose() {
|
|
424
|
+
this.life.dispose();
|
|
425
|
+
this.sources.clear();
|
|
426
|
+
this.listed = Object.freeze([]);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
20
429
|
// packages/web/src/ui-keyboard.ts
|
|
21
430
|
/** Host-only ownership metadata; native editing is never reconstructed from game keys. */
|
|
22
431
|
var gameButtons = /* @__PURE__ */ new WeakSet();
|
|
@@ -459,7 +868,8 @@ function nativeGeometry(el, node, ui, clip) {
|
|
|
459
868
|
el.disabled = !node.available || c.width <= 0 || c.height <= 0 || node.style.pointerEvents !== "auto";
|
|
460
869
|
const background = style.background ?? 2316622;
|
|
461
870
|
const color = `#${style.color.toString(16).padStart(6, "0")}`;
|
|
462
|
-
el.style.cssText = `left:${b.x * scale}px;top:${b.y * scale}px;width:${b.width * scale}px;height:${b.height * scale}px;padding:${style.padding * scale}px;font-
|
|
871
|
+
el.style.cssText = `left:${b.x * scale}px;top:${b.y * scale}px;width:${b.width * scale}px;height:${b.height * scale}px;padding:${style.padding * scale}px;font-size:${style.fontSize * scale}px;--s-native-color:${color};--s-native-background:#${background.toString(16).padStart(6, "0")};color-scheme:${(background >> 16) * .2126 + (background >> 8 & 255) * .7152 + (background & 255) * .0722 > 128 ? "light" : "dark"};clip-path:inset(${Math.max(0, c.y - b.y) * scale}px ${Math.max(0, b.x + b.width - c.x - c.width) * scale}px ${Math.max(0, b.y + b.height - c.y - c.height) * scale}px ${Math.max(0, c.x - b.x) * scale}px);`;
|
|
872
|
+
el.style.fontFamily = style.fontFamily;
|
|
463
873
|
if (el.disabled && document.activeElement === el) el.blur();
|
|
464
874
|
}
|
|
465
875
|
/** Native editors must not cover later Canvas hit surfaces or overlay scrollbars. */
|
|
@@ -1684,7 +2094,15 @@ var PixiRenderer = class {
|
|
|
1684
2094
|
}
|
|
1685
2095
|
acquireImage(url, signal) {
|
|
1686
2096
|
try {
|
|
1687
|
-
|
|
2097
|
+
const parsed = new URL(url, document.baseURI);
|
|
2098
|
+
if (![
|
|
2099
|
+
"http:",
|
|
2100
|
+
"https:",
|
|
2101
|
+
"data:",
|
|
2102
|
+
"blob:",
|
|
2103
|
+
"file:"
|
|
2104
|
+
].includes(parsed.protocol)) throw new Error("Unsupported image URL protocol");
|
|
2105
|
+
return this.images.acquire(parsed.href, signal);
|
|
1688
2106
|
} catch {
|
|
1689
2107
|
return Promise.reject(new EngineError("ASSET_LOAD_FAILED", "Invalid image URL", { path: url }));
|
|
1690
2108
|
}
|
|
@@ -2342,6 +2760,7 @@ var Game = class {
|
|
|
2342
2760
|
engine;
|
|
2343
2761
|
element;
|
|
2344
2762
|
camera;
|
|
2763
|
+
gamepads;
|
|
2345
2764
|
renderer;
|
|
2346
2765
|
keyboard;
|
|
2347
2766
|
pointers;
|
|
@@ -2425,8 +2844,10 @@ var Game = class {
|
|
|
2425
2844
|
camera: this.camera
|
|
2426
2845
|
});
|
|
2427
2846
|
if (options.pixelRatio !== void 0) positive(options.pixelRatio, "pixelRatio");
|
|
2428
|
-
const
|
|
2429
|
-
if (!
|
|
2847
|
+
const themeName = options.theme ?? Theme.Dark;
|
|
2848
|
+
if (!Object.hasOwn(Themes, themeName)) throw new EngineError("INVALID_THEME", "Unknown theme");
|
|
2849
|
+
const theme = Themes[themeName];
|
|
2850
|
+
for (const [name, value] of Object.entries(options.tokens ?? {})) if (!Object.hasOwn(theme, name) || typeof value !== "string" || (name === "font" ? !validFontFamily(value) : !value.trim().length || /[;{}<>\r\n\0]|url\s*\(/i.test(value) || !CSS.supports("color", value))) throw new EngineError("INVALID_THEME_TOKEN", `Invalid token: ${name}`);
|
|
2430
2851
|
const tokens = {
|
|
2431
2852
|
...theme,
|
|
2432
2853
|
...options.tokens
|
|
@@ -2454,6 +2875,7 @@ var Game = class {
|
|
|
2454
2875
|
cancel: (handle) => cancelAnimationFrame(handle)
|
|
2455
2876
|
};
|
|
2456
2877
|
this.keyboard = new BrowserInput(this.element, this.engine.input);
|
|
2878
|
+
this.gamepads = new GamepadInput(this.engine.input, { active: () => !document.hidden && document.hasFocus() && this.element.contains(document.activeElement) && !blocksGameKeyboard(document.activeElement) });
|
|
2457
2879
|
this.pointers = new BrowserPointer(this.element, (event) => {
|
|
2458
2880
|
const rect = this.element.getBoundingClientRect();
|
|
2459
2881
|
if (rect.width <= 0 || rect.height <= 0) throw new EngineError("VIEWPORT_UNAVAILABLE", "Game has no visible bounds");
|
|
@@ -2929,6 +3351,7 @@ var Game = class {
|
|
|
2929
3351
|
() => this.stopReplay(),
|
|
2930
3352
|
() => this.staging?.dispose(),
|
|
2931
3353
|
() => this.pointers.dispose(),
|
|
3354
|
+
() => this.gamepads.dispose(),
|
|
2932
3355
|
() => this.keyboard.dispose(),
|
|
2933
3356
|
() => this.canvasPresentation?.dispose(),
|
|
2934
3357
|
() => this.canvas?.dispose(),
|
|
@@ -2954,6 +3377,10 @@ var Game = class {
|
|
|
2954
3377
|
this.pendingScenes.clear();
|
|
2955
3378
|
}
|
|
2956
3379
|
};
|
|
2957
|
-
|
|
3380
|
+
function validFontFamily(value) {
|
|
3381
|
+
const syntax = value.replace(/"(?:\\[^\r\n]|[^"\\])*"|'(?:\\[^\r\n]|[^'\\])*'/g, "family").replace(/\\(?:[0-9a-f]{1,6}[ \t]?|[^\r\n])/gi, "x");
|
|
3382
|
+
return value.trim().length > 0 && !/[\r\n\0]/.test(value) && !/[;{}<>\\"']|url\s*\(/i.test(syntax);
|
|
3383
|
+
}
|
|
3384
|
+
export { BrowserInput, Button, CanvasButton, Game, GamepadAxis, GamepadBindings, GamepadButton, GamepadInput, GamepadSource, HoldButton, Label, Layout, Panel, PixiRenderer, Placement, Theme, Themes, browserAbortSignal, gamepadButtonLabel, mountUI };
|
|
2958
3385
|
|
|
2959
3386
|
//# sourceMappingURL=index.js.map
|