@originkit/avatars 0.0.0-stage → 0.1.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.
Files changed (168) hide show
  1. package/GUIDE.md +44 -0
  2. package/LICENSE +58 -0
  3. package/README.md +119 -2
  4. package/bin/avatars.js +65 -0
  5. package/dist/art.d.ts +43 -0
  6. package/dist/art.js +35 -0
  7. package/dist/auras/calming.d.ts +18 -0
  8. package/dist/auras/calming.js +30 -0
  9. package/dist/auras/controls.d.ts +2 -0
  10. package/dist/auras/controls.js +60 -0
  11. package/dist/auras/index.d.ts +119 -0
  12. package/dist/auras/index.js +355 -0
  13. package/dist/auras/skies.d.ts +13 -0
  14. package/dist/auras/skies.js +22 -0
  15. package/dist/blobs/color.d.ts +19 -0
  16. package/dist/blobs/color.js +113 -0
  17. package/dist/blobs/controls.d.ts +2 -0
  18. package/dist/blobs/controls.js +77 -0
  19. package/dist/blobs/index.d.ts +5 -0
  20. package/dist/blobs/index.js +107 -0
  21. package/dist/blobs/paths.d.ts +33 -0
  22. package/dist/blobs/paths.js +102 -0
  23. package/dist/blobs/shapes.d.ts +46 -0
  24. package/dist/blobs/shapes.js +186 -0
  25. package/dist/blobs/variance.d.ts +22 -0
  26. package/dist/blobs/variance.js +42 -0
  27. package/dist/bots/attachments.d.ts +24 -0
  28. package/dist/bots/attachments.js +119 -0
  29. package/dist/bots/calming.d.ts +4 -0
  30. package/dist/bots/calming.js +19 -0
  31. package/dist/bots/controls.d.ts +2 -0
  32. package/dist/bots/controls.js +78 -0
  33. package/dist/bots/faces.d.ts +23 -0
  34. package/dist/bots/faces.js +122 -0
  35. package/dist/bots/heads.d.ts +15 -0
  36. package/dist/bots/heads.js +66 -0
  37. package/dist/bots/index.d.ts +5 -0
  38. package/dist/bots/index.js +47 -0
  39. package/dist/bots/palette.d.ts +15 -0
  40. package/dist/bots/palette.js +30 -0
  41. package/dist/bots/paths.d.ts +25 -0
  42. package/dist/bots/paths.js +89 -0
  43. package/dist/bots/trim.d.ts +12 -0
  44. package/dist/bots/trim.js +37 -0
  45. package/dist/bubbles/calming.d.ts +7 -0
  46. package/dist/bubbles/calming.js +19 -0
  47. package/dist/bubbles/controls.d.ts +2 -0
  48. package/dist/bubbles/controls.js +69 -0
  49. package/dist/bubbles/index.d.ts +65 -0
  50. package/dist/bubbles/index.js +279 -0
  51. package/dist/buddies/calming.d.ts +8 -0
  52. package/dist/buddies/calming.js +18 -0
  53. package/dist/buddies/controls.d.ts +2 -0
  54. package/dist/buddies/controls.js +35 -0
  55. package/dist/buddies/index.d.ts +113 -0
  56. package/dist/buddies/index.js +363 -0
  57. package/dist/colors.d.ts +20 -0
  58. package/dist/colors.js +41 -0
  59. package/dist/combinations.d.ts +9 -0
  60. package/dist/combinations.js +28 -0
  61. package/dist/container.d.ts +14 -0
  62. package/dist/container.js +67 -0
  63. package/dist/controls.d.ts +128 -0
  64. package/dist/controls.js +170 -0
  65. package/dist/critters/calming.d.ts +8 -0
  66. package/dist/critters/calming.js +18 -0
  67. package/dist/critters/controls.d.ts +2 -0
  68. package/dist/critters/controls.js +49 -0
  69. package/dist/critters/index.d.ts +113 -0
  70. package/dist/critters/index.js +307 -0
  71. package/dist/doodles/calming.d.ts +7 -0
  72. package/dist/doodles/calming.js +18 -0
  73. package/dist/doodles/controls.d.ts +2 -0
  74. package/dist/doodles/controls.js +50 -0
  75. package/dist/doodles/index.d.ts +66 -0
  76. package/dist/doodles/index.js +236 -0
  77. package/dist/faces/adventurer-neutral.json +3168 -0
  78. package/dist/faces/calming.d.ts +8 -0
  79. package/dist/faces/calming.js +20 -0
  80. package/dist/faces/controls.d.ts +2 -0
  81. package/dist/faces/controls.js +38 -0
  82. package/dist/faces/index.d.ts +44 -0
  83. package/dist/faces/index.js +108 -0
  84. package/dist/glances/calming.d.ts +8 -0
  85. package/dist/glances/calming.js +16 -0
  86. package/dist/glances/controls.d.ts +2 -0
  87. package/dist/glances/controls.js +27 -0
  88. package/dist/glances/index.d.ts +54 -0
  89. package/dist/glances/index.js +79 -0
  90. package/dist/glyphs/calming.d.ts +8 -0
  91. package/dist/glyphs/calming.js +17 -0
  92. package/dist/glyphs/controls.d.ts +2 -0
  93. package/dist/glyphs/controls.js +55 -0
  94. package/dist/glyphs/index.d.ts +62 -0
  95. package/dist/glyphs/index.js +197 -0
  96. package/dist/grins/calming.d.ts +8 -0
  97. package/dist/grins/calming.js +21 -0
  98. package/dist/grins/controls.d.ts +2 -0
  99. package/dist/grins/controls.js +33 -0
  100. package/dist/grins/index.d.ts +81 -0
  101. package/dist/grins/index.js +240 -0
  102. package/dist/gummies/art.d.ts +77 -0
  103. package/dist/gummies/art.js +168 -0
  104. package/dist/gummies/calming.d.ts +8 -0
  105. package/dist/gummies/calming.js +21 -0
  106. package/dist/gummies/controls.d.ts +2 -0
  107. package/dist/gummies/controls.js +53 -0
  108. package/dist/gummies/index.d.ts +37 -0
  109. package/dist/gummies/index.js +283 -0
  110. package/dist/history.d.ts +21 -0
  111. package/dist/history.js +48 -0
  112. package/dist/index.d.ts +13 -0
  113. package/dist/index.js +16 -0
  114. package/dist/kinds.d.ts +30 -0
  115. package/dist/kinds.js +266 -0
  116. package/dist/kitties/calming.d.ts +8 -0
  117. package/dist/kitties/calming.js +19 -0
  118. package/dist/kitties/controls.d.ts +2 -0
  119. package/dist/kitties/controls.js +43 -0
  120. package/dist/kitties/index.d.ts +181 -0
  121. package/dist/kitties/index.js +673 -0
  122. package/dist/knits/calming.d.ts +7 -0
  123. package/dist/knits/calming.js +19 -0
  124. package/dist/knits/controls.d.ts +2 -0
  125. package/dist/knits/controls.js +32 -0
  126. package/dist/knits/index.d.ts +74 -0
  127. package/dist/knits/index.js +445 -0
  128. package/dist/mallows/calming.d.ts +8 -0
  129. package/dist/mallows/calming.js +19 -0
  130. package/dist/mallows/controls.d.ts +2 -0
  131. package/dist/mallows/controls.js +45 -0
  132. package/dist/mallows/index.d.ts +111 -0
  133. package/dist/mallows/index.js +454 -0
  134. package/dist/peeks/calming.d.ts +8 -0
  135. package/dist/peeks/calming.js +21 -0
  136. package/dist/peeks/controls.d.ts +2 -0
  137. package/dist/peeks/controls.js +54 -0
  138. package/dist/peeks/index.d.ts +80 -0
  139. package/dist/peeks/index.js +225 -0
  140. package/dist/pixels/calming.d.ts +8 -0
  141. package/dist/pixels/calming.js +22 -0
  142. package/dist/pixels/controls.d.ts +3 -0
  143. package/dist/pixels/controls.js +82 -0
  144. package/dist/pixels/index.d.ts +34 -0
  145. package/dist/pixels/index.js +174 -0
  146. package/dist/pixels/palette.d.ts +29 -0
  147. package/dist/pixels/palette.js +107 -0
  148. package/dist/pixels/sprites.d.ts +42 -0
  149. package/dist/pixels/sprites.js +154 -0
  150. package/dist/possibilities.d.ts +39 -0
  151. package/dist/possibilities.js +88 -0
  152. package/dist/shapes/calming.d.ts +7 -0
  153. package/dist/shapes/calming.js +19 -0
  154. package/dist/shapes/controls.d.ts +2 -0
  155. package/dist/shapes/controls.js +41 -0
  156. package/dist/shapes/index.d.ts +44 -0
  157. package/dist/shapes/index.js +104 -0
  158. package/dist/share.d.ts +21 -0
  159. package/dist/share.js +83 -0
  160. package/dist/traits.d.ts +34 -0
  161. package/dist/traits.js +58 -0
  162. package/dist/types.d.ts +99 -0
  163. package/dist/types.js +1 -0
  164. package/dist/variants.d.ts +11 -0
  165. package/dist/variants.js +41 -0
  166. package/mcp/server.js +7 -0
  167. package/mcp/tools.js +70 -0
  168. package/package.json +54 -4
