@worldmesh/runtime 0.2.0 → 0.2.2
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 +33 -3
- package/dist/audio/footsteps.d.ts +37 -0
- package/dist/audio/footsteps.d.ts.map +1 -0
- package/dist/audio/footsteps.js +178 -0
- package/dist/audio/footsteps.js.map +1 -0
- package/dist/avatar/descriptor.d.ts +15 -0
- package/dist/avatar/descriptor.d.ts.map +1 -1
- package/dist/avatar/descriptor.js +19 -0
- package/dist/avatar/descriptor.js.map +1 -1
- package/dist/avatar/loader.d.ts +12 -1
- package/dist/avatar/loader.d.ts.map +1 -1
- package/dist/avatar/loader.js +16 -1
- package/dist/avatar/loader.js.map +1 -1
- package/dist/avatar/share.d.ts +30 -0
- package/dist/avatar/share.d.ts.map +1 -0
- package/dist/avatar/share.js +203 -0
- package/dist/avatar/share.js.map +1 -0
- package/dist/camera/cameraRig.d.ts +4 -0
- package/dist/camera/cameraRig.d.ts.map +1 -1
- package/dist/camera/cameraRig.js +10 -1
- package/dist/camera/cameraRig.js.map +1 -1
- package/dist/camera/viewHandoff.d.ts +20 -0
- package/dist/camera/viewHandoff.d.ts.map +1 -0
- package/dist/camera/viewHandoff.js +64 -0
- package/dist/camera/viewHandoff.js.map +1 -0
- package/dist/controls/input.d.ts +29 -0
- package/dist/controls/input.d.ts.map +1 -1
- package/dist/controls/input.js +124 -1
- package/dist/controls/input.js.map +1 -1
- package/dist/controls/touch.d.ts +10 -0
- package/dist/controls/touch.d.ts.map +1 -1
- package/dist/controls/touch.js +57 -1
- package/dist/controls/touch.js.map +1 -1
- package/dist/core/worldmesh.d.ts +9 -0
- package/dist/core/worldmesh.d.ts.map +1 -1
- package/dist/core/worldmesh.js +498 -26
- package/dist/core/worldmesh.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/net/follow.d.ts +7 -0
- package/dist/net/follow.d.ts.map +1 -0
- package/dist/net/follow.js +10 -0
- package/dist/net/follow.js.map +1 -0
- package/dist/net/presence.d.ts +126 -4
- package/dist/net/presence.d.ts.map +1 -1
- package/dist/net/presence.js +472 -25
- package/dist/net/presence.js.map +1 -1
- package/dist/net/roomFull.d.ts +44 -0
- package/dist/net/roomFull.d.ts.map +1 -0
- package/dist/net/roomFull.js +147 -0
- package/dist/net/roomFull.js.map +1 -0
- package/dist/player/avatar.d.ts +17 -4
- package/dist/player/avatar.d.ts.map +1 -1
- package/dist/player/avatar.js +85 -18
- package/dist/player/avatar.js.map +1 -1
- package/dist/player/player.d.ts +6 -2
- package/dist/player/player.d.ts.map +1 -1
- package/dist/player/player.js +13 -3
- package/dist/player/player.js.map +1 -1
- package/dist/player/stroke.d.ts +11 -0
- package/dist/player/stroke.d.ts.map +1 -0
- package/dist/player/stroke.js +96 -0
- package/dist/player/stroke.js.map +1 -0
- package/dist/portals/portals.d.ts +1 -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 +53 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlay.d.ts +18 -0
- package/dist/ui/overlay.d.ts.map +1 -1
- package/dist/ui/overlay.js +166 -1
- package/dist/ui/overlay.js.map +1 -1
- package/dist/xr/session.d.ts +13 -0
- package/dist/xr/session.d.ts.map +1 -0
- package/dist/xr/session.js +33 -0
- package/dist/xr/session.js.map +1 -0
- package/package.json +1 -1
- package/src/audio/footsteps.ts +185 -0
- package/src/avatar/descriptor.ts +28 -0
- package/src/avatar/loader.ts +19 -2
- package/src/avatar/share.ts +196 -0
- package/src/camera/cameraRig.ts +10 -1
- package/src/camera/viewHandoff.ts +67 -0
- package/src/controls/input.ts +119 -1
- package/src/controls/touch.ts +59 -1
- package/src/core/worldmesh.ts +495 -25
- package/src/index.ts +14 -7
- package/src/net/follow.ts +9 -0
- package/src/net/presence.ts +548 -28
- package/src/net/roomFull.ts +160 -0
- package/src/player/avatar.ts +87 -18
- package/src/player/player.ts +13 -4
- package/src/player/stroke.ts +93 -0
- package/src/portals/portals.ts +5 -2
- package/src/types.ts +52 -9
- package/src/ui/overlay.ts +171 -1
- package/src/xr/session.ts +33 -0
package/src/ui/overlay.ts
CHANGED
|
@@ -101,6 +101,49 @@ const CSS = `
|
|
|
101
101
|
border-color: rgba(255,255,255,0.6);
|
|
102
102
|
transform: scale(1.02);
|
|
103
103
|
}
|
|
104
|
+
.wm-vr-launch {
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: max(16px, env(safe-area-inset-top));
|
|
107
|
+
left: 50%;
|
|
108
|
+
transform: translateX(-50%);
|
|
109
|
+
pointer-events: auto;
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
letter-spacing: .06em;
|
|
112
|
+
text-transform: uppercase;
|
|
113
|
+
padding: 10px 22px;
|
|
114
|
+
border: 1px solid rgba(255,255,255,0.4);
|
|
115
|
+
border-radius: 999px;
|
|
116
|
+
background: rgba(8,11,16,0.72);
|
|
117
|
+
color: #fff;
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
font-family: inherit;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
touch-action: manipulation;
|
|
122
|
+
-webkit-tap-highlight-color: transparent;
|
|
123
|
+
}
|
|
124
|
+
.wm-vr-launch:hover, .wm-vr-launch:active {
|
|
125
|
+
background: rgba(8,11,16,0.9);
|
|
126
|
+
border-color: rgba(255,255,255,0.7);
|
|
127
|
+
}
|
|
128
|
+
.wm-overlay:not([data-locked="true"]) .wm-vr-launch,
|
|
129
|
+
.wm-overlay[data-xr="true"] .wm-vr-launch,
|
|
130
|
+
.wm-overlay[data-xr="true"] .wm-vr-menu {
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
.wm-vr-menu {
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
letter-spacing: .06em;
|
|
136
|
+
text-transform: uppercase;
|
|
137
|
+
padding: 10px 22px;
|
|
138
|
+
border: 1px solid rgba(255,255,255,0.35);
|
|
139
|
+
border-radius: 999px;
|
|
140
|
+
background: rgba(255,255,255,0.08);
|
|
141
|
+
color: #fff;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
font-family: inherit;
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
touch-action: manipulation;
|
|
146
|
+
}
|
|
104
147
|
.wm-keys {
|
|
105
148
|
display: grid;
|
|
106
149
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -201,6 +244,55 @@ const CSS = `
|
|
|
201
244
|
transition: opacity .2s ease;
|
|
202
245
|
}
|
|
203
246
|
.wm-overlay[data-locked="true"] .wm-hint { opacity: 1; }
|
|
247
|
+
.wm-room-full {
|
|
248
|
+
position: absolute;
|
|
249
|
+
inset: 0;
|
|
250
|
+
display: none;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: center;
|
|
254
|
+
gap: 18px;
|
|
255
|
+
background: rgba(8, 11, 16, 0.22);
|
|
256
|
+
backdrop-filter: blur(8px);
|
|
257
|
+
-webkit-backdrop-filter: blur(8px);
|
|
258
|
+
pointer-events: auto;
|
|
259
|
+
z-index: 6;
|
|
260
|
+
padding: max(20px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
|
|
261
|
+
}
|
|
262
|
+
.wm-overlay[data-room-full="true"] .wm-room-full { display: flex; }
|
|
263
|
+
.wm-overlay[data-room-full="true"] .wm-lock { display: none; }
|
|
264
|
+
.wm-room-full-label {
|
|
265
|
+
margin: 0;
|
|
266
|
+
padding: 14px 36px;
|
|
267
|
+
border-radius: 4px;
|
|
268
|
+
background: rgba(160, 12, 18, 0.92);
|
|
269
|
+
color: #fff;
|
|
270
|
+
font-family: Urbanist, ui-sans-serif, system-ui, sans-serif;
|
|
271
|
+
font-size: clamp(22px, 4.4vw, 36px);
|
|
272
|
+
font-weight: 700;
|
|
273
|
+
letter-spacing: .02em;
|
|
274
|
+
text-align: center;
|
|
275
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
|
|
276
|
+
}
|
|
277
|
+
.wm-room-full-retry {
|
|
278
|
+
font-size: 13.5px;
|
|
279
|
+
letter-spacing: .14em;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
padding: 12px 28px;
|
|
282
|
+
border: 1px solid rgba(255,255,255,0.45);
|
|
283
|
+
border-radius: 999px;
|
|
284
|
+
background: rgba(255,255,255,0.12);
|
|
285
|
+
color: #fff;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
font-family: inherit;
|
|
288
|
+
font-weight: 500;
|
|
289
|
+
touch-action: manipulation;
|
|
290
|
+
-webkit-tap-highlight-color: transparent;
|
|
291
|
+
}
|
|
292
|
+
.wm-room-full-retry:hover, .wm-room-full-retry:active {
|
|
293
|
+
background: rgba(255,255,255,0.22);
|
|
294
|
+
border-color: rgba(255,255,255,0.7);
|
|
295
|
+
}
|
|
204
296
|
|
|
205
297
|
@media (max-width: 400px) {
|
|
206
298
|
.wm-lock {
|
|
@@ -278,13 +370,20 @@ export class Overlay {
|
|
|
278
370
|
readonly root: HTMLDivElement;
|
|
279
371
|
private prompt: HTMLDivElement;
|
|
280
372
|
private lock: HTMLDivElement;
|
|
373
|
+
private vrLaunch: HTMLButtonElement;
|
|
374
|
+
private vrMenu: HTMLButtonElement;
|
|
375
|
+
private roomFull: HTMLDivElement;
|
|
376
|
+
private roomFullLabel: HTMLParagraphElement;
|
|
377
|
+
private roomFullRetry: HTMLButtonElement;
|
|
281
378
|
private onEnter: (touch?: boolean) => void;
|
|
379
|
+
private onRoomFullRetry: (() => void) | null = null;
|
|
282
380
|
private deferLockPanel: boolean;
|
|
283
381
|
|
|
284
382
|
constructor(
|
|
285
383
|
options: UiOptions & {
|
|
286
384
|
onEnter: (touch?: boolean) => void;
|
|
287
385
|
onInteract?: () => void;
|
|
386
|
+
onEnterVr?: () => void;
|
|
288
387
|
},
|
|
289
388
|
) {
|
|
290
389
|
injectStyles();
|
|
@@ -299,7 +398,7 @@ export class Overlay {
|
|
|
299
398
|
this.root.dataset.locked = options.moveBeforeLock ? 'true' : 'false';
|
|
300
399
|
if (this.deferLockPanel) this.root.dataset.deferLock = 'true';
|
|
301
400
|
|
|
302
|
-
if (options.crosshair
|
|
401
|
+
if (options.crosshair === true) {
|
|
303
402
|
const crosshair = document.createElement('div');
|
|
304
403
|
crosshair.className = 'wm-crosshair';
|
|
305
404
|
this.root.appendChild(crosshair);
|
|
@@ -345,6 +444,18 @@ export class Overlay {
|
|
|
345
444
|
handleEnter(event);
|
|
346
445
|
});
|
|
347
446
|
|
|
447
|
+
const vrMenu = document.createElement('button');
|
|
448
|
+
vrMenu.type = 'button';
|
|
449
|
+
vrMenu.className = 'wm-vr-menu';
|
|
450
|
+
vrMenu.textContent = 'Enter VR';
|
|
451
|
+
vrMenu.hidden = true;
|
|
452
|
+
vrMenu.addEventListener('click', (event) => {
|
|
453
|
+
event.stopPropagation();
|
|
454
|
+
options.onEnterVr?.();
|
|
455
|
+
});
|
|
456
|
+
this.vrMenu = vrMenu;
|
|
457
|
+
content.appendChild(vrMenu);
|
|
458
|
+
|
|
348
459
|
if (options.controlsHint !== false) {
|
|
349
460
|
const keys = document.createElement('div');
|
|
350
461
|
keys.className = 'wm-keys';
|
|
@@ -382,6 +493,8 @@ export class Overlay {
|
|
|
382
493
|
const badge = document.createElement('a');
|
|
383
494
|
badge.className = 'wm-badge';
|
|
384
495
|
badge.href = options.hubUrl ?? 'https://worldmesh.net';
|
|
496
|
+
// Shown inside a hub's door: back to the hub replaces the whole page, not the frame.
|
|
497
|
+
if (window.parent !== window) badge.target = '_top';
|
|
385
498
|
const dot = document.createElement('span');
|
|
386
499
|
dot.className = 'wm-badge-dot';
|
|
387
500
|
const label = document.createElement('span');
|
|
@@ -395,9 +508,53 @@ export class Overlay {
|
|
|
395
508
|
hint.textContent = isTouch ? 'Tap ⏸ to open the menu' : 'Esc to open the menu';
|
|
396
509
|
this.root.appendChild(hint);
|
|
397
510
|
|
|
511
|
+
const vrLaunch = document.createElement('button');
|
|
512
|
+
vrLaunch.type = 'button';
|
|
513
|
+
vrLaunch.className = 'wm-vr-launch';
|
|
514
|
+
vrLaunch.textContent = 'Enter VR';
|
|
515
|
+
vrLaunch.hidden = true;
|
|
516
|
+
vrLaunch.addEventListener('click', (event) => {
|
|
517
|
+
event.stopPropagation();
|
|
518
|
+
options.onEnterVr?.();
|
|
519
|
+
});
|
|
520
|
+
this.vrLaunch = vrLaunch;
|
|
521
|
+
this.root.appendChild(vrLaunch);
|
|
522
|
+
|
|
523
|
+
const roomFull = document.createElement('div');
|
|
524
|
+
roomFull.className = 'wm-room-full';
|
|
525
|
+
roomFull.hidden = true;
|
|
526
|
+
const roomFullLabel = document.createElement('p');
|
|
527
|
+
roomFullLabel.className = 'wm-room-full-label';
|
|
528
|
+
const roomFullRetry = document.createElement('button');
|
|
529
|
+
roomFullRetry.type = 'button';
|
|
530
|
+
roomFullRetry.className = 'wm-room-full-retry';
|
|
531
|
+
roomFullRetry.textContent = 'Try again';
|
|
532
|
+
roomFullRetry.hidden = true;
|
|
533
|
+
roomFullRetry.addEventListener('click', (event) => {
|
|
534
|
+
event.stopPropagation();
|
|
535
|
+
this.onRoomFullRetry?.();
|
|
536
|
+
});
|
|
537
|
+
roomFull.append(roomFullLabel, roomFullRetry);
|
|
538
|
+
this.roomFull = roomFull;
|
|
539
|
+
this.roomFullLabel = roomFullLabel;
|
|
540
|
+
this.roomFullRetry = roomFullRetry;
|
|
541
|
+
this.root.appendChild(roomFull);
|
|
542
|
+
|
|
398
543
|
document.body.appendChild(this.root);
|
|
399
544
|
}
|
|
400
545
|
|
|
546
|
+
/** Show Enter VR once the browser reports immersive-vr support. */
|
|
547
|
+
setVrAvailable(available: boolean): void {
|
|
548
|
+
this.vrLaunch.hidden = !available;
|
|
549
|
+
this.vrMenu.hidden = !available;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Hide the 2D Enter VR controls while the headset is presenting. */
|
|
553
|
+
setVrPresenting(presenting: boolean): void {
|
|
554
|
+
this.root.dataset.xr = String(presenting);
|
|
555
|
+
if (presenting) this.setLocked(true);
|
|
556
|
+
}
|
|
557
|
+
|
|
401
558
|
/** The pause screen, with the control list, is on screen. */
|
|
402
559
|
isPaused(): boolean {
|
|
403
560
|
return this.root.dataset.lockReady === 'true' && this.root.dataset.locked !== 'true';
|
|
@@ -428,6 +585,19 @@ export class Overlay {
|
|
|
428
585
|
if (content instanceof HTMLElement) content.style.visibility = '';
|
|
429
586
|
}
|
|
430
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Presence refused the join because the room is full. Soft-blur the scene
|
|
590
|
+
* and show the same red banner language as a full world door.
|
|
591
|
+
*/
|
|
592
|
+
setRoomFull(label: string | null, options?: { onRetry?: () => void }): void {
|
|
593
|
+
const full = Boolean(label);
|
|
594
|
+
this.root.dataset.roomFull = String(full);
|
|
595
|
+
this.roomFull.hidden = !full;
|
|
596
|
+
this.roomFullLabel.textContent = label ?? '';
|
|
597
|
+
this.onRoomFullRetry = options?.onRetry ?? null;
|
|
598
|
+
this.roomFullRetry.hidden = !this.onRoomFullRetry;
|
|
599
|
+
}
|
|
600
|
+
|
|
431
601
|
setPrompt(text: string | null): void {
|
|
432
602
|
if (!text) {
|
|
433
603
|
this.prompt.dataset.visible = 'false';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WebGLRenderer } from 'three';
|
|
2
|
+
|
|
3
|
+
export const IMMERSIVE_VR = 'immersive-vr';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether this browser can start a WebXR immersive-vr session.
|
|
7
|
+
* Headset browsers resolve true; ordinary desktop Chrome resolves false.
|
|
8
|
+
*/
|
|
9
|
+
export async function immersiveVrSupported(): Promise<boolean> {
|
|
10
|
+
const xr = navigator.xr;
|
|
11
|
+
if (!xr?.isSessionSupported) return false;
|
|
12
|
+
try {
|
|
13
|
+
return await xr.isSessionSupported(IMMERSIVE_VR);
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ask the browser for an immersive VR session and hand it to Three.js.
|
|
21
|
+
* Must run from a user gesture (the Enter VR button).
|
|
22
|
+
*/
|
|
23
|
+
export async function requestImmersiveVr(renderer: WebGLRenderer): Promise<XRSession> {
|
|
24
|
+
const xr = navigator.xr;
|
|
25
|
+
if (!xr?.requestSession) throw new Error('WebXR is not available');
|
|
26
|
+
renderer.xr.enabled = true;
|
|
27
|
+
renderer.xr.setReferenceSpaceType('local-floor');
|
|
28
|
+
const session = await xr.requestSession(IMMERSIVE_VR, {
|
|
29
|
+
optionalFeatures: ['local-floor', 'bounded-floor'],
|
|
30
|
+
});
|
|
31
|
+
await renderer.xr.setSession(session);
|
|
32
|
+
return session;
|
|
33
|
+
}
|