@originkit/avatars 0.0.0-stage → 0.2.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/GUIDE.md +44 -0
- package/LICENSE +58 -0
- package/README.md +141 -2
- package/bin/avatars.js +65 -0
- package/dist/art.d.ts +43 -0
- package/dist/art.js +35 -0
- package/dist/auras/calming.d.ts +18 -0
- package/dist/auras/calming.js +30 -0
- package/dist/auras/controls.d.ts +2 -0
- package/dist/auras/controls.js +60 -0
- package/dist/auras/index.d.ts +119 -0
- package/dist/auras/index.js +355 -0
- package/dist/auras/skies.d.ts +13 -0
- package/dist/auras/skies.js +22 -0
- package/dist/blobs/color.d.ts +19 -0
- package/dist/blobs/color.js +113 -0
- package/dist/blobs/controls.d.ts +2 -0
- package/dist/blobs/controls.js +77 -0
- package/dist/blobs/index.d.ts +5 -0
- package/dist/blobs/index.js +107 -0
- package/dist/blobs/paths.d.ts +33 -0
- package/dist/blobs/paths.js +102 -0
- package/dist/blobs/shapes.d.ts +46 -0
- package/dist/blobs/shapes.js +186 -0
- package/dist/blobs/variance.d.ts +22 -0
- package/dist/blobs/variance.js +42 -0
- package/dist/bots/attachments.d.ts +24 -0
- package/dist/bots/attachments.js +119 -0
- package/dist/bots/calming.d.ts +4 -0
- package/dist/bots/calming.js +19 -0
- package/dist/bots/controls.d.ts +2 -0
- package/dist/bots/controls.js +78 -0
- package/dist/bots/faces.d.ts +23 -0
- package/dist/bots/faces.js +122 -0
- package/dist/bots/heads.d.ts +15 -0
- package/dist/bots/heads.js +66 -0
- package/dist/bots/index.d.ts +5 -0
- package/dist/bots/index.js +47 -0
- package/dist/bots/palette.d.ts +15 -0
- package/dist/bots/palette.js +30 -0
- package/dist/bots/paths.d.ts +25 -0
- package/dist/bots/paths.js +89 -0
- package/dist/bots/trim.d.ts +12 -0
- package/dist/bots/trim.js +37 -0
- package/dist/bubbles/calming.d.ts +7 -0
- package/dist/bubbles/calming.js +19 -0
- package/dist/bubbles/controls.d.ts +2 -0
- package/dist/bubbles/controls.js +69 -0
- package/dist/bubbles/index.d.ts +65 -0
- package/dist/bubbles/index.js +279 -0
- package/dist/buddies/calming.d.ts +8 -0
- package/dist/buddies/calming.js +18 -0
- package/dist/buddies/controls.d.ts +2 -0
- package/dist/buddies/controls.js +35 -0
- package/dist/buddies/index.d.ts +113 -0
- package/dist/buddies/index.js +363 -0
- package/dist/colors.d.ts +20 -0
- package/dist/colors.js +41 -0
- package/dist/combinations.d.ts +9 -0
- package/dist/combinations.js +28 -0
- package/dist/container.d.ts +14 -0
- package/dist/container.js +67 -0
- package/dist/controls.d.ts +125 -0
- package/dist/controls.js +220 -0
- package/dist/critters/calming.d.ts +8 -0
- package/dist/critters/calming.js +18 -0
- package/dist/critters/controls.d.ts +2 -0
- package/dist/critters/controls.js +49 -0
- package/dist/critters/index.d.ts +113 -0
- package/dist/critters/index.js +307 -0
- package/dist/doodles/calming.d.ts +7 -0
- package/dist/doodles/calming.js +18 -0
- package/dist/doodles/controls.d.ts +2 -0
- package/dist/doodles/controls.js +50 -0
- package/dist/doodles/index.d.ts +66 -0
- package/dist/doodles/index.js +236 -0
- package/dist/faces/adventurer-neutral.json +3168 -0
- package/dist/faces/calming.d.ts +8 -0
- package/dist/faces/calming.js +20 -0
- package/dist/faces/controls.d.ts +2 -0
- package/dist/faces/controls.js +38 -0
- package/dist/faces/index.d.ts +44 -0
- package/dist/faces/index.js +108 -0
- package/dist/glances/calming.d.ts +8 -0
- package/dist/glances/calming.js +16 -0
- package/dist/glances/controls.d.ts +2 -0
- package/dist/glances/controls.js +27 -0
- package/dist/glances/index.d.ts +54 -0
- package/dist/glances/index.js +79 -0
- package/dist/glyphs/calming.d.ts +8 -0
- package/dist/glyphs/calming.js +17 -0
- package/dist/glyphs/controls.d.ts +2 -0
- package/dist/glyphs/controls.js +55 -0
- package/dist/glyphs/index.d.ts +62 -0
- package/dist/glyphs/index.js +197 -0
- package/dist/grins/calming.d.ts +8 -0
- package/dist/grins/calming.js +21 -0
- package/dist/grins/controls.d.ts +2 -0
- package/dist/grins/controls.js +33 -0
- package/dist/grins/index.d.ts +81 -0
- package/dist/grins/index.js +240 -0
- package/dist/gummies/art.d.ts +77 -0
- package/dist/gummies/art.js +168 -0
- package/dist/gummies/calming.d.ts +8 -0
- package/dist/gummies/calming.js +21 -0
- package/dist/gummies/controls.d.ts +2 -0
- package/dist/gummies/controls.js +53 -0
- package/dist/gummies/index.d.ts +37 -0
- package/dist/gummies/index.js +283 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +48 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +16 -0
- package/dist/kinds.d.ts +30 -0
- package/dist/kinds.js +266 -0
- package/dist/kitties/calming.d.ts +8 -0
- package/dist/kitties/calming.js +19 -0
- package/dist/kitties/controls.d.ts +2 -0
- package/dist/kitties/controls.js +43 -0
- package/dist/kitties/index.d.ts +181 -0
- package/dist/kitties/index.js +673 -0
- package/dist/knits/calming.d.ts +7 -0
- package/dist/knits/calming.js +19 -0
- package/dist/knits/controls.d.ts +2 -0
- package/dist/knits/controls.js +32 -0
- package/dist/knits/index.d.ts +74 -0
- package/dist/knits/index.js +445 -0
- package/dist/mallows/calming.d.ts +8 -0
- package/dist/mallows/calming.js +19 -0
- package/dist/mallows/controls.d.ts +2 -0
- package/dist/mallows/controls.js +45 -0
- package/dist/mallows/index.d.ts +111 -0
- package/dist/mallows/index.js +454 -0
- package/dist/peeks/calming.d.ts +8 -0
- package/dist/peeks/calming.js +21 -0
- package/dist/peeks/controls.d.ts +2 -0
- package/dist/peeks/controls.js +54 -0
- package/dist/peeks/index.d.ts +80 -0
- package/dist/peeks/index.js +225 -0
- package/dist/pixels/calming.d.ts +8 -0
- package/dist/pixels/calming.js +22 -0
- package/dist/pixels/controls.d.ts +3 -0
- package/dist/pixels/controls.js +82 -0
- package/dist/pixels/index.d.ts +34 -0
- package/dist/pixels/index.js +174 -0
- package/dist/pixels/palette.d.ts +29 -0
- package/dist/pixels/palette.js +107 -0
- package/dist/pixels/sprites.d.ts +42 -0
- package/dist/pixels/sprites.js +154 -0
- package/dist/possibilities.d.ts +39 -0
- package/dist/possibilities.js +88 -0
- package/dist/shapes/calming.d.ts +7 -0
- package/dist/shapes/calming.js +19 -0
- package/dist/shapes/controls.d.ts +2 -0
- package/dist/shapes/controls.js +41 -0
- package/dist/shapes/index.d.ts +44 -0
- package/dist/shapes/index.js +104 -0
- package/dist/share.d.ts +21 -0
- package/dist/share.js +83 -0
- package/dist/traits.d.ts +34 -0
- package/dist/traits.js +58 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/variants.d.ts +13 -0
- package/dist/variants.js +43 -0
- package/mcp/server.js +7 -0
- package/mcp/tools.js +70 -0
- package/package.json +68 -4
- package/react/index.d.ts +28 -0
- package/react/index.js +869 -0
- package/react/styles.js +1 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buddies: sticker cats in a thick, warm outline and flat colour. Two characters, a striped
|
|
3
|
+
* tabby and a calico, each traced from its own reference, and both sharing the same
|
|
4
|
+
* expressions and gestures.
|
|
5
|
+
*
|
|
6
|
+
* The hands and the shades are traced too, each from the reference that has them: the
|
|
7
|
+
* tabby's paws to its cheeks, the calico's paw at its sunglasses, and a salute traced from a
|
|
8
|
+
* croc's. Every character uses those same drawings, placed on its own face, and the other gestures are
|
|
9
|
+
* drawn with the calico's paw, so all the hands are the same hands.
|
|
10
|
+
*/
|
|
11
|
+
import { mix, WHITE } from "../colors.js";
|
|
12
|
+
import { scopeAll } from "../art.js";
|
|
13
|
+
import { calmed } from "../possibilities.js";
|
|
14
|
+
import { createTraits } from "../traits.js";
|
|
15
|
+
import { CALMING } from "./calming.js";
|
|
16
|
+
/** Solid cards. */
|
|
17
|
+
export const TILES = ["#ffffff", "#fff3d6", "#ffe0e6", "#dff1ff", "#e3f7dc", "#efe6ff", "#ffe8cf", "#fff8b8", "#d9f5f0"];
|
|
18
|
+
export const INK = "#1f1a17";
|
|
19
|
+
const PINK = "#f2896a";
|
|
20
|
+
const BLUSH = "#ff9aa2";
|
|
21
|
+
const TONGUE = "#ff7a93";
|
|
22
|
+
const HEART = "#ef4b5f";
|
|
23
|
+
const LENS = "#2b211d";
|
|
24
|
+
const GLINT = "#6d6058";
|
|
25
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
26
|
+
const point = (x, y) => `${round2(x)} ${round2(y)}`;
|
|
27
|
+
const at = ([x, y]) => point(x, y);
|
|
28
|
+
/** A closed curve through every point, as a Catmull-Rom spline. */
|
|
29
|
+
function blob(points) {
|
|
30
|
+
const n = points.length;
|
|
31
|
+
let d = `M${at(points[0])}`;
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
const [p0, p1, p2, p3] = [points[(i + n - 1) % n], points[i], points[(i + 1) % n], points[(i + 2) % n]];
|
|
34
|
+
d += `C${point(p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6)} ${point(p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6)} ${at(p2)}`;
|
|
35
|
+
}
|
|
36
|
+
return `${d}Z`;
|
|
37
|
+
}
|
|
38
|
+
// The tabby's paws to its cheeks, each placed by the eye above it. Their outsides are the
|
|
39
|
+
// tabby's own sides, so on the tabby only the lines are drawn.
|
|
40
|
+
const CHEEK_PAWS = [
|
|
41
|
+
{
|
|
42
|
+
from: [43.4, 48.8],
|
|
43
|
+
inOutline: true,
|
|
44
|
+
lines: "M20.5 55Q22.6 52.4 26 51.2Q30.6 50 34.6 46.8Q36.8 44.6 38.6 44.4Q40.6 44.4 40.2 46.4Q39.4 49.4 37.6 51.4Q37.4 52.4 39 52.8Q41.6 53.6 43.6 54.8Q44.8 55.8 43.8 56.6Q41.6 57.4 38.6 57.8Q37 58.4 36.4 61.2Q35.8 64 33.2 66.4Q35.6 68 37.6 71.6Q39 75.6 39.2 80Q39 84 37.2 86.8",
|
|
45
|
+
fill: "M20.5 55Q22.6 52.4 26 51.2Q30.6 50 34.6 46.8Q36.8 44.6 38.6 44.4Q40.6 44.4 40.2 46.4Q39.4 49.4 37.6 51.4Q37.4 52.4 39 52.8Q41.6 53.6 43.6 54.8Q44.8 55.8 43.8 56.6Q41.6 57.4 38.6 57.8Q37 58.4 36.4 61.2Q35.8 64 33.2 66.4Q35.6 68 37.6 71.6Q39 75.6 39.2 80Q39 84 37.2 86.8L36.4 112L18.4 112Q18 100 16.6 92Q14.4 86 13.8 78Q13.8 70 16 65Q13.6 64 14.6 60Z",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
from: [55.7, 47.4],
|
|
49
|
+
inOutline: true,
|
|
50
|
+
lines: "M79.4 50.6Q74 48.6 67.4 48.2Q65.8 47.8 65.2 45.6Q64.4 42.2 62.6 41.2Q61.4 41 61.6 43Q62 46.4 62.4 48.6Q62.2 51.4 59.6 52.8Q57 54 56.4 55.6Q56.4 57.2 58.6 57.6Q62.4 57.8 64.4 57.8Q65.6 59.4 66 62Q66.4 63.8 68.6 65.2Q66.8 67.6 65.2 72Q64.4 76 64.8 80Q65.2 82.4 66 83.6",
|
|
51
|
+
fill: "M79.4 50.6Q74 48.6 67.4 48.2Q65.8 47.8 65.2 45.6Q64.4 42.2 62.6 41.2Q61.4 41 61.6 43Q62 46.4 62.4 48.6Q62.2 51.4 59.6 52.8Q57 54 56.4 55.6Q56.4 57.2 58.6 57.6Q62.4 57.8 64.4 57.8Q65.6 59.4 66 62Q66.4 63.8 68.6 65.2Q66.8 67.6 65.2 72Q64.4 76 64.8 80Q65.2 82.4 66 83.6L66 112L92 112Q91 100 89.6 93Q86.2 89.2 83.4 86.2Q86 85 87.6 82Q89 76 88 70Q86 65 82.6 61Q81 58 80.2 53Z",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
/** The outsides of those arms, for anyone whose sides aren't already the tabby's. */
|
|
55
|
+
const CHEEK_SIDES = [
|
|
56
|
+
"M14.6 60Q13.6 64 16 65Q13.8 70 13.8 78Q14.4 86 16.6 92Q18 100 18.4 112",
|
|
57
|
+
"M80.2 53Q81 58 82.6 61Q86 65 88 70Q89 76 87.6 82Q86 85 83.4 86.2Q86.2 89.2 89.6 93Q91 100 92 112",
|
|
58
|
+
];
|
|
59
|
+
const TABBY_CHIN = "M38.8 77.4Q47 78.2 54 76.8Q60 75.4 64.2 74.4";
|
|
60
|
+
// The calico's sunglasses, placed by the middle of its eyes: a heavy bar along the top, two
|
|
61
|
+
// wide lenses flat on top and round below, and a glint in each.
|
|
62
|
+
const SHADES_AT = [41.5, 47.5];
|
|
63
|
+
const LENSES = "M11 36.6Q20 35.6 30 36.6Q35 37.4 37.8 39.8Q37.6 47 34.6 51.6Q30.6 55 24 55.2Q17.4 54.6 14.2 49Q12 44 11 36.6Z" +
|
|
64
|
+
"M44.6 41Q46 40.2 52 39.4Q62 38.6 70 39.4Q73 39.8 74.2 41.2Q73.4 49 69.6 54.4Q65 59.6 58.8 60.2Q52 60 48.4 55.2Q45.4 50 44.6 41Z" +
|
|
65
|
+
"M37.6 39.6Q41 39.8 44.8 41.2L44.8 44.4Q41 43.2 37.4 43.4Z";
|
|
66
|
+
const SHADES_BAR = "M10.4 36Q22 34.4 37.6 38.6Q41 39.6 44.8 40.6Q60 38 74.4 40.2";
|
|
67
|
+
const SHADES_GLINT = "M14.6 38.6Q24 37.6 32.4 39.6M47.6 42.4Q58 41 70.4 42M13.4 40.6Q13.6 45 15.6 49";
|
|
68
|
+
/** The end of the bar, where the calico's paw holds them. */
|
|
69
|
+
const SHADES_END = [74.2, 41.2];
|
|
70
|
+
// The calico's paw at the shades, its arm curving down and round its side.
|
|
71
|
+
const SHADES_PAW = {
|
|
72
|
+
from: SHADES_END,
|
|
73
|
+
lines: "M73.6 44.8Q74 41.6 77.4 40.4Q82 39.4 86.6 42.6Q91.6 46.6 94 55Q95.8 63.6 95.4 72Q94.8 82 91.6 88.6Q88 93.6 82.6 94.4Q76 94 71 90Q68.6 88 67.6 86.6" +
|
|
74
|
+
"M73.6 44.8Q72.2 47 73.6 49Q75.6 50.4 74.6 52.6Q74.4 55.4 76.6 56.4Q78.2 57.6 77.4 60Q78 62.6 80.4 63.8Q82.6 64.8 84.6 65" +
|
|
75
|
+
"M82 65.4Q79.6 69 79.4 73Q79.4 77 79.8 80M75.4 48.2Q79 47 82.8 47.4M76.4 55.8Q78.6 54.2 80.6 53.8",
|
|
76
|
+
fill: "M73.6 44.8Q74 41.6 77.4 40.4Q82 39.4 86.6 42.6Q91.6 46.6 94 55Q95.8 63.6 95.4 72Q94.8 82 91.6 88.6Q88 93.6 82.6 94.4Q76 94 71 90Q68.6 88 67.6 86.6" +
|
|
77
|
+
"L79.8 80Q79.4 77 79.4 73Q79.6 69 82 65.4L84.6 65Q82.6 64.8 80.4 63.8Q78 62.6 77.4 60Q78.2 57.6 76.6 56.4Q74.4 55.4 74.6 52.6Q75.6 50.4 73.6 49Q72.2 47 73.6 44.8Z",
|
|
78
|
+
};
|
|
79
|
+
// A salute, traced from a croc's: the arm up from the side, fingers along the brow and the
|
|
80
|
+
// thumb under the eye, placed by that eye. No character is traced with it, so it's always filled.
|
|
81
|
+
const SALUTE = {
|
|
82
|
+
from: [35, 39.4],
|
|
83
|
+
lines: "M26.7 84Q20 81 12 76Q7 70 6.6 62Q7.6 56 13 50.4Q15.6 47.6 14.4 42.6Q15.4 39.6 20 38.8L22.6 38.4Q22.4 34 24.6 31.8Q27.6 30.2 30 31.4" +
|
|
84
|
+
"M26 34.6Q28.6 35.6 28.6 38.4L28.6 42.6Q33 42.4 37 44Q38.6 46 37.4 47Q34 47.8 29.6 47.6Q28.6 50.6 28.4 55Q26.6 57.4 24.4 58.6Q26.4 61.4 28.4 63Q30 68 31.6 72.4Q32.6 74.6 34.6 75",
|
|
85
|
+
fill: "M26.7 84Q20 81 12 76Q7 70 6.6 62Q7.6 56 13 50.4Q15.6 47.6 14.4 42.6Q15.4 39.6 20 38.8L22.6 38.4Q22.4 34 24.6 31.8Q27.6 30.2 30 31.4" +
|
|
86
|
+
"L26 34.6Q28.6 35.6 28.6 38.4L28.6 42.6Q33 42.4 37 44Q38.6 46 37.4 47Q34 47.8 29.6 47.6Q28.6 50.6 28.4 55Q26.6 57.4 24.4 58.6Q26.4 61.4 28.4 63Q30 68 31.6 72.4Q32.6 74.6 34.6 75L30 80Z",
|
|
87
|
+
};
|
|
88
|
+
/** The calico's paw on its own, fingers up, for the gestures no reference shows. */
|
|
89
|
+
const MITTEN = "M-6.4 -1Q-7 -7.6 -2.4 -9.6Q2.6 -11 6 -7.6Q8.6 -4.4 7.8 1Q7.2 6.6 3 8.4Q-1.6 9.6 -4.4 7.6Q-7.4 5.6 -6.4 2.4Q-7.6 0.6 -6.4 -1Z";
|
|
90
|
+
const MITTEN_CREASES = "M-6 -3.8Q-1 -4.6 3.8 -4.4M-5.4 2.2Q-2.6 1.4 -0.6 0.4";
|
|
91
|
+
const tabbyStripes = (fur) => mix(fur, "#e0a020", 0.3);
|
|
92
|
+
/** Neighbours are as unlike as possible. */
|
|
93
|
+
export const CHARACTERS = [
|
|
94
|
+
{
|
|
95
|
+
// Traced from the first reference at a tenth of 1080 px: one soft blob with no neck, its
|
|
96
|
+
// rounded left ear and pointed right ear part of the same outline, and arms whose
|
|
97
|
+
// outsides are its sides.
|
|
98
|
+
name: "Tabby",
|
|
99
|
+
head: [
|
|
100
|
+
[18.5, 43], [18.6, 36], [19.6, 31], [21.4, 29.6], [25, 29.6], [27.6, 30.8], [29.2, 32], [32, 30.6], [40, 29.6], [50, 29.2], [56, 30],
|
|
101
|
+
[57.2, 30.6], [59, 28.4], [61.8, 25.8], [64.2, 24.2], [65.6, 24.6], [66.4, 27.4], [67.2, 31.2], [69, 32.6], [73, 35.4], [76, 39.6],
|
|
102
|
+
[78.4, 44.6], [79.4, 48.6], [80.2, 53], [81, 57.4], [82.6, 61], [86, 65], [88, 70], [88.8, 76], [87.6, 82], [83.4, 86.2], [86.2, 89.2],
|
|
103
|
+
[89.6, 93], [91, 100], [92, 112], [19, 112], [18, 100], [17, 91], [15, 84.6], [14, 78], [14, 72], [15, 68], [16.4, 65.4], [15.4, 63.6],
|
|
104
|
+
[14.4, 59.6], [14.2, 54.6], [15, 50], [16.6, 46.4],
|
|
105
|
+
],
|
|
106
|
+
body: "",
|
|
107
|
+
furs: ["#f2cd7a", "#c9c3bb", "#f3a85b", "#e9e4dc", "#8a8a92"],
|
|
108
|
+
eyes: [[43.4, 48.8], [55.7, 47.4]],
|
|
109
|
+
mouth: [51.1, 56.2, 0.95, 0],
|
|
110
|
+
nose: (_, weight) => `<path d="M47.9 51.8Q47.9 50.6 51.1 50.6Q54.3 50.6 54.3 51.8Q53.7 53.8 51.1 54.1Q48.5 53.8 47.9 51.8Z" fill="${INK}"/><path d="M51.1 54V56.2" fill="none" stroke="${INK}" stroke-width="${round2(weight)}" stroke-linecap="round"/>`,
|
|
111
|
+
cheeks: [[36, 55], [62, 54]],
|
|
112
|
+
shoulders: [[21, 82], [80, 80]],
|
|
113
|
+
chest: [51, 88],
|
|
114
|
+
whiskers: "M8 50.2L20 53M7.6 60L17.4 60M79.8 48.2L85 44.6M81 54.4L88.4 53.2",
|
|
115
|
+
markings: (fur) => `<g clip-path="url(#buddy-head)"><path d="M34.4 32.2L35.4 40.2M43 31.2L43.6 38.6M50 31.6L50.6 37.4M75.2 45.8L78.6 46.4" fill="none" stroke="${tabbyStripes(fur)}" stroke-width="3.6" stroke-linecap="round"/>` +
|
|
116
|
+
`<path d="M15 74.6L24 74.4M16 86L28 82.6M74 68.4L85 66.6M75 79L85 80.4" fill="none" stroke="${tabbyStripes(fur)}" stroke-width="4.6" stroke-linecap="round"/>` +
|
|
117
|
+
`<ellipse cx="23.8" cy="35.4" rx="2.2" ry="2.6" fill="${tabbyStripes(fur)}"/><ellipse cx="63.6" cy="28.8" rx="1.3" ry="2" transform="rotate(-15 63.6 28.8)" fill="${tabbyStripes(fur)}"/></g>`,
|
|
118
|
+
shades: [0.62, -4],
|
|
119
|
+
ovalEyes: true,
|
|
120
|
+
weight: 1.75,
|
|
121
|
+
frame: "",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
// Traced from the third reference at a tenth of 736 px: a cream calico turned to one side,
|
|
125
|
+
// orange patches with stripes, ears pink and tufted, a pink nose and fur on its chest.
|
|
126
|
+
name: "Calico",
|
|
127
|
+
head: [
|
|
128
|
+
[23.4, 6.8], [26, 9], [32, 13.4], [40, 17.8], [50, 17.6], [58, 19], [64.6, 20.6], [72, 15.6], [78, 12.2], [84, 10.6], [84.8, 14], [84.2, 24],
|
|
129
|
+
[83, 33], [82.4, 40], [83, 52], [80, 62], [70, 69.4], [57.6, 73.4], [45, 75], [33, 72.6], [24, 70], [18.6, 65.4], [15.4, 60], [14.4, 52],
|
|
130
|
+
[15, 44], [17.4, 36], [20, 29], [20.4, 20], [21.4, 12],
|
|
131
|
+
],
|
|
132
|
+
body: "M24 69C18 76 12 88 7.5 105L93 105C92 90 89 76 79 64Z",
|
|
133
|
+
furs: ["#fdf0d8", "#ffffff", "#f1e6da", "#fbe3c8", "#eef0f3"],
|
|
134
|
+
eyes: [[27.5, 47], [54, 49.5]],
|
|
135
|
+
mouth: [38.2, 62, 1, 0],
|
|
136
|
+
nose: (_, weight) => `<path d="M34.8 53.8Q38.2 52.8 41.8 53.8Q40.6 56.6 38.2 57Q35.8 56.6 34.8 53.8Z" fill="${PINK}" stroke="${INK}" stroke-width="${round2(weight * 0.8)}" stroke-linejoin="round"/>` +
|
|
137
|
+
`<path d="M38.2 57V61" fill="none" stroke="${INK}" stroke-width="${round2(weight)}" stroke-linecap="round"/>`,
|
|
138
|
+
cheeks: [[22, 58], [68, 60]],
|
|
139
|
+
shoulders: [[22, 80], [80, 78]],
|
|
140
|
+
chest: [45, 88],
|
|
141
|
+
whiskers: "M9.4 58.4L14.6 58.2M11.4 64L16 61.4",
|
|
142
|
+
details: (fur, _, weight) => `<path d="M73.6 17L82.8 12.4Q83.4 22 81.6 33Q78 31 74.6 30.6Q76 27 73.6 26Q75.6 23.4 73.4 21Z" fill="${mix(fur, PINK, 0.6)}"/>` +
|
|
143
|
+
`<path d="M25.4 11.4Q28 17 29.8 22.6M24.6 22.6L29.8 24.4L27.4 26M71.4 25.2L76.4 26.4M70.4 29.4L75.6 29M23.8 80Q21.6 88 19.6 96" fill="none" stroke="${INK}" stroke-width="${round2(weight * 0.8)}" stroke-linecap="round" stroke-linejoin="round"/>` +
|
|
144
|
+
`<path d="M31.4 84Q32.6 93.4 35.6 87.6Q36.6 94.6 39.6 89.6M40.6 91.6Q43.6 96 46.4 88.6" fill="none" stroke="${mix(fur, "#b8875a", 0.5)}" stroke-width="1.6" stroke-linecap="round"/><circle cx="51.6" cy="86.2" r="0.9" fill="${mix(fur, "#b8875a", 0.5)}"/>`,
|
|
145
|
+
markings: (_, patch) => `<g clip-path="url(#buddy-head)"><path d="${blob([[21, 8], [32, 13], [40, 18], [64, 20.6], [72, 15.6], [84, 10.6], [84.4, 38], [74, 37.6], [62, 36.4], [52, 37.4], [47, 34], [45.6, 28.6], [42, 24.4], [36, 23.6], [30, 26.6], [25, 30], [20, 29]])}" fill="${patch}"/>` +
|
|
146
|
+
`<path d="M39.6 19.2Q37.6 23 36.2 27.4M48.2 20.4Q47.2 25 46.4 30M55.2 21Q53.4 25.4 51.6 30.4" fill="none" stroke="${mix(patch, INK, 0.22)}" stroke-width="2.2" stroke-linecap="round"/></g>` +
|
|
147
|
+
`<g clip-path="url(#buddy-body)"><ellipse cx="71.6" cy="96.4" rx="5" ry="7" fill="${patch}"/></g>`,
|
|
148
|
+
shades: [1, 0],
|
|
149
|
+
weight: 1.25,
|
|
150
|
+
frame: "",
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
/** A calico's patches, which can be changed apart from its fur. */
|
|
154
|
+
export const PATCHES = ["#f0a04b", "#3a3330", "#8c8279", "#d9772f", "#c9a27a"];
|
|
155
|
+
export const MOODS = [
|
|
156
|
+
{ name: "Content", eyes: ["dot", "dot"], mouth: "w" },
|
|
157
|
+
{ name: "Happy", eyes: ["happy", "happy"], mouth: "smile" },
|
|
158
|
+
{ name: "Smug", eyes: ["half", "half"], mouth: "smirk" },
|
|
159
|
+
{ name: "Wink", eyes: ["dot", "wink"], mouth: "w" },
|
|
160
|
+
{ name: "Blushing", eyes: ["dot", "dot"], mouth: "w", blush: true },
|
|
161
|
+
{ name: "Surprised", eyes: ["round", "round"], mouth: "o" },
|
|
162
|
+
{ name: "Grumpy", eyes: ["dot", "dot"], mouth: "frown", brows: true },
|
|
163
|
+
{ name: "Sleepy", eyes: ["line", "line"], mouth: "o" },
|
|
164
|
+
{ name: "In love", eyes: ["heart", "heart"], mouth: "w" },
|
|
165
|
+
{ name: "Cheeky", eyes: ["dot", "dot"], mouth: "tongue" },
|
|
166
|
+
];
|
|
167
|
+
/** What its paws are doing. Neighbours are as unlike as possible. */
|
|
168
|
+
export const GESTURES = [
|
|
169
|
+
{ name: "Cheeks" },
|
|
170
|
+
{ name: "Shades" },
|
|
171
|
+
{ name: "Salute" },
|
|
172
|
+
{ name: "Heart" },
|
|
173
|
+
{ name: "Wave" },
|
|
174
|
+
{ name: "Shy" },
|
|
175
|
+
{ name: "Peace" },
|
|
176
|
+
{ name: "Chill" },
|
|
177
|
+
];
|
|
178
|
+
export const POSES = [
|
|
179
|
+
{ name: "Centre", transform: "" },
|
|
180
|
+
{ name: "Mirrored", transform: "translate(100 0) scale(-1 1)", mirrored: true },
|
|
181
|
+
{ name: "Close up", transform: "translate(50 56) scale(1.2) translate(-50 -56)" },
|
|
182
|
+
{ name: "Peeking", transform: "translate(0 16)" },
|
|
183
|
+
{ name: "Small", transform: "translate(50 110) scale(0.84) translate(-50 -110)" },
|
|
184
|
+
{ name: "Tilted", transform: "rotate(-9 50 100)" },
|
|
185
|
+
];
|
|
186
|
+
export const characterOf = (t) => t.pick("character", CHARACTERS);
|
|
187
|
+
export const moodOf = (t) => t.pick("mood", MOODS);
|
|
188
|
+
export const gestureOf = (t) => t.pick("gesture", GESTURES).name;
|
|
189
|
+
export const poseOf = (t) => t.pick("pose", POSES);
|
|
190
|
+
export function coloursOf(t, character) {
|
|
191
|
+
return {
|
|
192
|
+
tile: t.color("tile") ?? t.pick("tile", TILES),
|
|
193
|
+
fur: t.color("fur") ?? t.pick("fur", character.furs),
|
|
194
|
+
patch: t.color("patch") ?? t.pick("patch", PATCHES),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function drawEye(kind, size, oval) {
|
|
198
|
+
const line = `fill="none" stroke="${INK}" stroke-width="${round2(1.7 * size)}" stroke-linecap="round"`;
|
|
199
|
+
switch (kind) {
|
|
200
|
+
case "happy":
|
|
201
|
+
return `<path d="M${point(-2.6 * size, 0.8 * size)}Q0 ${round2(-2.6 * size)} ${point(2.6 * size, 0.8 * size)}" ${line}/>`;
|
|
202
|
+
case "half":
|
|
203
|
+
return `<g data-pupil=""><circle cx="0" cy="${round2(0.5 * size)}" r="${round2(1.9 * size)}" fill="${INK}"/></g><path d="M${point(-3 * size, -0.9 * size)}H${round2(3 * size)}" ${line}/>`;
|
|
204
|
+
case "wink":
|
|
205
|
+
return `<path d="M${point(-2.6 * size, 0.6 * size)}Q0 ${round2(-2.2 * size)} ${point(2.6 * size, 0.6 * size)}" ${line}/>`;
|
|
206
|
+
case "round":
|
|
207
|
+
return `<circle cx="0" cy="0" r="${round2(3.4 * size)}" fill="#fff" stroke="${INK}" stroke-width="${round2(1.4 * size)}"/><g data-pupil=""><circle cx="0" cy="0" r="${round2(1.6 * size)}" fill="${INK}"/></g>`;
|
|
208
|
+
case "line":
|
|
209
|
+
return `<path d="M${point(-2.6 * size, 0.2 * size)}Q0 ${round2(1.6 * size)} ${point(2.6 * size, 0.2 * size)}" ${line}/>`;
|
|
210
|
+
case "heart": {
|
|
211
|
+
const s = size * 0.95;
|
|
212
|
+
return `<g data-pupil=""><path d="M0 ${round2(2.6 * s)}C${point(-4 * s, 0)} ${point(-3.2 * s, -3 * s)} ${point(-1.5 * s, -3 * s)}C${point(-0.6 * s, -3 * s)} 0 ${round2(-2.2 * s)} 0 ${round2(-1.6 * s)}C0 ${round2(-2.2 * s)} ${point(0.6 * s, -3 * s)} ${point(1.5 * s, -3 * s)}C${point(3.2 * s, -3 * s)} ${point(4 * s, 0)} 0 ${round2(2.6 * s)}Z" fill="${HEART}"/></g>`;
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
// Upright ovals, as the references have them, or round with a shine.
|
|
216
|
+
return oval
|
|
217
|
+
? `<g data-pupil=""><ellipse cx="0" cy="0" rx="${round2(1.45 * size)}" ry="${round2(2.1 * size)}" fill="${INK}"/></g>`
|
|
218
|
+
: `<g data-pupil=""><circle cx="0" cy="0" r="${round2(2 * size)}" fill="${INK}"/><circle cx="${round2(0.7 * size)}" cy="${round2(-0.7 * size)}" r="${round2(0.6 * size)}" fill="#fff"/></g>`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** The mouth, in its own frame, its middle at 0 0 just under the nose. */
|
|
222
|
+
function drawMouth(kind, weight) {
|
|
223
|
+
const line = `fill="none" stroke="${INK}" stroke-width="${round2(weight)}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
224
|
+
const w = "M-3.8 0Q-1.9 2.8 0 0Q1.9 2.8 3.8 0";
|
|
225
|
+
switch (kind) {
|
|
226
|
+
case "smile":
|
|
227
|
+
return `<path d="M-4.2 -0.4Q0 7 4.2 -0.4Z" fill="${INK}" stroke="${INK}" stroke-width="${round2(weight * 0.8)}" stroke-linejoin="round"/><path d="M-2.2 3.2Q0 5.2 2.2 3.2Q0 1.8 -2.2 3.2Z" fill="${TONGUE}"/>`;
|
|
228
|
+
case "smirk":
|
|
229
|
+
return `<path d="M-3.4 0.8Q0.6 2.6 4 -0.8" ${line}/>`;
|
|
230
|
+
case "o":
|
|
231
|
+
return `<ellipse cx="0" cy="1.6" rx="1.6" ry="2" fill="${INK}"/>`;
|
|
232
|
+
case "frown":
|
|
233
|
+
// As the calico wears it: down from the nose, then out to either side.
|
|
234
|
+
return `<path d="M-3.4 2.4Q-1.6 0 0 -0.4Q1.8 0 4.4 3" ${line}/>`;
|
|
235
|
+
case "tongue":
|
|
236
|
+
return `<path d="M-1.6 1.2Q0 6.4 1.6 1.2Z" fill="${TONGUE}" stroke="${INK}" stroke-width="${round2(weight * 0.7)}"/><path d="${w}" ${line}/>`;
|
|
237
|
+
default:
|
|
238
|
+
return `<path d="${w}" ${line}/>`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Places a traced drawing by one point, at a size, and turned. */
|
|
242
|
+
const placed = (from, to, scale = 1, turn = 0) => `translate(${at(to)}) rotate(${round2(turn)}) scale(${round2(scale)}) translate(${point(-from[0], -from[1])})`;
|
|
243
|
+
export function createBuddy(name, overrides, ids = "buddy") {
|
|
244
|
+
const t = calmed(createTraits(name, overrides), CALMING);
|
|
245
|
+
const character = characterOf(t);
|
|
246
|
+
const mood = moodOf(t);
|
|
247
|
+
const gesture = gestureOf(t);
|
|
248
|
+
const pose = poseOf(t);
|
|
249
|
+
const { tile, fur, patch } = coloursOf(t, character);
|
|
250
|
+
const weight = character.weight * t.num("weight", 0.88, 1.12);
|
|
251
|
+
const line = `stroke="${INK}" stroke-width="${round2(weight)}" stroke-linejoin="round"`;
|
|
252
|
+
const lines = (d, w = weight) => `<path d="${d}" fill="none" stroke="${INK}" stroke-width="${round2(w)}" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
253
|
+
const head = blob(character.head);
|
|
254
|
+
const lift = t.jitter("eye.y", 1);
|
|
255
|
+
const eyeSize = t.num("eye.size", 0.88, 1.15);
|
|
256
|
+
const eyes = mood.eyes
|
|
257
|
+
.map((kind, i) => {
|
|
258
|
+
const [x, y] = character.eyes[i];
|
|
259
|
+
const eye = drawEye(kind, eyeSize, Boolean(character.ovalEyes));
|
|
260
|
+
const blinks = kind !== "happy" && kind !== "wink" && kind !== "line";
|
|
261
|
+
return `<g transform="translate(${point(x, y + lift)})"><g data-widen="">${blinks ? `<g data-blink="">${eye}</g>` : eye}</g></g>`;
|
|
262
|
+
})
|
|
263
|
+
.join("");
|
|
264
|
+
const [mx, my, ms, mt] = character.mouth;
|
|
265
|
+
const mouth = `<g transform="translate(${point(mx, my)}) rotate(${mt}) scale(${ms})">${drawMouth(mood.mouth, weight / ms)}</g>`;
|
|
266
|
+
const blush = mood.blush
|
|
267
|
+
? character.cheeks.map(([x, y]) => `<ellipse cx="${round2(x + (x < 50 ? 3 : -3))}" cy="${round2(y - 4)}" rx="3.6" ry="2" fill="${BLUSH}" fill-opacity="0.75"/>`).join("")
|
|
268
|
+
: "";
|
|
269
|
+
const brows = mood.brows
|
|
270
|
+
? character.eyes.map(([x, y], i) => `<path d="M${point(x + (i ? 3.6 : -3.6), y - 5.2)}L${point(x + (i ? -2.2 : 2.2), y - 3.8)}" fill="none" stroke="${INK}" stroke-width="${round2(weight * 0.8)}" stroke-linecap="round"/>`).join("")
|
|
271
|
+
: "";
|
|
272
|
+
const whiskers = character.whiskers ? lines(character.whiskers, weight * 0.9) : "";
|
|
273
|
+
// A traced drawing, placed on this face: filled in fur unless it's on the one it was traced from.
|
|
274
|
+
const drawArt = (art, to, scale, traced, turn = 0) => `<g transform="${placed(art.from, to, scale, turn)}">` +
|
|
275
|
+
((art.inOutline && character.name === traced) || !art.fill ? "" : `<path d="${art.fill}" fill="${fur}"/>`) +
|
|
276
|
+
lines(art.lines, weight / scale) +
|
|
277
|
+
`</g>`;
|
|
278
|
+
// The other gestures use the calico's paw, on an arm with an outline.
|
|
279
|
+
const [shoulderL, shoulderR] = character.shoulders;
|
|
280
|
+
const [cheekL, cheekR] = character.cheeks;
|
|
281
|
+
const [eyeL, eyeR] = character.eyes;
|
|
282
|
+
const armWidth = 10.5 * (character.arms ?? 1);
|
|
283
|
+
const arm = (from, to, bulge) => {
|
|
284
|
+
const bend = [(from[0] + to[0]) / 2 + bulge, (from[1] + to[1]) / 2 + 4];
|
|
285
|
+
const d = `M${at(from)}Q${at(bend)} ${at(to)}`;
|
|
286
|
+
return `<path d="${d}" fill="none" stroke="${INK}" stroke-width="${round2(armWidth + weight * 2)}" stroke-linecap="round"/><path d="${d}" fill="none" stroke="${fur}" stroke-width="${round2(armWidth)}" stroke-linecap="round"/>`;
|
|
287
|
+
};
|
|
288
|
+
const mitten = ([x, y], turn, extra = "") => `<g transform="translate(${point(x, y)}) rotate(${round2(turn)})">${extra}<path d="${MITTEN}" fill="${fur}" ${line}/>${lines(MITTEN_CREASES, weight * 0.85)}</g>`;
|
|
289
|
+
const reach = (from, to, bulge, turn, extra = "") => arm(from, to, bulge) + mitten(to, turn, extra);
|
|
290
|
+
const front = (() => {
|
|
291
|
+
switch (gesture) {
|
|
292
|
+
case "Cheeks": {
|
|
293
|
+
const scale = character.arms ?? 1;
|
|
294
|
+
const paws = CHEEK_PAWS.map((paw, i) => drawArt(paw, character.eyes[i], scale, "Tabby")).join("");
|
|
295
|
+
// Anyone else needs the outsides of the arms, and only the tabby has its chin.
|
|
296
|
+
const sides = character.name === "Tabby"
|
|
297
|
+
? lines(TABBY_CHIN)
|
|
298
|
+
: CHEEK_SIDES.map((side, i) => `<g transform="${placed(CHEEK_PAWS[i].from, character.eyes[i], scale)}">${lines(side, weight / scale)}</g>`).join("");
|
|
299
|
+
return paws + sides;
|
|
300
|
+
}
|
|
301
|
+
case "Shades": {
|
|
302
|
+
const [scale, turn] = character.shades;
|
|
303
|
+
const middle = [(eyeL[0] + eyeR[0]) / 2, (eyeL[1] + eyeR[1]) / 2];
|
|
304
|
+
const glasses = `<g transform="${placed(SHADES_AT, middle, scale, turn)}"><path d="${LENSES}" fill="${LENS}" stroke="${INK}" stroke-width="${round2(1.3 / scale)}" stroke-linejoin="round"/>` +
|
|
305
|
+
`${lines(SHADES_BAR, 2.8)}<path d="${SHADES_GLINT}" fill="none" stroke="${GLINT}" stroke-width="0.9" stroke-linecap="round"/>` +
|
|
306
|
+
`<ellipse cx="32" cy="43" rx="1.9" ry="1.2" transform="rotate(15 32 43)" fill="${GLINT}"/><ellipse cx="66.4" cy="45.2" rx="2.1" ry="1.3" transform="rotate(10 66.4 45.2)" fill="${GLINT}"/></g>`;
|
|
307
|
+
// The paw holds the end of the bar, wherever that falls on this face.
|
|
308
|
+
const r = (turn * Math.PI) / 180;
|
|
309
|
+
const dx = (SHADES_END[0] - SHADES_AT[0]) * scale;
|
|
310
|
+
const dy = (SHADES_END[1] - SHADES_AT[1]) * scale;
|
|
311
|
+
const end = [middle[0] + dx * Math.cos(r) - dy * Math.sin(r), middle[1] + dx * Math.sin(r) + dy * Math.cos(r)];
|
|
312
|
+
return glasses + drawArt(SHADES_PAW, end, Math.max(0.8, scale), "Calico");
|
|
313
|
+
}
|
|
314
|
+
case "Salute":
|
|
315
|
+
return drawArt(SALUTE, eyeL, character.arms ?? 1, "");
|
|
316
|
+
case "Heart": {
|
|
317
|
+
const [cx, cy] = character.chest;
|
|
318
|
+
const heart = `<path d="M${point(cx, cy + 3)}C${point(cx - 7, cy - 2)} ${point(cx - 5, cy - 8)} ${point(cx - 2, cy - 7.4)}C${point(cx - 0.8, cy - 7.2)} ${point(cx, cy - 6)} ${point(cx, cy - 5)}C${point(cx, cy - 6)} ${point(cx + 0.8, cy - 7.2)} ${point(cx + 2, cy - 7.4)}C${point(cx + 5, cy - 8)} ${point(cx + 7, cy - 2)} ${point(cx, cy + 3)}Z" fill="${HEART}" ${line}/>`;
|
|
319
|
+
return heart + reach(shoulderL, [cx - 8, cy - 1], -4, 60) + reach(shoulderR, [cx + 8, cy - 1], 4, -60);
|
|
320
|
+
}
|
|
321
|
+
case "Wave": {
|
|
322
|
+
const hand = [cheekR[0] + 12, cheekR[1] - 16];
|
|
323
|
+
const marks = lines(`M${point(hand[0] + 8, hand[1] - 7)}q3 3 0 6M${point(hand[0] + 11, hand[1] - 9)}q4.4 4.6 0 9.2`, weight * 0.8);
|
|
324
|
+
return reach(shoulderR, hand, 8, 12) + marks;
|
|
325
|
+
}
|
|
326
|
+
case "Shy": {
|
|
327
|
+
// Both paws held together under the chin.
|
|
328
|
+
const x = (cheekL[0] + cheekR[0]) / 2;
|
|
329
|
+
const y = Math.max(cheekL[1], cheekR[1]) + 12;
|
|
330
|
+
return reach(shoulderL, [x - 5.4, y], -5, 50) + reach(shoulderR, [x + 5.4, y], 5, -50);
|
|
331
|
+
}
|
|
332
|
+
case "Peace": {
|
|
333
|
+
// Two fingers up from the paw.
|
|
334
|
+
const finger = (d) => `<path d="${d}" fill="none" stroke="${INK}" stroke-width="${round2(4.4 + weight * 2)}" stroke-linecap="round"/><path d="${d}" fill="none" stroke="${fur}" stroke-width="4.4" stroke-linecap="round"/>`;
|
|
335
|
+
const fingers = finger("M-2.6 -6L-4.8 -14.6") + finger("M2.6 -6L5.4 -14");
|
|
336
|
+
return reach(shoulderR, [cheekR[0] + 10, cheekR[1] - 2], 6, 0, fingers);
|
|
337
|
+
}
|
|
338
|
+
default:
|
|
339
|
+
return "";
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
342
|
+
return scopeAll({
|
|
343
|
+
defs: `<clipPath id="buddy-head"><path d="${head}"/></clipPath><clipPath id="buddy-body"><path d="${character.body || head}"/></clipPath>`,
|
|
344
|
+
tile,
|
|
345
|
+
pose: `${pose.transform} rotate(${round2(t.jitter("tilt", 5))} 50 100) ${character.frame}`.trim(),
|
|
346
|
+
mirrored: Boolean(pose.mirrored),
|
|
347
|
+
ears: ["", ""],
|
|
348
|
+
body: (character.body ? `<path d="${character.body}" fill="${fur}" ${line}/>` : "") +
|
|
349
|
+
`<path d="${head}" fill="${fur}" ${line}/>${character.markings(fur, patch)}${character.details?.(fur, patch, weight) ?? ""}`,
|
|
350
|
+
eyes,
|
|
351
|
+
face: (character.nose?.(fur, weight) ?? "") + mouth + blush + brows + whiskers,
|
|
352
|
+
front,
|
|
353
|
+
blinkDelay: t.num("blink", 0, 4),
|
|
354
|
+
}, "buddy", ids);
|
|
355
|
+
}
|
|
356
|
+
export const TILE_SHAPE = `<rect width="100" height="100" rx="22.22"/>`;
|
|
357
|
+
/** The buddy at rest, for thumbnails. */
|
|
358
|
+
export const buddySvg = (name, overrides) => {
|
|
359
|
+
const b = createBuddy(name, overrides);
|
|
360
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs>${b.defs}<clipPath id="buddy-tile">${TILE_SHAPE}</clipPath></defs>` +
|
|
361
|
+
TILE_SHAPE.replace("/>", ` fill="${b.tile}"/>`) +
|
|
362
|
+
`<g clip-path="url(#buddy-tile)"><g transform="${b.pose}">${b.body}${b.face}${b.eyes}${b.front}</g></g></svg>`);
|
|
363
|
+
};
|
package/dist/colors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The sums behind colours picked by hand. Generators hold their own designed palettes. */
|
|
2
|
+
/** Hue in degrees, then saturation and brightness from 0 to 1. */
|
|
3
|
+
export interface Hsv {
|
|
4
|
+
h: number;
|
|
5
|
+
s: number;
|
|
6
|
+
v: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const BLACK = "#000000";
|
|
9
|
+
export declare const WHITE = "#ffffff";
|
|
10
|
+
export declare const isHex: (value: string) => boolean;
|
|
11
|
+
export declare const hexToInt: (hex: string) => number;
|
|
12
|
+
export declare const intToHex: (value: number) => string;
|
|
13
|
+
/** Part of the way from `hex` to `target`, for shades of a picked colour. */
|
|
14
|
+
export declare function mix(hex: string, target: string, amount: number): string;
|
|
15
|
+
export declare function hsvToHex({ h, s, v }: Hsv): string;
|
|
16
|
+
export declare function hexToHsv(hex: string): Hsv;
|
|
17
|
+
/** Black or white, whichever reads better on `hex`. */
|
|
18
|
+
export declare const inkOn: (hex: string) => "#000000" | "#ffffff";
|
|
19
|
+
/** The WCAG contrast ratio, from 1 for the same colour to 21 for black on white. */
|
|
20
|
+
export declare function contrast(a: string, b: string): number;
|
package/dist/colors.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The sums behind colours picked by hand. Generators hold their own designed palettes. */
|
|
2
|
+
export const BLACK = "#000000";
|
|
3
|
+
export const WHITE = "#ffffff";
|
|
4
|
+
export const isHex = (value) => /^#[0-9a-f]{6}$/i.test(value);
|
|
5
|
+
export const hexToInt = (hex) => parseInt(hex.slice(1), 16);
|
|
6
|
+
export const intToHex = (value) => `#${value.toString(16).padStart(6, "0")}`;
|
|
7
|
+
const channels = (hex) => [1, 3, 5].map((start) => parseInt(hex.slice(start, start + 2), 16));
|
|
8
|
+
const fromChannels = (values) => `#${values.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("")}`;
|
|
9
|
+
/** Part of the way from `hex` to `target`, for shades of a picked colour. */
|
|
10
|
+
export function mix(hex, target, amount) {
|
|
11
|
+
const to = channels(target);
|
|
12
|
+
return fromChannels(channels(hex).map((v, i) => v + (to[i] - v) * amount));
|
|
13
|
+
}
|
|
14
|
+
export function hsvToHex({ h, s, v }) {
|
|
15
|
+
const channel = (n) => {
|
|
16
|
+
const k = (n + h / 60) % 6;
|
|
17
|
+
return (v - v * s * Math.max(0, Math.min(k, 4 - k, 1))) * 255;
|
|
18
|
+
};
|
|
19
|
+
return fromChannels([channel(5), channel(3), channel(1)]);
|
|
20
|
+
}
|
|
21
|
+
export function hexToHsv(hex) {
|
|
22
|
+
const [r, g, b] = channels(hex).map((v) => v / 255);
|
|
23
|
+
const max = Math.max(r, g, b);
|
|
24
|
+
const range = max - Math.min(r, g, b);
|
|
25
|
+
const sector = range === 0 ? 0 : max === r ? ((g - b) / range + 6) % 6 : max === g ? (b - r) / range + 2 : (r - g) / range + 4;
|
|
26
|
+
return { h: sector * 60, s: max === 0 ? 0 : range / max, v: max };
|
|
27
|
+
}
|
|
28
|
+
function luminance(hex) {
|
|
29
|
+
const [r, g, b] = channels(hex).map((v) => {
|
|
30
|
+
const unit = v / 255;
|
|
31
|
+
return unit <= 0.04045 ? unit / 12.92 : ((unit + 0.055) / 1.055) ** 2.4;
|
|
32
|
+
});
|
|
33
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
34
|
+
}
|
|
35
|
+
/** Black or white, whichever reads better on `hex`. */
|
|
36
|
+
export const inkOn = (hex) => (luminance(hex) > 0.179 ? BLACK : WHITE);
|
|
37
|
+
/** The WCAG contrast ratio, from 1 for the same colour to 21 for black on white. */
|
|
38
|
+
export function contrast(a, b) {
|
|
39
|
+
const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
40
|
+
return (light + 0.05) / (dark + 0.05);
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** How many distinct looks a type offers, for showing beside the selected avatar. */
|
|
2
|
+
import type { AvatarKind } from "./kinds.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Every choice the panel offers, multiplied together. A control that appears only with
|
|
5
|
+
* some looks counts too, at its largest. Sliders are left out, as they have no count.
|
|
6
|
+
*/
|
|
7
|
+
export declare function combinations(kind: AvatarKind): number;
|
|
8
|
+
/** "383.2 million combinations", as a person would say it. */
|
|
9
|
+
export declare const combinationsLabel: (count: number) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** How many distinct looks a type offers, for showing beside the selected avatar. */
|
|
2
|
+
/** Enough names that controls shown only for some looks are all seen. */
|
|
3
|
+
const SAMPLE = 60;
|
|
4
|
+
const known = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Every choice the panel offers, multiplied together. A control that appears only with
|
|
7
|
+
* some looks counts too, at its largest. Sliders are left out, as they have no count.
|
|
8
|
+
*/
|
|
9
|
+
export function combinations(kind) {
|
|
10
|
+
const cached = known.get(kind.id);
|
|
11
|
+
if (cached !== undefined)
|
|
12
|
+
return cached;
|
|
13
|
+
const options = new Map();
|
|
14
|
+
for (let i = 0; i < SAMPLE; i++) {
|
|
15
|
+
const name = i === 0 ? kind.defaultName : `${kind.defaultName}-${i}`;
|
|
16
|
+
for (const control of kind.controls(name, {}).flatMap((section) => section.controls)) {
|
|
17
|
+
if (control.type !== "choice")
|
|
18
|
+
continue;
|
|
19
|
+
options.set(control.id, Math.max(options.get(control.id) ?? 1, control.options.length));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const total = [...options.values()].reduce((product, count) => product * count, 1);
|
|
23
|
+
known.set(kind.id, total);
|
|
24
|
+
return total;
|
|
25
|
+
}
|
|
26
|
+
const compact = new Intl.NumberFormat("en", { notation: "compact", compactDisplay: "long", maximumFractionDigits: 1 });
|
|
27
|
+
/** "383.2 million combinations", as a person would say it. */
|
|
28
|
+
export const combinationsLabel = (count) => `${compact.format(count)} combination${count === 1 ? "" : "s"}`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The card a 3D avatar sits on, as a peek sits on its tile. It's off until switched on,
|
|
3
|
+
* and its size and colour are set by hand, never from the name.
|
|
4
|
+
*/
|
|
5
|
+
import { type Control } from "./controls.ts";
|
|
6
|
+
import type { Traits } from "./traits.ts";
|
|
7
|
+
import type { AvatarParts } from "./types.ts";
|
|
8
|
+
/** Every avatar starts on the first, and the rest are offered beside it. */
|
|
9
|
+
export declare const CONTAINERS: string[];
|
|
10
|
+
export declare function containerOf(t: Traits): AvatarParts["container"];
|
|
11
|
+
/** The switch, then its size and colour while it's on. */
|
|
12
|
+
export declare function containerControls(t: Traits): Control[];
|
|
13
|
+
/** A still picture's art on its card, clipped to it, or as it was with no card. */
|
|
14
|
+
export declare function inContainer(art: string, container: AvatarParts["container"], id: string): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The card a 3D avatar sits on, as a peek sits on its tile. It's off until switched on,
|
|
3
|
+
* and its size and colour are set by hand, never from the name.
|
|
4
|
+
*/
|
|
5
|
+
import { fixedPalette } from "./controls.js";
|
|
6
|
+
/** Every avatar starts on the first, and the rest are offered beside it. */
|
|
7
|
+
export const CONTAINERS = [
|
|
8
|
+
"#17181c", "#e6e1ff", "#1f2a44", "#d6f0ff", "#3a2346", "#ffe4d6", "#163a33", "#fff1c4", "#ffd8e6",
|
|
9
|
+
];
|
|
10
|
+
/** Its side as a share of the slot, from its slider's position. */
|
|
11
|
+
const RANGE = [0.5, 1];
|
|
12
|
+
const POSITION = 0.8;
|
|
13
|
+
const sizeAt = (position) => RANGE[0] + (RANGE[1] - RANGE[0]) * position;
|
|
14
|
+
/** Its corner radius as a share of its side, the same as a peek's tile. */
|
|
15
|
+
const CORNER = 0.22;
|
|
16
|
+
/** How much of it the avatar fills in a still picture, leaving the same margin as in 3D. */
|
|
17
|
+
const FILL = 0.86;
|
|
18
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
19
|
+
const shown = (t) => (t.forced("container") ?? 1) < 0.5;
|
|
20
|
+
export function containerOf(t) {
|
|
21
|
+
if (!shown(t))
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
size: sizeAt(t.forced("container.size") ?? POSITION),
|
|
25
|
+
color: t.color("container.color") ?? CONTAINERS[0],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The switch, then its size and colour while it's on. */
|
|
29
|
+
export function containerControls(t) {
|
|
30
|
+
const on = shown(t);
|
|
31
|
+
const toggle = {
|
|
32
|
+
type: "choice",
|
|
33
|
+
id: "container",
|
|
34
|
+
label: "Container",
|
|
35
|
+
layout: "switch",
|
|
36
|
+
options: [
|
|
37
|
+
{ id: "on", label: "On", set: { container: 0 } },
|
|
38
|
+
{ id: "off", label: "Off", set: { container: undefined } },
|
|
39
|
+
],
|
|
40
|
+
selected: on ? "on" : "off",
|
|
41
|
+
};
|
|
42
|
+
if (!on)
|
|
43
|
+
return [toggle];
|
|
44
|
+
const position = t.forced("container.size") ?? POSITION;
|
|
45
|
+
const size = {
|
|
46
|
+
type: "slider",
|
|
47
|
+
id: "container.size",
|
|
48
|
+
label: "Container size",
|
|
49
|
+
value: position,
|
|
50
|
+
display: `${Math.round(sizeAt(position) * 100)}%`,
|
|
51
|
+
range: [RANGE[0] * 100, RANGE[1] * 100],
|
|
52
|
+
};
|
|
53
|
+
return [toggle, size, fixedPalette(t, "container.color", "Container colour", CONTAINERS)];
|
|
54
|
+
}
|
|
55
|
+
/** A still picture's art on its card, clipped to it, or as it was with no card. */
|
|
56
|
+
export function inContainer(art, container, id) {
|
|
57
|
+
const open = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">`;
|
|
58
|
+
if (!container)
|
|
59
|
+
return `${open}${art}</svg>`;
|
|
60
|
+
const side = round2(container.size * 100);
|
|
61
|
+
const card = `<rect x="${round2(50 - side / 2)}" y="${round2(50 - side / 2)}" width="${side}" height="${side}" rx="${round2(side * CORNER)}"/>`;
|
|
62
|
+
return (open +
|
|
63
|
+
`<clipPath id="${id}">${card}</clipPath>` +
|
|
64
|
+
card.replace("/>", ` fill="${container.color}"/>`) +
|
|
65
|
+
`<g clip-path="url(#${id})"><g transform="translate(50 50) scale(${round2(container.size * FILL)}) translate(-50 -50)">${art}</g></g>` +
|
|
66
|
+
`</svg>`);
|
|
67
|
+
}
|