@botharness/pixel-avatar 0.7.0 → 0.9.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 +2 -0
- package/dist/index.d.ts +81 -8
- package/dist/index.js +469 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -333,6 +333,33 @@ const AVATAR_SPECIES = [
|
|
|
333
333
|
];
|
|
334
334
|
/** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
|
|
335
335
|
const AVATAR_PIECE_COLORS = ["leftSideHairColor", "rightSideHairColor"];
|
|
336
|
+
/**
|
|
337
|
+
* Asset version 4 adds a color for every other hair piece; an absent piece uses `hairColor`.
|
|
338
|
+
* `strandColor` colors the single strand (`strand`).
|
|
339
|
+
*/
|
|
340
|
+
const AVATAR_PIECE_COLORS_V4 = [
|
|
341
|
+
"bangsColor",
|
|
342
|
+
"backHairColor",
|
|
343
|
+
"strandColor"
|
|
344
|
+
];
|
|
345
|
+
/** A single strand of hair standing up from the crown (asset version 4). */
|
|
346
|
+
const AVATAR_STRANDS = [
|
|
347
|
+
"ahoge",
|
|
348
|
+
"curl",
|
|
349
|
+
"double"
|
|
350
|
+
];
|
|
351
|
+
/**
|
|
352
|
+
* Built-in headpieces (asset version 4), worn in the headpiece slot alongside an accessory and
|
|
353
|
+
* drawn partly behind the hair. The first five were accessories in earlier versions.
|
|
354
|
+
*/
|
|
355
|
+
const AVATAR_HEADPIECES = [
|
|
356
|
+
"catears",
|
|
357
|
+
"bunnyears",
|
|
358
|
+
"horseears",
|
|
359
|
+
"horns",
|
|
360
|
+
"halo",
|
|
361
|
+
"wings"
|
|
362
|
+
];
|
|
336
363
|
/** Part choices that exist only in asset version 2, added after every version 1 choice. */
|
|
337
364
|
const AVATAR_PARTS_V2 = {
|
|
338
365
|
...AVATAR_PARTS,
|
|
@@ -410,17 +437,22 @@ function isPixelAvatarRecipe(value) {
|
|
|
410
437
|
const r = value;
|
|
411
438
|
const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
|
|
412
439
|
if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
|
|
440
|
+
const v4 = r["assetVersion"] === 4;
|
|
413
441
|
const v3 = r["assetVersion"] === 3;
|
|
414
|
-
const v2 = r["assetVersion"] === 2 || v3;
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
442
|
+
const v2 = r["assetVersion"] === 2 || v3 || v4;
|
|
443
|
+
const builtInHead = v4 && typeof r["headpiece"] === "string";
|
|
444
|
+
if (builtInHead && !AVATAR_HEADPIECES.includes(r["headpiece"])) return false;
|
|
445
|
+
const worn = PART_KEY_ENTRIES.filter(([slot, key]) => Object.hasOwn(r, key) && !(slot === "headpiece" && builtInHead));
|
|
446
|
+
if (v3 ? worn.length === 0 : !v4 && worn.length !== 0) return false;
|
|
447
|
+
const newer = [...AVATAR_PIECE_COLORS_V4, "strand"].filter((key) => Object.hasOwn(r, key));
|
|
448
|
+
if (newer.length !== 0 && (!v4 || !AVATAR_PIECE_COLORS_V4.every((key) => !Object.hasOwn(r, key) || typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key])) || Object.hasOwn(r, "strand") && !AVATAR_STRANDS.includes(r["strand"]))) return false;
|
|
417
449
|
for (const [slot, key] of worn) {
|
|
418
450
|
const part = r[key];
|
|
419
451
|
if (!isPixelCustomPart(part) || part.slot !== slot) return false;
|
|
420
452
|
}
|
|
421
453
|
const pieces = AVATAR_PIECE_COLORS.filter((key) => Object.hasOwn(r, key));
|
|
422
454
|
const extras = Object.keys(AVATAR_EXTRA_PARTS).filter((key) => Object.hasOwn(r, key));
|
|
423
|
-
const v2Keys = v2 ? 2 + pieces.length + extras.length + worn.length : 0;
|
|
455
|
+
const v2Keys = v2 ? 2 + pieces.length + extras.length + worn.length + newer.length + (builtInHead ? 1 : 0) : 0;
|
|
424
456
|
if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
|
|
425
457
|
if (v2 && (detailed === 0 || !AVATAR_SPECIES.includes(r["species"]) || !AVATAR_HAIR_PARTS.sideHair.includes(r["rightSideHair"]) || !pieces.every((key) => typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key])) || !extras.every((key) => AVATAR_EXTRA_PARTS[key].includes(r[key])))) return false;
|
|
426
458
|
const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
|
|
@@ -445,9 +477,13 @@ function canonicalRecipe(recipe) {
|
|
|
445
477
|
canonical["rightSideHair"] = recipe.rightSideHair;
|
|
446
478
|
for (const key of AVATAR_PIECE_COLORS) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
447
479
|
for (const key of Object.keys(AVATAR_EXTRA_PARTS)) if (recipe[key] !== void 0) canonical[key] = recipe[key];
|
|
448
|
-
if (recipe.assetVersion === 3) for (const [, key] of PART_KEY_ENTRIES) {
|
|
480
|
+
if (recipe.assetVersion === 3 || recipe.assetVersion === 4) for (const [, key] of PART_KEY_ENTRIES) {
|
|
449
481
|
const part = recipe[key];
|
|
450
|
-
if (part !== void 0) canonical[key] = canonicalCustomPart(part);
|
|
482
|
+
if (part !== void 0) canonical[key] = typeof part === "string" ? part : canonicalCustomPart(part);
|
|
483
|
+
}
|
|
484
|
+
if (recipe.assetVersion === 4) {
|
|
485
|
+
if (recipe.strand !== void 0) canonical["strand"] = recipe.strand;
|
|
486
|
+
for (const key of AVATAR_PIECE_COLORS_V4) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
451
487
|
}
|
|
452
488
|
}
|
|
453
489
|
return canonical;
|
|
@@ -464,8 +500,12 @@ const HAIR_PIECES = [
|
|
|
464
500
|
* the species or headwear changes back.
|
|
465
501
|
*/
|
|
466
502
|
function hiddenChoices(recipe) {
|
|
503
|
+
const strand = recipe.strand && (recipe.species === "flower" || recipe.bangs !== "none") ? ["strand"] : [];
|
|
504
|
+
const bald = recipe.strand && recipe.bangs === "none" ? ["strand"] : [];
|
|
467
505
|
if (recipe.species === "flower") return [
|
|
468
506
|
...HAIR_PIECES,
|
|
507
|
+
...strand,
|
|
508
|
+
...typeof recipe.headpiece === "string" ? ["headpiece"] : [],
|
|
469
509
|
"outfit",
|
|
470
510
|
"accessory",
|
|
471
511
|
"eyes",
|
|
@@ -476,8 +516,9 @@ function hiddenChoices(recipe) {
|
|
|
476
516
|
...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : []
|
|
477
517
|
];
|
|
478
518
|
const hidden = [];
|
|
479
|
-
if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES);
|
|
519
|
+
if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES, ...strand);
|
|
480
520
|
if (recipe.species === "dwarf") hidden.push("nose");
|
|
521
|
+
if (!hidden.includes("strand")) hidden.push(...bald);
|
|
481
522
|
return hidden;
|
|
482
523
|
}
|
|
483
524
|
function withSpecies(recipe, species) {
|
|
@@ -486,16 +527,12 @@ function withSpecies(recipe, species) {
|
|
|
486
527
|
const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
|
|
487
528
|
return {
|
|
488
529
|
...detailed,
|
|
489
|
-
assetVersion: recipe.assetVersion ===
|
|
530
|
+
assetVersion: recipe.assetVersion === 1 ? 2 : recipe.assetVersion,
|
|
490
531
|
species,
|
|
491
532
|
rightSideHair: detailed.rightSideHair ?? detailed.sideHair,
|
|
492
533
|
skinColor: suggested && previous !== species ? AVATAR_SPECIES_SWATCHES[species][0] : recipe.skinColor
|
|
493
534
|
};
|
|
494
535
|
}
|
|
495
|
-
/**
|
|
496
|
-
* Returns the recipe wearing `part` in `slot`, or with that slot's part taken off. The recipe
|
|
497
|
-
* embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise.
|
|
498
|
-
*/
|
|
499
536
|
function withCustomPart(recipe, slot, part) {
|
|
500
537
|
const base = { ...recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe };
|
|
501
538
|
const key = CUSTOM_PART_KEYS[slot];
|
|
@@ -504,19 +541,51 @@ function withCustomPart(recipe, slot, part) {
|
|
|
504
541
|
...part,
|
|
505
542
|
slot
|
|
506
543
|
});
|
|
544
|
+
if (recipe.assetVersion === 4) return base;
|
|
507
545
|
const wearing = PART_KEY_ENTRIES.some(([, k]) => base[k] !== void 0);
|
|
508
546
|
return {
|
|
509
547
|
...base,
|
|
510
548
|
assetVersion: wearing ? 3 : 2
|
|
511
549
|
};
|
|
512
550
|
}
|
|
551
|
+
const MOVED_HEADPIECES = AVATAR_HEADPIECES;
|
|
552
|
+
/**
|
|
553
|
+
* Returns the recipe as asset version 4, keeping every choice. An accessory that is now a
|
|
554
|
+
* headpiece moves into the empty headpiece slot, freeing the accessory slot; call this only when
|
|
555
|
+
* the Human edits, so saved recipes keep rendering as they were.
|
|
556
|
+
*/
|
|
557
|
+
function withPieces(recipe) {
|
|
558
|
+
if (recipe.assetVersion === 4) return recipe;
|
|
559
|
+
const base = {
|
|
560
|
+
...recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe,
|
|
561
|
+
assetVersion: 4
|
|
562
|
+
};
|
|
563
|
+
if (base["headpiece"] === void 0 && MOVED_HEADPIECES.includes(recipe.accessory)) {
|
|
564
|
+
base["headpiece"] = recipe.accessory;
|
|
565
|
+
base["accessory"] = "none";
|
|
566
|
+
}
|
|
567
|
+
return base;
|
|
568
|
+
}
|
|
569
|
+
/** Returns the recipe as asset version 4 wearing a built-in headpiece, or none. */
|
|
570
|
+
function withBuiltInHeadpiece(recipe, headpiece) {
|
|
571
|
+
const base = { ...withPieces(recipe) };
|
|
572
|
+
delete base["headpiece"];
|
|
573
|
+
if (headpiece !== void 0) base["headpiece"] = headpiece;
|
|
574
|
+
return base;
|
|
575
|
+
}
|
|
576
|
+
/** The built-in headpiece worn, if any. */
|
|
577
|
+
function builtInHeadpiece(recipe) {
|
|
578
|
+
return typeof recipe.headpiece === "string" ? recipe.headpiece : void 0;
|
|
579
|
+
}
|
|
513
580
|
/** `withCustomPart` for the headpiece slot. */
|
|
514
581
|
function withHeadpiece(recipe, part) {
|
|
515
582
|
return withCustomPart(recipe, "headpiece", part);
|
|
516
583
|
}
|
|
517
584
|
/** The Custom Part worn in a slot, if any. */
|
|
518
585
|
function wornPart(recipe, slot) {
|
|
519
|
-
|
|
586
|
+
if (recipe.assetVersion !== 3 && recipe.assetVersion !== 4) return void 0;
|
|
587
|
+
const part = recipe[CUSTOM_PART_KEYS[slot]];
|
|
588
|
+
return typeof part === "string" ? void 0 : part;
|
|
520
589
|
}
|
|
521
590
|
function detailedRecipe(recipe) {
|
|
522
591
|
if (recipe.bangs !== void 0) return recipe;
|
|
@@ -1080,6 +1149,182 @@ function fillPartLayer(slot, layer, x, y, ink, mirror = false) {
|
|
|
1080
1149
|
}
|
|
1081
1150
|
return out;
|
|
1082
1151
|
}
|
|
1152
|
+
/** Pixel-perfect line points (Bresenham). With `snap`, the end snaps to 0°, 45° or 90°. */
|
|
1153
|
+
function partLinePoints(from, to, snap = false) {
|
|
1154
|
+
let [x1, y1] = to;
|
|
1155
|
+
const [x0, y0] = from;
|
|
1156
|
+
if (snap) {
|
|
1157
|
+
const dx = x1 - x0;
|
|
1158
|
+
const dy = y1 - y0;
|
|
1159
|
+
const ax = Math.abs(dx);
|
|
1160
|
+
const ay = Math.abs(dy);
|
|
1161
|
+
if (ax > ay * 2) y1 = y0;
|
|
1162
|
+
else if (ay > ax * 2) x1 = x0;
|
|
1163
|
+
else {
|
|
1164
|
+
const m = Math.max(ax, ay);
|
|
1165
|
+
x1 = x0 + Math.sign(dx) * m;
|
|
1166
|
+
y1 = y0 + Math.sign(dy) * m;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const points = [];
|
|
1170
|
+
const dx = Math.abs(x1 - x0);
|
|
1171
|
+
const dy = -Math.abs(y1 - y0);
|
|
1172
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
1173
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
1174
|
+
let error = dx + dy;
|
|
1175
|
+
let [x, y] = [x0, y0];
|
|
1176
|
+
for (;;) {
|
|
1177
|
+
points.push([x, y]);
|
|
1178
|
+
if (x === x1 && y === y1) break;
|
|
1179
|
+
const e2 = 2 * error;
|
|
1180
|
+
if (e2 >= dy) {
|
|
1181
|
+
error += dy;
|
|
1182
|
+
x += sx;
|
|
1183
|
+
}
|
|
1184
|
+
if (e2 <= dx) {
|
|
1185
|
+
error += dx;
|
|
1186
|
+
y += sy;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return points;
|
|
1190
|
+
}
|
|
1191
|
+
/** Rectangle outline points between two corners. With `square`, the far corner makes a square. */
|
|
1192
|
+
function partRectPoints(from, to, square = false) {
|
|
1193
|
+
const [x0, y0] = from;
|
|
1194
|
+
let [x1, y1] = to;
|
|
1195
|
+
if (square) {
|
|
1196
|
+
const m = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0));
|
|
1197
|
+
x1 = x0 + (x1 < x0 ? -m : m);
|
|
1198
|
+
y1 = y0 + (y1 < y0 ? -m : m);
|
|
1199
|
+
}
|
|
1200
|
+
const [left, right] = [Math.min(x0, x1), Math.max(x0, x1)];
|
|
1201
|
+
const [top, bottom] = [Math.min(y0, y1), Math.max(y0, y1)];
|
|
1202
|
+
const points = /* @__PURE__ */ new Map();
|
|
1203
|
+
for (let x = left; x <= right; x++) for (const y of [top, bottom]) points.set(`${x},${y}`, [x, y]);
|
|
1204
|
+
for (let y = top; y <= bottom; y++) for (const x of [left, right]) points.set(`${x},${y}`, [x, y]);
|
|
1205
|
+
return [...points.values()];
|
|
1206
|
+
}
|
|
1207
|
+
const BAYER4 = [
|
|
1208
|
+
[
|
|
1209
|
+
0,
|
|
1210
|
+
8,
|
|
1211
|
+
2,
|
|
1212
|
+
10
|
|
1213
|
+
],
|
|
1214
|
+
[
|
|
1215
|
+
12,
|
|
1216
|
+
4,
|
|
1217
|
+
14,
|
|
1218
|
+
6
|
|
1219
|
+
],
|
|
1220
|
+
[
|
|
1221
|
+
3,
|
|
1222
|
+
11,
|
|
1223
|
+
1,
|
|
1224
|
+
9
|
|
1225
|
+
],
|
|
1226
|
+
[
|
|
1227
|
+
15,
|
|
1228
|
+
7,
|
|
1229
|
+
13,
|
|
1230
|
+
5
|
|
1231
|
+
]
|
|
1232
|
+
];
|
|
1233
|
+
const BAYER2 = [[0, 2], [3, 1]];
|
|
1234
|
+
/** The ordered-dither threshold in [0, 1) for a cell. */
|
|
1235
|
+
function ditherThreshold(x, y, size = 4) {
|
|
1236
|
+
return size === 4 ? (BAYER4[y & 3][x & 3] + .5) / 16 : (BAYER2[y & 1][x & 1] + .5) / 4;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* A linear gradient over the 4-connected region of the start cell: each cell's position along
|
|
1240
|
+
* `from`→`to` steps through the tones between `fromTone` and `toTone`, with ordered dithering
|
|
1241
|
+
* between neighboring tones. No RGB is blended; the result is ordinary cells.
|
|
1242
|
+
*/
|
|
1243
|
+
function gradientPartLayer(slot, layer, from, to, color, fromTone, toTone, options = {}) {
|
|
1244
|
+
const { width, height } = PART_SLOTS[slot];
|
|
1245
|
+
const out = layer.map((row) => [...row]);
|
|
1246
|
+
const [fx, fy] = from;
|
|
1247
|
+
const region = (sx, sy) => {
|
|
1248
|
+
if (sx < 0 || sx >= width || sy < 0 || sy >= height) return [];
|
|
1249
|
+
const target = layer[sy][sx];
|
|
1250
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1251
|
+
const cells = [];
|
|
1252
|
+
const stack = [[sx, sy]];
|
|
1253
|
+
while (stack.length) {
|
|
1254
|
+
const [x, y] = stack.pop();
|
|
1255
|
+
if (x < 0 || x >= width || y < 0 || y >= height || seen.has(y * width + x)) continue;
|
|
1256
|
+
if (!same(layer[y][x], target)) continue;
|
|
1257
|
+
seen.add(y * width + x);
|
|
1258
|
+
cells.push([x, y]);
|
|
1259
|
+
stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
|
|
1260
|
+
}
|
|
1261
|
+
return cells;
|
|
1262
|
+
};
|
|
1263
|
+
const paint = (cells, start, end, ditherX) => {
|
|
1264
|
+
const vx = end[0] - start[0];
|
|
1265
|
+
const vy = end[1] - start[1];
|
|
1266
|
+
const length = vx * vx + vy * vy;
|
|
1267
|
+
const steps = toTone - fromTone;
|
|
1268
|
+
for (const [x, y] of cells) {
|
|
1269
|
+
const position = (length === 0 ? 0 : Math.min(1, Math.max(0, ((x - start[0]) * vx + (y - start[1]) * vy) / length))) * Math.abs(steps);
|
|
1270
|
+
const base = Math.floor(position);
|
|
1271
|
+
const up = position - base > ditherThreshold(ditherX(x), y, options.dither ?? 4) ? 1 : 0;
|
|
1272
|
+
const tone = fromTone + Math.sign(steps) * Math.min(Math.abs(steps), base + up);
|
|
1273
|
+
out[y][x] = {
|
|
1274
|
+
color,
|
|
1275
|
+
tone
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
const own = region(fx, fy);
|
|
1280
|
+
if (!options.mirror) {
|
|
1281
|
+
paint(own, from, to, (x) => x);
|
|
1282
|
+
return out;
|
|
1283
|
+
}
|
|
1284
|
+
const mx = mirrorPartX(slot, fx);
|
|
1285
|
+
const mirroredCells = region(mx, fy);
|
|
1286
|
+
const inOwn = new Set(own.map(([x, y]) => y * width + x));
|
|
1287
|
+
const inMirrored = new Set(mirroredCells.map(([x, y]) => y * width + x));
|
|
1288
|
+
const startSide = (x) => x < width / 2 === fx < width / 2;
|
|
1289
|
+
paint(own.filter(([x, y]) => startSide(x) || !inMirrored.has(y * width + x)), from, to, (x) => x);
|
|
1290
|
+
paint(mirroredCells.filter(([x, y]) => !startSide(x) || !inOwn.has(y * width + x)), [mx, fy], [mirrorPartX(slot, to[0]), to[1]], (x) => mirrorPartX(slot, x));
|
|
1291
|
+
return out;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Moves the tone of colored cells by ±1 at random, for texture. The same seed gives the same
|
|
1295
|
+
* result; the seed is used only now, and the result is ordinary cells. With `mirror`, each cell
|
|
1296
|
+
* and its mirror get the same change.
|
|
1297
|
+
*/
|
|
1298
|
+
function noisePartLayer(slot, layer, amount, seed, mirror = false) {
|
|
1299
|
+
const random = seededRandom(seed);
|
|
1300
|
+
const rolls = layer.map((row) => row.map(() => [random(), random() < .5 ? -1 : 1]));
|
|
1301
|
+
return layer.map((row, y) => row.map((ink, x) => {
|
|
1302
|
+
const source = mirror ? Math.min(x, mirrorPartX(slot, x)) : x;
|
|
1303
|
+
const [roll, direction] = rolls[y][source];
|
|
1304
|
+
if (!ink || roll >= amount) return ink;
|
|
1305
|
+
return {
|
|
1306
|
+
color: ink.color,
|
|
1307
|
+
tone: Math.max(-2, Math.min(2, ink.tone + direction))
|
|
1308
|
+
};
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
/** Moves the tone of each colored cell on the points by `delta` once; empty cells are skipped. */
|
|
1312
|
+
function shadePartLayer(slot, layer, points, delta, mirror = false) {
|
|
1313
|
+
const { width, height } = PART_SLOTS[slot];
|
|
1314
|
+
const out = layer.map((row) => [...row]);
|
|
1315
|
+
const done = /* @__PURE__ */ new Set();
|
|
1316
|
+
for (const [x, y] of points) for (const px of mirror ? [x, mirrorPartX(slot, x)] : [x]) {
|
|
1317
|
+
const key = `${px},${y}`;
|
|
1318
|
+
if (done.has(key) || px < 0 || px >= width || y < 0 || y >= height) continue;
|
|
1319
|
+
done.add(key);
|
|
1320
|
+
const ink = layer[y][px];
|
|
1321
|
+
if (ink) out[y][px] = {
|
|
1322
|
+
color: ink.color,
|
|
1323
|
+
tone: Math.max(-2, Math.min(2, ink.tone + delta))
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
return out;
|
|
1327
|
+
}
|
|
1083
1328
|
const K = Uint32Array.from("428a2f98 71374491 b5c0fbcf e9b5dba5 3956c25b 59f111f1 923f82a4 ab1c5ed5 d807aa98 12835b01 243185be 550c7dc3 72be5d74 80deb1fe 9bdc06a7 c19bf174 e49b69c1 efbe4786 0fc19dc6 240ca1cc 2de92c6f 4a7484aa 5cb0a9dc 76f988da 983e5152 a831c66d b00327c8 bf597fc7 c6e00bf3 d5a79147 06ca6351 14292967 27b70a85 2e1b2138 4d2c6dfc 53380d13 650a7354 766a0abb 81c2c92e 92722c85 a2bfe8a1 a81a664b c24b8b70 c76c51a3 d192e819 d6990624 f40e3585 106aa070 19a4c116 1e376c08 2748774c 34b0bcb5 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3 748f82ee 78a5636f 84c87814 8cc70208 90befffa a4506ceb bef9a3f7 c67178f2".split(" ").map((h) => Number.parseInt(h, 16)));
|
|
1084
1329
|
function sha256(text) {
|
|
1085
1330
|
const bytes = new TextEncoder().encode(text);
|
|
@@ -1483,6 +1728,39 @@ function hair(recipe, s, b) {
|
|
|
1483
1728
|
};
|
|
1484
1729
|
}
|
|
1485
1730
|
}
|
|
1731
|
+
const STRANDS = {
|
|
1732
|
+
ahoge: {
|
|
1733
|
+
rows: [
|
|
1734
|
+
"XX.",
|
|
1735
|
+
".XX",
|
|
1736
|
+
".XX"
|
|
1737
|
+
],
|
|
1738
|
+
dx: 0
|
|
1739
|
+
},
|
|
1740
|
+
curl: {
|
|
1741
|
+
rows: [
|
|
1742
|
+
".XX.",
|
|
1743
|
+
"X..X",
|
|
1744
|
+
"..XX",
|
|
1745
|
+
".X.."
|
|
1746
|
+
],
|
|
1747
|
+
dx: -1
|
|
1748
|
+
},
|
|
1749
|
+
double: {
|
|
1750
|
+
rows: [
|
|
1751
|
+
"X...X",
|
|
1752
|
+
".X.X.",
|
|
1753
|
+
".X.X."
|
|
1754
|
+
],
|
|
1755
|
+
dx: -2
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
function strandMask(style, s) {
|
|
1759
|
+
const { rows, dx } = STRANDS[style];
|
|
1760
|
+
const top = 4 - rows.length;
|
|
1761
|
+
const left = Math.round(C + s) + dx;
|
|
1762
|
+
return (x, y) => rows[y - top]?.[x - left] === "X";
|
|
1763
|
+
}
|
|
1486
1764
|
function detailedHair(recipe, s, b) {
|
|
1487
1765
|
const of = (style) => hair({
|
|
1488
1766
|
...recipe,
|
|
@@ -1497,8 +1775,11 @@ function detailedHair(recipe, s, b) {
|
|
|
1497
1775
|
const sideOf = (sign) => (x, y) => y > 7 && sign * (x + .5 - C - s) >= 7 && lengthen((sign < 0 ? side : right).front)(x, y);
|
|
1498
1776
|
const fringe = (x, y) => y <= 7 ? bangs.front(x, y) || back.front(x, y) : Math.abs(x + .5 - C - s) < 7 && bangs.front(x, y);
|
|
1499
1777
|
const behind = length === 0 ? back.back : lengthen(back.back);
|
|
1778
|
+
const tuft = recipe.strand && recipe.bangs !== "none" ? strandMask(recipe.strand, s) : void 0;
|
|
1779
|
+
const strand = tuft ? (x, y) => tuft(x, y) && !fringe(x, y) && !sideOf(-1)(x, y) && !sideOf(1)(x, y) : void 0;
|
|
1500
1780
|
return {
|
|
1501
|
-
front: (x, y) => fringe(x, y) || sideOf(-1)(x, y) || sideOf(1)(x, y),
|
|
1781
|
+
front: (x, y) => fringe(x, y) || sideOf(-1)(x, y) || sideOf(1)(x, y) || (strand?.(x, y) ?? false),
|
|
1782
|
+
...strand ? { strand } : {},
|
|
1502
1783
|
sides: [sideOf(-1), sideOf(1)],
|
|
1503
1784
|
pieces: {
|
|
1504
1785
|
bangs: fringe,
|
|
@@ -1843,6 +2124,87 @@ function accessory(recipe, g, s, face) {
|
|
|
1843
2124
|
return;
|
|
1844
2125
|
}
|
|
1845
2126
|
}
|
|
2127
|
+
/**
|
|
2128
|
+
* A built-in headpiece: the row that meets the head goes behind the hair, the rest in front.
|
|
2129
|
+
* Turned, the far side of a pair is hidden, like a turned ear.
|
|
2130
|
+
*/
|
|
2131
|
+
function drawHeadpiece(style, recipe, behind, front, s, far) {
|
|
2132
|
+
const cx = Math.round(C + s);
|
|
2133
|
+
const pair = (rows, left, right, y, pal, mirror = true) => {
|
|
2134
|
+
const tips = rows.slice(0, -1);
|
|
2135
|
+
if (far >= 0) {
|
|
2136
|
+
sprite(behind, rows, cx + left, y, pal);
|
|
2137
|
+
sprite(front, tips, cx + left, y, pal);
|
|
2138
|
+
}
|
|
2139
|
+
if (far <= 0) {
|
|
2140
|
+
sprite(behind, rows, cx + right, y, pal, mirror);
|
|
2141
|
+
sprite(front, tips, cx + right, y, pal, mirror);
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
switch (style) {
|
|
2145
|
+
case "catears":
|
|
2146
|
+
pair([
|
|
2147
|
+
"X...",
|
|
2148
|
+
"XX..",
|
|
2149
|
+
"XPX.",
|
|
2150
|
+
"XXXX"
|
|
2151
|
+
], -9, 5, 2, {
|
|
2152
|
+
X: recipe.hairColor,
|
|
2153
|
+
P: "#f4a3b5"
|
|
2154
|
+
});
|
|
2155
|
+
return;
|
|
2156
|
+
case "bunnyears":
|
|
2157
|
+
pair([
|
|
2158
|
+
"WW",
|
|
2159
|
+
"WP",
|
|
2160
|
+
"WP",
|
|
2161
|
+
"WP",
|
|
2162
|
+
"WW"
|
|
2163
|
+
], -6, 4, 0, {
|
|
2164
|
+
W: "#f4f1ec",
|
|
2165
|
+
P: "#f4a3b5"
|
|
2166
|
+
});
|
|
2167
|
+
return;
|
|
2168
|
+
case "horseears":
|
|
2169
|
+
pair([
|
|
2170
|
+
".X.",
|
|
2171
|
+
"XPX",
|
|
2172
|
+
"XPX",
|
|
2173
|
+
"XXX"
|
|
2174
|
+
], -9, 6, 1, {
|
|
2175
|
+
X: shade(recipe.hairColor, .9),
|
|
2176
|
+
P: "#f4a3b5"
|
|
2177
|
+
}, false);
|
|
2178
|
+
return;
|
|
2179
|
+
case "horns":
|
|
2180
|
+
pair([
|
|
2181
|
+
".H",
|
|
2182
|
+
"HH",
|
|
2183
|
+
"Hh"
|
|
2184
|
+
], -8, 6, 1, {
|
|
2185
|
+
H: "#5b3a6e",
|
|
2186
|
+
h: "#7d5694"
|
|
2187
|
+
});
|
|
2188
|
+
return;
|
|
2189
|
+
case "halo": {
|
|
2190
|
+
const ring = (x, y) => ellipse(C + s, 1.6, 6.5, 1.8)(x, y) && !ellipse(C + s, 1.6, 4.4, .8)(x, y);
|
|
2191
|
+
paint(behind, (x, y) => ring(x, y) && y < 2, GOLD);
|
|
2192
|
+
paint(front, (x, y) => ring(x, y) && y >= 2, GOLD);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
case "wings":
|
|
2196
|
+
pair([
|
|
2197
|
+
"W..",
|
|
2198
|
+
"WW.",
|
|
2199
|
+
"WwW",
|
|
2200
|
+
".ww"
|
|
2201
|
+
], -13, 10, 7, {
|
|
2202
|
+
W: "#f4f1ec",
|
|
2203
|
+
w: "#cfd6e2"
|
|
2204
|
+
});
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
1846
2208
|
const POINTED_EARS = {
|
|
1847
2209
|
goblin: {
|
|
1848
2210
|
rows: [
|
|
@@ -2029,7 +2391,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2029
2391
|
};
|
|
2030
2392
|
const masks = drawnHair.length === 0 ? builtin : {
|
|
2031
2393
|
...builtin,
|
|
2032
|
-
front: (x, y) => piece("bangs")(x, y) || piece("leftSideHair")(x, y) || piece("rightSideHair")(x, y),
|
|
2394
|
+
front: (x, y) => piece("bangs")(x, y) || piece("leftSideHair")(x, y) || piece("rightSideHair")(x, y) || (builtin.strand?.(x, y) ?? false),
|
|
2033
2395
|
sides: [piece("leftSideHair"), piece("rightSideHair")],
|
|
2034
2396
|
back: piece("backHair"),
|
|
2035
2397
|
...drawnHair.some((drawn) => drawn.slot === "backHair") ? { ties: [] } : {}
|
|
@@ -2068,6 +2430,9 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2068
2430
|
placeCells(back, inked("backHair"), false);
|
|
2069
2431
|
overlay(back, "petals");
|
|
2070
2432
|
if (headpiece) placeCells(back, headpiece.back);
|
|
2433
|
+
const wornHead = flower ? void 0 : builtInHeadpiece(recipe);
|
|
2434
|
+
const headFront = blank();
|
|
2435
|
+
if (wornHead) drawHeadpiece(wornHead, recipe, back, headFront, s, turned ? d : 0);
|
|
2071
2436
|
const head = blank();
|
|
2072
2437
|
paint(head, face, skin);
|
|
2073
2438
|
const h = recipe.height ?? 0;
|
|
@@ -2119,8 +2484,10 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2119
2484
|
const shadeHair = (g) => {
|
|
2120
2485
|
const out = g.map((r) => [...r]);
|
|
2121
2486
|
const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
|
|
2122
|
-
const
|
|
2123
|
-
const
|
|
2487
|
+
const framed = cells.filter(([x, y]) => !builtin.strand?.(x, y));
|
|
2488
|
+
const bounds = framed.length ? framed : cells;
|
|
2489
|
+
const xs = bounds.map(([x]) => x);
|
|
2490
|
+
const ys = bounds.map(([, y]) => y);
|
|
2124
2491
|
const left = Math.min(...xs);
|
|
2125
2492
|
const right = Math.max(...xs);
|
|
2126
2493
|
const top = Math.min(...ys);
|
|
@@ -2149,7 +2516,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2149
2516
|
}
|
|
2150
2517
|
const ring = mix$1(hairColor, WHITE, .45);
|
|
2151
2518
|
for (let x = Math.round(C + s) - 7; x <= Math.round(C + s) + 3; x++) {
|
|
2152
|
-
const first = g.findIndex((row) => row[x] === hairColor);
|
|
2519
|
+
const first = g.findIndex((row, y) => row[x] === hairColor && !builtin.strand?.(x, y));
|
|
2153
2520
|
if (first < 0) continue;
|
|
2154
2521
|
const y = first + 2 + Math.round(Math.abs(x + .5 - (C + s - 2)) / 4);
|
|
2155
2522
|
if (g[y]?.[x] === hairColor && x % 3 !== 0) out[y][x] = ring;
|
|
@@ -2162,6 +2529,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2162
2529
|
const crown = blank();
|
|
2163
2530
|
placeCells(crown, inked("all-front"), false);
|
|
2164
2531
|
if (headpiece) placeCells(crown, headpiece.front);
|
|
2532
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (headFront[y][x]) crown[y][x] = headFront[y][x];
|
|
2165
2533
|
const figure = blank();
|
|
2166
2534
|
for (const layer of [
|
|
2167
2535
|
back,
|
|
@@ -2299,17 +2667,42 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2299
2667
|
], right.x + right.w, eyeTop + 5, { G: GOLD });
|
|
2300
2668
|
}
|
|
2301
2669
|
const full = clip(outline(figure));
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
const
|
|
2305
|
-
if (
|
|
2670
|
+
const variants = /* @__PURE__ */ new Map();
|
|
2671
|
+
const recolor = (color) => {
|
|
2672
|
+
const cached = variants.get(color.toLowerCase());
|
|
2673
|
+
if (cached) return cached;
|
|
2306
2674
|
const variant = blank();
|
|
2675
|
+
variants.set(color.toLowerCase(), variant);
|
|
2307
2676
|
const plain = {
|
|
2308
2677
|
...recipe,
|
|
2309
2678
|
hairColor: color
|
|
2310
2679
|
};
|
|
2311
|
-
for (const k of AVATAR_PIECE_COLORS) delete plain[k];
|
|
2680
|
+
for (const k of [...AVATAR_PIECE_COLORS, ...AVATAR_PIECE_COLORS_V4]) delete plain[k];
|
|
2312
2681
|
for (const cell of renderFigure(plain, yawDeg, {}, omit).cells) variant[cell.y][cell.x] = cell.c;
|
|
2682
|
+
return variant;
|
|
2683
|
+
};
|
|
2684
|
+
if (recipe.assetVersion === 4) {
|
|
2685
|
+
const over = (x, y) => !!crown[y]?.[x] || !!drawnOver[y]?.[x] || !!eyes[y]?.[x] || !!glasses[y]?.[x];
|
|
2686
|
+
const hairAt = (x, y) => head[y]?.[x] === hairColor && !over(x, y);
|
|
2687
|
+
const backAt = (x, y) => back[y]?.[x] !== void 0 && figure[y][x] === back[y][x];
|
|
2688
|
+
const owners = [
|
|
2689
|
+
[flower ? void 0 : recipe.backHairColor, (x, y) => masks.back(x, y) && backAt(x, y)],
|
|
2690
|
+
[recipe.leftSideHairColor, (x, y) => !!masks.sides?.[0](x, y) && (hairAt(x, y) || backAt(x, y))],
|
|
2691
|
+
[recipe.rightSideHairColor, (x, y) => !!masks.sides?.[1](x, y) && (hairAt(x, y) || backAt(x, y))],
|
|
2692
|
+
[recipe.bangsColor, (x, y) => (builtin.pieces ? piece("bangs")(x, y) : false) && hairAt(x, y)],
|
|
2693
|
+
[recipe.strandColor, (x, y) => (builtin.strand?.(x, y) ?? false) && hairAt(x, y)]
|
|
2694
|
+
];
|
|
2695
|
+
for (const [color, owns] of owners) {
|
|
2696
|
+
if (color === void 0 || color.toLowerCase() === hairColor.toLowerCase()) continue;
|
|
2697
|
+
const variant = recolor(color);
|
|
2698
|
+
const near = (x, y) => owns(x - 1, y) || owns(x + 1, y) || owns(x, y - 1) || owns(x, y + 1);
|
|
2699
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (full[y][x] && !over(x, y) && (owns(x, y) || !figure[y][x] && near(x, y))) full[y][x] = variant[y][x];
|
|
2700
|
+
}
|
|
2701
|
+
} else AVATAR_PIECE_COLORS.forEach((key, i) => {
|
|
2702
|
+
const color = recipe[key];
|
|
2703
|
+
const mask = masks.sides?.[i];
|
|
2704
|
+
if (color === void 0 || !mask || color.toLowerCase() === hairColor.toLowerCase()) return;
|
|
2705
|
+
const variant = recolor(color);
|
|
2313
2706
|
const near = (x, y) => mask(x, y) || mask(x - 1, y) || mask(x + 1, y) || mask(x, y - 1) || mask(x, y + 1);
|
|
2314
2707
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (full[y][x] && near(x, y) && !eyes[y][x] && !glasses[y][x]) full[y][x] = variant[y][x];
|
|
2315
2708
|
});
|
|
@@ -2384,6 +2777,57 @@ function hairPieceStart(recipe, slot) {
|
|
|
2384
2777
|
back: []
|
|
2385
2778
|
};
|
|
2386
2779
|
}
|
|
2780
|
+
function cellSource(recipe) {
|
|
2781
|
+
return (c) => {
|
|
2782
|
+
for (const key of AVATAR_COLORS) for (const tone of [
|
|
2783
|
+
0,
|
|
2784
|
+
-1,
|
|
2785
|
+
1,
|
|
2786
|
+
-2,
|
|
2787
|
+
2
|
|
2788
|
+
]) if (TONES[tone](recipe[key].toLowerCase()) === c) return [key, tone];
|
|
2789
|
+
return [c, 0];
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* The headpiece as a Custom Part to start drawing from: the drawn headpiece worn, or the
|
|
2794
|
+
* built-in headpiece flattened for this recipe in the front pose, its base on the back layer and
|
|
2795
|
+
* the rest on the front layer, so an unchanged copy renders identically facing front.
|
|
2796
|
+
*/
|
|
2797
|
+
function headpieceStart(recipe) {
|
|
2798
|
+
const worn = wornPart(recipe, "headpiece");
|
|
2799
|
+
if (worn) return {
|
|
2800
|
+
slot: "headpiece",
|
|
2801
|
+
front: [...worn.front],
|
|
2802
|
+
back: [...worn.back]
|
|
2803
|
+
};
|
|
2804
|
+
const style = builtInHeadpiece(recipe);
|
|
2805
|
+
const behind = blank();
|
|
2806
|
+
const over = blank();
|
|
2807
|
+
if (style) drawHeadpiece(style, recipe, behind, over, 0, 0);
|
|
2808
|
+
const source = cellSource(recipe);
|
|
2809
|
+
const front = [];
|
|
2810
|
+
const back = [];
|
|
2811
|
+
for (let y = 0; y < 16; y++) for (let x = 0; x < N; x++) {
|
|
2812
|
+
const c = over[y][x];
|
|
2813
|
+
const b = behind[y][x];
|
|
2814
|
+
if (c) front.push([
|
|
2815
|
+
x,
|
|
2816
|
+
y,
|
|
2817
|
+
...source(c)
|
|
2818
|
+
]);
|
|
2819
|
+
if (b && b !== c) back.push([
|
|
2820
|
+
x,
|
|
2821
|
+
y,
|
|
2822
|
+
...source(b)
|
|
2823
|
+
]);
|
|
2824
|
+
}
|
|
2825
|
+
return {
|
|
2826
|
+
slot: "headpiece",
|
|
2827
|
+
front,
|
|
2828
|
+
back
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2387
2831
|
/**
|
|
2388
2832
|
* The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
|
|
2389
2833
|
* there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
|
|
@@ -2401,16 +2845,7 @@ function replacePartStart(recipe, slot) {
|
|
|
2401
2845
|
const layer = slot === "glasses" ? "glasses" : slot === "nose" || slot === "cheeks" ? "features" : slot === "petals" ? "back" : slot === "outfit" || slot === "flowerBase" ? "body" : "figure";
|
|
2402
2846
|
const shown = renderFigure(recipe, 0, {}, /* @__PURE__ */ new Set()).grids[layer];
|
|
2403
2847
|
const without = renderFigure(recipe, 0, {}, new Set([slot])).grids[layer];
|
|
2404
|
-
const source = (
|
|
2405
|
-
for (const key of AVATAR_COLORS) for (const tone of [
|
|
2406
|
-
0,
|
|
2407
|
-
-1,
|
|
2408
|
-
1,
|
|
2409
|
-
-2,
|
|
2410
|
-
2
|
|
2411
|
-
]) if (TONES[tone](recipe[key].toLowerCase()) === c) return [key, tone];
|
|
2412
|
-
return [c, 0];
|
|
2413
|
-
};
|
|
2848
|
+
const source = cellSource(recipe);
|
|
2414
2849
|
const front = [];
|
|
2415
2850
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
2416
2851
|
const c = shown[y][x];
|
|
@@ -2949,5 +3384,5 @@ function symbolArtCells(rows, color) {
|
|
|
2949
3384
|
}
|
|
2950
3385
|
|
|
2951
3386
|
//#endregion
|
|
2952
|
-
export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_SWATCHES, AVATAR_TURNS, CUSTOM_PART_KEYS, DEFAULT_RECIPE, HAIR_PART_SLOTS, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, REPLACE_PART_SLOTS, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
|
|
3387
|
+
export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_HEADPIECES, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PIECE_COLORS_V4, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_STRANDS, AVATAR_SWATCHES, AVATAR_TURNS, CUSTOM_PART_KEYS, DEFAULT_RECIPE, HAIR_PART_SLOTS, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, REPLACE_PART_SLOTS, builtInHeadpiece, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, headpieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withBuiltInHeadpiece, withCustomPart, withHeadpiece, withPieces, withSpecies, wornPart };
|
|
2953
3388
|
//# sourceMappingURL=index.js.map
|