@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,69 @@
1
+ import { mix, WHITE } from "../colors.js";
2
+ import { choice, clearingColor, optional, slider } from "../controls.js";
3
+ import { calmed, narrowed, possibilitiesSlider } from "../possibilities.js";
4
+ import { colorKey, createTraits } from "../traits.js";
5
+ import { CALMING, NOTES } from "./calming.js";
6
+ import { coloursOf, defaultTitle, EYE_KINDS, MAX_TEXT, MOUTH_ODDS, MOUTHS, PALETTES, SHAPES } from "./index.js";
7
+ const COLOURS = ["core", "accent", "edge"];
8
+ /** A new palette starts from its own colours, not the ones set by hand for the last. */
9
+ function paletteControl(t) {
10
+ const swatches = PALETTES.map(({ label, core, accent }) => ({
11
+ label,
12
+ color: `radial-gradient(circle at 50% 45%, ${core} 35%, ${accent} 75%, ${mix(core, WHITE, 0.5)})`,
13
+ }));
14
+ return COLOURS.reduce(clearingColor, choice(t, "palette", "Colour", "swatches", swatches));
15
+ }
16
+ export const controls = (name, overrides) => {
17
+ const t = calmed(createTraits(name, overrides), CALMING);
18
+ const list = (control) => narrowed(control, t, CALMING);
19
+ const colours = coloursOf(t);
20
+ return [
21
+ {
22
+ title: "Words",
23
+ controls: [
24
+ {
25
+ type: "text",
26
+ id: "text.title",
27
+ label: "Title",
28
+ value: t.text("text.title") ?? "",
29
+ placeholder: defaultTitle(t),
30
+ maxLength: MAX_TEXT,
31
+ },
32
+ slider(t, "tilt", "Tilt", -13, 13, "°"),
33
+ ],
34
+ },
35
+ {
36
+ title: "Colour",
37
+ controls: [
38
+ paletteControl(t),
39
+ {
40
+ type: "colors",
41
+ id: "bubble.colors",
42
+ label: "Blend",
43
+ colors: [colours.core, colours.accent, colours.edge].map((color, i) => ({ id: colorKey(COLOURS[i]), color })),
44
+ },
45
+ slider(t, "blend", "Core size", 0, 100, "%"),
46
+ slider(t, "grain", "Grain", 0, 100, "%"),
47
+ ],
48
+ },
49
+ {
50
+ title: "Shape",
51
+ controls: [
52
+ possibilitiesSlider(t, NOTES),
53
+ list(choice(t, "shape", "Shape", "list", SHAPES.map(({ name: label }) => ({ label })))),
54
+ slider(t, "depth", "Depth", 0, 100, "%"),
55
+ slider(t, "body.size", "Body size", 0, 100, "%"),
56
+ ],
57
+ },
58
+ {
59
+ title: "Eyes",
60
+ controls: [
61
+ list(choice(t, "eye.kind", "Eyes", "list", EYE_KINDS.map((label) => ({ label })))),
62
+ optional(t, "mouth", MOUTH_ODDS, "mouth.kind", "Mouth", "list", MOUTHS.map((label) => ({ label }))),
63
+ slider(t, "eye.size", "Eye size", 0, 100, "%"),
64
+ slider(t, "eye.gap", "Gap", 0, 100, "%"),
65
+ slider(t, "eye.lean", "Lean", -100, 100, "%"),
66
+ ],
67
+ },
68
+ ];
69
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Bubbles: a soft shape with words on it. A vivid core blends out to a pale rim, under
3
+ * fine white grain that gathers towards the edge, with two eyes on top.
4
+ *
5
+ * The art and the eyes are kept apart. The art has the costly filters, so it goes on a
6
+ * layer of its own and is only ever moved, while the eyes can repaint on their own.
7
+ */
8
+ import { type Overrides, type Traits } from "../traits.ts";
9
+ /** Warm and cool take turns, so neighbouring variants never share a colour. */
10
+ export declare const PALETTES: {
11
+ label: string;
12
+ core: string;
13
+ accent: string;
14
+ }[];
15
+ export declare const EFFECTS: readonly ["squish", "wiggle", "bounce"];
16
+ export type Effect = (typeof EFFECTS)[number];
17
+ export declare const EYE_KINDS: string[];
18
+ /** Most bubbles have no mouth, as in the reference: one in this many hundred does. */
19
+ export declare const MOUTH_ODDS = 30;
20
+ export declare const MOUTHS: string[];
21
+ /** Words past this would be too small to read, however far they shrink. */
22
+ export declare const MAX_TEXT = 28;
23
+ export declare const VIEW_BOX = "-2 -2 104 104";
24
+ /** `[how many times round, how deep, where it starts]`. */
25
+ type Wave = [number, number, number];
26
+ interface Shape {
27
+ name: string;
28
+ /** How a tap moves it. The motion suits the silhouette, so it comes with it. */
29
+ effect: Effect;
30
+ /** Wider than tall above 1, and taller than wide below it. */
31
+ wide?: number;
32
+ /** How far the whole shape may lean either way, in degrees. */
33
+ lean?: number;
34
+ waves(t: Traits): Wave[];
35
+ }
36
+ /**
37
+ * Every outline is a circle with waves added to its radius. Curves made this way have
38
+ * no corners anywhere, which is what gives the shapes their soft, even edge. A wave of
39
+ * twice the count taken away flattens each tip into a fat, round lobe.
40
+ *
41
+ * Neighbours in this list are as unlike as they can be, in both proportion and lobes,
42
+ * because a variant is the next along from the one before it.
43
+ */
44
+ export declare const SHAPES: Shape[];
45
+ export declare const shapeOf: (t: Traits) => Shape;
46
+ /** The colours in use, each of which can be changed by hand. */
47
+ export declare function coloursOf(t: Traits): {
48
+ core: string;
49
+ accent: string;
50
+ edge: string;
51
+ };
52
+ export declare const defaultTitle: (t: Traits) => string;
53
+ export declare const eyeKindOf: (t: Traits) => string;
54
+ /** Which mouth, if any. */
55
+ export declare const mouthOf: (t: Traits) => string | undefined;
56
+ export interface Bubble {
57
+ art: string;
58
+ eyes: string;
59
+ effect: Effect;
60
+ blinkDelay: number;
61
+ }
62
+ export declare function createBubble(name: string, overrides?: Overrides, ids?: string): Bubble;
63
+ /** The bubble at rest, for thumbnails. */
64
+ export declare const bubbleSvg: (name: string, overrides?: Overrides) => string;
65
+ export {};
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Bubbles: a soft shape with words on it. A vivid core blends out to a pale rim, under
3
+ * fine white grain that gathers towards the edge, with two eyes on top.
4
+ *
5
+ * The art and the eyes are kept apart. The art has the costly filters, so it goes on a
6
+ * layer of its own and is only ever moved, while the eyes can repaint on their own.
7
+ */
8
+ import { mix, WHITE } from "../colors.js";
9
+ import { scopeIds } from "../art.js";
10
+ import { calmed } from "../possibilities.js";
11
+ import { createTraits } from "../traits.js";
12
+ import { CALMING } from "./calming.js";
13
+ /** Warm and cool take turns, so neighbouring variants never share a colour. */
14
+ export const PALETTES = [
15
+ { label: "Azure", core: "#1584ff", accent: "#3deaff" },
16
+ { label: "Lemon", core: "#ffb800", accent: "#fff35c" },
17
+ { label: "Magenta", core: "#ff0a9c", accent: "#ff7ae6" },
18
+ { label: "Ocean", core: "#0057ff", accent: "#00e0c6" },
19
+ { label: "Emerald", core: "#08c236", accent: "#3fe6c4" },
20
+ { label: "Plum", core: "#9b1fe8", accent: "#ff5cab" },
21
+ { label: "Sunset", core: "#ff4d1a", accent: "#ffc02e" },
22
+ { label: "Teal", core: "#008f8f", accent: "#5cc8ff" },
23
+ { label: "Violet", core: "#7032ff", accent: "#e05cff" },
24
+ { label: "Lime", core: "#5ccc00", accent: "#e6ff4d" },
25
+ { label: "Coral", core: "#ff2a52", accent: "#ff9d5c" },
26
+ { label: "Grape", core: "#4b1fd1", accent: "#ff7ad9" },
27
+ { label: "Lagoon", core: "#00aec4", accent: "#3cf2a6" },
28
+ { label: "Ruby", core: "#e0002e", accent: "#ff3dd2" },
29
+ { label: "Indigo", core: "#2c31ff", accent: "#35c0ff" },
30
+ { label: "Peach", core: "#ff7a45", accent: "#ffb3c7" },
31
+ { label: "Arctic", core: "#00b3ff", accent: "#a5f3ff" },
32
+ { label: "Mango", core: "#ff8c00", accent: "#ffe14d" },
33
+ { label: "Forest", core: "#00995c", accent: "#7dff8a" },
34
+ { label: "Rose", core: "#ff3d7f", accent: "#ffc2e0" },
35
+ { label: "Cobalt", core: "#1a3dff", accent: "#9d7dff" },
36
+ { label: "Candy", core: "#ff3db8", accent: "#ffd23d" },
37
+ { label: "Aqua", core: "#00d4b4", accent: "#4da6ff" },
38
+ { label: "Amber", core: "#ff9f1a", accent: "#ff4d6d" },
39
+ ];
40
+ /** What a bubble says until it is told otherwise. */
41
+ const PHRASES = [
42
+ "say hi!", "good vibes only", "hello, legend", "stay curious", "big mood", "main character",
43
+ "dream bigger", "on a roll", "pure magic", "made you look", "vibe check", "soft launch",
44
+ "stay golden", "level up", "zero chill", "hey, you",
45
+ ];
46
+ export const EFFECTS = ["squish", "wiggle", "bounce"];
47
+ export const EYE_KINDS = ["Pill", "Oval", "Dot", "Slit", "Wide", "Bar", "Square", "Diamond", "Happy", "Sparkle", "Heart", "Cross"];
48
+ /** Most bubbles have no mouth, as in the reference: one in this many hundred does. */
49
+ export const MOUTH_ODDS = 30;
50
+ export const MOUTHS = ["Smile", "Grin", "Ooh", "Cat", "Flat", "Smirk", "Wobbly"];
51
+ /** Words past this would be too small to read, however far they shrink. */
52
+ export const MAX_TEXT = 28;
53
+ export const VIEW_BOX = "-2 -2 104 104";
54
+ /** How far the widest part of a bubble reaches from the centre, so all read the same size. */
55
+ const REACH = 49;
56
+ /** Fine enough that an outline never shows a corner, even at its largest. */
57
+ const OUTLINE_POINTS = 360;
58
+ const MONO = `ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace`;
59
+ /** Width of a character in that stack, as a share of its size. */
60
+ const CHARACTER = 0.6;
61
+ const TITLE_SIZE = 7.6;
62
+ const SEEDS = 997;
63
+ const round2 = (value) => Math.round(value * 100) / 100;
64
+ const escaped = (text) => text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
65
+ const turn = (t, key) => t.num(key, 0, 2 * Math.PI);
66
+ /**
67
+ * Every outline is a circle with waves added to its radius. Curves made this way have
68
+ * no corners anywhere, which is what gives the shapes their soft, even edge. A wave of
69
+ * twice the count taken away flattens each tip into a fat, round lobe.
70
+ *
71
+ * Neighbours in this list are as unlike as they can be, in both proportion and lobes,
72
+ * because a variant is the next along from the one before it.
73
+ */
74
+ export const SHAPES = [
75
+ { name: "Scallop", effect: "wiggle", waves: (t) => [[t.int("scallop.n", 12, 15), t.num("depth", 0.05, 0.07), turn(t, "a")]] },
76
+ { name: "Capsule", effect: "bounce", wide: 1.65, lean: 16, waves: (t) => [[4, t.num("depth", -0.1, -0.06), 0], [2, 0.03, 0]] },
77
+ { name: "Clover", effect: "squish", wide: 1.12, lean: 10, waves: (t) => [[4, t.num("depth", 0.16, 0.2), 0], [8, -0.055, 0], [2, 0.04, turn(t, "a")]] },
78
+ { name: "Egg", effect: "bounce", wide: 0.72, lean: 14, waves: (t) => [[1, t.num("depth", 0.07, 0.12), Math.PI / 2], [3, 0.025, turn(t, "a")]] },
79
+ { name: "Star", effect: "wiggle", lean: 14, waves: (t) => [[5, t.num("depth", 0.2, 0.25), -Math.PI / 2], [10, 0.025, -Math.PI]] },
80
+ { name: "Cloud", effect: "squish", wide: 1.5, lean: 6, waves: (t) => [[6, t.num("depth", 0.07, 0.1), turn(t, "a")], [3, 0.05, turn(t, "b")], [12, -0.018, 0]] },
81
+ { name: "Pillow", effect: "bounce", waves: (t) => [[3, t.num("depth", 0.11, 0.14), turn(t, "a")], [2, 0.07, turn(t, "b")], [6, 0.04, turn(t, "c")]] },
82
+ // Deep enough on one side to dimple at the top.
83
+ { name: "Apple", effect: "bounce", wide: 1.08, lean: 14, waves: (t) => [[1, t.num("depth", 0.68, 0.76), -Math.PI / 2]] },
84
+ { name: "Flower", effect: "wiggle", waves: (t) => [[6, t.num("depth", 0.17, 0.21), turn(t, "a")], [12, -0.05, 0]] },
85
+ { name: "Bean", effect: "squish", wide: 1.3, lean: 22, waves: (t) => [[2, t.num("depth", 0.16, 0.2), Math.PI / 2], [1, 0.1, Math.PI / 2], [3, 0.09, -Math.PI / 2]] },
86
+ { name: "Gem", effect: "wiggle", wide: 1.18, lean: 12, waves: (t) => [[6, t.num("depth", -0.09, -0.07), 0], [2, 0.03, 0]] },
87
+ { name: "Trefoil", effect: "squish", lean: 18, waves: (t) => [[3, t.num("depth", 0.2, 0.25), -Math.PI / 2], [6, -0.06, -Math.PI]] },
88
+ { name: "Kite", effect: "bounce", wide: 0.84, waves: (t) => [[4, t.num("depth", 0.13, 0.17), 0], [8, 0.035, 0]] },
89
+ { name: "Burst", effect: "wiggle", waves: (t) => [[t.int("burst.n", 8, 10), t.num("depth", 0.12, 0.15), turn(t, "a")]] },
90
+ // Past a fifth, a wave of two pinches the middle in.
91
+ { name: "Peanut", effect: "squish", lean: 20, waves: (t) => [[2, t.num("depth", 0.3, 0.34), 0], [4, -0.03, 0]] },
92
+ { name: "Tile", effect: "bounce", lean: 24, waves: (t) => [[4, t.num("depth", -0.11, -0.08), 0]] },
93
+ { name: "Prism", effect: "bounce", lean: 14, waves: (t) => [[3, t.num("depth", 0.1, 0.13), Math.PI / 2], [6, 0.015, 0]] },
94
+ { name: "Pebble", effect: "squish", wide: 1.22, lean: 20, waves: (t) => [[2, t.num("depth", 0.05, 0.08), turn(t, "a")], [3, 0.05, turn(t, "b")], [5, 0.03, turn(t, "c")]] },
95
+ ];
96
+ export const shapeOf = (t) => t.pick("shape", SHAPES);
97
+ function drawOutline(t, shape) {
98
+ const waves = shape.waves(t);
99
+ const wide = shape.wide ?? 1;
100
+ const lean = (t.jitter("lean", shape.lean ?? 0) * Math.PI) / 180;
101
+ // Around the origin, stretched then leaned, to be sized once its extent is known.
102
+ const turned = Array.from({ length: OUTLINE_POINTS }, (_, i) => {
103
+ const angle = (i * 2 * Math.PI) / OUTLINE_POINTS;
104
+ const r = 1 + waves.reduce((sum, [count, depth, start]) => sum + depth * Math.cos(count * angle + start), 0);
105
+ const x = r * wide * Math.cos(angle);
106
+ const y = r * Math.sin(angle);
107
+ return [x * Math.cos(lean) - y * Math.sin(lean), x * Math.sin(lean) + y * Math.cos(lean)];
108
+ });
109
+ // Centred on its own extent, so a lopsided shape such as the apple keeps its face in the middle.
110
+ const middle = (axis) => {
111
+ const values = turned.map((point) => point[axis]);
112
+ return (Math.min(...values) + Math.max(...values)) / 2;
113
+ };
114
+ const [mx, my] = [middle(0), middle(1)];
115
+ const raw = turned.map(([x, y]) => [x - mx, y - my]);
116
+ const scale = (REACH * t.num("body.size", 0.88, 1)) / Math.max(...raw.flatMap(([x, y]) => [Math.abs(x), Math.abs(y)]));
117
+ const points = raw.map(([x, y]) => [50 + x * scale, 50 + y * scale]);
118
+ /** How far the body runs each way from the centre, along a line. */
119
+ const reachAlong = (axis, at) => Math.min(...[-1, 1].map((side) => Math.max(0, ...points.filter((p) => Math.abs(p[1 - axis] - at) < 3 && (p[axis] - 50) * side > 0).map((p) => Math.abs(p[axis] - 50)))));
120
+ return {
121
+ d: `M${points.map(([x, y]) => `${round2(x)} ${round2(y)}`).join("L")}Z`,
122
+ rx: scale * wide,
123
+ ry: scale,
124
+ /** How much room the words have across, at a given height. */
125
+ across: (y) => reachAlong(0, y),
126
+ /** How much room there is up and down the middle. */
127
+ tall: reachAlong(1, 50),
128
+ };
129
+ }
130
+ /** The colours in use, each of which can be changed by hand. */
131
+ export function coloursOf(t) {
132
+ const palette = PALETTES[Math.floor(t("palette") * PALETTES.length)];
133
+ const core = t.color("core") ?? palette.core;
134
+ return {
135
+ core,
136
+ accent: t.color("accent") ?? palette.accent,
137
+ edge: t.color("edge") ?? mix(core, WHITE, 0.5),
138
+ };
139
+ }
140
+ export const defaultTitle = (t) => t.pick("title", PHRASES);
141
+ /** Puts a unit gradient over an oval, so a long shape blends along its length. */
142
+ const over = ({ cx, cy, rx, ry }) => `gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1" gradientTransform="translate(${round2(cx)} ${round2(cy)}) scale(${round2(rx)} ${round2(ry)})"`;
143
+ function grainFilter(id, seed, cut) {
144
+ return (`<filter id="${id}" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">` +
145
+ `<feTurbulence type="fractalNoise" baseFrequency="1.73 1.57" numOctaves="2" seed="${seed}"/>` +
146
+ `<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 20 ${round2(cut)}"/>` +
147
+ `<feComposite in2="SourceAlpha" operator="in"/></filter>`);
148
+ }
149
+ function drawDefs(t, outline, rx, ry, core, accent) {
150
+ const blend = t.num("blend", 0.22, 0.62);
151
+ const seed = t.int("grain.seed", 1, SEEDS);
152
+ // Noise above the cut-off becomes a dot, so more grain is a lower cut-off.
153
+ const cut = t.num("grain", -15.6, -14.2);
154
+ // The core drifts off centre, and the second colour can come in from any side.
155
+ const centre = { cx: 50 + t.jitter("core.x", 0.3) * rx, cy: 50 + t.jitter("core.y", 0.28) * ry, rx: rx * 0.98, ry: ry * 0.98 };
156
+ const side = turn(t, "accent.side");
157
+ const sideways = { cx: 50 + Math.cos(side) * rx * 0.92, cy: 50 + Math.sin(side) * ry * 0.92, rx: rx * 0.9, ry: ry * 0.72 };
158
+ return (`<defs>` +
159
+ `<path id="bubble-shape" d="${outline}"/>` +
160
+ `<clipPath id="bubble-clip"><use href="#bubble-shape"/></clipPath>` +
161
+ `<radialGradient id="bubble-core" ${over(centre)}>` +
162
+ `<stop offset="0" stop-color="${core}"/>` +
163
+ `<stop offset="${round2(blend)}" stop-color="${core}"/>` +
164
+ `<stop offset="1" stop-color="${core}" stop-opacity="0"/></radialGradient>` +
165
+ `<radialGradient id="bubble-accent" ${over(sideways)}>` +
166
+ `<stop offset="0" stop-color="${accent}"/>` +
167
+ `<stop offset="1" stop-color="${accent}" stop-opacity="0"/></radialGradient>` +
168
+ `<radialGradient id="bubble-rim" ${over({ cx: 50, cy: 50, rx, ry })}>` +
169
+ `<stop offset="0.55" stop-color="#000"/>` +
170
+ `<stop offset="0.98" stop-color="#fff"/></radialGradient>` +
171
+ `<mask id="bubble-edges" maskUnits="userSpaceOnUse" x="-2" y="-2" width="104" height="104">` +
172
+ `<rect x="-2" y="-2" width="104" height="104" fill="url(#bubble-rim)"/></mask>` +
173
+ `<filter id="bubble-soft" x="-5%" y="-5%" width="110%" height="110%"><feGaussianBlur stdDeviation="0.3"/></filter>` +
174
+ grainFilter("bubble-grain", seed, cut) +
175
+ grainFilter("bubble-rim-grain", seed + 1, cut + 0.8) +
176
+ `</defs>`);
177
+ }
178
+ /** `[width, height, corner, turn]` of each kind of eye, before scaling. Corners default to fully round. */
179
+ const EYE_SIZES = {
180
+ Pill: [4, 10.5],
181
+ Oval: [5.6, 8],
182
+ Dot: [5.6, 5.6],
183
+ Slit: [2.6, 11.5],
184
+ Wide: [8.4, 5],
185
+ Bar: [9, 2.8],
186
+ Square: [6, 6, 1.6],
187
+ Diamond: [5.6, 5.6, 1, 45],
188
+ };
189
+ const LINE = `fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"`;
190
+ /** Drawn eyes, around their own centre. The rest are rounded rectangles. */
191
+ const EYE_PATHS = {
192
+ Happy: `<path d="M-2.8 1.4Q0 -3 2.8 1.4" ${LINE}/>`,
193
+ Sparkle: `<path d="M0 -4Q0.7 -0.7 4 0Q0.7 0.7 0 4Q-0.7 0.7 -4 0Q-0.7 -0.7 0 -4Z" fill="#fff"/>`,
194
+ Heart: `<path d="M0 3.4C-4.6 0.4 -3.6 -3.6 -1.4 -3.2C-0.6 -3 0 -2.2 0 -1.6C0 -2.2 0.6 -3 1.4 -3.2C3.6 -3.6 4.6 0.4 0 3.4Z" fill="#fff"/>`,
195
+ Cross: `<path d="M-2.2 -2.2L2.2 2.2M2.2 -2.2L-2.2 2.2" ${LINE}/>`,
196
+ };
197
+ /** Mouths, around their own centre, in the eyes' white. */
198
+ const MOUTH_PATHS = {
199
+ Smile: `<path d="M-3 -0.6Q0 2.6 3 -0.6" ${LINE}/>`,
200
+ Grin: `<path d="M-3.4 -1.2H3.4Q3.4 3 0 3Q-3.4 3 -3.4 -1.2Z" fill="#fff"/>`,
201
+ Ooh: `<ellipse rx="1.7" ry="2.2" fill="#fff"/>`,
202
+ Cat: `<path d="M-3.4 -0.4Q-1.7 2 0 -0.2Q1.7 2 3.4 -0.4" ${LINE}/>`,
203
+ Flat: `<path d="M-2.6 0H2.6" ${LINE}/>`,
204
+ Smirk: `<path d="M-2.6 0.6Q0.6 1.6 3 -1" ${LINE}/>`,
205
+ Wobbly: `<path d="M-3.6 0Q-2.7 -1.2 -1.8 0T0 0T1.8 0T3.6 0" ${LINE}/>`,
206
+ };
207
+ export const eyeKindOf = (t) => t.pick("eye.kind", EYE_KINDS);
208
+ /** Which mouth, if any. */
209
+ export const mouthOf = (t) => (t("mouth") * 100 < MOUTH_ODDS ? t.pick("mouth.kind", MOUTHS) : undefined);
210
+ function drawEyes(t, eyesY) {
211
+ const scale = t.num("eye.size", 0.8, 1.3);
212
+ const gap = t.num("eye.gap", 4.6, 11) * scale;
213
+ const lean = round2(t.num("eye.lean", -24, 24));
214
+ const kind = eyeKindOf(t);
215
+ const drawn = EYE_PATHS[kind];
216
+ if (drawn) {
217
+ return [50 - gap, 50 + gap]
218
+ .map((x) => `<g><g transform="translate(${round2(x)} ${eyesY}) rotate(${lean}) scale(${round2(scale)})">${drawn}</g></g>`)
219
+ .join("");
220
+ }
221
+ const [wide, tall, corner, turned = 0] = EYE_SIZES[kind];
222
+ const width = round2(wide * scale);
223
+ const height = round2(tall * scale);
224
+ const rx = round2(corner === undefined ? Math.min(width, height) / 2 : corner * scale);
225
+ return [50 - gap, 50 + gap]
226
+ .map((x) => `<g><rect x="${round2(x - width / 2)}" y="${round2(eyesY - height / 2)}" width="${width}" height="${height}" ` +
227
+ `rx="${rx}" fill="#fff" transform="rotate(${round2(lean + turned)} ${round2(x)} ${round2(eyesY)})"/></g>`)
228
+ .join("");
229
+ }
230
+ /** Under the eyes, and above `wordsTop` when there are words. Marked, so the blink leaves it alone. */
231
+ function drawMouth(t, eyesY, wordsTop) {
232
+ const mouth = mouthOf(t);
233
+ if (!mouth)
234
+ return "";
235
+ const scale = round2(t.num("eye.size", 0.8, 1.3));
236
+ const y = round2(Math.min(eyesY + 8.5 * scale, (eyesY + 5 * scale + wordsTop) / 2));
237
+ return `<g data-mouth="" transform="translate(50 ${y}) scale(${scale})">${MOUTH_PATHS[mouth]}</g>`;
238
+ }
239
+ export function createBubble(name, overrides, ids = "bubble") {
240
+ const t = calmed(createTraits(name, overrides), CALMING);
241
+ const shape = shapeOf(t);
242
+ const { d, rx, ry, across, tall } = drawOutline(t, shape);
243
+ const { core, accent, edge } = coloursOf(t);
244
+ const tilt = round2(t.jitter("tilt", 13));
245
+ // A loose spread keeps tall shapes from scattering their face to either end.
246
+ const spread = Math.min(tall, 34) * t.num("spread", 0.85, 1.15);
247
+ // Only words the visitor typed are drawn. The suggestion stays in the panel's input, and
248
+ // with nothing to share the space, the eyes have the middle to themselves.
249
+ const title = t.text("text.title")?.slice(0, MAX_TEXT);
250
+ const eyesY = round2(title ? 50 - 0.36 * spread : 50 + t.jitter("eyes.y", 7));
251
+ const titleY = round2(50 + 0.34 * spread);
252
+ const room = Math.min(across(titleY - 3), across(titleY + 3)) * 1.7;
253
+ const fontSize = title ? round2(Math.min(TITLE_SIZE, room / (title.length * CHARACTER))) : 0;
254
+ const words = title
255
+ ? `<text x="50" y="${titleY}" transform="rotate(${tilt} 50 50)" text-anchor="middle" font-family="${MONO}" ` +
256
+ `font-size="${fontSize}" font-weight="500" fill="#fff">${escaped(title)}</text>`
257
+ : "";
258
+ const wordsTop = title ? titleY - 0.75 * fontSize : Infinity;
259
+ return {
260
+ // Scoped before the words go in, so typed text is never taken for an id.
261
+ art: scopeIds(drawDefs(t, d, rx, ry, core, accent) +
262
+ // Clipped after the blur, so the colours blend softly but the outline stays sharp.
263
+ `<g clip-path="url(#bubble-clip)"><use href="#bubble-shape" fill="${edge}"/><g filter="url(#bubble-soft)">` +
264
+ `<use href="#bubble-shape" fill="${edge}"/>` +
265
+ `<use href="#bubble-shape" fill="url(#bubble-core)"/>` +
266
+ `<use href="#bubble-shape" fill="url(#bubble-accent)"/>` +
267
+ `</g></g>` +
268
+ `<use href="#bubble-shape" fill="#fff" filter="url(#bubble-grain)"/>` +
269
+ `<g mask="url(#bubble-edges)"><use href="#bubble-shape" fill="#fff" filter="url(#bubble-rim-grain)"/></g>`, "bubble", ids) + words,
270
+ eyes: `<g transform="rotate(${tilt} 50 50)">${drawEyes(t, eyesY)}${drawMouth(t, eyesY, wordsTop)}</g>`,
271
+ effect: shape.effect,
272
+ blinkDelay: t.num("blink", 0, 4),
273
+ };
274
+ }
275
+ /** The bubble at rest, for thumbnails. */
276
+ export const bubbleSvg = (name, overrides) => {
277
+ const { art, eyes } = createBubble(name, overrides);
278
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${VIEW_BOX}">${art}${eyes}</svg>`;
279
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * What Possibilities calms on a buddy. Low keeps to the eight mildest expressions, one for
3
+ * each variant, standing straight or mirrored. Every gesture stays, so the variants still
4
+ * differ in what they do. Indices are into the lists in `index.ts`.
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,18 @@
1
+ /**
2
+ * What Possibilities calms on a buddy. Low keeps to the eight mildest expressions, one for
3
+ * each variant, standing straight or mirrored. Every gesture stays, so the variants still
4
+ * differ in what they do. Indices are into the lists in `index.ts`.
5
+ */
6
+ export const CALMING = {
7
+ features: /^(eye)\.|^(weight|tilt)$/,
8
+ lists: {
9
+ // Content, Happy, Blushing, Wink, Smug, Sleepy, In love, Cheeky, then Surprised, Grumpy.
10
+ mood: [10, 8, [0, 1, 4, 3, 2, 7, 8, 9, 5, 6]],
11
+ pose: [6, 2],
12
+ },
13
+ };
14
+ export const NOTES = [
15
+ "sweet looks like content, happy and blushing, standing straight.",
16
+ "adds surprise, and leans and close ups.",
17
+ "every expression, up to grumpy, in any pose.",
18
+ ];
@@ -0,0 +1,2 @@
1
+ import { type ControlBuilder } from "../controls.ts";
2
+ export declare const controls: ControlBuilder;
@@ -0,0 +1,35 @@
1
+ import { choice, palette, slider } 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 { CHARACTERS, characterOf, GESTURES, MOODS, PATCHES, POSES, TILES } from "./index.js";
6
+ const named = (list) => list.map(({ name: label }) => ({ label }));
7
+ export const controls = (name, overrides) => {
8
+ const t = calmed(createTraits(name, overrides), CALMING);
9
+ const list = (control) => narrowed(control, t, CALMING);
10
+ const character = characterOf(t);
11
+ return [
12
+ {
13
+ title: "Buddy",
14
+ controls: [
15
+ choice(t, "character", "Character", "list", named(CHARACTERS)),
16
+ list(choice(t, "mood", "Expression", "list", named(MOODS))),
17
+ possibilitiesSlider(t, NOTES),
18
+ choice(t, "gesture", "Gesture", "list", named(GESTURES)),
19
+ list(choice(t, "pose", "Pose", "list", named(POSES))),
20
+ slider(t, "eye.size", "Eye size", 0, 100, "%"),
21
+ slider(t, "tilt", "Tilt", -5, 5, "°"),
22
+ slider(t, "weight", "Line weight", 0, 100, "%"),
23
+ ],
24
+ },
25
+ {
26
+ title: "Colour",
27
+ controls: [
28
+ palette(t, "tile", "Colour", TILES),
29
+ palette(t, "fur", "Fur", character.furs),
30
+ // Only a calico has patches to colour.
31
+ ...(character.name === "Calico" ? [palette(t, "patch", "Patches", PATCHES)] : []),
32
+ ],
33
+ },
34
+ ];
35
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Buddies: sticker cats in a thick, warm outline and flat colour. Two characters, a striped
3
+ * tabby and a calico, each traced from its own reference, and both sharing the same
4
+ * expressions and gestures.
5
+ *
6
+ * The hands and the shades are traced too, each from the reference that has them: the
7
+ * tabby's paws to its cheeks, the calico's paw at its sunglasses, and a salute traced from a
8
+ * croc's. Every character uses those same drawings, placed on its own face, and the other gestures are
9
+ * drawn with the calico's paw, so all the hands are the same hands.
10
+ */
11
+ import { type Overrides, type Traits } from "../traits.ts";
12
+ /** Solid cards. */
13
+ export declare const TILES: string[];
14
+ export declare const INK = "#1f1a17";
15
+ type Point = readonly [number, number];
16
+ /** Everything that makes one character: its shapes in tile units, and where its face and paws go. */
17
+ interface Character {
18
+ name: string;
19
+ head: Point[];
20
+ body: string;
21
+ /** Default fur. */
22
+ furs: string[];
23
+ eyes: [Point, Point];
24
+ /** Where the mouth goes, its size, and its turn. */
25
+ mouth: [x: number, y: number, scale: number, turn: number];
26
+ /** Its nose, drawn in tile units, if it has one. */
27
+ nose?: (fur: string, weight: number) => string;
28
+ cheeks: [Point, Point];
29
+ shoulders: [Point, Point];
30
+ chest: Point;
31
+ whiskers?: string;
32
+ /** Lines and marks that are always there, over the head: tufts, a brow, chest fur. */
33
+ details?: (fur: string, patch: string, weight: number) => string;
34
+ /** Markings inside the head, and inside the body. */
35
+ markings: (fur: string, patch: string) => string;
36
+ /** How big the shades are on its face, and their turn, and how big its arms are. */
37
+ shades: [scale: number, turn: number];
38
+ arms?: number;
39
+ ovalEyes?: boolean;
40
+ /** How heavy its line is, and how it's fitted to the card. */
41
+ weight: number;
42
+ frame: string;
43
+ }
44
+ /** Neighbours are as unlike as possible. */
45
+ export declare const CHARACTERS: Character[];
46
+ /** A calico's patches, which can be changed apart from its fur. */
47
+ export declare const PATCHES: string[];
48
+ type EyeKind = "dot" | "happy" | "half" | "wink" | "round" | "line" | "heart";
49
+ type MouthKind = "w" | "smile" | "smirk" | "o" | "frown" | "tongue";
50
+ interface Mood {
51
+ name: string;
52
+ eyes: [EyeKind, EyeKind];
53
+ mouth: MouthKind;
54
+ blush?: boolean;
55
+ brows?: boolean;
56
+ }
57
+ export declare const MOODS: Mood[];
58
+ /** What its paws are doing. Neighbours are as unlike as possible. */
59
+ export declare const GESTURES: readonly [{
60
+ readonly name: "Cheeks";
61
+ }, {
62
+ readonly name: "Shades";
63
+ }, {
64
+ readonly name: "Salute";
65
+ }, {
66
+ readonly name: "Heart";
67
+ }, {
68
+ readonly name: "Wave";
69
+ }, {
70
+ readonly name: "Shy";
71
+ }, {
72
+ readonly name: "Peace";
73
+ }, {
74
+ readonly name: "Chill";
75
+ }];
76
+ type Gesture = (typeof GESTURES)[number]["name"];
77
+ interface Pose {
78
+ name: string;
79
+ transform: string;
80
+ mirrored?: boolean;
81
+ }
82
+ export declare const POSES: Pose[];
83
+ export declare const characterOf: (t: Traits) => Character;
84
+ export declare const moodOf: (t: Traits) => Mood;
85
+ export declare const gestureOf: (t: Traits) => Gesture;
86
+ export declare const poseOf: (t: Traits) => Pose;
87
+ export declare function coloursOf(t: Traits, character: Character): {
88
+ tile: string;
89
+ fur: string;
90
+ patch: string;
91
+ };
92
+ export interface Buddy {
93
+ defs: string;
94
+ tile: string;
95
+ pose: string;
96
+ mirrored: boolean;
97
+ /** Ears that twitch on hover. Every buddy's are part of its outline, so these are empty. */
98
+ ears: [string, string];
99
+ /** Body and head, with markings and the details always drawn on them. */
100
+ body: string;
101
+ /** Each eye, marked so it can blink, widen and look about on its own. */
102
+ eyes: string;
103
+ /** Nose, mouth, whiskers, blush and brows. */
104
+ face: string;
105
+ /** Arms, paws and whatever they hold, and shades, in front of the face. */
106
+ front: string;
107
+ blinkDelay: number;
108
+ }
109
+ export declare function createBuddy(name: string, overrides?: Overrides, ids?: string): Buddy;
110
+ export declare const TILE_SHAPE = "<rect width=\"100\" height=\"100\" rx=\"22.22\"/>";
111
+ /** The buddy at rest, for thumbnails. */
112
+ export declare const buddySvg: (name: string, overrides?: Overrides) => string;
113
+ export {};