@worldmesh/runtime 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/dist/avatar/descriptor.d.ts +27 -0
- package/dist/avatar/descriptor.d.ts.map +1 -0
- package/dist/avatar/descriptor.js +41 -0
- package/dist/avatar/descriptor.js.map +1 -0
- package/dist/avatar/handoff.d.ts +29 -0
- package/dist/avatar/handoff.d.ts.map +1 -0
- package/dist/avatar/handoff.js +92 -0
- package/dist/avatar/handoff.js.map +1 -0
- package/dist/avatar/loader.d.ts +26 -0
- package/dist/avatar/loader.d.ts.map +1 -0
- package/dist/avatar/loader.js +177 -0
- package/dist/avatar/loader.js.map +1 -0
- package/dist/controls/input.d.ts +25 -0
- package/dist/controls/input.d.ts.map +1 -1
- package/dist/controls/input.js +60 -2
- package/dist/controls/input.js.map +1 -1
- package/dist/controls/touch.d.ts.map +1 -1
- package/dist/controls/touch.js +5 -0
- package/dist/controls/touch.js.map +1 -1
- package/dist/core/worldmesh.d.ts.map +1 -1
- package/dist/core/worldmesh.js +114 -8
- package/dist/core/worldmesh.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/movement/controller.d.ts +10 -1
- package/dist/movement/controller.d.ts.map +1 -1
- package/dist/movement/controller.js +61 -4
- package/dist/movement/controller.js.map +1 -1
- package/dist/net/adapter.d.ts +3 -14
- package/dist/net/adapter.d.ts.map +1 -1
- package/dist/net/adapter.js.map +1 -1
- package/dist/net/presence.d.ts +68 -0
- package/dist/net/presence.d.ts.map +1 -0
- package/dist/net/presence.js +425 -0
- package/dist/net/presence.js.map +1 -0
- package/dist/player/avatar.d.ts +4 -0
- package/dist/player/avatar.d.ts.map +1 -1
- package/dist/player/avatar.js +97 -4
- package/dist/player/avatar.js.map +1 -1
- package/dist/player/player.d.ts +13 -0
- package/dist/player/player.d.ts.map +1 -1
- package/dist/player/player.js +44 -5
- package/dist/player/player.js.map +1 -1
- package/dist/portals/portals.d.ts +4 -1
- package/dist/portals/portals.d.ts.map +1 -1
- package/dist/portals/portals.js +6 -2
- package/dist/portals/portals.js.map +1 -1
- package/dist/types.d.ts +78 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlay.d.ts +7 -0
- package/dist/ui/overlay.d.ts.map +1 -1
- package/dist/ui/overlay.js +62 -7
- package/dist/ui/overlay.js.map +1 -1
- package/package.json +4 -1
- package/src/avatar/descriptor.ts +56 -0
- package/src/avatar/handoff.ts +91 -0
- package/src/avatar/loader.ts +184 -0
- package/src/controls/input.ts +61 -2
- package/src/controls/touch.ts +5 -0
- package/src/core/worldmesh.ts +110 -8
- package/src/index.ts +21 -1
- package/src/movement/controller.ts +59 -5
- package/src/net/adapter.ts +3 -14
- package/src/net/presence.ts +498 -0
- package/src/player/avatar.ts +109 -4
- package/src/player/player.ts +42 -3
- package/src/portals/portals.ts +6 -2
- package/src/types.ts +77 -4
- package/src/ui/overlay.ts +60 -7
package/src/player/player.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Group, Mesh, Object3D, type Vector3 } from 'three';
|
|
2
|
+
import type { LoadedAvatar } from '../avatar/loader.js';
|
|
2
3
|
import type { PlayerOptions } from '../types.js';
|
|
3
4
|
import {
|
|
4
5
|
animateDefaultAvatar,
|
|
@@ -30,6 +31,9 @@ export class Player {
|
|
|
30
31
|
facing = 0;
|
|
31
32
|
|
|
32
33
|
private isDefaultAvatar: boolean;
|
|
34
|
+
/** The body the world started with, kept aside while an external avatar is shown. */
|
|
35
|
+
private baseBody: Object3D | null = null;
|
|
36
|
+
private external: LoadedAvatar | null = null;
|
|
33
37
|
|
|
34
38
|
constructor(options: PlayerAvatarOptions) {
|
|
35
39
|
this.height = options.height;
|
|
@@ -48,6 +52,38 @@ export class Player {
|
|
|
48
52
|
|
|
49
53
|
const body = options.avatar ?? createDefaultAvatar(options.height);
|
|
50
54
|
this.root.add(body as Object3D);
|
|
55
|
+
this.baseBody = body;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether an external avatar (e.g. from the visitor's Avatar Wallet) is shown. */
|
|
59
|
+
get hasExternalBody(): boolean {
|
|
60
|
+
return !!this.external;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Show an external avatar instead of the world's body. The original body is
|
|
65
|
+
* kept, so `clearExternalBody()` brings it back. Ignored when the world
|
|
66
|
+
* turned the avatar off (`avatar: false`).
|
|
67
|
+
*/
|
|
68
|
+
setExternalBody(avatar: LoadedAvatar): boolean {
|
|
69
|
+
if (!this.root || !this.baseBody) {
|
|
70
|
+
avatar.dispose();
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
this.clearExternalBody();
|
|
74
|
+
this.baseBody.visible = false;
|
|
75
|
+
this.root.scale.y = 1;
|
|
76
|
+
this.root.add(avatar.object);
|
|
77
|
+
this.external = avatar;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clearExternalBody(): void {
|
|
82
|
+
if (!this.external) return;
|
|
83
|
+
this.external.object.removeFromParent();
|
|
84
|
+
this.external.dispose();
|
|
85
|
+
this.external = null;
|
|
86
|
+
if (this.baseBody) this.baseBody.visible = true;
|
|
51
87
|
}
|
|
52
88
|
|
|
53
89
|
/** Place the avatar at the player's feet, facing `yaw`, and animate it for `motion`. */
|
|
@@ -61,16 +97,18 @@ export class Player {
|
|
|
61
97
|
if (!this.root) return;
|
|
62
98
|
this.root.position.copy(feet);
|
|
63
99
|
this.root.rotation.y = this.facing;
|
|
64
|
-
if (this.
|
|
100
|
+
if (this.external) {
|
|
101
|
+
if (motion) this.external.animate(motion);
|
|
102
|
+
} else if (this.isDefaultAvatar) {
|
|
65
103
|
// Squash the default body while crouching instead of rebuilding it.
|
|
66
104
|
this.root.scale.y = currentHeight / this.height;
|
|
67
|
-
if (motion) animateDefaultAvatar(this.
|
|
105
|
+
if (motion && this.baseBody) animateDefaultAvatar(this.baseBody, motion);
|
|
68
106
|
}
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
setExpression(expression: AvatarExpression): void {
|
|
72
110
|
this.expression = expression;
|
|
73
|
-
if (this.
|
|
111
|
+
if (this.baseBody && this.isDefaultAvatar) setAvatarExpression(this.baseBody, expression);
|
|
74
112
|
}
|
|
75
113
|
|
|
76
114
|
setVisible(visible: boolean): void {
|
|
@@ -78,6 +116,7 @@ export class Player {
|
|
|
78
116
|
}
|
|
79
117
|
|
|
80
118
|
dispose(): void {
|
|
119
|
+
this.clearExternalBody();
|
|
81
120
|
this.root?.traverse((child) => {
|
|
82
121
|
if (child instanceof Mesh) {
|
|
83
122
|
child.geometry.dispose();
|
package/src/portals/portals.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TorusGeometry,
|
|
12
12
|
Vector3,
|
|
13
13
|
} from 'three';
|
|
14
|
+
import { withAvatarTicket } from '../avatar/handoff.js';
|
|
14
15
|
import type { PortalMode, PortalOptions } from '../types.js';
|
|
15
16
|
|
|
16
17
|
export interface ResolvedPortal extends PortalOptions {
|
|
@@ -107,14 +108,17 @@ export class PortalManager {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Build the destination URL, carrying a back-reference to this world, and the
|
|
113
|
+
* visitor's avatar handoff ticket (in the fragment) if they arrived with one.
|
|
114
|
+
*/
|
|
111
115
|
export function buildTravelUrl(target: string): string {
|
|
112
116
|
try {
|
|
113
117
|
const url = new URL(target, window.location.href);
|
|
114
118
|
if (!url.searchParams.has('from')) {
|
|
115
119
|
url.searchParams.set('from', window.location.origin + window.location.pathname);
|
|
116
120
|
}
|
|
117
|
-
return url.toString();
|
|
121
|
+
return withAvatarTicket(url).toString();
|
|
118
122
|
} catch {
|
|
119
123
|
return target;
|
|
120
124
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AvatarDescriptor } from './avatar/descriptor.js';
|
|
1
2
|
import type { AvatarExpression } from './player/avatar.js';
|
|
2
3
|
import type { Camera, Object3D, PerspectiveCamera, Scene, WebGLRenderer } from 'three';
|
|
3
4
|
|
|
@@ -78,6 +79,30 @@ export interface PlayerOptions {
|
|
|
78
79
|
avatar?: Object3D | false;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Let visitors bring their own character. Entirely optional: without it, or
|
|
84
|
+
* whenever resolving or loading fails, the player keeps the world's body.
|
|
85
|
+
*
|
|
86
|
+
* - `worldmesh`: the avatar the visitor picked in their WorldMesh Avatar
|
|
87
|
+
* Wallet, handed over through the URL fragment when they arrive from the
|
|
88
|
+
* hub or another world. The model loads from the avatar platform itself;
|
|
89
|
+
* this world never sees the visitor's account or platform credentials.
|
|
90
|
+
* - `descriptor`: an avatar the world resolved on its own.
|
|
91
|
+
*/
|
|
92
|
+
export type AvatarOptions =
|
|
93
|
+
| {
|
|
94
|
+
source: 'worldmesh';
|
|
95
|
+
/** The WorldMesh hub that resolves handoff tickets. Defaults to https://worldmesh.net/. */
|
|
96
|
+
hubUrl?: string;
|
|
97
|
+
/** Refuse models larger than this many bytes. Defaults to 40 MB. */
|
|
98
|
+
maxBytes?: number;
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
source: 'descriptor';
|
|
102
|
+
descriptor: AvatarDescriptor;
|
|
103
|
+
maxBytes?: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
81
106
|
export interface ViewOptions {
|
|
82
107
|
mode?: ViewMode;
|
|
83
108
|
/** Third-person boom length in metres. */
|
|
@@ -119,6 +144,17 @@ export interface UiOptions {
|
|
|
119
144
|
controlsHint?: boolean;
|
|
120
145
|
/** World name shown in the overlay. */
|
|
121
146
|
title?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Hide the click-to-enter panel until the visitor has entered once and then
|
|
149
|
+
* pressed Esc. The first screen stays clear; a click/tap still starts them.
|
|
150
|
+
*/
|
|
151
|
+
deferLockPanel?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Walking starts immediately. The keyboard, the on-screen joysticks and
|
|
154
|
+
* mouse look (with the cursor visible) work before any click, and Esc holds
|
|
155
|
+
* them until Continue. The first click captures the cursor for unlimited turning.
|
|
156
|
+
*/
|
|
157
|
+
moveBeforeLock?: boolean;
|
|
122
158
|
}
|
|
123
159
|
|
|
124
160
|
/** Everything another peer would need to draw this player. Serializable on purpose. */
|
|
@@ -149,12 +185,27 @@ export interface WorldMeshEvents {
|
|
|
149
185
|
/** E pressed with nothing else claiming it. */
|
|
150
186
|
interact: { position: Vec3Tuple };
|
|
151
187
|
respawn: { reason: 'fell' | 'manual' };
|
|
188
|
+
/** The visitor's own avatar replaced the default body. */
|
|
189
|
+
'avatar:load': { descriptor: AvatarDescriptor };
|
|
190
|
+
/** An external avatar could not be shown; the default body stays. */
|
|
191
|
+
'avatar:error': { descriptor: AvatarDescriptor | null; error: unknown };
|
|
192
|
+
/** People in this world including the visitor, or null while not connected. Only with `multiplayer`. */
|
|
193
|
+
players: { count: number | null };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Someone else's body, an upright cylinder the local player cannot walk through. */
|
|
197
|
+
export interface PeerBody {
|
|
198
|
+
/** Feet position in world space. */
|
|
199
|
+
x: number;
|
|
200
|
+
y: number;
|
|
201
|
+
z: number;
|
|
202
|
+
radius: number;
|
|
203
|
+
height: number;
|
|
152
204
|
}
|
|
153
205
|
|
|
154
206
|
/**
|
|
155
|
-
* Multiplayer seam.
|
|
156
|
-
*
|
|
157
|
-
* (peer-to-peer, a world-hosted server, or a WorldMesh signaling service).
|
|
207
|
+
* Multiplayer seam. `multiplayer` plugs in the built-in relay client; pass
|
|
208
|
+
* your own adapter here to network the world some other way.
|
|
158
209
|
*/
|
|
159
210
|
export interface NetworkAdapter {
|
|
160
211
|
/** Called once with a read-only view of the local world. */
|
|
@@ -163,6 +214,8 @@ export interface NetworkAdapter {
|
|
|
163
214
|
sendLocalState?(state: PlayerState): void;
|
|
164
215
|
/** Called by the adapter when a remote peer's state arrives. */
|
|
165
216
|
onRemoteState?(peerId: string, state: PlayerState): void;
|
|
217
|
+
/** Where everyone else is standing right now. The local player is kept out of them. */
|
|
218
|
+
bodies?(): Iterable<PeerBody>;
|
|
166
219
|
detach?(): void;
|
|
167
220
|
}
|
|
168
221
|
|
|
@@ -180,6 +233,8 @@ export interface WorldMeshOptions {
|
|
|
180
233
|
colliders?: Object3D[] | (() => Object3D[]);
|
|
181
234
|
portals?: PortalOptions[];
|
|
182
235
|
player?: PlayerOptions;
|
|
236
|
+
/** Show the visitor's own avatar. Off by default. */
|
|
237
|
+
avatar?: AvatarOptions;
|
|
183
238
|
view?: ViewOptions;
|
|
184
239
|
keymap?: Partial<Keymap>;
|
|
185
240
|
movement?: Partial<MovementTuning>;
|
|
@@ -192,10 +247,21 @@ export interface WorldMeshOptions {
|
|
|
192
247
|
autoResize?: boolean;
|
|
193
248
|
/** Your per-frame world logic. Runs after movement, before rendering. */
|
|
194
249
|
onUpdate?: (dt: number, world: WorldMeshHandle) => void;
|
|
195
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* Show everyone else who is in this world. `true` uses the relay WorldMesh
|
|
252
|
+
* hosts; `{ server }` points at your own copy of workers/presence.
|
|
253
|
+
* Ignored when `network` is set.
|
|
254
|
+
*/
|
|
255
|
+
multiplayer?: boolean | MultiplayerOptions;
|
|
256
|
+
/** A custom network layer. Replaces `multiplayer`. */
|
|
196
257
|
network?: NetworkAdapter;
|
|
197
258
|
}
|
|
198
259
|
|
|
260
|
+
export interface MultiplayerOptions {
|
|
261
|
+
/** WebSocket base URL of a presence relay, e.g. `wss://relay.example.com`. */
|
|
262
|
+
server?: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
199
265
|
export interface WorldMeshHandle {
|
|
200
266
|
readonly scene: Scene;
|
|
201
267
|
readonly camera: Camera;
|
|
@@ -203,6 +269,8 @@ export interface WorldMeshHandle {
|
|
|
203
269
|
readonly abilities: Readonly<Abilities>;
|
|
204
270
|
/** The avatar root. Move it and you move the player. */
|
|
205
271
|
readonly avatar: Object3D | null;
|
|
272
|
+
/** The external avatar being shown, or null while the default body is. */
|
|
273
|
+
readonly avatarDescriptor: AvatarDescriptor | null;
|
|
206
274
|
|
|
207
275
|
start(): void;
|
|
208
276
|
stop(): void;
|
|
@@ -225,6 +293,11 @@ export interface WorldMeshHandle {
|
|
|
225
293
|
on<K extends keyof WorldMeshEvents>(event: K, fn: (payload: WorldMeshEvents[K]) => void): () => void;
|
|
226
294
|
off<K extends keyof WorldMeshEvents>(event: K, fn: (payload: WorldMeshEvents[K]) => void): void;
|
|
227
295
|
|
|
296
|
+
/** Show an external avatar. Resolves to false (default body kept) if it cannot be loaded. */
|
|
297
|
+
loadAvatar(descriptor: AvatarDescriptor): Promise<boolean>;
|
|
298
|
+
/** Go back to the world's default body. */
|
|
299
|
+
clearAvatar(): void;
|
|
300
|
+
|
|
228
301
|
/** Refresh the collider list after the world adds geometry. */
|
|
229
302
|
refreshColliders(): void;
|
|
230
303
|
}
|
package/src/ui/overlay.ts
CHANGED
|
@@ -47,6 +47,14 @@ const CSS = `
|
|
|
47
47
|
-webkit-tap-highlight-color: transparent;
|
|
48
48
|
}
|
|
49
49
|
.wm-overlay[data-locked="true"] .wm-lock { display: none; }
|
|
50
|
+
/* First visit with deferLockPanel: keep the click target, hide the chrome. */
|
|
51
|
+
.wm-overlay[data-defer-lock="true"]:not([data-lock-ready="true"]) .wm-lock {
|
|
52
|
+
background: transparent;
|
|
53
|
+
backdrop-filter: none;
|
|
54
|
+
}
|
|
55
|
+
.wm-overlay[data-defer-lock="true"]:not([data-lock-ready="true"]) .wm-lock-content {
|
|
56
|
+
visibility: hidden;
|
|
57
|
+
}
|
|
50
58
|
.wm-lock-content {
|
|
51
59
|
margin: auto 0;
|
|
52
60
|
width: 100%;
|
|
@@ -59,10 +67,14 @@ const CSS = `
|
|
|
59
67
|
box-sizing: border-box;
|
|
60
68
|
}
|
|
61
69
|
.wm-lock-title {
|
|
70
|
+
width: 100%;
|
|
71
|
+
margin: 0;
|
|
72
|
+
padding: 0;
|
|
73
|
+
display: block;
|
|
74
|
+
text-align: center;
|
|
62
75
|
font-size: clamp(22px, 5vw, 34px);
|
|
63
76
|
font-weight: 600;
|
|
64
77
|
letter-spacing: .01em;
|
|
65
|
-
margin: 0;
|
|
66
78
|
}
|
|
67
79
|
.wm-lock-cta {
|
|
68
80
|
font-size: 13.5px;
|
|
@@ -265,7 +277,9 @@ const KEY_LEGEND: [string, string][] = [
|
|
|
265
277
|
export class Overlay {
|
|
266
278
|
readonly root: HTMLDivElement;
|
|
267
279
|
private prompt: HTMLDivElement;
|
|
280
|
+
private lock: HTMLDivElement;
|
|
268
281
|
private onEnter: (touch?: boolean) => void;
|
|
282
|
+
private deferLockPanel: boolean;
|
|
269
283
|
|
|
270
284
|
constructor(
|
|
271
285
|
options: UiOptions & {
|
|
@@ -275,12 +289,15 @@ export class Overlay {
|
|
|
275
289
|
) {
|
|
276
290
|
injectStyles();
|
|
277
291
|
this.onEnter = options.onEnter;
|
|
292
|
+
this.deferLockPanel = options.deferLockPanel === true;
|
|
278
293
|
|
|
279
294
|
const isTouch = isTouchDevice();
|
|
280
295
|
|
|
281
296
|
this.root = document.createElement('div');
|
|
282
297
|
this.root.className = 'wm-overlay';
|
|
283
|
-
|
|
298
|
+
// Already walking: no click-to-enter layer. Esc is what brings the pause screen up.
|
|
299
|
+
this.root.dataset.locked = options.moveBeforeLock ? 'true' : 'false';
|
|
300
|
+
if (this.deferLockPanel) this.root.dataset.deferLock = 'true';
|
|
284
301
|
|
|
285
302
|
if (options.crosshair !== false) {
|
|
286
303
|
const crosshair = document.createElement('div');
|
|
@@ -290,6 +307,7 @@ export class Overlay {
|
|
|
290
307
|
|
|
291
308
|
const lock = document.createElement('div');
|
|
292
309
|
lock.className = 'wm-lock';
|
|
310
|
+
this.lock = lock;
|
|
293
311
|
|
|
294
312
|
const content = document.createElement('div');
|
|
295
313
|
content.className = 'wm-lock-content';
|
|
@@ -302,11 +320,10 @@ export class Overlay {
|
|
|
302
320
|
const cta = document.createElement('button');
|
|
303
321
|
cta.type = 'button';
|
|
304
322
|
cta.className = 'wm-lock-cta';
|
|
305
|
-
cta.textContent =
|
|
323
|
+
cta.textContent = 'Continue';
|
|
306
324
|
content.appendChild(cta);
|
|
307
325
|
|
|
308
326
|
const handleEnter = (e: Event) => {
|
|
309
|
-
e.preventDefault();
|
|
310
327
|
e.stopPropagation();
|
|
311
328
|
const touchInitiated =
|
|
312
329
|
('pointerType' in e && (e as PointerEvent).pointerType === 'touch') ||
|
|
@@ -315,9 +332,18 @@ export class Overlay {
|
|
|
315
332
|
this.onEnter(touchInitiated);
|
|
316
333
|
};
|
|
317
334
|
|
|
335
|
+
// A click or tap on the button resumes. The Enter key does not: that key talks.
|
|
336
|
+
cta.addEventListener('keydown', (event) => {
|
|
337
|
+
if (event.key === 'Enter' || event.key === ' ') event.preventDefault();
|
|
338
|
+
});
|
|
318
339
|
cta.addEventListener('click', handleEnter);
|
|
319
|
-
|
|
320
|
-
|
|
340
|
+
lock.addEventListener('click', (event) => {
|
|
341
|
+
const target = event.target;
|
|
342
|
+
if (target instanceof Element && target.closest('button, a, input, textarea, select, label, .walk-pause-actions')) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
handleEnter(event);
|
|
346
|
+
});
|
|
321
347
|
|
|
322
348
|
if (options.controlsHint !== false) {
|
|
323
349
|
const keys = document.createElement('div');
|
|
@@ -341,6 +367,7 @@ export class Overlay {
|
|
|
341
367
|
}
|
|
342
368
|
lock.appendChild(content);
|
|
343
369
|
this.root.appendChild(lock);
|
|
370
|
+
if (this.deferLockPanel) this.hideLockChrome();
|
|
344
371
|
|
|
345
372
|
this.prompt = document.createElement('div');
|
|
346
373
|
this.prompt.className = 'wm-prompt';
|
|
@@ -365,14 +392,40 @@ export class Overlay {
|
|
|
365
392
|
|
|
366
393
|
const hint = document.createElement('div');
|
|
367
394
|
hint.className = 'wm-hint';
|
|
368
|
-
hint.textContent = isTouch ? 'Tap ⏸ to
|
|
395
|
+
hint.textContent = isTouch ? 'Tap ⏸ to open the menu' : 'Esc to open the menu';
|
|
369
396
|
this.root.appendChild(hint);
|
|
370
397
|
|
|
371
398
|
document.body.appendChild(this.root);
|
|
372
399
|
}
|
|
373
400
|
|
|
401
|
+
/** The pause screen, with the control list, is on screen. */
|
|
402
|
+
isPaused(): boolean {
|
|
403
|
+
return this.root.dataset.lockReady === 'true' && this.root.dataset.locked !== 'true';
|
|
404
|
+
}
|
|
405
|
+
|
|
374
406
|
setLocked(locked: boolean): void {
|
|
375
407
|
this.root.dataset.locked = String(locked);
|
|
408
|
+
if (!locked && this.deferLockPanel) {
|
|
409
|
+
this.root.dataset.lockReady = 'true';
|
|
410
|
+
this.showLockChrome();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Keep the full-screen click target; strip the dimmer and copy. */
|
|
415
|
+
private hideLockChrome(): void {
|
|
416
|
+
this.lock.style.background = 'transparent';
|
|
417
|
+
this.lock.style.backdropFilter = 'none';
|
|
418
|
+
this.lock.style.setProperty('-webkit-backdrop-filter', 'none');
|
|
419
|
+
const content = this.lock.querySelector('.wm-lock-content');
|
|
420
|
+
if (content instanceof HTMLElement) content.style.visibility = 'hidden';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private showLockChrome(): void {
|
|
424
|
+
this.lock.style.background = '';
|
|
425
|
+
this.lock.style.backdropFilter = '';
|
|
426
|
+
this.lock.style.removeProperty('-webkit-backdrop-filter');
|
|
427
|
+
const content = this.lock.querySelector('.wm-lock-content');
|
|
428
|
+
if (content instanceof HTMLElement) content.style.visibility = '';
|
|
376
429
|
}
|
|
377
430
|
|
|
378
431
|
setPrompt(text: string | null): void {
|