@driftengine/native-host 4.0.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/LICENSE +202 -0
- package/NOTICE +29 -0
- package/README.md +58 -0
- package/dist/activation.d.ts +26 -0
- package/dist/activation.js +38 -0
- package/dist/alphaCopy.d.ts +36 -0
- package/dist/alphaCopy.js +178 -0
- package/dist/audio.d.ts +55 -0
- package/dist/audio.js +193 -0
- package/dist/audioParams.d.ts +25 -0
- package/dist/audioParams.js +128 -0
- package/dist/bridge.d.ts +70 -0
- package/dist/bridge.js +170 -0
- package/dist/canvas.d.ts +97 -0
- package/dist/canvas.js +197 -0
- package/dist/canvasStyle.d.ts +23 -0
- package/dist/canvasStyle.js +79 -0
- package/dist/device.d.ts +34 -0
- package/dist/device.js +144 -0
- package/dist/dialogs.d.ts +23 -0
- package/dist/dialogs.js +85 -0
- package/dist/domEvents.d.ts +37 -0
- package/dist/domEvents.js +145 -0
- package/dist/domTree.d.ts +34 -0
- package/dist/domTree.js +194 -0
- package/dist/dualRumble.d.ts +19 -0
- package/dist/dualRumble.js +47 -0
- package/dist/eventClasses.d.ts +12 -0
- package/dist/eventClasses.js +35 -0
- package/dist/files.d.ts +16 -0
- package/dist/files.js +59 -0
- package/dist/fullscreen.d.ts +33 -0
- package/dist/fullscreen.js +65 -0
- package/dist/game.d.ts +25 -0
- package/dist/game.js +38 -0
- package/dist/gamepads.d.ts +65 -0
- package/dist/gamepads.js +178 -0
- package/dist/globals.d.ts +24 -0
- package/dist/globals.js +37 -0
- package/dist/gpu.d.ts +26 -0
- package/dist/gpu.js +47 -0
- package/dist/images.d.ts +50 -0
- package/dist/images.js +111 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +21 -0
- package/dist/keyCodes.d.ts +30 -0
- package/dist/keyCodes.js +205 -0
- package/dist/keyEvents.d.ts +54 -0
- package/dist/keyEvents.js +117 -0
- package/dist/mediaQueries.d.ts +62 -0
- package/dist/mediaQueries.js +152 -0
- package/dist/mouseEvents.d.ts +96 -0
- package/dist/mouseEvents.js +287 -0
- package/dist/page.d.ts +118 -0
- package/dist/page.js +302 -0
- package/dist/pointerCapture.d.ts +32 -0
- package/dist/pointerCapture.js +69 -0
- package/dist/present.d.ts +27 -0
- package/dist/present.js +83 -0
- package/dist/rawPad.d.ts +44 -0
- package/dist/rawPad.js +65 -0
- package/dist/readback.d.ts +18 -0
- package/dist/readback.js +46 -0
- package/dist/runtime.d.ts +69 -0
- package/dist/runtime.js +103 -0
- package/dist/standardPad.d.ts +49 -0
- package/dist/standardPad.js +80 -0
- package/dist/textHost.d.ts +37 -0
- package/dist/textHost.js +43 -0
- package/dist/threads.d.ts +27 -0
- package/dist/threads.js +59 -0
- package/dist/tileFiles.d.ts +16 -0
- package/dist/tileFiles.js +36 -0
- package/dist/touchEvents.d.ts +67 -0
- package/dist/touchEvents.js +210 -0
- package/dist/uiEvents.d.ts +157 -0
- package/dist/uiEvents.js +206 -0
- package/dist/uiTouch.d.ts +59 -0
- package/dist/uiTouch.js +75 -0
- package/dist/window.d.ts +71 -0
- package/dist/window.js +164 -0
- package/dist/windowMode.d.ts +19 -0
- package/dist/windowMode.js +15 -0
- package/dist/windowTitle.d.ts +18 -0
- package/dist/windowTitle.js +33 -0
- package/dist/workerScope.mjs +48 -0
- package/licenses/GPL-3.0.txt +674 -0
- package/package.json +83 -0
- package/src/activation.ts +42 -0
- package/src/alphaCopy.ts +201 -0
- package/src/audio.ts +246 -0
- package/src/audioParams.ts +150 -0
- package/src/bridge.ts +230 -0
- package/src/canvas.ts +227 -0
- package/src/canvasStyle.ts +89 -0
- package/src/device.ts +167 -0
- package/src/dialogs.ts +98 -0
- package/src/domEvents.ts +170 -0
- package/src/domTree.ts +215 -0
- package/src/drs.d.ts +9 -0
- package/src/dualRumble.ts +58 -0
- package/src/eventClasses.ts +46 -0
- package/src/files.ts +61 -0
- package/src/fixtures/grey.jpg +0 -0
- package/src/fixtures/rgb.png +0 -0
- package/src/fixtures/tone.opus +0 -0
- package/src/fullscreen.ts +71 -0
- package/src/game.ts +53 -0
- package/src/gamepads.ts +195 -0
- package/src/globals.ts +39 -0
- package/src/gpu.ts +59 -0
- package/src/images.ts +148 -0
- package/src/index.ts +24 -0
- package/src/keyCodes.ts +203 -0
- package/src/keyEvents.ts +135 -0
- package/src/mediaQueries.ts +173 -0
- package/src/mouseEvents.ts +328 -0
- package/src/page.ts +337 -0
- package/src/pointerCapture.ts +73 -0
- package/src/present.ts +92 -0
- package/src/rawPad.ts +85 -0
- package/src/readback.ts +61 -0
- package/src/runtime.ts +162 -0
- package/src/standardPad.ts +126 -0
- package/src/textHost.ts +66 -0
- package/src/threads.ts +66 -0
- package/src/tileFiles.ts +39 -0
- package/src/touchEvents.ts +238 -0
- package/src/uiEvents.ts +268 -0
- package/src/uiTouch.ts +97 -0
- package/src/window.ts +192 -0
- package/src/windowMode.ts +24 -0
- package/src/windowTitle.ts +36 -0
- package/src/workerScope.mjs +48 -0
package/dist/page.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a scene expects around its canvas, for a host that has none: `window`, `document`, and
|
|
3
|
+
* the animation frames a browser schedules.
|
|
4
|
+
*
|
|
5
|
+
* **What is here is what gets read**, listed from every listener and property the engine's packages
|
|
6
|
+
* and the published scenes reach: keys, pointers, the mouse, the wheel, focus, resizing and
|
|
7
|
+
* unloading on `window`; visibility, focus and pointer lock on `document`; `requestAnimationFrame`,
|
|
8
|
+
* which the engine's input polls from. The three are a tree (`domTree.ts`), so an event is
|
|
9
|
+
* captured and bubbles as it does in a browser, and each answers the `on…` attributes. Anything
|
|
10
|
+
* else a scene reaches for is a gap named when it is met — `createElement` refuses by name —
|
|
11
|
+
* rather than a DOM this pretends to be.
|
|
12
|
+
*
|
|
13
|
+
* **Focus is a browser's.** The window has it or not, as SDL says; within it the canvas is focused
|
|
14
|
+
* only once it is focusable, which a canvas is when it is given a `tabIndex`, and keys go to what
|
|
15
|
+
* is focused, or to the document when nothing is.
|
|
16
|
+
*
|
|
17
|
+
* **A `document` is what the engine's splash looks for**, and it mounts one unless the scene says
|
|
18
|
+
* `splash: false` or the page carries a shell's bridge (`__driftHost`). The published scenes all
|
|
19
|
+
* say so; a packaged game is given the bridge.
|
|
20
|
+
*/
|
|
21
|
+
import { UserActivation } from './activation.js';
|
|
22
|
+
import { HostNode } from './domTree.js';
|
|
23
|
+
import { installEventClasses } from './eventClasses.js';
|
|
24
|
+
import { PageFullscreen } from './fullscreen.js';
|
|
25
|
+
import { MediaQueries } from './mediaQueries.js';
|
|
26
|
+
import { PointerCapture } from './pointerCapture.js';
|
|
27
|
+
import { FocusEvent } from './uiEvents.js';
|
|
28
|
+
/** Every input event a page can be told of, which each of the three nodes answers `on…` for. */
|
|
29
|
+
export const INPUT_TYPES = [
|
|
30
|
+
...['keydown', 'keypress', 'keyup', 'focus', 'blur', 'wheel', 'contextmenu'],
|
|
31
|
+
...['click', 'dblclick', 'auxclick', 'mousedown', 'mouseup', 'mousemove'],
|
|
32
|
+
...['mouseover', 'mouseout', 'mouseenter', 'mouseleave'],
|
|
33
|
+
...['pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout'],
|
|
34
|
+
...['pointerenter', 'pointerleave', 'pointercancel', 'gotpointercapture', 'lostpointercapture'],
|
|
35
|
+
...['touchstart', 'touchmove', 'touchend', 'touchcancel', 'fullscreenchange', 'fullscreenerror'],
|
|
36
|
+
];
|
|
37
|
+
const WINDOW_TYPES = ['resize', 'beforeunload', 'gamepadconnected', 'gamepaddisconnected'];
|
|
38
|
+
const DOCUMENT_TYPES = [
|
|
39
|
+
'visibilitychange',
|
|
40
|
+
'pointerlockchange',
|
|
41
|
+
'pointerlockerror',
|
|
42
|
+
'fullscreenchange',
|
|
43
|
+
'fullscreenerror',
|
|
44
|
+
];
|
|
45
|
+
export class PageWindow extends HostNode {
|
|
46
|
+
canvas = null;
|
|
47
|
+
/** Answered by the page, which knows what a query asks about (`mediaQueries.ts`). */
|
|
48
|
+
matchMedia = () => {
|
|
49
|
+
throw new Error('[driftengine] a window no page holds has no media to query');
|
|
50
|
+
};
|
|
51
|
+
get innerWidth() {
|
|
52
|
+
return this.canvas?.clientWidth ?? 0;
|
|
53
|
+
}
|
|
54
|
+
get innerHeight() {
|
|
55
|
+
return this.canvas?.clientHeight ?? 0;
|
|
56
|
+
}
|
|
57
|
+
get devicePixelRatio() {
|
|
58
|
+
return this.canvas?.pixelRatio ?? 1;
|
|
59
|
+
}
|
|
60
|
+
static {
|
|
61
|
+
HostNode.handles(PageWindow.prototype, [...INPUT_TYPES, ...WINDOW_TYPES]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export class PageDocument extends HostNode {
|
|
65
|
+
visibilityState = 'visible';
|
|
66
|
+
pointerLockElement = null;
|
|
67
|
+
/** What is focused: the canvas, once it is focusable and asked, or nothing. */
|
|
68
|
+
activeElement = null;
|
|
69
|
+
/** Set by the page that owns this document, which is what holds the lock and the focus. */
|
|
70
|
+
release = null;
|
|
71
|
+
focused = true;
|
|
72
|
+
/** The page's fullscreen, which this document reports. */
|
|
73
|
+
fullscreenControl = null;
|
|
74
|
+
fullscreenEnabled = true;
|
|
75
|
+
get fullscreenElement() {
|
|
76
|
+
return this.fullscreenControl?.element ?? null;
|
|
77
|
+
}
|
|
78
|
+
/** The legacy flag, still read. */
|
|
79
|
+
get fullscreen() {
|
|
80
|
+
return this.fullscreenElement !== null;
|
|
81
|
+
}
|
|
82
|
+
exitFullscreen() {
|
|
83
|
+
return this.fullscreenControl?.exit() ?? Promise.resolve();
|
|
84
|
+
}
|
|
85
|
+
get hidden() {
|
|
86
|
+
return this.visibilityState !== 'visible';
|
|
87
|
+
}
|
|
88
|
+
hasFocus() {
|
|
89
|
+
return this.focused;
|
|
90
|
+
}
|
|
91
|
+
exitPointerLock() {
|
|
92
|
+
this.release?.();
|
|
93
|
+
}
|
|
94
|
+
createElement(tag) {
|
|
95
|
+
throw new Error(`[driftengine] the native host has no DOM to make a <${tag}> in`);
|
|
96
|
+
}
|
|
97
|
+
static {
|
|
98
|
+
HostNode.handles(PageDocument.prototype, [...INPUT_TYPES, ...DOCUMENT_TYPES]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export class HostPage {
|
|
102
|
+
window = new PageWindow();
|
|
103
|
+
document = new PageDocument();
|
|
104
|
+
/** Whether a person has just done something (`activation.ts`), as `navigator.userActivation`. */
|
|
105
|
+
userActivation;
|
|
106
|
+
fullscreen;
|
|
107
|
+
/** Which pointers the page holds (`pointerCapture.ts`). */
|
|
108
|
+
capture = new PointerCapture();
|
|
109
|
+
media;
|
|
110
|
+
/** Whether a finger has landed: the one proof of a touch screen SDL gives. */
|
|
111
|
+
touched = false;
|
|
112
|
+
/* Two queues swapped each frame, so a frame allocates nothing and asks made during it wait. */
|
|
113
|
+
asked = new Map();
|
|
114
|
+
due = new Map();
|
|
115
|
+
nextId = 1;
|
|
116
|
+
lockListeners = [];
|
|
117
|
+
locker = null;
|
|
118
|
+
constructor(options = {}) {
|
|
119
|
+
this.userActivation = new UserActivation(options.now ?? (() => performance.now()));
|
|
120
|
+
this.fullscreen = new PageFullscreen(this.userActivation);
|
|
121
|
+
this.document.parentNode = this.window;
|
|
122
|
+
this.document.release = () => this.unlock();
|
|
123
|
+
this.document.fullscreenControl = this.fullscreen;
|
|
124
|
+
this.fullscreen.changed = () => this.media.changed();
|
|
125
|
+
this.media = new MediaQueries(() => ({
|
|
126
|
+
width: this.window.innerWidth,
|
|
127
|
+
height: this.window.innerHeight,
|
|
128
|
+
pixelRatio: this.window.devicePixelRatio,
|
|
129
|
+
fullscreen: this.document.fullscreenElement !== null,
|
|
130
|
+
touched: this.touched,
|
|
131
|
+
}));
|
|
132
|
+
this.window.matchMedia = (query) => this.media.match(query);
|
|
133
|
+
}
|
|
134
|
+
/** A finger landed, so this is a touch screen, whatever SDL listed. */
|
|
135
|
+
noteTouch() {
|
|
136
|
+
if (this.touched)
|
|
137
|
+
return;
|
|
138
|
+
this.touched = true;
|
|
139
|
+
this.media.changed();
|
|
140
|
+
}
|
|
141
|
+
requestAnimationFrame = (callback) => {
|
|
142
|
+
const id = this.nextId;
|
|
143
|
+
this.nextId += 1;
|
|
144
|
+
this.asked.set(id, callback);
|
|
145
|
+
return id;
|
|
146
|
+
};
|
|
147
|
+
cancelAnimationFrame = (id) => {
|
|
148
|
+
this.asked.delete(id);
|
|
149
|
+
this.due.delete(id);
|
|
150
|
+
};
|
|
151
|
+
/** One frame: the callbacks asked for before it began, in the order they were asked. */
|
|
152
|
+
runFrame(now) {
|
|
153
|
+
const due = this.asked;
|
|
154
|
+
this.asked = this.due;
|
|
155
|
+
this.due = due;
|
|
156
|
+
for (const [id, callback] of due) {
|
|
157
|
+
/* Looked up again, so a callback cancelled by an earlier one this frame does not run. */
|
|
158
|
+
if (!due.has(id))
|
|
159
|
+
continue;
|
|
160
|
+
due.delete(id);
|
|
161
|
+
callback(now);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Whether the window can be seen, as `document.visibilityState` reports it. */
|
|
165
|
+
setVisible(visible) {
|
|
166
|
+
const state = visible ? 'visible' : 'hidden';
|
|
167
|
+
if (this.document.visibilityState === state)
|
|
168
|
+
return;
|
|
169
|
+
this.document.visibilityState = state;
|
|
170
|
+
this.document.dispatchEvent(new Event('visibilitychange'));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The window gaining or losing the focus: the focused canvas is told first, as the element a
|
|
174
|
+
* browser blurs, and then the window.
|
|
175
|
+
*/
|
|
176
|
+
setFocused(focused) {
|
|
177
|
+
if (this.document.focused === focused)
|
|
178
|
+
return;
|
|
179
|
+
this.document.focused = focused;
|
|
180
|
+
const canvas = this.document.activeElement;
|
|
181
|
+
if (canvas !== null)
|
|
182
|
+
focusEvents(canvas, focused);
|
|
183
|
+
this.window.dispatchEvent(new FocusEvent(focused ? 'focus' : 'blur'));
|
|
184
|
+
}
|
|
185
|
+
/** Where a key goes: the focused element, or the document when nothing is focused. */
|
|
186
|
+
keyTarget() {
|
|
187
|
+
return this.document.activeElement ?? this.document;
|
|
188
|
+
}
|
|
189
|
+
/** Hold `canvas` in this page: its parent, its pointer lock and its focus are the page's. */
|
|
190
|
+
attach(canvas) {
|
|
191
|
+
canvas.parentNode = this.document;
|
|
192
|
+
this.window.canvas = canvas;
|
|
193
|
+
canvas.lockPointer = () => {
|
|
194
|
+
if (!this.userActivation.isActive) {
|
|
195
|
+
this.document.dispatchEvent(new Event('pointerlockerror', { bubbles: true }));
|
|
196
|
+
return Promise.reject(refusal('NotAllowedError', 'A user gesture is required to request Pointer Lock.'));
|
|
197
|
+
}
|
|
198
|
+
return this.lock(canvas)
|
|
199
|
+
? Promise.resolve()
|
|
200
|
+
: Promise.reject(refusal('NotAllowedError', '[driftengine] the window could not hold the pointer'));
|
|
201
|
+
};
|
|
202
|
+
canvas.fullscreener = () => this.fullscreen.request(canvas);
|
|
203
|
+
canvas.capture = this.capture;
|
|
204
|
+
canvas.focuser = (focus) => {
|
|
205
|
+
const now = this.document.activeElement;
|
|
206
|
+
if (focus === (now === canvas))
|
|
207
|
+
return;
|
|
208
|
+
this.document.activeElement = focus ? canvas : null;
|
|
209
|
+
if (this.document.focused)
|
|
210
|
+
focusEvents(canvas, focus);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The platform's half of the lock: hide the cursor and hold it, or let it go. Answers false when it
|
|
215
|
+
* cannot — a window without focus, as a hidden one is — and the lock is then refused, the way a
|
|
216
|
+
* browser refuses one: the promise rejects and `pointerlockerror` fires, and nothing is locked.
|
|
217
|
+
*/
|
|
218
|
+
setPointerLocker(locker) {
|
|
219
|
+
this.locker = locker;
|
|
220
|
+
}
|
|
221
|
+
/** The platform's half of fullscreen: put the window in it or take it out, false when it cannot. */
|
|
222
|
+
setFullscreener(setter) {
|
|
223
|
+
this.fullscreen.setter = setter;
|
|
224
|
+
}
|
|
225
|
+
/** Told when the lock is taken or released. */
|
|
226
|
+
onPointerLock(listener) {
|
|
227
|
+
this.lockListeners.push(listener);
|
|
228
|
+
}
|
|
229
|
+
unlock() {
|
|
230
|
+
if (this.document.pointerLockElement === null)
|
|
231
|
+
return;
|
|
232
|
+
this.locker?.(false);
|
|
233
|
+
this.document.pointerLockElement = null;
|
|
234
|
+
this.changedLock(false);
|
|
235
|
+
}
|
|
236
|
+
/** Put the globals a page has on `scope`, and hand back what takes them off again. */
|
|
237
|
+
install(scope = globalThis) {
|
|
238
|
+
const names = [
|
|
239
|
+
'window',
|
|
240
|
+
'document',
|
|
241
|
+
'requestAnimationFrame',
|
|
242
|
+
'cancelAnimationFrame',
|
|
243
|
+
'devicePixelRatio',
|
|
244
|
+
'matchMedia',
|
|
245
|
+
];
|
|
246
|
+
const held = names.map((name) => [name, Object.getOwnPropertyDescriptor(scope, name)]);
|
|
247
|
+
scope['window'] = this.window;
|
|
248
|
+
scope['document'] = this.document;
|
|
249
|
+
scope['requestAnimationFrame'] = this.requestAnimationFrame;
|
|
250
|
+
scope['cancelAnimationFrame'] = this.cancelAnimationFrame;
|
|
251
|
+
scope['matchMedia'] = this.window.matchMedia;
|
|
252
|
+
Object.defineProperty(scope, 'devicePixelRatio', {
|
|
253
|
+
get: () => this.window.devicePixelRatio,
|
|
254
|
+
configurable: true,
|
|
255
|
+
});
|
|
256
|
+
const classes = installEventClasses(scope);
|
|
257
|
+
/* Node has a `navigator` of its own, behind a getter, so it is added to rather than replaced. */
|
|
258
|
+
const navigator = scope['navigator'];
|
|
259
|
+
if (navigator !== undefined) {
|
|
260
|
+
Object.defineProperty(navigator, 'userActivation', {
|
|
261
|
+
value: this.userActivation,
|
|
262
|
+
configurable: true,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return () => {
|
|
266
|
+
classes();
|
|
267
|
+
if (navigator !== undefined)
|
|
268
|
+
delete navigator.userActivation;
|
|
269
|
+
for (const [name, descriptor] of held) {
|
|
270
|
+
if (descriptor === undefined)
|
|
271
|
+
delete scope[name];
|
|
272
|
+
else
|
|
273
|
+
Object.defineProperty(scope, name, descriptor);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
lock(element) {
|
|
278
|
+
if (this.document.pointerLockElement === element)
|
|
279
|
+
return true;
|
|
280
|
+
if (this.locker !== null && !this.locker(true)) {
|
|
281
|
+
this.document.dispatchEvent(new Event('pointerlockerror', { bubbles: true }));
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
this.document.pointerLockElement = element;
|
|
285
|
+
this.changedLock(true);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
changedLock(locked) {
|
|
289
|
+
this.document.dispatchEvent(new Event('pointerlockchange', { bubbles: true }));
|
|
290
|
+
for (const listener of this.lockListeners)
|
|
291
|
+
listener(locked);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** `focus` and `focusin`, or `blur` and `focusout`: the first stays on the element, the second bubbles. */
|
|
295
|
+
function focusEvents(canvas, focused) {
|
|
296
|
+
canvas.dispatchEvent(new FocusEvent(focused ? 'focus' : 'blur'));
|
|
297
|
+
canvas.dispatchEvent(new FocusEvent(focused ? 'focusin' : 'focusout', { bubbles: true }));
|
|
298
|
+
}
|
|
299
|
+
/** A `DOMException`-shaped refusal: a browser's error name, with its message. */
|
|
300
|
+
function refusal(name, message) {
|
|
301
|
+
return Object.assign(new Error(message), { name });
|
|
302
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pointer capture: `setPointerCapture`, `releasePointerCapture` and `hasPointerCapture` on the
|
|
3
|
+
* canvas, and `gotpointercapture` and `lostpointercapture` when it changes.
|
|
4
|
+
*
|
|
5
|
+
* **As the Pointer Events specification processes it, and in the order Chrome sends it** — measured
|
|
6
|
+
* 2026-09-19 in Chrome 151 over the DevTools protocol. A capture asked for is pending, and
|
|
7
|
+
* `hasPointerCapture` already says so; it is taken, and `gotpointercapture` sent, just before that
|
|
8
|
+
* pointer's next event. A pointer's release lets it go at once, so `lostpointercapture` falls
|
|
9
|
+
* between the `mouseup` and the `click`. A mouse with no button down cannot be captured, and the
|
|
10
|
+
* request is dropped without a word; an id that is no pointer is refused as Chrome refuses it.
|
|
11
|
+
*
|
|
12
|
+
* **What capture moves here is when things are told, not where.** A browser retargets a captured
|
|
13
|
+
* pointer's events at the capturing element; this host has one element, which is every target
|
|
14
|
+
* already, and SDL keeps a pressed pointer to the window past its edge.
|
|
15
|
+
*/
|
|
16
|
+
import type { HostNode } from './domTree.ts';
|
|
17
|
+
import { type PointerInit } from './uiEvents.ts';
|
|
18
|
+
export declare class PointerCapture {
|
|
19
|
+
/** Each pointer a page can capture, and whether it is pressed — in contact, or a button down. */
|
|
20
|
+
private readonly pointers;
|
|
21
|
+
private readonly pending;
|
|
22
|
+
private readonly current;
|
|
23
|
+
add(pointerId: number, pressed: () => boolean): void;
|
|
24
|
+
remove(pointerId: number): void;
|
|
25
|
+
set(element: HostNode, pointerId: number): void;
|
|
26
|
+
release(element: HostNode, pointerId: number): void;
|
|
27
|
+
has(element: HostNode, pointerId: number): boolean;
|
|
28
|
+
/** Before a pointer's next event: a change of capture, told with that event's fields. */
|
|
29
|
+
process(pointerId: number, init: PointerInit): void;
|
|
30
|
+
/** After a pointer's up or cancel: its capture is let go, and told at once. */
|
|
31
|
+
releaseImplicitly(pointerId: number, init: PointerInit): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pointer capture: `setPointerCapture`, `releasePointerCapture` and `hasPointerCapture` on the
|
|
3
|
+
* canvas, and `gotpointercapture` and `lostpointercapture` when it changes.
|
|
4
|
+
*
|
|
5
|
+
* **As the Pointer Events specification processes it, and in the order Chrome sends it** — measured
|
|
6
|
+
* 2026-09-19 in Chrome 151 over the DevTools protocol. A capture asked for is pending, and
|
|
7
|
+
* `hasPointerCapture` already says so; it is taken, and `gotpointercapture` sent, just before that
|
|
8
|
+
* pointer's next event. A pointer's release lets it go at once, so `lostpointercapture` falls
|
|
9
|
+
* between the `mouseup` and the `click`. A mouse with no button down cannot be captured, and the
|
|
10
|
+
* request is dropped without a word; an id that is no pointer is refused as Chrome refuses it.
|
|
11
|
+
*
|
|
12
|
+
* **What capture moves here is when things are told, not where.** A browser retargets a captured
|
|
13
|
+
* pointer's events at the capturing element; this host has one element, which is every target
|
|
14
|
+
* already, and SDL keeps a pressed pointer to the window past its edge.
|
|
15
|
+
*/
|
|
16
|
+
import { PointerEvent } from './uiEvents.js';
|
|
17
|
+
export class PointerCapture {
|
|
18
|
+
/** Each pointer a page can capture, and whether it is pressed — in contact, or a button down. */
|
|
19
|
+
pointers = new Map();
|
|
20
|
+
pending = new Map();
|
|
21
|
+
current = new Map();
|
|
22
|
+
add(pointerId, pressed) {
|
|
23
|
+
this.pointers.set(pointerId, pressed);
|
|
24
|
+
}
|
|
25
|
+
remove(pointerId) {
|
|
26
|
+
this.pointers.delete(pointerId);
|
|
27
|
+
this.pending.delete(pointerId);
|
|
28
|
+
this.current.delete(pointerId);
|
|
29
|
+
}
|
|
30
|
+
set(element, pointerId) {
|
|
31
|
+
const pressed = this.pointers.get(pointerId);
|
|
32
|
+
if (pressed === undefined)
|
|
33
|
+
throw notFound('setPointerCapture');
|
|
34
|
+
if (pressed())
|
|
35
|
+
this.pending.set(pointerId, element);
|
|
36
|
+
}
|
|
37
|
+
release(element, pointerId) {
|
|
38
|
+
if (!this.pointers.has(pointerId))
|
|
39
|
+
throw notFound('releasePointerCapture');
|
|
40
|
+
if (this.pending.get(pointerId) === element)
|
|
41
|
+
this.pending.delete(pointerId);
|
|
42
|
+
}
|
|
43
|
+
has(element, pointerId) {
|
|
44
|
+
return this.pending.get(pointerId) === element;
|
|
45
|
+
}
|
|
46
|
+
/** Before a pointer's next event: a change of capture, told with that event's fields. */
|
|
47
|
+
process(pointerId, init) {
|
|
48
|
+
const was = this.current.get(pointerId);
|
|
49
|
+
const now = this.pending.get(pointerId);
|
|
50
|
+
if (was === now)
|
|
51
|
+
return;
|
|
52
|
+
if (now === undefined)
|
|
53
|
+
this.current.delete(pointerId);
|
|
54
|
+
else
|
|
55
|
+
this.current.set(pointerId, now);
|
|
56
|
+
const told = { ...init, pointerId, bubbles: true, cancelable: false };
|
|
57
|
+
was?.dispatchEvent(new PointerEvent('lostpointercapture', told));
|
|
58
|
+
now?.dispatchEvent(new PointerEvent('gotpointercapture', told));
|
|
59
|
+
}
|
|
60
|
+
/** After a pointer's up or cancel: its capture is let go, and told at once. */
|
|
61
|
+
releaseImplicitly(pointerId, init) {
|
|
62
|
+
this.pending.delete(pointerId);
|
|
63
|
+
this.process(pointerId, init);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function notFound(method) {
|
|
67
|
+
const message = `Failed to execute '${method}' on 'Element': No active pointer with the given id is found.`;
|
|
68
|
+
return Object.assign(new Error(message), { name: 'NotFoundError' });
|
|
69
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Putting the engine's frame in the window: one full-screen triangle, reading the canvas's texture.
|
|
3
|
+
*
|
|
4
|
+
* **A browser presents a canvas by itself and a host has to.** The engine draws into the canvas's
|
|
5
|
+
* current texture and stops; this copies that texture into the window's swap chain.
|
|
6
|
+
*
|
|
7
|
+
* **Decoded to linear on the way into an sRGB swap chain**, because Dawn's window renderer hands back
|
|
8
|
+
* `bgra8unorm-srgb`, and writing into one encodes. The engine has already graded and encoded its
|
|
9
|
+
* frame, so writing its bytes as they are would encode them a second time and every picture would
|
|
10
|
+
* come out paler. Decoding first puts the engine's own bytes on the screen, within a step of
|
|
11
|
+
* rounding. **The pixel gate reads the canvas texture, not the window**, so that rounding is on the
|
|
12
|
+
* screen and never in a comparison.
|
|
13
|
+
*/
|
|
14
|
+
/** Whether writing into `format` encodes, so the blit has to decode first. */
|
|
15
|
+
export declare function encodesOnWrite(format: GPUTextureFormat): boolean;
|
|
16
|
+
/** The blit's source, with the decode chosen for the swap chain it writes into. */
|
|
17
|
+
export declare function blitSource(target: GPUTextureFormat): string;
|
|
18
|
+
/** One pipeline, and one bind group a frame texture, made when they are first needed. */
|
|
19
|
+
export declare class Presenter {
|
|
20
|
+
private readonly device;
|
|
21
|
+
private readonly pipeline;
|
|
22
|
+
private source;
|
|
23
|
+
private group;
|
|
24
|
+
constructor(device: GPUDevice, target: GPUTextureFormat);
|
|
25
|
+
/** Draw `frame` into `view`, and submit. */
|
|
26
|
+
present(frame: GPUTexture, view: GPUTextureView): void;
|
|
27
|
+
}
|
package/dist/present.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Putting the engine's frame in the window: one full-screen triangle, reading the canvas's texture.
|
|
3
|
+
*
|
|
4
|
+
* **A browser presents a canvas by itself and a host has to.** The engine draws into the canvas's
|
|
5
|
+
* current texture and stops; this copies that texture into the window's swap chain.
|
|
6
|
+
*
|
|
7
|
+
* **Decoded to linear on the way into an sRGB swap chain**, because Dawn's window renderer hands back
|
|
8
|
+
* `bgra8unorm-srgb`, and writing into one encodes. The engine has already graded and encoded its
|
|
9
|
+
* frame, so writing its bytes as they are would encode them a second time and every picture would
|
|
10
|
+
* come out paler. Decoding first puts the engine's own bytes on the screen, within a step of
|
|
11
|
+
* rounding. **The pixel gate reads the canvas texture, not the window**, so that rounding is on the
|
|
12
|
+
* screen and never in a comparison.
|
|
13
|
+
*/
|
|
14
|
+
const BLIT_WGSL = /* wgsl */ `
|
|
15
|
+
@group(0) @binding(0) var frame: texture_2d<f32>;
|
|
16
|
+
|
|
17
|
+
@vertex
|
|
18
|
+
fn vertexMain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
|
|
19
|
+
let x = f32((index << 1u) & 2u);
|
|
20
|
+
let y = f32(index & 2u);
|
|
21
|
+
return vec4f(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn toLinear(encoded: vec3f) -> vec3f {
|
|
25
|
+
let low = encoded / 12.92;
|
|
26
|
+
let high = pow((encoded + 0.055) / 1.055, vec3f(2.4));
|
|
27
|
+
return select(high, low, encoded <= vec3f(0.04045));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@fragment
|
|
31
|
+
fn fragmentMain(@builtin(position) position: vec4f) -> @location(0) vec4f {
|
|
32
|
+
let size = vec2i(textureDimensions(frame)) - vec2i(1, 1);
|
|
33
|
+
let texel = textureLoad(frame, min(vec2i(position.xy), size), 0);
|
|
34
|
+
return vec4f(DECODE(texel.rgb), 1.0);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
/** Whether writing into `format` encodes, so the blit has to decode first. */
|
|
38
|
+
export function encodesOnWrite(format) {
|
|
39
|
+
return format.endsWith('-srgb');
|
|
40
|
+
}
|
|
41
|
+
/** The blit's source, with the decode chosen for the swap chain it writes into. */
|
|
42
|
+
export function blitSource(target) {
|
|
43
|
+
return BLIT_WGSL.replace('DECODE(texel.rgb)', encodesOnWrite(target) ? 'toLinear(texel.rgb)' : 'texel.rgb');
|
|
44
|
+
}
|
|
45
|
+
/** One pipeline, and one bind group a frame texture, made when they are first needed. */
|
|
46
|
+
export class Presenter {
|
|
47
|
+
device;
|
|
48
|
+
pipeline;
|
|
49
|
+
source = null;
|
|
50
|
+
group = null;
|
|
51
|
+
constructor(device, target) {
|
|
52
|
+
this.device = device;
|
|
53
|
+
const module = device.createShaderModule({ label: 'native present', code: blitSource(target) });
|
|
54
|
+
this.pipeline = device.createRenderPipeline({
|
|
55
|
+
label: 'native present',
|
|
56
|
+
layout: 'auto',
|
|
57
|
+
vertex: { module, entryPoint: 'vertexMain' },
|
|
58
|
+
fragment: { module, entryPoint: 'fragmentMain', targets: [{ format: target }] },
|
|
59
|
+
primitive: { topology: 'triangle-list' },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Draw `frame` into `view`, and submit. */
|
|
63
|
+
present(frame, view) {
|
|
64
|
+
if (this.source !== frame) {
|
|
65
|
+
this.source = frame;
|
|
66
|
+
this.group = this.device.createBindGroup({
|
|
67
|
+
label: 'native present',
|
|
68
|
+
layout: this.pipeline.getBindGroupLayout(0),
|
|
69
|
+
entries: [{ binding: 0, resource: frame.createView() }],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const encoder = this.device.createCommandEncoder({ label: 'native present' });
|
|
73
|
+
const pass = encoder.beginRenderPass({
|
|
74
|
+
label: 'native present',
|
|
75
|
+
colorAttachments: [{ view, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] }],
|
|
76
|
+
});
|
|
77
|
+
pass.setPipeline(this.pipeline);
|
|
78
|
+
pass.setBindGroup(0, this.group);
|
|
79
|
+
pass.draw(3);
|
|
80
|
+
pass.end();
|
|
81
|
+
this.device.queue.submit([encoder.finish()]);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/rawPad.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A joystick SDL has no controller mapping for — a flight stick, a wheel, an arcade panel, a pad
|
|
3
|
+
* too new or too rare — as Chrome shows a page one it cannot map: `mapping` empty, the device's own
|
|
4
|
+
* axes and buttons in the device's own order, and a name without "STANDARD GAMEPAD".
|
|
5
|
+
*
|
|
6
|
+
* **A hat is two axes**, as a browser on Linux reads one: the device reports it as a pair of
|
|
7
|
+
* absolute axes that are −1, 0 or 1, and SDL folds the pair into a direction, which is unfolded
|
|
8
|
+
* here — left and up negative. They follow the joystick's own axes, which is where their codes put
|
|
9
|
+
* them on a typical device.
|
|
10
|
+
*
|
|
11
|
+
* What it gives up: a device whose other axes sort after its hat in the kernel's numbering has
|
|
12
|
+
* them in a different order than Chrome gives; nothing here has been checked against a device, as
|
|
13
|
+
* there was none to hand.
|
|
14
|
+
*/
|
|
15
|
+
import { type DualRumble, type Rumbler } from './dualRumble.ts';
|
|
16
|
+
import { type HostButton } from './standardPad.ts';
|
|
17
|
+
export interface JoystickDevice {
|
|
18
|
+
readonly id: number;
|
|
19
|
+
readonly name: string | null;
|
|
20
|
+
readonly vendor: number | null;
|
|
21
|
+
readonly product: number | null;
|
|
22
|
+
}
|
|
23
|
+
/** What this reads and drives of one of SDL's open joysticks. */
|
|
24
|
+
export interface JoystickInstance extends Rumbler {
|
|
25
|
+
readonly device: JoystickDevice;
|
|
26
|
+
readonly axes: readonly number[];
|
|
27
|
+
readonly hats: readonly string[];
|
|
28
|
+
readonly buttons: readonly boolean[];
|
|
29
|
+
readonly hasRumble: boolean;
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare class RawPad {
|
|
33
|
+
readonly index: number;
|
|
34
|
+
readonly source: JoystickInstance;
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly mapping = "";
|
|
37
|
+
connected: boolean;
|
|
38
|
+
timestamp: number;
|
|
39
|
+
readonly buttons: HostButton[];
|
|
40
|
+
readonly axes: number[];
|
|
41
|
+
readonly vibrationActuator: DualRumble | null;
|
|
42
|
+
constructor(index: number, source: JoystickInstance);
|
|
43
|
+
read(now: number): void;
|
|
44
|
+
}
|
package/dist/rawPad.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A joystick SDL has no controller mapping for — a flight stick, a wheel, an arcade panel, a pad
|
|
3
|
+
* too new or too rare — as Chrome shows a page one it cannot map: `mapping` empty, the device's own
|
|
4
|
+
* axes and buttons in the device's own order, and a name without "STANDARD GAMEPAD".
|
|
5
|
+
*
|
|
6
|
+
* **A hat is two axes**, as a browser on Linux reads one: the device reports it as a pair of
|
|
7
|
+
* absolute axes that are −1, 0 or 1, and SDL folds the pair into a direction, which is unfolded
|
|
8
|
+
* here — left and up negative. They follow the joystick's own axes, which is where their codes put
|
|
9
|
+
* them on a typical device.
|
|
10
|
+
*
|
|
11
|
+
* What it gives up: a device whose other axes sort after its hat in the kernel's numbering has
|
|
12
|
+
* them in a different order than Chrome gives; nothing here has been checked against a device, as
|
|
13
|
+
* there was none to hand.
|
|
14
|
+
*/
|
|
15
|
+
import { dualRumble } from './dualRumble.js';
|
|
16
|
+
import { hex } from './standardPad.js';
|
|
17
|
+
/** A hat's direction as the two axes it came from: x, then y, left and up negative. */
|
|
18
|
+
const HAT = {
|
|
19
|
+
centered: [0, 0],
|
|
20
|
+
up: [0, -1],
|
|
21
|
+
down: [0, 1],
|
|
22
|
+
left: [-1, 0],
|
|
23
|
+
right: [1, 0],
|
|
24
|
+
leftup: [-1, -1],
|
|
25
|
+
rightup: [1, -1],
|
|
26
|
+
leftdown: [-1, 1],
|
|
27
|
+
rightdown: [1, 1],
|
|
28
|
+
};
|
|
29
|
+
export class RawPad {
|
|
30
|
+
index;
|
|
31
|
+
source;
|
|
32
|
+
id;
|
|
33
|
+
mapping = '';
|
|
34
|
+
connected = true;
|
|
35
|
+
timestamp = 0;
|
|
36
|
+
buttons;
|
|
37
|
+
axes;
|
|
38
|
+
vibrationActuator;
|
|
39
|
+
constructor(index, source) {
|
|
40
|
+
this.index = index;
|
|
41
|
+
this.source = source;
|
|
42
|
+
const { name, vendor, product } = source.device;
|
|
43
|
+
this.id = `${name ?? 'Joystick'} (Vendor: ${hex(vendor)} Product: ${hex(product)})`;
|
|
44
|
+
this.buttons = source.buttons.map(() => ({ pressed: false, touched: false, value: 0 }));
|
|
45
|
+
this.axes = Array.from({ length: source.axes.length + source.hats.length * 2 }, () => 0);
|
|
46
|
+
this.vibrationActuator = source.hasRumble ? dualRumble(source) : null;
|
|
47
|
+
}
|
|
48
|
+
read(now) {
|
|
49
|
+
const { axes, hats, buttons } = this.source;
|
|
50
|
+
for (let at = 0; at < axes.length; at += 1)
|
|
51
|
+
this.axes[at] = axes[at];
|
|
52
|
+
for (let at = 0; at < hats.length; at += 1) {
|
|
53
|
+
const [x, y] = HAT[hats[at]] ?? [0, 0];
|
|
54
|
+
this.axes[axes.length + at * 2] = x;
|
|
55
|
+
this.axes[axes.length + at * 2 + 1] = y;
|
|
56
|
+
}
|
|
57
|
+
for (let at = 0; at < buttons.length; at += 1) {
|
|
58
|
+
const button = this.buttons[at];
|
|
59
|
+
button.pressed = buttons[at] === true;
|
|
60
|
+
button.touched = button.pressed;
|
|
61
|
+
button.value = button.pressed ? 1 : 0;
|
|
62
|
+
}
|
|
63
|
+
this.timestamp = now;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame as the engine left it in the canvas, read back to the CPU for the pixel gate.
|
|
3
|
+
*
|
|
4
|
+
* **The canvas texture, not the window**: the window is Dawn's `bgra8unorm-srgb` swap chain, which
|
|
5
|
+
* the present decodes into, and a comparison against it would compare that round trip. The canvas
|
|
6
|
+
* texture carries `COPY_SRC` for exactly this (see `canvas.ts`).
|
|
7
|
+
*/
|
|
8
|
+
export interface Frame {
|
|
9
|
+
readonly width: number;
|
|
10
|
+
readonly height: number;
|
|
11
|
+
/** Top row first, four bytes a pixel, in the canvas's own channel order. */
|
|
12
|
+
readonly rgba: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
/** Rows are copied at a stride of 256 bytes, which `copyTextureToBuffer` requires. */
|
|
15
|
+
export declare function paddedRow(width: number): number;
|
|
16
|
+
/** The padded rows, packed. */
|
|
17
|
+
export declare function unpad(bytes: Uint8Array, width: number, height: number): Uint8Array;
|
|
18
|
+
export declare function readFrame(context: GPUCanvasContext): Promise<Frame>;
|