@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.
Files changed (134) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +58 -0
  4. package/dist/activation.d.ts +26 -0
  5. package/dist/activation.js +38 -0
  6. package/dist/alphaCopy.d.ts +36 -0
  7. package/dist/alphaCopy.js +178 -0
  8. package/dist/audio.d.ts +55 -0
  9. package/dist/audio.js +193 -0
  10. package/dist/audioParams.d.ts +25 -0
  11. package/dist/audioParams.js +128 -0
  12. package/dist/bridge.d.ts +70 -0
  13. package/dist/bridge.js +170 -0
  14. package/dist/canvas.d.ts +97 -0
  15. package/dist/canvas.js +197 -0
  16. package/dist/canvasStyle.d.ts +23 -0
  17. package/dist/canvasStyle.js +79 -0
  18. package/dist/device.d.ts +34 -0
  19. package/dist/device.js +144 -0
  20. package/dist/dialogs.d.ts +23 -0
  21. package/dist/dialogs.js +85 -0
  22. package/dist/domEvents.d.ts +37 -0
  23. package/dist/domEvents.js +145 -0
  24. package/dist/domTree.d.ts +34 -0
  25. package/dist/domTree.js +194 -0
  26. package/dist/dualRumble.d.ts +19 -0
  27. package/dist/dualRumble.js +47 -0
  28. package/dist/eventClasses.d.ts +12 -0
  29. package/dist/eventClasses.js +35 -0
  30. package/dist/files.d.ts +16 -0
  31. package/dist/files.js +59 -0
  32. package/dist/fullscreen.d.ts +33 -0
  33. package/dist/fullscreen.js +65 -0
  34. package/dist/game.d.ts +25 -0
  35. package/dist/game.js +38 -0
  36. package/dist/gamepads.d.ts +65 -0
  37. package/dist/gamepads.js +178 -0
  38. package/dist/globals.d.ts +24 -0
  39. package/dist/globals.js +37 -0
  40. package/dist/gpu.d.ts +26 -0
  41. package/dist/gpu.js +47 -0
  42. package/dist/images.d.ts +50 -0
  43. package/dist/images.js +111 -0
  44. package/dist/index.d.ts +23 -0
  45. package/dist/index.js +21 -0
  46. package/dist/keyCodes.d.ts +30 -0
  47. package/dist/keyCodes.js +205 -0
  48. package/dist/keyEvents.d.ts +54 -0
  49. package/dist/keyEvents.js +117 -0
  50. package/dist/mediaQueries.d.ts +62 -0
  51. package/dist/mediaQueries.js +152 -0
  52. package/dist/mouseEvents.d.ts +96 -0
  53. package/dist/mouseEvents.js +287 -0
  54. package/dist/page.d.ts +118 -0
  55. package/dist/page.js +302 -0
  56. package/dist/pointerCapture.d.ts +32 -0
  57. package/dist/pointerCapture.js +69 -0
  58. package/dist/present.d.ts +27 -0
  59. package/dist/present.js +83 -0
  60. package/dist/rawPad.d.ts +44 -0
  61. package/dist/rawPad.js +65 -0
  62. package/dist/readback.d.ts +18 -0
  63. package/dist/readback.js +46 -0
  64. package/dist/runtime.d.ts +69 -0
  65. package/dist/runtime.js +103 -0
  66. package/dist/standardPad.d.ts +49 -0
  67. package/dist/standardPad.js +80 -0
  68. package/dist/textHost.d.ts +37 -0
  69. package/dist/textHost.js +43 -0
  70. package/dist/threads.d.ts +27 -0
  71. package/dist/threads.js +59 -0
  72. package/dist/tileFiles.d.ts +16 -0
  73. package/dist/tileFiles.js +36 -0
  74. package/dist/touchEvents.d.ts +67 -0
  75. package/dist/touchEvents.js +210 -0
  76. package/dist/uiEvents.d.ts +157 -0
  77. package/dist/uiEvents.js +206 -0
  78. package/dist/uiTouch.d.ts +59 -0
  79. package/dist/uiTouch.js +75 -0
  80. package/dist/window.d.ts +71 -0
  81. package/dist/window.js +164 -0
  82. package/dist/windowMode.d.ts +19 -0
  83. package/dist/windowMode.js +15 -0
  84. package/dist/windowTitle.d.ts +18 -0
  85. package/dist/windowTitle.js +33 -0
  86. package/dist/workerScope.mjs +48 -0
  87. package/licenses/GPL-3.0.txt +674 -0
  88. package/package.json +83 -0
  89. package/src/activation.ts +42 -0
  90. package/src/alphaCopy.ts +201 -0
  91. package/src/audio.ts +246 -0
  92. package/src/audioParams.ts +150 -0
  93. package/src/bridge.ts +230 -0
  94. package/src/canvas.ts +227 -0
  95. package/src/canvasStyle.ts +89 -0
  96. package/src/device.ts +167 -0
  97. package/src/dialogs.ts +98 -0
  98. package/src/domEvents.ts +170 -0
  99. package/src/domTree.ts +215 -0
  100. package/src/drs.d.ts +9 -0
  101. package/src/dualRumble.ts +58 -0
  102. package/src/eventClasses.ts +46 -0
  103. package/src/files.ts +61 -0
  104. package/src/fixtures/grey.jpg +0 -0
  105. package/src/fixtures/rgb.png +0 -0
  106. package/src/fixtures/tone.opus +0 -0
  107. package/src/fullscreen.ts +71 -0
  108. package/src/game.ts +53 -0
  109. package/src/gamepads.ts +195 -0
  110. package/src/globals.ts +39 -0
  111. package/src/gpu.ts +59 -0
  112. package/src/images.ts +148 -0
  113. package/src/index.ts +24 -0
  114. package/src/keyCodes.ts +203 -0
  115. package/src/keyEvents.ts +135 -0
  116. package/src/mediaQueries.ts +173 -0
  117. package/src/mouseEvents.ts +328 -0
  118. package/src/page.ts +337 -0
  119. package/src/pointerCapture.ts +73 -0
  120. package/src/present.ts +92 -0
  121. package/src/rawPad.ts +85 -0
  122. package/src/readback.ts +61 -0
  123. package/src/runtime.ts +162 -0
  124. package/src/standardPad.ts +126 -0
  125. package/src/textHost.ts +66 -0
  126. package/src/threads.ts +66 -0
  127. package/src/tileFiles.ts +39 -0
  128. package/src/touchEvents.ts +238 -0
  129. package/src/uiEvents.ts +268 -0
  130. package/src/uiTouch.ts +97 -0
  131. package/src/window.ts +192 -0
  132. package/src/windowMode.ts +24 -0
  133. package/src/windowTitle.ts +36 -0
  134. package/src/workerScope.mjs +48 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Open and save dialogs from the desktop, since SDL 2 gives a window none.
