@diffenderfer-games/hub 2.0.0-next.5 → 2.0.0-next.6
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 +54 -19
- package/README.md +3 -6
- package/dist/auto.d.ts +5 -5
- package/dist/auto.js +2 -2
- package/dist/avatar-pixi.d.ts +147 -0
- package/dist/avatar-pixi.d.ts.map +1 -0
- package/dist/avatar-pixi.js +280 -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 +487 -0
- package/dist/avatar.js.map +1 -0
- package/dist/daily.d.ts +2 -2
- package/dist/doctor/cli.mjs +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/input.d.ts +2 -2
- package/dist/shared/animator.d.ts +104 -0
- package/dist/shared/animator.d.ts.map +1 -0
- package/dist/shared/art.js +1804 -0
- package/dist/shared/art.js.map +1 -0
- package/dist/shared/atlas.d.ts +134 -0
- package/dist/shared/atlas.d.ts.map +1 -0
- package/dist/shared/avatar.d.ts +117 -0
- package/dist/shared/avatar.d.ts.map +1 -0
- package/dist/shared/bridge.js +13 -5
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +129 -18
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +120 -26
- 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 +101 -0
- package/dist/shared/describe.js.map +1 -0
- package/dist/shared/globals.js +1 -39
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +503 -14
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.d.ts +31 -13
- package/dist/shared/input.d.ts.map +1 -1
- package/dist/shared/input.js +40 -2
- 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/live-canvas.js +62 -0
- package/dist/shared/live-canvas.js.map +1 -0
- package/dist/shared/mp.js +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.js +1 -1
- package/dist/shared/rooms.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 +55 -4
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/src.js +2020 -144
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/svg-markup.js +1860 -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 +166 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/social.d.ts +2 -2
- package/dist/social.js +2 -2
- package/dist/turns.d.ts +39 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +117 -0
- package/dist/turns.js.map +1 -0
- package/package.json +33 -3
- package/src/avatar/adapters/pixi-avatar.ts +73 -0
- package/src/avatar/adapters/pixi-crowd.ts +302 -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 +9 -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 +68 -0
- package/src/avatar/art-store.ts +36 -0
- package/src/avatar/atlas.ts +236 -0
- package/src/avatar/catalog-view.ts +14 -0
- package/src/avatar/colour.ts +109 -0
- package/src/avatar/describe.ts +76 -0
- package/src/avatar/face.ts +187 -0
- package/src/avatar/geom.ts +168 -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 +258 -0
- package/src/avatar/render/context.ts +117 -0
- package/src/avatar/render/eyes.ts +78 -0
- package/src/avatar/render/eyewear.ts +56 -0
- package/src/avatar/render/frame.ts +60 -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 +84 -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 +62 -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 +8 -0
- package/src/core/errors.ts +7 -1
- package/src/core/hub-progress.ts +269 -238
- package/src/core/hub.ts +174 -6
- package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
- package/src/core/offline/hub-kinds/index.ts +9 -0
- package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
- package/src/core/page-account.ts +121 -0
- package/src/core/rest/routes.gen.ts +31 -1
- package/src/core/resume/hub-resume.ts +39 -5
- package/src/core/resume/resume-types.ts +2 -2
- 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/turns/hub-turns.ts +140 -0
- package/src/core/turns/turn-game.ts +388 -0
- package/src/core/version/client-version.gen.ts +2 -2
- package/src/daily/daily-system.ts +112 -13
- package/src/daily/page-daily.ts +2 -2
- package/src/daily/types.ts +46 -9
- package/src/doctor/hub-version-fetch.ts +43 -43
- package/src/index.ts +10 -1
- package/src/input/input-system.ts +10 -1
- package/src/input/navigator.ts +11 -0
- package/src/input.ts +1 -0
- package/src/overlay/overlay.ts +25 -1
- package/src/realtime/runtime-api.ts +14 -3
- package/src/social.ts +1 -0
- package/src/turns-kit/check-rules.ts +182 -0
- package/src/turns.ts +13 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import { t as _defineProperty } from "./defineProperty.js";
|
|
2
|
+
import { C as ellipse, D as rect, E as path, N as toCode, O as rot, P as toLook, S as pixelSize, T as line, a as composeAvatar, d as EXPRESSIONS, g as mix, k as tr, m as faceOf, w as g } from "./svg-markup.js";
|
|
3
|
+
import { r as renderCanvas, t as renderKey } from "./pose-key.js";
|
|
4
|
+
import { n as renderSvg } from "./svg.js";
|
|
5
|
+
import { hashSeed, mulberry32 } from "@diffenderfer-games/hub-contract/avatar";
|
|
6
|
+
//#region src/avatar/outputs/bitmap.ts
|
|
7
|
+
/**
|
|
8
|
+
* `render.bitmap` and `render.dataUrl`: cached images of an avatar, keyed by
|
|
9
|
+
* code, pose bucket, size and view (an LRU of 256 entries).
|
|
10
|
+
*/
|
|
11
|
+
const CACHE_SIZE = 256;
|
|
12
|
+
const bitmaps = /* @__PURE__ */ new Map();
|
|
13
|
+
function remember(cache, key, make) {
|
|
14
|
+
const hit = cache.get(key);
|
|
15
|
+
if (hit !== void 0) {
|
|
16
|
+
cache.delete(key);
|
|
17
|
+
cache.set(key, hit);
|
|
18
|
+
return hit;
|
|
19
|
+
}
|
|
20
|
+
const value = make();
|
|
21
|
+
cache.set(key, value);
|
|
22
|
+
if (cache.size > CACHE_SIZE) {
|
|
23
|
+
const oldest = cache.keys().next().value;
|
|
24
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/** A canvas of the avatar's frame size (an `OffscreenCanvas` where available). */
|
|
29
|
+
function drawToCanvas(like, opts) {
|
|
30
|
+
const { width, height } = pixelSize(opts);
|
|
31
|
+
const w = Math.max(1, Math.ceil(width));
|
|
32
|
+
const h = Math.max(1, Math.ceil(height));
|
|
33
|
+
if (typeof OffscreenCanvas === "undefined") {
|
|
34
|
+
const canvas = Object.assign(document.createElement("canvas"), {
|
|
35
|
+
width: w,
|
|
36
|
+
height: h
|
|
37
|
+
});
|
|
38
|
+
const ctx = canvas.getContext("2d");
|
|
39
|
+
if (ctx) renderCanvas(ctx, like, {
|
|
40
|
+
...opts,
|
|
41
|
+
x: 0,
|
|
42
|
+
y: 0
|
|
43
|
+
});
|
|
44
|
+
return canvas;
|
|
45
|
+
}
|
|
46
|
+
const canvas = new OffscreenCanvas(w, h);
|
|
47
|
+
const ctx = canvas.getContext("2d");
|
|
48
|
+
if (ctx) renderCanvas(ctx, like, {
|
|
49
|
+
...opts,
|
|
50
|
+
x: 0,
|
|
51
|
+
y: 0
|
|
52
|
+
});
|
|
53
|
+
return canvas;
|
|
54
|
+
}
|
|
55
|
+
/** An `ImageBitmap` of the avatar (cached). */
|
|
56
|
+
function renderBitmap(like, opts = {}) {
|
|
57
|
+
return remember(bitmaps, renderKey(toCode(like), opts), () => createImageBitmap(drawToCanvas(like, opts)));
|
|
58
|
+
}
|
|
59
|
+
/** A data URL: `svg` (synchronous inside, the default) or `png`. */
|
|
60
|
+
async function renderDataUrl(like, opts = {}, type = "svg") {
|
|
61
|
+
if (type === "svg") return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(renderSvg(like, opts))}`;
|
|
62
|
+
const canvas = drawToCanvas(like, opts);
|
|
63
|
+
if ("toDataURL" in canvas) return canvas.toDataURL("image/png");
|
|
64
|
+
const blob = await canvas.convertToBlob({ type: "image/png" });
|
|
65
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
66
|
+
let binary = "";
|
|
67
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
68
|
+
return `data:image/png;base64,${btoa(binary)}`;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/avatar/animator/reactions.ts
|
|
72
|
+
const still = {
|
|
73
|
+
bob: {
|
|
74
|
+
x: 0,
|
|
75
|
+
y: 0
|
|
76
|
+
},
|
|
77
|
+
tilt: 0,
|
|
78
|
+
squash: 0
|
|
79
|
+
};
|
|
80
|
+
const ease = (u) => Math.sin(u * Math.PI);
|
|
81
|
+
/** Every reaction's track; `u` runs 0 … 1 over `ms`. */
|
|
82
|
+
const REACTIONS = {
|
|
83
|
+
nod: {
|
|
84
|
+
ms: 700,
|
|
85
|
+
at: (u) => ({
|
|
86
|
+
bob: {
|
|
87
|
+
x: 0,
|
|
88
|
+
y: Math.sin(u * Math.PI * 2) * 3 * (1 - u)
|
|
89
|
+
},
|
|
90
|
+
tilt: 0,
|
|
91
|
+
squash: 0
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
shake: {
|
|
95
|
+
ms: 800,
|
|
96
|
+
at: (u) => ({
|
|
97
|
+
bob: {
|
|
98
|
+
x: Math.sin(u * Math.PI * 4) * 4 * (1 - u),
|
|
99
|
+
y: 0
|
|
100
|
+
},
|
|
101
|
+
tilt: 0,
|
|
102
|
+
squash: 0
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
bounce: {
|
|
106
|
+
ms: 600,
|
|
107
|
+
expression: "happy",
|
|
108
|
+
at: (u) => ({
|
|
109
|
+
bob: {
|
|
110
|
+
x: 0,
|
|
111
|
+
y: -ease(u) * 8
|
|
112
|
+
},
|
|
113
|
+
tilt: 0,
|
|
114
|
+
squash: -ease(u) * .04
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
cheer: {
|
|
118
|
+
ms: 1400,
|
|
119
|
+
expression: "laugh",
|
|
120
|
+
body: "cheer",
|
|
121
|
+
at: (u) => ({
|
|
122
|
+
bob: {
|
|
123
|
+
x: 0,
|
|
124
|
+
y: -Math.abs(Math.sin(u * Math.PI * 3)) * 6
|
|
125
|
+
},
|
|
126
|
+
tilt: 0,
|
|
127
|
+
squash: 0
|
|
128
|
+
})
|
|
129
|
+
},
|
|
130
|
+
gasp: {
|
|
131
|
+
ms: 900,
|
|
132
|
+
expression: "surprised",
|
|
133
|
+
at: (u) => ({
|
|
134
|
+
bob: {
|
|
135
|
+
x: 0,
|
|
136
|
+
y: -ease(Math.min(1, u * 2)) * 3
|
|
137
|
+
},
|
|
138
|
+
tilt: 0,
|
|
139
|
+
squash: -ease(u) * .05
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
flinch: {
|
|
143
|
+
ms: 600,
|
|
144
|
+
expression: "shocked",
|
|
145
|
+
at: (u) => ({
|
|
146
|
+
bob: {
|
|
147
|
+
x: -ease(u) * 5,
|
|
148
|
+
y: ease(u) * 2
|
|
149
|
+
},
|
|
150
|
+
tilt: -ease(u) * 6,
|
|
151
|
+
squash: ease(u) * .04
|
|
152
|
+
})
|
|
153
|
+
},
|
|
154
|
+
shrug: {
|
|
155
|
+
ms: 900,
|
|
156
|
+
expression: "smug",
|
|
157
|
+
at: (u) => ({
|
|
158
|
+
bob: {
|
|
159
|
+
x: 0,
|
|
160
|
+
y: -ease(u) * 3
|
|
161
|
+
},
|
|
162
|
+
tilt: Math.sin(u * Math.PI * 2) * 5,
|
|
163
|
+
squash: 0
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
sleep: {
|
|
167
|
+
ms: 3e3,
|
|
168
|
+
expression: "sleepy",
|
|
169
|
+
body: "away",
|
|
170
|
+
at: () => still
|
|
171
|
+
},
|
|
172
|
+
wave: {
|
|
173
|
+
ms: 1500,
|
|
174
|
+
expression: "happy",
|
|
175
|
+
body: "wave",
|
|
176
|
+
at: (u) => ({
|
|
177
|
+
bob: {
|
|
178
|
+
x: 0,
|
|
179
|
+
y: 0
|
|
180
|
+
},
|
|
181
|
+
tilt: Math.sin(u * Math.PI * 4) * 3,
|
|
182
|
+
squash: 0
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
/** No motion. */
|
|
187
|
+
const STILL = still;
|
|
188
|
+
const BLINK_BUCKET = 4e3;
|
|
189
|
+
/** A deterministic random in [0, 1) for (seed, channel, bucket). */
|
|
190
|
+
function cell(seed, channel, bucket) {
|
|
191
|
+
return mulberry32(hashSeed(`${String(seed)}:${String(channel)}:${String(bucket)}`));
|
|
192
|
+
}
|
|
193
|
+
/** One blink's closure at offset `dt` from its start: up then down. */
|
|
194
|
+
function closure(dt) {
|
|
195
|
+
if (dt < 0 || dt > 150) return 0;
|
|
196
|
+
const half = 75;
|
|
197
|
+
return dt < half ? dt / half : 1 - (dt - half) / half;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Lid closure from blinking, 0 … 1: one blink per 4 s bucket at a seeded
|
|
201
|
+
* offset (so intervals fall between about 2 and 6 s), 10% of them doubled.
|
|
202
|
+
*/
|
|
203
|
+
function blinkAt(seed, t) {
|
|
204
|
+
const bucket = Math.floor(t / BLINK_BUCKET);
|
|
205
|
+
let out = 0;
|
|
206
|
+
for (const b of [bucket - 1, bucket]) {
|
|
207
|
+
const r = cell(seed, 1, b);
|
|
208
|
+
const start = b * BLINK_BUCKET + 1e3 + r() * 2e3;
|
|
209
|
+
const isDouble = r() < .1;
|
|
210
|
+
out = Math.max(out, closure(t - start), isDouble ? closure(t - start - 150 - 90) : 0);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
/** An idle wandering gaze target: holds for 3–8 s, small amplitude. */
|
|
215
|
+
function wanderAt(seed, t) {
|
|
216
|
+
const r = cell(seed, 2, Math.floor(t / 5500));
|
|
217
|
+
if (r() < .45) return {
|
|
218
|
+
x: 0,
|
|
219
|
+
y: 0
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
x: (r() * 2 - 1) * .6,
|
|
223
|
+
y: (r() * 2 - 1) * .3
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Breathing bob (rig units) and a slow sway (degrees). */
|
|
227
|
+
function breathAt(seed, t) {
|
|
228
|
+
const phase = seed % 1e3 / 1e3;
|
|
229
|
+
return {
|
|
230
|
+
bob: Math.sin((t / 2400 + phase) * Math.PI * 2) * .6,
|
|
231
|
+
sway: Math.sin((t / 5200 + phase) * Math.PI * 2) * 1.2
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** An occasional micro smile while idle: its weight at t (0 most of the time). */
|
|
235
|
+
function microSmileAt(seed, t) {
|
|
236
|
+
const bucket = Math.floor(t / 11e3);
|
|
237
|
+
const r = cell(seed, 3, bucket);
|
|
238
|
+
if (r() < .6) return 0;
|
|
239
|
+
const dt = t - (bucket * 11e3 + 2e3 + r() * 6e3);
|
|
240
|
+
if (dt < 0 || dt > 900) return 0;
|
|
241
|
+
return Math.sin(dt / 900 * Math.PI) * .35;
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/avatar/animator/animator.ts
|
|
245
|
+
/**
|
|
246
|
+
* The animator (avatars.md §3.7): a pure, time-based face and body driver.
|
|
247
|
+
* Every call (`express`, `lookAt`, `react`, …) appends to a timestamped
|
|
248
|
+
* schedule and `pose(t)` evaluates it, with no clock or animation frame of
|
|
249
|
+
* its own: the same schedule and `t` always give the same pose, so it suits
|
|
250
|
+
* game loops, Pixi tickers, three.js, CSS-driven DOM, tests and replays.
|
|
251
|
+
*/
|
|
252
|
+
const KEEP = 48;
|
|
253
|
+
const push = (list, item) => {
|
|
254
|
+
list.push(item);
|
|
255
|
+
if (list.length > KEEP) list.shift();
|
|
256
|
+
};
|
|
257
|
+
/** The last event at or before t. */
|
|
258
|
+
const latest = (list, t) => {
|
|
259
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
260
|
+
const e = list[i];
|
|
261
|
+
if (e && e.t <= t) return e;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const easeOut = (u) => 1 - (1 - Math.min(1, Math.max(0, u))) ** 2;
|
|
265
|
+
function prefersReducedMotion() {
|
|
266
|
+
return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
267
|
+
}
|
|
268
|
+
/** A face and body driver for one avatar. */
|
|
269
|
+
var Animator = class {
|
|
270
|
+
constructor(look, opts = {}) {
|
|
271
|
+
_defineProperty(this, "reducedMotion", void 0);
|
|
272
|
+
_defineProperty(this, "seed", void 0);
|
|
273
|
+
_defineProperty(this, "idle", void 0);
|
|
274
|
+
_defineProperty(this, "clock", void 0);
|
|
275
|
+
_defineProperty(this, "bases", []);
|
|
276
|
+
_defineProperty(this, "expresses", []);
|
|
277
|
+
_defineProperty(this, "looks", []);
|
|
278
|
+
_defineProperty(this, "talks", []);
|
|
279
|
+
_defineProperty(this, "reacts", []);
|
|
280
|
+
_defineProperty(this, "bodies", []);
|
|
281
|
+
_defineProperty(this, "statuses", []);
|
|
282
|
+
_defineProperty(this, "current", void 0);
|
|
283
|
+
this.current = look;
|
|
284
|
+
this.seed = hashSeed(opts.seed ?? (typeof look === "string" ? look : "dg"));
|
|
285
|
+
this.idle = opts.idle ?? true;
|
|
286
|
+
this.reducedMotion = opts.reducedMotion === "auto" || opts.reducedMotion === void 0 ? prefersReducedMotion() : opts.reducedMotion;
|
|
287
|
+
this.clock = opts.clock ?? (() => typeof performance === "undefined" ? Date.now() : performance.now());
|
|
288
|
+
this.bases.push({
|
|
289
|
+
t: -Infinity,
|
|
290
|
+
face: EXPRESSIONS[opts.base ?? "neutral"],
|
|
291
|
+
fade: 0
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** The look this animator drives (adapters render it with each pose). */
|
|
295
|
+
get look() {
|
|
296
|
+
return this.current;
|
|
297
|
+
}
|
|
298
|
+
/** Swaps the look (an `ev:avatar` change) without resetting the schedule. */
|
|
299
|
+
setLook(look) {
|
|
300
|
+
this.current = look;
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
now(o) {
|
|
304
|
+
return o?.at ?? this.clock();
|
|
305
|
+
}
|
|
306
|
+
fade(ms) {
|
|
307
|
+
return this.reducedMotion ? Math.min(ms, 120) : ms;
|
|
308
|
+
}
|
|
309
|
+
/** Shows an expression (or raw parameters), then returns to the base. */
|
|
310
|
+
express(e, o = {}) {
|
|
311
|
+
push(this.expresses, {
|
|
312
|
+
t: this.now(o),
|
|
313
|
+
face: faceOf(e),
|
|
314
|
+
fade: this.fade(o.fadeMs ?? 180),
|
|
315
|
+
hold: o.holdMs ?? 1500
|
|
316
|
+
});
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
/** Sets the resting expression. */
|
|
320
|
+
setBase(e, o = {}) {
|
|
321
|
+
push(this.bases, {
|
|
322
|
+
t: this.now(o),
|
|
323
|
+
face: faceOf(e),
|
|
324
|
+
fade: this.fade(o.fadeMs ?? 250)
|
|
325
|
+
});
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
/** Eases the gaze toward a direction; `null` goes back to wandering (or the centre with `idle: false`). */
|
|
329
|
+
lookAt(target, o = {}) {
|
|
330
|
+
push(this.looks, {
|
|
331
|
+
t: this.now(o),
|
|
332
|
+
target,
|
|
333
|
+
speed: o.speed ?? 6
|
|
334
|
+
});
|
|
335
|
+
return this;
|
|
336
|
+
}
|
|
337
|
+
/** Flaps the mouth until `talk(false)`. */
|
|
338
|
+
talk(isOn, o = {}) {
|
|
339
|
+
const t = this.now(o);
|
|
340
|
+
push(this.talks, {
|
|
341
|
+
t,
|
|
342
|
+
level: isOn ? 1 : 0,
|
|
343
|
+
until: Infinity
|
|
344
|
+
});
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
/** Talks for a duration. */
|
|
348
|
+
talkFor(ms, o = {}) {
|
|
349
|
+
const t = this.now(o);
|
|
350
|
+
push(this.talks, {
|
|
351
|
+
t,
|
|
352
|
+
level: 1,
|
|
353
|
+
until: t + ms
|
|
354
|
+
});
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
/** Drives the mouth from audio amplitude (0 … 1). */
|
|
358
|
+
talkLevel(level, o = {}) {
|
|
359
|
+
push(this.talks, {
|
|
360
|
+
t: this.now(o),
|
|
361
|
+
level: Math.max(0, Math.min(1, level)),
|
|
362
|
+
until: Infinity
|
|
363
|
+
});
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
/** Plays a one-shot reaction. */
|
|
367
|
+
react(reaction, o = {}) {
|
|
368
|
+
push(this.reacts, {
|
|
369
|
+
t: this.now(o),
|
|
370
|
+
reaction
|
|
371
|
+
});
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
/** Sets the body pose; cycle poses (`walk`, `run`, `dance`) loop every `cycleMs`. */
|
|
375
|
+
setBody(body, o = {}) {
|
|
376
|
+
push(this.bodies, {
|
|
377
|
+
t: this.now(o),
|
|
378
|
+
body,
|
|
379
|
+
cycleMs: o.cycleMs ?? (body === "run" ? 320 : 520)
|
|
380
|
+
});
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
/** Sets or clears the status prop. */
|
|
384
|
+
setStatus(status, o = {}) {
|
|
385
|
+
push(this.statuses, {
|
|
386
|
+
t: this.now(o),
|
|
387
|
+
status
|
|
388
|
+
});
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
/** Plays the tuned face and reaction for a game event. */
|
|
392
|
+
feel(feeling, o = {}) {
|
|
393
|
+
const at = { at: this.now(o) };
|
|
394
|
+
switch (feeling) {
|
|
395
|
+
case "win": return this.express("laugh", {
|
|
396
|
+
...at,
|
|
397
|
+
holdMs: 1800
|
|
398
|
+
}).react("cheer", at);
|
|
399
|
+
case "lose": return this.express("sad", {
|
|
400
|
+
...at,
|
|
401
|
+
holdMs: 1800
|
|
402
|
+
});
|
|
403
|
+
case "bigPlay": return this.express("grin", at).react("bounce", at);
|
|
404
|
+
case "victim": return this.express("shocked", {
|
|
405
|
+
...at,
|
|
406
|
+
holdMs: 900
|
|
407
|
+
}).react("flinch", at);
|
|
408
|
+
case "waiting": return this.setBase("thinking", at);
|
|
409
|
+
case "yourTurn": return this.express("determined", at).react("nod", at);
|
|
410
|
+
case "idle": return this.setBase("neutral", at);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
faceAt(t) {
|
|
414
|
+
const base = latest(this.bases, t);
|
|
415
|
+
const prior = base ? latest(this.bases.filter((b) => b.t < base.t), t) : void 0;
|
|
416
|
+
let face = base ? prior && base.fade > 0 ? mix(prior.face, base.face, easeOut((t - base.t) / base.fade)) : base.face : EXPRESSIONS.neutral;
|
|
417
|
+
if (this.idle) {
|
|
418
|
+
const smile = microSmileAt(this.seed, t);
|
|
419
|
+
if (smile > 0) face = mix(face, "happy", smile);
|
|
420
|
+
}
|
|
421
|
+
const e = latest(this.expresses, t);
|
|
422
|
+
if (e) {
|
|
423
|
+
const dt = t - e.t;
|
|
424
|
+
if (dt < e.fade * 2 + e.hold) {
|
|
425
|
+
const w = dt < e.fade ? easeOut(dt / e.fade) : dt < e.fade + e.hold ? 1 : 1 - easeOut((dt - e.fade - e.hold) / e.fade);
|
|
426
|
+
face = mix(face, e.face, w);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const r = latest(this.reacts, t);
|
|
430
|
+
const track = r ? REACTIONS[r.reaction] : void 0;
|
|
431
|
+
if (r && track?.expression && t - r.t < track.ms) face = mix(face, track.expression, Math.min(1, (t - r.t) / 120));
|
|
432
|
+
return face;
|
|
433
|
+
}
|
|
434
|
+
gazeAt(t) {
|
|
435
|
+
let from = {
|
|
436
|
+
x: 0,
|
|
437
|
+
y: 0
|
|
438
|
+
};
|
|
439
|
+
let current;
|
|
440
|
+
for (const e of this.looks) {
|
|
441
|
+
if (e.t > t) break;
|
|
442
|
+
if (current) from = this.approach(from, current, e.t);
|
|
443
|
+
current = e;
|
|
444
|
+
}
|
|
445
|
+
if (!current || current.target === null) {
|
|
446
|
+
const wander = this.idle && !this.reducedMotion ? wanderAt(this.seed, t) : {
|
|
447
|
+
x: 0,
|
|
448
|
+
y: 0
|
|
449
|
+
};
|
|
450
|
+
return current ? this.approach(from, {
|
|
451
|
+
...current,
|
|
452
|
+
target: wander
|
|
453
|
+
}, t) : wander;
|
|
454
|
+
}
|
|
455
|
+
return this.approach(from, current, t);
|
|
456
|
+
}
|
|
457
|
+
approach(from, e, t) {
|
|
458
|
+
const target = e.target ?? {
|
|
459
|
+
x: 0,
|
|
460
|
+
y: 0
|
|
461
|
+
};
|
|
462
|
+
const k = this.reducedMotion ? 1 : 1 - Math.exp(-e.speed * Math.max(0, t - e.t) / 1e3);
|
|
463
|
+
return {
|
|
464
|
+
x: from.x + (target.x - from.x) * k,
|
|
465
|
+
y: from.y + (target.y - from.y) * k
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
talkAt(t) {
|
|
469
|
+
const e = latest(this.talks, t);
|
|
470
|
+
if (!e || e.level === 0 || t > e.until) return 0;
|
|
471
|
+
if (this.reducedMotion) return .5 * e.level;
|
|
472
|
+
const flap = Math.abs(Math.sin((t - e.t) / 1e3 * Math.PI * 5.5)) * (.65 + .35 * Math.sin((t - e.t) / 1e3 * 3.1));
|
|
473
|
+
return e.level * flap;
|
|
474
|
+
}
|
|
475
|
+
reactionAt(t) {
|
|
476
|
+
const r = latest(this.reacts, t);
|
|
477
|
+
if (!r) return { frame: STILL };
|
|
478
|
+
const track = REACTIONS[r.reaction];
|
|
479
|
+
const u = (t - r.t) / track.ms;
|
|
480
|
+
if (u >= 1) return { frame: STILL };
|
|
481
|
+
return {
|
|
482
|
+
frame: this.reducedMotion ? STILL : track.at(u),
|
|
483
|
+
...track.body ? { body: track.body } : {}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/** The pose at time `t` (same clock as the calls). Pure: the same schedule and `t` give the same pose. */
|
|
487
|
+
pose(t = this.clock()) {
|
|
488
|
+
const face = this.faceAt(t);
|
|
489
|
+
const reaction = this.reactionAt(t);
|
|
490
|
+
const bodyEvent = latest(this.bodies, t);
|
|
491
|
+
const body = reaction.body ?? bodyEvent?.body ?? "idle";
|
|
492
|
+
const cycle = bodyEvent ? (t - bodyEvent.t) / bodyEvent.cycleMs % 1 : 0;
|
|
493
|
+
const breath = this.idle && !this.reducedMotion ? breathAt(this.seed, t) : {
|
|
494
|
+
bob: 0,
|
|
495
|
+
sway: 0
|
|
496
|
+
};
|
|
497
|
+
const status = latest(this.statuses, t)?.status ?? null;
|
|
498
|
+
return {
|
|
499
|
+
face,
|
|
500
|
+
look: this.gazeAt(t),
|
|
501
|
+
blink: blinkAt(this.seed, t),
|
|
502
|
+
talk: this.talkAt(t),
|
|
503
|
+
body,
|
|
504
|
+
phase: reaction.body === "cheer" ? t / 467 % 1 : cycle,
|
|
505
|
+
status,
|
|
506
|
+
bob: {
|
|
507
|
+
x: reaction.frame.bob.x,
|
|
508
|
+
y: reaction.frame.bob.y + breath.bob
|
|
509
|
+
},
|
|
510
|
+
tilt: reaction.frame.tilt + breath.sway,
|
|
511
|
+
squash: reaction.frame.squash
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
/** A new animator for a look: `animator(look, { seed: userId })`. */
|
|
516
|
+
function animator(look, opts = {}) {
|
|
517
|
+
return new Animator(look, opts);
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/avatar/layout.ts
|
|
521
|
+
/** Anchors, facings from movement, and the look-at helper (§3.2, §3.3, §3.6). */
|
|
522
|
+
/**
|
|
523
|
+
* The rig anchors for a look in a facing and pose: place name plates,
|
|
524
|
+
* bubbles, props and world objects with these, never with magic numbers.
|
|
525
|
+
* Rig units (a 100-unit head), feet at (0, 0), y down.
|
|
526
|
+
*/
|
|
527
|
+
function layout(like, opts = {}) {
|
|
528
|
+
return composeAvatar(toLook(like), {
|
|
529
|
+
frame: "full",
|
|
530
|
+
...opts
|
|
531
|
+
}).anchors;
|
|
532
|
+
}
|
|
533
|
+
const EIGHT = [
|
|
534
|
+
"E",
|
|
535
|
+
"SE",
|
|
536
|
+
"S",
|
|
537
|
+
"SW",
|
|
538
|
+
"W",
|
|
539
|
+
"NW",
|
|
540
|
+
"N",
|
|
541
|
+
"NE"
|
|
542
|
+
];
|
|
543
|
+
const FOUR = [
|
|
544
|
+
"E",
|
|
545
|
+
"S",
|
|
546
|
+
"W",
|
|
547
|
+
"N"
|
|
548
|
+
];
|
|
549
|
+
/** The facing for a movement vector (screen space, y down), in 8 or 4 directions. */
|
|
550
|
+
function facingFrom(dx, dy, dirs = 8) {
|
|
551
|
+
if (dx === 0 && dy === 0) return "S";
|
|
552
|
+
const angle = Math.atan2(dy, dx);
|
|
553
|
+
const list = dirs === 8 ? EIGHT : FOUR;
|
|
554
|
+
const step = Math.PI * 2 / list.length;
|
|
555
|
+
return list[(Math.round(angle / step) % list.length + list.length) % list.length] ?? "S";
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* A look direction from one screen point toward another, for card tables
|
|
559
|
+
* where each seat looks at the active player. `strength` scales it.
|
|
560
|
+
*/
|
|
561
|
+
function lookToward(from, to, strength = 1) {
|
|
562
|
+
const dx = to.x - from.x;
|
|
563
|
+
const dy = to.y - from.y;
|
|
564
|
+
const len = Math.hypot(dx, dy);
|
|
565
|
+
if (len < 1e-6) return {
|
|
566
|
+
x: 0,
|
|
567
|
+
y: 0
|
|
568
|
+
};
|
|
569
|
+
return {
|
|
570
|
+
x: dx / len * strength,
|
|
571
|
+
y: dy / len * strength * .7
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/avatar/overlays.ts
|
|
576
|
+
/**
|
|
577
|
+
* Overlays drawn above an avatar (§3.11, §14.3), as scene nodes in screen
|
|
578
|
+
* pixels with the origin at the anchor they sit on (`nameplate`, `bubble`,
|
|
579
|
+
* `shadow`). Every surface draws them the same way. Text is a username or a
|
|
580
|
+
* catalog title, never player-typed bubble text, and is escaped on output.
|
|
581
|
+
*/
|
|
582
|
+
const INK = "#0d0f14";
|
|
583
|
+
const PAPER = "#e8e6df";
|
|
584
|
+
const MONO_ADVANCE = .6;
|
|
585
|
+
const textWidth = (text, size) => Array.from(text).length * size * MONO_ADVANCE;
|
|
586
|
+
/** A name tag: the username in a pill; friends get a green dot and a ★ (never colour alone), you get an accent outline. */
|
|
587
|
+
function nameTag(o) {
|
|
588
|
+
const size = 12;
|
|
589
|
+
const lead = o.friend ? 22 : 0;
|
|
590
|
+
const w = textWidth(o.name, size) + 16 + lead;
|
|
591
|
+
const h = 19;
|
|
592
|
+
const nodes = [rect(-w / 2, -19, w, h, {
|
|
593
|
+
fill: INK,
|
|
594
|
+
fillOpacity: .86,
|
|
595
|
+
stroke: o.you ? "#ff5f3b" : "#2a3142",
|
|
596
|
+
sw: o.you ? 2 : 1.2
|
|
597
|
+
}, h / 2)];
|
|
598
|
+
if (o.friend) nodes.push(ellipse(-w / 2 + 10, -19 / 2, 3.6, 3.6, { fill: "#4ade80" }), {
|
|
599
|
+
k: "text",
|
|
600
|
+
x: -w / 2 + 19,
|
|
601
|
+
y: -5,
|
|
602
|
+
text: "★",
|
|
603
|
+
size: 11,
|
|
604
|
+
font: "mono",
|
|
605
|
+
anchor: "middle",
|
|
606
|
+
fill: "#ffd23f"
|
|
607
|
+
});
|
|
608
|
+
nodes.push({
|
|
609
|
+
k: "text",
|
|
610
|
+
x: lead / 2,
|
|
611
|
+
y: -5.5,
|
|
612
|
+
text: o.name,
|
|
613
|
+
size,
|
|
614
|
+
font: "mono",
|
|
615
|
+
bold: true,
|
|
616
|
+
anchor: "middle",
|
|
617
|
+
fill: o.muted ? "#6f7787" : PAPER
|
|
618
|
+
});
|
|
619
|
+
return {
|
|
620
|
+
nodes,
|
|
621
|
+
width: w,
|
|
622
|
+
height: h,
|
|
623
|
+
box: [
|
|
624
|
+
-w / 2,
|
|
625
|
+
-19,
|
|
626
|
+
w,
|
|
627
|
+
h
|
|
628
|
+
]
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function icon(kind) {
|
|
632
|
+
switch (kind) {
|
|
633
|
+
case "cards": return g([rect(-6, -8, 12, 16, {
|
|
634
|
+
fill: "#ff5f3b",
|
|
635
|
+
stroke: INK,
|
|
636
|
+
sw: 1.6
|
|
637
|
+
}, 2.5), g([rect(-1, -11, 12, 16, {
|
|
638
|
+
fill: "#3d7bff",
|
|
639
|
+
stroke: INK,
|
|
640
|
+
sw: 1.6
|
|
641
|
+
}, 2.5)], { tf: [rot(18)] })], { tf: [rot(-10)] });
|
|
642
|
+
case "controller": return path("M-9 -2C-9 -7 9 -7 9 -2C11 6 6 8 3 4L-3 4C-6 8 -11 6 -9 -2Z", {
|
|
643
|
+
fill: "#2a3142",
|
|
644
|
+
stroke: INK,
|
|
645
|
+
sw: 1.6
|
|
646
|
+
});
|
|
647
|
+
case "puzzle": return path("M-7 -7H-1C-1 -11 4 -11 4 -7H7V-1C11 -1 11 4 7 4V7H-7Z", {
|
|
648
|
+
fill: "#4ade80",
|
|
649
|
+
stroke: INK,
|
|
650
|
+
sw: 1.6
|
|
651
|
+
});
|
|
652
|
+
case "race": return path("M-7 -8V8M-7 -8H7L4 -4L7 0H-7", {
|
|
653
|
+
fill: "#ffd23f",
|
|
654
|
+
stroke: INK,
|
|
655
|
+
sw: 1.6
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/** "Playing Uno": the game's catalog title in a light bubble with a tail. */
|
|
660
|
+
function activityBubble(o) {
|
|
661
|
+
const text = `${o.verb ?? "Playing"} ${o.game}`;
|
|
662
|
+
const w = textWidth(text, 12) + 40;
|
|
663
|
+
return {
|
|
664
|
+
nodes: [
|
|
665
|
+
rect(-w / 2, -33, w, 26, {
|
|
666
|
+
fill: PAPER,
|
|
667
|
+
stroke: INK,
|
|
668
|
+
sw: 2
|
|
669
|
+
}, 8),
|
|
670
|
+
path("M-6 -7.5L0 0L6 -7.5Z", {
|
|
671
|
+
fill: PAPER,
|
|
672
|
+
stroke: INK,
|
|
673
|
+
sw: 2
|
|
674
|
+
}),
|
|
675
|
+
rect(-7, -10, 14, 4, { fill: PAPER }),
|
|
676
|
+
g([icon(o.icon ?? "cards")], { tf: [tr(-w / 2 + 16, -20)] }),
|
|
677
|
+
{
|
|
678
|
+
k: "text",
|
|
679
|
+
x: 10,
|
|
680
|
+
y: -15.5,
|
|
681
|
+
text,
|
|
682
|
+
size: 12,
|
|
683
|
+
font: "mono",
|
|
684
|
+
bold: true,
|
|
685
|
+
anchor: "middle",
|
|
686
|
+
fill: INK
|
|
687
|
+
}
|
|
688
|
+
],
|
|
689
|
+
width: w,
|
|
690
|
+
height: 33,
|
|
691
|
+
box: [
|
|
692
|
+
-w / 2,
|
|
693
|
+
-33,
|
|
694
|
+
w,
|
|
695
|
+
33
|
|
696
|
+
]
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
/** An emote popping above the head (the hub's quick-chat emotes). */
|
|
700
|
+
function emoteBubble(emote) {
|
|
701
|
+
const glyph = emote.startsWith("e:") ? emote.slice(2) : emote;
|
|
702
|
+
return {
|
|
703
|
+
nodes: [
|
|
704
|
+
ellipse(0, -24, 20, 17, {
|
|
705
|
+
fill: PAPER,
|
|
706
|
+
stroke: INK,
|
|
707
|
+
sw: 2
|
|
708
|
+
}),
|
|
709
|
+
path("M-5 -9L0 0L5 -9Z", {
|
|
710
|
+
fill: PAPER,
|
|
711
|
+
stroke: INK,
|
|
712
|
+
sw: 2
|
|
713
|
+
}),
|
|
714
|
+
ellipse(0, -10, 5, 2.4, { fill: PAPER }),
|
|
715
|
+
{
|
|
716
|
+
k: "text",
|
|
717
|
+
x: 0,
|
|
718
|
+
y: -17,
|
|
719
|
+
text: glyph,
|
|
720
|
+
size: 20,
|
|
721
|
+
font: "mono",
|
|
722
|
+
anchor: "middle",
|
|
723
|
+
fill: INK
|
|
724
|
+
}
|
|
725
|
+
],
|
|
726
|
+
width: 40,
|
|
727
|
+
height: 42,
|
|
728
|
+
box: [
|
|
729
|
+
-21,
|
|
730
|
+
-42,
|
|
731
|
+
42,
|
|
732
|
+
42
|
|
733
|
+
]
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
/** A reaction glyph in a small bubble. */
|
|
737
|
+
function reactionBubble(glyph) {
|
|
738
|
+
const colour = glyph === "♥" ? "#ff5f8f" : glyph === "!" ? "#ff5f3b" : INK;
|
|
739
|
+
return {
|
|
740
|
+
nodes: [
|
|
741
|
+
rect(-15, -30, 30, 24, {
|
|
742
|
+
fill: PAPER,
|
|
743
|
+
stroke: INK,
|
|
744
|
+
sw: 2
|
|
745
|
+
}, 9),
|
|
746
|
+
path("M-4 -6.5L0 0L4 -6.5Z", {
|
|
747
|
+
fill: PAPER,
|
|
748
|
+
stroke: INK,
|
|
749
|
+
sw: 2
|
|
750
|
+
}),
|
|
751
|
+
rect(-5, -9, 10, 4, { fill: PAPER }),
|
|
752
|
+
{
|
|
753
|
+
k: "text",
|
|
754
|
+
x: 0,
|
|
755
|
+
y: -12,
|
|
756
|
+
text: glyph,
|
|
757
|
+
size: glyph === "zzz" ? 12 : 17,
|
|
758
|
+
font: "mono",
|
|
759
|
+
bold: true,
|
|
760
|
+
anchor: "middle",
|
|
761
|
+
fill: colour
|
|
762
|
+
}
|
|
763
|
+
],
|
|
764
|
+
width: 30,
|
|
765
|
+
height: 30,
|
|
766
|
+
box: [
|
|
767
|
+
-16,
|
|
768
|
+
-31,
|
|
769
|
+
32,
|
|
770
|
+
31
|
|
771
|
+
]
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
/** The hover or selection ring at the feet (`radius` in px). */
|
|
775
|
+
function selectionRing(o = {}) {
|
|
776
|
+
const r = o.radius ?? 18;
|
|
777
|
+
return {
|
|
778
|
+
nodes: [ellipse(0, 0, r, r * .3, line(o.selected ? "#ff5f3b" : "#e8e6df", o.selected ? 2.4 : 1.6, "round", .85))],
|
|
779
|
+
width: r * 2,
|
|
780
|
+
height: r * .6,
|
|
781
|
+
box: [
|
|
782
|
+
-r - 2,
|
|
783
|
+
-r * .3 - 2,
|
|
784
|
+
r * 2 + 4,
|
|
785
|
+
r * .6 + 4
|
|
786
|
+
]
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/** Every overlay builder, as the `overlays` namespace. */
|
|
790
|
+
const overlays = {
|
|
791
|
+
nameTag,
|
|
792
|
+
activityBubble,
|
|
793
|
+
emoteBubble,
|
|
794
|
+
reactionBubble,
|
|
795
|
+
selectionRing
|
|
796
|
+
};
|
|
797
|
+
//#endregion
|
|
798
|
+
export { Animator as a, drawToCanvas as c, lookToward as i, renderBitmap as l, facingFrom as n, animator as o, layout as r, REACTIONS as s, overlays as t, renderDataUrl as u };
|
|
799
|
+
|
|
800
|
+
//# sourceMappingURL=overlays.js.map
|