@simpludify/web 0.1.2 → 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/canvas-visual.d.ts +4 -0
- 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 +485 -11
- package/dist/index.js.map +1 -1
- package/dist/renderer.d.ts +1 -0
- package/dist/style.css +13 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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";
|
|
3
|
-
import { Application, CanvasTextMetrics, Container, Graphics, Rectangle, Sprite, Text, TextStyle, Texture } from "pixi.js";
|
|
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
|
+
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
|
|
6
6
|
/** Bridge owner cancellation to browser APIs such as fetch. */
|
|
@@ -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. */
|
|
@@ -527,6 +937,7 @@ var CanvasVisual = class {
|
|
|
527
937
|
dirty = true;
|
|
528
938
|
dead = false;
|
|
529
939
|
active = false;
|
|
940
|
+
cacheResolution;
|
|
530
941
|
mirroredFocus;
|
|
531
942
|
subscriptions = [];
|
|
532
943
|
syncs = 0;
|
|
@@ -581,6 +992,7 @@ var CanvasVisual = class {
|
|
|
581
992
|
get diagnostics() {
|
|
582
993
|
return {
|
|
583
994
|
nodes: this.entries.size,
|
|
995
|
+
visibleNodes: [...this.entries.values()].filter((e) => e.display.visible).length,
|
|
584
996
|
buttons: [...this.entries.values()].filter((e) => e.button).length,
|
|
585
997
|
syncs: this.syncs,
|
|
586
998
|
textMetrics: this.metrics.size
|
|
@@ -624,7 +1036,14 @@ var CanvasVisual = class {
|
|
|
624
1036
|
cache.values.set(width, result);
|
|
625
1037
|
return result;
|
|
626
1038
|
};
|
|
1039
|
+
clearCache() {
|
|
1040
|
+
if (this.cacheResolution === void 0) return;
|
|
1041
|
+
this.display.cacheAsTexture(false);
|
|
1042
|
+
this.cacheResolution = void 0;
|
|
1043
|
+
}
|
|
627
1044
|
beforeResolutionChange() {
|
|
1045
|
+
this.clearCache();
|
|
1046
|
+
this.dirty = true;
|
|
628
1047
|
for (const entry of this.entries.values()) if (entry.text && !entry.text.destroyed) this.resources.changedText(entry.text.styleKey);
|
|
629
1048
|
}
|
|
630
1049
|
focus(node) {
|
|
@@ -753,6 +1172,7 @@ var CanvasVisual = class {
|
|
|
753
1172
|
const visited = /* @__PURE__ */ new Set(), buttons = [];
|
|
754
1173
|
const paint = [];
|
|
755
1174
|
let order = 0;
|
|
1175
|
+
let scrollVisible = false;
|
|
756
1176
|
let restoreNativeFocus;
|
|
757
1177
|
this.ui.walk((node, parentClip) => {
|
|
758
1178
|
const measuredText = node.kind === "panel" ? void 0 : this.measure(node, Math.max(0, node.bounds.width - node.style.padding * 2));
|
|
@@ -777,6 +1197,19 @@ var CanvasVisual = class {
|
|
|
777
1197
|
entry.button = entry.native.element;
|
|
778
1198
|
} else entry.button = this.button(node);
|
|
779
1199
|
this.element.append(entry.button);
|
|
1200
|
+
} else if (node.kind === "label") {
|
|
1201
|
+
entry.label = document.createElement("span");
|
|
1202
|
+
entry.label.className = "s-canvas-label";
|
|
1203
|
+
const semantics = node.labelSemantics;
|
|
1204
|
+
if (semantics?.role) {
|
|
1205
|
+
entry.label.setAttribute("role", semantics.role);
|
|
1206
|
+
if (semantics.role === "heading") entry.label.setAttribute("aria-level", String(semantics.level));
|
|
1207
|
+
else {
|
|
1208
|
+
entry.label.setAttribute("aria-live", semantics.role === "alert" ? "assertive" : "polite");
|
|
1209
|
+
entry.label.setAttribute("aria-atomic", "true");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
this.element.append(entry.label);
|
|
780
1213
|
}
|
|
781
1214
|
if (node.kind !== "panel") {
|
|
782
1215
|
entry.text = new Text({ text: node.text });
|
|
@@ -802,7 +1235,14 @@ var CanvasVisual = class {
|
|
|
802
1235
|
} : {}
|
|
803
1236
|
});
|
|
804
1237
|
if (entry.button) buttons.push(entry.button);
|
|
1238
|
+
if (entry.label) {
|
|
1239
|
+
buttons.push(entry.label);
|
|
1240
|
+
if (entry.label.textContent !== node.text) entry.label.textContent = node.text;
|
|
1241
|
+
entry.label.setAttribute("aria-hidden", String(canvasOpacity(node) === 0));
|
|
1242
|
+
}
|
|
805
1243
|
const b = node.bounds, s = node.style;
|
|
1244
|
+
const scrollBox = intersectUI(b, clip);
|
|
1245
|
+
if (node.style.scroll && scrollBox.width > 0 && scrollBox.height > 0 && canvasOpacity(node) > 0) scrollVisible = true;
|
|
806
1246
|
let enabled = !node.busy;
|
|
807
1247
|
for (let p = node; p; p = p.parent) enabled &&= p.enabled;
|
|
808
1248
|
const signature = JSON.stringify([
|
|
@@ -823,9 +1263,12 @@ var CanvasVisual = class {
|
|
|
823
1263
|
if (entry.signature === signature) return;
|
|
824
1264
|
entry.signature = signature;
|
|
825
1265
|
entry.display.position.set(b.x, b.y);
|
|
1266
|
+
const paintBox = intersectUI(b, clip);
|
|
1267
|
+
const visible = paintBox.width > 0 && paintBox.height > 0 && canvasOpacity(node) > 0;
|
|
1268
|
+
entry.display.visible = visible;
|
|
826
1269
|
const clipped = clip.x !== 0 || clip.y !== 0 || clip.width !== width / this.ui.scale || clip.height !== height / this.ui.scale;
|
|
827
1270
|
entry.display.mask = clipped ? entry.mask : null;
|
|
828
|
-
entry.mask.renderable = clipped;
|
|
1271
|
+
entry.mask.renderable = clipped && visible;
|
|
829
1272
|
const maskSignature = JSON.stringify([
|
|
830
1273
|
clipped,
|
|
831
1274
|
clip.x,
|
|
@@ -927,7 +1370,8 @@ var CanvasVisual = class {
|
|
|
927
1370
|
const display = entry.scrollbar;
|
|
928
1371
|
if (!display) return;
|
|
929
1372
|
this.display.setChildIndex(display, order++);
|
|
930
|
-
|
|
1373
|
+
const viewport = intersectUI(node.bounds, clip);
|
|
1374
|
+
display.visible = bars.length > 0 && viewport.width > 0 && viewport.height > 0 && canvasOpacity(node) > 0;
|
|
931
1375
|
display.mask = entry.display.mask;
|
|
932
1376
|
const signature = JSON.stringify([
|
|
933
1377
|
node.style.color,
|
|
@@ -982,6 +1426,14 @@ var CanvasVisual = class {
|
|
|
982
1426
|
const button = focused ? this.entries.get(focused)?.button : void 0;
|
|
983
1427
|
if ((focused !== this.mirroredFocus || focused === restoreNativeFocus) && button && !button.disabled && document.activeElement !== button) button.focus({ preventScroll: true });
|
|
984
1428
|
this.mirroredFocus = this.ui.focusedNode;
|
|
1429
|
+
const ratio = this.resources.cacheResolution();
|
|
1430
|
+
const cache = scrollVisible && ratio !== void 0 && Math.max(width, height) * ratio <= 4096 ? ratio * this.ui.scale : void 0;
|
|
1431
|
+
if (cache !== this.cacheResolution) {
|
|
1432
|
+
this.clearCache();
|
|
1433
|
+
this.cacheResolution = cache;
|
|
1434
|
+
if (cache !== void 0) this.display.cacheAsTexture({ resolution: cache });
|
|
1435
|
+
}
|
|
1436
|
+
if (cache !== void 0) this.display.updateCacheTexture();
|
|
985
1437
|
}
|
|
986
1438
|
releaseEntry(entry) {
|
|
987
1439
|
const backgroundContext = entry.background.context?.uid, maskContext = entry.mask.context?.uid;
|
|
@@ -991,6 +1443,7 @@ var CanvasVisual = class {
|
|
|
991
1443
|
() => entry.abort.abort(),
|
|
992
1444
|
() => entry.native?.dispose(),
|
|
993
1445
|
() => entry.button?.remove(),
|
|
1446
|
+
() => entry.label?.remove(),
|
|
994
1447
|
() => entry.background.destroy({ context: true }),
|
|
995
1448
|
() => entry.scrollbar?.destroy({ context: true }),
|
|
996
1449
|
() => entry.text?.destroy({ style: true }),
|
|
@@ -1015,6 +1468,7 @@ var CanvasVisual = class {
|
|
|
1015
1468
|
cleanupAll([
|
|
1016
1469
|
...this.subscriptions.splice(0),
|
|
1017
1470
|
...entries.map((e) => () => this.releaseEntry(e)),
|
|
1471
|
+
() => this.clearCache(),
|
|
1018
1472
|
() => this.display.destroy({ children: true }),
|
|
1019
1473
|
() => this.element.remove()
|
|
1020
1474
|
], "Canvas visual cleanup failed");
|
|
@@ -1462,10 +1916,11 @@ var PixiRenderer = class {
|
|
|
1462
1916
|
pendingUITextKeys = /* @__PURE__ */ new Set();
|
|
1463
1917
|
canvasPresentations = /* @__PURE__ */ new Set();
|
|
1464
1918
|
get canvasDiagnostics() {
|
|
1465
|
-
let nodes = 0, buttons = 0, syncs = 0, textMetrics = 0;
|
|
1919
|
+
let nodes = 0, visibleNodes = 0, buttons = 0, syncs = 0, textMetrics = 0;
|
|
1466
1920
|
for (const p of this.canvasPresentations) {
|
|
1467
1921
|
const d = p.diagnostics;
|
|
1468
1922
|
nodes += d.nodes;
|
|
1923
|
+
visibleNodes += d.visibleNodes;
|
|
1469
1924
|
buttons += d.buttons;
|
|
1470
1925
|
syncs += d.syncs;
|
|
1471
1926
|
textMetrics += d.textMetrics;
|
|
@@ -1473,6 +1928,7 @@ var PixiRenderer = class {
|
|
|
1473
1928
|
return {
|
|
1474
1929
|
presentations: this.canvasPresentations.size,
|
|
1475
1930
|
nodes,
|
|
1931
|
+
visibleNodes,
|
|
1476
1932
|
buttons,
|
|
1477
1933
|
syncs,
|
|
1478
1934
|
textMetrics
|
|
@@ -1483,7 +1939,8 @@ var PixiRenderer = class {
|
|
|
1483
1939
|
const visual = new CanvasVisual(ui, this.host, routing, {
|
|
1484
1940
|
graphics: (graphics, context) => releaseUIGraphics(this.app.renderer, graphics, context),
|
|
1485
1941
|
text: (text, key) => releaseUIText(this.app.renderer, text, key),
|
|
1486
|
-
changedText: (key) => this.pendingUITextKeys.add(key)
|
|
1942
|
+
changedText: (key) => this.pendingUITextKeys.add(key),
|
|
1943
|
+
cacheResolution: () => this.app.renderer.type === RendererType.WEBGL ? this.app.renderer.resolution : void 0
|
|
1487
1944
|
});
|
|
1488
1945
|
this.canvasPresentations.add(visual);
|
|
1489
1946
|
let disposed = false;
|
|
@@ -1637,7 +2094,15 @@ var PixiRenderer = class {
|
|
|
1637
2094
|
}
|
|
1638
2095
|
acquireImage(url, signal) {
|
|
1639
2096
|
try {
|
|
1640
|
-
|
|
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);
|
|
1641
2106
|
} catch {
|
|
1642
2107
|
return Promise.reject(new EngineError("ASSET_LOAD_FAILED", "Invalid image URL", { path: url }));
|
|
1643
2108
|
}
|
|
@@ -2295,6 +2760,7 @@ var Game = class {
|
|
|
2295
2760
|
engine;
|
|
2296
2761
|
element;
|
|
2297
2762
|
camera;
|
|
2763
|
+
gamepads;
|
|
2298
2764
|
renderer;
|
|
2299
2765
|
keyboard;
|
|
2300
2766
|
pointers;
|
|
@@ -2378,8 +2844,10 @@ var Game = class {
|
|
|
2378
2844
|
camera: this.camera
|
|
2379
2845
|
});
|
|
2380
2846
|
if (options.pixelRatio !== void 0) positive(options.pixelRatio, "pixelRatio");
|
|
2381
|
-
const
|
|
2382
|
-
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}`);
|
|
2383
2851
|
const tokens = {
|
|
2384
2852
|
...theme,
|
|
2385
2853
|
...options.tokens
|
|
@@ -2407,6 +2875,7 @@ var Game = class {
|
|
|
2407
2875
|
cancel: (handle) => cancelAnimationFrame(handle)
|
|
2408
2876
|
};
|
|
2409
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) });
|
|
2410
2879
|
this.pointers = new BrowserPointer(this.element, (event) => {
|
|
2411
2880
|
const rect = this.element.getBoundingClientRect();
|
|
2412
2881
|
if (rect.width <= 0 || rect.height <= 0) throw new EngineError("VIEWPORT_UNAVAILABLE", "Game has no visible bounds");
|
|
@@ -2882,6 +3351,7 @@ var Game = class {
|
|
|
2882
3351
|
() => this.stopReplay(),
|
|
2883
3352
|
() => this.staging?.dispose(),
|
|
2884
3353
|
() => this.pointers.dispose(),
|
|
3354
|
+
() => this.gamepads.dispose(),
|
|
2885
3355
|
() => this.keyboard.dispose(),
|
|
2886
3356
|
() => this.canvasPresentation?.dispose(),
|
|
2887
3357
|
() => this.canvas?.dispose(),
|
|
@@ -2907,6 +3377,10 @@ var Game = class {
|
|
|
2907
3377
|
this.pendingScenes.clear();
|
|
2908
3378
|
}
|
|
2909
3379
|
};
|
|
2910
|
-
|
|
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 };
|
|
2911
3385
|
|
|
2912
3386
|
//# sourceMappingURL=index.js.map
|