@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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `matchMedia`, answered from what this host knows: a desktop window's pointer, its size and
|
|
3
|
+
* shape, its pixel ratio, and whether it is fullscreen.
|
|
4
|
+
*
|
|
5
|
+
* **The engine asks `(pointer: coarse)`** to choose touch controls and to ask for fullscreen on a
|
|
6
|
+
* first gesture, and a window with no `matchMedia` at all read as a desktop only because the engine
|
|
7
|
+
* guards the call. A consumer may ask anything, so the common features are answered:
|
|
8
|
+
* - `pointer` and `hover` as a desktop's mouse, `fine` and `hover`. `any-pointer` adds `coarse`
|
|
9
|
+
* once a finger has landed, because SDL cannot say whether a touch screen exists — on the machine
|
|
10
|
+
* this was written on it listed X11's master pointer, "Virtual core pointer", as one.
|
|
11
|
+
* - `width`, `height` and `orientation` from the window, in either spelling of a range;
|
|
12
|
+
* `resolution` from its pixel ratio.
|
|
13
|
+
* - `display-mode` as `fullscreen` or `standalone`, the display mode of an app's own window.
|
|
14
|
+
* - Preferences as a person who set none: `prefers-reduced-motion: no-preference`,
|
|
15
|
+
* `prefers-color-scheme: light`, `prefers-contrast: no-preference`, `forced-colors: none`.
|
|
16
|
+
*
|
|
17
|
+
* A list is live, as a browser's is, and is sent `change` when its answer flips. **A feature this
|
|
18
|
+
* does not know answers false and is named once on the console**, rather than a guess at what a
|
|
19
|
+
* browser would have said.
|
|
20
|
+
*
|
|
21
|
+
* What it gives up: a desktop's colour scheme is not read, so a dark desktop still answers
|
|
22
|
+
* `light`; and a machine with only a touch screen answers as one with a mouse until told
|
|
23
|
+
* otherwise.
|
|
24
|
+
*/
|
|
25
|
+
import { HostNode } from './domTree.js';
|
|
26
|
+
const is = (answer) => (value) => value === null || value === answer;
|
|
27
|
+
const px = (value) => Number.parseFloat(value);
|
|
28
|
+
const dppx = (value) => value.endsWith('dpi') ? Number.parseFloat(value) / 96 : Number.parseFloat(value);
|
|
29
|
+
const FEATURES = {
|
|
30
|
+
pointer: is('fine'),
|
|
31
|
+
'any-pointer': (v, f) => v === null || v === 'fine' || (v === 'coarse' && f.touched),
|
|
32
|
+
hover: is('hover'),
|
|
33
|
+
'any-hover': is('hover'),
|
|
34
|
+
orientation: (v, f) => v === (f.height >= f.width ? 'portrait' : 'landscape'),
|
|
35
|
+
width: (v, f) => v === null || f.width === px(v),
|
|
36
|
+
'min-width': (v, f) => v !== null && f.width >= px(v),
|
|
37
|
+
'max-width': (v, f) => v !== null && f.width <= px(v),
|
|
38
|
+
height: (v, f) => v === null || f.height === px(v),
|
|
39
|
+
'min-height': (v, f) => v !== null && f.height >= px(v),
|
|
40
|
+
'max-height': (v, f) => v !== null && f.height <= px(v),
|
|
41
|
+
'min-resolution': (v, f) => v !== null && f.pixelRatio >= dppx(v),
|
|
42
|
+
'max-resolution': (v, f) => v !== null && f.pixelRatio <= dppx(v),
|
|
43
|
+
'display-mode': (v, f) => v === (f.fullscreen ? 'fullscreen' : 'standalone'),
|
|
44
|
+
'prefers-reduced-motion': (v) => v === 'no-preference',
|
|
45
|
+
'prefers-color-scheme': (v) => v === 'light',
|
|
46
|
+
'prefers-contrast': (v) => v === 'no-preference',
|
|
47
|
+
'forced-colors': (v) => v === 'none',
|
|
48
|
+
color: () => true,
|
|
49
|
+
};
|
|
50
|
+
/** `(width >= 600px)` and its kin, the range spelling, as the equivalent `min-` or `max-`. */
|
|
51
|
+
function range(condition) {
|
|
52
|
+
const match = /^(width|height)\s*(>=|<=|>|<)\s*(\S+)$/.exec(condition);
|
|
53
|
+
if (match === null)
|
|
54
|
+
return null;
|
|
55
|
+
const [, name, op, value] = match;
|
|
56
|
+
const nudge = op === '>' ? 0.001 : op === '<' ? -0.001 : 0;
|
|
57
|
+
const bound = `${px(value) + nudge}px`;
|
|
58
|
+
return [op.startsWith('>') ? `min-${name}` : `max-${name}`, bound];
|
|
59
|
+
}
|
|
60
|
+
/** `pointer: coarse` as its name and value; a bare `color` has no value. */
|
|
61
|
+
function nameAndValue(condition) {
|
|
62
|
+
const colon = condition.indexOf(':');
|
|
63
|
+
if (colon < 0)
|
|
64
|
+
return [condition, null];
|
|
65
|
+
return [condition.slice(0, colon).trim(), condition.slice(colon + 1).trim()];
|
|
66
|
+
}
|
|
67
|
+
export class MediaQueryList extends HostNode {
|
|
68
|
+
media;
|
|
69
|
+
answer;
|
|
70
|
+
last;
|
|
71
|
+
constructor(media, answer) {
|
|
72
|
+
super();
|
|
73
|
+
this.media = media;
|
|
74
|
+
this.answer = answer;
|
|
75
|
+
this.last = answer();
|
|
76
|
+
}
|
|
77
|
+
get matches() {
|
|
78
|
+
return this.answer();
|
|
79
|
+
}
|
|
80
|
+
/** The legacy pair, still read. */
|
|
81
|
+
addListener(listener) {
|
|
82
|
+
this.addEventListener('change', listener);
|
|
83
|
+
}
|
|
84
|
+
removeListener(listener) {
|
|
85
|
+
this.removeEventListener('change', listener);
|
|
86
|
+
}
|
|
87
|
+
/** Tell the list's listeners if its answer has flipped since they were last told. */
|
|
88
|
+
recheck() {
|
|
89
|
+
const now = this.answer();
|
|
90
|
+
if (now === this.last)
|
|
91
|
+
return;
|
|
92
|
+
this.last = now;
|
|
93
|
+
this.dispatchEvent(Object.assign(new Event('change'), { media: this.media, matches: now }));
|
|
94
|
+
}
|
|
95
|
+
static {
|
|
96
|
+
HostNode.handles(MediaQueryList.prototype, ['change']);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export class MediaQueries {
|
|
100
|
+
facts;
|
|
101
|
+
lists = [];
|
|
102
|
+
unknown = new Set();
|
|
103
|
+
constructor(facts) {
|
|
104
|
+
this.facts = facts;
|
|
105
|
+
}
|
|
106
|
+
match(query) {
|
|
107
|
+
const list = new MediaQueryList(query.trim(), () => this.evaluate(query));
|
|
108
|
+
this.lists.push(list);
|
|
109
|
+
return list;
|
|
110
|
+
}
|
|
111
|
+
/** Something a query can read has changed: each list whose answer flipped is told. */
|
|
112
|
+
changed() {
|
|
113
|
+
for (const list of this.lists)
|
|
114
|
+
list.recheck();
|
|
115
|
+
}
|
|
116
|
+
evaluate(query) {
|
|
117
|
+
const facts = this.facts();
|
|
118
|
+
return query.split(',').some((one) => this.single(one.trim().toLowerCase(), facts));
|
|
119
|
+
}
|
|
120
|
+
/** One query of a list: `not` or `only`, a media type, and conditions joined by `and`. */
|
|
121
|
+
single(query, facts) {
|
|
122
|
+
let rest = query;
|
|
123
|
+
const negated = rest.startsWith('not ');
|
|
124
|
+
if (negated || rest.startsWith('only '))
|
|
125
|
+
rest = rest.slice(rest.indexOf(' ') + 1).trim();
|
|
126
|
+
const parts = rest.split(/\s+and\s+/);
|
|
127
|
+
let result = true;
|
|
128
|
+
for (const part of parts) {
|
|
129
|
+
if (part === 'all' || part === 'screen')
|
|
130
|
+
continue;
|
|
131
|
+
if (!part.startsWith('(') || !part.endsWith(')')) {
|
|
132
|
+
result = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!this.condition(part.slice(1, -1).trim(), facts))
|
|
136
|
+
result = false;
|
|
137
|
+
}
|
|
138
|
+
return negated ? !result : result;
|
|
139
|
+
}
|
|
140
|
+
condition(condition, facts) {
|
|
141
|
+
const [name, value] = range(condition) ?? nameAndValue(condition);
|
|
142
|
+
const feature = FEATURES[name];
|
|
143
|
+
if (feature === undefined) {
|
|
144
|
+
if (!this.unknown.has(name)) {
|
|
145
|
+
this.unknown.add(name);
|
|
146
|
+
console.warn(`[driftengine] the native host does not answer the media feature \`${name}\`; it reads as false`);
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return feature(value, facts);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mouse, as a page hears it from Chrome: pointer 1, and every event a browser builds around it.
|
|
3
|
+
*
|
|
4
|
+
* **What Chrome sends, measured 2026-09-19 in Chrome 151 over the DevTools protocol**, on a page
|
|
5
|
+
* whose canvas filled it, is what this sends:
|
|
6
|
+
* - Coming in is `pointerover`, `pointerenter` at the document and then the canvas, the same three as
|
|
7
|
+
* mouse events, and then the move. Going out is the reverse, the canvas before the document.
|
|
8
|
+
* - A press is `pointerdown` then `mousedown`, whose `detail` counts the clicks, and a release is
|
|
9
|
+
* `pointerup`, `mouseup` and a `click`. The click is a `PointerEvent`, as are `contextmenu` and
|
|
10
|
+
* `auxclick`, and a second click in time and place is followed by `dblclick`.
|
|
11
|
+
* - The right button opens its context menu on the press, as on Linux.
|
|
12
|
+
* - A second button pressed while one is held is a `pointermove` rather than a `pointerdown`. The
|
|
13
|
+
* first button's release after that clicks nothing, with a `detail` of 0.
|
|
14
|
+
*
|
|
15
|
+
* **Pointer lock is the cursor hidden and put back in the middle of the window after every move**,
|
|
16
|
+
* the delta being how far it got. `@kmamal/sdl` 0.11 offers no relative mouse mode and reports only
|
|
17
|
+
* where the pointer is, so this is the one way to it; it carries the desktop's acceleration, which a
|
|
18
|
+
* browser's lock without `unadjustedMovement` does too.
|
|
19
|
+
*
|
|
20
|
+
* What it gives up: clicks count by Chromium's own figures, 500 ms between presses and 2 px either
|
|
21
|
+
* way of the first, where Chrome on a desktop that sets its own uses that; a wheel notch is 100
|
|
22
|
+
* pixels, a browser's figure varying by platform; and a drag out of the window stays over the canvas
|
|
23
|
+
* until its release, which is SDL's capture of a held button.
|
|
24
|
+
*/
|
|
25
|
+
import type { NativeCanvas } from './canvas.ts';
|
|
26
|
+
import type { HostPage } from './page.ts';
|
|
27
|
+
import { type ModifierInit } from './uiEvents.ts';
|
|
28
|
+
export interface SdlPointer {
|
|
29
|
+
readonly x: number;
|
|
30
|
+
readonly y: number;
|
|
31
|
+
readonly button?: number;
|
|
32
|
+
readonly dx?: number;
|
|
33
|
+
readonly dy?: number;
|
|
34
|
+
readonly flipped?: boolean;
|
|
35
|
+
/** SDL's mouse made from a finger, which the touch events already said. */
|
|
36
|
+
readonly touch?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Where the window is on the screen and how large, and SDL's mouse, which the lock warps. */
|
|
39
|
+
export interface MouseWindow {
|
|
40
|
+
readonly x: number;
|
|
41
|
+
readonly y: number;
|
|
42
|
+
readonly width: number;
|
|
43
|
+
readonly height: number;
|
|
44
|
+
}
|
|
45
|
+
export interface EventMouse {
|
|
46
|
+
setPosition(x: number, y: number): void;
|
|
47
|
+
showCursor(show?: boolean): void;
|
|
48
|
+
capture(): void;
|
|
49
|
+
uncapture(): void;
|
|
50
|
+
/** One of SDL's system cursors, by name. */
|
|
51
|
+
setCursor?(cursor: string): void;
|
|
52
|
+
}
|
|
53
|
+
export declare class MouseModel {
|
|
54
|
+
private readonly window;
|
|
55
|
+
private readonly mouse;
|
|
56
|
+
private readonly page;
|
|
57
|
+
private readonly canvas;
|
|
58
|
+
private readonly modifiers;
|
|
59
|
+
private readonly now;
|
|
60
|
+
private buttons;
|
|
61
|
+
/** The button whose press a release would complete as a click, or −1. */
|
|
62
|
+
private pressed;
|
|
63
|
+
private count;
|
|
64
|
+
private lastPress;
|
|
65
|
+
private x;
|
|
66
|
+
private y;
|
|
67
|
+
private inside;
|
|
68
|
+
/** The pointer left while a button held it here, so it leaves once the last one is let go. */
|
|
69
|
+
private away;
|
|
70
|
+
private locked;
|
|
71
|
+
/** Whether the page hid the cursor, `cursor: none`, which a lock let go does not undo. */
|
|
72
|
+
private cursorHidden;
|
|
73
|
+
constructor(window: MouseWindow, mouse: EventMouse, page: HostPage, canvas: NativeCanvas, modifiers: () => ModifierInit, now: () => number);
|
|
74
|
+
/** The cursor a page wrote to the canvas's style. */
|
|
75
|
+
cursor(css: string): void;
|
|
76
|
+
get isLocked(): boolean;
|
|
77
|
+
move(event: SdlPointer): void;
|
|
78
|
+
down(event: SdlPointer): void;
|
|
79
|
+
up(event: SdlPointer): void;
|
|
80
|
+
wheel(event: SdlPointer): void;
|
|
81
|
+
/** SDL's word that the pointer is over the window again, which cancels a leave still waiting. */
|
|
82
|
+
hover(): void;
|
|
83
|
+
/** SDL's word that the pointer left the window: out and leave, once no button holds it here. */
|
|
84
|
+
leave(): void;
|
|
85
|
+
/** A press or the wheel where no move came first: the pointer is here, so it came in. */
|
|
86
|
+
private settle;
|
|
87
|
+
private enter;
|
|
88
|
+
/** An enter or a leave, which does not bubble: sent to each node it crosses, in turn. */
|
|
89
|
+
private boundary;
|
|
90
|
+
private both;
|
|
91
|
+
private pointer;
|
|
92
|
+
private mouseEvent;
|
|
93
|
+
private init;
|
|
94
|
+
private middle;
|
|
95
|
+
private lock;
|
|
96
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mouse, as a page hears it from Chrome: pointer 1, and every event a browser builds around it.
|
|
3
|
+
*
|
|
4
|
+
* **What Chrome sends, measured 2026-09-19 in Chrome 151 over the DevTools protocol**, on a page
|
|
5
|
+
* whose canvas filled it, is what this sends:
|
|
6
|
+
* - Coming in is `pointerover`, `pointerenter` at the document and then the canvas, the same three as
|
|
7
|
+
* mouse events, and then the move. Going out is the reverse, the canvas before the document.
|
|
8
|
+
* - A press is `pointerdown` then `mousedown`, whose `detail` counts the clicks, and a release is
|
|
9
|
+
* `pointerup`, `mouseup` and a `click`. The click is a `PointerEvent`, as are `contextmenu` and
|
|
10
|
+
* `auxclick`, and a second click in time and place is followed by `dblclick`.
|
|
11
|
+
* - The right button opens its context menu on the press, as on Linux.
|
|
12
|
+
* - A second button pressed while one is held is a `pointermove` rather than a `pointerdown`. The
|
|
13
|
+
* first button's release after that clicks nothing, with a `detail` of 0.
|
|
14
|
+
*
|
|
15
|
+
* **Pointer lock is the cursor hidden and put back in the middle of the window after every move**,
|
|
16
|
+
* the delta being how far it got. `@kmamal/sdl` 0.11 offers no relative mouse mode and reports only
|
|
17
|
+
* where the pointer is, so this is the one way to it; it carries the desktop's acceleration, which a
|
|
18
|
+
* browser's lock without `unadjustedMovement` does too.
|
|
19
|
+
*
|
|
20
|
+
* What it gives up: clicks count by Chromium's own figures, 500 ms between presses and 2 px either
|
|
21
|
+
* way of the first, where Chrome on a desktop that sets its own uses that; a wheel notch is 100
|
|
22
|
+
* pixels, a browser's figure varying by platform; and a drag out of the window stays over the canvas
|
|
23
|
+
* until its release, which is SDL's capture of a held button.
|
|
24
|
+
*/
|
|
25
|
+
import { sdlCursor } from './canvasStyle.js';
|
|
26
|
+
import { MouseEvent, PointerEvent, WheelEvent, } from './uiEvents.js';
|
|
27
|
+
/** SDL counts buttons from 1: left, middle, right, back, forward. A browser counts from 0. */
|
|
28
|
+
const BUTTON = [-1, 0, 1, 2, 3, 4];
|
|
29
|
+
/** And `buttons` is a mask in which the right button comes before the middle. */
|
|
30
|
+
const BUTTON_BIT = [0, 1, 4, 2, 8, 16];
|
|
31
|
+
/** One notch of the wheel, in the pixels `deltaMode` 0 counts in. */
|
|
32
|
+
const NOTCH_PX = 100;
|
|
33
|
+
const CLICK_MS = 500;
|
|
34
|
+
const CLICK_PX = 2;
|
|
35
|
+
export class MouseModel {
|
|
36
|
+
window;
|
|
37
|
+
mouse;
|
|
38
|
+
page;
|
|
39
|
+
canvas;
|
|
40
|
+
modifiers;
|
|
41
|
+
now;
|
|
42
|
+
buttons = 0;
|
|
43
|
+
/** The button whose press a release would complete as a click, or −1. */
|
|
44
|
+
pressed = -1;
|
|
45
|
+
count = 0;
|
|
46
|
+
lastPress = { at: Number.NEGATIVE_INFINITY, x: 0, y: 0, button: -1 };
|
|
47
|
+
x = 0;
|
|
48
|
+
y = 0;
|
|
49
|
+
inside = false;
|
|
50
|
+
/** The pointer left while a button held it here, so it leaves once the last one is let go. */
|
|
51
|
+
away = false;
|
|
52
|
+
locked = false;
|
|
53
|
+
/** Whether the page hid the cursor, `cursor: none`, which a lock let go does not undo. */
|
|
54
|
+
cursorHidden = false;
|
|
55
|
+
constructor(window, mouse, page, canvas, modifiers, now) {
|
|
56
|
+
this.window = window;
|
|
57
|
+
this.mouse = mouse;
|
|
58
|
+
this.page = page;
|
|
59
|
+
this.canvas = canvas;
|
|
60
|
+
this.modifiers = modifiers;
|
|
61
|
+
this.now = now;
|
|
62
|
+
page.setPointerLocker((lock) => this.lock(lock));
|
|
63
|
+
page.capture.add(1, () => this.buttons !== 0);
|
|
64
|
+
}
|
|
65
|
+
/** The cursor a page wrote to the canvas's style. */
|
|
66
|
+
cursor(css) {
|
|
67
|
+
const cursor = sdlCursor(css);
|
|
68
|
+
const hidden = cursor === null;
|
|
69
|
+
if (hidden !== this.cursorHidden) {
|
|
70
|
+
this.cursorHidden = hidden;
|
|
71
|
+
if (!this.locked)
|
|
72
|
+
this.mouse.showCursor(!hidden);
|
|
73
|
+
}
|
|
74
|
+
if (cursor !== null)
|
|
75
|
+
this.mouse.setCursor?.(cursor);
|
|
76
|
+
}
|
|
77
|
+
get isLocked() {
|
|
78
|
+
return this.locked;
|
|
79
|
+
}
|
|
80
|
+
move(event) {
|
|
81
|
+
let dx = event.x - this.x;
|
|
82
|
+
let dy = event.y - this.y;
|
|
83
|
+
if (this.locked) {
|
|
84
|
+
/* The warp back lands as a move to where the pointer already is: nothing moved. */
|
|
85
|
+
if (dx === 0 && dy === 0)
|
|
86
|
+
return;
|
|
87
|
+
const centre = this.middle();
|
|
88
|
+
this.mouse.setPosition(centre.x, centre.y);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.x = event.x;
|
|
92
|
+
this.y = event.y;
|
|
93
|
+
if (!this.inside) {
|
|
94
|
+
this.enter();
|
|
95
|
+
dx = 0;
|
|
96
|
+
dy = 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.both('move', this.init(dx, dy, -1, 0));
|
|
100
|
+
}
|
|
101
|
+
down(event) {
|
|
102
|
+
this.settle(event);
|
|
103
|
+
const button = BUTTON[event.button ?? 0] ?? -1;
|
|
104
|
+
const chord = this.buttons !== 0;
|
|
105
|
+
this.buttons |= BUTTON_BIT[event.button ?? 0] ?? 0;
|
|
106
|
+
const at = this.now();
|
|
107
|
+
const last = this.lastPress;
|
|
108
|
+
const again = button === last.button &&
|
|
109
|
+
at - last.at <= CLICK_MS &&
|
|
110
|
+
Math.abs(this.x - last.x) <= CLICK_PX &&
|
|
111
|
+
Math.abs(this.y - last.y) <= CLICK_PX;
|
|
112
|
+
this.count = again ? this.count + 1 : 1;
|
|
113
|
+
this.lastPress = { at, x: this.x, y: this.y, button };
|
|
114
|
+
this.pressed = button;
|
|
115
|
+
/* A press is a person's gesture, noted before its events so their listeners may use it. */
|
|
116
|
+
this.page.userActivation.notify();
|
|
117
|
+
const init = this.init(0, 0, button, this.count);
|
|
118
|
+
this.pointer(chord ? 'pointermove' : 'pointerdown', init);
|
|
119
|
+
const mousedown = this.mouseEvent('mousedown', init);
|
|
120
|
+
if (!mousedown.defaultPrevented)
|
|
121
|
+
this.canvas.focus();
|
|
122
|
+
/* On the press, where Chrome on Linux sends it. */
|
|
123
|
+
if (button === 2)
|
|
124
|
+
this.pointer('contextmenu', { ...init, detail: 0 }, false);
|
|
125
|
+
}
|
|
126
|
+
up(event) {
|
|
127
|
+
this.settle(event);
|
|
128
|
+
const button = BUTTON[event.button ?? 0] ?? -1;
|
|
129
|
+
this.buttons &= ~(BUTTON_BIT[event.button ?? 0] ?? 0);
|
|
130
|
+
const clicked = this.pressed === button;
|
|
131
|
+
const init = this.init(0, 0, button, clicked ? this.count : 0);
|
|
132
|
+
this.pointer(this.buttons !== 0 ? 'pointermove' : 'pointerup', init);
|
|
133
|
+
this.mouseEvent('mouseup', init);
|
|
134
|
+
if (this.buttons === 0)
|
|
135
|
+
this.page.capture.releaseImplicitly(1, { ...init, ...POINTER(0) });
|
|
136
|
+
if (clicked) {
|
|
137
|
+
this.pointer(button === 0 ? 'click' : 'auxclick', init, false);
|
|
138
|
+
if (button === 0 && this.count === 2)
|
|
139
|
+
this.mouseEvent('dblclick', init);
|
|
140
|
+
}
|
|
141
|
+
this.pressed = -1;
|
|
142
|
+
const { width, height } = this.window;
|
|
143
|
+
const outside = this.x < 0 || this.y < 0 || this.x >= width || this.y >= height;
|
|
144
|
+
if (this.buttons === 0 && (this.away || outside))
|
|
145
|
+
this.leave();
|
|
146
|
+
}
|
|
147
|
+
wheel(event) {
|
|
148
|
+
this.settle(event);
|
|
149
|
+
const direction = event.flipped === true ? -1 : 1;
|
|
150
|
+
this.canvas.dispatchEvent(new WheelEvent('wheel', {
|
|
151
|
+
...this.init(0, 0, -1, 0),
|
|
152
|
+
bubbles: true,
|
|
153
|
+
cancelable: true,
|
|
154
|
+
deltaX: (event.dx ?? 0) * direction * NOTCH_PX,
|
|
155
|
+
deltaY: -(event.dy ?? 0) * direction * NOTCH_PX,
|
|
156
|
+
deltaMode: 0,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
/** SDL's word that the pointer is over the window again, which cancels a leave still waiting. */
|
|
160
|
+
hover() {
|
|
161
|
+
this.away = false;
|
|
162
|
+
}
|
|
163
|
+
/** SDL's word that the pointer left the window: out and leave, once no button holds it here. */
|
|
164
|
+
leave() {
|
|
165
|
+
if (!this.inside || this.locked)
|
|
166
|
+
return;
|
|
167
|
+
if (this.buttons !== 0) {
|
|
168
|
+
this.away = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.away = false;
|
|
172
|
+
this.inside = false;
|
|
173
|
+
const init = this.init(0, 0, -1, 0);
|
|
174
|
+
this.pointer('pointerout', init);
|
|
175
|
+
this.boundary('pointerleave', init, [this.canvas, this.page.document]);
|
|
176
|
+
this.mouseEvent('mouseout', init);
|
|
177
|
+
this.boundary('mouseleave', init, [this.canvas, this.page.document]);
|
|
178
|
+
}
|
|
179
|
+
/** A press or the wheel where no move came first: the pointer is here, so it came in. */
|
|
180
|
+
settle(event) {
|
|
181
|
+
if (this.locked)
|
|
182
|
+
return;
|
|
183
|
+
this.x = event.x;
|
|
184
|
+
this.y = event.y;
|
|
185
|
+
if (!this.inside)
|
|
186
|
+
this.enter();
|
|
187
|
+
}
|
|
188
|
+
enter() {
|
|
189
|
+
this.inside = true;
|
|
190
|
+
const init = this.init(0, 0, -1, 0);
|
|
191
|
+
this.pointer('pointerover', init);
|
|
192
|
+
this.boundary('pointerenter', init, [this.page.document, this.canvas]);
|
|
193
|
+
this.mouseEvent('mouseover', init);
|
|
194
|
+
this.boundary('mouseenter', init, [this.page.document, this.canvas]);
|
|
195
|
+
}
|
|
196
|
+
/** An enter or a leave, which does not bubble: sent to each node it crosses, in turn. */
|
|
197
|
+
boundary(type, init, nodes) {
|
|
198
|
+
const Kind = type.startsWith('pointer') ? PointerEvent : MouseEvent;
|
|
199
|
+
const extra = type.startsWith('pointer') ? POINTER(init.buttons ?? 0) : {};
|
|
200
|
+
for (const node of nodes) {
|
|
201
|
+
node.dispatchEvent(new Kind(type, { ...init, ...extra, bubbles: false, cancelable: false }));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
both(type, init) {
|
|
205
|
+
this.pointer(`pointer${type}`, init);
|
|
206
|
+
this.mouseEvent(`mouse${type}`, init);
|
|
207
|
+
}
|
|
208
|
+
pointer(type, init, primary = true) {
|
|
209
|
+
/* A change of capture is told just before the pointer's next pointer event. */
|
|
210
|
+
if (type.startsWith('pointer')) {
|
|
211
|
+
this.page.capture.process(1, { ...init, ...POINTER(init.buttons ?? 0), isPrimary: true });
|
|
212
|
+
}
|
|
213
|
+
const event = new PointerEvent(type, {
|
|
214
|
+
...init,
|
|
215
|
+
...POINTER(init.buttons ?? 0),
|
|
216
|
+
isPrimary: primary,
|
|
217
|
+
detail: type === 'click' || type === 'auxclick' ? (init.detail ?? 0) : 0,
|
|
218
|
+
bubbles: true,
|
|
219
|
+
cancelable: true,
|
|
220
|
+
});
|
|
221
|
+
this.canvas.dispatchEvent(event);
|
|
222
|
+
return event;
|
|
223
|
+
}
|
|
224
|
+
mouseEvent(type, init) {
|
|
225
|
+
const event = new MouseEvent(type, { ...init, bubbles: true, cancelable: true });
|
|
226
|
+
this.canvas.dispatchEvent(event);
|
|
227
|
+
return event;
|
|
228
|
+
}
|
|
229
|
+
init(movementX, movementY, button, detail) {
|
|
230
|
+
return {
|
|
231
|
+
clientX: this.x,
|
|
232
|
+
clientY: this.y,
|
|
233
|
+
screenX: this.window.x + this.x,
|
|
234
|
+
screenY: this.window.y + this.y,
|
|
235
|
+
movementX,
|
|
236
|
+
movementY,
|
|
237
|
+
button,
|
|
238
|
+
buttons: this.buttons,
|
|
239
|
+
detail,
|
|
240
|
+
...this.modifiers(),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
middle() {
|
|
244
|
+
return {
|
|
245
|
+
x: Math.round(this.window.x + this.window.width / 2),
|
|
246
|
+
y: Math.round(this.window.y + this.window.height / 2),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/*
|
|
250
|
+
* Hide the cursor and hold it, or let it go. SDL refuses to capture the mouse for a window without
|
|
251
|
+
* focus, and says so by throwing; that is a refused lock, with the cursor left as it was, and not
|
|
252
|
+
* an error to take the process down from inside a click.
|
|
253
|
+
*/
|
|
254
|
+
lock(lock) {
|
|
255
|
+
const { mouse } = this;
|
|
256
|
+
if (!lock) {
|
|
257
|
+
this.locked = false;
|
|
258
|
+
/* A release always succeeds from the page's side, whatever SDL says about it. */
|
|
259
|
+
try {
|
|
260
|
+
mouse.showCursor(!this.cursorHidden);
|
|
261
|
+
mouse.uncapture();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
/* Nothing held to let go of. */
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
mouse.showCursor(false);
|
|
270
|
+
mouse.capture();
|
|
271
|
+
const centre = this.middle();
|
|
272
|
+
mouse.setPosition(centre.x, centre.y);
|
|
273
|
+
this.x = centre.x - this.window.x;
|
|
274
|
+
this.y = centre.y - this.window.y;
|
|
275
|
+
this.locked = true;
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
mouse.showCursor(true);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** The mouse is pointer 1, always primary, one pixel across, and pressing at half pressure. */
|
|
285
|
+
function POINTER(buttons) {
|
|
286
|
+
return { pointerId: 1, pointerType: 'mouse', pressure: buttons !== 0 ? 0.5 : 0 };
|
|
287
|
+
}
|
package/dist/page.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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.ts';
|
|
22
|
+
import type { NativeCanvas } from './canvas.ts';
|
|
23
|
+
import { type Handlers, HostNode } from './domTree.ts';
|
|
24
|
+
import { PageFullscreen } from './fullscreen.ts';
|
|
25
|
+
import { MediaQueries, type MediaQueryList } from './mediaQueries.ts';
|
|
26
|
+
import { PointerCapture } from './pointerCapture.ts';
|
|
27
|
+
/** Every input event a page can be told of, which each of the three nodes answers `on…` for. */
|
|
28
|
+
export declare const INPUT_TYPES: readonly ["keydown", "keypress", "keyup", "focus", "blur", "wheel", "contextmenu", "click", "dblclick", "auxclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "pointerdown", "pointerup", "pointermove", "pointerover", "pointerout", "pointerenter", "pointerleave", "pointercancel", "gotpointercapture", "lostpointercapture", "touchstart", "touchmove", "touchend", "touchcancel", "fullscreenchange", "fullscreenerror"];
|
|
29
|
+
type InputType = (typeof INPUT_TYPES)[number];
|
|
30
|
+
declare const WINDOW_TYPES: readonly ['resize', 'beforeunload', 'gamepadconnected', 'gamepaddisconnected'];
|
|
31
|
+
declare const DOCUMENT_TYPES: readonly ['visibilitychange', 'pointerlockchange', 'pointerlockerror', 'fullscreenchange', 'fullscreenerror'];
|
|
32
|
+
export interface PageWindow extends Handlers<InputType | (typeof WINDOW_TYPES)[number]> {
|
|
33
|
+
}
|
|
34
|
+
export interface PageDocument extends Handlers<InputType | (typeof DOCUMENT_TYPES)[number]> {
|
|
35
|
+
}
|
|
36
|
+
export declare class PageWindow extends HostNode {
|
|
37
|
+
canvas: NativeCanvas | null;
|
|
38
|
+
/** Answered by the page, which knows what a query asks about (`mediaQueries.ts`). */
|
|
39
|
+
matchMedia: (query: string) => MediaQueryList;
|
|
40
|
+
get innerWidth(): number;
|
|
41
|
+
get innerHeight(): number;
|
|
42
|
+
get devicePixelRatio(): number;
|
|
43
|
+
}
|
|
44
|
+
export declare class PageDocument extends HostNode {
|
|
45
|
+
visibilityState: DocumentVisibilityState;
|
|
46
|
+
pointerLockElement: Element | null;
|
|
47
|
+
/** What is focused: the canvas, once it is focusable and asked, or nothing. */
|
|
48
|
+
activeElement: NativeCanvas | null;
|
|
49
|
+
/** Set by the page that owns this document, which is what holds the lock and the focus. */
|
|
50
|
+
release: (() => void) | null;
|
|
51
|
+
focused: boolean;
|
|
52
|
+
/** The page's fullscreen, which this document reports. */
|
|
53
|
+
fullscreenControl: PageFullscreen | null;
|
|
54
|
+
readonly fullscreenEnabled = true;
|
|
55
|
+
get fullscreenElement(): NativeCanvas | null;
|
|
56
|
+
/** The legacy flag, still read. */
|
|
57
|
+
get fullscreen(): boolean;
|
|
58
|
+
exitFullscreen(): Promise<void>;
|
|
59
|
+
get hidden(): boolean;
|
|
60
|
+
hasFocus(): boolean;
|
|
61
|
+
exitPointerLock(): void;
|
|
62
|
+
createElement(tag: string): never;
|
|
63
|
+
}
|
|
64
|
+
export interface HostPageOptions {
|
|
65
|
+
/** The clock a gesture's five seconds are measured on, in milliseconds. */
|
|
66
|
+
readonly now?: () => number;
|
|
67
|
+
}
|
|
68
|
+
export declare class HostPage {
|
|
69
|
+
readonly window: PageWindow;
|
|
70
|
+
readonly document: PageDocument;
|
|
71
|
+
/** Whether a person has just done something (`activation.ts`), as `navigator.userActivation`. */
|
|
72
|
+
readonly userActivation: UserActivation;
|
|
73
|
+
readonly fullscreen: PageFullscreen;
|
|
74
|
+
/** Which pointers the page holds (`pointerCapture.ts`). */
|
|
75
|
+
readonly capture: PointerCapture;
|
|
76
|
+
readonly media: MediaQueries;
|
|
77
|
+
/** Whether a finger has landed: the one proof of a touch screen SDL gives. */
|
|
78
|
+
private touched;
|
|
79
|
+
private asked;
|
|
80
|
+
private due;
|
|
81
|
+
private nextId;
|
|
82
|
+
private readonly lockListeners;
|
|
83
|
+
private locker;
|
|
84
|
+
constructor(options?: HostPageOptions);
|
|
85
|
+
/** A finger landed, so this is a touch screen, whatever SDL listed. */
|
|
86
|
+
noteTouch(): void;
|
|
87
|
+
readonly requestAnimationFrame: (callback: FrameRequestCallback) => number;
|
|
88
|
+
readonly cancelAnimationFrame: (id: number) => void;
|
|
89
|
+
/** One frame: the callbacks asked for before it began, in the order they were asked. */
|
|
90
|
+
runFrame(now: number): void;
|
|
91
|
+
/** Whether the window can be seen, as `document.visibilityState` reports it. */
|
|
92
|
+
setVisible(visible: boolean): void;
|
|
93
|
+
/**
|
|
94
|
+
* The window gaining or losing the focus: the focused canvas is told first, as the element a
|
|
95
|
+
* browser blurs, and then the window.
|
|
96
|
+
*/
|
|
97
|
+
setFocused(focused: boolean): void;
|
|
98
|
+
/** Where a key goes: the focused element, or the document when nothing is focused. */
|
|
99
|
+
keyTarget(): HostNode;
|
|
100
|
+
/** Hold `canvas` in this page: its parent, its pointer lock and its focus are the page's. */
|
|
101
|
+
attach(canvas: NativeCanvas): void;
|
|
102
|
+
/**
|
|
103
|
+
* The platform's half of the lock: hide the cursor and hold it, or let it go. Answers false when it
|
|
104
|
+
* cannot — a window without focus, as a hidden one is — and the lock is then refused, the way a
|
|
105
|
+
* browser refuses one: the promise rejects and `pointerlockerror` fires, and nothing is locked.
|
|
106
|
+
*/
|
|
107
|
+
setPointerLocker(locker: (lock: boolean) => boolean): void;
|
|
108
|
+
/** The platform's half of fullscreen: put the window in it or take it out, false when it cannot. */
|
|
109
|
+
setFullscreener(setter: (on: boolean) => boolean): void;
|
|
110
|
+
/** Told when the lock is taken or released. */
|
|
111
|
+
onPointerLock(listener: (locked: boolean) => void): void;
|
|
112
|
+
unlock(): void;
|
|
113
|
+
/** Put the globals a page has on `scope`, and hand back what takes them off again. */
|
|
114
|
+
install(scope?: Record<string, unknown>): () => void;
|
|
115
|
+
private lock;
|
|
116
|
+
private changedLock;
|
|
117
|
+
}
|
|
118
|
+
export {};
|