@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.
Files changed (171) hide show
  1. package/GUIDE.md +44 -0
  2. package/LICENSE +58 -0
  3. package/README.md +141 -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 +125 -0
  64. package/dist/controls.js +220 -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 +13 -0
  165. package/dist/variants.js +43 -0
  166. package/mcp/server.js +7 -0
  167. package/mcp/tools.js +70 -0
  168. package/package.json +68 -4
  169. package/react/index.d.ts +28 -0
  170. package/react/index.js +869 -0
  171. package/react/styles.js +1 -0
@@ -0,0 +1,54 @@
1
+ import { choice, palette, slider, toggle, turnSlider } from "../controls.js";
2
+ import { calmed, narrowed, possibilitiesSlider } from "../possibilities.js";
3
+ import { colorKey, createTraits } from "../traits.js";
4
+ import { CALMING, NOTES } from "./calming.js";
5
+ import { BODIES, bodyOf, coloursOf, EYE_KINDS, FORMATIONS, formationOf, NATURAL_ANGLE, POSES, TILES } from "./index.js";
6
+ export const controls = (name, overrides) => {
7
+ const t = calmed(createTraits(name, overrides), CALMING);
8
+ const list = (control) => narrowed(control, t, CALMING);
9
+ const { body, iris } = coloursOf(t);
10
+ return [
11
+ {
12
+ title: "Shape",
13
+ controls: [
14
+ possibilitiesSlider(t, NOTES),
15
+ list(choice(t, "body", "Body", "list", BODIES.map(({ name: label }) => ({ label })))),
16
+ turnSlider(t, NATURAL_ANGLE),
17
+ list(choice(t, "pose", "Peeking from", "list", POSES.map(({ name: label }) => ({ label })))),
18
+ ...(bodyOf(t).counted
19
+ ? [choice(t, "bumps", "Bumps", "list", [5, 6, 7, 8, 9].map((count) => ({ label: String(count) })))]
20
+ : []),
21
+ slider(t, "fluff", "Depth", 0, 100, "%"),
22
+ slider(t, "body.size", "Body size", 0, 100, "%"),
23
+ ],
24
+ },
25
+ {
26
+ title: "Colour",
27
+ controls: [
28
+ palette(t, "tile", "Colour", TILES),
29
+ {
30
+ type: "colors",
31
+ id: "peek.colors",
32
+ label: "Body, eyes",
33
+ colors: [
34
+ { id: colorKey("body"), color: body },
35
+ { id: colorKey("iris"), color: iris },
36
+ ],
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ title: "Eyes",
42
+ controls: [
43
+ choice(t, "eye.formation", "Formation", "list", FORMATIONS),
44
+ list(choice(t, "eye.kind", "Eyes", "list", EYE_KINDS.map((label) => ({ label })))),
45
+ toggle(t, "brows", 80, "Brows"),
46
+ slider(t, "eye.size", "Eye size", 0, 100, "%"),
47
+ // A single eye has nothing to sit apart from, or to lean against.
48
+ ...(formationOf(t) === "One"
49
+ ? []
50
+ : [slider(t, "eye.gap", "Gap", 0, 100, "%"), slider(t, "tilt", "Tilt", -8, 8, "°")]),
51
+ ],
52
+ },
53
+ ];
54
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Peeks: a creature peering into a bright tile from somewhere off its edge. Its outline
3
+ * is echoed by contour rings that fade into the tile, and it has big glossy eyes.
4
+ * Everything is flat fills and gradients, so all of it can move cheaply.
5
+ */
6
+ import { type Overrides, type Traits } from "../traits.ts";
7
+ export declare const TILES: string[];
8
+ export declare const EFFECTS: readonly ["peek", "pop", "wobble"];
9
+ export type Effect = (typeof EFFECTS)[number];
10
+ export declare const EYE_KINDS: string[];
11
+ /** How many eyes it has, and how they sit. Most have two, as the rest are surprises. */
12
+ export declare const FORMATIONS: {
13
+ label: string;
14
+ weight: number;
15
+ }[];
16
+ interface Pose {
17
+ name: string;
18
+ /** Where it peers in from, which can be off the tile, and how big it is. */
19
+ x: number;
20
+ y: number;
21
+ size: number;
22
+ /** Where its eyes sit, and how they lean. */
23
+ eyeX: number;
24
+ eyeY: number;
25
+ lean: number;
26
+ effect: Effect;
27
+ }
28
+ /** Each comes in from a different side, so no two variants start from the same place. */
29
+ export declare const POSES: Pose[];
30
+ interface Body {
31
+ name: string;
32
+ /** Has a number of bumps worth choosing. */
33
+ counted?: boolean;
34
+ /**
35
+ * How far the radius strays, given the angle from the way it faces. `depth` scales
36
+ * it, `count` is the bumps, `start` slips the waves, and `pick` gives a stable value.
37
+ */
38
+ shape(angle: number, depth: number, count: number, start: number, pick: (i: number) => number): number;
39
+ }
40
+ /** What a creature is made of. The first is the cloud in the reference. */
41
+ export declare const BODIES: Body[];
42
+ export declare const VIEW_BOX = "5 5 90 90";
43
+ export declare const TILE_SHAPE = "<rect x=\"5\" y=\"5\" width=\"90\" height=\"90\" rx=\"20\"/>";
44
+ /** Left to itself an angle stays this close to upright, as upside down reads as a mistake. */
45
+ export declare const NATURAL_ANGLE = 32;
46
+ export declare const poseOf: (t: Traits) => Pose;
47
+ export declare const bodyOf: (t: Traits) => Body;
48
+ export declare const eyeKindOf: (t: Traits) => string;
49
+ export declare function formationOf(t: Traits): string;
50
+ /** The colours in use, each of which can be changed by hand. */
51
+ export declare function coloursOf(t: Traits): {
52
+ tile: string;
53
+ body: string;
54
+ iris: string;
55
+ };
56
+ export interface Peek {
57
+ defs: string;
58
+ tile: string;
59
+ /** The turn of the rings, the creature and its face together, in degrees. */
60
+ angle: number;
61
+ turn: string;
62
+ rings: string;
63
+ body: string;
64
+ face: string;
65
+ effect: Effect;
66
+ /** Which way it leans further in, and the point its rings spread from. */
67
+ into: {
68
+ x: number;
69
+ y: number;
70
+ };
71
+ origin: {
72
+ x: number;
73
+ y: number;
74
+ };
75
+ blinkDelay: number;
76
+ }
77
+ export declare function createPeek(name: string, overrides?: Overrides, ids?: string): Peek;
78
+ /** The peek at rest, for thumbnails. */
79
+ export declare const peekSvg: (name: string, overrides?: Overrides) => string;
80
+ export {};
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Peeks: a creature peering into a bright tile from somewhere off its edge. Its outline
3
+ * is echoed by contour rings that fade into the tile, and it has big glossy eyes.
4
+ * Everything is flat fills and gradients, so all of it can move cheaply.
5
+ */
6
+ import { BLACK, mix, WHITE } from "../colors.js";
7
+ import { turnOf } from "../controls.js";
8
+ import { scopeAll } from "../art.js";
9
+ import { calmed } from "../possibilities.js";
10
+ import { createTraits } from "../traits.js";
11
+ import { CALMING } from "./calming.js";
12
+ export const TILES = [
13
+ "#3f4ff0", "#7a3dff", "#0fa3b1", "#ff4f8b", "#ff7a1a", "#17a85a", "#e8344a", "#1f2a6b", "#c23dd6",
14
+ ];
15
+ export const EFFECTS = ["peek", "pop", "wobble"];
16
+ export const EYE_KINDS = ["Glossy", "Sleepy", "Tiny"];
17
+ /** How many eyes it has, and how they sit. Most have two, as the rest are surprises. */
18
+ export const FORMATIONS = [
19
+ { label: "Pair", weight: 5 },
20
+ { label: "Close", weight: 2 },
21
+ { label: "Wide", weight: 2 },
22
+ { label: "One", weight: 1.5 },
23
+ { label: "Three", weight: 1.5 },
24
+ ];
25
+ /** Each comes in from a different side, so no two variants start from the same place. */
26
+ export const POSES = [
27
+ { name: "Corner", x: 68, y: 96, size: 60, eyeX: 62, eyeY: 67, lean: -9, effect: "peek" },
28
+ { name: "Hanging", x: 50, y: -14, size: 66, eyeX: 50, eyeY: 33, lean: 4, effect: "wobble" },
29
+ { name: "Right", x: 106, y: 56, size: 67, eyeX: 67, eyeY: 53, lean: -7, effect: "peek" },
30
+ { name: "Other corner", x: 32, y: 96, size: 60, eyeX: 38, eyeY: 67, lean: 9, effect: "pop" },
31
+ { name: "Top corner", x: 76, y: 2, size: 62, eyeX: 66, eyeY: 32, lean: 8, effect: "wobble" },
32
+ { name: "Left", x: -6, y: 56, size: 67, eyeX: 33, eyeY: 53, lean: 7, effect: "peek" },
33
+ { name: "Rising", x: 50, y: 112, size: 70, eyeX: 50, eyeY: 67, lean: -3, effect: "pop" },
34
+ { name: "Top left", x: 24, y: 2, size: 62, eyeX: 34, eyeY: 32, lean: -8, effect: "wobble" },
35
+ { name: "Whole", x: 50, y: 54, size: 31, eyeX: 50, eyeY: 54, lean: -5, effect: "pop" },
36
+ ];
37
+ /** A bump `width` across, `height` tall, at `at` radians from the way it is facing. */
38
+ const bump = (angle, at, width, height) => {
39
+ const away = Math.atan2(Math.sin(angle - at), Math.cos(angle - at));
40
+ return height * Math.exp(-((away / width) ** 2));
41
+ };
42
+ /** What a creature is made of. The first is the cloud in the reference. */
43
+ export const BODIES = [
44
+ {
45
+ name: "Fluffy",
46
+ counted: true,
47
+ shape: (a, depth, count, start) => depth * Math.cos(count * a + start) + 0.03 * Math.cos(3 * a + start),
48
+ },
49
+ { name: "Ears", shape: (a, depth) => bump(a, -0.42, 0.17, depth * 4.4) + bump(a, 0.42, 0.17, depth * 4.4) },
50
+ {
51
+ name: "Swell",
52
+ shape: (a, depth, _count, start) => depth * 2.4 * Math.cos(3 * a + start) + depth * 0.9 * Math.cos(5 * a + start * 2),
53
+ },
54
+ { name: "Tuft", shape: (a, depth) => bump(a, 0, 0.14, depth * 6) + bump(a, -0.3, 0.09, depth * 3.2) + bump(a, 0.32, 0.1, depth * 3.6) },
55
+ {
56
+ name: "Spiky",
57
+ counted: true,
58
+ shape: (a, depth, count, start) => depth * 1.1 * (Math.abs(Math.sin(((count + 5) * a + start) / 2)) * 2 - 1),
59
+ },
60
+ { name: "Horns", shape: (a, depth) => bump(a, -0.62, 0.1, depth * 5.4) + bump(a, 0.62, 0.1, depth * 5.4) },
61
+ {
62
+ name: "Drips",
63
+ shape: (a, depth, _count, _start, pick) => [-0.9, -0.45, 0, 0.5, 0.95].reduce((sum, at, i) => sum + bump(a, at + pick(i) * 0.1, 0.11, depth * (2.5 + pick(i + 5) * 3)), 0),
64
+ },
65
+ { name: "Dome", shape: (a, depth) => depth * 0.4 * Math.cos(2 * a) + bump(a, 0, 0.5, depth * 2.2) },
66
+ {
67
+ name: "Lumpy",
68
+ shape: (a, depth, _count, start) => depth * 1.7 * Math.cos(2 * a + start) + depth * 1.3 * Math.cos(3 * a + start * 3) + depth * Math.cos(7 * a + start * 5),
69
+ },
70
+ ];
71
+ export const VIEW_BOX = "5 5 90 90";
72
+ export const TILE_SHAPE = `<rect x="5" y="5" width="90" height="90" rx="20"/>`;
73
+ const OUTLINE_POINTS = 360;
74
+ const RINGS = 3;
75
+ /** How much further out each ring sits. */
76
+ const RING_STEP = 0.13;
77
+ /** Where a sparkle's four points reach, as a star around the origin. */
78
+ const SPARKLE = "M0-1Q.12-.12 1 0Q.12 .12 0 1Q-.12 .12-1 0Q-.12-.12 0-1Z";
79
+ /** Left to itself an angle stays this close to upright, as upside down reads as a mistake. */
80
+ export const NATURAL_ANGLE = 32;
81
+ /** How close an eye may come to the frame, which leaves it clear of the tile's edge. */
82
+ const FACE_EDGE = 9;
83
+ const round2 = (value) => Math.round(value * 100) / 100;
84
+ export const poseOf = (t) => t.pick("pose", POSES);
85
+ export const bodyOf = (t) => t.pick("body", BODIES);
86
+ export const eyeKindOf = (t) => t.pick("eye.kind", EYE_KINDS);
87
+ const angleOf = (t) => turnOf(t, NATURAL_ANGLE);
88
+ export function formationOf(t) {
89
+ const position = t("eye.formation");
90
+ const total = FORMATIONS.reduce((sum, { weight }) => sum + weight, 0);
91
+ let edge = 0;
92
+ for (const { label, weight } of FORMATIONS) {
93
+ edge += weight / total;
94
+ if (position < edge)
95
+ return label;
96
+ }
97
+ return FORMATIONS[0].label;
98
+ }
99
+ /** The colours in use, each of which can be changed by hand. */
100
+ export function coloursOf(t) {
101
+ const tile = t.color("tile") ?? t.pick("tile", TILES);
102
+ return {
103
+ tile,
104
+ body: t.color("body") ?? mix(tile, WHITE, 0.86),
105
+ iris: t.color("iris") ?? mix(tile, BLACK, 0.66),
106
+ };
107
+ }
108
+ function drawOutline(t, pose, body, grown) {
109
+ const size = pose.size * t.num("body.size", 0.92, 1.08);
110
+ const depth = t.num("fluff", 0.04, 0.09);
111
+ const count = t.int("bumps", 5, 9);
112
+ // Each ring's waves slip a little from the last, so they read as drawn contours.
113
+ const start = t.num("wave", 0, 2 * Math.PI) + grown * 2.4;
114
+ const pick = (i) => t(`body.pick${i}`);
115
+ const facing = pose.x === 50 && pose.y === 54 ? -Math.PI / 2 : Math.atan2(54 - pose.y, 50 - pose.x);
116
+ const points = Array.from({ length: OUTLINE_POINTS }, (_, i) => {
117
+ const angle = (i * 2 * Math.PI) / OUTLINE_POINTS;
118
+ const r = size * (1 + grown) * (1 + body.shape(angle - facing, depth, count, start, pick));
119
+ return `${round2(pose.x + r * Math.cos(angle))} ${round2(pose.y + r * Math.sin(angle))}`;
120
+ });
121
+ return `M${points.join("L")}Z`;
122
+ }
123
+ function drawEye(t, side, radius, x, y, body, tile) {
124
+ const kind = eyeKindOf(t);
125
+ const size = round2(radius * (kind === "Tiny" ? 0.56 : 0.86));
126
+ // The iris sits off centre, which leaves the white as a crescent.
127
+ const offX = round2(radius - size - 0.8);
128
+ const offY = round2(-(radius - size) * 0.7);
129
+ const sparkle = round2(size * 0.34);
130
+ const glint = `translate(${round2(offX + size * 0.26)} ${round2(offY - size * 0.26)}) scale(${sparkle})`;
131
+ // Covers only the top third, with a line along it, so it still reads as an eye.
132
+ const lidY = round2(-radius * 0.36);
133
+ const lidWide = round2(Math.sqrt(radius ** 2 - lidY ** 2));
134
+ const cover = round2(radius + 0.6);
135
+ const lid = kind === "Sleepy"
136
+ ? `<path d="M${-lidWide - 0.6} ${lidY}A${cover} ${cover} 0 0 1 ${lidWide + 0.6} ${lidY}Z" fill="${body}"/>` +
137
+ `<path d="M${-lidWide} ${lidY}H${lidWide}" stroke="${tile}" stroke-width="1.5" stroke-linecap="round"/>`
138
+ : "";
139
+ const lift = round2(t.num("brow.y", 1.5, 4));
140
+ const arch = round2(radius * 0.74);
141
+ const brow = t("brows") * 100 < 80
142
+ ? `<path data-brow="" d="M${round2(side * arch - 3.6)} ${round2(-radius - lift + 1)}q3.6 -3 7.2 -.4" fill="none" stroke="${tile}" stroke-width="1.6" stroke-linecap="round"/>`
143
+ : "";
144
+ return (`<g transform="translate(${round2(x)} ${round2(y)})">${brow}<g data-eye="">` +
145
+ `<circle r="${round2(radius)}" fill="#fff"/>` +
146
+ `<g data-iris=""><circle cx="${offX}" cy="${offY}" r="${size}" fill="url(#peek-iris)"/>` +
147
+ `<path d="${SPARKLE}" transform="${glint}" fill="#fff"/>` +
148
+ `<circle cx="${round2(offX + size * 0.56)}" cy="${round2(offY - size * 0.02)}" r="${round2(size * 0.08)}" fill="#fff"/></g>` +
149
+ `${lid}</g></g>`);
150
+ }
151
+ function drawFace(t, pose, body, tile) {
152
+ const formation = formationOf(t);
153
+ const scale = t.num("eye.size", 0.8, 1.2);
154
+ const lean = pose.lean + t.jitter("tilt", 8);
155
+ const radians = (lean * Math.PI) / 180;
156
+ const big = 11 * scale;
157
+ const small = big * t.num("eye.ratio", 0.68, 1);
158
+ const spread = { Close: 0.74, Wide: 1.34 }[formation] ?? 1;
159
+ const gap = t.num("eye.gap", 10.5, 14) * scale * spread;
160
+ const eyes = formation === "One"
161
+ ? [{ side: 0, radius: big * 1.42, by: 0 }]
162
+ : formation === "Three"
163
+ ? [{ side: -1, radius: small * 0.84, by: -gap * 1.5 }, { side: 0, radius: big, by: 0 }, { side: 1, radius: small * 0.84, by: gap * 1.5 }]
164
+ : [{ side: -1, radius: big, by: -gap }, { side: 1, radius: small, by: gap }];
165
+ // Measured where they end up after the turn, as that's where the tile could cut one off.
166
+ const turn = (angleOf(t) * Math.PI) / 180;
167
+ const spots = eyes.map(({ radius, by }) => {
168
+ const x = pose.eyeX - 50 + Math.cos(radians) * by;
169
+ const y = pose.eyeY - 50 + Math.sin(radians) * by;
170
+ return { radius, across: 50 + x * Math.cos(turn) - y * Math.sin(turn), down: 50 + x * Math.sin(turn) + y * Math.cos(turn) };
171
+ });
172
+ const backInside = (axis) => {
173
+ const low = Math.min(...spots.map((spot) => spot[axis] - spot.radius));
174
+ const high = Math.max(...spots.map((spot) => spot[axis] + spot.radius));
175
+ return low < FACE_EDGE ? FACE_EDGE - low : high > 100 - FACE_EDGE ? 100 - FACE_EDGE - high : 0;
176
+ };
177
+ const pushX = backInside("across");
178
+ const pushY = backInside("down");
179
+ // The face is drawn before the turn, so the push is turned back the other way.
180
+ const shiftX = pushX * Math.cos(turn) + pushY * Math.sin(turn);
181
+ const shiftY = pushY * Math.cos(turn) - pushX * Math.sin(turn);
182
+ return eyes
183
+ .map(({ side, radius, by }) => drawEye(t, side, radius, pose.eyeX + shiftX + Math.cos(radians) * by, pose.eyeY + shiftY + Math.sin(radians) * by, body, tile))
184
+ .join("");
185
+ }
186
+ export function createPeek(name, overrides, ids = "peek") {
187
+ const t = calmed(createTraits(name, overrides), CALMING);
188
+ const pose = poseOf(t);
189
+ const shape = bodyOf(t);
190
+ const { tile, body, iris } = coloursOf(t);
191
+ const size = pose.size * t.num("body.size", 0.92, 1.08);
192
+ const rings = Array.from({ length: RINGS }, (_, i) => {
193
+ const ring = RINGS - i;
194
+ return `<path d="${drawOutline(t, pose, shape, ring * RING_STEP)}" fill="${mix(tile, WHITE, round2(0.05 + i * 0.05))}"/>`;
195
+ }).join("");
196
+ const toCentre = Math.hypot(50 - pose.x, 54 - pose.y) || 1;
197
+ return scopeAll({
198
+ defs: `<defs>` +
199
+ `<linearGradient id="peek-tile" gradientUnits="userSpaceOnUse" x1="50" y1="5" x2="50" y2="95">` +
200
+ `<stop offset="0" stop-color="${tile}"/><stop offset="1" stop-color="${mix(tile, WHITE, 0.14)}"/></linearGradient>` +
201
+ `<linearGradient id="peek-body" gradientUnits="userSpaceOnUse" x1="50" y1="${round2(pose.y - size)}" x2="50" y2="${round2(pose.y + size)}">` +
202
+ `<stop offset="0" stop-color="${body}"/><stop offset="1" stop-color="${mix(body, WHITE, 0.6)}"/></linearGradient>` +
203
+ `<linearGradient id="peek-iris" x1="0.5" y1="0" x2="0.5" y2="1">` +
204
+ `<stop offset="0" stop-color="${iris}"/><stop offset="1" stop-color="${mix(iris, tile, 0.62)}"/></linearGradient>` +
205
+ `</defs>`,
206
+ tile: "url(#peek-tile)",
207
+ angle: angleOf(t),
208
+ turn: `rotate(${round2(angleOf(t))} 50 50)`,
209
+ rings,
210
+ body: `<path d="${drawOutline(t, pose, shape, 0)}" fill="url(#peek-body)"/>`,
211
+ face: drawFace(t, pose, body, tile),
212
+ effect: pose.effect,
213
+ into: { x: round2(((50 - pose.x) / toCentre) * 7), y: round2(((54 - pose.y) / toCentre) * 7) },
214
+ origin: { x: pose.x, y: pose.y },
215
+ blinkDelay: t.num("blink", 0, 4),
216
+ }, "peek", ids);
217
+ }
218
+ /** The peek at rest, for thumbnails. */
219
+ export const peekSvg = (name, overrides) => {
220
+ const peek = createPeek(name, overrides);
221
+ const tile = TILE_SHAPE.replace("/>", ` fill="${peek.tile}"/>`);
222
+ return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${VIEW_BOX}">${peek.defs}${tile}` +
223
+ `<clipPath id="peek-box">${TILE_SHAPE}</clipPath>` +
224
+ `<g clip-path="url(#peek-box)"><g transform="${peek.turn}">${peek.rings}${peek.body}${peek.face}</g></g></svg>`);
225
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * What Possibilities calms on a pixel person. Low keeps eight everyday hairstyles, one for
3
+ * each variant, a smile, plain hats, and makes hats, beards, glasses, marks and earrings
4
+ * rarer. Indices are into the lists in `sprites.ts` and `index.ts`, plainest first.
5
+ */
6
+ import type { Calming } from "../possibilities.ts";
7
+ export declare const CALMING: Calming;
8
+ export declare const NOTES: [string, string, string];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What Possibilities calms on a pixel person. Low keeps eight everyday hairstyles, one for
3
+ * each variant, a smile, plain hats, and makes hats, beards, glasses, marks and earrings
4
+ * rarer. Indices are into the lists in `sprites.ts` and `index.ts`, plainest first.
5
+ */
6
+ export const CALMING = {
7
+ lists: {
8
+ // Crop, side part, bob, long, curly, afro, bun, hijab, then ponytail, slick, dreads,
9
+ // balding, and the loudest, spiky and mohawk.
10
+ hair: [14, 8, [0, 1, 6, 5, 3, 4, 7, 13, 8, 12, 10, 11, 2, 9]],
11
+ // Smile and small first, then tiny, line, smirk and frown.
12
+ mouth: [6, 2, [3, 1, 5, 0, 2, 4]],
13
+ // Cap, backwards cap and beanie, then the bucket hat and santa's.
14
+ "hat.kind": [5, 3],
15
+ },
16
+ rarer: ["hat", "beard", "glasses", "mark", "earring"],
17
+ };
18
+ export const NOTES = [
19
+ "everyday hair, a smile, and hardly ever a hat, beard, glasses or mark.",
20
+ "more hairstyles and mouths, with hats, beards and glasses now and then.",
21
+ "every hairstyle up to mohawks, every mouth, hat and extra.",
22
+ ];
@@ -0,0 +1,3 @@
1
+ import { type ControlBuilder } from "../controls.ts";
2
+ export declare const colorControls: ControlBuilder;
3
+ export declare const retroControls: ControlBuilder;
@@ -0,0 +1,82 @@
1
+ import { choice, fixedPalette, optional, palette, spaced, toggle, } from "../controls.js";
2
+ import { calmed, narrowed, possibilitiesSlider } from "../possibilities.js";
3
+ import { createTraits } from "../traits.js";
4
+ import { CALMING, NOTES } from "./calming.js";
5
+ import { MOUTH_NAMES } from "./index.js";
6
+ import { HAIR_COLORS, HAT_COLORS, isPaleHair, SCREENS, SKINS } from "./palette.js";
7
+ import { BEARDS, EYE_SPACING_NAMES, EYEBROWS, HAIRS, HATS, MARKS } from "./sprites.js";
8
+ const named = (names) => names.map((name) => ({ label: spaced(name) }));
9
+ const firsts = (colors) => colors.map(([color]) => color);
10
+ const indexOf = (value, count) => Math.floor(value * count);
11
+ /** Retro only has dark and light, so it offers those instead of every colour. */
12
+ function tone(t, key, colors, label) {
13
+ const position = (index) => (index + 0.5) / colors.length;
14
+ const pale = colors.findIndex(isPaleHair);
15
+ const dark = colors.findIndex((color) => !isPaleHair(color));
16
+ return {
17
+ type: "choice",
18
+ id: key,
19
+ label,
20
+ layout: "segmented",
21
+ options: [
22
+ { id: "dark", label: "Dark", set: { [key]: position(dark) } },
23
+ { id: "pale", label: "Light", set: { [key]: position(pale) } },
24
+ ],
25
+ selected: isPaleHair(colors[indexOf(t(key), colors.length)]) ? "pale" : "dark",
26
+ };
27
+ }
28
+ const builder = (theme) => (name, overrides) => {
29
+ const t = calmed(createTraits(name, overrides), CALMING);
30
+ const list = (control) => narrowed(control, t, CALMING);
31
+ const retro = theme === "retro";
32
+ const hair = HAIRS[indexOf(t("hair"), HAIRS.length)];
33
+ const hijab = hair.name === "hijab";
34
+ const wearsHat = hair.takesHat && t("hat") * 100 < 38;
35
+ const hat = HATS[indexOf(t("hat.kind"), HATS.length)];
36
+ const wearsGlasses = t("glasses") * 100 < 24;
37
+ const hairColor = retro
38
+ ? hijab ? tone(t, "hat.color", HAT_COLORS, "Scarf") : tone(t, "hair.color", HAIR_COLORS, "Hair tone")
39
+ : hijab ? palette(t, "hat.color", "Scarf colour", firsts(HAT_COLORS))
40
+ : palette(t, "hair.color", "Hair colour", firsts(HAIR_COLORS));
41
+ const sections = [
42
+ {
43
+ title: "Person",
44
+ controls: [
45
+ retro ? fixedPalette(t, "screen", "Colour", SCREENS) : palette(t, "skin", "Colour", firsts(SKINS)),
46
+ choice(t, "mirrored", "Facing", "segmented", [{ label: "Right" }, { label: "Left" }]),
47
+ possibilitiesSlider(t, NOTES),
48
+ ],
49
+ },
50
+ {
51
+ title: "Hair",
52
+ controls: [list(choice(t, "hair", "Hair", "list", named(HAIRS.map((entry) => entry.name)))), hairColor],
53
+ },
54
+ {
55
+ title: "Face",
56
+ controls: [
57
+ ...(wearsGlasses
58
+ ? []
59
+ : [
60
+ choice(t, "eyes.spacing", "Eye spacing", "segmented", EYE_SPACING_NAMES.map((label) => ({ label }))),
61
+ toggle(t, "eyes.tall", 65, "Tall eyes"),
62
+ ]),
63
+ optional(t, "brows", 55, "brows.kind", "Brows", "list", named(Object.keys(EYEBROWS))),
64
+ list(choice(t, "mouth", "Mouth", "list", named(MOUTH_NAMES))),
65
+ optional(t, "mark", 30, "mark.kind", "Marks", "list", named(Object.keys(MARKS))),
66
+ ...(hijab ? [] : [optional(t, "beard", 34, "beard.kind", "Beard", "list", named(Object.keys(BEARDS)))]),
67
+ ],
68
+ },
69
+ {
70
+ title: "Accessories",
71
+ controls: [
72
+ ...(hair.takesHat ? [optional(t, "hat", 38, "hat.kind", "Hat", "list", named(HATS.map((entry) => entry.name)))] : []),
73
+ ...(!retro && wearsHat && hat.name !== "santa" ? [palette(t, "hat.color", "Hat colour", firsts(HAT_COLORS))] : []),
74
+ optional(t, "glasses", 24, "glasses.kind", "Glasses", "segmented", [{ label: "Frames", weight: 2 }, { label: "Shades" }]),
75
+ ...(hair.wraps ? [] : [toggle(t, "earring", 16, "Earring")]),
76
+ ],
77
+ },
78
+ ];
79
+ return sections;
80
+ };
81
+ export const colorControls = builder("color");
82
+ export const retroControls = builder("retro");
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pixel people. Parts from `sprites.ts` are stacked on a 20×20 grid, then each
3
+ * layer becomes SVG rectangles so it can move on its own without leaving the grid.
4
+ */
5
+ import { type Overrides } from "../traits.ts";
6
+ import { type Theme } from "./palette.ts";
7
+ export declare const GRID = 20;
8
+ /** Empty cells around the person, so tall hair never touches the tile's edge. */
9
+ export declare const MARGIN = 2;
10
+ /** The mouths a person can have, in the order they are picked from. */
11
+ export declare const MOUTH_NAMES: string[];
12
+ export interface PixelFace {
13
+ tile: string;
14
+ /** The head's shape, which keeps the moving face from sliding off it. */
15
+ headMask: string;
16
+ /** Everything that stays put: the body and hair, with any outline. */
17
+ behind: string;
18
+ /** The face moves over the head as it turns towards the pointer. */
19
+ face: string;
20
+ front: string;
21
+ hat: string | null;
22
+ glasses: string | null;
23
+ eyes: Record<"open" | "wide" | "shut", string>;
24
+ mouths: {
25
+ rest: string;
26
+ hover: string;
27
+ };
28
+ /** Shades hide the eyes, so only the head can show where they look. */
29
+ eyesVisible: boolean;
30
+ blinkDelay: number;
31
+ }
32
+ export declare function createPixelFace(name: string, theme: Theme, overrides?: Overrides): PixelFace;
33
+ /** The whole face at rest, for sheets and tests. */
34
+ export declare const pixelSvg: (name: string, theme: Theme, overrides?: Overrides) => string;