3
+ *
4
+ * **The desktop's own dialog program, asked and waited for**: zenity, which GNOME and the GTK
5
+ * desktops carry, and kdialog where zenity is not installed. What comes back is what the desktop
6
+ * shell hands a game (`main/ipc.ts`): a picked file's name and bytes, `null` for a cancelled pick,
7
+ * and `false` for a cancelled save — a cancellation being exit code 1 for both programs.
8
+ *
9
+ * **A filter is the extensions the game named**, `.json` becoming `*.json`; a MIME type has no
10
+ * pattern either program takes, so it is left out rather than guessed at.
11
+ *
12
+ * What it gives up: Windows and macOS, which carry neither program and are Task 7's; the desktop
13
+ * portal, which is what a sandboxed Linux build would need and answers over D-Bus signals rather
14
+ * than an exit code; and where neither program is installed, `desktopFiles` answers null and the
15
+ * bridge reports every pick cancelled.
16
+ */
17
+ import { spawn } from 'node:child_process';
18
+ import { accessSync, constants } from 'node:fs';
19
+ import { readFile, writeFile } from 'node:fs/promises';
20
+ import { basename, delimiter, join } from 'node:path';
21
+ const spawnRunner = (command, args) => new Promise((resolve, reject) => {
22
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'ignore'] });
23
+ let stdout = '';
24
+ child.stdout.setEncoding('utf8');
25
+ child.stdout.on('data', (chunk) => {
26
+ stdout += chunk;
27
+ });
28
+ child.once('error', reject);
29
+ child.once('close', (code) => resolve({ code: code ?? 1, stdout }));
30
+ });
31
+ /** Whether `name` is an executable on the search path. */
32
+ function onPath(name) {
33
+ for (const directory of (process.env['PATH'] ?? '').split(delimiter)) {
34
+ try {
35
+ accessSync(join(directory, name), constants.X_OK);
36
+ return true;
37
+ }
38
+ catch {
39
+ /* Not in this one. */
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ const patternsOf = (accept) => accept.filter((entry) => entry.startsWith('.')).map((extension) => `*${extension}`);
45
+ /** The chosen path, or null when the person cancelled; anything else is a fault and says so. */
46
+ function chosen(command, answer) {
47
+ if (answer.code === 0)
48
+ return answer.stdout.replace(/\r?\n$/, '');
49
+ if (answer.code !== 1)
50
+ console.warn(`[driftengine] ${command} ended with code ${answer.code}`);
51
+ return null;
52
+ }
53
+ export function desktopFiles(available = onPath, run = spawnRunner) {
54
+ const command = available('zenity') ? 'zenity' : available('kdialog') ? 'kdialog' : null;
55
+ if (command === null)
56
+ return null;
57
+ const zenity = command === 'zenity';
58
+ return {
59
+ async openFile(accept) {
60
+ const patterns = patternsOf(accept);
61
+ const args = zenity
62
+ ? [
63
+ '--file-selection',
64
+ ...(patterns.length > 0
65
+ ? [`--file-filter=${patterns.join(' ')} | ${patterns.join(' ')}`]
66
+ : []),
67
+ ]
68
+ : ['--getopenfilename', '.', ...(patterns.length > 0 ? [patterns.join(' ')] : [])];
69
+ const path = chosen(command, await run(command, args));
70
+ if (path === null)
71
+ return null;
72
+ return { name: basename(path), bytes: new Uint8Array(await readFile(path)) };
73
+ },
74
+ async saveFile(name, bytes) {
75
+ const args = zenity
76
+ ? ['--file-selection', '--save', `--filename=${name}`]
77
+ : ['--getsavefilename', name];
78
+ const path = chosen(command, await run(command, args));
79
+ if (path === null)
80
+ return false;
81
+ await writeFile(path, bytes);
82
+ return true;
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * What SDL tells a window, as the events a page hears: keys, the mouse, the wheel, focus,
3
+ * visibility, resizing and closing.
4
+ *
5
+ * **The engine's input is not changed for this host.** `InputSource` listens on `window`,
6
+ * `document` and its canvas for the events a browser sends, and so do the scenes, so they are sent
7
+ * here in that shape: each the browser's class (`uiEvents.ts`), a key by its `code` and with the
8
+ * character the layout typed (`keyEvents.ts`), a button in the browser's numbering, the wheel in
9
+ * the browser's direction. Each is dispatched at the canvas, or at what has the focus, and the tree
10
+ * (`domTree.ts`) captures and bubbles it as a browser's does.
11
+ *
12
+ * The mouse is `mouseEvents.ts`, which says what Chrome sends for it; keys are `keyEvents.ts`.
13
+ *
14
+ * Fingers are `touchEvents.ts`.
15
+ */
16
+ import type { NativeCanvas } from './canvas.ts';
17
+ import { type EventMouse } from './mouseEvents.ts';
18
+ import type { HostPage } from './page.ts';
19
+ export type { EventMouse } from './mouseEvents.ts';
20
+ /** What this reads of an SDL window: its events, where it is on the screen, and its size. */
21
+ export interface EventWindow {
22
+ on(type: string, listener: (event: never) => void): unknown;
23
+ off(type: string, listener: (event: never) => void): unknown;
24
+ readonly x: number;
25
+ readonly y: number;
26
+ readonly width: number;
27
+ readonly height: number;
28
+ /** Whether SDL has the window fullscreen, and the way to ask; a window without them has none. */
29
+ readonly fullscreen?: boolean;
30
+ setFullscreen?(on: boolean): void;
31
+ }
32
+ export interface EventOptions {
33
+ /** The clock clicks are counted against, in milliseconds. */
34
+ readonly now?: () => number;
35
+ }
36
+ /** Connect `window`'s events to `page` and `canvas`, and hand back what disconnects them. */
37
+ export declare function connectDomEvents(window: EventWindow, mouse: EventMouse, page: HostPage, canvas: NativeCanvas, options?: EventOptions): () => void;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * What SDL tells a window, as the events a page hears: keys, the mouse, the wheel, focus,
3
+ * visibility, resizing and closing.
4
+ *
5
+ * **The engine's input is not changed for this host.** `InputSource` listens on `window`,
6
+ * `document` and its canvas for the events a browser sends, and so do the scenes, so they are sent
7
+ * here in that shape: each the browser's class (`uiEvents.ts`), a key by its `code` and with the
8
+ * character the layout typed (`keyEvents.ts`), a button in the browser's numbering, the wheel in
9
+ * the browser's direction. Each is dispatched at the canvas, or at what has the focus, and the tree
10
+ * (`domTree.ts`) captures and bubbles it as a browser's does.
11
+ *
12
+ * The mouse is `mouseEvents.ts`, which says what Chrome sends for it; keys are `keyEvents.ts`.
13
+ *
14
+ * Fingers are `touchEvents.ts`.
15
+ */
16
+ import { KeyEvents } from './keyEvents.js';
17
+ import { MouseModel } from './mouseEvents.js';
18
+ import { TouchModel } from './touchEvents.js';
19
+ const ESCAPE = 41;
20
+ /** The modifier keys by scancode, both sides of each, as the bits `modifiers` keeps. */
21
+ const MODIFIER = new Map([
22
+ [224, 1],
23
+ [228, 1],
24
+ [225, 2],
25
+ [229, 2],
26
+ [226, 4],
27
+ [230, 4],
28
+ [227, 8],
29
+ [231, 8],
30
+ ]);
31
+ /** Connect `window`'s events to `page` and `canvas`, and hand back what disconnects them. */
32
+ export function connectDomEvents(window, mouse, page, canvas, options = {}) {
33
+ const keys = new KeyEvents(() => page.keyTarget(), () => page.userActivation.notify());
34
+ /*
35
+ * **Leaving puts the window back as it was**, not into a window: one already fullscreen before the
36
+ * page asked — a game packaged fullscreen, or put there by the shell's bridge — stays so, as a
37
+ * browser under F11 stays fullscreen when a page's element leaves.
38
+ */
39
+ let before = false;
40
+ page.setFullscreener((on) => {
41
+ if (window.setFullscreen === undefined)
42
+ return false;
43
+ try {
44
+ if (on)
45
+ before = window.fullscreen === true;
46
+ window.setFullscreen(on || before);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ });
53
+ /* Which modifiers are down, so a pointer event can say, as a browser's does. */
54
+ let modifiers = 0;
55
+ const held = () => ({
56
+ ctrlKey: (modifiers & 1) !== 0,
57
+ shiftKey: (modifiers & 2) !== 0,
58
+ altKey: (modifiers & 4) !== 0,
59
+ metaKey: (modifiers & 8) !== 0,
60
+ });
61
+ const now = options.now ?? (() => performance.now());
62
+ const pointer = new MouseModel(window, mouse, page, canvas, held, now);
63
+ const fingers = new TouchModel(window, page, canvas, held, now);
64
+ canvas.cursorer = (css) => pointer.cursor(css);
65
+ /* SDL makes a mouse of every finger, which the finger's own events have already said. */
66
+ const mouseOnly = (handle) => (event) => {
67
+ if (event.touch !== true)
68
+ handle(event);
69
+ };
70
+ const listeners = [
71
+ [
72
+ 'keyDown',
73
+ (event) => {
74
+ /* Escape is the browser's before it is the page's: it lets go of the lock and fullscreen. */
75
+ if (event.scancode === ESCAPE && pointer.isLocked)
76
+ page.unlock();
77
+ if (event.scancode === ESCAPE)
78
+ void page.document.exitFullscreen().catch(() => undefined);
79
+ modifiers |= MODIFIER.get(event.scancode) ?? 0;
80
+ keys.down(event);
81
+ },
82
+ ],
83
+ ['textInput', ({ text }) => keys.text(text)],
84
+ [
85
+ 'keyUp',
86
+ (event) => {
87
+ modifiers &= ~(MODIFIER.get(event.scancode) ?? 0);
88
+ keys.up(event);
89
+ },
90
+ ],
91
+ ['mouseMove', mouseOnly((event) => pointer.move(event))],
92
+ ['mouseButtonDown', mouseOnly((event) => pointer.down(event))],
93
+ ['mouseButtonUp', mouseOnly((event) => pointer.up(event))],
94
+ ['mouseWheel', mouseOnly((event) => pointer.wheel(event))],
95
+ ['fingerDown', (event) => fingers.down(event)],
96
+ ['fingerMove', (event) => fingers.move(event)],
97
+ ['fingerUp', (event) => fingers.up(event)],
98
+ ['hover', () => pointer.hover()],
99
+ ['leave', () => pointer.leave()],
100
+ ['focus', () => page.setFocused(true)],
101
+ [
102
+ 'blur',
103
+ () => {
104
+ page.unlock();
105
+ /* A key released in another window is never heard here. */
106
+ modifiers = 0;
107
+ page.setFocused(false);
108
+ },
109
+ ],
110
+ ['minimize', () => page.setVisible(false)],
111
+ ['hide', () => page.setVisible(false)],
112
+ ['restore', () => page.setVisible(true)],
113
+ ['show', () => page.setVisible(true)],
114
+ ['maximize', () => page.setVisible(true)],
115
+ /* After the host's own listener has resized the canvas, which it registered first. */
116
+ [
117
+ 'resize',
118
+ () => {
119
+ /* Taken out of fullscreen by something other than the page: the element leaves with it. */
120
+ if (window.fullscreen === false)
121
+ page.fullscreen.lost();
122
+ page.window.dispatchEvent(new Event('resize'));
123
+ /* A query about the window's size or shape may now answer otherwise. */
124
+ page.media.changed();
125
+ },
126
+ ],
127
+ ['beforeClose', () => page.window.dispatchEvent(new Event('beforeunload'))],
128
+ ];
129
+ /* A key held for its text is sent before anything that happened after it. */
130
+ for (const entry of listeners) {
131
+ const [type, listener] = entry;
132
+ if (type === 'keyDown' || type === 'textInput')
133
+ continue;
134
+ entry[1] = (event) => {
135
+ keys.flush();
136
+ listener(event);
137
+ };
138
+ }
139
+ for (const [type, listener] of listeners)
140
+ window.on(type, listener);
141
+ return () => {
142
+ for (const [type, listener] of listeners)
143
+ window.off(type, listener);
144
+ };
145
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The three nodes a host's page has — window, document and canvas, each the parent of the next —
3
+ * and a browser's dispatch across them: capture from the window down, the target, then bubbling
4
+ * back up.
5
+ *
6
+ * **Node's `EventTarget` has no tree**, so an event sent to each node in turn reaches a window's
7
+ * capture listener last rather than first, tells a listener on the window that the window was its
8
+ * target, and lets a passive listener cancel it. Each of those is a page behaving differently here
9
+ * than in Chrome — the engine's own `requestFullscreenOnGesture` listens on the window in the
10
+ * capture phase — so dispatch is the DOM's, for the one shape of tree this host has.
11
+ *
12
+ * **Event handler attributes are listeners**, as the HTML specification makes them: `onclick`
13
+ * assigned is added to the list at that moment, keeps its place when reassigned, and a handler
14
+ * returning `false` cancels the event. A page reached for them — `canvas.ondblclick` — and on a
15
+ * bare `EventTarget` an assignment is a property nothing reads.
16
+ *
17
+ * What it gives up: no shadow roots, no `composed`, and no retargeting, none of which a canvas
18
+ * with nothing inside it can need.
19
+ */
20
+ /** The `on…` attributes for `Types`, as a class declares them for the checker. */
21
+ export type Handlers<Types extends string> = {
22
+ [Type in Types as `on${Type}`]: ((event: Event) => unknown) | null;
23
+ };
24
+ export declare class HostNode implements EventTarget {
25
+ parentNode: HostNode | null;
26
+ private readonly listeners;
27
+ private readonly handlers;
28
+ addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
29
+ removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
30
+ dispatchEvent(event: Event): boolean;
31
+ /** `on<type>`, for each type given: a listener held in place, replaced by assignment. */
32
+ protected static handles(prototype: object, types: readonly string[]): void;
33
+ private invoke;
34
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The three nodes a host's page has — window, document and canvas, each the parent of the next —
3
+ * and a browser's dispatch across them: capture from the window down, the target, then bubbling
4
+ * back up.
5
+ *
6
+ * **Node's `EventTarget` has no tree**, so an event sent to each node in turn reaches a window's
7
+ * capture listener last rather than first, tells a listener on the window that the window was its
8
+ * target, and lets a passive listener cancel it. Each of those is a page behaving differently here
9
+ * than in Chrome — the engine's own `requestFullscreenOnGesture` listens on the window in the
10
+ * capture phase — so dispatch is the DOM's, for the one shape of tree this host has.
11
+ *
12
+ * **Event handler attributes are listeners**, as the HTML specification makes them: `onclick`
13
+ * assigned is added to the list at that moment, keeps its place when reassigned, and a handler
14
+ * returning `false` cancels the event. A page reached for them — `canvas.ondblclick` — and on a
15
+ * bare `EventTarget` an assignment is a property nothing reads.
16
+ *
17
+ * What it gives up: no shadow roots, no `composed`, and no retargeting, none of which a canvas
18
+ * with nothing inside it can need.
19
+ */
20
+ const CAPTURING = 1;
21
+ const AT_TARGET = 2;
22
+ const BUBBLING = 3;
23
+ const dispatches = new WeakMap();
24
+ function optionsOf(options) {
25
+ if (typeof options === 'boolean')
26
+ return { capture: options, once: false, passive: false };
27
+ return {
28
+ capture: options?.capture === true,
29
+ once: options?.once === true,
30
+ passive: options?.passive === true,
31
+ signal: options?.signal,
32
+ };
33
+ }
34
+ /** Give `event` the accessors a dispatch answers, once; Node's own are read-only slots. */
35
+ function adopt(event, dispatch) {
36
+ const known = dispatches.has(event);
37
+ dispatches.set(event, dispatch);
38
+ if (known)
39
+ return;
40
+ const own = (name, get) => Object.defineProperty(event, name, { get, configurable: true });
41
+ const now = () => dispatches.get(event);
42
+ own('target', () => now().target);
43
+ own('srcElement', () => now().target);
44
+ own('currentTarget', () => now().current);
45
+ own('eventPhase', () => now().phase);
46
+ Object.defineProperty(event, 'composedPath', {
47
+ value: () => (now().current === null ? [] : [...now().path]),
48
+ configurable: true,
49
+ });
50
+ const stop = event.stopPropagation.bind(event);
51
+ const stopNow = event.stopImmediatePropagation.bind(event);
52
+ Object.defineProperty(event, 'stopPropagation', {
53
+ value: () => {
54
+ now().stopped = true;
55
+ stop();
56
+ },
57
+ configurable: true,
58
+ });
59
+ Object.defineProperty(event, 'stopImmediatePropagation', {
60
+ value: () => {
61
+ now().stopped = true;
62
+ now().stoppedNow = true;
63
+ stopNow();
64
+ },
65
+ configurable: true,
66
+ });
67
+ }
68
+ export class HostNode {
69
+ parentNode = null;
70
+ listeners = new Map();
71
+ handlers = new Map();
72
+ addEventListener(type, callback, options) {
73
+ if (callback === null)
74
+ return;
75
+ const { capture, once, passive, signal } = optionsOf(options);
76
+ if (signal?.aborted === true)
77
+ return;
78
+ const list = this.listeners.get(type) ?? [];
79
+ if (list.some((l) => l.callback === callback && l.capture === capture))
80
+ return;
81
+ list.push({ callback, capture, once, passive, removed: false });
82
+ this.listeners.set(type, list);
83
+ signal?.addEventListener('abort', () => this.removeEventListener(type, callback, capture));
84
+ }
85
+ removeEventListener(type, callback, options) {
86
+ const capture = typeof options === 'boolean' ? options : options?.capture === true;
87
+ const list = this.listeners.get(type);
88
+ const at = list?.findIndex((l) => l.callback === callback && l.capture === capture) ?? -1;
89
+ if (list === undefined || at < 0)
90
+ return;
91
+ list[at].removed = true;
92
+ list.splice(at, 1);
93
+ }
94
+ dispatchEvent(event) {
95
+ const path = [];
96
+ for (let node = this; node !== null; node = node.parentNode)
97
+ path.push(node);
98
+ const dispatch = {
99
+ target: this,
100
+ current: null,
101
+ phase: 0,
102
+ path,
103
+ stopped: false,
104
+ stoppedNow: false,
105
+ };
106
+ adopt(event, dispatch);
107
+ for (let at = path.length - 1; at > 0 && !dispatch.stopped; at -= 1) {
108
+ path[at].invoke(event, dispatch, CAPTURING, true);
109
+ }
110
+ if (!dispatch.stopped)
111
+ this.invoke(event, dispatch, AT_TARGET, true);
112
+ if (!dispatch.stopped)
113
+ this.invoke(event, dispatch, AT_TARGET, false);
114
+ for (let at = 1; at < path.length && event.bubbles && !dispatch.stopped; at += 1) {
115
+ path[at].invoke(event, dispatch, BUBBLING, false);
116
+ }
117
+ dispatch.current = null;
118
+ dispatch.phase = 0;
119
+ return !event.defaultPrevented;
120
+ }
121
+ /** `on<type>`, for each type given: a listener held in place, replaced by assignment. */
122
+ static handles(prototype, types) {
123
+ for (const type of types) {
124
+ Object.defineProperty(prototype, `on${type}`, {
125
+ configurable: true,
126
+ get() {
127
+ return this.handlers.get(type)?.value ?? null;
128
+ },
129
+ set(value) {
130
+ const held = this.handlers.get(type);
131
+ if (typeof value !== 'function') {
132
+ if (held !== undefined)
133
+ this.removeEventListener(type, held.listener);
134
+ this.handlers.delete(type);
135
+ return;
136
+ }
137
+ if (held !== undefined) {
138
+ held.value = value;
139
+ return;
140
+ }
141
+ const slot = {
142
+ value,
143
+ listener: (event) => {
144
+ const result = slot.value.call(this, event);
145
+ if (result === false)
146
+ event.preventDefault();
147
+ },
148
+ };
149
+ this.handlers.set(type, slot);
150
+ this.addEventListener(type, slot.listener);
151
+ },
152
+ });
153
+ }
154
+ }
155
+ invoke(event, dispatch, phase, capture) {
156
+ const list = this.listeners.get(event.type);
157
+ if (list === undefined)
158
+ return;
159
+ dispatch.current = this;
160
+ dispatch.phase = phase;
161
+ for (const listener of [...list]) {
162
+ if (listener.removed || listener.capture !== capture)
163
+ continue;
164
+ if (listener.once)
165
+ this.removeEventListener(event.type, listener.callback, capture);
166
+ const passive = listener.passive;
167
+ if (passive)
168
+ Object.defineProperty(event, 'preventDefault', {
169
+ value: () => undefined,
170
+ configurable: true,
171
+ });
172
+ try {
173
+ const { callback } = listener;
174
+ if (typeof callback === 'function')
175
+ callback.call(this, event);
176
+ else
177
+ callback.handleEvent(event);
178
+ }
179
+ catch (error) {
180
+ /*
181
+ * Reported, and the rest still run: a browser logs an uncaught error in a listener and
182
+ * carries on dispatching, where a rethrow here would end the host's process.
183
+ */
184
+ console.error(error);
185
+ }
186
+ finally {
187
+ if (passive)
188
+ delete event.preventDefault;
189
+ }
190
+ if (dispatch.stoppedNow)
191
+ return;
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A pad's `vibrationActuator`, the standard's `dual-rumble` effect, driven through SDL's rumble.
3
+ *
4
+ * The strong magnitude drives the low-frequency motor and the weak one the high-frequency motor,
5
+ * the pair every rumbling pad has. A later effect preempts the one playing, whose promise then
6
+ * settles `preempted`, as the standard describes; one that runs its course settles `complete`.
7
+ */
8
+ /** What this drives of an SDL pad, a controller's or a joystick's. */
9
+ export interface Rumbler {
10
+ rumble(low: number, high: number, durationMs: number): void;
11
+ stopRumble(): void;
12
+ }
13
+ export interface DualRumble {
14
+ readonly type: 'dual-rumble';
15
+ readonly effects: readonly string[];
16
+ playEffect(type: string, params: Readonly<Record<string, number>>): Promise<string>;
17
+ reset(): Promise<string>;
18
+ }
19
+ export declare function dualRumble(motors: Rumbler): DualRumble;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A pad's `vibrationActuator`, the standard's `dual-rumble` effect, driven through SDL's rumble.
3
+ *
4
+ * The strong magnitude drives the low-frequency motor and the weak one the high-frequency motor,
5
+ * the pair every rumbling pad has. A later effect preempts the one playing, whose promise then
6
+ * settles `preempted`, as the standard describes; one that runs its course settles `complete`.
7
+ */
8
+ export function dualRumble(motors) {
9
+ let settle = null;
10
+ let timer = null;
11
+ const finish = (result) => {
12
+ if (timer !== null)
13
+ clearTimeout(timer);
14
+ timer = null;
15
+ const settling = settle;
16
+ settle = null;
17
+ settling?.(result);
18
+ };
19
+ return {
20
+ type: 'dual-rumble',
21
+ effects: ['dual-rumble'],
22
+ playEffect(type, params) {
23
+ if (type !== 'dual-rumble') {
24
+ return Promise.reject(new Error(`[driftengine] the native host plays no ${type} effect`));
25
+ }
26
+ finish('preempted');
27
+ const duration = params['duration'] ?? 0;
28
+ const delay = params['startDelay'] ?? 0;
29
+ return new Promise((resolve) => {
30
+ settle = resolve;
31
+ const start = () => {
32
+ motors.rumble(params['strongMagnitude'] ?? 0, params['weakMagnitude'] ?? 0, duration);
33
+ timer = setTimeout(() => finish('complete'), duration);
34
+ };
35
+ if (delay > 0)
36
+ timer = setTimeout(start, delay);
37
+ else
38
+ start();
39
+ });
40
+ },
41
+ reset() {
42
+ finish('preempted');
43
+ motors.stopRumble();
44
+ return Promise.resolve('complete');
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Put the browser's input event classes on the global a page reads them from, where the runtime
3
+ * has none, and hand back what takes them away again. Node has `Event` and none of these.
4
+ */
5
+ /** A pad arriving or leaving, as `gamepadconnected` and `gamepaddisconnected` carry it. */
6
+ export declare class GamepadEvent extends Event {
7
+ readonly gamepad: unknown;
8
+ constructor(type: string, init: EventInit & {
9
+ gamepad: unknown;
10
+ });
11
+ }
12
+ export declare function installEventClasses(scope?: Record<string, unknown>): () => void;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Put the browser's input event classes on the global a page reads them from, where the runtime
3
+ * has none, and hand back what takes them away again. Node has `Event` and none of these.
4
+ */
5
+ import { FocusEvent, KeyboardEvent, MouseEvent, PointerEvent, UIEvent, WheelEvent, } from './uiEvents.js';
6
+ import { Touch, TouchEvent, TouchList } from './uiTouch.js';
7
+ /** A pad arriving or leaving, as `gamepadconnected` and `gamepaddisconnected` carry it. */
8
+ export class GamepadEvent extends Event {
9
+ gamepad;
10
+ constructor(type, init) {
11
+ super(type, init);
12
+ this.gamepad = init.gamepad;
13
+ }
14
+ }
15
+ const CLASSES = {
16
+ UIEvent,
17
+ FocusEvent,
18
+ MouseEvent,
19
+ WheelEvent,
20
+ PointerEvent,
21
+ KeyboardEvent,
22
+ Touch,
23
+ TouchList,
24
+ TouchEvent,
25
+ GamepadEvent,
26
+ };
27
+ export function installEventClasses(scope = globalThis) {
28
+ const added = Object.entries(CLASSES).filter(([name]) => !(name in scope));
29
+ for (const [name, value] of added)
30
+ scope[name] = value;
31
+ return () => {
32
+ for (const [name] of added)
33
+ delete scope[name];
34
+ };
35
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The page's own files, answered from disk: the `fetch` a scene calls, under a host.
3
+ *
4
+ * **A published scene fetches by a path relative to its page**: the showroom's model, the voxel
5
+ * sandbox's tiles. A browser resolves that against the server that sent the page, and a host has no
6
+ * server — so it answers those paths from the directory the dev server serves (`demo/dev/public`),
7
+ * and a `file:` URL, which is what `new URL(path, import.meta.url)` makes, as the file it names.
8
+ * Anything carrying a scheme of its own is the network's, and goes to the platform's `fetch`.
9
+ *
10
+ * **A missing file is a 404, not a throw**, because that is what the scenes already handle: the
11
+ * sandbox paints a missing tile magenta, and the showroom asks with `HEAD` whether a model is there.
12
+ * A path that climbs out of the served directory is a 404 too, as it would be from a server.
13
+ */
14
+ type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
15
+ export declare function hostFetch(root: string, network: Fetch): Fetch;
16
+ export {};