@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,107 @@
1
+ import { createTraits } from "../traits.js";
2
+ import { containerOf, inContainer } from "../container.js";
3
+ import { createPalette } from "./color.js";
4
+ import { circle, round2, superellipse } from "./paths.js";
5
+ import { calmed, shapeFor } from "./variance.js";
6
+ import { drawBody } from "./shapes.js";
7
+ /** Sizes and places both eyes, scaling them down until they fit the face. */
8
+ function layoutEyes(t, body, face) {
9
+ const baseWidth = t.num("eye.rx", 0.075, 0.105) * body.rx;
10
+ const ratio = t.num("eye.ratio", 1.9, 3.2);
11
+ const scale = t.num("eye.scale", 0.78, 1.24);
12
+ const stretch = t.num("eye.stretch", 0.85, 1.18);
13
+ const clearance = t.num("eye.gap", 0.1, 0.24) * body.rx;
14
+ const widest = baseWidth * Math.max(1, scale);
15
+ const tallest = baseWidth * ratio * Math.max(1, scale * stretch);
16
+ const baseGap = widest + body.rx * 0.03 + clearance;
17
+ const gazeX = t.jitter("gaze.x", 0.09) * face.rx;
18
+ const gazeY = t.num("gaze.y", -0.2, 0.08) * face.ry;
19
+ const offsetY = t.jitter("eye.dy", 0.04) * face.ry;
20
+ const reach = Math.hypot(widest, tallest);
21
+ const needed = Math.hypot((Math.abs(gazeX) + baseGap + reach) / face.rx, (Math.abs(gazeY) + Math.abs(offsetY) + reach) / face.ry);
22
+ const fit = needed > 0.9 ? 0.9 / needed : 1;
23
+ const width = baseWidth * fit;
24
+ const height = width * ratio;
25
+ const gap = baseGap * fit;
26
+ // Leaning is limited by the room between the eyes, so they can never touch.
27
+ const room = Math.max(0, Math.min(1, clearance / tallest));
28
+ const maxLean = Math.min(12, (Math.asin(room) * 180) / Math.PI);
29
+ const lean = t.num("eye.lean", -1, 1) * maxLean;
30
+ const secondLean = Math.max(-12, Math.min(12, lean + t.jitter("eye.lean2", 3.5)));
31
+ const cx = face.cx + gazeX * fit;
32
+ const cy = face.cy + gazeY * fit;
33
+ const n = t.num("eye.n", 3.5, 6);
34
+ return [
35
+ { cx: cx - gap, cy, rx: width, ry: height, n, rot: lean },
36
+ {
37
+ cx: cx + gap,
38
+ cy: cy + offsetY * fit,
39
+ rx: width * scale,
40
+ ry: height * scale * stretch,
41
+ n,
42
+ rot: secondLean,
43
+ },
44
+ ];
45
+ }
46
+ function describe(name, overrides) {
47
+ const t = calmed(createTraits(name, overrides));
48
+ const { head, eye } = createPalette(t.num("hue", 0, 360), t("tone"), t.color("hue"));
49
+ const shape = shapeFor(t);
50
+ const radius = t.num("body.r", 31, 38) * shape.core;
51
+ const body = {
52
+ cx: 50 + t.jitter("body.x", 1.5),
53
+ cy: 50 + t.jitter("body.y", 1.5),
54
+ rx: radius,
55
+ ry: radius * t.num("body.ratio", 0.92, 1.08),
56
+ n: t.num("body.n", 1.9, 2.5),
57
+ rot: 0,
58
+ radii: Array.from({ length: t.int("body.pts", 6, 8) }, (_, i) => 1 + t.jitter(`body.r${i}`, 0.16)),
59
+ };
60
+ shape.body?.(t, body);
61
+ const face = shape.face?.(body) ?? body;
62
+ const decoration = { circles: [], paths: [] };
63
+ shape.decorate?.(t, body, decoration);
64
+ const [left, right] = layoutEyes(t, body, face);
65
+ return {
66
+ head,
67
+ eye,
68
+ circles: decoration.circles,
69
+ bodyPaths: [...decoration.paths, drawBody(shape, body)],
70
+ eyes: [left, right],
71
+ container: containerOf(t),
72
+ };
73
+ }
74
+ export function blobParts(name, overrides) {
75
+ const { head, eye, circles, bodyPaths, eyes, container } = describe(name, overrides);
76
+ const [left, right] = eyes;
77
+ return {
78
+ // The eyes are painted on the skin, so the visor colours are never seen.
79
+ colors: { body: head, accent: head, visor: head, light: eye },
80
+ head: [...circles.map(({ cx, cy, r }) => circle(cx, cy, r)), ...bodyPaths],
81
+ accents: [],
82
+ visor: [],
83
+ visorShapes: [],
84
+ eyes: eyes.map(superellipse),
85
+ eyeShapes: eyes.map((e) => ({ kind: "superellipse", ...e })),
86
+ lights: [],
87
+ lightShapes: [],
88
+ chrome: [],
89
+ chromeShapes: [],
90
+ studs: [],
91
+ studShapes: [],
92
+ speckle: 0,
93
+ eyeY: (left.cy + right.cy) / 2,
94
+ painted: true,
95
+ container,
96
+ };
97
+ }
98
+ /** Renders the blob for a name as SVG markup. The same name always gives the same blob. */
99
+ export function generateBlob(name, overrides) {
100
+ const { head, eye, circles: circleList, bodyPaths, eyes: eyeList, container } = describe(name, overrides);
101
+ const circles = circleList
102
+ .map(({ cx, cy, r }) => `<circle cx="${round2(cx)}" cy="${round2(cy)}" r="${round2(r)}"/>`)
103
+ .join("");
104
+ const paths = bodyPaths.map((d) => `<path d="${d}"/>`).join("");
105
+ const eyes = eyeList.map((e) => `<path d="${superellipse(e)}"/>`).join("");
106
+ return inContainer(`<g fill="${head}">${circles}${paths}</g><g fill="${eye}">${eyes}</g>`, container, "blob-card");
107
+ }
@@ -0,0 +1,33 @@
1
+ /** SVG path builders. Coordinates are in the 100×100 viewBox. */
2
+ export interface Superellipse {
3
+ cx: number;
4
+ cy: number;
5
+ rx: number;
6
+ ry: number;
7
+ /** Squareness: 2 is an ellipse, higher tends towards a rectangle. */
8
+ n: number;
9
+ /** Clockwise degrees, baked into the points so the SVG needs no transform. */
10
+ rot: number;
11
+ }
12
+ export interface Polygon {
13
+ cx: number;
14
+ cy: number;
15
+ rx: number;
16
+ ry: number;
17
+ sides: number;
18
+ /** Corner rounding, from 0 (sharp) to 1 (all curve). */
19
+ round: number;
20
+ rot: number;
21
+ }
22
+ export declare const round2: (v: number) => number;
23
+ /** |x/a|^n + |y/b|^n = 1, with one cubic Bézier per quadrant. */
24
+ export declare function superellipse({ cx, cy, rx, ry, n, rot }: Superellipse): string;
25
+ export declare function circle(cx: number, cy: number, r: number): string;
26
+ /** A lumpy closed curve: a Catmull-Rom spline through radii around a circle. */
27
+ export declare function spline(cx: number, cy: number, rx: number, ry: number, radii: number[], rot: number): string;
28
+ /** A regular polygon with rounded corners and a vertex at the top. */
29
+ export declare function polygon({ cx, cy, rx, ry, sides, round, rot }: Polygon): string;
30
+ /** The straight run between a capsule's two end circles. */
31
+ export declare function box(cx: number, cy: number, rx: number, ry: number): string;
32
+ /** A droplet's point: two tangents from an apex above the body ellipse. */
33
+ export declare function taper(cx: number, cy: number, rx: number, ry: number, tip: number): string;
@@ -0,0 +1,102 @@
1
+ /** SVG path builders. Coordinates are in the 100×100 viewBox. */
2
+ export const round2 = (v) => Math.round(v * 100) / 100;
3
+ const fmt = (v) => {
4
+ const rounded = round2(v);
5
+ return Object.is(rounded, -0) ? "0" : String(rounded);
6
+ };
7
+ /** Wraps an index, so closed outlines can read past either end. */
8
+ const wrap = (items, i) => items[((i % items.length) + items.length) % items.length];
9
+ /** |x/a|^n + |y/b|^n = 1, with one cubic Bézier per quadrant. */
10
+ export function superellipse({ cx, cy, rx, ry, n, rot }) {
11
+ // Above n≈5.55 the handle passes the radius and the shape would bulge outwards.
12
+ const k = Math.min(1, (8 * 2 ** (-1 / n) - 4) / 3);
13
+ const ax = rx * k;
14
+ const ay = ry * k;
15
+ const points = [
16
+ [rx, 0],
17
+ [rx, ay], [ax, ry], [0, ry],
18
+ [-ax, ry], [-rx, ay], [-rx, 0],
19
+ [-rx, -ay], [-ax, -ry], [0, -ry],
20
+ [ax, -ry], [rx, -ay], [rx, 0],
21
+ ];
22
+ const angle = (rot * Math.PI) / 180;
23
+ const cos = Math.cos(angle);
24
+ const sin = Math.sin(angle);
25
+ const at = (i) => {
26
+ const [x, y] = points[i];
27
+ return `${fmt(cx + x * cos - y * sin)} ${fmt(cy + x * sin + y * cos)}`;
28
+ };
29
+ let d = `M${at(0)}`;
30
+ for (let i = 1; i < 13; i += 3)
31
+ d += `C${at(i)} ${at(i + 1)} ${at(i + 2)}`;
32
+ return `${d}Z`;
33
+ }
34
+ export function circle(cx, cy, r) {
35
+ const left = `${fmt(cx - r)} ${fmt(cy)}`;
36
+ const right = `${fmt(cx + r)} ${fmt(cy)}`;
37
+ return `M${left}A${fmt(r)} ${fmt(r)} 0 1 0 ${right}A${fmt(r)} ${fmt(r)} 0 1 0 ${left}Z`;
38
+ }
39
+ /** A lumpy closed curve: a Catmull-Rom spline through radii around a circle. */
40
+ export function spline(cx, cy, rx, ry, radii, rot) {
41
+ const start = (rot * Math.PI) / 180;
42
+ const points = radii.map((radius, i) => {
43
+ const angle = start + (2 * Math.PI * i) / radii.length;
44
+ return [cx + rx * radius * Math.cos(angle), cy + ry * radius * Math.sin(angle)];
45
+ });
46
+ const [firstX, firstY] = points[0];
47
+ let d = `M${fmt(firstX)} ${fmt(firstY)}`;
48
+ for (let i = 0; i < points.length; i++) {
49
+ const [x0, y0] = wrap(points, i - 1);
50
+ const [x1, y1] = wrap(points, i);
51
+ const [x2, y2] = wrap(points, i + 1);
52
+ const [x3, y3] = wrap(points, i + 2);
53
+ d +=
54
+ `C${fmt(x1 + (x2 - x0) / 6)} ${fmt(y1 + (y2 - y0) / 6)}` +
55
+ ` ${fmt(x2 - (x3 - x1) / 6)} ${fmt(y2 - (y3 - y1) / 6)}` +
56
+ ` ${fmt(x2)} ${fmt(y2)}`;
57
+ }
58
+ return `${d}Z`;
59
+ }
60
+ /** A regular polygon with rounded corners and a vertex at the top. */
61
+ export function polygon({ cx, cy, rx, ry, sides, round, rot }) {
62
+ // Halved because every edge is cut from both ends.
63
+ const k = round > 0 ? (round < 1 ? round / 2 : 0.5) : 0;
64
+ const start = (rot * Math.PI) / 180 - Math.PI / 2;
65
+ const vertices = Array.from({ length: sides }, (_, i) => {
66
+ const angle = start + (2 * Math.PI * i) / sides;
67
+ return [cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)];
68
+ });
69
+ /** The point on the edge from vertex `i` to vertex `j` where the corner starts. */
70
+ const cut = (i, j) => {
71
+ const [x0, y0] = wrap(vertices, i);
72
+ const [x1, y1] = wrap(vertices, j);
73
+ return `${fmt(x0 + (x1 - x0) * k)} ${fmt(y0 + (y1 - y0) * k)}`;
74
+ };
75
+ let d = `M${cut(0, -1)}`;
76
+ for (let i = 0; i < sides; i++) {
77
+ const [x, y] = wrap(vertices, i);
78
+ d += `Q${fmt(x)} ${fmt(y)} ${cut(i, i + 1)}`;
79
+ if (k < 0.5)
80
+ d += `L${cut(i + 1, i)}`;
81
+ }
82
+ return `${d}Z`;
83
+ }
84
+ /** The straight run between a capsule's two end circles. */
85
+ export function box(cx, cy, rx, ry) {
86
+ const left = fmt(cx - rx);
87
+ const right = fmt(cx + rx);
88
+ return `M${left} ${fmt(cy - ry)}H${right}V${fmt(cy + ry)}H${left}Z`;
89
+ }
90
+ /** A droplet's point: two tangents from an apex above the body ellipse. */
91
+ export function taper(cx, cy, rx, ry, tip) {
92
+ const height = Math.max(1.05, tip);
93
+ const tangentX = rx * Math.sqrt(1 - 1 / (height * height));
94
+ const tangentY = cy - ry / height;
95
+ const apex = cy - height * ry;
96
+ const easeX = tangentX * 0.14;
97
+ const easeY = tangentY + 0.86 * (apex - tangentY);
98
+ return (`M${fmt(cx - tangentX)} ${fmt(tangentY)}` +
99
+ `L${fmt(cx - easeX)} ${fmt(easeY)}` +
100
+ `Q${fmt(cx)} ${fmt(apex)} ${fmt(cx + easeX)} ${fmt(easeY)}` +
101
+ `L${fmt(cx + tangentX)} ${fmt(tangentY)}Z`);
102
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The silhouettes. Each one describes how it changes the shared body, where the
3
+ * eyes may sit, what it adds around the body, and how its outline is drawn.
4
+ */
5
+ import type { Traits } from "../traits.ts";
6
+ export interface Ellipse {
7
+ cx: number;
8
+ cy: number;
9
+ rx: number;
10
+ ry: number;
11
+ }
12
+ export interface Body extends Ellipse {
13
+ n: number;
14
+ rot: number;
15
+ radii: number[];
16
+ /** Polygon shapes only. */
17
+ sides?: number;
18
+ round?: number;
19
+ }
20
+ export interface Circle {
21
+ cx: number;
22
+ cy: number;
23
+ r: number;
24
+ }
25
+ /** Outlines drawn behind the body and merged with it by sharing its fill. */
26
+ export interface Decoration {
27
+ circles: Circle[];
28
+ paths: string[];
29
+ }
30
+ export interface Shape {
31
+ name: string;
32
+ /** How much of the frame the body takes. */
33
+ core: number;
34
+ /** Adjusts the body before anything is measured against it. */
35
+ body?(t: Traits, body: Body): void;
36
+ /** The region the eyes must fit inside. Defaults to the body. */
37
+ face?(body: Body): Ellipse;
38
+ decorate?(t: Traits, body: Body, out: Decoration): void;
39
+ /** Draws the outline. Defaults to a superellipse. */
40
+ path?(body: Body): string;
41
+ }
42
+ /** `[shape, upper edge]`, so everyday shapes are common and loud ones are finds. */
43
+ export declare const BANDS: [Shape, number][];
44
+ export declare const PLAIN: Shape[];
45
+ export declare const pickShape: (value: number) => Shape;
46
+ export declare const drawBody: (shape: Shape, body: Body) => string;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The silhouettes. Each one describes how it changes the shared body, where the
3
+ * eyes may sit, what it adds around the body, and how its outline is drawn.
4
+ */
5
+ import { box, polygon, spline, superellipse, taper } from "./paths.js";
6
+ const polygonPath = (b) => polygon({ ...b, sides: b.sides, round: b.round });
7
+ const splinePath = (b) => spline(b.cx, b.cy, b.rx, b.ry, b.radii, b.rot);
8
+ const shrunk = (k) => (b) => ({ cx: b.cx, cy: b.cy, rx: b.rx * k, ry: b.ry * k });
9
+ const splineFace = (b) => shrunk(Math.min(...b.radii) * 0.95)(b);
10
+ const round = { name: "Round", core: 1 };
11
+ const organic = { name: "Organic", core: 0.98, path: splinePath, face: splineFace };
12
+ const boxy = {
13
+ name: "Boxy",
14
+ core: 0.86,
15
+ body: (t, b) => {
16
+ b.n = t.num("body.n", 3.4, 6);
17
+ b.rot = t.num("body.rot", -20, 20);
18
+ },
19
+ };
20
+ const capsule = {
21
+ name: "Capsule",
22
+ core: 1.02,
23
+ body: (t, b) => {
24
+ b.ry *= t.num("capsule.squat", 0.55, 0.68);
25
+ },
26
+ face: shrunk(0.94),
27
+ decorate: (_t, b, out) => {
28
+ for (const side of [-1, 1]) {
29
+ out.circles.push({ cx: b.cx + side * (b.rx - b.ry), cy: b.cy, r: b.ry });
30
+ }
31
+ },
32
+ path: (b) => box(b.cx, b.cy, b.rx - b.ry, b.ry),
33
+ };
34
+ const nub = {
35
+ name: "Nub",
36
+ core: 0.88,
37
+ decorate: (t, b, out) => {
38
+ const count = t.int("nub.n", 1, 2);
39
+ for (let i = 0; i < count; i++) {
40
+ const angle = t.num(`nub.a${i}`, 0, 2 * Math.PI);
41
+ out.circles.push({
42
+ cx: b.cx + Math.cos(angle) * b.rx * 0.88,
43
+ cy: b.cy + Math.sin(angle) * b.rx * 0.88,
44
+ r: b.rx * t.num(`nub.r${i}`, 0.24, 0.4),
45
+ });
46
+ }
47
+ },
48
+ };
49
+ const cloud = {
50
+ name: "Cloud",
51
+ core: 0.78,
52
+ face: splineFace,
53
+ path: splinePath,
54
+ decorate: (t, b, out) => {
55
+ const count = t.int("cloud.n", 4, 6);
56
+ for (let i = 0; i < count; i++) {
57
+ const angle = Math.PI + (Math.PI * (i + 0.5)) / count;
58
+ out.circles.push({
59
+ cx: b.cx + Math.cos(angle) * b.rx * 0.8,
60
+ cy: b.cy + Math.sin(angle) * b.rx * 0.5,
61
+ r: b.rx * t.num(`cloud.r${i}`, 0.44, 0.62),
62
+ });
63
+ }
64
+ },
65
+ };
66
+ const droplet = {
67
+ name: "Droplet",
68
+ core: 0.78,
69
+ // Moved down so the head and point together sit centred in the frame.
70
+ body: (_t, b) => {
71
+ b.cy += 0.22 * b.ry;
72
+ b.n = 2;
73
+ },
74
+ face: (b) => ({ cx: b.cx, cy: b.cy + b.ry * 0.05, rx: b.rx * 0.88, ry: b.ry * 0.88 }),
75
+ decorate: (t, b, out) => {
76
+ out.paths.push(taper(b.cx, b.cy, b.rx, b.ry, t.num("droplet.tip", 1.4, 1.65)));
77
+ },
78
+ };
79
+ const hexagon = {
80
+ name: "Hexagon",
81
+ core: 1.05,
82
+ path: polygonPath,
83
+ face: shrunk(0.84),
84
+ body: (t, b) => {
85
+ b.sides = 6;
86
+ b.rot = t.num("body.rot", -12, 12);
87
+ b.round = t.num("poly.round", 0.24, 0.5);
88
+ },
89
+ };
90
+ const sun = {
91
+ name: "Sun",
92
+ core: 0.7,
93
+ decorate: (t, b, out) => {
94
+ const count = t.int("sun.n", 6, 9);
95
+ const distance = b.rx * t.num("sun.dist", 1, 1.08);
96
+ const radius = b.rx * t.num("sun.r", 0.2, 0.26);
97
+ const offset = t.num("sun.rot", 0, 2 * Math.PI);
98
+ for (let i = 0; i < count; i++) {
99
+ const angle = offset + (2 * Math.PI * i) / count;
100
+ out.circles.push({
101
+ cx: b.cx + Math.cos(angle) * distance,
102
+ cy: b.cy + Math.sin(angle) * distance,
103
+ r: radius,
104
+ });
105
+ }
106
+ },
107
+ };
108
+ const triangle = {
109
+ name: "Triangle",
110
+ core: 1.15,
111
+ path: polygonPath,
112
+ body: (t, b) => {
113
+ b.sides = 3;
114
+ b.rot = t.num("body.rot", -5, 5);
115
+ b.round = t.num("poly.round", 0.24, 0.5);
116
+ },
117
+ face: (b) => ({ cx: b.cx, cy: b.cy + b.ry * 0.1, rx: b.rx * 0.54, ry: b.ry * 0.36 }),
118
+ };
119
+ /** `[shape, upper edge]`, so everyday shapes are common and loud ones are finds. */
120
+ export const BANDS = [
121
+ [round, 0.22],
122
+ [organic, 0.48],
123
+ [boxy, 0.6],
124
+ [capsule, 0.7],
125
+ [nub, 0.79],
126
+ [cloud, 0.86],
127
+ [droplet, 0.915],
128
+ [hexagon, 0.95],
129
+ [sun, 0.98],
130
+ [triangle, 1],
131
+ ];
132
+ /**
133
+ * Plain shapes, kept out of `BANDS` so that no name's blob changes. They are offered once
134
+ * Possibilities is set, so even its lowest level has eight shapes to tell variants apart.
135
+ */
136
+ const r2 = (value) => Math.round(value * 100) / 100;
137
+ export const PLAIN = [
138
+ {
139
+ name: "Oval",
140
+ core: 0.95,
141
+ body: (_t, b) => {
142
+ b.rx *= 0.8;
143
+ b.ry *= 1.12;
144
+ b.n = 2;
145
+ },
146
+ },
147
+ {
148
+ name: "Pill",
149
+ core: 0.98,
150
+ body: (t, b) => {
151
+ b.rx *= 0.62;
152
+ b.n = t.num("pill.n", 2.6, 3.2);
153
+ },
154
+ face: shrunk(0.9),
155
+ },
156
+ {
157
+ name: "Spindle",
158
+ core: 1.04,
159
+ body: (t, b) => {
160
+ b.ry *= t.num("spindle.squat", 0.5, 0.62);
161
+ },
162
+ face: shrunk(0.66),
163
+ // A lens: two curves that meet at a point at either end.
164
+ path: ({ cx, cy, rx, ry }) => `M${r2(cx - rx)} ${r2(cy)}Q${r2(cx)} ${r2(cy - ry * 2)} ${r2(cx + rx)} ${r2(cy)}Q${r2(cx)} ${r2(cy + ry * 2)} ${r2(cx - rx)} ${r2(cy)}Z`,
165
+ },
166
+ {
167
+ name: "Diamond",
168
+ core: 0.84,
169
+ body: (t, b) => {
170
+ b.n = t.num("diamond.n", 2.6, 3.4);
171
+ b.rot = 45;
172
+ },
173
+ face: shrunk(0.74),
174
+ },
175
+ {
176
+ name: "Wide",
177
+ core: 1.04,
178
+ body: (_t, b) => {
179
+ b.ry *= 0.66;
180
+ b.n = 2.2;
181
+ },
182
+ face: shrunk(0.9),
183
+ },
184
+ ];
185
+ export const pickShape = (value) => (BANDS.find(([, edge]) => value < edge) ?? BANDS[BANDS.length - 1])[0];
186
+ export const drawBody = (shape, body) => shape.path ? shape.path(body) : superellipse(body);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Possibilities for Blobs. The shared calming evens out proportions. Shapes need more, as
3
+ * the plain ones sit outside `BANDS`: every level offers at least eight, so the eight
4
+ * variants never repeat one. Left alone, a blob is exactly as it always was.
5
+ */
6
+ import type { Traits } from "../traits.ts";
7
+ import { type Shape } from "./shapes.ts";
8
+ /** A shape picked by hand, as a position along `EVERY_SHAPE`. */
9
+ export declare const SHAPE_PICK = "shape.pick";
10
+ /** From the plainest to the wildest. The first eight are the lowest level. */
11
+ export declare const EVERY_SHAPE: Shape[];
12
+ /** The shapes on offer: those at the level set, or the usual ones. */
13
+ export declare function shapesOffered(t: Traits): Shape[];
14
+ /** The blob's shape: one picked by hand, else one of those on offer. */
15
+ export declare function shapeFor(t: Traits): Shape;
16
+ /** The setting that picks `shape` by hand. */
17
+ export declare const pickOf: (shape: Shape) => {
18
+ "shape.pick": number;
19
+ };
20
+ /** Proportions and features: a low blob has even eyes and a regular outline. */
21
+ export declare const calmed: (t: Traits) => Traits;
22
+ export declare const NOTES: [string, string, string];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Possibilities for Blobs. The shared calming evens out proportions. Shapes need more, as
3
+ * the plain ones sit outside `BANDS`: every level offers at least eight, so the eight
4
+ * variants never repeat one. Left alone, a blob is exactly as it always was.
5
+ */
6
+ import { calmed as calmedBy, varianceSet } from "../possibilities.js";
7
+ import { BANDS, pickShape, PLAIN } from "./shapes.js";
8
+ /** A shape picked by hand, as a position along `EVERY_SHAPE`. */
9
+ export const SHAPE_PICK = "shape.pick";
10
+ const byName = (name) => [...BANDS.map(([shape]) => shape), ...PLAIN].find((shape) => shape.name === name);
11
+ /** From the plainest to the wildest. The first eight are the lowest level. */
12
+ export const EVERY_SHAPE = [
13
+ "Round", "Boxy", "Capsule", "Oval", "Pill", "Spindle", "Diamond", "Wide",
14
+ "Droplet", "Hexagon", "Organic", "Triangle", "Nub", "Cloud", "Sun",
15
+ ].map(byName);
16
+ const FEWEST = 8;
17
+ /** The shapes on offer: those at the level set, or the usual ones. */
18
+ export function shapesOffered(t) {
19
+ const variance = varianceSet(t);
20
+ if (variance === undefined)
21
+ return BANDS.map(([shape]) => shape);
22
+ return EVERY_SHAPE.slice(0, FEWEST + Math.floor(variance * (EVERY_SHAPE.length - FEWEST + 1)));
23
+ }
24
+ /** The blob's shape: one picked by hand, else one of those on offer. */
25
+ export function shapeFor(t) {
26
+ const picked = t.forced(SHAPE_PICK);
27
+ if (picked !== undefined)
28
+ return EVERY_SHAPE[Math.floor(picked * EVERY_SHAPE.length)];
29
+ if (varianceSet(t) === undefined)
30
+ return pickShape(t("shape"));
31
+ const offered = shapesOffered(t);
32
+ return offered[Math.floor(t("shape") * offered.length)];
33
+ }
34
+ /** The setting that picks `shape` by hand. */
35
+ export const pickOf = (shape) => ({ [SHAPE_PICK]: (EVERY_SHAPE.indexOf(shape) + 0.5) / EVERY_SHAPE.length });
36
+ /** Proportions and features: a low blob has even eyes and a regular outline. */
37
+ export const calmed = (t) => calmedBy(t, { features: /^(body|eye|gaze|capsule|nub|cloud|droplet|poly|sun)\./ });
38
+ export const NOTES = [
39
+ "calm, simple shapes like circles, ovals, pills and rounded squares, with even eyes.",
40
+ "a wider mix, adding drops, hexagons and soft lumps.",
41
+ "anything goes, from lumpy clouds to triangles and rays.",
42
+ ];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * What is bolted onto the head. These are drawn in the accent colour and inflated
3
+ * separately from the head, so in 3D they read as parts rather than as lumps.
4
+ *
5
+ * `headroom` and `sideroom` are measured from this head, so nothing leaves the frame.
6
+ */
7
+ import type { Head } from "./heads.ts";
8
+ import type { Traits } from "../traits.ts";
9
+ /** Where the ear discs sit, or nothing if this bot has none. */
10
+ export declare function earDiscs(t: Traits, head: Head): {
11
+ x: number;
12
+ y: number;
13
+ r: number;
14
+ }[];
15
+ /** In the order the generator picks from. Antenna is listed there twice, so it counts double. */
16
+ export declare const TOP_OPTIONS: ({
17
+ label: string;
18
+ weight: number;
19
+ } | {
20
+ weight?: undefined;
21
+ label: string;
22
+ })[];
23
+ export declare const SIDE_NAMES: string[];
24
+ export declare const buildAttachments: (t: Traits, head: Head) => string[];
@@ -0,0 +1,119 @@
1
+ /**
2
+ * What is bolted onto the head. These are drawn in the accent colour and inflated
3
+ * separately from the head, so in 3D they read as parts rather than as lumps.
4
+ *
5
+ * `headroom` and `sideroom` are measured from this head, so nothing leaves the frame.
6
+ */
7
+ import { circle, rod, roundedPolygon, superellipse } from "./paths.js";
8
+ const MARGIN = 5;
9
+ const headroom = (head) => head.cy - head.ry - MARGIN;
10
+ const sideroom = (head) => head.cx - head.rx - MARGIN;
11
+ const clear = () => [];
12
+ function antenna(t, head) {
13
+ const ball = t.num("top.ball", 4.2, 5.8);
14
+ const length = Math.min(t.num("top.length", 12, 20), headroom(head) - ball);
15
+ const lean = t.jitter("top.lean", 6);
16
+ const top = head.cy - head.ry;
17
+ const [x, y] = [head.cx + lean, top - length];
18
+ return [rod(head.cx, top + 4, x, y, 1.9), circle(x, y, ball)];
19
+ }
20
+ function twinAntennas(t, head) {
21
+ const ball = t.num("top.ball", 3.2, 4.2);
22
+ const length = Math.min(t.num("top.length", 11, 17), headroom(head) - ball);
23
+ const spread = t.num("top.spread", 8, 13);
24
+ const top = head.cy - head.ry;
25
+ return [-1, 1].flatMap((side) => {
26
+ const [x, y] = [head.cx + side * (spread + 5), top - length];
27
+ return [rod(head.cx + side * spread * 0.6, top + 4, x, y, 1.7), circle(x, y, ball)];
28
+ });
29
+ }
30
+ function bolt(t, head) {
31
+ const r = t.num("top.bolt", 5.5, 7.5);
32
+ const top = head.cy - head.ry;
33
+ return [
34
+ superellipse({ cx: head.cx, cy: top - 3, rx: r * 1.5, ry: r * 0.9, n: 4 }),
35
+ superellipse({ cx: head.cx, cy: top + 2, rx: r * 0.8, ry: r, n: 3 }),
36
+ ];
37
+ }
38
+ function crest(t, head) {
39
+ const top = head.cy - head.ry;
40
+ const bumps = t.int("top.bumps", 3, 4);
41
+ return Array.from({ length: bumps }, (_, i) => {
42
+ const offset = (i - (bumps - 1) / 2) * 8.5;
43
+ const r = 6.4 - Math.abs(offset) * 0.12;
44
+ return circle(head.cx + offset, top - Math.min(4.6, headroom(head) - r) + Math.abs(offset) * 0.18, r);
45
+ });
46
+ }
47
+ function catEars(t, head) {
48
+ const top = head.cy - head.ry;
49
+ const height = Math.min(t.num("top.ear", 13, 18), headroom(head));
50
+ const spread = head.rx * t.num("top.spread", 0.46, 0.6);
51
+ return [-1, 1].map((side) => roundedPolygon([
52
+ [head.cx + side * (spread - 10), top + 6],
53
+ [head.cx + side * (spread + 2), top - height],
54
+ [head.cx + side * (spread + 12), top + 6],
55
+ ], 0.42));
56
+ }
57
+ function propeller(t, head) {
58
+ const top = head.cy - head.ry;
59
+ const lift = Math.min(t.num("top.length", 7, 11), headroom(head) - 3);
60
+ const span = t.num("top.span", 13, 18);
61
+ const y = top - lift;
62
+ return [
63
+ rod(head.cx, top + 4, head.cx, y, 1.9),
64
+ superellipse({ cx: head.cx - span / 2 - 1, cy: y, rx: span / 2 + 1, ry: 3, n: 2.2 }),
65
+ superellipse({ cx: head.cx + span / 2 + 1, cy: y, rx: span / 2 + 1, ry: 3, n: 2.2 }),
66
+ circle(head.cx, y, 3.4),
67
+ ];
68
+ }
69
+ /** Where the ear discs sit, or nothing if this bot has none. */
70
+ export function earDiscs(t, head) {
71
+ if (Math.floor(t("side") * SIDES.length) !== 0)
72
+ return [];
73
+ const r = Math.min(t.num("side.r", 7, 10), sideroom(head) / 2 + 3);
74
+ const y = head.cy + t.num("side.y", -5, 3);
75
+ return [-1, 1].map((side) => ({ x: head.cx + side * (head.rx + r * 0.2), y, r }));
76
+ }
77
+ const discs = (t, head) => earDiscs(t, head).map(({ x, y, r }) => circle(x, y, r));
78
+ function earBolts(t, head) {
79
+ const ball = t.num("side.ball", 3.6, 4.8);
80
+ const reach = Math.min(t.num("side.reach", 7, 11), sideroom(head) - ball);
81
+ const y = head.cy + t.num("side.y", -6, 2);
82
+ return [-1, 1].flatMap((side) => {
83
+ const x = head.cx + side * (head.rx + reach);
84
+ return [rod(head.cx + side * (head.rx - 4), y, x, y, 1.8), circle(x, y, ball)];
85
+ });
86
+ }
87
+ function headphones(t, head) {
88
+ const rx = Math.min(t.num("side.cup", 6, 7.5), sideroom(head) / 2 + 2);
89
+ const ry = Math.min(head.ry * 0.6, t.num("side.tall", 13, 16));
90
+ return [-1, 1].map((side) => superellipse({ cx: head.cx + side * (head.rx + rx * 0.3), cy: head.cy, rx, ry, n: 3.4 }));
91
+ }
92
+ function fins(t, head) {
93
+ const reach = Math.min(t.num("side.reach", 8, 12), sideroom(head));
94
+ const y = head.cy + t.num("side.y", -6, 4);
95
+ return [-1, 1].map((side) => {
96
+ const root = head.cx + side * (head.rx - 5);
97
+ return roundedPolygon([
98
+ [root, y - 11],
99
+ [head.cx + side * (head.rx + reach), y + 2],
100
+ [root, y + 11],
101
+ ], 0.5);
102
+ });
103
+ }
104
+ const SIDES = [discs, earBolts, headphones, fins, clear];
105
+ /** In the order the generator picks from. Antenna is listed there twice, so it counts double. */
106
+ export const TOP_OPTIONS = [
107
+ { label: "Antenna", weight: 2 },
108
+ { label: "Twin antennas" },
109
+ { label: "Bolt" },
110
+ { label: "Crest" },
111
+ { label: "Cat ears" },
112
+ { label: "Propeller" },
113
+ { label: "None" },
114
+ ];
115
+ export const SIDE_NAMES = ["Discs", "Bolts", "Headphones", "Fins", "None"];
116
+ export const buildAttachments = (t, head) => [
117
+ ...t.pick("top", [antenna, antenna, twinAntennas, bolt, crest, catEars, propeller, clear])(t, head),
118
+ ...t.pick("side", SIDES)(t, head),
119
+ ];