@@ -0,0 +1,4 @@
1
+ /** What Possibilities calms on a bot. All eight heads stay, so variants never repeat one. */
2
+ import type { Calming } from "../possibilities.ts";
3
+ export declare const CALMING: Calming;
4
+ export declare const NOTES: [string, string, string];
@@ -0,0 +1,19 @@
1
+ /** What Possibilities calms on a bot. All eight heads stay, so variants never repeat one. */
2
+ export const CALMING = {
3
+ features: /^(head|eye|side|top|mouth|port|lens|screen|vent)\./,
4
+ lists: {
5
+ head: [8, 8],
6
+ face: [4, 1],
7
+ eye: [6, 1],
8
+ mouth: [4, 1],
9
+ // Antennas, ears and side parts are the extras, and "None" is last in both lists.
10
+ top: [8, 1, "end"],
11
+ side: [5, 1, "end"],
12
+ },
13
+ rarer: ["trim.straps", "trim.studs"],
14
+ };
15
+ export const NOTES = [
16
+ "plain bots, with one face style, dot eyes, hardly any add-ons and even proportions.",
17
+ "more face styles and eyes, and some antennas, ears and side parts.",
18
+ "anything goes: every face, eye, antenna and bit of trim.",
19
+ ];
@@ -0,0 +1,2 @@
1
+ import { type ControlBuilder } from "../controls.ts";
2
+ export declare const controls: ControlBuilder;
@@ -0,0 +1,78 @@
1
+ import { containerControls } from "../container.js";
2
+ import { choice, palette, slider, toggle } from "../controls.js";
3
+ import { calmed, narrowed, possibilitiesSlider } from "../possibilities.js";
4
+ import { createTraits } from "../traits.js";
5
+ import { CALMING, NOTES } from "./calming.js";
6
+ import { SIDE_NAMES, TOP_OPTIONS } from "./attachments.js";
7
+ import { CYCLOPS_EYES, EYE_LABELS, FACE_NAMES, LENS_EYES, MOUTH_KINDS, PAIR_EYES } from "./faces.js";
8
+ import { HEAD_NAMES } from "./heads.js";
9
+ import { LIGHTS, PAINTS } from "./palette.js";
10
+ const indexOf = (value, count) => Math.floor(value * count);
11
+ /** The heads built with each extra measurement. */
12
+ const TALL_HEADS = ["TV", "Pill", "Dome", "Acorn", "Bucket"];
13
+ const SQUARE_HEADS = ["TV", "Cube", "Orb", "Dome", "Acorn"];
14
+ const CUT_HEADS = ["Bucket", "Octagon"];
15
+ const EYES_FOR = {
16
+ Band: PAIR_EYES,
17
+ Screen: PAIR_EYES,
18
+ Goggles: LENS_EYES,
19
+ Porthole: CYCLOPS_EYES,
20
+ };
21
+ export const controls = (name, overrides) => {
22
+ const t = calmed(createTraits(name, overrides), CALMING);
23
+ const list = (control) => narrowed(control, t, CALMING);
24
+ const head = HEAD_NAMES[indexOf(t("head"), HEAD_NAMES.length)];
25
+ const face = FACE_NAMES[indexOf(t("face"), FACE_NAMES.length)];
26
+ const paint = PAINTS[indexOf(t("paint"), PAINTS.length)];
27
+ const top = list(choice(t, "top", "Top", "list", TOP_OPTIONS));
28
+ const side = list(choice(t, "side", "Sides", "list", SIDE_NAMES.map((label) => ({ label }))));
29
+ const topName = TOP_OPTIONS[Number(top.selected)].label;
30
+ const hasParts = topName !== "None" || SIDE_NAMES[Number(side.selected)] !== "None";
31
+ const sections = [
32
+ {
33
+ title: "Head",
34
+ controls: [
35
+ possibilitiesSlider(t, NOTES),
36
+ choice(t, "head", "Head", "list", HEAD_NAMES.map((label) => ({ label }))),
37
+ slider(t, "head.rx", "Head size", 0, 100, "%"),
38
+ ...(TALL_HEADS.includes(head) ? [slider(t, "head.ry", "Height", 0, 100, "%")] : []),
39
+ ...(SQUARE_HEADS.includes(head) ? [slider(t, "head.n", "Squareness", 0, 100, "%")] : []),
40
+ ...(CUT_HEADS.includes(head) ? [slider(t, "head.round", "Corners", 0, 100, "%")] : []),
41
+ ...(head === "Bucket" ? [slider(t, "head.taper", "Taper", 0, 100, "%")] : []),
42
+ ],
43
+ },
44
+ {
45
+ title: "Face",
46
+ controls: [
47
+ list(choice(t, "face", "Style", "list", FACE_NAMES.map((label) => ({ label })))),
48
+ list(choice(t, "eye", "Eyes", "list", EYES_FOR[face].map((kind) => ({ label: EYE_LABELS[kind] })))),
49
+ ...(face === "Screen" ? [list(choice(t, "mouth", "Mouth", "list", MOUTH_KINDS.map((label) => ({ label }))))] : []),
50
+ ...(face === "Band" || face === "Screen" ? [slider(t, "eye.gap", "Eye gap", 0, 100, "%")] : []),
51
+ ],
52
+ },
53
+ {
54
+ title: "Parts",
55
+ controls: [
56
+ top,
57
+ ...(topName === "Antenna" ? [slider(t, "top.length", "Antenna length", 0, 100, "%"), slider(t, "top.lean", "Antenna lean", -100, 100, "%")] : []),
58
+ side,
59
+ toggle(t, "trim.straps", 60, "Chrome straps"),
60
+ toggle(t, "trim.studs", 75, "Studs"),
61
+ ],
62
+ },
63
+ {
64
+ title: "Paint",
65
+ controls: [
66
+ palette(t, "paint", "Colour", PAINTS.map(({ body }) => body)),
67
+ ...(hasParts ? [palette(t, "paint.accent", "Parts", paint.accents)] : []),
68
+ palette(t, "paint.light", "Lights", LIGHTS),
69
+ slider(t, "speckle", "Speckle", 0, 100, "%"),
70
+ ],
71
+ },
72
+ {
73
+ title: "Container",
74
+ controls: containerControls(t),
75
+ },
76
+ ];
77
+ return sections;
78
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The faces. A face is dark glass with lights behind it, so the visor layer is
3
+ * drawn as glass and the lights layer glows.
4
+ */
5
+ import type { Box } from "./heads.ts";
6
+ import type { Traits } from "../traits.ts";
7
+ import type { Shape } from "../types.ts";
8
+ export interface Face {
9
+ visor: Shape[];
10
+ eyes: Shape[];
11
+ /** The mouth, kept apart from the eyes so a blink never moves it. */
12
+ lights: Shape[];
13
+ /** The height of the eyes, which a blink closes towards. */
14
+ eyeY: number;
15
+ }
16
+ export declare const PAIR_EYES: string[];
17
+ export declare const LENS_EYES: string[];
18
+ export declare const CYCLOPS_EYES: string[];
19
+ export declare const EYE_LABELS: Record<string, string>;
20
+ /** In the order the generator picks from, because the panel selects by position. */
21
+ export declare const MOUTH_KINDS: string[];
22
+ export declare const FACE_NAMES: string[];
23
+ export declare const buildFace: (t: Traits, box: Box) => Face;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The faces. A face is dark glass with lights behind it, so the visor layer is
3
+ * drawn as glass and the lights layer glows.
4
+ */
5
+ const squircle = (cx, cy, rx, ry, n) => ({ kind: "superellipse", cx, cy, rx, ry, n, rot: 0 });
6
+ function eye(kind, x, y, r) {
7
+ switch (kind) {
8
+ case "bar":
9
+ return squircle(x, y, r * 0.58, r * 1.3, 4);
10
+ case "pixel":
11
+ return squircle(x, y, r * 0.92, r * 0.92, 8);
12
+ case "diamond":
13
+ return squircle(x, y, r * 1.25, r * 1.25, 1.2);
14
+ case "slit":
15
+ return squircle(x, y, r * 1.3, r * 0.42, 4);
16
+ case "happy":
17
+ return { kind: "crescent", cx: x, cy: y + r * 0.45, width: r * 1.2, lift: r * 0.9, thickness: r * 0.5 };
18
+ default:
19
+ return squircle(x, y, r, r * 1.28, 2);
20
+ }
21
+ }
22
+ const ring = (cx, cy, r) => ({ kind: "circle", cx, cy, r });
23
+ const bar = (x1, y1, x2, y2, r) => ({ kind: "rod", x1, y1, x2, y2, r });
24
+ export const PAIR_EYES = ["dot", "bar", "pixel", "diamond", "slit", "happy"];
25
+ export const LENS_EYES = ["dot", "pixel", "diamond", "bar"];
26
+ export const CYCLOPS_EYES = ["dot", "pixel", "diamond"];
27
+ export const EYE_LABELS = {
28
+ dot: "Dots",
29
+ bar: "Bars",
30
+ pixel: "Squares",
31
+ diamond: "Diamonds",
32
+ slit: "Slits",
33
+ happy: "Happy",
34
+ };
35
+ /** In the order the generator picks from, because the panel selects by position. */
36
+ export const MOUTH_KINDS = ["Smile", "Flat", "Equalizer", "Chirp"];
37
+ export const FACE_NAMES = ["Band", "Screen", "Goggles", "Porthole"];
38
+ function mouth(t, x, y, width) {
39
+ const kind = t.pick("mouth", ["smile", "flat", "equalizer", "chirp"]);
40
+ switch (kind) {
41
+ case "flat":
42
+ return [{ kind: "rod", x1: x - width, y1: y, x2: x + width, y2: y, r: 1.3 }];
43
+ case "chirp":
44
+ return [{ kind: "circle", cx: x, cy: y, r: width * 0.34 }];
45
+ case "equalizer": {
46
+ const bars = t.int("mouth.bars", 4, 6);
47
+ return Array.from({ length: bars }, (_, i) => {
48
+ const barX = x - width + (2 * width * i) / (bars - 1);
49
+ const reach = t.num(`mouth.bar${i}`, 0.8, 3.4);
50
+ return { kind: "rod", x1: barX, y1: y - reach, x2: barX, y2: y + reach, r: 1 };
51
+ });
52
+ }
53
+ default:
54
+ return [{ kind: "crescent", cx: x, cy: y + 1.6, width, lift: -2.2, thickness: -1.4 }];
55
+ }
56
+ }
57
+ /** Cooling slots cut under the visor, in the same dark glass. */
58
+ function vents(t, x, y, width) {
59
+ const count = t.int("vent.n", 3, 5);
60
+ return Array.from({ length: count }, (_, i) => {
61
+ const slotX = x - width + (2 * width * i) / (count - 1);
62
+ return bar(slotX, y - 2.2, slotX, y + 2.2, 0.95);
63
+ });
64
+ }
65
+ function band(t, box) {
66
+ const ry = box.ry * t.num("band.ry", 0.46, 0.56);
67
+ const y = box.cy - box.ry * 0.26;
68
+ const r = Math.min(ry * 0.54, box.rx * 0.2);
69
+ const gap = box.rx * t.num("eye.gap", 0.4, 0.54);
70
+ const kind = t.pick("eye", PAIR_EYES);
71
+ return {
72
+ visor: [
73
+ squircle(box.cx, y, box.rx, ry, t.num("band.n", 3.4, 5.5)),
74
+ ...vents(t, box.cx, box.cy + box.ry * 0.72, box.rx * 0.42),
75
+ ],
76
+ eyes: [eye(kind, box.cx - gap, y, r), eye(kind, box.cx + gap, y, r)],
77
+ lights: [],
78
+ eyeY: y,
79
+ };
80
+ }
81
+ function screen(t, box) {
82
+ const eyeY = box.cy - box.ry * 0.26;
83
+ const mouthY = box.cy + box.ry * 0.5;
84
+ const r = Math.min(box.ry * 0.24, box.rx * 0.2);
85
+ const gap = box.rx * t.num("eye.gap", 0.38, 0.52);
86
+ const kind = t.pick("eye", PAIR_EYES);
87
+ return {
88
+ visor: [squircle(box.cx, box.cy, box.rx, box.ry, t.num("screen.n", 3.6, 5.5))],
89
+ eyes: [eye(kind, box.cx - gap, eyeY, r), eye(kind, box.cx + gap, eyeY, r)],
90
+ lights: mouth(t, box.cx, mouthY, box.rx * t.num("mouth.width", 0.26, 0.4)),
91
+ eyeY,
92
+ };
93
+ }
94
+ function goggles(t, box) {
95
+ const lens = Math.min(box.rx * 0.46, box.ry * 0.74);
96
+ const y = box.cy - box.ry * 0.2;
97
+ const gap = box.rx - lens;
98
+ const r = lens * t.num("lens.pupil", 0.4, 0.52);
99
+ const kind = t.pick("eye", LENS_EYES);
100
+ return {
101
+ visor: [
102
+ ring(box.cx - gap, y, lens),
103
+ ring(box.cx + gap, y, lens),
104
+ bar(box.cx - gap, y, box.cx + gap, y, 2),
105
+ ...vents(t, box.cx, box.cy + box.ry * 0.82, box.rx * 0.34),
106
+ ],
107
+ eyes: [eye(kind, box.cx - gap, y, r), eye(kind, box.cx + gap, y, r)],
108
+ lights: [],
109
+ eyeY: y,
110
+ };
111
+ }
112
+ function porthole(t, box) {
113
+ const r = Math.min(box.rx, box.ry) * t.num("port.r", 0.86, 1);
114
+ const pupil = r * t.num("port.pupil", 0.42, 0.56);
115
+ return {
116
+ visor: [ring(box.cx, box.cy, r)],
117
+ eyes: [eye(t.pick("eye", CYCLOPS_EYES), box.cx, box.cy, pupil)],
118
+ lights: [],
119
+ eyeY: box.cy,
120
+ };
121
+ }
122
+ export const buildFace = (t, box) => t.pick("face", [band, screen, goggles, porthole])(t, box);
@@ -0,0 +1,15 @@
1
+ /** The head shapes, each with the box its face has to fit inside. */
2
+ import type { Traits } from "../traits.ts";
3
+ export interface Box {
4
+ cx: number;
5
+ cy: number;
6
+ rx: number;
7
+ ry: number;
8
+ }
9
+ export interface Head extends Box {
10
+ path: string;
11
+ face: Box;
12
+ }
13
+ /** In the order of `HEADS`, because the panel selects by position. */
14
+ export declare const HEAD_NAMES: string[];
15
+ export declare const pickHead: (t: Traits) => Head;
@@ -0,0 +1,66 @@
1
+ /** The head shapes, each with the box its face has to fit inside. */
2
+ import { roundedPolygon, superellipse } from "./paths.js";
3
+ const CENTER_X = 50;
4
+ const CENTER_Y = 58;
5
+ /** Places a head at the frame's centre, with a face that is a fraction of its size. */
6
+ function head(rx, ry, path, faceRx, faceRy, faceDrop = 0) {
7
+ const cy = CENTER_Y;
8
+ return {
9
+ cx: CENTER_X,
10
+ cy,
11
+ rx,
12
+ ry,
13
+ path,
14
+ face: { cx: CENTER_X, cy: cy + ry * faceDrop, rx: rx * faceRx, ry: ry * faceRy },
15
+ };
16
+ }
17
+ const place = (rx, ry, n, nLower = n) => ({ cx: CENTER_X, cy: CENTER_Y, rx, ry, n, nLower });
18
+ const tv = (t) => {
19
+ const [rx, ry] = [t.num("head.rx", 33, 37), t.num("head.ry", 25, 29)];
20
+ return head(rx, ry, superellipse(place(rx, ry, t.num("head.n", 4.5, 6))), 0.8, 0.68);
21
+ };
22
+ const cube = (t) => {
23
+ const r = t.num("head.rx", 29, 33);
24
+ return head(r, r, superellipse(place(r, r, t.num("head.n", 4.5, 6))), 0.78, 0.66);
25
+ };
26
+ const orb = (t) => {
27
+ const r = t.num("head.rx", 31, 35);
28
+ return head(r, r, superellipse(place(r, r, t.num("head.n", 2, 2.3))), 0.72, 0.6);
29
+ };
30
+ const pill = (t) => {
31
+ const [rx, ry] = [t.num("head.rx", 24, 27), t.num("head.ry", 33, 36)];
32
+ return head(rx, ry, superellipse(place(rx, ry, 3)), 0.78, 0.52, -0.12);
33
+ };
34
+ const dome = (t) => {
35
+ const [rx, ry] = [t.num("head.rx", 31, 35), t.num("head.ry", 29, 32)];
36
+ return head(rx, ry, superellipse(place(rx, ry, 2.1, t.num("head.n", 5, 6.5))), 0.78, 0.58, 0.12);
37
+ };
38
+ const acorn = (t) => {
39
+ const [rx, ry] = [t.num("head.rx", 31, 35), t.num("head.ry", 29, 33)];
40
+ return head(rx, ry, superellipse(place(rx, ry, t.num("head.n", 5, 6.5), 2.1)), 0.78, 0.54, -0.14);
41
+ };
42
+ const bucket = (t) => {
43
+ const [rx, ry] = [t.num("head.rx", 31, 35), t.num("head.ry", 27, 31)];
44
+ const top = rx * t.num("head.taper", 0.68, 0.8);
45
+ const [cx, cy] = [CENTER_X, CENTER_Y];
46
+ const path = roundedPolygon([
47
+ [cx - top, cy - ry],
48
+ [cx + top, cy - ry],
49
+ [cx + rx, cy + ry],
50
+ [cx - rx, cy + ry],
51
+ ], t.num("head.round", 0.55, 0.75));
52
+ return head(rx, ry, path, 0.7, 0.62, 0.08);
53
+ };
54
+ const octagon = (t) => {
55
+ const r = t.num("head.rx", 31, 34);
56
+ const corner = r / Math.cos(Math.PI / 8);
57
+ const vertices = Array.from({ length: 8 }, (_, i) => {
58
+ const angle = Math.PI / 8 + (i * Math.PI) / 4;
59
+ return [CENTER_X + corner * Math.cos(angle), CENTER_Y + corner * Math.sin(angle)];
60
+ });
61
+ return head(r, r, roundedPolygon(vertices, t.num("head.round", 0.45, 0.65)), 0.76, 0.64);
62
+ };
63
+ const HEADS = [tv, cube, orb, pill, dome, acorn, bucket, octagon];
64
+ /** In the order of `HEADS`, because the panel selects by position. */
65
+ export const HEAD_NAMES = ["TV", "Cube", "Orb", "Pill", "Dome", "Acorn", "Bucket", "Octagon"];
66
+ export const pickHead = (t) => t.pick("head", HEADS)(t);
@@ -0,0 +1,5 @@
1
+ import { type Overrides } from "../traits.ts";
2
+ import type { AvatarParts } from "../types.ts";
3
+ export declare function botParts(name: string, overrides?: Overrides): AvatarParts;
4
+ /** Renders the bot for a name as SVG markup. The same name always gives the same bot. */
5
+ export declare function generateBot(name: string, overrides?: Overrides): string;
@@ -0,0 +1,47 @@
1
+ import { buildAttachments } from "./attachments.js";
2
+ import { buildFace } from "./faces.js";
3
+ import { pickHead } from "./heads.js";
4
+ import { pickColors } from "./palette.js";
5
+ import { shapePath } from "./paths.js";
6
+ import { buildTrim } from "./trim.js";
7
+ import { containerOf, inContainer } from "../container.js";
8
+ import { calmed } from "../possibilities.js";
9
+ import { createTraits } from "../traits.js";
10
+ import { CALMING } from "./calming.js";
11
+ export function botParts(name, overrides) {
12
+ const t = calmed(createTraits(name, overrides), CALMING);
13
+ const head = pickHead(t);
14
+ const face = buildFace(t, head.face);
15
+ const { chrome, studs } = buildTrim(t, head);
16
+ return {
17
+ colors: pickColors(t),
18
+ head: [head.path],
19
+ accents: buildAttachments(t, head),
20
+ visor: face.visor.map(shapePath),
21
+ visorShapes: face.visor,
22
+ eyes: face.eyes.map(shapePath),
23
+ eyeShapes: face.eyes,
24
+ lights: face.lights.map(shapePath),
25
+ lightShapes: face.lights,
26
+ chrome: chrome.map(shapePath),
27
+ chromeShapes: chrome,
28
+ studs: studs.map(shapePath),
29
+ studShapes: studs,
30
+ speckle: t("speckle"),
31
+ eyeY: face.eyeY,
32
+ container: containerOf(t),
33
+ };
34
+ }
35
+ /** Chrome's colour in flat pictures. In 3D it reflects the studio instead. */
36
+ const CHROME = "#c9ced8";
37
+ const layer = (fill, paths) => paths.length ? `<g fill="${fill}">${paths.map((d) => `<path d="${d}"/>`).join("")}</g>` : "";
38
+ /** Renders the bot for a name as SVG markup. The same name always gives the same bot. */
39
+ export function generateBot(name, overrides) {
40
+ const { colors, head, accents, visor, eyes, lights, chrome, studs, container } = botParts(name, overrides);
41
+ const art = layer(colors.accent, accents) +
42
+ layer(colors.body, head) +
43
+ layer(CHROME, chrome) +
44
+ layer(colors.visor, [...visor, ...studs]) +
45
+ layer(colors.light, [...eyes, ...lights]);
46
+ return inContainer(art, container, "bot-card");
47
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The paint box. Colours are chosen by hand rather than computed, so a bot is
3
+ * only ever a combination that already looks good. `bots.test` holds every
4
+ * combination to the contrast the face needs.
5
+ */
6
+ import type { Traits } from "../traits.ts";
7
+ import type { Colors } from "../types.ts";
8
+ /** Each paint lists the accents that were picked to sit well on it. */
9
+ export declare const PAINTS: {
10
+ body: string;
11
+ accents: string[];
12
+ }[];
13
+ export declare const VISOR = "#000000";
14
+ export declare const LIGHTS: string[];
15
+ export declare function pickColors(t: Traits): Colors;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The paint box. Colours are chosen by hand rather than computed, so a bot is
3
+ * only ever a combination that already looks good. `bots.test` holds every
4
+ * combination to the contrast the face needs.
5
+ */
6
+ /** Each paint lists the accents that were picked to sit well on it. */
7
+ export const PAINTS = [
8
+ { body: "#f2483f", accents: ["#ffd23f", "#f6efe2", "#1f2a6b"] }, // tomato
9
+ { body: "#ff8a2b", accents: ["#fff1cf", "#2740a8", "#7a2bd6"] }, // tangerine
10
+ { body: "#ffc933", accents: ["#ff5a4f", "#2b63ff", "#6a3fd1"] }, // sunflower
11
+ { body: "#35c46b", accents: ["#fff1cf", "#ffd23f", "#0f5e6e"] }, // apple
12
+ { body: "#16b8a6", accents: ["#fff1cf", "#17406b", "#ffd23f"] }, // lagoon
13
+ { body: "#35a7ff", accents: ["#ffd23f", "#fff1cf", "#5a2bc8"] }, // sky
14
+ { body: "#3b5bff", accents: ["#ff8a2b", "#ffd23f", "#8ff0d8"] }, // cobalt
15
+ { body: "#8a4dff", accents: ["#ffd23f", "#6ff0c8", "#ffb3d9"] }, // grape
16
+ { body: "#ff5fa2", accents: ["#fff1cf", "#ffd23f", "#4a2bb8"] }, // bubblegum
17
+ { body: "#c9d2e6", accents: ["#ff5a4f", "#3b5bff", "#0f7f70"] }, // chrome
18
+ { body: "#f4efe3", accents: ["#ff5a4f", "#16b8a6", "#3b5bff"] }, // cream
19
+ ];
20
+ export const VISOR = "#000000";
21
+ export const LIGHTS = ["#59f5b0", "#4fe3ff", "#ffc24b", "#ff6fb5", "#eef4ff", "#c9ff4d"];
22
+ export function pickColors(t) {
23
+ const paint = t.pick("paint", PAINTS);
24
+ return {
25
+ body: t.color("paint") ?? paint.body,
26
+ accent: t.color("paint.accent") ?? t.pick("paint.accent", paint.accents),
27
+ visor: VISOR,
28
+ light: t.color("paint.light") ?? t.pick("paint.light", LIGHTS),
29
+ };
30
+ }
@@ -0,0 +1,25 @@
1
+ /** SVG path builders. Every coordinate is absolute, in the 100×100 viewBox. */
2
+ import type { Shape } from "../types.ts";
3
+ export interface Superellipse {
4
+ cx: number;
5
+ cy: number;
6
+ rx: number;
7
+ ry: number;
8
+ /** Squareness: 2 is an ellipse, higher tends towards a rectangle. */
9
+ n: number;
10
+ /** Squareness of the lower half, for shapes that differ top to bottom. */
11
+ nLower?: number;
12
+ }
13
+ type Point = [number, number];
14
+ /** |x/a|^n + |y/b|^n = 1, with one cubic Bézier per quadrant. */
15
+ export declare function superellipse({ cx, cy, rx, ry, n, nLower }: Superellipse): string;
16
+ export declare function circle(cx: number, cy: number, r: number): string;
17
+ /** A straight bar with fully rounded ends. */
18
+ export declare function rod(x1: number, y1: number, x2: number, y2: number, r: number): string;
19
+ /** A closed outline with its corners cut back along each edge by `round`, from 0 to 1. */
20
+ export declare function roundedPolygon(vertices: Point[], round: number): string;
21
+ /** A curved sliver, bowing upwards for a positive `lift`. Used for smiles and happy eyes. */
22
+ export declare function crescent(cx: number, cy: number, width: number, lift: number, thickness: number): string;
23
+ /** A shape's outline for still pictures, from the same numbers the 3D shader draws. */
24
+ export declare function shapePath(shape: Shape): string;
25
+ export {};
@@ -0,0 +1,89 @@
1
+ /** SVG path builders. Every coordinate is absolute, in the 100×100 viewBox. */
2
+ const fmt = (v) => {
3
+ const rounded = Math.round(v * 100) / 100;
4
+ return Object.is(rounded, -0) ? "0" : String(rounded);
5
+ };
6
+ /** Bézier handle length that passes through the curve's 45° point. */
7
+ const handle = (n) => Math.min(1, (8 * 2 ** (-1 / n) - 4) / 3);
8
+ /** |x/a|^n + |y/b|^n = 1, with one cubic Bézier per quadrant. */
9
+ export function superellipse({ cx, cy, rx, ry, n, nLower = n }) {
10
+ const upper = handle(n);
11
+ const lower = handle(nLower);
12
+ const at = (x, y) => `${fmt(cx + x)} ${fmt(cy + y)}`;
13
+ return (`M${at(rx, 0)}` +
14
+ `C${at(rx, ry * lower)} ${at(rx * lower, ry)} ${at(0, ry)}` +
15
+ `C${at(-rx * lower, ry)} ${at(-rx, ry * lower)} ${at(-rx, 0)}` +
16
+ `C${at(-rx, -ry * upper)} ${at(-rx * upper, -ry)} ${at(0, -ry)}` +
17
+ `C${at(rx * upper, -ry)} ${at(rx, -ry * upper)} ${at(rx, 0)}Z`);
18
+ }
19
+ export function circle(cx, cy, r) {
20
+ const left = `${fmt(cx - r)} ${fmt(cy)}`;
21
+ const right = `${fmt(cx + r)} ${fmt(cy)}`;
22
+ return `M${left}A${fmt(r)} ${fmt(r)} 0 1 0 ${right}A${fmt(r)} ${fmt(r)} 0 1 0 ${left}Z`;
23
+ }
24
+ /** A straight bar with fully rounded ends. */
25
+ export function rod(x1, y1, x2, y2, r) {
26
+ const length = Math.hypot(x2 - x1, y2 - y1);
27
+ const nx = (-(y2 - y1) / length) * r;
28
+ const ny = ((x2 - x1) / length) * r;
29
+ const arc = `A${fmt(r)} ${fmt(r)} 0 0 0`;
30
+ return (`M${fmt(x1 + nx)} ${fmt(y1 + ny)}` +
31
+ `L${fmt(x2 + nx)} ${fmt(y2 + ny)}` +
32
+ `${arc} ${fmt(x2 - nx)} ${fmt(y2 - ny)}` +
33
+ `L${fmt(x1 - nx)} ${fmt(y1 - ny)}` +
34
+ `${arc} ${fmt(x1 + nx)} ${fmt(y1 + ny)}Z`);
35
+ }
36
+ /** A closed outline with its corners cut back along each edge by `round`, from 0 to 1. */
37
+ export function roundedPolygon(vertices, round) {
38
+ const count = vertices.length;
39
+ const k = round / 2;
40
+ const at = (i) => vertices[((i % count) + count) % count];
41
+ const cut = (i, j) => {
42
+ const [x0, y0] = at(i);
43
+ const [x1, y1] = at(j);
44
+ return `${fmt(x0 + (x1 - x0) * k)} ${fmt(y0 + (y1 - y0) * k)}`;
45
+ };
46
+ let d = `M${cut(0, -1)}`;
47
+ for (let i = 0; i < count; i++) {
48
+ const [x, y] = at(i);
49
+ d += `Q${fmt(x)} ${fmt(y)} ${cut(i, i + 1)}L${cut(i + 1, i)}`;
50
+ }
51
+ return `${d}Z`;
52
+ }
53
+ /** A curved sliver, bowing upwards for a positive `lift`. Used for smiles and happy eyes. */
54
+ export function crescent(cx, cy, width, lift, thickness) {
55
+ const left = `${fmt(cx - width)} ${fmt(cy)}`;
56
+ const right = `${fmt(cx + width)} ${fmt(cy)}`;
57
+ return (`M${left}Q${fmt(cx)} ${fmt(cy - lift * 2)} ${right}` +
58
+ `Q${fmt(cx)} ${fmt(cy - lift * 2 + thickness * 2)} ${left}Z`);
59
+ }
60
+ /** A quadratic Bézier stroke with round ends, as a filled outline: each side offset along it. */
61
+ function curve(x1, y1, cx, cy, x2, y2, r) {
62
+ const steps = 16;
63
+ const along = Array.from({ length: steps + 1 }, (_, i) => {
64
+ const u = i / steps;
65
+ const point = [(1 - u) ** 2 * x1 + 2 * u * (1 - u) * cx + u * u * x2, (1 - u) ** 2 * y1 + 2 * u * (1 - u) * cy + u * u * y2];
66
+ const tangent = [2 * (1 - u) * (cx - x1) + 2 * u * (x2 - cx), 2 * (1 - u) * (cy - y1) + 2 * u * (y2 - cy)];
67
+ const length = Math.hypot(...tangent) || 1;
68
+ return { point, normal: [-tangent[1] / length, tangent[0] / length] };
69
+ });
70
+ const side = (sign) => along.map(({ point, normal }) => `${fmt(point[0] + normal[0] * r * sign)} ${fmt(point[1] + normal[1] * r * sign)}`);
71
+ const [left, right] = [side(1), side(-1).reverse()];
72
+ const cap = `A${fmt(r)} ${fmt(r)} 0 0 1`;
73
+ return `M${left.join("L")}${cap} ${right[0]}L${right.join("L")}${cap} ${left[0]}Z`;
74
+ }
75
+ /** A shape's outline for still pictures, from the same numbers the 3D shader draws. */
76
+ export function shapePath(shape) {
77
+ switch (shape.kind) {
78
+ case "superellipse":
79
+ return superellipse(shape);
80
+ case "crescent":
81
+ return crescent(shape.cx, shape.cy, shape.width, shape.lift, shape.thickness);
82
+ case "rod":
83
+ return rod(shape.x1, shape.y1, shape.x2, shape.y2, shape.r);
84
+ case "circle":
85
+ return circle(shape.cx, shape.cy, shape.r);
86
+ case "curve":
87
+ return curve(shape.x1, shape.y1, shape.cx, shape.cy, shape.x2, shape.y2, shape.r);
88
+ }
89
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The fine detail on a bot's shell: chrome straps and ear rings, and dark studs.
3
+ * Each sits in the band of skin between the top of the head and the face.
4
+ */
5
+ import type { Head } from "./heads.ts";
6
+ import type { Traits } from "../traits.ts";
7
+ import type { Shape } from "../types.ts";
8
+ export interface Trim {
9
+ chrome: Shape[];
10
+ studs: Shape[];
11
+ }
12
+ export declare function buildTrim(t: Traits, head: Head): Trim;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The fine detail on a bot's shell: chrome straps and ear rings, and dark studs.
3
+ * Each sits in the band of skin between the top of the head and the face.
4
+ */
5
+ import { earDiscs } from "./attachments.js";
6
+ const circle = (cx, cy, r) => ({ kind: "circle", cx, cy, r });
7
+ /** Below this much skin there is no room for detail, and it would crowd the face. */
8
+ const MIN_ROOM = 6;
9
+ export function buildTrim(t, head) {
10
+ const { cx, cy, ry, rx, face } = head;
11
+ const top = cy - ry;
12
+ const faceTop = face.cy - face.ry;
13
+ const headroom = faceTop - top;
14
+ const sideroom = rx - face.rx;
15
+ const chrome = earDiscs(t, head).map(({ x, y, r }) => circle(x, y, r * 0.5));
16
+ const studs = [];
17
+ if (headroom >= MIN_ROOM && t("trim.straps") * 100 < 60) {
18
+ const gap = rx * t.num("trim.gap", 0.34, 0.46);
19
+ for (const side of [-1, 1]) {
20
+ chrome.push({ kind: "rod", x1: cx + side * gap, y1: top + 3, x2: cx + side * gap * 1.08, y2: faceTop - 1.5, r: 0.9 });
21
+ }
22
+ }
23
+ if (t("trim.studs") * 100 < 75) {
24
+ if (headroom >= MIN_ROOM) {
25
+ const y = top + headroom * 0.5;
26
+ for (const side of [-1, 1])
27
+ studs.push(circle(cx + side * rx * 0.17, y, 1.9));
28
+ }
29
+ if (sideroom >= MIN_ROOM) {
30
+ for (const side of [-1, 1]) {
31
+ const x = cx + side * (face.rx + sideroom * 0.5);
32
+ studs.push(circle(x, cy - 3.2, 1.5), circle(x, cy + 3.2, 1.5));
33
+ }
34
+ }
35
+ }
36
+ return { chrome, studs };
37
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * What Possibilities calms on a bubble. Low keeps the eight softest shapes, one for each
3
+ * variant, then adds the lobed and spiky ones. Indices are into `SHAPES`, plainest first.
4
+ */
5
+ import type { Calming } from "../possibilities.ts";
6
+ export declare const CALMING: Calming;
7
+ export declare const NOTES: [string, string, string];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * What Possibilities calms on a bubble. Low keeps the eight softest shapes, one for each
3
+ * variant, then adds the lobed and spiky ones. Indices are into `SHAPES`, plainest first.
4
+ */
5
+ export const CALMING = {
6
+ features: /^(depth|lean|tilt|spread|eyes\.y|core\.y|body\.size|eye\.(gap|lean|size))$/,
7
+ lists: {
8
+ // Capsule, Egg, Pillow, Tile, Pebble, Bean, Kite, Scallop, then Apple, Gem, Prism, Peanut,
9
+ // Cloud, Clover, Trefoil, Flower, Star, Burst.
10
+ shape: [18, 8, [1, 3, 6, 15, 17, 9, 12, 0, 7, 10, 16, 14, 5, 2, 11, 8, 4, 13]],
11
+ "eye.kind": [12, 1],
12
+ },
13
+ rarer: ["mouth"],
14
+ };
15
+ export const NOTES = [
16
+ "soft, simple shapes like capsules, eggs, pebbles and pillows, with one kind of eye.",
17
+ "adds apples, gems, peanuts, clouds and clovers, and more kinds of eye.",
18
+ "anything goes, up to flowers, stars and bursts.",
19
+ ];
@@ -0,0 +1,2 @@
1
+ import { type ControlBuilder } from "../controls.ts";
2
+ export declare const controls: ControlBuilder;