@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.10
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/CHANGELOG.md +163 -0
- package/LICENSE +21 -21
- package/README.md +203 -139
- package/dist/auto.d.ts +8 -8
- package/dist/auto.js +3 -3
- package/dist/avatar-pixi.d.ts +165 -0
- package/dist/avatar-pixi.d.ts.map +1 -0
- package/dist/avatar-pixi.js +299 -0
- package/dist/avatar-pixi.js.map +1 -0
- package/dist/avatar-svg.d.ts +4 -0
- package/dist/avatar-svg.js +5 -0
- package/dist/avatar-three.d.ts +69 -0
- package/dist/avatar-three.d.ts.map +1 -0
- package/dist/avatar-three.js +49 -0
- package/dist/avatar-three.js.map +1 -0
- package/dist/avatar.d.ts +7 -0
- package/dist/avatar.js +500 -0
- package/dist/avatar.js.map +1 -0
- package/dist/daily.d.ts +2 -2
- package/dist/doctor/cli.mjs +119 -28
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +3 -3
- package/dist/input.d.ts +2 -2
- package/dist/input.js +2 -2
- package/dist/mp.d.ts +3 -3
- package/dist/race.d.ts +1 -1
- package/dist/rooms.d.ts +3 -3
- package/dist/rooms.js +1 -1
- package/dist/shared/animator.d.ts +104 -0
- package/dist/shared/animator.d.ts.map +1 -0
- package/dist/shared/art.js +1946 -0
- package/dist/shared/art.js.map +1 -0
- package/dist/shared/atlas.d.ts +138 -0
- package/dist/shared/atlas.d.ts.map +1 -0
- package/dist/shared/avatar.d.ts +123 -0
- package/dist/shared/avatar.d.ts.map +1 -0
- package/dist/shared/bridge.js +18 -4
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +145 -19
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +138 -25
- package/dist/shared/daily.js.map +1 -1
- package/dist/shared/defineProperty.js +39 -0
- package/dist/shared/describe.d.ts +32 -0
- package/dist/shared/describe.d.ts.map +1 -0
- package/dist/shared/describe.js +109 -0
- package/dist/shared/describe.js.map +1 -0
- package/dist/shared/globals.js +9 -42
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1122 -46
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.d.ts +74 -14
- package/dist/shared/input.d.ts.map +1 -1
- package/dist/shared/input.js +179 -25
- package/dist/shared/input.js.map +1 -1
- package/dist/shared/json-guards.js +48 -0
- package/dist/shared/json-guards.js.map +1 -0
- package/dist/shared/latest-state.js +1 -1
- package/dist/shared/live-canvas.js +62 -0
- package/dist/shared/live-canvas.js.map +1 -0
- package/dist/shared/mp.js +10 -2
- package/dist/shared/mp.js.map +1 -1
- package/dist/shared/multiplayer-types.d.ts +15 -2
- package/dist/shared/multiplayer-types.d.ts.map +1 -1
- package/dist/shared/overlays.js +800 -0
- package/dist/shared/overlays.js.map +1 -0
- package/dist/shared/page-stack.js +2 -1
- package/dist/shared/page-stack.js.map +1 -1
- package/dist/shared/pose-key.js +249 -0
- package/dist/shared/pose-key.js.map +1 -0
- package/dist/shared/race-types.d.ts +1 -1
- package/dist/shared/race.js +1 -1
- package/dist/shared/room-types.d.ts +28 -3
- package/dist/shared/room-types.d.ts.map +1 -1
- package/dist/shared/rooms.d.ts.map +1 -1
- package/dist/shared/rooms.js +23 -4
- package/dist/shared/rooms.js.map +1 -1
- package/dist/shared/runtime-opener.js +1 -1
- package/dist/shared/scene.d.ts +90 -0
- package/dist/shared/scene.d.ts.map +1 -0
- package/dist/shared/social.d.ts +90 -9
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/social.js +6 -1
- package/dist/shared/social.js.map +1 -1
- package/dist/shared/src.js +4563 -667
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/svg-markup.js +2354 -0
- package/dist/shared/svg-markup.js.map +1 -0
- package/dist/shared/svg.js +27 -0
- package/dist/shared/svg.js.map +1 -0
- package/dist/shared/types.d.ts +174 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/wire-types.d.ts +6 -2
- package/dist/shared/wire-types.d.ts.map +1 -1
- package/dist/social.d.ts +5 -5
- package/dist/social.js +2 -2
- package/dist/turns-rules.d.ts +2 -0
- package/dist/turns-rules.js +1 -0
- package/dist/turns.d.ts +61 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +183 -0
- package/dist/turns.js.map +1 -0
- package/package.json +125 -89
- package/src/auto.ts +39 -39
- package/src/avatar/adapters/pixi-avatar.ts +73 -0
- package/src/avatar/adapters/pixi-crowd.ts +329 -0
- package/src/avatar/adapters/pixi-types.ts +84 -0
- package/src/avatar/adapters/three.ts +126 -0
- package/src/avatar/animator/animator.ts +285 -0
- package/src/avatar/animator/reactions.ts +44 -0
- package/src/avatar/animator/schedule.ts +73 -0
- package/src/avatar/art/beard.ts +49 -0
- package/src/avatar/art/glasses.ts +67 -0
- package/src/avatar/art/hair.ts +254 -0
- package/src/avatar/art/headwear.ts +75 -0
- package/src/avatar/art/index.ts +21 -0
- package/src/avatar/art/shape.ts +87 -0
- package/src/avatar/art-store.ts +36 -0
- package/src/avatar/atlas.ts +246 -0
- package/src/avatar/catalog-view.ts +14 -0
- package/src/avatar/colour.ts +109 -0
- package/src/avatar/describe.ts +79 -0
- package/src/avatar/face.ts +187 -0
- package/src/avatar/geom.ts +173 -0
- package/src/avatar/layout.ts +47 -0
- package/src/avatar/look-like.ts +52 -0
- package/src/avatar/outputs/bitmap.ts +75 -0
- package/src/avatar/outputs/canvas-paint.ts +177 -0
- package/src/avatar/outputs/canvas.ts +62 -0
- package/src/avatar/outputs/custom-element.ts +140 -0
- package/src/avatar/outputs/element.ts +58 -0
- package/src/avatar/outputs/live-canvas.ts +57 -0
- package/src/avatar/outputs/overlay-svg.ts +22 -0
- package/src/avatar/outputs/pose-key.ts +30 -0
- package/src/avatar/outputs/svg-markup.ts +113 -0
- package/src/avatar/outputs/svg.ts +41 -0
- package/src/avatar/overlays.ts +128 -0
- package/src/avatar/render/body.ts +116 -0
- package/src/avatar/render/colours.ts +81 -0
- package/src/avatar/render/compose.ts +320 -0
- package/src/avatar/render/context.ts +117 -0
- package/src/avatar/render/eyes.ts +78 -0
- package/src/avatar/render/eyewear.ts +118 -0
- package/src/avatar/render/frame.ts +73 -0
- package/src/avatar/render/head.ts +81 -0
- package/src/avatar/render/mouth.ts +86 -0
- package/src/avatar/render/props.ts +91 -0
- package/src/avatar/render/types.ts +93 -0
- package/src/avatar/render/walker-parts.ts +121 -0
- package/src/avatar/render/walker.ts +199 -0
- package/src/avatar/scene.ts +119 -0
- package/src/avatar-pixi.ts +20 -0
- package/src/avatar-svg.ts +21 -0
- package/src/avatar-three.ts +18 -0
- package/src/avatar.ts +63 -0
- package/src/core/avatar/avatar-cache.ts +95 -0
- package/src/core/avatar/avatar-ref.ts +34 -0
- package/src/core/avatar/avatar-types.ts +43 -0
- package/src/core/avatar/hub-avatar.ts +308 -0
- package/src/core/connection.ts +408 -299
- package/src/core/errors.ts +160 -147
- package/src/core/game-data.ts +40 -38
- package/src/core/hub-economy.ts +216 -179
- package/src/core/hub-progress.ts +269 -205
- package/src/core/hub.ts +352 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/cloud-saves.ts +62 -0
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
- package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
- package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
- package/src/core/offline/hub-kinds/game-fields.ts +2 -0
- package/src/core/offline/hub-kinds/index.ts +52 -0
- package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
- package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
- package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
- package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
- package/src/core/offline/kind-adapters.ts +48 -0
- package/src/core/offline/local-inventory.ts +64 -0
- package/src/core/offline/local-store.ts +304 -261
- package/src/core/offline/offline-api.ts +65 -0
- package/src/core/offline/outbox-entry.ts +59 -0
- package/src/core/offline/outbox-kinds.ts +78 -0
- package/src/core/offline/outbox-merge.ts +39 -0
- package/src/core/offline/outbox-replayer.ts +141 -0
- package/src/core/offline/outbox-store.ts +85 -0
- package/src/core/offline/outbox.ts +232 -0
- package/src/core/offline/queue.ts +126 -171
- package/src/core/offline/replay-lease.ts +87 -0
- package/src/core/offline/replay-policy.ts +52 -0
- package/src/core/page-account.ts +121 -0
- package/src/core/page.ts +87 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +163 -152
- package/src/core/rest/routes.gen.ts +144 -88
- package/src/core/resume/hub-resume.ts +286 -0
- package/src/core/resume/local-resume.ts +130 -0
- package/src/core/resume/resume-list.ts +65 -0
- package/src/core/resume/resume-request.ts +52 -0
- package/src/core/resume/resume-types.ts +64 -0
- package/src/core/saved/daily-saves.ts +196 -0
- package/src/core/saved/daily-seed.ts +16 -0
- package/src/core/saved/hub-saved.ts +221 -0
- package/src/core/saved/local-saved.ts +122 -0
- package/src/core/saved/saved-list.ts +129 -0
- package/src/core/saved/saved-open.ts +104 -0
- package/src/core/saved/saved-order.ts +49 -0
- package/src/core/saved/saved-types.ts +24 -0
- package/src/core/storage/json-guards.ts +80 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/turns/hub-turns.ts +161 -0
- package/src/core/turns/turn-game.ts +510 -0
- package/src/core/turns/your-turn-buzz.ts +69 -0
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +2 -2
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +276 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +105 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +86 -76
- package/src/doctor/cli.ts +101 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/doctor/world-check.ts +61 -0
- package/src/globals.ts +115 -110
- package/src/index.ts +68 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/focus-ring.ts +13 -0
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +171 -164
- package/src/input/input-system.ts +406 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/modality.ts +122 -0
- package/src/input/nav-state.ts +200 -191
- package/src/input/navigator.ts +182 -171
- package/src/input/on-screen-controls.ts +101 -101
- package/src/input/page-control.ts +43 -43
- package/src/input/page-input.ts +193 -193
- package/src/input/player-remaps.ts +77 -77
- package/src/input/source-values.ts +116 -116
- package/src/input/spatial.ts +47 -47
- package/src/input/types.ts +331 -331
- package/src/input/zero-config.ts +164 -177
- package/src/input.ts +26 -22
- package/src/mp.ts +10 -10
- package/src/overlay/controls-panel.ts +163 -163
- package/src/overlay/dom.ts +46 -46
- package/src/overlay/layout-clusters.ts +162 -162
- package/src/overlay/layout-geometry.ts +136 -136
- package/src/overlay/layout.ts +203 -203
- package/src/overlay/listener-scope.ts +23 -23
- package/src/overlay/on-screen-keyboard.ts +137 -137
- package/src/overlay/overlay.ts +234 -208
- package/src/overlay/styles.ts +13 -12
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +126 -121
- package/src/realtime/cross-bundle.ts +29 -29
- package/src/realtime/fallback-notice.ts +48 -48
- package/src/realtime/hub-link.ts +65 -65
- package/src/realtime/latest-state.ts +33 -33
- package/src/realtime/multiplayer-forwarder.ts +40 -28
- package/src/realtime/multiplayer-types.ts +66 -52
- package/src/realtime/page-runtime.ts +60 -60
- package/src/realtime/race-forwarder.ts +59 -59
- package/src/realtime/race-types.ts +132 -132
- package/src/realtime/room-types.ts +151 -125
- package/src/realtime/rooms-forwarder.ts +104 -79
- package/src/realtime/runtime-api.ts +74 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +77 -72
- package/src/realtime/social-types.ts +86 -78
- package/src/realtime/types.ts +54 -53
- package/src/realtime/wire-types.ts +79 -72
- package/src/realtime/world-types.ts +20 -0
- package/src/rooms.ts +10 -10
- package/src/social.ts +19 -18
- package/src/turns-kit/check-rules.ts +281 -0
- package/src/turns-rules.ts +12 -0
- package/src/turns.ts +14 -0
- package/src/types.ts +13 -13
- package/src/uistack/page-stack.ts +45 -45
- package/src/uistack/play-suspension.ts +82 -82
- package/src/uistack/types.ts +76 -76
- package/src/uistack/ui-stack.ts +144 -144
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The walker's drawing parts (`figure: 'walker'`): limbs as outlined round
|
|
3
|
+
* strokes, the shirt, trousers and shoes. Colours come from the look: the
|
|
4
|
+
* shirt is the body colour, the trousers and shoes are picked from the body
|
|
5
|
+
* colour so they always stand apart from the shirt.
|
|
6
|
+
*/
|
|
7
|
+
import { lightness, outline, shade } from '../colour';
|
|
8
|
+
import { Path } from '../geom';
|
|
9
|
+
import { ellipse, g, path, sc, tr, type SceneNode } from '../scene';
|
|
10
|
+
import type { Colours } from './colours';
|
|
11
|
+
import { LINE } from './context';
|
|
12
|
+
|
|
13
|
+
/** A point in rig units. */
|
|
14
|
+
export type Vec = readonly [number, number];
|
|
15
|
+
|
|
16
|
+
/** The walker's clothes, derived from a look's colours. */
|
|
17
|
+
export interface Outfit {
|
|
18
|
+
readonly trousers: string;
|
|
19
|
+
readonly trousersInk: string;
|
|
20
|
+
readonly shoes: string;
|
|
21
|
+
readonly shoesInk: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TROUSERS = ['#34466e', '#2e323f', '#6a5642', '#4a3a60', '#3c5a4c', '#5a3a3a'] as const;
|
|
25
|
+
const SHOES = ['#2c2430', '#f1ece2', '#5a3a2a', '#2c2430'] as const;
|
|
26
|
+
|
|
27
|
+
/** A small stable hash of a colour string. */
|
|
28
|
+
function hashOf(text: string): number {
|
|
29
|
+
let hash = 7;
|
|
30
|
+
for (let i = 0; i < text.length; i++) {
|
|
31
|
+
hash = (hash * 31 + text.charCodeAt(i)) >>> 0;
|
|
32
|
+
}
|
|
33
|
+
return hash;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Trousers and shoes for a shirt colour: a trouser tone at least a little lighter or darker than the shirt. */
|
|
37
|
+
export function outfitFor(c: Colours): Outfit {
|
|
38
|
+
const hash = hashOf(c.body);
|
|
39
|
+
const shirt = lightness(c.body);
|
|
40
|
+
let trousers: string = TROUSERS[hash % TROUSERS.length] ?? TROUSERS[0];
|
|
41
|
+
for (let i = 1; i < TROUSERS.length && Math.abs(lightness(trousers) - shirt) < 0.12; i++) {
|
|
42
|
+
trousers = TROUSERS[(hash + i) % TROUSERS.length] ?? trousers;
|
|
43
|
+
}
|
|
44
|
+
const shoes: string = SHOES[(hash >>> 4) % SHOES.length] ?? SHOES[0];
|
|
45
|
+
return { trousers, trousersInk: outline(trousers), shoes, shoesInk: outline(shoes) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A polyline as a path `d`. */
|
|
49
|
+
function polyline(points: readonly Vec[]): string {
|
|
50
|
+
const line = new Path();
|
|
51
|
+
points.forEach(([px, py], i) => (i === 0 ? line.m(px, py) : line.l(px, py)));
|
|
52
|
+
return line.d();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A limb: a round stroke with a tonal outline under it. */
|
|
56
|
+
export function limb(points: readonly Vec[], width: number, fill: string, ink: string): SceneNode[] {
|
|
57
|
+
const d = polyline(points);
|
|
58
|
+
return [
|
|
59
|
+
path(d, { fill: 'none', stroke: ink, sw: width + LINE * 1.3, cap: 'round' }),
|
|
60
|
+
path(d, { fill: 'none', stroke: fill, sw: width, cap: 'round' }),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A shoe at a foot point, nudged along the facing. */
|
|
65
|
+
export function shoe(at: Vec, forward: Vec, o: Outfit): SceneNode {
|
|
66
|
+
return ellipse(at[0] + forward[0] * 2.4, at[1] + forward[1] * 1.6 - 1.5, 6.2 + Math.abs(forward[0]) * 1.4, 3.8, { fill: o.shoes, stroke: o.shoesInk, sw: LINE * 0.8 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A hand. */
|
|
70
|
+
export function hand(c: Colours, at: Vec): SceneNode {
|
|
71
|
+
return ellipse(at[0], at[1], 4.3, 4.3, { fill: c.skin, stroke: c.skinInk, sw: LINE * 0.8 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The torso's outline: shoulders at `top`, the hem at `hem`, `w` its width factor (narrower when turned). */
|
|
75
|
+
export function torsoPath(w: number, top: number, hem: number): string {
|
|
76
|
+
const shoulder = 17.5 * w;
|
|
77
|
+
const waist = 14.5 * w;
|
|
78
|
+
const hip = 15.5 * w;
|
|
79
|
+
const mid = (top + hem) / 2 + 4;
|
|
80
|
+
return new Path()
|
|
81
|
+
.m(-shoulder, top + 6)
|
|
82
|
+
.c(-shoulder, top - 1, -shoulder + 5, top - 3, -shoulder * 0.35, top - 3)
|
|
83
|
+
.l(shoulder * 0.35, top - 3)
|
|
84
|
+
.c(shoulder - 5, top - 3, shoulder, top - 1, shoulder, top + 6)
|
|
85
|
+
.l(waist, mid)
|
|
86
|
+
.l(hip, hem)
|
|
87
|
+
.q(0, hem + 3, -hip, hem)
|
|
88
|
+
.l(-waist, mid)
|
|
89
|
+
.z()
|
|
90
|
+
.d();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The shirt: a shade base, the lit front, the outline, and the neckline (front) or collar (back). */
|
|
94
|
+
export function shirt(c: Colours, w: number, top: number, hem: number, lean: number, isBack: boolean): SceneNode[] {
|
|
95
|
+
const d = torsoPath(w, top, hem);
|
|
96
|
+
const cy = (top + hem) / 2;
|
|
97
|
+
const nodes: SceneNode[] = [
|
|
98
|
+
path(d, { fill: c.bodyShade }),
|
|
99
|
+
g([path(d, { fill: c.body })], { tf: [tr(-1.5 - lean * 2, cy - 1.5), sc(0.9, 0.94), tr(0, -cy)] }),
|
|
100
|
+
path(`M${String(-15 * w)} ${String(hem - 2.5)}Q0 ${String(hem + 0.5)} ${String(15 * w)} ${String(hem - 2.5)}`, { fill: 'none', stroke: shade(c.bodyShade), sw: 2.2, opacity: 0.7 }),
|
|
101
|
+
path(d, { fill: 'none', stroke: c.bodyInk, sw: LINE }),
|
|
102
|
+
];
|
|
103
|
+
const neckX = lean * 7;
|
|
104
|
+
if (isBack) {
|
|
105
|
+
nodes.push(path(`M${String(neckX - 6)} ${String(top - 2)}Q${String(neckX)} ${String(top + 1.5)} ${String(neckX + 6)} ${String(top - 2)}`, { fill: 'none', stroke: c.bodyShade, sw: 3 }));
|
|
106
|
+
} else {
|
|
107
|
+
nodes.push(path(`M${String(neckX - 6 * w)} ${String(top - 2.6)}Q${String(neckX)} ${String(top + 6)} ${String(neckX + 6 * w)} ${String(top - 2.6)}Z`, { fill: c.skin, stroke: c.bodyInk, sw: LINE * 0.8 }));
|
|
108
|
+
}
|
|
109
|
+
return nodes;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The seat of the trousers joining the legs under the hem. */
|
|
113
|
+
export function hips(o: Outfit, w: number, hipY: number): SceneNode {
|
|
114
|
+
const half = 12 * w;
|
|
115
|
+
return path(new Path().m(-half, hipY - 4).l(half, hipY - 4).l(half, hipY + 4).q(0, hipY + 9, -half, hipY + 4).z().d(), { fill: o.trousers, stroke: o.trousersInk, sw: LINE * 0.8 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The neck under the head. */
|
|
119
|
+
export function neck(c: Colours, top: number, dx: number): SceneNode {
|
|
120
|
+
return path(new Path().m(dx - 4.5, top - 10).l(dx + 4.5, top - 10).l(dx + 4.5, top).l(dx - 4.5, top).z().d(), { fill: c.skinShade, stroke: c.skinInk, sw: LINE * 0.6 });
|
|
121
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The world walker (`figure: 'walker'`): a small full-body character for
|
|
3
|
+
* maps and crowds, with legs, arms, a shirt, trousers and shoes, and the
|
|
4
|
+
* head drawn smaller on top ({@link WALKER_HEAD_SCALE}). It takes the blob's
|
|
5
|
+
* pose names: `walk` and `run` swing the legs and arms along the facing,
|
|
6
|
+
* `sit` folds the legs over a seat, `idle` breathes, `wave` and `cheer` lift
|
|
7
|
+
* the arms.
|
|
8
|
+
*
|
|
9
|
+
* Standing, the origin is the feet. Sitting (`sit`, `play`), the origin is
|
|
10
|
+
* the seat: the hips rest on it and the feet hang {@link WALKER_SEAT_DROP}
|
|
11
|
+
* units below, on the floor in front of it.
|
|
12
|
+
*/
|
|
13
|
+
import { ellipse, type SceneNode } from '../scene';
|
|
14
|
+
import type { BodyLayout } from './body';
|
|
15
|
+
import type { Colours } from './colours';
|
|
16
|
+
import type { BodyPose } from './types';
|
|
17
|
+
import { hand, hips, limb, neck, outfitFor, shirt, shoe, type Outfit, type Vec } from './walker-parts';
|
|
18
|
+
|
|
19
|
+
/** The head's scale on a walker (the blob draws it at 1). */
|
|
20
|
+
export const WALKER_HEAD_SCALE = 0.58;
|
|
21
|
+
|
|
22
|
+
/** How far below the seat a sitter's feet reach (rig units). */
|
|
23
|
+
export const WALKER_SEAT_DROP = 26;
|
|
24
|
+
|
|
25
|
+
/** Heights of the hips, shoulders and head centre above the origin. */
|
|
26
|
+
interface Heights {
|
|
27
|
+
readonly hip: number;
|
|
28
|
+
readonly shoulder: number;
|
|
29
|
+
readonly head: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STANDING: Heights = { hip: -35, shoulder: -72, head: -103 };
|
|
33
|
+
const SEATED: Heights = { hip: -3, shoulder: -40, head: -71 };
|
|
34
|
+
const LEG = 32;
|
|
35
|
+
const UPPER_ARM = 13;
|
|
36
|
+
const FOREARM = 12;
|
|
37
|
+
const LEG_W = 10.5;
|
|
38
|
+
const SLEEVE_W = 10;
|
|
39
|
+
const ARM_W = 7.5;
|
|
40
|
+
const DEG = Math.PI / 180;
|
|
41
|
+
|
|
42
|
+
/** A walker's layout: the blob's fields plus where the head goes and its scale. */
|
|
43
|
+
export interface WalkerLayout extends BodyLayout {
|
|
44
|
+
readonly headY: number;
|
|
45
|
+
readonly headScale: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One side of the body: −1 is the character's right, +1 its left. */
|
|
49
|
+
type Side = -1 | 1;
|
|
50
|
+
|
|
51
|
+
/** The view of a facing: how far it is turned, which way is forward on screen. */
|
|
52
|
+
interface View {
|
|
53
|
+
/** sin of the turn: 0 front or back, ~1 side on. */
|
|
54
|
+
readonly side: number;
|
|
55
|
+
/** cos of the turn: 1 front, −1 back. */
|
|
56
|
+
readonly front: number;
|
|
57
|
+
/** Forward on the floor, in screen units per unit (y is foreshortened). */
|
|
58
|
+
readonly forward: Vec;
|
|
59
|
+
readonly isBack: boolean;
|
|
60
|
+
/** The torso's width factor. */
|
|
61
|
+
readonly width: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function viewOf(angleDeg: number): View {
|
|
65
|
+
const side = Math.sin(angleDeg * DEG);
|
|
66
|
+
const front = Math.cos(angleDeg * DEG);
|
|
67
|
+
return { side, front, forward: [side, front * 0.45], isBack: angleDeg > 90, width: 1 - 0.36 * Math.abs(side) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A body side's screen x for a lateral distance (sides overlap when turned). */
|
|
71
|
+
const lateralX = (v: View, sideOf: Side, distance: number): number => sideOf * distance * v.front;
|
|
72
|
+
|
|
73
|
+
/** How near the viewer a side is (> 0: drawn in front when turned). */
|
|
74
|
+
const nearness = (v: View, sideOf: Side): number => -sideOf * v.side;
|
|
75
|
+
|
|
76
|
+
const add = (a: Vec, b: Vec, k = 1): Vec => [a[0] + b[0] * k, a[1] + b[1] * k];
|
|
77
|
+
|
|
78
|
+
/** What each limb does in a pose. */
|
|
79
|
+
interface Limbs {
|
|
80
|
+
/** Foot (standing) or knee-then-foot (sitting) per side. */
|
|
81
|
+
readonly leg: (sideOf: Side) => readonly [Vec, Vec, Vec];
|
|
82
|
+
readonly arm: (sideOf: Side) => readonly [Vec, Vec, Vec];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function standingLimbs(v: View, h: Heights, pose: BodyPose, phase: number, bob: number): Limbs {
|
|
86
|
+
const isRun = pose === 'run';
|
|
87
|
+
const isWalking = pose === 'walk' || isRun || pose === 'dance';
|
|
88
|
+
const stride = isRun ? 38 : 27;
|
|
89
|
+
const swingOf = (sideOf: Side): number => (isWalking ? Math.sin((phase + (sideOf === 1 ? 0 : 0.5)) * Math.PI * 2) : 0);
|
|
90
|
+
return {
|
|
91
|
+
leg: (sideOf) => {
|
|
92
|
+
const hip: Vec = [lateralX(v, sideOf, 6), h.hip + bob];
|
|
93
|
+
const swing = swingOf(sideOf);
|
|
94
|
+
const lift = isWalking ? Math.max(0, Math.cos((phase + (sideOf === 1 ? 0 : 0.5)) * Math.PI * 2)) * (isRun ? 7 : 4.5) : 0;
|
|
95
|
+
const reach = Math.sin(stride * DEG) * LEG * swing;
|
|
96
|
+
const foot: Vec = [hip[0] + v.forward[0] * reach, -bob * 0.2 + v.forward[1] * reach - lift];
|
|
97
|
+
const knee: Vec = [(hip[0] + foot[0]) / 2 + v.forward[0] * lift * 0.9, (hip[1] + foot[1]) / 2 + v.forward[1] * lift * 0.9];
|
|
98
|
+
return [hip, knee, foot];
|
|
99
|
+
},
|
|
100
|
+
arm: (sideOf) => {
|
|
101
|
+
const shoulder: Vec = [lateralX(v, sideOf, 15.5 * v.width) + v.forward[0] * 1.5, h.shoulder + 4 + bob];
|
|
102
|
+
const out = sideOf * v.front * 2.2;
|
|
103
|
+
if (pose === 'cheer' || (pose === 'wave' && sideOf === -1)) {
|
|
104
|
+
const wave = pose === 'wave' ? Math.sin(phase * Math.PI * 2) * 4 : 0;
|
|
105
|
+
const elbow: Vec = [shoulder[0] + out * 3, shoulder[1] - 10];
|
|
106
|
+
return [shoulder, elbow, [elbow[0] + out * 1.5 + wave, elbow[1] - 12]];
|
|
107
|
+
}
|
|
108
|
+
const swing = isWalking ? -swingOf(sideOf) * (isRun ? 1.3 : 1) : Math.sin(phase * Math.PI * 2) * 0.06;
|
|
109
|
+
const reach = Math.sin(24 * DEG) * swing;
|
|
110
|
+
const elbow: Vec = [shoulder[0] + out + v.forward[0] * UPPER_ARM * reach * 0.6, shoulder[1] + UPPER_ARM + v.forward[1] * UPPER_ARM * reach];
|
|
111
|
+
const wrist: Vec = [elbow[0] + out * 0.4 + v.forward[0] * FOREARM * reach * 1.6, elbow[1] + FOREARM - Math.abs(reach) * 3 + v.forward[1] * FOREARM * reach];
|
|
112
|
+
return [shoulder, elbow, wrist];
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function seatedLimbs(v: View, h: Heights, pose: BodyPose): Limbs {
|
|
118
|
+
const thigh: Vec = [v.forward[0] * 15, v.forward[1] * 16];
|
|
119
|
+
return {
|
|
120
|
+
leg: (sideOf) => {
|
|
121
|
+
const hip: Vec = [lateralX(v, sideOf, 6), h.hip];
|
|
122
|
+
const knee = add(hip, thigh);
|
|
123
|
+
return [hip, knee, [knee[0] + v.forward[0] * 3, WALKER_SEAT_DROP - 2]];
|
|
124
|
+
},
|
|
125
|
+
arm: (sideOf) => {
|
|
126
|
+
const shoulder: Vec = [lateralX(v, sideOf, 15.5 * v.width) + v.forward[0] * 1.5, h.shoulder + 4];
|
|
127
|
+
const elbow: Vec = [shoulder[0] + sideOf * v.front * 2 + v.forward[0] * 3, shoulder[1] + UPPER_ARM];
|
|
128
|
+
const lap = add([lateralX(v, sideOf, pose === 'play' ? 4 : 7), h.hip - 4], thigh, pose === 'play' ? 0.9 : 0.6);
|
|
129
|
+
return [shoulder, elbow, lap];
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The walker's drawing for a pose, at a facing angle (0 = S, 180 = N; west facings are mirrored by the caller). */
|
|
135
|
+
export function walkerLayout(c: Colours, pose: BodyPose, angleDeg: number, phase: number, outfit: Outfit = outfitFor(c)): WalkerLayout {
|
|
136
|
+
const v = viewOf(angleDeg);
|
|
137
|
+
const isSitting = pose === 'sit' || pose === 'play';
|
|
138
|
+
const h = isSitting ? SEATED : STANDING;
|
|
139
|
+
const cycle = Math.sin(phase * Math.PI * 2);
|
|
140
|
+
const isWalking = pose === 'walk' || pose === 'run' || pose === 'dance';
|
|
141
|
+
const hop = pose === 'jump' || pose === 'cheer' ? Math.abs(Math.sin(phase * Math.PI)) * (pose === 'jump' ? 16 : 8) : 0;
|
|
142
|
+
const bob = isWalking ? -2.2 * (1 - Math.abs(cycle)) : pose === 'idle' ? cycle * 0.6 : 0;
|
|
143
|
+
const limbs = isSitting ? seatedLimbs(v, h, pose) : standingLimbs(v, h, pose, phase, bob);
|
|
144
|
+
const o = outfit;
|
|
145
|
+
const [farSide, nearSide]: readonly [Side, Side] = nearness(v, 1) > nearness(v, -1) ? [-1, 1] : [1, -1];
|
|
146
|
+
const isTurned = Math.abs(v.side) > 0.3;
|
|
147
|
+
const lean = v.side * (isSitting ? 0.6 : 1);
|
|
148
|
+
const armNodes = (sideOf: Side): SceneNode[] => {
|
|
149
|
+
const [shoulder, elbow, wrist] = limbs.arm(sideOf);
|
|
150
|
+
return [...limb([shoulder, elbow, wrist], ARM_W, c.skin, c.skinInk), ...limb([shoulder, add(shoulder, [elbow[0] - shoulder[0], elbow[1] - shoulder[1]], 0.62)], SLEEVE_W, c.body, c.bodyInk), hand(c, wrist)];
|
|
151
|
+
};
|
|
152
|
+
const legNodes = (sideOf: Side): SceneNode[] => {
|
|
153
|
+
const points = limbs.leg(sideOf);
|
|
154
|
+
const foot = points[2];
|
|
155
|
+
return [...limb(points, LEG_W, o.trousers, o.trousersInk), shoe(foot, v.forward, o)];
|
|
156
|
+
};
|
|
157
|
+
const torso = shirt(c, v.width, h.shoulder + (isWalking ? bob : 0), h.hip + 3 + (isWalking ? bob : 0), lean, v.isBack);
|
|
158
|
+
// Seated with our back to us, the arms reach forward and the legs are under the seat: both hide behind the back.
|
|
159
|
+
const isTucked = isSitting && v.isBack;
|
|
160
|
+
const behind: SceneNode[] = isTucked ? [...armNodes(farSide), ...armNodes(nearSide)] : isTurned ? armNodes(farSide) : [];
|
|
161
|
+
// Sitting facing us, the thighs come toward the viewer over the shirt's hem.
|
|
162
|
+
const isLegsFirst = v.isBack || !isSitting;
|
|
163
|
+
const legs = isTucked ? [] : [...legNodes(farSide), ...legNodes(nearSide)];
|
|
164
|
+
const body: SceneNode[] = [
|
|
165
|
+
...(isLegsFirst ? legs : []),
|
|
166
|
+
hips(o, v.width, h.hip + (isWalking ? bob : 0)),
|
|
167
|
+
neck(c, h.shoulder + bob, v.forward[0] * 2),
|
|
168
|
+
...torso,
|
|
169
|
+
...(isLegsFirst ? [] : legs),
|
|
170
|
+
...(isTucked ? [] : isTurned ? armNodes(nearSide) : [...armNodes(-1), ...armNodes(1)]),
|
|
171
|
+
];
|
|
172
|
+
const prop = add([0, h.hip - 12], v.forward, 8);
|
|
173
|
+
return {
|
|
174
|
+
behind, body, front: [],
|
|
175
|
+
headDx: v.forward[0] * 1.5, headDy: 0, headY: h.head + bob * 1.1, headScale: WALKER_HEAD_SCALE,
|
|
176
|
+
tilt: isWalking ? v.side * 2 : 0, lift: hop, prop: { x: prop[0], y: prop[1] },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const GREY = '#6f7787';
|
|
181
|
+
const GREY_INK = '#5c6474';
|
|
182
|
+
|
|
183
|
+
/** Every colour slot in the silhouette grey. */
|
|
184
|
+
const GREY_COLOURS: Colours = {
|
|
185
|
+
skin: GREY, skinShade: GREY, skinLight: GREY, skinInk: GREY_INK, eyeRim: GREY, hair: GREY, hairShade: GREY, hairLight: GREY,
|
|
186
|
+
hairInk: GREY_INK, hairBuzz: GREY, brow: GREY, beard: GREY, beardShade: GREY, beardInk: GREY_INK, stubble: GREY, eye: GREY,
|
|
187
|
+
eyeDark: GREY, body: GREY, bodyShade: GREY, bodyLight: GREY, bodyInk: GREY_INK,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** The grey walker shown before the art loads (or for a hidden player): its nodes, head centre and top. */
|
|
191
|
+
export function walkerSilhouette(): { readonly nodes: readonly SceneNode[]; readonly headY: number; readonly top: number } {
|
|
192
|
+
const b = walkerLayout(GREY_COLOURS, 'idle', 0, 0, { trousers: GREY, trousersInk: GREY_INK, shoes: GREY, shoesInk: GREY_INK });
|
|
193
|
+
const r = 46 * WALKER_HEAD_SCALE;
|
|
194
|
+
return {
|
|
195
|
+
nodes: [...b.behind, ...b.body, ellipse(0, b.headY, r, r * 1.05, { fill: GREY })],
|
|
196
|
+
headY: b.headY,
|
|
197
|
+
top: b.headY - r - 4,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The display list every renderer draws: a small tree of groups and shapes
|
|
3
|
+
* in rig units. The composer builds it once per pose; `toSvg` turns it into
|
|
4
|
+
* markup, `paintCanvas` draws it on a 2D canvas, and the Pixi and three.js
|
|
5
|
+
* adapters draw it through a canvas. Colours in a scene are always derived
|
|
6
|
+
* `#rrggbb` values or fixed constants, never caller text.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** One transform step, applied in order (SVG semantics). */
|
|
10
|
+
export type TfOp =
|
|
11
|
+
| { readonly k: 't'; readonly x: number; readonly y: number }
|
|
12
|
+
| { readonly k: 's'; readonly x: number; readonly y: number }
|
|
13
|
+
| { readonly k: 'r'; readonly a: number }
|
|
14
|
+
| { readonly k: 'skx'; readonly a: number };
|
|
15
|
+
|
|
16
|
+
/** How a shape is painted. */
|
|
17
|
+
export interface Paint {
|
|
18
|
+
readonly fill?: string;
|
|
19
|
+
readonly fillOpacity?: number;
|
|
20
|
+
readonly stroke?: string;
|
|
21
|
+
readonly sw?: number;
|
|
22
|
+
readonly cap?: 'round' | 'butt';
|
|
23
|
+
readonly opacity?: number;
|
|
24
|
+
readonly evenOdd?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A path shape. */
|
|
28
|
+
export interface PathNode extends Paint {
|
|
29
|
+
readonly k: 'path';
|
|
30
|
+
readonly d: string;
|
|
31
|
+
readonly tf?: readonly TfOp[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** An ellipse (a circle when rx = ry). */
|
|
35
|
+
export interface EllipseNode extends Paint {
|
|
36
|
+
readonly k: 'ellipse';
|
|
37
|
+
readonly cx: number;
|
|
38
|
+
readonly cy: number;
|
|
39
|
+
readonly rx: number;
|
|
40
|
+
readonly ry: number;
|
|
41
|
+
readonly tf?: readonly TfOp[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A rectangle with optional rounded corners. */
|
|
45
|
+
export interface RectNode extends Paint {
|
|
46
|
+
readonly k: 'rect';
|
|
47
|
+
readonly x: number;
|
|
48
|
+
readonly y: number;
|
|
49
|
+
readonly w: number;
|
|
50
|
+
readonly h: number;
|
|
51
|
+
readonly r?: number;
|
|
52
|
+
readonly tf?: readonly TfOp[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A line of text (overlay labels and FX glyphs). */
|
|
56
|
+
export interface TextNode extends Paint {
|
|
57
|
+
readonly k: 'text';
|
|
58
|
+
readonly x: number;
|
|
59
|
+
readonly y: number;
|
|
60
|
+
readonly text: string;
|
|
61
|
+
readonly size: number;
|
|
62
|
+
readonly font: 'mono' | 'pixel';
|
|
63
|
+
readonly anchor?: 'start' | 'middle';
|
|
64
|
+
readonly bold?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A clip region: the union of its shapes (even-odd when asked). */
|
|
68
|
+
export interface Clip {
|
|
69
|
+
readonly shapes: readonly (PathNode | EllipseNode | RectNode)[];
|
|
70
|
+
readonly evenOdd?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A group: a transform, an optional clip and opacity, and children. */
|
|
74
|
+
export interface GroupNode {
|
|
75
|
+
readonly k: 'g';
|
|
76
|
+
readonly tf?: readonly TfOp[];
|
|
77
|
+
readonly clip?: Clip;
|
|
78
|
+
readonly opacity?: number;
|
|
79
|
+
/** A soft light rim around the silhouette (the world, §14.2). */
|
|
80
|
+
readonly rim?: boolean;
|
|
81
|
+
readonly children: readonly SceneNode[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Anything in a scene. */
|
|
85
|
+
export type SceneNode = GroupNode | PathNode | EllipseNode | RectNode | TextNode;
|
|
86
|
+
|
|
87
|
+
/** A group helper that drops empty groups' wrapping. */
|
|
88
|
+
export function g(children: readonly SceneNode[], opts: Omit<GroupNode, 'k' | 'children'> = {}): GroupNode {
|
|
89
|
+
return { k: 'g', ...opts, children };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A path helper. */
|
|
93
|
+
export function path(d: string, paint: Paint, tf?: readonly TfOp[]): PathNode {
|
|
94
|
+
return { k: 'path', d, ...paint, ...(tf ? { tf } : {}) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** An ellipse helper. */
|
|
98
|
+
export function ellipse(cx: number, cy: number, rx: number, ry: number, paint: Paint, tf?: readonly TfOp[]): EllipseNode {
|
|
99
|
+
return { k: 'ellipse', cx, cy, rx, ry, ...paint, ...(tf ? { tf } : {}) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A rect helper. */
|
|
103
|
+
export function rect(left: number, top: number, w: number, h: number, paint: Paint, r?: number): RectNode {
|
|
104
|
+
return { k: 'rect', x: left, y: top, w, h, ...paint, ...(r === undefined ? {} : { r }) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A translate step. */
|
|
108
|
+
export const tr = (dx: number, dy: number): TfOp => ({ k: 't', x: dx, y: dy });
|
|
109
|
+
/** A scale step (uniform when `sy` is omitted). */
|
|
110
|
+
export const sc = (sx: number, sy = sx): TfOp => ({ k: 's', x: sx, y: sy });
|
|
111
|
+
/** A rotate step, degrees. */
|
|
112
|
+
export const rot = (a: number): TfOp => ({ k: 'r', a });
|
|
113
|
+
/** A horizontal skew step, degrees. */
|
|
114
|
+
export const skewX = (a: number): TfOp => ({ k: 'skx', a });
|
|
115
|
+
|
|
116
|
+
/** A round-joined stroke paint. */
|
|
117
|
+
export function line(colour: string, width: number, cap: 'round' | 'butt' = 'round', opacity?: number): Paint {
|
|
118
|
+
return { fill: 'none', stroke: colour, sw: width, cap, ...(opacity === undefined ? {} : { opacity }) };
|
|
119
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@diffenderfer-games/hub/avatar/pixi`: avatars in PixiJS. Pass your PIXI
|
|
3
|
+
* namespace (the hub has no pixi dependency):
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import * as PIXI from 'pixi.js';
|
|
7
|
+
* import { ready, animator } from '@diffenderfer-games/hub/avatar';
|
|
8
|
+
* import { pixiAvatar } from '@diffenderfer-games/hub/avatar/pixi';
|
|
9
|
+
*
|
|
10
|
+
* await ready();
|
|
11
|
+
* const face = pixiAvatar(PIXI, seat.look, { size: 64, animator: animator(seat.look, { seed: seat.userId }) });
|
|
12
|
+
* stage.addChild(face.view);
|
|
13
|
+
* app.ticker.add(() => face.update(performance.now()));
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
export { pixiAvatar, avatarTexture, type PixiAvatar, type PixiAvatarOptions } from './avatar/adapters/pixi-avatar';
|
|
19
|
+
export { createCrowd, type AvatarCrowd, type CrowdMember, type CrowdOptions } from './avatar/adapters/pixi-crowd';
|
|
20
|
+
export type { PixiContainer, PixiNs, PixiSprite, PixiTexture } from './avatar/adapters/pixi-types';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@diffenderfer-games/hub/avatar/svg`: avatars as SVG markup with no DOM,
|
|
3
|
+
* for servers, workers and server-side rendering. The same renderer as
|
|
4
|
+
* `@diffenderfer-games/hub/avatar` (`render.svg`), without the DOM, canvas
|
|
5
|
+
* and animation outputs.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { ready, renderSvg } from '@diffenderfer-games/hub/avatar/svg';
|
|
9
|
+
*
|
|
10
|
+
* await ready();
|
|
11
|
+
* const markup = renderSvg(player.look, { size: 64, pose: { expression: 'happy' } });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
export { decodeLook, defaultLook, encodeLook, type AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
|
|
17
|
+
export { ready } from './avatar/art-store';
|
|
18
|
+
export { EXPRESSION_NAMES, isExpression, type Expression } from './avatar/face';
|
|
19
|
+
export { renderSvg } from './avatar/outputs/svg';
|
|
20
|
+
export { describe } from './avatar/describe';
|
|
21
|
+
export type { Facing, Frame, LookLike, Pose, RenderOptions } from './avatar/render/types';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@diffenderfer-games/hub/avatar/three`: avatars in three.js, as a
|
|
3
|
+
* `CanvasTexture` or a billboard `Sprite`. Pass your THREE namespace (the hub
|
|
4
|
+
* has no three dependency):
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import * as THREE from 'three';
|
|
8
|
+
* import { ready } from '@diffenderfer-games/hub/avatar';
|
|
9
|
+
* import { avatarSprite } from '@diffenderfer-games/hub/avatar/three';
|
|
10
|
+
*
|
|
11
|
+
* await ready();
|
|
12
|
+
* const { sprite } = avatarSprite(THREE, me.look, { frame: 'head', size: 128, height: 0.4 });
|
|
13
|
+
* helmet.add(sprite);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
export { avatarCanvasTexture, avatarSprite, type ThreeAvatarOptions, type ThreeAvatarTexture, type ThreeNs, type ThreeTextureNs } from './avatar/adapters/three';
|
package/src/avatar.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@diffenderfer-games/hub/avatar`: draw players' avatars in any game.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { ready, render, animator } from '@diffenderfer-games/hub/avatar';
|
|
6
|
+
*
|
|
7
|
+
* await ready(); // loads the art once
|
|
8
|
+
* el.innerHTML = render.svg(seat.look, { size: 48 });
|
|
9
|
+
* const anim = animator(seat.look, { seed: seat.userId });
|
|
10
|
+
* anim.express('happy');
|
|
11
|
+
* render.canvas(ctx, seat.look, { x, y, size: 64, pose: anim.pose(now) });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The art loads lazily (`ready()`); before it does, renders draw the
|
|
15
|
+
* silhouette. Zero dependencies; the Pixi and three.js adapters take the
|
|
16
|
+
* library as an argument (`/avatar/pixi`, `/avatar/three`). Docs: "Avatars"
|
|
17
|
+
* in docs/hub.md.
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
import { renderBitmap, renderDataUrl } from './avatar/outputs/bitmap';
|
|
22
|
+
import { renderCanvas, renderCanvasAt } from './avatar/outputs/canvas';
|
|
23
|
+
import { renderElement } from './avatar/outputs/element';
|
|
24
|
+
import { renderSvg } from './avatar/outputs/svg';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
ACCESSORY_SLOTS, CATALOG, CATALOG_REVISION, DEFAULT_GEN, FRAME_PRESETS, LOOK_CODE_PREFIX, PALETTES, SECOND_COLOUR_CODE,
|
|
28
|
+
canonicalLook, decodeLook, defaultLook, distinctLooks, encodeLook, isLookCode, isTinted, isValidLook, lookProblem,
|
|
29
|
+
normaliseHex, partById, partName, partsOf, randomLook, resolveColour, sameLook, seededRng,
|
|
30
|
+
type AccessorySlot, type AvatarLook, type CatalogEntry, type DecodedLook, type LookAccessory, type LookColour,
|
|
31
|
+
type LookPart, type LookProblem, type PaletteName, type PartCategory, type Preset, type RandomLookOptions, type Rng,
|
|
32
|
+
} from '@diffenderfer-games/hub-contract/avatar';
|
|
33
|
+
export { ready, onArtReady } from './avatar/art-store';
|
|
34
|
+
export { animator, Animator, type AnimatorOptions, type At, type Feeling } from './avatar/animator/animator';
|
|
35
|
+
export { REACTIONS, type Reaction } from './avatar/animator/reactions';
|
|
36
|
+
export { EXPRESSIONS, EXPRESSION_NAMES, expressionFromEmote, isExpression, mix, type BrowParams, type Expression, type FaceExtra, type FaceParams, type MouthParams } from './avatar/face';
|
|
37
|
+
export { facingFrom, layout, lookToward } from './avatar/layout';
|
|
38
|
+
export { avatarEmoji, describe } from './avatar/describe';
|
|
39
|
+
export { defineAvatarElement } from './avatar/outputs/custom-element';
|
|
40
|
+
export type { AvatarElement } from './avatar/outputs/element';
|
|
41
|
+
export type { CanvasPlacement } from './avatar/outputs/canvas';
|
|
42
|
+
export { overlays, type ActivityIcon, type Overlay, type ReactionGlyph } from './avatar/overlays';
|
|
43
|
+
export { overlaySvg, paintOverlay } from './avatar/outputs/overlay-svg';
|
|
44
|
+
export { catalog } from './avatar/catalog-view';
|
|
45
|
+
export { composeAvatar, HEAD_Y, type Composed } from './avatar/render/compose';
|
|
46
|
+
export { WALKER_HEAD_SCALE, WALKER_SEAT_DROP } from './avatar/render/walker';
|
|
47
|
+
export { paintNodes, type Ctx2D } from './avatar/outputs/canvas-paint';
|
|
48
|
+
export { sceneToSvg } from './avatar/outputs/svg-markup';
|
|
49
|
+
export { toCode, toLook } from './avatar/look-like';
|
|
50
|
+
export { contrast, deltaE, lightness } from './avatar/colour';
|
|
51
|
+
export type { SceneNode } from './avatar/scene';
|
|
52
|
+
export type { Anchor, BodyPose, Facing, Figure, Frame, LookDir, LookLike, Point, Pose, RenderOptions, StatusProp, View } from './avatar/render/types';
|
|
53
|
+
export { AvatarAtlas, createAtlas, type AtlasFrame, type AtlasOptions } from './avatar/atlas';
|
|
54
|
+
|
|
55
|
+
/** Every output: SVG markup, a live element, Canvas 2D, a bitmap or a data URL. */
|
|
56
|
+
export const render = {
|
|
57
|
+
svg: renderSvg,
|
|
58
|
+
element: renderElement,
|
|
59
|
+
canvas: renderCanvas,
|
|
60
|
+
canvasAt: renderCanvasAt,
|
|
61
|
+
bitmap: renderBitmap,
|
|
62
|
+
dataUrl: renderDataUrl,
|
|
63
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The device's avatar cache (avatars.md §5.6), so faces render offline:
|
|
3
|
+
* your own look (`hub:avatar:me`, read synchronously at startup) and an LRU
|
|
4
|
+
* of other players' codes (`hub:avatar:cache`, 200 entries).
|
|
5
|
+
*/
|
|
6
|
+
import { isLookCode } from '@diffenderfer-games/hub-contract/avatar';
|
|
7
|
+
import { isRecord, optionalNumber, optionalString, parseJsonText } from '../storage/json-guards';
|
|
8
|
+
import type { KeyValueStore } from '../storage/key-value';
|
|
9
|
+
|
|
10
|
+
/** Your own look as cached. */
|
|
11
|
+
export interface CachedMine {
|
|
12
|
+
readonly userId: number;
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly isDefault: boolean;
|
|
15
|
+
readonly updatedAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MINE_KEY = 'hub:avatar:me';
|
|
19
|
+
const OTHERS_KEY = 'hub:avatar:cache';
|
|
20
|
+
const OTHERS_MAX = 200;
|
|
21
|
+
|
|
22
|
+
/** The device cache over a key-value store. */
|
|
23
|
+
export class AvatarCache {
|
|
24
|
+
private readonly store: KeyValueStore;
|
|
25
|
+
private others: Map<number, string> | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(store: KeyValueStore) {
|
|
28
|
+
this.store = store;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Your cached look, or `null` (never cached, or another player's on this device). */
|
|
32
|
+
readMine(userId: number | null): CachedMine | null {
|
|
33
|
+
const parsed = parseJsonText(this.store.read(MINE_KEY) ?? '');
|
|
34
|
+
if (!isRecord(parsed)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const id = optionalNumber(parsed.userId);
|
|
38
|
+
const code = optionalString(parsed.code);
|
|
39
|
+
if (id === undefined || code === undefined || !isLookCode(code) || (userId !== null && id !== userId)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return { userId: id, code, isDefault: parsed.isDefault === true, updatedAt: optionalNumber(parsed.updatedAt) ?? 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Remember your look. */
|
|
46
|
+
writeMine(mine: CachedMine): void {
|
|
47
|
+
this.store.write(MINE_KEY, JSON.stringify(mine));
|
|
48
|
+
this.remember(mine.userId, mine.code);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A player's cached code, or `undefined`. */
|
|
52
|
+
codeOf(userId: number): string | undefined {
|
|
53
|
+
return this.loadOthers().get(userId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Remember a player's code (most recent last; the oldest drop past 200). */
|
|
57
|
+
remember(userId: number, code: string): void {
|
|
58
|
+
if (code === '') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const others = this.loadOthers();
|
|
62
|
+
if (others.get(userId) === code) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
others.delete(userId);
|
|
66
|
+
others.set(userId, code);
|
|
67
|
+
while (others.size > OTHERS_MAX) {
|
|
68
|
+
const oldest = others.keys().next().value;
|
|
69
|
+
if (oldest === undefined) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
others.delete(oldest);
|
|
73
|
+
}
|
|
74
|
+
this.store.write(OTHERS_KEY, JSON.stringify([...others]));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private loadOthers(): Map<number, string> {
|
|
78
|
+
if (this.others !== null) {
|
|
79
|
+
return this.others;
|
|
80
|
+
}
|
|
81
|
+
const others = new Map<number, string>();
|
|
82
|
+
const parsed = parseJsonText(this.store.read(OTHERS_KEY) ?? '[]');
|
|
83
|
+
if (Array.isArray(parsed)) {
|
|
84
|
+
for (const entry of parsed) {
|
|
85
|
+
const id = Array.isArray(entry) ? optionalNumber(entry[0]) : undefined;
|
|
86
|
+
const code = Array.isArray(entry) ? optionalString(entry[1]) : undefined;
|
|
87
|
+
if (id !== undefined && code !== undefined && isLookCode(code)) {
|
|
88
|
+
others.set(id, code);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.others = others;
|
|
93
|
+
return others;
|
|
94
|
+
}
|
|
95
|
+
}
|