@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,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny path toolkit. Every shape is authored as points in head-local units
|
|
3
|
+
* (1 unit = 1/100 of the head height), so a whole part can be warped point by
|
|
4
|
+
* point (the 2.5D head turn) before it becomes an SVG `d` string.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** A point; the optional third flag marks a sharp corner in {@link blob}. */
|
|
8
|
+
export type Pt = readonly [number, number] | readonly [number, number, 'c'];
|
|
9
|
+
|
|
10
|
+
/** A point warp (the head turn). */
|
|
11
|
+
export type Warp = (px: number, py: number) => readonly [number, number];
|
|
12
|
+
|
|
13
|
+
interface Cmd {
|
|
14
|
+
readonly c: 'M' | 'L' | 'C' | 'Q' | 'Z';
|
|
15
|
+
readonly p: readonly number[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Rounds to one decimal and drops a trailing `.0` and negative zero. */
|
|
19
|
+
export function n(v: number): string {
|
|
20
|
+
const r = Math.round(v * 10) / 10;
|
|
21
|
+
return Object.is(r, -0) ? '0' : String(r);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A path of absolute M/L/C/Q/Z commands that can be warped as a whole. */
|
|
25
|
+
export class Path {
|
|
26
|
+
private readonly cmds: Cmd[] = [];
|
|
27
|
+
|
|
28
|
+
m(px: number, py: number): this {
|
|
29
|
+
this.cmds.push({ c: 'M', p: [px, py] });
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
l(px: number, py: number): this {
|
|
34
|
+
this.cmds.push({ c: 'L', p: [px, py] });
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
c(x1: number, y1: number, x2: number, y2: number, px: number, py: number): this {
|
|
39
|
+
this.cmds.push({ c: 'C', p: [x1, y1, x2, y2, px, py] });
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
q(x1: number, y1: number, px: number, py: number): this {
|
|
44
|
+
this.cmds.push({ c: 'Q', p: [x1, y1, px, py] });
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
z(): this {
|
|
49
|
+
this.cmds.push({ c: 'Z', p: [] });
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Appends every command of another path. */
|
|
54
|
+
add(other: Path): this {
|
|
55
|
+
this.cmds.push(...other.cmds);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The lowest y of any point (control points included, so it never under-reports). */
|
|
60
|
+
bottom(): number {
|
|
61
|
+
return this.cmds.reduce((low, { p }) => p.reduce((lo, v, i) => (i % 2 === 1 ? Math.max(lo, v) : lo), low), -Infinity);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The SVG `d` string, optionally warping every point. */
|
|
65
|
+
d(warp?: Warp): string {
|
|
66
|
+
return this.cmds.map(({ c, p }) => {
|
|
67
|
+
if (c === 'Z') {
|
|
68
|
+
return 'Z';
|
|
69
|
+
}
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
for (let i = 0; i < p.length; i += 2) {
|
|
72
|
+
const px = p[i] ?? 0;
|
|
73
|
+
const py = p[i + 1] ?? 0;
|
|
74
|
+
const [wx, wy] = warp ? warp(px, py) : [px, py];
|
|
75
|
+
out.push(`${n(wx)} ${n(wy)}`);
|
|
76
|
+
}
|
|
77
|
+
return c + out.join(' ');
|
|
78
|
+
}).join('');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A point by index, or the origin. */
|
|
83
|
+
const at = (pts: readonly Pt[], i: number): Pt => pts[i] ?? [0, 0];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A smooth Catmull-Rom curve through the points (cubic segments). Points
|
|
87
|
+
* flagged `'c'` become sharp corners.
|
|
88
|
+
*/
|
|
89
|
+
export function blob(pts: readonly Pt[], isClosed = true, tension = 1, into = new Path()): Path {
|
|
90
|
+
const count = pts.length;
|
|
91
|
+
if (count === 0) {
|
|
92
|
+
return into;
|
|
93
|
+
}
|
|
94
|
+
const wrap = (i: number): Pt => (isClosed ? at(pts, (i + count) % count) : at(pts, Math.max(0, Math.min(count - 1, i))));
|
|
95
|
+
const tangent = (i: number): readonly [number, number] => {
|
|
96
|
+
const p = wrap(i);
|
|
97
|
+
if (p[2] === 'c' || (!isClosed && (i === 0 || i === count - 1))) {
|
|
98
|
+
return [0, 0];
|
|
99
|
+
}
|
|
100
|
+
const a = wrap(i - 1);
|
|
101
|
+
const b = wrap(i + 1);
|
|
102
|
+
return [((b[0] - a[0]) * tension) / 6, ((b[1] - a[1]) * tension) / 6];
|
|
103
|
+
};
|
|
104
|
+
const first = at(pts, 0);
|
|
105
|
+
into.m(first[0], first[1]);
|
|
106
|
+
const segs = isClosed ? count : count - 1;
|
|
107
|
+
for (let i = 0; i < segs; i += 1) {
|
|
108
|
+
const p1 = wrap(i);
|
|
109
|
+
const p2 = wrap(i + 1);
|
|
110
|
+
const t1 = tangent(i);
|
|
111
|
+
const t2 = tangent(i + 1);
|
|
112
|
+
into.c(p1[0] + t1[0], p1[1] + t1[1], p2[0] - t2[0], p2[1] - t2[1], p2[0], p2[1]);
|
|
113
|
+
}
|
|
114
|
+
if (isClosed) {
|
|
115
|
+
into.z();
|
|
116
|
+
}
|
|
117
|
+
return into;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Points along an ellipse arc; angles in degrees, 0 = right, 90 = down. */
|
|
121
|
+
export function arc(cx: number, cy: number, rx: number, ry: number, a0: number, a1: number, steps: number): Pt[] {
|
|
122
|
+
const out: Pt[] = [];
|
|
123
|
+
for (let i = 0; i <= steps; i += 1) {
|
|
124
|
+
const a = ((a0 + ((a1 - a0) * i) / steps) * Math.PI) / 180;
|
|
125
|
+
out.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A scalloped edge: one outward bump between each pair of points (quadratic
|
|
132
|
+
* curves), for curls and clouds. `side` 1 bumps to the left of travel.
|
|
133
|
+
*/
|
|
134
|
+
export function scallop(pts: readonly Pt[], bump: number, isClosed: boolean, side = 1, into = new Path()): Path {
|
|
135
|
+
const count = pts.length;
|
|
136
|
+
const first = at(pts, 0);
|
|
137
|
+
into.m(first[0], first[1]);
|
|
138
|
+
const segs = isClosed ? count : count - 1;
|
|
139
|
+
for (let i = 0; i < segs; i += 1) {
|
|
140
|
+
const a = at(pts, i);
|
|
141
|
+
const b = at(pts, (i + 1) % count);
|
|
142
|
+
const dx = b[0] - a[0];
|
|
143
|
+
const dy = b[1] - a[1];
|
|
144
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
145
|
+
into.q((a[0] + b[0]) / 2 + (dy / len) * bump * side, (a[1] + b[1]) / 2 + (-dx / len) * bump * side, b[0], b[1]);
|
|
146
|
+
}
|
|
147
|
+
if (isClosed) {
|
|
148
|
+
into.z();
|
|
149
|
+
}
|
|
150
|
+
return into;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A rounded rectangle path. */
|
|
154
|
+
export function roundedRect(px: number, py: number, w: number, h: number, r: number): Path {
|
|
155
|
+
return new Path()
|
|
156
|
+
.m(px + r, py).l(px + w - r, py).q(px + w, py, px + w, py + r).l(px + w, py + h - r).q(px + w, py + h, px + w - r, py + h)
|
|
157
|
+
.l(px + r, py + h).q(px, py + h, px, py + h - r).l(px, py + r).q(px, py, px + r, py).z();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A closed ellipse as cubic curves (never Pixi's `arc()`, which draws a stray line from the origin). */
|
|
161
|
+
export function ellipsePath(cx: number, cy: number, rx: number, ry: number): Path {
|
|
162
|
+
const k = 0.5523;
|
|
163
|
+
return new Path()
|
|
164
|
+
.m(cx + rx, cy)
|
|
165
|
+
.c(cx + rx, cy + ry * k, cx + rx * k, cy + ry, cx, cy + ry)
|
|
166
|
+
.c(cx - rx * k, cy + ry, cx - rx, cy + ry * k, cx - rx, cy)
|
|
167
|
+
.c(cx - rx, cy - ry * k, cx - rx * k, cy - ry, cx, cy - ry)
|
|
168
|
+
.c(cx + rx * k, cy - ry, cx + rx, cy - ry * k, cx + rx, cy)
|
|
169
|
+
.z();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Clamps `v` to [lo, hi]. */
|
|
173
|
+
export const clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Anchors, facings from movement, and the look-at helper (§3.2, §3.3, §3.6). */
|
|
2
|
+
import { toLook } from './look-like';
|
|
3
|
+
import { composeAvatar } from './render/compose';
|
|
4
|
+
import type { Anchor, Facing, LookDir, LookLike, Point, Pose, RenderOptions } from './render/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The rig anchors for a look in a facing and pose: place name plates,
|
|
8
|
+
* bubbles, props and world objects with these, never with magic numbers.
|
|
9
|
+
* Rig units (a 100-unit head), feet at (0, 0), y down.
|
|
10
|
+
*/
|
|
11
|
+
export function layout(like: LookLike, opts: Pick<RenderOptions, 'facing' | 'view' | 'pose' | 'figure'> = {}): Readonly<Record<Anchor, Point>> {
|
|
12
|
+
return composeAvatar(toLook(like), { frame: 'full', ...opts }).anchors;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const EIGHT: readonly Facing[] = ['E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'NE'];
|
|
16
|
+
const FOUR: readonly Facing[] = ['E', 'S', 'W', 'N'];
|
|
17
|
+
|
|
18
|
+
/** The facing for a movement vector (screen space, y down), in 8 or 4 directions. */
|
|
19
|
+
export function facingFrom(dx: number, dy: number, dirs: 4 | 8 = 8): Facing {
|
|
20
|
+
if (dx === 0 && dy === 0) {
|
|
21
|
+
return 'S';
|
|
22
|
+
}
|
|
23
|
+
const angle = Math.atan2(dy, dx);
|
|
24
|
+
const list = dirs === 8 ? EIGHT : FOUR;
|
|
25
|
+
const step = (Math.PI * 2) / list.length;
|
|
26
|
+
const index = ((Math.round(angle / step) % list.length) + list.length) % list.length;
|
|
27
|
+
return list[index] ?? 'S';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A look direction from one screen point toward another, for card tables
|
|
32
|
+
* where each seat looks at the active player. `strength` scales it.
|
|
33
|
+
*/
|
|
34
|
+
export function lookToward(from: Point, to: Point, strength = 1): LookDir {
|
|
35
|
+
const dx = to.x - from.x;
|
|
36
|
+
const dy = to.y - from.y;
|
|
37
|
+
const len = Math.hypot(dx, dy);
|
|
38
|
+
if (len < 1e-6) {
|
|
39
|
+
return { x: 0, y: 0 };
|
|
40
|
+
}
|
|
41
|
+
return { x: (dx / len) * strength, y: (dy / len) * strength * 0.7 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A pose's gaze toward a point, keeping the rest of the pose. */
|
|
45
|
+
export function withGaze(pose: Pose, look: LookDir): Pose {
|
|
46
|
+
return { ...pose, look };
|
|
47
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Turns anything a render accepts into a look (or `null` for the silhouette), caching decoded codes. */
|
|
2
|
+
import { decodeLook, encodeLook, type AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
|
|
3
|
+
import type { LookLike } from './render/types';
|
|
4
|
+
|
|
5
|
+
const decoded = new Map<string, AvatarLook | null>();
|
|
6
|
+
const DECODE_CACHE = 512;
|
|
7
|
+
|
|
8
|
+
/** Decodes a code once (an LRU of 512). */
|
|
9
|
+
function fromCode(code: string): AvatarLook | null {
|
|
10
|
+
const hit = decoded.get(code);
|
|
11
|
+
if (hit !== undefined || decoded.has(code)) {
|
|
12
|
+
return hit ?? null;
|
|
13
|
+
}
|
|
14
|
+
const look = decodeLook(code)?.look ?? null;
|
|
15
|
+
if (decoded.size >= DECODE_CACHE) {
|
|
16
|
+
const oldest = decoded.keys().next().value;
|
|
17
|
+
if (oldest !== undefined) {
|
|
18
|
+
decoded.delete(oldest);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
decoded.set(code, look);
|
|
22
|
+
return look;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The look a {@link LookLike} names, or `null` (hidden, malformed, missing). */
|
|
26
|
+
export function toLook(like: LookLike): AvatarLook | null {
|
|
27
|
+
if (like === null || like === undefined || like === '') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (typeof like === 'string') {
|
|
31
|
+
return fromCode(like);
|
|
32
|
+
}
|
|
33
|
+
if ('v' in like) {
|
|
34
|
+
return like;
|
|
35
|
+
}
|
|
36
|
+
if ('look' in like) {
|
|
37
|
+
return like.look;
|
|
38
|
+
}
|
|
39
|
+
return fromCode(like.code);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The code a {@link LookLike} names (`''` for none). */
|
|
43
|
+
export function toCode(like: LookLike): string {
|
|
44
|
+
if (typeof like === 'string') {
|
|
45
|
+
return like;
|
|
46
|
+
}
|
|
47
|
+
if (like && 'code' in like) {
|
|
48
|
+
return like.code;
|
|
49
|
+
}
|
|
50
|
+
const look = toLook(like);
|
|
51
|
+
return look ? encodeLook(look) : '';
|
|
52
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `render.bitmap` and `render.dataUrl`: cached images of an avatar, keyed by
|
|
3
|
+
* code, pose bucket, size and view (an LRU of 256 entries).
|
|
4
|
+
*/
|
|
5
|
+
import { toCode } from '../look-like';
|
|
6
|
+
import { pixelSize } from '../render/frame';
|
|
7
|
+
import type { LookLike, RenderOptions } from '../render/types';
|
|
8
|
+
import { renderCanvas } from './canvas';
|
|
9
|
+
import { renderKey } from './pose-key';
|
|
10
|
+
import { renderSvg } from './svg';
|
|
11
|
+
|
|
12
|
+
const CACHE_SIZE = 256;
|
|
13
|
+
const bitmaps = new Map<string, Promise<ImageBitmap>>();
|
|
14
|
+
|
|
15
|
+
function remember<T>(cache: Map<string, T>, key: string, make: () => T): T {
|
|
16
|
+
const hit = cache.get(key);
|
|
17
|
+
if (hit !== undefined) {
|
|
18
|
+
cache.delete(key);
|
|
19
|
+
cache.set(key, hit);
|
|
20
|
+
return hit;
|
|
21
|
+
}
|
|
22
|
+
const value = make();
|
|
23
|
+
cache.set(key, value);
|
|
24
|
+
if (cache.size > CACHE_SIZE) {
|
|
25
|
+
const oldest = cache.keys().next().value;
|
|
26
|
+
if (oldest !== undefined) {
|
|
27
|
+
cache.delete(oldest);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A canvas of the avatar's frame size (an `OffscreenCanvas` where available). */
|
|
34
|
+
export function drawToCanvas(like: LookLike, opts: RenderOptions): OffscreenCanvas | HTMLCanvasElement {
|
|
35
|
+
const { width, height } = pixelSize(opts);
|
|
36
|
+
const w = Math.max(1, Math.ceil(width));
|
|
37
|
+
const h = Math.max(1, Math.ceil(height));
|
|
38
|
+
if (typeof OffscreenCanvas === 'undefined') {
|
|
39
|
+
const canvas = Object.assign(document.createElement('canvas'), { width: w, height: h });
|
|
40
|
+
const ctx = canvas.getContext('2d');
|
|
41
|
+
if (ctx) {
|
|
42
|
+
renderCanvas(ctx, like, { ...opts, x: 0, y: 0 });
|
|
43
|
+
}
|
|
44
|
+
return canvas;
|
|
45
|
+
}
|
|
46
|
+
const canvas = new OffscreenCanvas(w, h);
|
|
47
|
+
const ctx = canvas.getContext('2d');
|
|
48
|
+
if (ctx) {
|
|
49
|
+
renderCanvas(ctx, like, { ...opts, x: 0, y: 0 });
|
|
50
|
+
}
|
|
51
|
+
return canvas;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An `ImageBitmap` of the avatar (cached). */
|
|
55
|
+
export function renderBitmap(like: LookLike, opts: RenderOptions = {}): Promise<ImageBitmap> {
|
|
56
|
+
return remember(bitmaps, renderKey(toCode(like), opts), () => createImageBitmap(drawToCanvas(like, opts)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A data URL: `svg` (synchronous inside, the default) or `png`. */
|
|
60
|
+
export async function renderDataUrl(like: LookLike, opts: RenderOptions = {}, type: 'svg' | 'png' = 'svg'): Promise<string> {
|
|
61
|
+
if (type === 'svg') {
|
|
62
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(renderSvg(like, opts))}`;
|
|
63
|
+
}
|
|
64
|
+
const canvas = drawToCanvas(like, opts);
|
|
65
|
+
if ('toDataURL' in canvas) {
|
|
66
|
+
return canvas.toDataURL('image/png');
|
|
67
|
+
}
|
|
68
|
+
const blob = await canvas.convertToBlob({ type: 'image/png' });
|
|
69
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
70
|
+
let binary = '';
|
|
71
|
+
for (const byte of bytes) {
|
|
72
|
+
binary += String.fromCharCode(byte);
|
|
73
|
+
}
|
|
74
|
+
return `data:image/png;base64,${btoa(binary)}`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene → Canvas 2D. Paths become cached `Path2D`s (no string building per
|
|
3
|
+
* frame after the first), transforms map to `ctx.transform`, clips to
|
|
4
|
+
* `ctx.clip`. The rim draws its group into a layer and stamps it with a
|
|
5
|
+
* light edge in one `drawImage`.
|
|
6
|
+
*/
|
|
7
|
+
import type { Clip, Paint, SceneNode, TfOp } from '../scene';
|
|
8
|
+
import { RIM_COLOUR } from './svg-markup';
|
|
9
|
+
|
|
10
|
+
/** A 2D context from a canvas or an `OffscreenCanvas`. */
|
|
11
|
+
export type Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
12
|
+
|
|
13
|
+
/** Makes a scratch canvas of a size (for the rim layer); `null` where none can be made. */
|
|
14
|
+
export type LayerFactory = (width: number, height: number) => { readonly canvas: CanvasImageSource; readonly ctx: Ctx2D } | null;
|
|
15
|
+
|
|
16
|
+
const FONTS = { mono: "'Space Mono', monospace", pixel: "'VT323', monospace" } as const;
|
|
17
|
+
const pathCache = new Map<string, Path2D>();
|
|
18
|
+
const PATH_CACHE = 4096;
|
|
19
|
+
|
|
20
|
+
function path2d(d: string): Path2D {
|
|
21
|
+
let p = pathCache.get(d);
|
|
22
|
+
if (!p) {
|
|
23
|
+
p = new Path2D(d);
|
|
24
|
+
if (pathCache.size >= PATH_CACHE) {
|
|
25
|
+
pathCache.clear();
|
|
26
|
+
}
|
|
27
|
+
pathCache.set(d, p);
|
|
28
|
+
}
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyTf(ctx: Ctx2D, tf: readonly TfOp[] | undefined): void {
|
|
33
|
+
for (const op of tf ?? []) {
|
|
34
|
+
switch (op.k) {
|
|
35
|
+
case 't':
|
|
36
|
+
ctx.translate(op.x, op.y);
|
|
37
|
+
break;
|
|
38
|
+
case 's':
|
|
39
|
+
ctx.scale(op.x, op.y);
|
|
40
|
+
break;
|
|
41
|
+
case 'r':
|
|
42
|
+
ctx.rotate((op.a * Math.PI) / 180);
|
|
43
|
+
break;
|
|
44
|
+
case 'skx':
|
|
45
|
+
ctx.transform(1, 0, Math.tan((op.a * Math.PI) / 180), 1, 0, 0);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shapePath(node: SceneNode): Path2D | null {
|
|
52
|
+
if (node.k === 'path') {
|
|
53
|
+
return path2d(node.d);
|
|
54
|
+
}
|
|
55
|
+
if (node.k === 'ellipse') {
|
|
56
|
+
const p = new Path2D();
|
|
57
|
+
p.ellipse(node.cx, node.cy, node.rx, node.ry, 0, 0, Math.PI * 2);
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
if (node.k === 'rect') {
|
|
61
|
+
const p = new Path2D();
|
|
62
|
+
const r = Math.min(node.r ?? 0, node.w / 2, node.h / 2);
|
|
63
|
+
p.moveTo(node.x + r, node.y);
|
|
64
|
+
p.arcTo(node.x + node.w, node.y, node.x + node.w, node.y + node.h, r);
|
|
65
|
+
p.arcTo(node.x + node.w, node.y + node.h, node.x, node.y + node.h, r);
|
|
66
|
+
p.arcTo(node.x, node.y + node.h, node.x, node.y, r);
|
|
67
|
+
p.arcTo(node.x, node.y, node.x + node.w, node.y, r);
|
|
68
|
+
p.closePath();
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function applyClip(ctx: Ctx2D, clip: Clip): void {
|
|
75
|
+
const all = new Path2D();
|
|
76
|
+
for (const shape of clip.shapes) {
|
|
77
|
+
const p = shapePath(shape);
|
|
78
|
+
if (p) {
|
|
79
|
+
all.addPath(p);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
ctx.clip(all, clip.evenOdd ? 'evenodd' : 'nonzero');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fillAndStroke(ctx: Ctx2D, p: Path2D, paint: Paint): void {
|
|
86
|
+
const alpha = ctx.globalAlpha;
|
|
87
|
+
if (paint.opacity !== undefined) {
|
|
88
|
+
ctx.globalAlpha = alpha * paint.opacity;
|
|
89
|
+
}
|
|
90
|
+
if (paint.fill && paint.fill !== 'none') {
|
|
91
|
+
const fillAlpha = ctx.globalAlpha;
|
|
92
|
+
if (paint.fillOpacity !== undefined) {
|
|
93
|
+
ctx.globalAlpha = fillAlpha * paint.fillOpacity;
|
|
94
|
+
}
|
|
95
|
+
ctx.fillStyle = paint.fill;
|
|
96
|
+
ctx.fill(p, paint.evenOdd ? 'evenodd' : 'nonzero');
|
|
97
|
+
ctx.globalAlpha = fillAlpha;
|
|
98
|
+
}
|
|
99
|
+
if (paint.stroke && paint.stroke !== 'none') {
|
|
100
|
+
ctx.strokeStyle = paint.stroke;
|
|
101
|
+
ctx.lineWidth = paint.sw ?? 1;
|
|
102
|
+
ctx.lineJoin = 'round';
|
|
103
|
+
ctx.lineCap = paint.cap ?? 'round';
|
|
104
|
+
ctx.stroke(p);
|
|
105
|
+
}
|
|
106
|
+
ctx.globalAlpha = alpha;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Paints scene nodes in the context's current transform (rig units → pixels is the caller's job). */
|
|
110
|
+
export function paintNodes(ctx: Ctx2D, nodes: readonly SceneNode[], layer?: LayerFactory): void {
|
|
111
|
+
for (const node of nodes) {
|
|
112
|
+
paintNode(ctx, node, layer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function paintRim(ctx: Ctx2D, children: readonly SceneNode[], layer: LayerFactory | undefined): boolean {
|
|
117
|
+
const target = ctx.canvas;
|
|
118
|
+
const made = layer?.(target.width, target.height);
|
|
119
|
+
if (!made || !('filter' in made.ctx)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
made.ctx.setTransform(ctx.getTransform());
|
|
123
|
+
paintNodes(made.ctx, children, layer);
|
|
124
|
+
const scale = Math.hypot(ctx.getTransform().a, ctx.getTransform().b);
|
|
125
|
+
const r = Math.max(1, 2.8 * scale);
|
|
126
|
+
const c = `${RIM_COLOUR}7a`;
|
|
127
|
+
ctx.save();
|
|
128
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
129
|
+
ctx.filter = `drop-shadow(${String(r)}px 0 0 ${c}) drop-shadow(${String(-r)}px 0 0 ${c}) drop-shadow(0 ${String(r)}px 0 ${c}) drop-shadow(0 ${String(-r)}px 0 ${c})`;
|
|
130
|
+
ctx.drawImage(made.canvas, 0, 0);
|
|
131
|
+
ctx.restore();
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function paintNode(ctx: Ctx2D, node: SceneNode, layer: LayerFactory | undefined): void {
|
|
136
|
+
ctx.save();
|
|
137
|
+
if (node.k === 'g') {
|
|
138
|
+
applyTf(ctx, node.tf);
|
|
139
|
+
if (node.clip) {
|
|
140
|
+
applyClip(ctx, node.clip);
|
|
141
|
+
}
|
|
142
|
+
if (node.opacity !== undefined) {
|
|
143
|
+
ctx.globalAlpha *= node.opacity;
|
|
144
|
+
}
|
|
145
|
+
if (!(node.rim === true && paintRim(ctx, node.children, layer))) {
|
|
146
|
+
paintNodes(ctx, node.children, layer);
|
|
147
|
+
}
|
|
148
|
+
ctx.restore();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (node.k === 'text') {
|
|
152
|
+
ctx.font = `${node.bold ? '700 ' : ''}${String(node.size)}px ${FONTS[node.font]}`;
|
|
153
|
+
ctx.textAlign = node.anchor === 'middle' ? 'center' : 'left';
|
|
154
|
+
ctx.textBaseline = 'alphabetic';
|
|
155
|
+
if (node.opacity !== undefined) {
|
|
156
|
+
ctx.globalAlpha *= node.opacity;
|
|
157
|
+
}
|
|
158
|
+
if (node.stroke && node.stroke !== 'none') {
|
|
159
|
+
ctx.strokeStyle = node.stroke;
|
|
160
|
+
ctx.lineWidth = node.sw ?? 1;
|
|
161
|
+
ctx.lineJoin = 'round';
|
|
162
|
+
ctx.strokeText(node.text, node.x, node.y);
|
|
163
|
+
}
|
|
164
|
+
if (node.fill && node.fill !== 'none') {
|
|
165
|
+
ctx.fillStyle = node.fill;
|
|
166
|
+
ctx.fillText(node.text, node.x, node.y);
|
|
167
|
+
}
|
|
168
|
+
ctx.restore();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
applyTf(ctx, node.tf);
|
|
172
|
+
const p = shapePath(node);
|
|
173
|
+
if (p) {
|
|
174
|
+
fillAndStroke(ctx, p, node);
|
|
175
|
+
}
|
|
176
|
+
ctx.restore();
|
|
177
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `render.canvas` and `render.canvasAt`: draw an avatar on a 2D canvas,
|
|
3
|
+
* synchronously once the art is ready. `canvas` fills a frame box at (x, y);
|
|
4
|
+
* `canvasAt` puts the feet at (x, y) with a pixels-per-unit scale (the world
|
|
5
|
+
* and crowds).
|
|
6
|
+
*/
|
|
7
|
+
import { toLook } from '../look-like';
|
|
8
|
+
import { composeAvatar } from '../render/compose';
|
|
9
|
+
import { frameBox, framed, pixelSize } from '../render/frame';
|
|
10
|
+
import type { LookLike, RenderOptions } from '../render/types';
|
|
11
|
+
import type { SceneNode } from '../scene';
|
|
12
|
+
import { paintNodes, type Ctx2D, type LayerFactory } from './canvas-paint';
|
|
13
|
+
|
|
14
|
+
/** A scratch canvas for the rim layer: an `OffscreenCanvas`, else a DOM canvas, else none. */
|
|
15
|
+
export const defaultLayer: LayerFactory = (width, height) => {
|
|
16
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
17
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
18
|
+
const ctx = canvas.getContext('2d');
|
|
19
|
+
return ctx ? { canvas, ctx } : null;
|
|
20
|
+
}
|
|
21
|
+
if (typeof document !== 'undefined') {
|
|
22
|
+
const canvas = document.createElement('canvas');
|
|
23
|
+
canvas.width = width;
|
|
24
|
+
canvas.height = height;
|
|
25
|
+
const ctx = canvas.getContext('2d');
|
|
26
|
+
return ctx ? { canvas, ctx } : null;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Where and how big: the frame box's top-left corner in canvas pixels. */
|
|
32
|
+
export interface CanvasPlacement extends RenderOptions {
|
|
33
|
+
readonly x: number;
|
|
34
|
+
readonly y: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Paints scene nodes with rig space mapped to the frame box at (x, y). */
|
|
38
|
+
export function paintFramed(ctx: Ctx2D, nodes: readonly SceneNode[], opts: CanvasPlacement): void {
|
|
39
|
+
const [fx, fy] = frameBox(opts);
|
|
40
|
+
const { scale } = pixelSize(opts);
|
|
41
|
+
ctx.save();
|
|
42
|
+
ctx.translate(opts.x, opts.y);
|
|
43
|
+
ctx.scale(scale, scale);
|
|
44
|
+
ctx.translate(-fx, -fy);
|
|
45
|
+
paintNodes(ctx, nodes, defaultLayer);
|
|
46
|
+
ctx.restore();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Draws an avatar in a frame box whose top-left is (x, y), `size` px on its longer edge. */
|
|
50
|
+
export function renderCanvas(ctx: Ctx2D, like: LookLike, opts: CanvasPlacement): void {
|
|
51
|
+
const look = toLook(like);
|
|
52
|
+
paintFramed(ctx, framed(composeAvatar(look, opts).nodes, look, opts), opts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Draws the full character with its feet at (x, y), `scale` px per rig unit (a 100-unit head). */
|
|
56
|
+
export function renderCanvasAt(ctx: Ctx2D, like: LookLike, opts: RenderOptions & { readonly x: number; readonly y: number; readonly scale: number }): void {
|
|
57
|
+
ctx.save();
|
|
58
|
+
ctx.translate(opts.x, opts.y);
|
|
59
|
+
ctx.scale(opts.scale, opts.scale);
|
|
60
|
+
paintNodes(ctx, composeAvatar(toLook(like), { frame: 'full', ...opts }).nodes, defaultLayer);
|
|
61
|
+
ctx.restore();
|
|
62
|
+
}
|