@fabi-play/sdk 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/dist/avatars.d.ts +26 -14
- package/dist/avatars.js +1 -1
- package/dist/chunks/{chunk-GFNOIOSX.js → chunk-H3BKYVPL.js} +301 -22
- package/dist/fabi-play.d.ts +193 -10
- package/dist/fabi-play.js +1 -1
- package/dist/fabi-play.schema.json +32 -3
- package/dist/fabi-play.umd.js +453 -26
- package/dist/fabi-play.umd.min.js +1 -1
- package/dist/index.d.ts +212 -11
- package/dist/index.js +166 -4
- package/dist/integrity.json +6 -6
- package/package.json +3 -3
package/dist/fabi-play.umd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fabi-play/sdk 0.
|
|
1
|
+
/*! @fabi-play/sdk 0.5.0 | Fabi Play Kit License | https://docs.fabiplay.app */
|
|
2
2
|
(function (root, factory) {
|
|
3
3
|
var script = typeof document !== 'undefined' ? document.currentScript : null;
|
|
4
4
|
if (typeof define === 'function' && define.amd) define([], function () { return factory(script); });
|
|
@@ -229,6 +229,88 @@ function pointer(play, options = {}) {
|
|
|
229
229
|
target.cursor(x, y, clicked ? 0 : progress);
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
|
+
function domCursor(doc = document) {
|
|
233
|
+
const win = doc.defaultView;
|
|
234
|
+
const Pointer = win?.PointerEvent;
|
|
235
|
+
const Mouse = win?.MouseEvent ?? MouseEvent;
|
|
236
|
+
let over = null;
|
|
237
|
+
let pressed = null;
|
|
238
|
+
const fire = (el, type, x, y, buttons, related = null) => {
|
|
239
|
+
const init = { bubbles: true, cancelable: true, composed: true, view: win, clientX: x, clientY: y, screenX: x, screenY: y, button: 0, buttons, relatedTarget: related, pointerId: 1, pointerType: "mouse", isPrimary: true };
|
|
240
|
+
if (type.startsWith("pointer")) {
|
|
241
|
+
if (Pointer) el.dispatchEvent(new Pointer(type, init));
|
|
242
|
+
} else el.dispatchEvent(new Mouse(type, init));
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
// The viewport (the game's frame), not the document, which may be taller.
|
|
246
|
+
size: () => ({ width: win?.innerWidth ?? doc.documentElement.clientWidth, height: win?.innerHeight ?? doc.documentElement.clientHeight }),
|
|
247
|
+
dispatch(type, x, y, buttons) {
|
|
248
|
+
const el = doc.elementFromPoint(x, y) ?? doc.body ?? doc.documentElement;
|
|
249
|
+
if (type === "pointermove" && el !== over) {
|
|
250
|
+
if (over?.isConnected) for (const t of ["pointerout", "mouseout"]) fire(over, t, x, y, buttons, el);
|
|
251
|
+
for (const t of ["pointerover", "mouseover"]) fire(el, t, x, y, buttons, over);
|
|
252
|
+
over = el;
|
|
253
|
+
}
|
|
254
|
+
if (type === "pointerdown") pressed = el;
|
|
255
|
+
if (type === "click") {
|
|
256
|
+
const same = pressed === el;
|
|
257
|
+
pressed = null;
|
|
258
|
+
if (!same) return;
|
|
259
|
+
}
|
|
260
|
+
fire(el, type, x, y, buttons);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function cursorDriver(target) {
|
|
265
|
+
let last = null;
|
|
266
|
+
const send = (types, x, y, buttons) => {
|
|
267
|
+
const { width, height } = target.size();
|
|
268
|
+
for (const t of types) target.dispatch(t, x * width, y * height, buttons);
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
update(c) {
|
|
272
|
+
const was = last?.down ?? false;
|
|
273
|
+
if (!last || last.x !== c.x || last.y !== c.y) send(["pointermove", "mousemove"], c.x, c.y, was ? 1 : 0);
|
|
274
|
+
if (c.down && !was) send(["pointerdown", "mousedown"], c.x, c.y, 1);
|
|
275
|
+
if (!c.down && was) send(["pointerup", "mouseup", "click"], c.x, c.y, 0);
|
|
276
|
+
last = { x: c.x, y: c.y, down: c.down };
|
|
277
|
+
},
|
|
278
|
+
cancel() {
|
|
279
|
+
if (!last?.down) return;
|
|
280
|
+
send(["pointercancel", "mouseup"], last.x, last.y, 0);
|
|
281
|
+
last = { ...last, down: false };
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ../protocol/src/picture.ts
|
|
287
|
+
var MAX_PICTURE_BYTES = 3 * 1024 * 1024;
|
|
288
|
+
var MAX_PICTURE_SIDE = 1920;
|
|
289
|
+
var PICTURE_JPEG_QUALITY = 0.85;
|
|
290
|
+
var MAX_PICTURE_TITLE = 60;
|
|
291
|
+
var PICTURE_RESULTS = ["sent", "unavailable", "too-big", "error"];
|
|
292
|
+
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
293
|
+
var MAGIC = { "image/jpeg": "/9j/", "image/png": "iVBORw0KGg" };
|
|
294
|
+
var PREFIX = /^data:(image\/(?:jpeg|png));base64,/;
|
|
295
|
+
var decodedSize = (b64) => b64.length / 4 * 3 - (b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0);
|
|
296
|
+
var MAX_BASE64 = Math.ceil(MAX_PICTURE_BYTES / 3) * 4;
|
|
297
|
+
var isImage = (mime, b64) => b64.length % 4 === 0 && b64.length > 0 && b64.startsWith(MAGIC[mime]) && BASE64.test(b64);
|
|
298
|
+
function checkPicture(v) {
|
|
299
|
+
if (typeof v !== "string") return { error: "invalid" };
|
|
300
|
+
const m = PREFIX.exec(v);
|
|
301
|
+
if (!m) return { error: "invalid" };
|
|
302
|
+
const mime = m[1];
|
|
303
|
+
const base64 = v.slice(m[0].length);
|
|
304
|
+
if (base64.length > MAX_BASE64) return { error: "too-big" };
|
|
305
|
+
if (!isImage(mime, base64)) return { error: "invalid" };
|
|
306
|
+
const size = decodedSize(base64);
|
|
307
|
+
return size > MAX_PICTURE_BYTES ? { error: "too-big" } : { ok: { mime, base64, size } };
|
|
308
|
+
}
|
|
309
|
+
function parsePictureTitle(v) {
|
|
310
|
+
if (typeof v !== "string") return void 0;
|
|
311
|
+
const t = Array.from(v.replace(/[\p{Cc}\p{Cf}]/gu, "").trim()).slice(0, MAX_PICTURE_TITLE).join("").trim();
|
|
312
|
+
return t || void 0;
|
|
313
|
+
}
|
|
232
314
|
|
|
233
315
|
// ../protocol/src/index.ts
|
|
234
316
|
var PROTOCOL_VERSION = 1;
|
|
@@ -285,6 +367,15 @@ function parseGameStats(v) {
|
|
|
285
367
|
if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
|
|
286
368
|
return out;
|
|
287
369
|
}
|
|
370
|
+
function parseCursor(v) {
|
|
371
|
+
if (!isObject(v) || !isNumber(v.x) || !isNumber(v.y) || typeof v.down !== "boolean") return void 0;
|
|
372
|
+
const c = (n) => Math.min(Math.max(n, 0), 1);
|
|
373
|
+
return { type: "cursor", x: c(v.x), y: c(v.y), down: v.down };
|
|
374
|
+
}
|
|
375
|
+
function parsePictureResult(v) {
|
|
376
|
+
if (!isObject(v) || !PICTURE_RESULTS.includes(v.result)) return void 0;
|
|
377
|
+
return { type: "picture-result", ...Number.isInteger(v.id) ? { id: v.id } : {}, result: v.result };
|
|
378
|
+
}
|
|
288
379
|
var isPlayerId = (id) => Number.isInteger(id) && id >= 1 && id <= MAX_PLAYERS;
|
|
289
380
|
function parseTurnPlayers(v) {
|
|
290
381
|
return Array.isArray(v) && v.length === 1 && isPlayerId(v[0]) ? [v[0]] : void 0;
|
|
@@ -293,6 +384,7 @@ function parseTurnOrder(v) {
|
|
|
293
384
|
if (!Array.isArray(v) || v.length < 1 || v.length > MAX_PLAYERS || !v.every(isPlayerId) || new Set(v).size !== v.length) return void 0;
|
|
294
385
|
return [...v];
|
|
295
386
|
}
|
|
387
|
+
var COVER_ASPECT = 16 / 9;
|
|
296
388
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
297
389
|
var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
298
390
|
var POSE_HEADER = 12;
|
|
@@ -415,6 +507,7 @@ var AVATARS = [
|
|
|
415
507
|
id: "lion",
|
|
416
508
|
name: { en: "Lion", it: "Leoncino" },
|
|
417
509
|
band: "6-8",
|
|
510
|
+
alsoIn: ["3-5"],
|
|
418
511
|
palette: { body: "#f6c454", limbs: "#f6c454", belly: "#fde7b0", accent: "#fde7b0", outline: INK },
|
|
419
512
|
head: "round",
|
|
420
513
|
ears: "round",
|
|
@@ -781,6 +874,39 @@ var AVATARS = [
|
|
|
781
874
|
headExtras: ["hair", "domino"],
|
|
782
875
|
bodyExtras: ["cape", "emblem", "boots"],
|
|
783
876
|
extraColors: ["#ffd84a"]
|
|
877
|
+
},
|
|
878
|
+
// ── In both 3–5 and 6–8 (home 6–8, alsoIn 3–5: drawn with the proportions of the band shown) ──
|
|
879
|
+
{
|
|
880
|
+
// Next to the round 'dinosaur': a T-rex at a glance, a big head turned three-quarters with a long snout and a
|
|
881
|
+
// toothy grin, tiny arms, strong legs, a thick striped tail.
|
|
882
|
+
id: "trex",
|
|
883
|
+
name: { en: "T-Rex", it: "T-Rex" },
|
|
884
|
+
band: "6-8",
|
|
885
|
+
alsoIn: ["3-5"],
|
|
886
|
+
palette: { body: "#6cb84e", limbs: "#6cb84e", belly: "#f4e8a8", accent: "#f4e8a8", outline: INK },
|
|
887
|
+
head: "snout-long",
|
|
888
|
+
ears: "none",
|
|
889
|
+
face: { eyes: "dot", mouth: "toothy", muzzle: "none", cheeks: "#f59b8f" },
|
|
890
|
+
tail: "thick",
|
|
891
|
+
headExtras: ["stripes"],
|
|
892
|
+
bodyExtras: ["tiny-arms", "strong-legs"],
|
|
893
|
+
extraColors: ["#4a9235", "#58a23f"]
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
// An original child fairy: a bun with a flower crown, little pointed ears, a petal tunic, see-through wings and
|
|
897
|
+
// a star wand in one hand.
|
|
898
|
+
id: "fairy",
|
|
899
|
+
name: { en: "Fairy", it: "Fatina" },
|
|
900
|
+
band: "6-8",
|
|
901
|
+
alsoIn: ["3-5"],
|
|
902
|
+
palette: { body: "#b99af0", limbs: "#a8693f", belly: "#b99af0", accent: "#ffd84a", outline: INK, skin: "#a8693f", hair: "#2e1d17", hands: "#a8693f", feet: "#f58cbc" },
|
|
903
|
+
head: "round",
|
|
904
|
+
ears: "pixie",
|
|
905
|
+
face: { eyes: "dot", mouth: "smile", muzzle: "none", cheeks: "#f0857a" },
|
|
906
|
+
tail: "none",
|
|
907
|
+
headExtras: ["bun", "flower-crown"],
|
|
908
|
+
bodyExtras: ["wings-fairy", "petals", "wand"],
|
|
909
|
+
extraColors: ["#c4ecff", "#f7a8cf", "#fff3a0", "#ffffff"]
|
|
784
910
|
}
|
|
785
911
|
];
|
|
786
912
|
|
|
@@ -1017,6 +1143,7 @@ function headShapes(def, ow) {
|
|
|
1017
1143
|
if (has("locks")) {
|
|
1018
1144
|
out.push({ k: "path", d: smoothClosed(pts(-1.14, 0.58, -1.24, -0.2, -0.86, -0.98, 0, -1.2, 0.86, -0.98, 1.24, -0.2, 1.14, 0.58, 0.8, 0.66, 0, 0.2, -0.8, 0.66)), fill: p.hair ?? p.outline, ...ol });
|
|
1019
1145
|
}
|
|
1146
|
+
if (has("bun")) out.push({ k: "circle", x: 0.04, y: -1.14, r: 0.4, fill: p.hair ?? p.outline, ...ol });
|
|
1020
1147
|
if (has("ninja-hood")) {
|
|
1021
1148
|
for (const [rot, len2] of [
|
|
1022
1149
|
[0.35, 0.5],
|
|
@@ -1054,7 +1181,15 @@ function headShapes(def, ow) {
|
|
|
1054
1181
|
if (muzzle === "disc") for (const s of SIDES) out.push({ k: "circle", x: s * 0.4, y: 0.02, r: 0.44, fill: mc });
|
|
1055
1182
|
if (muzzle === "cheeks") for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.4, y: 0.44, rx: 0.44, ry: 0.32, fill: mc });
|
|
1056
1183
|
if (muzzle === "band") out.push({ k: "path", d: lowerFace(def, ow), fill: mc });
|
|
1057
|
-
if (has("stripes")) {
|
|
1184
|
+
if (has("stripes") && def.head === "snout-long") {
|
|
1185
|
+
const c = extra(0, p.outline);
|
|
1186
|
+
for (const [x0, y0, x1, y1] of [
|
|
1187
|
+
[0.46, -0.94, 0.36, -0.66],
|
|
1188
|
+
[0.8, -0.72, 0.62, -0.5],
|
|
1189
|
+
[1, -0.36, 0.78, -0.24]
|
|
1190
|
+
])
|
|
1191
|
+
out.push({ k: "line", pts: [x0, y0, x1, y1], stroke: c, lw: 0.11 });
|
|
1192
|
+
} else if (has("stripes")) {
|
|
1058
1193
|
const c = extra(0, p.outline);
|
|
1059
1194
|
out.push({ k: "line", pts: [0, -0.97, 0, -0.66], stroke: c, lw: 0.13 });
|
|
1060
1195
|
for (const s of SIDES) {
|
|
@@ -1069,6 +1204,13 @@ function headShapes(def, ow) {
|
|
|
1069
1204
|
out.push({ k: "circle", x: 0.58, y: -0.42, r: 0.11, fill: c });
|
|
1070
1205
|
out.push({ k: "circle", x: 0.22, y: -0.76, r: 0.09, fill: c });
|
|
1071
1206
|
}
|
|
1207
|
+
if (def.head === "snout-long") {
|
|
1208
|
+
for (const [x, y, r] of [
|
|
1209
|
+
[-1.38, -0.14, 0.06],
|
|
1210
|
+
[-1.14, -0.22, 0.055]
|
|
1211
|
+
])
|
|
1212
|
+
out.push({ k: "ellipse", x, y, rx: r, ry: r * 0.8, rot: -0.3, fill: p.outline, alpha: 0.85 });
|
|
1213
|
+
}
|
|
1072
1214
|
if (has("forehead")) out.push({ k: "path", d: smoothClosed(pts(-0.62, -0.66, 0, -0.86, 0.62, -0.66, 0.26, -0.36, 0, -0.1, -0.26, -0.36), 0.9), fill: extra(0, p.accent) });
|
|
1073
1215
|
if (has("eye-spot")) out.push({ k: "ellipse", x: 0.42, y: -0.06, rx: 0.3, ry: 0.33, rot: 0.3, fill: p.ears ?? p.accent });
|
|
1074
1216
|
if (has("gills")) {
|
|
@@ -1077,6 +1219,7 @@ function headShapes(def, ow) {
|
|
|
1077
1219
|
const hair = p.hair ?? p.outline;
|
|
1078
1220
|
if (has("hair")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.06, -1.02, -0.58, -0.62, -1.04, 0.08, -1.14, 0.76, -0.92, 1.05, -0.42, 1, -0.06, 0.84, -0.4, 0.42, -0.5, 0.06, -0.4, -0.34, -0.56, -0.74, -0.38)), fill: hair, ...ol });
|
|
1079
1221
|
if (has("locks")) out.push({ k: "path", d: smoothClosed(pts(-1, -0.08, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1, -0.08, 0.8, -0.38, 0.36, -0.44, 0, -0.4, -0.36, -0.44, -0.8, -0.38)), fill: hair, ...ol });
|
|
1222
|
+
if (has("bun")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.1, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1.02, -0.1, 0.84, -0.36, 0.5, -0.56, 0.12, -0.48, -0.22, -0.6, -0.6, -0.5, -0.86, -0.36)), fill: hair, ...ol });
|
|
1080
1223
|
if (has("curls")) {
|
|
1081
1224
|
for (let i = 0; i < 7; i++) {
|
|
1082
1225
|
const c = polar(-1.3 + i / 6 * 2.6, 0.86);
|
|
@@ -1098,7 +1241,7 @@ function headShapes(def, ow) {
|
|
|
1098
1241
|
}
|
|
1099
1242
|
if (def.face.brows) for (const s of SIDES) out.push({ k: "line", pts: [s * 0.2, -0.2, s * 0.54, -0.28], stroke: def.face.brows, lw: 0.09 });
|
|
1100
1243
|
out.push(...mouth(def, ow));
|
|
1101
|
-
if (def.face.cheeks) for (const
|
|
1244
|
+
if (def.face.cheeks) for (const c of cheekSpots(def)) out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.15, ry: 0.1, fill: def.face.cheeks, alpha: 0.8 });
|
|
1102
1245
|
if (has("whiskers")) {
|
|
1103
1246
|
for (const s of SIDES)
|
|
1104
1247
|
for (const dy of [-0.1, 0.06]) out.push({ k: "line", pts: [s * 0.62, 0.38 + dy, s * 1.06, 0.3 + dy * 2.2], stroke: p.outline, lw: ow * 0.7, alpha: 0.75 });
|
|
@@ -1120,6 +1263,24 @@ function headShapes(def, ow) {
|
|
|
1120
1263
|
out.push({ k: "line", pts: [-w, y + 0.04, w, y - 0.04], stroke: p.outline, lw: ow * 0.7, alpha: 0.6 });
|
|
1121
1264
|
}
|
|
1122
1265
|
}
|
|
1266
|
+
if (has("flower-crown")) {
|
|
1267
|
+
const cols = [extra(1, "#f7a8cf"), extra(2, "#fff3a0"), extra(3, WHITE)];
|
|
1268
|
+
for (const a of [-0.84, -0.28, 0.28, 0.84]) {
|
|
1269
|
+
const c = polar(a, 0.9);
|
|
1270
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.14, ry: 0.07, rot: a + 0.5, fill: "#7cc76a", stroke: p.outline, lw: ow * 0.7 });
|
|
1271
|
+
}
|
|
1272
|
+
;
|
|
1273
|
+
[-1.12, -0.56, 0, 0.56, 1.12].forEach((a, i) => {
|
|
1274
|
+
const c = polar(a, 0.9);
|
|
1275
|
+
const r = i === 2 ? 0.13 : 0.11;
|
|
1276
|
+
for (let k = 0; k < 5; k++) {
|
|
1277
|
+
const t = a + k / 5 * Math.PI * 2;
|
|
1278
|
+
out.push({ k: "circle", x: c.x + Math.sin(t) * r, y: c.y - Math.cos(t) * r, r: r * 0.82, fill: cols[i % cols.length], stroke: p.outline, lw: ow * 0.7 });
|
|
1279
|
+
}
|
|
1280
|
+
out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.62, fill: cols[i % cols.length] });
|
|
1281
|
+
out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.42, fill: p.accent });
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1123
1284
|
if (has("helmet")) {
|
|
1124
1285
|
for (const s of SIDES) out.push({ k: "rect", x: s * 1, y: 0.05, w: 0.26, h: 0.56, r: 0.12, fill: p.accent, ...ol });
|
|
1125
1286
|
out.push({ k: "path", d: "M-0.62 -0.62 Q-0.3 -0.92 0.12 -0.9", stroke: WHITE, lw: 0.1, alpha: 0.9 });
|
|
@@ -1184,6 +1345,8 @@ function headShape(def, ol) {
|
|
|
1184
1345
|
return { k: "rect", x: 0, y: 0, w: 2, h: 1.8, r: 0.46, fill, ...ol };
|
|
1185
1346
|
case "bucket":
|
|
1186
1347
|
return { k: "path", d: roundedPolygon(pts(-0.94, 0.96, -0.98, -0.36, -0.62, -1.04, 0.62, -1.04, 0.98, -0.36, 0.94, 0.96), 0.42), fill, ...ol };
|
|
1348
|
+
case "snout-long":
|
|
1349
|
+
return { k: "path", d: smoothClosed(SNOUT_HEAD, 0.95), fill, ...ol };
|
|
1187
1350
|
case "bulb":
|
|
1188
1351
|
return {
|
|
1189
1352
|
k: "path",
|
|
@@ -1202,6 +1365,19 @@ function headShape(def, ol) {
|
|
|
1202
1365
|
};
|
|
1203
1366
|
}
|
|
1204
1367
|
}
|
|
1368
|
+
var SNOUT_HEAD = pts(0.7, -0.88, 0.12, -1.04, -0.3, -0.98, -0.72, -0.74, -1.2, -0.56, -1.5, -0.32, -1.6, 0.04, -1.44, 0.42, -0.9, 0.72, -0.2, 0.92, 0.5, 0.88, 0.96, 0.56, 1.08, 0, 0.98, -0.5);
|
|
1369
|
+
function eyeSpots(def, ex, ey) {
|
|
1370
|
+
if (def.head === "snout-long")
|
|
1371
|
+
return [
|
|
1372
|
+
{ x: -0.36, y: -0.5, k: 0.95 },
|
|
1373
|
+
{ x: 0.26, y: -0.48, k: 1.1 }
|
|
1374
|
+
];
|
|
1375
|
+
return SIDES.map((s) => ({ x: s * ex, y: ey, k: 1 }));
|
|
1376
|
+
}
|
|
1377
|
+
function cheekSpots(def) {
|
|
1378
|
+
if (def.head === "snout-long") return pts(-0.66, -0.14, 0.66, 0.1);
|
|
1379
|
+
return SIDES.map((s) => ({ x: s * 0.64, y: 0.3 }));
|
|
1380
|
+
}
|
|
1205
1381
|
function headEllipse(def) {
|
|
1206
1382
|
if (def.head === "round") return { cy: 0, rx: 1, ry: 1 };
|
|
1207
1383
|
if (def.head === "tall") return { cy: -0.02, rx: 0.94, ry: 1.04 };
|
|
@@ -1282,6 +1458,10 @@ function ears(def, ow) {
|
|
|
1282
1458
|
out.push({ k: "ellipse", x: s * 1.04, y: -0.22, rx: 0.4, ry: 0.18, rot: s * 0.32, fill: p.skin ?? p.body, ...ol });
|
|
1283
1459
|
out.push({ k: "ellipse", x: s * 1.08, y: -0.2, rx: 0.24, ry: 0.08, rot: s * 0.32, fill: p.accent });
|
|
1284
1460
|
break;
|
|
1461
|
+
case "pixie":
|
|
1462
|
+
out.push({ k: "path", d: roundedPolygon(pts(s * 0.86, -0.22, s * 1.36, -0.5, s * 1.02, 0.26), 0.28), fill: p.skin ?? p.body, ...ol });
|
|
1463
|
+
out.push({ k: "line", pts: [s * 0.98, -0.12, s * 1.16, -0.3], stroke: p.accent, lw: ow * 0.8, alpha: 0.5 });
|
|
1464
|
+
break;
|
|
1285
1465
|
case "human":
|
|
1286
1466
|
out.push({ k: "ellipse", x: s * 0.98, y: 0.06, rx: 0.17, ry: 0.23, fill: p.skin ?? p.body, ...ol });
|
|
1287
1467
|
break;
|
|
@@ -1314,15 +1494,15 @@ function eyes(def, ow) {
|
|
|
1314
1494
|
const shine = (x, y, r) => ({ k: "circle", x, y, r, fill: WHITE });
|
|
1315
1495
|
switch (f2.eyes) {
|
|
1316
1496
|
case "dot":
|
|
1317
|
-
for (const
|
|
1318
|
-
out.push({ k: "ellipse", x:
|
|
1319
|
-
out.push(shine(
|
|
1497
|
+
for (const e of eyeSpots(def, ex, ey)) {
|
|
1498
|
+
out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.12 * e.k, ry: 0.155 * e.k, fill: ink });
|
|
1499
|
+
out.push(shine(e.x + 0.04 * e.k, e.y - 0.06 * e.k, 0.045 * e.k));
|
|
1320
1500
|
}
|
|
1321
1501
|
break;
|
|
1322
1502
|
case "bead":
|
|
1323
|
-
for (const
|
|
1324
|
-
out.push({ k: "ellipse", x:
|
|
1325
|
-
out.push(shine(
|
|
1503
|
+
for (const e of eyeSpots(def, ex, ey)) {
|
|
1504
|
+
out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.16 * e.k, ry: 0.22 * e.k, fill: ink });
|
|
1505
|
+
out.push(shine(e.x + 0.05 * e.k, e.y - 0.08 * e.k, 0.065 * e.k));
|
|
1326
1506
|
}
|
|
1327
1507
|
break;
|
|
1328
1508
|
case "big":
|
|
@@ -1431,6 +1611,21 @@ function mouth(def, ow) {
|
|
|
1431
1611
|
out.push({ k: "ellipse", x: 0, y: 0.48, rx: 0.16, ry: 0.11, fill: MOUTH });
|
|
1432
1612
|
out.push({ k: "path", d: "M-0.3 0.12 Q0 -0.02 0.3 0.12 Q0.36 0.52 0.04 0.76 Q0.08 0.52 -0.08 0.46 Q-0.28 0.38 -0.3 0.12 Z", fill: p.accent, stroke: p.outline, lw: ow * 0.8 });
|
|
1433
1613
|
break;
|
|
1614
|
+
case "toothy": {
|
|
1615
|
+
out.push({ k: "path", d: "M-1.4 0.1 Q-0.5 0.36 0.5 0.2 Q0.64 0.16 0.68 0.02 Q0.78 0.28 0.52 0.38 Q-0.4 0.78 -1.3 0.26 Z", fill: dark, stroke: dark, lw: ow * 0.6 });
|
|
1616
|
+
out.push({ k: "ellipse", x: -0.38, y: 0.5, rx: 0.24, ry: 0.08, fill: TONGUE });
|
|
1617
|
+
for (const t of [0.12, 0.27, 0.42, 0.57, 0.72, 0.86]) {
|
|
1618
|
+
const x = (1 - t) * (1 - t) * -1.4 + 2 * (1 - t) * t * -0.5 + t * t * 0.5;
|
|
1619
|
+
const y = (1 - t) * (1 - t) * 0.1 + 2 * (1 - t) * t * 0.36 + t * t * 0.2 - 0.01;
|
|
1620
|
+
out.push({ k: "path", d: roundedPolygon(pts(x - 0.08, y, x + 0.08, y, x, y + 0.14), 0.3), fill: WHITE });
|
|
1621
|
+
}
|
|
1622
|
+
for (const t of [0.3, 0.7]) {
|
|
1623
|
+
const x = (1 - t) * (1 - t) * 0.52 + 2 * (1 - t) * t * -0.4 + t * t * -1.3;
|
|
1624
|
+
const y = (1 - t) * (1 - t) * 0.38 + 2 * (1 - t) * t * 0.78 + t * t * 0.26 + 0.01;
|
|
1625
|
+
out.push({ k: "path", d: roundedPolygon(pts(x - 0.07, y, x + 0.07, y, x, y - 0.12), 0.3), fill: WHITE });
|
|
1626
|
+
}
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
1434
1629
|
case "jaws": {
|
|
1435
1630
|
out.push({ k: "path", d: "M-0.5 0.3 Q0 0.5 0.5 0.3 Q0.38 0.72 0 0.72 Q-0.38 0.72 -0.5 0.3 Z", fill: dark, stroke: dark, lw: ow * 0.5 });
|
|
1436
1631
|
for (const x of [-0.34, -0.17, 0, 0.17, 0.34]) {
|
|
@@ -1453,7 +1648,10 @@ var BAND_BODY = {
|
|
|
1453
1648
|
"9-12": { head: 0.5, neck: 0.92, arm: 0.17, leg: 0.21, hand: 0.125, foot: 0.9, shoulderMin: 0.36, shoulderPad: 0.07, hipMin: 0.32, hipPad: 0.08, bottom: 0.1, bulge: 1, armsOverHead: false }
|
|
1454
1649
|
};
|
|
1455
1650
|
var DEFAULT_BAND = "6-8";
|
|
1456
|
-
var bodyBand = (def) =>
|
|
1651
|
+
var bodyBand = (def, shown) => {
|
|
1652
|
+
if (shown !== void 0 && def.band !== "all" && def.alsoIn?.includes(shown) && shown in BAND_BODY) return shown;
|
|
1653
|
+
return def.band in BAND_BODY ? def.band : DEFAULT_BAND;
|
|
1654
|
+
};
|
|
1457
1655
|
var HEAD = BAND_BODY[DEFAULT_BAND].head;
|
|
1458
1656
|
var MAX_TILT = 0.3;
|
|
1459
1657
|
var TILT_DEAD_ZONE = 0.12;
|
|
@@ -1547,7 +1745,7 @@ function headTilt(body, at, angle, U) {
|
|
|
1547
1745
|
return 0;
|
|
1548
1746
|
}
|
|
1549
1747
|
function avatarShapes(body, def, box, opts = {}) {
|
|
1550
|
-
const band = bodyBand(def);
|
|
1748
|
+
const band = bodyBand(def, opts.band);
|
|
1551
1749
|
const bb = BAND_BODY[band];
|
|
1552
1750
|
const F = frameOf(body, box, { ...opts, band });
|
|
1553
1751
|
if (!F) return [];
|
|
@@ -1579,8 +1777,8 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1579
1777
|
out.push({ k: "line", pts: flat, stroke: p.outline, lw: w + ow * 2 });
|
|
1580
1778
|
out.push({ k: "line", pts: flat, stroke: color, lw: w });
|
|
1581
1779
|
};
|
|
1582
|
-
const legW = bb.leg * U;
|
|
1583
|
-
const armW = bb.arm * U;
|
|
1780
|
+
const legW = bb.leg * U * (has("strong-legs") ? 1.3 : 1);
|
|
1781
|
+
const armW = bb.arm * U * (has("tiny-arms") ? 0.6 : 1);
|
|
1584
1782
|
const pad = Math.max(bb.arm / 0.22, 0.9);
|
|
1585
1783
|
if (F.hipsOk) {
|
|
1586
1784
|
for (const [hip, kneeI, ankleI, side] of [
|
|
@@ -1594,7 +1792,7 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1594
1792
|
const pts2 = ankle.ok ? [top, knee, ankle] : [top, knee];
|
|
1595
1793
|
limb(legs, pts2, legW, p.limbs);
|
|
1596
1794
|
if (has("joints")) legs.push({ k: "circle", x: knee.x, y: knee.y, r: 0.15 * U * pad, fill: p.accent, ...ol });
|
|
1597
|
-
if (ankle.ok) legs.push(
|
|
1795
|
+
if (ankle.ok) legs.push(...feet(def, ankle, F, side, ow, bb.foot));
|
|
1598
1796
|
}
|
|
1599
1797
|
}
|
|
1600
1798
|
for (const [sh, elbowI, wristI, side] of [
|
|
@@ -1627,7 +1825,20 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1627
1825
|
out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.2 * U, ry: 0.09 * U, rot: Math.atan2(fd.y, fd.x), fill: p.hands ?? p.accent, ...ol });
|
|
1628
1826
|
}
|
|
1629
1827
|
}
|
|
1630
|
-
|
|
1828
|
+
if (has("wand") && side === 1 && !opts.portrait) out.push(...wand(def, hand, norm(add(mul(dir, 0.4), sub(mul(r, 0.6), d))), U, ow));
|
|
1829
|
+
const handR = bb.hand * U * (has("tiny-arms") ? 0.72 : 1);
|
|
1830
|
+
if (has("tiny-arms")) {
|
|
1831
|
+
for (const a of [-0.45, 0.45]) {
|
|
1832
|
+
const c = Math.cos(a);
|
|
1833
|
+
const s = Math.sin(a);
|
|
1834
|
+
const cd = { x: dir.x * c - dir.y * s, y: dir.x * s + dir.y * c };
|
|
1835
|
+
const base = add(hand, mul(cd, handR * 0.75));
|
|
1836
|
+
const tip = add(hand, mul(cd, handR * 1.6));
|
|
1837
|
+
const nrm = { x: -cd.y * handR * 0.32, y: cd.x * handR * 0.32 };
|
|
1838
|
+
out.push({ k: "path", d: roundedPolygon([add(base, nrm), tip, sub(base, nrm)], 0.2), fill: p.belly, ...ol });
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
out.push({ k: "circle", x: hand.x, y: hand.y, r: handR, fill: p.hands ?? p.limbs, ...ol });
|
|
1631
1842
|
}
|
|
1632
1843
|
if (has("wings")) {
|
|
1633
1844
|
for (const s of [-1, 1]) {
|
|
@@ -1642,6 +1853,7 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1642
1853
|
back.push({ k: "path", d: roundedPolygon(w, 0.18), fill: p.accent, ...ol });
|
|
1643
1854
|
}
|
|
1644
1855
|
}
|
|
1856
|
+
if (has("wings-fairy")) back.push(...fairyWings(def, T, ht, U, ow));
|
|
1645
1857
|
if (has("cape")) {
|
|
1646
1858
|
const yb = L + 0.8 * U;
|
|
1647
1859
|
const wb = hb + 0.36 * U;
|
|
@@ -1859,6 +2071,7 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1859
2071
|
const k = 0.15 * U;
|
|
1860
2072
|
torso.push({ k: "group", x: c.x, y: c.y, rot: F.angle - 0.2, s: k, items: [{ k: "path", d: STAR, fill: def.extraColors?.[0] ?? "#ffd84a", stroke: p.outline, lw: ow * 0.8 / k }] });
|
|
1861
2073
|
}
|
|
2074
|
+
if (has("petals")) torso.push(...petals(def, T, ht, hb, bulge, L, y1, U, ow, bb));
|
|
1862
2075
|
if (has("scarf")) {
|
|
1863
2076
|
const y = Math.max(0.04, bb.head * (1 - bb.neck) - 0.06) * U;
|
|
1864
2077
|
const c = T(0, y);
|
|
@@ -1883,6 +2096,18 @@ function cachedHead(def, ow) {
|
|
|
1883
2096
|
}
|
|
1884
2097
|
return shapes;
|
|
1885
2098
|
}
|
|
2099
|
+
function feet(def, ankle, F, side, ow, k) {
|
|
2100
|
+
const shoe = foot(def, ankle, F, side, ow, k);
|
|
2101
|
+
if (!def.bodyExtras.includes("strong-legs")) return [shoe];
|
|
2102
|
+
const U = F.U * k;
|
|
2103
|
+
const c = add(add(ankle, mul(F.d, 0.07 * U)), mul(F.r, side * 0.06 * U));
|
|
2104
|
+
const out = [shoe];
|
|
2105
|
+
for (const t of [-1, 0, 1]) {
|
|
2106
|
+
const q = add(add(c, mul(F.r, t * 0.12 * U)), mul(F.d, 0.09 * U));
|
|
2107
|
+
out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.05 * U, ry: 0.045 * U, rot: F.angle, fill: def.palette.belly, stroke: def.palette.outline, lw: ow * 0.8 });
|
|
2108
|
+
}
|
|
2109
|
+
return out;
|
|
2110
|
+
}
|
|
1886
2111
|
function foot(def, ankle, F, side, ow, k) {
|
|
1887
2112
|
const p = def.palette;
|
|
1888
2113
|
const { d, r } = F;
|
|
@@ -2036,6 +2261,30 @@ function tail(def, F, hb, ow) {
|
|
|
2036
2261
|
});
|
|
2037
2262
|
break;
|
|
2038
2263
|
}
|
|
2264
|
+
case "thick": {
|
|
2265
|
+
const pts2 = TP([
|
|
2266
|
+
[hb * 0.3, L * 0.42],
|
|
2267
|
+
[hb + 0.42 * U, L * 0.62],
|
|
2268
|
+
[hb + 0.92 * U, L * 0.96],
|
|
2269
|
+
[hb + 1.32 * U, L * 1.08],
|
|
2270
|
+
[hb + 1.44 * U, L * 0.98],
|
|
2271
|
+
[hb + 0.98 * U, L * 1.2],
|
|
2272
|
+
[hb + 0.4 * U, L * 1.22],
|
|
2273
|
+
[hb * 0.3, L * 1.12]
|
|
2274
|
+
]);
|
|
2275
|
+
out.push({ k: "path", d: smoothClosed(pts2, 0.8), fill: p.body, ...ol });
|
|
2276
|
+
const c = def.extraColors?.[0] ?? p.accent;
|
|
2277
|
+
for (const [x, y, l] of [
|
|
2278
|
+
[0.28, 0.58, 0.26],
|
|
2279
|
+
[0.62, 0.8, 0.22],
|
|
2280
|
+
[0.96, 1, 0.16]
|
|
2281
|
+
]) {
|
|
2282
|
+
const a = T(hb + x * U, L * y + 0.03 * U);
|
|
2283
|
+
const b = T(hb + (x - 0.1) * U, L * y + l * U);
|
|
2284
|
+
out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.09 * U });
|
|
2285
|
+
}
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2039
2288
|
case "spiky":
|
|
2040
2289
|
case "arrow": {
|
|
2041
2290
|
const pts2 = TP([
|
|
@@ -2068,6 +2317,81 @@ function tail(def, F, hb, ow) {
|
|
|
2068
2317
|
}
|
|
2069
2318
|
return out;
|
|
2070
2319
|
}
|
|
2320
|
+
function wand(def, hand, wd, U, ow) {
|
|
2321
|
+
const p = def.palette;
|
|
2322
|
+
const from = add(hand, mul(wd, -0.08 * U));
|
|
2323
|
+
const tip = add(hand, mul(wd, 0.52 * U));
|
|
2324
|
+
const k = 0.2 * U;
|
|
2325
|
+
return [
|
|
2326
|
+
{ k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: p.outline, lw: 0.06 * U + ow * 2 },
|
|
2327
|
+
{ k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: "#fff3d6", lw: 0.06 * U },
|
|
2328
|
+
{ k: "circle", x: tip.x, y: tip.y, r: 0.24 * U, fill: p.accent, alpha: 0.28 },
|
|
2329
|
+
{ k: "group", x: tip.x, y: tip.y, rot: Math.atan2(wd.y, wd.x) + Math.PI / 2, s: k, items: [{ k: "path", d: STAR, fill: p.accent, stroke: p.outline, lw: ow * 0.9 / k }] }
|
|
2330
|
+
];
|
|
2331
|
+
}
|
|
2332
|
+
function fairyWings(def, T, ht, U, ow) {
|
|
2333
|
+
const p = def.palette;
|
|
2334
|
+
const fill = def.extraColors?.[0] ?? "#cdeeff";
|
|
2335
|
+
const out = [];
|
|
2336
|
+
for (const s of [-1, 1]) {
|
|
2337
|
+
const lobes = [
|
|
2338
|
+
// Upper: big, up and out.
|
|
2339
|
+
[
|
|
2340
|
+
[s * ht * 0.3, 0.12 * U],
|
|
2341
|
+
[s * (ht + 0.42 * U), -0.66 * U],
|
|
2342
|
+
[s * (ht + 1 * U), -0.74 * U],
|
|
2343
|
+
[s * (ht + 1.02 * U), -0.18 * U],
|
|
2344
|
+
[s * (ht + 0.5 * U), 0.26 * U]
|
|
2345
|
+
],
|
|
2346
|
+
// Lower: smaller, down and out.
|
|
2347
|
+
[
|
|
2348
|
+
[s * ht * 0.4, 0.3 * U],
|
|
2349
|
+
[s * (ht + 0.6 * U), 0.36 * U],
|
|
2350
|
+
[s * (ht + 0.74 * U), 0.78 * U],
|
|
2351
|
+
[s * (ht + 0.3 * U), 0.9 * U]
|
|
2352
|
+
]
|
|
2353
|
+
];
|
|
2354
|
+
for (const lobe of lobes) {
|
|
2355
|
+
const d = smoothClosed(lobe.map(([x, y]) => T(x, y)), 0.9);
|
|
2356
|
+
out.push({ k: "path", d, fill, alpha: 0.6 });
|
|
2357
|
+
out.push({ k: "path", d, stroke: p.outline, lw: ow * 0.9 });
|
|
2358
|
+
const inner = lobe[Math.floor(lobe.length / 2)];
|
|
2359
|
+
const a = T(lobe[0][0], lobe[0][1]);
|
|
2360
|
+
const b = T(lobe[0][0] + (inner[0] - lobe[0][0]) * 0.75, lobe[0][1] + (inner[1] - lobe[0][1]) * 0.75);
|
|
2361
|
+
out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: "#ffffff", lw: ow * 0.9, alpha: 0.85 });
|
|
2362
|
+
}
|
|
2363
|
+
for (const [x, y, r] of [
|
|
2364
|
+
[ht + 0.72 * U, -0.42 * U, 0.075 * U],
|
|
2365
|
+
[ht + 0.44 * U, -0.16 * U, 0.05 * U],
|
|
2366
|
+
[ht + 0.5 * U, 0.6 * U, 0.055 * U]
|
|
2367
|
+
]) {
|
|
2368
|
+
const q = T(s * x, y);
|
|
2369
|
+
out.push({ k: "group", x: q.x, y: q.y, s: r, items: [{ k: "path", d: STAR, fill: "#ffffff" }] });
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return out;
|
|
2373
|
+
}
|
|
2374
|
+
function petals(def, T, ht, hb, bulge, L, y1, U, ow, bb) {
|
|
2375
|
+
const p = def.palette;
|
|
2376
|
+
const second = def.extraColors?.[1] ?? p.belly;
|
|
2377
|
+
const out = [];
|
|
2378
|
+
const ang = Math.atan2(T(1, 0).y - T(0, 0).y, T(1, 0).x - T(0, 0).x);
|
|
2379
|
+
const n = 5;
|
|
2380
|
+
const w = Math.max(hb, bulge * 0.92) * 1.08;
|
|
2381
|
+
const y = (L * 0.8 + y1) / 2;
|
|
2382
|
+
const order = [0, 4, 1, 3, 2];
|
|
2383
|
+
for (const i of order) {
|
|
2384
|
+
const t = i / (n - 1) - 0.5;
|
|
2385
|
+
const c = T(t * 2 * w, y + (0.5 - Math.abs(t)) * 0.1 * U);
|
|
2386
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: w * 0.3, ry: 0.34 * U, rot: ang - t * 0.6, fill: i % 2 ? second : p.body, stroke: p.outline, lw: ow });
|
|
2387
|
+
}
|
|
2388
|
+
const cy = Math.max(0.04, bb.head * (1 - bb.neck) - 0.04) * U;
|
|
2389
|
+
for (const t of [-1, 1, 0]) {
|
|
2390
|
+
const c = T(t * ht * 0.32, cy + (t ? 0 : 0.05 * U));
|
|
2391
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: ht * 0.26, ry: 0.12 * U, rot: ang + t * 0.5, fill: t ? second : p.body, stroke: p.outline, lw: ow * 0.9 });
|
|
2392
|
+
}
|
|
2393
|
+
return out;
|
|
2394
|
+
}
|
|
2071
2395
|
|
|
2072
2396
|
// ../avatars/src/poses.ts
|
|
2073
2397
|
var STAND = {
|
|
@@ -2211,7 +2535,7 @@ var isAvatarId = (v) => typeof v === "string" && BY_ID.has(v);
|
|
|
2211
2535
|
var avatarById = (id) => BY_ID.get(id);
|
|
2212
2536
|
var resolve = (def) => typeof def === "string" ? BY_ID.get(def) ?? DEFAULT_AVATAR : def;
|
|
2213
2537
|
function drawAvatar(ctx, body, def, box, opts = {}) {
|
|
2214
|
-
const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored });
|
|
2538
|
+
const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored, band: opts.band });
|
|
2215
2539
|
if (!shapes.length) return false;
|
|
2216
2540
|
ctx.save();
|
|
2217
2541
|
if (opts.alpha !== void 0) ctx.globalAlpha *= Math.min(Math.max(opts.alpha, 0), 1);
|
|
@@ -2219,17 +2543,19 @@ function drawAvatar(ctx, body, def, box, opts = {}) {
|
|
|
2219
2543
|
ctx.restore();
|
|
2220
2544
|
return true;
|
|
2221
2545
|
}
|
|
2222
|
-
function iconShapes(def, size) {
|
|
2546
|
+
function iconShapes(def, size, band) {
|
|
2223
2547
|
const d = resolve(def);
|
|
2224
2548
|
const body = neutralPose();
|
|
2225
|
-
const
|
|
2226
|
-
const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band });
|
|
2227
|
-
const
|
|
2549
|
+
const shown = bodyBand(d, band);
|
|
2550
|
+
const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band: shown });
|
|
2551
|
+
const hb = bounds(headShapes(d, 0.05));
|
|
2552
|
+
const top = -hb.minY;
|
|
2228
2553
|
const headR = Math.min(0.27, 0.43 / Math.max(top, 1)) * size;
|
|
2229
|
-
const
|
|
2554
|
+
const dx = d.head === "snout-long" ? (hb.minX + hb.maxX) / 2 * headR : 0;
|
|
2555
|
+
const k = headR / BAND_BODY[shown].head / unit.U;
|
|
2230
2556
|
const cy = 0.5 * size + Math.min(0, (top - 1.6) * 0.06 * size);
|
|
2231
|
-
const box = { x: size / 2 - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
|
|
2232
|
-
return avatarShapes(body, d, box, { portrait: true });
|
|
2557
|
+
const box = { x: size / 2 - dx - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
|
|
2558
|
+
return avatarShapes(body, d, box, { portrait: true, band: shown });
|
|
2233
2559
|
}
|
|
2234
2560
|
function smoothBody(prev, next, amount = 0.5) {
|
|
2235
2561
|
if (!prev || prev.length !== next.length) return next.slice();
|
|
@@ -2459,6 +2785,7 @@ function easy(connecting) {
|
|
|
2459
2785
|
p.on("break-done", (m) => emit("break-end", { shown: m.shown }));
|
|
2460
2786
|
p.onBackdrop((backdrop, view) => emit("backdrop", { backdrop, view }));
|
|
2461
2787
|
p.on("turn", (m) => emit("turn", { players: [...m.players] }));
|
|
2788
|
+
p.on("capabilities", (m) => emit("capabilities", { pictures: m.pictures }));
|
|
2462
2789
|
p.on("frame", (f2) => {
|
|
2463
2790
|
emit("players", { players: f2.players });
|
|
2464
2791
|
for (const pl of f2.players) {
|
|
@@ -2505,6 +2832,13 @@ function easy(connecting) {
|
|
|
2505
2832
|
get hasPointer() {
|
|
2506
2833
|
return play?.pointer ?? false;
|
|
2507
2834
|
},
|
|
2835
|
+
get canSavePicture() {
|
|
2836
|
+
return play?.canSavePicture ?? false;
|
|
2837
|
+
},
|
|
2838
|
+
savePicture: (source, opts) => new Promise((resolve2) => withPlay((p) => void p.savePicture(source, opts).then(resolve2))),
|
|
2839
|
+
get muted() {
|
|
2840
|
+
return play?.muted ?? false;
|
|
2841
|
+
},
|
|
2508
2842
|
get view() {
|
|
2509
2843
|
return play?.view ?? null;
|
|
2510
2844
|
},
|
|
@@ -2519,7 +2853,7 @@ function easy(connecting) {
|
|
|
2519
2853
|
};
|
|
2520
2854
|
}
|
|
2521
2855
|
var none = Object.freeze([]);
|
|
2522
|
-
var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, backdrop: p.backdrop, view: p.view, device: p.device, turns: p.turns, input: p.input, pointer: p.pointer });
|
|
2856
|
+
var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, orientation: p.orientation, backdrop: p.backdrop, view: p.view, device: p.device, turns: p.turns, input: p.input, pointer: p.pointer, canSavePicture: p.canSavePicture, muted: p.muted });
|
|
2523
2857
|
var playingNow = (p) => p.input === "remote" ? p.players[0] ?? 1 : 1;
|
|
2524
2858
|
function endOf(p, score, last) {
|
|
2525
2859
|
const now2 = playingNow(p);
|
|
@@ -2528,6 +2862,46 @@ function endOf(p, score, last) {
|
|
|
2528
2862
|
return [...played.map((player) => ({ player, score: last.get(player) ?? 0, stars: 2 })), { player: now2, score, stars: 2 }];
|
|
2529
2863
|
}
|
|
2530
2864
|
|
|
2865
|
+
// src/picture.ts
|
|
2866
|
+
var QUALITIES = [PICTURE_JPEG_QUALITY, 0.7, 0.55];
|
|
2867
|
+
var SHRINK = 0.75;
|
|
2868
|
+
var MIN_SIDE = 320;
|
|
2869
|
+
function encodePicture(source) {
|
|
2870
|
+
if (typeof source === "string") {
|
|
2871
|
+
const checked = checkPicture(source);
|
|
2872
|
+
return "ok" in checked ? { data: source } : { error: checked.error === "too-big" ? "too-big" : "error" };
|
|
2873
|
+
}
|
|
2874
|
+
try {
|
|
2875
|
+
if (!source || typeof source.toDataURL !== "function") return { error: "error" };
|
|
2876
|
+
const { width, height } = source;
|
|
2877
|
+
if (!(width > 0 && height > 0)) return { error: "error" };
|
|
2878
|
+
const doc = source.ownerDocument ?? document;
|
|
2879
|
+
let scale = Math.min(1, MAX_PICTURE_SIDE / Math.max(width, height));
|
|
2880
|
+
for (; ; ) {
|
|
2881
|
+
const w = Math.max(1, Math.round(width * scale));
|
|
2882
|
+
const h = Math.max(1, Math.round(height * scale));
|
|
2883
|
+
const flat = doc.createElement("canvas");
|
|
2884
|
+
flat.width = w;
|
|
2885
|
+
flat.height = h;
|
|
2886
|
+
const ctx = flat.getContext("2d");
|
|
2887
|
+
if (!ctx) return { error: "error" };
|
|
2888
|
+
ctx.fillStyle = "#fff";
|
|
2889
|
+
ctx.fillRect(0, 0, w, h);
|
|
2890
|
+
ctx.drawImage(source, 0, 0, w, h);
|
|
2891
|
+
for (const q of QUALITIES) {
|
|
2892
|
+
const data = flat.toDataURL("image/jpeg", q);
|
|
2893
|
+
const checked = checkPicture(data);
|
|
2894
|
+
if ("ok" in checked) return { data };
|
|
2895
|
+
if (checked.error !== "too-big") return { error: "error" };
|
|
2896
|
+
}
|
|
2897
|
+
if (Math.max(w, h) * SHRINK < MIN_SIDE) return { error: "too-big" };
|
|
2898
|
+
scale *= SHRINK;
|
|
2899
|
+
}
|
|
2900
|
+
} catch {
|
|
2901
|
+
return { error: "error" };
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2531
2905
|
// src/index.ts
|
|
2532
2906
|
var windowChannel = (win = window) => ({
|
|
2533
2907
|
post: (m) => win.parent.postMessage(m, "*"),
|
|
@@ -2536,7 +2910,8 @@ var windowChannel = (win = window) => ({
|
|
|
2536
2910
|
});
|
|
2537
2911
|
var INIT_TIMEOUT_MS = 1e4;
|
|
2538
2912
|
var STATS_MS = 1e3;
|
|
2539
|
-
|
|
2913
|
+
var PICTURE_TIMEOUT_MS = 12e4;
|
|
2914
|
+
function connect(channel = windowChannel(), options = {}) {
|
|
2540
2915
|
return new Promise((resolve2, reject) => {
|
|
2541
2916
|
const listeners = /* @__PURE__ */ new Map();
|
|
2542
2917
|
const breaks = [];
|
|
@@ -2548,6 +2923,17 @@ function connect(channel = windowChannel()) {
|
|
|
2548
2923
|
let input = "body";
|
|
2549
2924
|
let players = Object.freeze([]);
|
|
2550
2925
|
let turns = players;
|
|
2926
|
+
let pictures = false;
|
|
2927
|
+
let pictureId = 0;
|
|
2928
|
+
const pictureWaits = /* @__PURE__ */ new Map();
|
|
2929
|
+
let cursor;
|
|
2930
|
+
const cursorTarget = () => {
|
|
2931
|
+
if (cursor === void 0) {
|
|
2932
|
+
const target = options.cursor !== void 0 ? options.cursor : typeof document !== "undefined" ? domCursor() : null;
|
|
2933
|
+
cursor = target ? cursorDriver(target) : null;
|
|
2934
|
+
}
|
|
2935
|
+
return cursor;
|
|
2936
|
+
};
|
|
2551
2937
|
const backdropListeners = /* @__PURE__ */ new Set();
|
|
2552
2938
|
const avatars = new AvatarBook();
|
|
2553
2939
|
const drawAvatar2 = avatarDrawer(avatars, () => ({ view }));
|
|
@@ -2568,6 +2954,7 @@ function connect(channel = windowChannel()) {
|
|
|
2568
2954
|
const device = parseDeviceHint(m.device) ?? null;
|
|
2569
2955
|
input = m.input === "remote" ? "remote" : "body";
|
|
2570
2956
|
const pointer2 = input === "remote" && m.pointer === true;
|
|
2957
|
+
pictures = m.pictures === true;
|
|
2571
2958
|
players = Object.freeze(Array.isArray(m.players) ? [...m.players] : []);
|
|
2572
2959
|
const order = input === "remote" && players.length === 1 ? parseTurnOrder(m.turns) : void 0;
|
|
2573
2960
|
turns = Object.freeze(order && order[0] === players[0] ? order : [...players]);
|
|
@@ -2591,6 +2978,8 @@ function connect(channel = windowChannel()) {
|
|
|
2591
2978
|
input,
|
|
2592
2979
|
pointer: pointer2,
|
|
2593
2980
|
muted: m.muted,
|
|
2981
|
+
// Hosts before orientations don't send it: landscape.
|
|
2982
|
+
orientation: m.orientation === "portrait" ? "portrait" : "landscape",
|
|
2594
2983
|
ageRange: m.ageRange ?? "6-8",
|
|
2595
2984
|
get backdrop() {
|
|
2596
2985
|
return backdrop;
|
|
@@ -2622,7 +3011,28 @@ function connect(channel = windowChannel()) {
|
|
|
2622
3011
|
breaks.push({ reason, done });
|
|
2623
3012
|
channel.post(wire({ type: "break", reason }));
|
|
2624
3013
|
}),
|
|
2625
|
-
end: (scores) => channel.post(wire({ type: "end", scores }))
|
|
3014
|
+
end: (scores) => channel.post(wire({ type: "end", scores })),
|
|
3015
|
+
get canSavePicture() {
|
|
3016
|
+
return pictures;
|
|
3017
|
+
},
|
|
3018
|
+
savePicture: async (source, opts) => {
|
|
3019
|
+
if (!pictures) return "unavailable";
|
|
3020
|
+
const encoded = encodePicture(source);
|
|
3021
|
+
if ("error" in encoded) return encoded.error;
|
|
3022
|
+
const id = ++pictureId;
|
|
3023
|
+
const title = parsePictureTitle(opts?.title);
|
|
3024
|
+
return new Promise((done) => {
|
|
3025
|
+
const timer2 = setTimeout(() => finish("error"), PICTURE_TIMEOUT_MS);
|
|
3026
|
+
timer2.unref?.();
|
|
3027
|
+
const finish = (r) => {
|
|
3028
|
+
clearTimeout(timer2);
|
|
3029
|
+
pictureWaits.delete(id);
|
|
3030
|
+
done(r);
|
|
3031
|
+
};
|
|
3032
|
+
pictureWaits.set(id, finish);
|
|
3033
|
+
channel.post(wire({ type: "picture", data: encoded.data, id, ...title ? { title } : {} }));
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
2626
3036
|
});
|
|
2627
3037
|
} else if (!started) return;
|
|
2628
3038
|
else {
|
|
@@ -2645,6 +3055,23 @@ function connect(channel = windowChannel()) {
|
|
|
2645
3055
|
players = Object.freeze(next);
|
|
2646
3056
|
message = { type: "turn", players: [...next] };
|
|
2647
3057
|
}
|
|
3058
|
+
if (m.type === "picture-result") {
|
|
3059
|
+
const r = parsePictureResult(m);
|
|
3060
|
+
if (r?.id !== void 0) pictureWaits.get(r.id)?.(r.result);
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (m.type === "capabilities") {
|
|
3064
|
+
if (typeof m.pictures !== "boolean" || m.pictures === pictures) return;
|
|
3065
|
+
pictures = m.pictures;
|
|
3066
|
+
message = { type: "capabilities", pictures };
|
|
3067
|
+
}
|
|
3068
|
+
if (m.type === "cursor") {
|
|
3069
|
+
const c = input === "remote" ? parseCursor(m) : void 0;
|
|
3070
|
+
if (!c) return;
|
|
3071
|
+
message = c;
|
|
3072
|
+
cursorTarget()?.update(c);
|
|
3073
|
+
}
|
|
3074
|
+
if (m.type === "pause" || m.type === "break-start" || m.type === "turn") cursor?.cancel();
|
|
2648
3075
|
if (m.type === "break-start") inBreak = breaks.length > 0;
|
|
2649
3076
|
for (const l of listeners.get(m.type) ?? []) l(message);
|
|
2650
3077
|
if (m.type === "break-done") {
|