@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/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The shared [WorldMesh](https://github.com/eliaswillnat/worldmesh) navigation
|
|
4
4
|
runtime: controls, movement, camera, player, abilities and portals.
|
|
5
5
|
|
|
6
|
-
WorldMesh is
|
|
6
|
+
WorldMesh is an open network for independently hosted 3D worlds, connected across the web. It
|
|
7
7
|
supplies the one thing separately hosted worlds need in common — a navigation
|
|
8
8
|
standard — so that moving through an unfamiliar world feels like moving through
|
|
9
9
|
a familiar one. This package is that standard, as code.
|
|
@@ -45,6 +45,24 @@ devices, the shared overlay and portals between worlds.
|
|
|
45
45
|
Removing the `createWorldMesh` call leaves you with the Three.js project you
|
|
46
46
|
started with.
|
|
47
47
|
|
|
48
|
+
## Visitors' own avatars (optional)
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
createWorldMesh({ scene, camera, renderer, avatar: { source: 'worldmesh' } });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Visitors who picked a character in their WorldMesh Avatar Wallet (VRoid Hub,
|
|
55
|
+
at3d) arrive with a short-lived ticket in the URL fragment. The runtime removes
|
|
56
|
+
it from the address bar, asks the hub for a descriptor, and loads the VRM or
|
|
57
|
+
glTF straight from the platform that hosts it. Portals carry the ticket on.
|
|
58
|
+
Without a ticket, or if anything fails, the player keeps the default body.
|
|
59
|
+
Your world never receives the visitor's account, session or platform tokens.
|
|
60
|
+
|
|
61
|
+
Already have a model URL? Pass `avatar: { source: 'descriptor', descriptor }`,
|
|
62
|
+
or call `world.loadAvatar(descriptor)` / `world.clearAvatar()` at any time.
|
|
63
|
+
`avatar:load` and `avatar:error` events report the outcome. The VRM and glTF
|
|
64
|
+
loaders (`@pixiv/three-vrm`) are only downloaded when an avatar is shown.
|
|
65
|
+
|
|
48
66
|
## What is exported
|
|
49
67
|
|
|
50
68
|
| | |
|
|
@@ -54,8 +72,9 @@ started with.
|
|
|
54
72
|
| `MovementController`, `CollisionWorld`, `DEFAULT_TUNING` | movement and collision |
|
|
55
73
|
| `CameraRig`, `Player`, `createDefaultAvatar`, `AVATAR_EXPRESSIONS` | camera and figure |
|
|
56
74
|
| `PortalManager`, `buildTravelUrl`, `getReferringWorld` | travel between worlds |
|
|
75
|
+
| `loadAvatarModel`, `resolveWorldMeshAvatar`, `parseAvatarDescriptor`, `AvatarDescriptor` | visitors' own avatars |
|
|
57
76
|
| `Overlay`, `Emitter` | the shared overlay, a tiny typed emitter |
|
|
58
|
-
| `
|
|
77
|
+
| `Presence`, `DEFAULT_PRESENCE_SERVER`, `NetworkAdapter` | multiplayer (`multiplayer: true`) and the seam for your own |
|
|
59
78
|
| `WORLDMESH_VERSION`, `WORLDMESH_PROTOCOL` | what your world is speaking |
|
|
60
79
|
|
|
61
80
|
Full option, handle and event reference:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only thing the runtime knows about external avatars: a normalized
|
|
3
|
+
* descriptor pointing at a model file the avatar's platform hosts. How it was
|
|
4
|
+
* found (which platform, which account, which OAuth flow) stays with whoever
|
|
5
|
+
* produced it — the WorldMesh hub's Avatar Wallet, or the world itself.
|
|
6
|
+
*/
|
|
7
|
+
export type AvatarFormat = 'vrm' | 'glb' | 'gltf';
|
|
8
|
+
export interface AvatarDescriptor {
|
|
9
|
+
/** Where the avatar lives, e.g. 'vroid' or 'atproto'. Informational. */
|
|
10
|
+
provider: string;
|
|
11
|
+
/** The platform's id for the avatar. Informational. */
|
|
12
|
+
avatarId: string;
|
|
13
|
+
name: string | null;
|
|
14
|
+
thumbnail: string | null;
|
|
15
|
+
format: AvatarFormat;
|
|
16
|
+
/** The model file, on the platform's own storage. May be short-lived. */
|
|
17
|
+
modelUrl: string;
|
|
18
|
+
/** When `modelUrl` stops working (ISO 8601), or null. */
|
|
19
|
+
expiresAt: string | null;
|
|
20
|
+
/** The avatar's public page, for credit where its license asks for it. */
|
|
21
|
+
sourceUrl: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** Model URLs must be https, except on a local development machine. */
|
|
24
|
+
export declare function isLoadableUrl(raw: unknown): raw is string;
|
|
25
|
+
/** Accepts a descriptor from the network only if it has the right shape. */
|
|
26
|
+
export declare function parseAvatarDescriptor(value: unknown): AvatarDescriptor | null;
|
|
27
|
+
//# sourceMappingURL=descriptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptor.d.ts","sourceRoot":"","sources":["../../src/avatar/descriptor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAID,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,MAAM,CASzD;AAED,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAe7E"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only thing the runtime knows about external avatars: a normalized
|
|
3
|
+
* descriptor pointing at a model file the avatar's platform hosts. How it was
|
|
4
|
+
* found (which platform, which account, which OAuth flow) stays with whoever
|
|
5
|
+
* produced it — the WorldMesh hub's Avatar Wallet, or the world itself.
|
|
6
|
+
*/
|
|
7
|
+
const FORMATS = ['vrm', 'glb', 'gltf'];
|
|
8
|
+
/** Model URLs must be https, except on a local development machine. */
|
|
9
|
+
export function isLoadableUrl(raw) {
|
|
10
|
+
if (typeof raw !== 'string')
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(raw);
|
|
14
|
+
if (url.protocol === 'https:')
|
|
15
|
+
return true;
|
|
16
|
+
return url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Accepts a descriptor from the network only if it has the right shape. */
|
|
23
|
+
export function parseAvatarDescriptor(value) {
|
|
24
|
+
if (!value || typeof value !== 'object')
|
|
25
|
+
return null;
|
|
26
|
+
const d = value;
|
|
27
|
+
if (typeof d.format !== 'string' || !FORMATS.includes(d.format) || !isLoadableUrl(d.modelUrl))
|
|
28
|
+
return null;
|
|
29
|
+
const text = (v) => (typeof v === 'string' ? v : null);
|
|
30
|
+
return {
|
|
31
|
+
provider: text(d.provider) ?? 'unknown',
|
|
32
|
+
avatarId: text(d.avatarId) ?? '',
|
|
33
|
+
name: text(d.name),
|
|
34
|
+
thumbnail: isLoadableUrl(d.thumbnail) ? d.thumbnail : null,
|
|
35
|
+
format: d.format,
|
|
36
|
+
modelUrl: d.modelUrl,
|
|
37
|
+
expiresAt: text(d.expiresAt),
|
|
38
|
+
sourceUrl: isLoadableUrl(d.sourceUrl) ? d.sourceUrl : null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=descriptor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptor.js","sourceRoot":"","sources":["../../src/avatar/descriptor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,MAAM,OAAO,GAAsB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAE1D,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3C,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAChG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3G,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,SAAS;QACvC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE;QAChC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QAClB,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;QAC1D,MAAM,EAAE,CAAC,CAAC,MAAsB;QAChC,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QAC5B,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;KAC3D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AvatarDescriptor } from './descriptor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Carrying a visitor's chosen avatar between worlds.
|
|
4
|
+
*
|
|
5
|
+
* The WorldMesh hub appends an opaque, short-lived ticket to a world's URL as
|
|
6
|
+
* a fragment (`#wm-avatar=…`): fragments are never sent to servers or in
|
|
7
|
+
* Referer headers. The runtime takes it out of the address bar (so it is not
|
|
8
|
+
* bookmarked or shared), keeps it in sessionStorage for this tab, and adds it
|
|
9
|
+
* again to portal destinations so the avatar follows the visitor.
|
|
10
|
+
*
|
|
11
|
+
* A ticket only ever resolves to an avatar descriptor. It carries no session,
|
|
12
|
+
* no account and no platform credentials.
|
|
13
|
+
*/
|
|
14
|
+
export declare const AVATAR_TICKET_PARAM = "wm-avatar";
|
|
15
|
+
/** sessionStorage key. The hub writes the same key. */
|
|
16
|
+
export declare const AVATAR_TICKET_STORAGE_KEY = "worldmesh.avatarTicket";
|
|
17
|
+
/** Moves a ticket from this page's URL fragment into storage, and returns the current one. */
|
|
18
|
+
export declare function takeAvatarTicket(): string | null;
|
|
19
|
+
export declare function getAvatarTicket(): string | null;
|
|
20
|
+
export declare function setAvatarTicket(ticket: string | null): void;
|
|
21
|
+
/** Adds the current ticket to a destination URL's fragment, unless it already has one. */
|
|
22
|
+
export declare function withAvatarTicket(url: URL): URL;
|
|
23
|
+
/**
|
|
24
|
+
* Exchanges a ticket at the hub for a descriptor. Sends no cookies and no
|
|
25
|
+
* credentials. Null when the visitor has not chosen an avatar, the ticket has
|
|
26
|
+
* expired, or the hub cannot be reached.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveWorldMeshAvatar(hubUrl: string, ticket: string, timeoutMs?: number): Promise<AvatarDescriptor | null>;
|
|
29
|
+
//# sourceMappingURL=handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../../src/avatar/handoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAE/E;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAC/C,uDAAuD;AACvD,eAAO,MAAM,yBAAyB,2BAA2B,CAAC;AAGlE,8FAA8F;AAC9F,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAahD;AAED,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAO/C;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAO3D;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,CAM9C;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAsBjI"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { parseAvatarDescriptor } from './descriptor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Carrying a visitor's chosen avatar between worlds.
|
|
4
|
+
*
|
|
5
|
+
* The WorldMesh hub appends an opaque, short-lived ticket to a world's URL as
|
|
6
|
+
* a fragment (`#wm-avatar=…`): fragments are never sent to servers or in
|
|
7
|
+
* Referer headers. The runtime takes it out of the address bar (so it is not
|
|
8
|
+
* bookmarked or shared), keeps it in sessionStorage for this tab, and adds it
|
|
9
|
+
* again to portal destinations so the avatar follows the visitor.
|
|
10
|
+
*
|
|
11
|
+
* A ticket only ever resolves to an avatar descriptor. It carries no session,
|
|
12
|
+
* no account and no platform credentials.
|
|
13
|
+
*/
|
|
14
|
+
export const AVATAR_TICKET_PARAM = 'wm-avatar';
|
|
15
|
+
/** sessionStorage key. The hub writes the same key. */
|
|
16
|
+
export const AVATAR_TICKET_STORAGE_KEY = 'worldmesh.avatarTicket';
|
|
17
|
+
const TICKET_PATTERN = /^[A-Za-z0-9_-]{16,256}\.[A-Za-z0-9_-]{16,512}$/;
|
|
18
|
+
/** Moves a ticket from this page's URL fragment into storage, and returns the current one. */
|
|
19
|
+
export function takeAvatarTicket() {
|
|
20
|
+
const hash = window.location.hash.slice(1);
|
|
21
|
+
if (hash) {
|
|
22
|
+
const params = new URLSearchParams(hash);
|
|
23
|
+
const ticket = params.get(AVATAR_TICKET_PARAM);
|
|
24
|
+
if (ticket !== null) {
|
|
25
|
+
params.delete(AVATAR_TICKET_PARAM);
|
|
26
|
+
const rest = params.toString();
|
|
27
|
+
history.replaceState(history.state, '', `${window.location.pathname}${window.location.search}${rest ? `#${rest}` : ''}`);
|
|
28
|
+
if (TICKET_PATTERN.test(ticket))
|
|
29
|
+
setAvatarTicket(ticket);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return getAvatarTicket();
|
|
33
|
+
}
|
|
34
|
+
export function getAvatarTicket() {
|
|
35
|
+
try {
|
|
36
|
+
const ticket = sessionStorage.getItem(AVATAR_TICKET_STORAGE_KEY);
|
|
37
|
+
return ticket && TICKET_PATTERN.test(ticket) ? ticket : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function setAvatarTicket(ticket) {
|
|
44
|
+
try {
|
|
45
|
+
if (ticket)
|
|
46
|
+
sessionStorage.setItem(AVATAR_TICKET_STORAGE_KEY, ticket);
|
|
47
|
+
else
|
|
48
|
+
sessionStorage.removeItem(AVATAR_TICKET_STORAGE_KEY);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Storage blocked: the avatar simply does not follow to the next world.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Adds the current ticket to a destination URL's fragment, unless it already has one. */
|
|
55
|
+
export function withAvatarTicket(url) {
|
|
56
|
+
const ticket = getAvatarTicket();
|
|
57
|
+
if (ticket && !url.hash && /^https?:$/.test(url.protocol)) {
|
|
58
|
+
url.hash = `${AVATAR_TICKET_PARAM}=${ticket}`;
|
|
59
|
+
}
|
|
60
|
+
return url;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Exchanges a ticket at the hub for a descriptor. Sends no cookies and no
|
|
64
|
+
* credentials. Null when the visitor has not chosen an avatar, the ticket has
|
|
65
|
+
* expired, or the hub cannot be reached.
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveWorldMeshAvatar(hubUrl, ticket, timeoutMs = 10_000) {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(new URL('/api/account/avatar/resolve', hubUrl), {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
mode: 'cors',
|
|
74
|
+
credentials: 'omit',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ ticket }),
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
});
|
|
79
|
+
if (response.status === 401) {
|
|
80
|
+
setAvatarTicket(null);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok)
|
|
84
|
+
return null;
|
|
85
|
+
const body = (await response.json());
|
|
86
|
+
return parseAvatarDescriptor(body.avatar);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=handoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.js","sourceRoot":"","sources":["../../src/avatar/handoff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Object3D } from 'three';
|
|
2
|
+
import type { AvatarMotion } from '../player/avatar.js';
|
|
3
|
+
import type { AvatarDescriptor } from './descriptor.js';
|
|
4
|
+
/** A loaded external avatar, normalised to the WorldMesh body convention. */
|
|
5
|
+
export interface LoadedAvatar {
|
|
6
|
+
/** Feet at y = 0, facing -Z, scaled to the player's height. */
|
|
7
|
+
object: Object3D;
|
|
8
|
+
/** Call once per frame with how the player moves. */
|
|
9
|
+
animate(motion: AvatarMotion): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface LoadAvatarOptions {
|
|
13
|
+
/** Standing height to scale the model to, in metres. */
|
|
14
|
+
height: number;
|
|
15
|
+
/** Refuse models larger than this. Defaults to 40 MB. */
|
|
16
|
+
maxBytes?: number;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Downloads a VRM / glTF model straight from the URL in the descriptor (the
|
|
21
|
+
* avatar platform's own storage) and turns it into a player body. The loaders
|
|
22
|
+
* are imported on first use, so worlds that never show an external avatar
|
|
23
|
+
* never download them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadAvatarModel(descriptor: AvatarDescriptor, options: LoadAvatarOptions): Promise<LoadedAvatar>;
|
|
26
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/avatar/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+E,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAC;AACnH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,MAAM,EAAE,QAAQ,CAAC;IACjB,qDAAqD;IACrD,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,gBAAgB,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkDrH"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { AnimationMixer, Box3, Group, MathUtils, Mesh, Texture } from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Downloads a VRM / glTF model straight from the URL in the descriptor (the
|
|
4
|
+
* avatar platform's own storage) and turns it into a player body. The loaders
|
|
5
|
+
* are imported on first use, so worlds that never show an external avatar
|
|
6
|
+
* never download them.
|
|
7
|
+
*/
|
|
8
|
+
export async function loadAvatarModel(descriptor, options) {
|
|
9
|
+
const buffer = await download(descriptor.modelUrl, options.maxBytes ?? 40 * 1024 * 1024, options.signal);
|
|
10
|
+
const [{ GLTFLoader }, vrmModule] = await Promise.all([
|
|
11
|
+
import('three/examples/jsm/loaders/GLTFLoader.js'),
|
|
12
|
+
descriptor.format === 'vrm' ? import('@pixiv/three-vrm') : Promise.resolve(null),
|
|
13
|
+
]);
|
|
14
|
+
const loader = new GLTFLoader();
|
|
15
|
+
if (vrmModule)
|
|
16
|
+
loader.register((parser) => new vrmModule.VRMLoaderPlugin(parser));
|
|
17
|
+
const gltf = await loader.parseAsync(buffer, '');
|
|
18
|
+
const vrm = vrmModule ? gltf.userData.vrm : undefined;
|
|
19
|
+
if (descriptor.format === 'vrm' && (!vrm || !vrmModule))
|
|
20
|
+
throw new Error('Not a VRM model');
|
|
21
|
+
const model = vrm ? vrm.scene : gltf.scene;
|
|
22
|
+
if (vrm && vrmModule) {
|
|
23
|
+
vrmModule.VRMUtils.removeUnnecessaryVertices(model);
|
|
24
|
+
vrmModule.VRMUtils.combineSkeletons(model);
|
|
25
|
+
// VRM 0.x faces -Z; this makes it face +Z like VRM 1.0 and plain glTF.
|
|
26
|
+
vrmModule.VRMUtils.rotateVRM0(vrm);
|
|
27
|
+
}
|
|
28
|
+
model.traverse((child) => {
|
|
29
|
+
// Skinned meshes are bounded by their rest pose; animated limbs would pop out of view.
|
|
30
|
+
child.frustumCulled = false;
|
|
31
|
+
if (child.isMesh)
|
|
32
|
+
child.castShadow = true;
|
|
33
|
+
});
|
|
34
|
+
// glTF and VRM face +Z; WorldMesh bodies face -Z. Scale to the player and
|
|
35
|
+
// stand the feet on y = 0.
|
|
36
|
+
const root = new Group();
|
|
37
|
+
root.name = 'worldmesh:external-avatar';
|
|
38
|
+
const turn = new Group();
|
|
39
|
+
turn.rotation.y = Math.PI;
|
|
40
|
+
turn.add(model);
|
|
41
|
+
root.add(turn);
|
|
42
|
+
root.updateMatrixWorld(true);
|
|
43
|
+
const box = new Box3().setFromObject(root);
|
|
44
|
+
const modelHeight = box.max.y - box.min.y;
|
|
45
|
+
if (!(modelHeight > 0.05 && modelHeight < 100))
|
|
46
|
+
throw new Error('Model has no usable size');
|
|
47
|
+
const scale = options.height / modelHeight;
|
|
48
|
+
turn.scale.setScalar(scale);
|
|
49
|
+
turn.position.y = -box.min.y * scale;
|
|
50
|
+
const animate = vrm ? vrmAnimator(vrm) : clipAnimator(model, gltf.animations);
|
|
51
|
+
return {
|
|
52
|
+
object: root,
|
|
53
|
+
animate,
|
|
54
|
+
dispose() {
|
|
55
|
+
if (vrm && vrmModule)
|
|
56
|
+
vrmModule.VRMUtils.deepDispose(model);
|
|
57
|
+
else
|
|
58
|
+
disposeTree(model);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function download(url, maxBytes, signal) {
|
|
63
|
+
const response = await fetch(url, { mode: 'cors', credentials: 'omit', referrerPolicy: 'no-referrer', signal });
|
|
64
|
+
if (!response.ok)
|
|
65
|
+
throw new Error(`Model download failed (${response.status})`);
|
|
66
|
+
if (Number(response.headers.get('Content-Length') ?? '0') > maxBytes)
|
|
67
|
+
throw new Error('Model is too large');
|
|
68
|
+
if (!response.body)
|
|
69
|
+
return response.arrayBuffer();
|
|
70
|
+
const reader = response.body.getReader();
|
|
71
|
+
const chunks = [];
|
|
72
|
+
let size = 0;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const { done, value } = await reader.read();
|
|
75
|
+
if (done)
|
|
76
|
+
break;
|
|
77
|
+
size += value.byteLength;
|
|
78
|
+
if (size > maxBytes) {
|
|
79
|
+
await reader.cancel();
|
|
80
|
+
throw new Error('Model is too large');
|
|
81
|
+
}
|
|
82
|
+
chunks.push(value);
|
|
83
|
+
}
|
|
84
|
+
const bytes = new Uint8Array(size);
|
|
85
|
+
let offset = 0;
|
|
86
|
+
for (const chunk of chunks) {
|
|
87
|
+
bytes.set(chunk, offset);
|
|
88
|
+
offset += chunk.byteLength;
|
|
89
|
+
}
|
|
90
|
+
return bytes.buffer;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* VRMs arrive in a T-pose with no animation. Lower the arms and swing arms
|
|
94
|
+
* and legs with speed, through the normalised humanoid rig so it works for
|
|
95
|
+
* any VRM, then let spring bones (hair, clothes) follow.
|
|
96
|
+
*/
|
|
97
|
+
function vrmAnimator(vrm) {
|
|
98
|
+
const bone = (name) => vrm.humanoid?.getNormalizedBoneNode(name) ?? null;
|
|
99
|
+
const armL = bone('leftUpperArm');
|
|
100
|
+
const armR = bone('rightUpperArm');
|
|
101
|
+
const foreArmL = bone('leftLowerArm');
|
|
102
|
+
const foreArmR = bone('rightLowerArm');
|
|
103
|
+
const legL = bone('leftUpperLeg');
|
|
104
|
+
const legR = bone('rightUpperLeg');
|
|
105
|
+
const kneeL = bone('leftLowerLeg');
|
|
106
|
+
const kneeR = bone('rightLowerLeg');
|
|
107
|
+
// VRM 0.x rigs are mirrored on X and Z relative to VRM 1.0.
|
|
108
|
+
const mirror = vrm.meta?.metaVersion === '0' ? -1 : 1;
|
|
109
|
+
let phase = 0;
|
|
110
|
+
let stride = 0;
|
|
111
|
+
let air = 0;
|
|
112
|
+
return (motion) => {
|
|
113
|
+
if (motion.dt <= 0)
|
|
114
|
+
return;
|
|
115
|
+
const blend = 1 - Math.exp(-motion.dt * 10);
|
|
116
|
+
const walking = motion.grounded ? MathUtils.clamp(motion.speed / 4.5, 0, 1) : 0;
|
|
117
|
+
stride += (walking - stride) * blend;
|
|
118
|
+
air += ((motion.grounded ? 0 : 1) - air) * blend;
|
|
119
|
+
phase += motion.dt * (4 + motion.speed * 1.2) * (stride > 0.01 ? 1 : 0);
|
|
120
|
+
const swing = Math.sin(phase) * 0.6 * stride;
|
|
121
|
+
const armDown = 1.2 - air * 0.5;
|
|
122
|
+
if (armL)
|
|
123
|
+
armL.rotation.set(mirror * swing * 0.7, 0, mirror * -armDown);
|
|
124
|
+
if (armR)
|
|
125
|
+
armR.rotation.set(mirror * -swing * 0.7, 0, mirror * armDown);
|
|
126
|
+
if (foreArmL)
|
|
127
|
+
foreArmL.rotation.set(0, 0, mirror * -0.15);
|
|
128
|
+
if (foreArmR)
|
|
129
|
+
foreArmR.rotation.set(0, 0, mirror * 0.15);
|
|
130
|
+
if (legL)
|
|
131
|
+
legL.rotation.set(mirror * (-swing - air * 0.3), 0, 0);
|
|
132
|
+
if (legR)
|
|
133
|
+
legR.rotation.set(mirror * (swing - air * 0.1), 0, 0);
|
|
134
|
+
if (kneeL)
|
|
135
|
+
kneeL.rotation.set(mirror * (Math.max(0, swing) * 0.8 + air * 0.4), 0, 0);
|
|
136
|
+
if (kneeR)
|
|
137
|
+
kneeR.rotation.set(mirror * (Math.max(0, -swing) * 0.8 + air * 0.2), 0, 0);
|
|
138
|
+
vrm.update(motion.dt);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Plain glTF bodies: play an idle clip, and a walk/run clip while moving, if the file has them. */
|
|
142
|
+
function clipAnimator(model, clips) {
|
|
143
|
+
if (!clips.length)
|
|
144
|
+
return () => { };
|
|
145
|
+
const mixer = new AnimationMixer(model);
|
|
146
|
+
const find = (pattern) => clips.find((clip) => pattern.test(clip.name));
|
|
147
|
+
const idleClip = find(/idle|stand|breath/i) ?? clips[0];
|
|
148
|
+
const walkClip = find(/walk|run|jog/i);
|
|
149
|
+
const idle = mixer.clipAction(idleClip).play();
|
|
150
|
+
const walk = walkClip && walkClip !== idleClip ? mixer.clipAction(walkClip) : null;
|
|
151
|
+
let moving = false;
|
|
152
|
+
return (motion) => {
|
|
153
|
+
const nowMoving = motion.grounded && motion.speed > 0.5;
|
|
154
|
+
if (walk && nowMoving !== moving) {
|
|
155
|
+
moving = nowMoving;
|
|
156
|
+
const [from, to] = moving ? [idle, walk] : [walk, idle];
|
|
157
|
+
to.reset().play();
|
|
158
|
+
from.crossFadeTo(to, 0.2, false);
|
|
159
|
+
}
|
|
160
|
+
mixer.update(motion.dt);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function disposeTree(root) {
|
|
164
|
+
root.traverse((child) => {
|
|
165
|
+
if (!(child instanceof Mesh))
|
|
166
|
+
return;
|
|
167
|
+
child.geometry.dispose();
|
|
168
|
+
for (const material of Array.isArray(child.material) ? child.material : [child.material]) {
|
|
169
|
+
for (const value of Object.values(material)) {
|
|
170
|
+
if (value instanceof Texture)
|
|
171
|
+
value.dispose();
|
|
172
|
+
}
|
|
173
|
+
material.dispose();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/avatar/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAuC,MAAM,OAAO,CAAC;AAqBnH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,UAA4B,EAAE,OAA0B;IAC5F,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzG,MAAM,CAAC,EAAE,UAAU,EAAE,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACpD,MAAM,CAAC,0CAA0C,CAAC;QAClD,UAAU,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;KACjF,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;IAChC,IAAI,SAAS;QAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEjD,MAAM,GAAG,GAAG,SAAS,CAAC,CAAC,CAAE,IAAI,CAAC,QAAQ,CAAC,GAAkD,CAAC,CAAC,CAAC,SAAS,CAAC;IACtG,IAAI,UAAU,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC5F,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;IAC3C,IAAI,GAAG,IAAI,SAAS,EAAE,CAAC;QACrB,SAAS,CAAC,QAAQ,CAAC,yBAAyB,CAAC,KAAK,CAAC,CAAC;QACpD,SAAS,CAAC,QAAQ,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC3C,uEAAuE;QACvE,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACrC,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE;QACvB,uFAAuF;QACvF,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC;QAC5B,IAAK,KAAc,CAAC,MAAM;YAAE,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,0EAA0E;IAC1E,2BAA2B;IAC3B,MAAM,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;IACzB,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;IACzB,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;IAC1B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAChB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACf,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,WAAW,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,IAAI,CAAC,CAAC,WAAW,GAAG,IAAI,IAAI,WAAW,GAAG,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC5F,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,WAAW,CAAC;IAC3C,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;IAErC,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9E,OAAO;QACL,MAAM,EAAE,IAAI;QACZ,OAAO;QACP,OAAO;YACL,IAAI,GAAG,IAAI,SAAS;gBAAE,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;;gBACvD,WAAW,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,GAAW,EAAE,QAAgB,EAAE,MAAoB;IACzE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC;IAChH,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAChF,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,GAAG,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC5G,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAO,QAAQ,CAAC,WAAW,EAAE,CAAC;IAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;IACzC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,SAAS,CAAC;QACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,IAAI;YAAE,MAAM;QAChB,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC;QACzB,IAAI,IAAI,GAAG,QAAQ,EAAE,CAAC;YACpB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;IAC7B,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,GAAmC;IACtD,MAAM,IAAI,GAAG,CAAC,IAA8E,EAAE,EAAE,CAC9F,GAAG,CAAC,QAAQ,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;IACpC,4DAA4D;IAC5D,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,GAAG,GAAG,CAAC,CAAC;IAEZ,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,IAAI,MAAM,CAAC,EAAE,IAAI,CAAC;YAAE,OAAO;QAC3B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,MAAM,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,KAAK,CAAC;QACrC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC;QACjD,KAAK,IAAI,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC;QAE7C,MAAM,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;QAChC,IAAI,IAAI;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,GAAG,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC;QACxE,IAAI,IAAI;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;QACxE,IAAI,QAAQ;YAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,QAAQ;YAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;QACzD,IAAI,IAAI;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACjE,IAAI,IAAI;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAChE,IAAI,KAAK;YAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACrF,IAAI,KAAK;YAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACtF,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxB,CAAC,CAAC;AACJ,CAAC;AAED,oGAAoG;AACpG,SAAS,YAAY,CAAC,KAAe,EAAE,KAAsC;IAC3E,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/C,MAAM,IAAI,GAA2B,QAAQ,IAAI,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3G,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC;QACxD,IAAI,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,CAAC;YACjC,MAAM,GAAG,SAAS,CAAC;YACnB,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACxD,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC;YAClB,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QACnC,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,IAAc;IACjC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE;QACtB,IAAI,CAAC,CAAC,KAAK,YAAY,IAAI,CAAC;YAAE,OAAO;QACrC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;QACzB,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzF,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5C,IAAI,KAAK,YAAY,OAAO;oBAAE,KAAK,CAAC,OAAO,EAAE,CAAC;YAChD,CAAC;YACD,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/controls/input.d.ts
CHANGED
|
@@ -3,7 +3,14 @@ export interface InputOptions {
|
|
|
3
3
|
/** The element that receives pointer lock. */
|
|
4
4
|
element: HTMLElement;
|
|
5
5
|
keymap?: Partial<Keymap>;
|
|
6
|
+
/** Keyboard and joysticks work before the cursor is captured. */
|
|
7
|
+
moveBeforeLock?: boolean;
|
|
6
8
|
onPointerLockChange?: (locked: boolean) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Escape toggles the pause screen. Return true when this press captures
|
|
11
|
+
* the mouse again, so the key's usual "show the cursor" action can be cancelled.
|
|
12
|
+
*/
|
|
13
|
+
onEscape?: () => boolean | void;
|
|
7
14
|
}
|
|
8
15
|
/**
|
|
9
16
|
* Keyboard + pointer-lock mouse input with mobile touch controls support.
|
|
@@ -19,6 +26,7 @@ export declare class Input {
|
|
|
19
26
|
wheelDelta: number;
|
|
20
27
|
locked: boolean;
|
|
21
28
|
private element;
|
|
29
|
+
private moveBeforeLock;
|
|
22
30
|
private pressed;
|
|
23
31
|
/** Actions that went down this frame and have not been consumed yet. */
|
|
24
32
|
private justPressed;
|
|
@@ -26,6 +34,7 @@ export declare class Input {
|
|
|
26
34
|
private pendingDigit;
|
|
27
35
|
private codeToActions;
|
|
28
36
|
private onPointerLockChange?;
|
|
37
|
+
private onEscape?;
|
|
29
38
|
private touch?;
|
|
30
39
|
private disposed;
|
|
31
40
|
constructor(options: InputOptions);
|
|
@@ -52,11 +61,27 @@ export declare class Input {
|
|
|
52
61
|
/** Drop stale edge-triggers at the end of a frame. */
|
|
53
62
|
endFrame(): void;
|
|
54
63
|
dispose(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Escape toggles the pause screen. The browser's own Esc action is what
|
|
66
|
+
* puts the cursor back, so that is left alone. It is cancelled only when
|
|
67
|
+
* this press captures the mouse again, or the cursor would vanish immediately.
|
|
68
|
+
*/
|
|
69
|
+
private handleEscape;
|
|
55
70
|
private handleKeyDown;
|
|
56
71
|
private handleKeyUp;
|
|
57
72
|
/** Losing focus must not leave a key stuck down. */
|
|
58
73
|
private handleBlur;
|
|
74
|
+
/**
|
|
75
|
+
* A click captures the cursor for looking. Touch is left alone: the joysticks
|
|
76
|
+
* already have the finger, and they do not need a tap before they work.
|
|
77
|
+
*/
|
|
78
|
+
private handlePointerDown;
|
|
79
|
+
/**
|
|
80
|
+
* Captured: every movement turns the camera. Before the first click, with
|
|
81
|
+
* moveBeforeLock, the visible cursor turns it too, so looking works from the start.
|
|
82
|
+
*/
|
|
59
83
|
private handleMouseMove;
|
|
84
|
+
private hoverLook;
|
|
60
85
|
private handleWheel;
|
|
61
86
|
private handlePointerLockChange;
|
|
62
87
|
private handlePointerLockError;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/controls/input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIvD,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/controls/input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIvD,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,iEAAiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,qBAAa,KAAK;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,WAAW,SAAK;IAChB,WAAW,SAAK;IAChB,sDAAsD;IACtD,UAAU,SAAK;IACf,MAAM,UAAS;IAEf,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,OAAO,CAAqB;IACpC,wEAAwE;IACxE,OAAO,CAAC,WAAW,CAA0B;IAC7C,sDAAsD;IACtD,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,mBAAmB,CAAC,CAA4B;IACxD,OAAO,CAAC,QAAQ,CAAC,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAC,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAS;gBAEb,OAAO,EAAE,YAAY;IAqCjC,kBAAkB,CAAC,cAAc,UAAQ,GAAG,IAAI;IA4BhD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAYhC,eAAe,IAAI,IAAI;IAevB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIxC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO;IAMpC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO;IAOrC,2EAA2E;IAC3E,YAAY,IAAI,MAAM,GAAG,IAAI;IAM7B,iEAAiE;IACjE,WAAW,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;IAgBvC,8EAA8E;IAC9E,QAAQ,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAerD,sDAAsD;IACtD,QAAQ,IAAI,IAAI;IAKhB,OAAO,IAAI,IAAI;IAgBf;;;;OAIG;IACH,OAAO,CAAC,YAAY,CAKlB;IAEF,OAAO,CAAC,aAAa,CAcnB;IAEF,OAAO,CAAC,WAAW,CAEjB;IAEF,oDAAoD;IACpD,OAAO,CAAC,UAAU,CAGhB;IAEF;;;OAGG;IACH,OAAO,CAAC,iBAAiB,CAMvB;IAEF;;;OAGG;IACH,OAAO,CAAC,eAAe,CAIrB;IAEF,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,WAAW,CAGjB;IAEF,OAAO,CAAC,uBAAuB,CAQ7B;IAEF,OAAO,CAAC,sBAAsB,CAK5B;CACH"}
|