@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,7 @@
1
+ /**
2
+ * What Possibilities calms on a knit. Low keeps the plainer hats, holes and clothes, and the
3
+ * eight calmest moods, one for each variant. Indices are into the lists in `index.ts`.
4
+ */
5
+ import type { Calming } from "../possibilities.ts";
6
+ export declare const CALMING: Calming;
7
+ export declare const NOTES: [string, string, string];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * What Possibilities calms on a knit. Low keeps the plainer hats, holes and clothes, and the
3
+ * eight calmest moods, one for each variant. Indices are into the lists in `index.ts`.
4
+ */
5
+ export const CALMING = {
6
+ lists: {
7
+ // Bored, Calm, Sleepy, Wide, Side eye, Weary, Surprised, Suspicious, then Angry.
8
+ mood: [9, 8, [0, 6, 2, 1, 3, 8, 4, 5, 7]],
9
+ // Cuffed, Watch cap, Pompom, then Slouch and Bare.
10
+ hat: [5, 3, [0, 2, 1, 3, 4]],
11
+ // Goggles, Two holes, Slot, then Three holes and Open face.
12
+ opening: [5, 3, [0, 4, 2, 1, 3]],
13
+ },
14
+ };
15
+ export const NOTES = [
16
+ "beanies and goggle holes, calm and bored eyes.",
17
+ "more hats and holes, and more moods.",
18
+ "every hat, hole and mood, angry eyes and open faces too.",
19
+ ];
@@ -0,0 +1,2 @@
1
+ import { type ControlBuilder } from "../controls.ts";
2
+ export declare const controls: ControlBuilder;
@@ -0,0 +1,32 @@
1
+ import { choice, palette } from "../controls.js";
2
+ import { narrowed, possibilitiesSlider } from "../possibilities.js";
3
+ import { CALMING, NOTES } from "./calming.js";
4
+ import { BEANIES, CLOTHES, CLOTHING, hatOf, HATS, knitTraits, MASKS, MOODS, OPENINGS, SKINS, TILES } from "./index.js";
5
+ const named = (list) => list.map(({ name: label }) => ({ label }));
6
+ export const controls = (name, overrides) => {
7
+ const t = knitTraits(name, overrides);
8
+ const list = (control) => narrowed(control, t, CALMING);
9
+ return [
10
+ {
11
+ title: "Knit",
12
+ controls: [
13
+ list(choice(t, "mood", "Mood", "list", named(MOODS))),
14
+ list(choice(t, "hat", "Hat", "list", named(HATS))),
15
+ list(choice(t, "opening", "Holes", "list", named(OPENINGS))),
16
+ possibilitiesSlider(t, NOTES),
17
+ choice(t, "clothing", "Clothes", "list", named(CLOTHING)),
18
+ ],
19
+ },
20
+ {
21
+ title: "Colour",
22
+ controls: [
23
+ // A bare head has no beanie to colour.
24
+ ...(hatOf(t) === "none" ? [] : [palette(t, "beanie", "Beanie", BEANIES)]),
25
+ palette(t, "mask", "Mask", MASKS),
26
+ palette(t, "clothes", "Clothes colour", CLOTHES),
27
+ palette(t, "skin", "Skin", SKINS),
28
+ palette(t, "tile", "Background", TILES),
29
+ ],
30
+ },
31
+ ];
32
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Knits: a bust in a knitted balaclava and a ribbed beanie, after the reference's look. A soft
3
+ * studio grey behind, a coat over a white tee, the mask's knit running in fine ribs, and the eyes
4
+ * looking out through its holes from under heavy lids. It's drawn to feel modelled: every part is
5
+ * shaded as the form it is, a ball, a dome or a cylinder, under one soft light from up to the left,
6
+ * with soft shadows where parts meet, the beanie's ribs as tubes wrapping round it, and the yarn's
7
+ * edges fuzzy and its surface flecked. The eyes can blink and follow the pointer.
8
+ *
9
+ * It's drawn from numbers in the 100×100 frame, so the thumbnail is the very same picture.
10
+ */
11
+ import { type Overrides, type Traits } from "../traits.ts";
12
+ /** The reference's first. Neighbours are unlike. */
13
+ export declare const BEANIES: string[];
14
+ export declare const MASKS: string[];
15
+ export declare const CLOTHES: string[];
16
+ export declare const SKINS: string[];
17
+ export declare const TILES: string[];
18
+ type Hat = "cuffed" | "pompom" | "slouch" | "watch" | "none";
19
+ type Opening = "goggles" | "slot" | "holes" | "three" | "open";
20
+ type Clothing = "coat" | "hoodie" | "puffer" | "turtleneck" | "bomber";
21
+ export declare const HATS: {
22
+ name: string;
23
+ hat: Hat;
24
+ }[];
25
+ export declare const OPENINGS: {
26
+ name: string;
27
+ opening: Opening;
28
+ }[];
29
+ export declare const CLOTHING: {
30
+ name: string;
31
+ clothing: Clothing;
32
+ }[];
33
+ /** A mood, as the eyes show it: how far down the lids are, their tilt, and where the eyes look. */
34
+ interface Mood {
35
+ name: string;
36
+ lid: number;
37
+ /** Above 0 lowers the lids towards the nose, as in anger; below it, towards the ears, as in sorrow. */
38
+ tilt: number;
39
+ look: [number, number];
40
+ pupil: number;
41
+ }
42
+ /** The reference's bored glance first. Neighbours are unlike. */
43
+ export declare const MOODS: Mood[];
44
+ export declare const knitTraits: (name: string, overrides?: Overrides) => Traits;
45
+ export declare const hatOf: (t: Traits) => Hat;
46
+ export declare const openingOf: (t: Traits) => Opening;
47
+ export declare const clothingOf: (t: Traits) => Clothing;
48
+ export declare const moodOf: (t: Traits) => Mood;
49
+ export declare function coloursOf(t: Traits): {
50
+ beanie: string;
51
+ mask: string;
52
+ clothes: string;
53
+ skin: string;
54
+ tile: string;
55
+ };
56
+ export interface Knit {
57
+ /** Gradients, patterns, filters and clips, which every layer uses. */
58
+ defs: string;
59
+ /** The studio behind, the clothes and the knitted head, which never move. */
60
+ back: string;
61
+ /** The eyes and a pompom, which blink, look and bounce. */
62
+ live: string;
63
+ /** The rims of the holes, the cuff's shadow and the hat, over the eyes, which never move. */
64
+ front: string;
65
+ blink: {
66
+ period: number;
67
+ delay: number;
68
+ };
69
+ }
70
+ /** `id` keeps each knit's patterns and clips its own where many share a page. */
71
+ export declare function createKnit(name: string, overrides: Overrides | undefined, id?: string): Knit;
72
+ /** The bust at rest, for thumbnails. */
73
+ export declare function knitSvg(name: string, overrides?: Overrides): string;
74
+ export {};
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Knits: a bust in a knitted balaclava and a ribbed beanie, after the reference's look. A soft
3
+ * studio grey behind, a coat over a white tee, the mask's knit running in fine ribs, and the eyes
4
+ * looking out through its holes from under heavy lids. It's drawn to feel modelled: every part is
5
+ * shaded as the form it is, a ball, a dome or a cylinder, under one soft light from up to the left,
6
+ * with soft shadows where parts meet, the beanie's ribs as tubes wrapping round it, and the yarn's
7
+ * edges fuzzy and its surface flecked. The eyes can blink and follow the pointer.
8
+ *
9
+ * It's drawn from numbers in the 100×100 frame, so the thumbnail is the very same picture.
10
+ */
11
+ import { BLACK, mix, WHITE } from "../colors.js";
12
+ import { calmed } from "../possibilities.js";
13
+ import { createTraits } from "../traits.js";
14
+ import { CALMING } from "./calming.js";
15
+ const r2 = (value) => Math.round(value * 100) / 100;
16
+ /** The reference's first. Neighbours are unlike. */
17
+ export const BEANIES = ["#1b1b1f", "#c23a2e", "#d9a43a", "#20324f", "#3f5f3a", "#e9e4d8", "#6b3a5a", "#2f7f86"];
18
+ export const MASKS = ["#a9aaac", "#26262b", "#d8d2c4", "#6c7558", "#d98fa0", "#8fa3b8", "#4a5a7a", "#efe7d2"];
19
+ export const CLOTHES = ["#1c1c20", "#7b5a3c", "#2b3a55", "#5b6b4a", "#a8322d", "#cfcac0", "#3b3b42", "#c99a3e"];
20
+ export const SKINS = ["#e3a980", "#f2c7a5", "#c98b5f", "#9c6340", "#6e4329", "#f5d6bf"];
21
+ export const TILES = ["#d9dadd", "#e4ddd3", "#d3dbe3", "#e2d6dd", "#d6ded3", "#cfcfd4"];
22
+ export const HATS = [
23
+ { name: "Cuffed", hat: "cuffed" },
24
+ { name: "Pompom", hat: "pompom" },
25
+ { name: "Watch cap", hat: "watch" },
26
+ { name: "Slouch", hat: "slouch" },
27
+ { name: "Bare", hat: "none" },
28
+ ];
29
+ export const OPENINGS = [
30
+ { name: "Goggles", opening: "goggles" },
31
+ { name: "Three holes", opening: "three" },
32
+ { name: "Slot", opening: "slot" },
33
+ { name: "Open face", opening: "open" },
34
+ { name: "Two holes", opening: "holes" },
35
+ ];
36
+ export const CLOTHING = [
37
+ { name: "Coat", clothing: "coat" },
38
+ { name: "Puffer", clothing: "puffer" },
39
+ { name: "Hoodie", clothing: "hoodie" },
40
+ { name: "Bomber", clothing: "bomber" },
41
+ { name: "Turtleneck", clothing: "turtleneck" },
42
+ ];
43
+ /** The reference's bored glance first. Neighbours are unlike. */
44
+ export const MOODS = [
45
+ { name: "Bored", lid: 0.5, tilt: 0, look: [-0.55, 0.35], pupil: 1 },
46
+ { name: "Wide", lid: 0.08, tilt: 0, look: [0, 0], pupil: 0.85 },
47
+ { name: "Sleepy", lid: 0.68, tilt: -4, look: [0, 0.5], pupil: 1 },
48
+ { name: "Side eye", lid: 0.38, tilt: 6, look: [0.8, 0.1], pupil: 1 },
49
+ { name: "Surprised", lid: 0, tilt: 0, look: [0, -0.2], pupil: 0.7 },
50
+ { name: "Suspicious", lid: 0.46, tilt: 14, look: [-0.7, 0], pupil: 0.95 },
51
+ { name: "Calm", lid: 0.3, tilt: -2, look: [0, 0.15], pupil: 1 },
52
+ { name: "Angry", lid: 0.36, tilt: 24, look: [0, 0], pupil: 0.9 },
53
+ { name: "Weary", lid: 0.58, tilt: -12, look: [0.3, 0.4], pupil: 1 },
54
+ ];
55
+ export const knitTraits = (name, overrides) => calmed(createTraits(name, overrides), CALMING);
56
+ export const hatOf = (t) => t.pick("hat", HATS).hat;
57
+ export const openingOf = (t) => t.pick("opening", OPENINGS).opening;
58
+ export const clothingOf = (t) => t.pick("clothing", CLOTHING).clothing;
59
+ export const moodOf = (t) => t.pick("mood", MOODS);
60
+ export function coloursOf(t) {
61
+ return {
62
+ beanie: t.color("beanie") ?? t.pick("beanie", BEANIES),
63
+ mask: t.color("mask") ?? t.pick("mask", MASKS),
64
+ clothes: t.color("clothes") ?? t.pick("clothes", CLOTHES),
65
+ skin: t.color("skin") ?? t.pick("skin", SKINS),
66
+ tile: t.color("tile") ?? t.pick("tile", TILES),
67
+ };
68
+ }
69
+ /** Points along `curve` from `x0` to `x1`, a unit or so apart, for a smooth edge. */
70
+ function along(curve, x0, x1, lift = 0) {
71
+ const steps = Math.ceil(Math.abs(x1 - x0));
72
+ return Array.from({ length: steps + 1 }, (_, i) => {
73
+ const x = x0 + ((x1 - x0) * i) / steps;
74
+ return `${r2(x)} ${r2(curve(x) + lift)}`;
75
+ }).join("L");
76
+ }
77
+ /** A knit as a pattern of ribs `pitch` apart, each a light and a dark line on the yarn's colour. */
78
+ function ribs(id, colour, pitch, depth = 1) {
79
+ const light = mix(colour, WHITE, 0.12 * depth);
80
+ const dark = mix(colour, BLACK, 0.16 * depth);
81
+ return (`<pattern id="${id}" patternUnits="userSpaceOnUse" width="${pitch}" height="${pitch * 3}">` +
82
+ `<rect width="${pitch}" height="${pitch * 3}" fill="${colour}"/>` +
83
+ `<rect x="${r2(pitch * 0.08)}" width="${r2(pitch * 0.38)}" height="${pitch * 3}" fill="${light}"/>` +
84
+ `<rect x="${r2(pitch * 0.62)}" width="${r2(pitch * 0.16)}" height="${pitch * 3}" fill="${dark}"/>` +
85
+ // The stitches: a small vee down each rib, which is what makes it read as knitted.
86
+ `<path d="M${r2(pitch * 0.08)} 0L${r2(pitch * 0.27)} ${r2(pitch * 0.5)}L${r2(pitch * 0.46)} 0M${r2(pitch * 0.08)} ${r2(pitch * 1.5)}L${r2(pitch * 0.27)} ${r2(pitch * 2)}L${r2(pitch * 0.46)} ${r2(pitch * 1.5)}" fill="none" stroke="${mix(colour, BLACK, 0.1)}" stroke-width="${r2(pitch * 0.07)}"/>` +
87
+ `</pattern>`);
88
+ }
89
+ /** Stops for a gradient, each `[offset, colour, opacity]`. */
90
+ const stops = (list) => list.map(([offset, colour, opacity]) => `<stop offset="${offset}" stop-color="${colour}" stop-opacity="${opacity}"/>`).join("");
91
+ /** Blurs `std` units wide, by name, with room round each part for the blur to spread. */
92
+ const BLURS = { b4: 0.4, b8: 0.8, b14: 1.4, b24: 2.4 };
93
+ const ROOM = `filterUnits="userSpaceOnUse" x="-10" y="-10" width="120" height="120"`;
94
+ /** A soft stroke, a shadow or a gleam, along `d`. */
95
+ const soft = (id, d, colour, opacity, width, blur) => `<path d="${d}" fill="none" stroke="${colour}" stroke-opacity="${opacity}" stroke-width="${width}" stroke-linecap="round" filter="url(#${id}-${blur})"/>`;
96
+ /** The holes and the eyes in them, drawn larger than their numbers, as the reference's eyes are big. */
97
+ const OPEN = `transform="translate(50 46) scale(1.22) translate(-50 -46)"`;
98
+ /** The head in its mask: round on top, the knit bunched in folds over the cheeks, and a soft chin. */
99
+ const HEAD = "M50 15.5C66.5 15.5 78.5 26 79.2 41.5C79.4 44 79.6 46 79.8 47.5Q82.6 50 80.2 52.6Q83 55.6 79.8 58.6Q82 61.8 77.8 64" +
100
+ "C73.5 68.6 62.5 71 50 71C37.5 71 26.5 68.6 22.2 64Q18 61.8 20.2 58.6Q17 55.6 19.8 52.6Q17.4 50 20.2 47.5" +
101
+ "C20.4 46 20.6 44 20.8 41.5C21.5 26 33.5 15.5 50 15.5Z";
102
+ const NECK = "M39 61H61Q61.2 70 62.6 75.2Q50 78.6 37.4 75.2Q38.8 70 39 61Z";
103
+ const BODY = "M19 104C19.5 90 24 80.5 35 75.6Q38.5 74.2 42 74H58Q61.5 74.2 65 75.6C76 80.5 80.5 90 81 104Z";
104
+ const TEE = "M41.5 74H58.5L57 104H43Z";
105
+ /** A pill from corner to corner, for the goggles' tall windows. */
106
+ function pill(x0, y0, x1, y1) {
107
+ const r = (x1 - x0) / 2;
108
+ return `M${r2(x0 + r)} ${y0}A${r2(r)} ${r2(r)} 0 0 1 ${x1} ${r2(y0 + r)}V${r2(y1 - r)}A${r2(r)} ${r2(r)} 0 0 1 ${r2(x0 + r)} ${y1}A${r2(r)} ${r2(r)} 0 0 1 ${x0} ${r2(y1 - r)}V${r2(y0 + r)}A${r2(r)} ${r2(r)} 0 0 1 ${r2(x0 + r)} ${y0}Z`;
109
+ }
110
+ /** The holes in the mask, as paths, and the eyes in them. */
111
+ function openingShape(opening) {
112
+ const eyes = (xs, y, rx, ry, open = 0, drop = 0, iris = 0.64) => xs.map((x) => ({ x, y, rx, ry, open, drop, iris }));
113
+ switch (opening) {
114
+ case "goggles":
115
+ // Two tall windows with a knitted bridge between them over the nose, as the reference's.
116
+ return { d: pill(36.9, 33.3, 47.95, 51.7) + pill(52.05, 33.3, 63.1, 51.7), eyes: eyes([42.43, 57.57], 42.5, 4.42, 8.1, 0.24, 0.2, 0.82) };
117
+ case "slot":
118
+ return { d: "M38 38.5H62Q69 38.5 69 45.5Q69 52 62 52H38Q31 52 31 45.5Q31 38.5 38 38.5Z", eyes: eyes([41.5, 58.5], 45.2, 5.2, 4.8) };
119
+ case "holes":
120
+ return { d: "M34.5 45.5A7 6.8 0 1 0 48.5 45.5A7 6.8 0 1 0 34.5 45.5ZM51.5 45.5A7 6.8 0 1 0 65.5 45.5A7 6.8 0 1 0 51.5 45.5Z", eyes: eyes([41.5, 58.5], 45.5, 5.6, 5.4) };
121
+ case "three":
122
+ return {
123
+ d: "M34.5 45A7 6.6 0 1 0 48.5 45A7 6.6 0 1 0 34.5 45ZM51.5 45A7 6.6 0 1 0 65.5 45A7 6.6 0 1 0 51.5 45ZM43.5 61.5A6.5 4 0 1 0 56.5 61.5A6.5 4 0 1 0 43.5 61.5Z",
124
+ eyes: eyes([41.5, 58.5], 45, 5.6, 5.2),
125
+ mouth: true,
126
+ };
127
+ case "open":
128
+ return { d: "M50 36Q67 36 67 50Q67 66 50 66Q33 66 33 50Q33 36 50 36Z", eyes: eyes([42, 58], 46, 5, 4.2), mouth: true, nose: true };
129
+ }
130
+ }
131
+ /** The eye as a rect with rounded ends, which is an ellipse or a pill as its shape asks. */
132
+ const eyeRect = (e, grow = 0) => `x="${r2(e.x - e.rx - grow)}" y="${r2(e.y - e.ry - grow)}" width="${r2(2 * (e.rx + grow))}" height="${r2(2 * (e.ry + grow))}" rx="${r2(e.rx + grow)}" ry="${r2(Math.min(e.rx, e.ry) + grow)}"`;
133
+ /** The eyes: shaded whites, big dark pupils that can look about, and heavy skin lids that set the mood and blink. */
134
+ function eyesMarkup(id, m, eyes) {
135
+ return eyes
136
+ .map((e, i) => {
137
+ const side = i === 0 ? -1 : 1;
138
+ const clip = `${id}-eye${i}`;
139
+ const radius = e.rx * e.iris * m.pupil;
140
+ const [px, py] = [e.x + m.look[0] * e.rx * 0.36, e.y + e.ry * e.drop + m.look[1] * e.ry * 0.3];
141
+ // The lid's rect runs from above the eye down past it; scaling it from its top drops its edge.
142
+ const top = e.y - e.ry - 0.4;
143
+ const height = 2 * e.ry + 0.8;
144
+ const edge = e.y - e.ry + 2 * e.ry * (e.open + (1 - e.open) * m.lid);
145
+ const scale = (edge - top) / height;
146
+ const layer = (fill, stretch, opacity = 1) => `<rect class="lid" x="${r2(e.x - e.rx * 2.2)}" y="${r2(top)}" width="${r2(e.rx * 4.4)}" height="${r2(height * stretch)}" fill="${fill}"${opacity < 1 ? ` fill-opacity="${opacity}"` : ""} style="--lid:${r2(scale)}" transform="translate(0 ${r2(top)}) scale(1 ${r2(scale)}) translate(0 ${r2(-top)})"/>`;
147
+ return (`<g class="eye" data-eye="${i}">` +
148
+ `<clipPath id="${clip}"><rect ${eyeRect(e)}/></clipPath>` +
149
+ `<rect ${eyeRect(e)} fill="url(#${id}-white)"/>` +
150
+ `<g clip-path="url(#${clip})">` +
151
+ `<g class="pupil"><circle cx="${r2(px)}" cy="${r2(py)}" r="${r2(radius)}" fill="url(#${id}-pupil)"/>` +
152
+ `<circle cx="${r2(px + radius * 0.38)}" cy="${r2(py - radius * 0.4)}" r="${r2(radius * 0.17)}" fill="#ffffff" fill-opacity="0.9"/>` +
153
+ `<circle cx="${r2(px - radius * 0.35)}" cy="${r2(py + radius * 0.45)}" r="${r2(radius * 0.22)}" fill="#ffffff" fill-opacity="0.12"/></g>` +
154
+ // The eyeball turns away into shadow round its edge.
155
+ `<rect ${eyeRect(e)} fill="url(#${id}-eyeball)"/>` +
156
+ // The lid's shadow across the white, soft and then deeper, its dark edge, and the lid itself.
157
+ `<g transform="rotate(${r2(-side * m.tilt)} ${r2(e.x)} ${r2(e.y)})"><g class="lids">` +
158
+ `${layer("#2a1810", 1.14, 0.1)}${layer("#2a1810", 1.06, 0.16)}${layer(`url(#${id}-lash)`, 1.02)}${layer(`url(#${id}-lid)`, 1)}` +
159
+ `</g></g>` +
160
+ `</g></g>`);
161
+ })
162
+ .join("");
163
+ }
164
+ function bandOf(hat) {
165
+ // The top arches up in the middle; the bottom wraps down round the sides of the head.
166
+ const arch = (x0, x1, sides, middle) => (x) => {
167
+ const t = (x - x0) / (x1 - x0);
168
+ return sides - 4 * (sides - middle) * t * (1 - t);
169
+ };
170
+ const wrap = (middle, sides, half) => (x) => middle + (sides - middle) * Math.abs((x - 50) / half) ** 2.6;
171
+ switch (hat) {
172
+ case "none":
173
+ return undefined;
174
+ case "watch":
175
+ return { x0: 22, x1: 78, top: arch(22, 78, 23, 18.5), bottom: wrap(30.5, 40, 28), peak: [50, 11.5] };
176
+ case "slouch": {
177
+ const bottom = wrap(35, 47, 29);
178
+ return { x0: 20.5, x1: 79.5, top: (x) => bottom(x) - 6.5, bottom, peak: [56, 2.5] };
179
+ }
180
+ default:
181
+ return { x0: 20.5, x1: 79.5, top: arch(20.5, 79.5, 24.5, 19.5), bottom: wrap(35, 47, 29), peak: [50, hat === "pompom" ? 9.5 : 8] };
182
+ }
183
+ }
184
+ /** Ribs round a cylinder, `count` of them: where each groove falls across the front, and how square on it is. */
185
+ const around = (count) => Array.from({ length: count - 1 }, (_, k) => {
186
+ const angle = -Math.PI / 2 + ((k + 1) * Math.PI) / count;
187
+ return { sin: Math.sin(angle), cos: Math.cos(angle), angle };
188
+ });
189
+ /** The light comes from up to the left, so a rib facing that way catches it. */
190
+ const lit = (angle) => Math.max(0, Math.cos(angle + 0.45));
191
+ /** The beanie: a ribbed crown and a thick turned-up cuff, each rib a soft tube, round the head. */
192
+ function hatMarkup(id, hat, band) {
193
+ const { x0, x1, top, bottom, peak } = band;
194
+ const [px, py] = peak;
195
+ const half = (x1 - x0) / 2;
196
+ const inset = hat === "slouch" ? 1.5 : 4.5;
197
+ const [l, r] = [x0 + inset, x1 - inset];
198
+ const [ly, ry] = [top(l), top(r)];
199
+ const crown = hat === "slouch"
200
+ ? `M${l} ${r2(ly + 1)}C${l - 2} ${py + 12} ${px - 20} ${py} ${px} ${py}C${px + 16} ${py} ${r + 5} ${py + 8} ${r + 1.5} ${py + 16}C${r + 0.5} ${r2(ry - 6)} ${r} ${r2(ry - 2)} ${r} ${r2(ry + 1)}Z`
201
+ : `M${l} ${r2(ly + 2)}C${l - 1} ${r2(py + (ly - py) * 0.5)} ${px - 12.5} ${py} ${px} ${py}C${px + 12.5} ${py} ${r + 1} ${r2(py + (ry - py) * 0.5)} ${r} ${r2(ry + 2)}Z`;
202
+ const bandPath =
203
+ // Its top corners round over into its sides.
204
+ `M${x0} ${r2(top(x0) + 3)}Q${x0} ${r2(top(x0))} ${x0 + 3} ${r2(top(x0 + 3))}L${along(top, x0 + 3, x1 - 3)}Q${x1} ${r2(top(x1))} ${x1} ${r2(top(x1) + 3)}` +
205
+ `Q${x1 + 2.6} ${r2((top(x1) + bottom(x1)) / 2 + 1.5)} ${x1 - 0.6} ${r2(bottom(x1 - 0.6))}` +
206
+ `L${along(bottom, x1 - 0.6, x0 + 0.6)}Q${x0 - 2.6} ${r2((top(x0) + bottom(x0)) / 2 + 1.5)} ${x0} ${r2(top(x0) + 3)}Z`;
207
+ // The crown's ribs run up to its peak like lines of longitude.
208
+ const crownRibs = around(12)
209
+ .map(({ sin, cos, angle }) => {
210
+ const xb = 50 + (half - inset) * sin;
211
+ const yb = top(xb) + 1;
212
+ const start = `M${r2(px + (xb - 50) * 0.08)} ${py + 1}`;
213
+ const bend = `Q${r2(xb + (xb - 50) * 0.08 + (px - 50) * 0.4)} ${r2(py + (yb - py) * 0.45)} ${r2(xb)} ${r2(yb)}`;
214
+ const gleam = 50 + (half - inset) * Math.sin(angle + Math.PI / 24 - 0.05);
215
+ const gb = top(gleam) + 1;
216
+ return (soft(id, start + bend, "#000000", 0.5, r2(0.35 + 0.8 * cos), "b4") +
217
+ soft(id, `M${r2(px + (gleam - 50) * 0.08)} ${py + 2}Q${r2(gleam + (gleam - 50) * 0.08 + (px - 50) * 0.4)} ${r2(py + (gb - py) * 0.45)} ${r2(gleam)} ${r2(gb)}`, "#ffffff", r2(0.03 + 0.12 * lit(angle)), r2(0.5 + 1.2 * cos), "b8"));
218
+ })
219
+ .join("");
220
+ // The cuff's ribs are bigger and plumper, packed closer towards the sides as it turns away.
221
+ const step = Math.PI / 14;
222
+ const cuffRibs = around(14)
223
+ .map(({ sin, cos, angle }) => {
224
+ const xt = 50 + half * sin;
225
+ const xb = 50 + (half - 0.8) * sin;
226
+ const mid = (top(xt) + bottom(xt)) / 2;
227
+ const groove = `M${r2(xt)} ${r2(top(xt) + 0.2)}Q${r2(50 + (half + 0.6) * sin)} ${r2(mid)} ${r2(xb)} ${r2(bottom(xb) - 0.2)}`;
228
+ // The rib to the left of this groove, lit on its upper left.
229
+ const g = Math.sin(angle - step * 0.62);
230
+ const gt = 50 + half * g;
231
+ const gb = 50 + (half - 0.8) * g;
232
+ const gleam = `M${r2(gt)} ${r2(top(gt) + 1.2)}Q${r2(50 + (half + 0.6) * g)} ${r2((top(gt) + bottom(gt)) / 2)} ${r2(gb)} ${r2(bottom(gb) - 1.4)}`;
233
+ return soft(id, groove, "#000000", 0.55, r2(0.4 + 0.9 * cos), "b4") + soft(id, gleam, "#ffffff", r2(0.05 + 0.2 * lit(angle - step)), r2(0.6 + 2 * cos), "b8");
234
+ })
235
+ .join("");
236
+ return (`<g filter="url(#${id}-fuzz)">` +
237
+ `<clipPath id="${id}-crown"><path d="${crown}"/></clipPath>` +
238
+ `<path d="${crown}" fill="url(#${id}-beanie)"/>` +
239
+ `<g clip-path="url(#${id}-crown)">${crownRibs}<path d="${crown}" fill="url(#${id}-dome)"/></g>` +
240
+ `<clipPath id="${id}-band"><path d="${bandPath}"/></clipPath>` +
241
+ `<path d="${bandPath}" fill="url(#${id}-beanie)"/>` +
242
+ `<g clip-path="url(#${id}-band)">${cuffRibs}` +
243
+ `<path d="${bandPath}" fill="url(#${id}-round)"/>` +
244
+ // The crown's shadow where it meets the cuff, the cuff's fold catching the light, and its edge curling under.
245
+ soft(id, `M${along(top, x0, x1, -0.3)}`, "#000000", 0.6, 1.6, "b8") +
246
+ soft(id, `M${along(top, x0 + 3, x1 - 3, 1.8)}`, "#ffffff", 0.14, 2, "b14") +
247
+ soft(id, `M${along(bottom, x0, x1, -0.4)}`, "#000000", 0.45, 2.6, "b14") +
248
+ `</g></g>`);
249
+ }
250
+ /** The pompom: a fluffy ball of yarn on the crown, drawn live so a tap can bounce it. */
251
+ function pompomMarkup(id, band) {
252
+ const [cx, cy] = [band.peak[0], band.peak[1] - 3.4];
253
+ const tufts = Array.from({ length: 18 }, (_, i) => {
254
+ const a = (i / 18) * Math.PI * 2;
255
+ const reach = 4.3 + (i % 3) * 0.35;
256
+ return `<circle cx="${r2(cx + Math.cos(a) * reach)}" cy="${r2(cy + Math.sin(a) * reach * 0.9)}" r="${r2(2.1 + (i % 2) * 0.3)}" fill="url(#${id}-pom)"/>`;
257
+ }).join("");
258
+ return `<g class="pompom">${tufts}<circle cx="${cx}" cy="${r2(cy)}" r="4.9" fill="url(#${id}-pom)"/></g>`;
259
+ }
260
+ /** The clothes, from the shoulders down, in two parts: what's behind the mask's neck, and what wraps in front of it. */
261
+ function clothesMarkup(id, clothing, c) {
262
+ const cloth = c.clothes;
263
+ const dark = mix(cloth, BLACK, 0.4);
264
+ const body = `<path d="${BODY}" fill="${cloth}"/>`;
265
+ const tee = `<path d="${TEE}" fill="#ecebe7"/><path d="${TEE}" fill="url(#${id}-tee)"/>`;
266
+ // Light on the shoulders, more on the left, and the body rounding away at its sides.
267
+ const form = soft(id, "M21.5 96C22.8 86 27 80 35.5 76.2", "#ffffff", 0.16, 2.4, "b14") +
268
+ soft(id, "M78.5 96C77.2 86 73 80 64.5 76.2", "#ffffff", 0.07, 2.4, "b14") +
269
+ `<path d="${BODY}" fill="url(#${id}-round)"/>`;
270
+ switch (clothing) {
271
+ case "coat": {
272
+ const lapel = mix(cloth, WHITE, 0.05);
273
+ return {
274
+ under: body + tee +
275
+ // The lapels' shadows on the tee and on the coat.
276
+ soft(id, "M42.6 74.6Q45.3 88 47.6 104", "#000000", 0.3, 1.8, "b8") + soft(id, "M57.4 74.6Q54.7 88 52.4 104", "#000000", 0.3, 1.8, "b8") +
277
+ soft(id, "M33.6 84.6Q38 93.5 41.2 104", "#000000", 0.5, 1.6, "b8") + soft(id, "M66.4 84.6Q62 93.5 58.8 104", "#000000", 0.5, 1.6, "b8") +
278
+ `<path d="M35.2 75.4L41.6 74.2Q44.4 88 46.6 104H41Q37.8 93 33.2 84L36.9 81.6Z" fill="${lapel}"/>` +
279
+ `<path d="M64.8 75.4L58.4 74.2Q55.6 88 53.4 104H59Q62.2 93 66.8 84L63.1 81.6Z" fill="${lapel}"/>` +
280
+ soft(id, "M41.2 75Q44 88 46.2 104", "#ffffff", 0.16, 0.7, "b4") + soft(id, "M58.8 75Q56 88 53.8 104", "#ffffff", 0.08, 0.7, "b4") +
281
+ soft(id, "M33.6 84.2L36.9 81.6", "#000000", 0.45, 0.6, "b4") + soft(id, "M66.4 84.2L63.1 81.6", "#000000", 0.45, 0.6, "b4") +
282
+ form,
283
+ over: "",
284
+ };
285
+ }
286
+ case "hoodie":
287
+ // The hood gathered round the neck behind, and its strings hanging in front.
288
+ return {
289
+ under: body +
290
+ `<path d="M29 77.5Q31 70.5 40 69.5H60Q69 70.5 71 77.5Q66 83.5 50 84Q34 83.5 29 77.5Z" fill="${mix(cloth, WHITE, 0.04)}"/>` +
291
+ `<path d="M35.5 75Q50 70 64.5 75Q60.5 81 50 81.5Q39.5 81 35.5 75Z" fill="${dark}"/>` +
292
+ soft(id, "M30 78Q36 84 50 84.2Q64 84 70 78", "#000000", 0.45, 1.6, "b8") +
293
+ soft(id, "M31.5 75.5Q33 71.5 39 70.8", "#ffffff", 0.18, 1.4, "b8") +
294
+ soft(id, "M33 79.5Q37 76.5 36.5 73M67 79.5Q63 76.5 63.5 73", "#000000", 0.3, 0.8, "b4") +
295
+ form,
296
+ over: soft(id, "M45.3 80Q44.8 88 45.4 95.5M56.3 80Q56.8 88 56.2 95.5", "#000000", 0.35, 1.4, "b8") +
297
+ `<path d="M44.5 79.5Q44 88 44.6 95M55.5 79.5Q56 88 55.4 95" fill="none" stroke="#efece6" stroke-width="1.1" stroke-linecap="round"/>` +
298
+ `<rect x="43.8" y="94.4" width="1.6" height="3" rx="0.6" fill="#c9c5bd"/><rect x="54.6" y="94.4" width="1.6" height="3" rx="0.6" fill="#c9c5bd"/>`,
299
+ };
300
+ case "puffer":
301
+ return {
302
+ under: body +
303
+ `<clipPath id="${id}-body"><path d="${BODY}"/></clipPath><g clip-path="url(#${id}-body)">` +
304
+ [82, 89, 96, 103]
305
+ .map((y) => soft(id, `M18 ${y - 3.6}Q50 ${y - 0.6} 82 ${y - 3.6}`, "#ffffff", 0.16, 3, "b14") + soft(id, `M14 ${y}Q50 ${y + 3} 86 ${y}`, "#000000", 0.55, 1, "b4"))
306
+ .join("") +
307
+ `</g>` +
308
+ soft(id, "M50 77V104", "#000000", 0.5, 0.9, "b4") +
309
+ form,
310
+ // A standing collar, zipped up round the neck.
311
+ over: `<path d="M36.5 72.5Q50 76.5 63.5 72.5L64 79.5Q50 83.5 36 79.5Z" fill="${cloth}"/><path d="M36.5 72.5Q50 76.5 63.5 72.5L64 79.5Q50 83.5 36 79.5Z" fill="url(#${id}-round)"/>` +
312
+ soft(id, "M37.5 74.5Q50 78.2 62.5 74.5", "#ffffff", 0.2, 1.4, "b8") +
313
+ soft(id, "M36.5 80Q50 84 63.5 80", "#000000", 0.45, 1.4, "b8") +
314
+ soft(id, "M50 77.5V83", "#000000", 0.5, 0.7, "b4"),
315
+ };
316
+ case "turtleneck":
317
+ return {
318
+ under: `<path d="${BODY}" fill="url(#${id}-cloth)"/>` + form,
319
+ // The rolled collar, folded over the mask's neck.
320
+ over: `<path d="M36 72.5Q50 70.5 64 72.5Q65.5 76.5 63.5 80Q50 83 36.5 80Q34.5 76.5 36 72.5Z" fill="url(#${id}-cloth)"/>` +
321
+ `<path d="M36 72.5Q50 70.5 64 72.5Q65.5 76.5 63.5 80Q50 83 36.5 80Q34.5 76.5 36 72.5Z" fill="url(#${id}-round)"/>` +
322
+ soft(id, "M37 74Q50 72.4 63 74", "#ffffff", 0.18, 1.6, "b8") +
323
+ soft(id, "M36.5 80.4Q50 83.6 63.5 80.4", "#000000", 0.5, 1.6, "b8"),
324
+ };
325
+ case "bomber":
326
+ return {
327
+ under: body + tee +
328
+ soft(id, "M42.5 86Q43.3 95 44 104M57.5 86Q56.7 95 56 104", "#000000", 0.35, 1.8, "b8") +
329
+ // The open front, its zip tapes, and the knitted collar.
330
+ `<path d="M41.5 86L43 104H39.5L37.5 86.5Z" fill="${cloth}"/><path d="M58.5 86L57 104H60.5L62.5 86.5Z" fill="${cloth}"/>` +
331
+ soft(id, "M41.6 86.5L43 103.5M58.4 86.5L57 103.5", "#000000", 0.6, 0.6, "b4") +
332
+ `<path d="M34.5 75.5Q41 78.5 41.5 86L39.5 86.5Q38.6 80.5 32.5 77.5Z" fill="url(#${id}-cloth)"/>` +
333
+ `<path d="M65.5 75.5Q59 78.5 58.5 86L60.5 86.5Q61.4 80.5 67.5 77.5Z" fill="url(#${id}-cloth)"/>` +
334
+ form,
335
+ over: "",
336
+ };
337
+ }
338
+ }
339
+ /** `id` keeps each knit's patterns and clips its own where many share a page. */
340
+ export function createKnit(name, overrides, id = "knit") {
341
+ const t = knitTraits(name, overrides);
342
+ const c = coloursOf(t);
343
+ const hat = hatOf(t);
344
+ const band = bandOf(hat);
345
+ const opening = openingShape(openingOf(t));
346
+ const mood = moodOf(t);
347
+ const clothes = clothesMarkup(id, clothingOf(t), c);
348
+ const knit = `url(#${id}-mask)`;
349
+ const skinDark = mix(c.skin, BLACK, 0.32);
350
+ const blurs = Object.entries(BLURS)
351
+ .map(([key, std]) => `<filter id="${id}-${key}" ${ROOM}><feGaussianBlur stdDeviation="${std}"/></filter>`)
352
+ .join("");
353
+ const defs = `<defs>` +
354
+ // The mask's knit is fine and soft, the beanie's a fine stitch under its big ribs, as in the reference.
355
+ ribs(`${id}-mask`, c.mask, 1, 0.65) +
356
+ ribs(`${id}-beanie`, c.beanie, 0.9, 0.55) +
357
+ ribs(`${id}-cloth`, c.clothes, 2.4) +
358
+ blurs +
359
+ // Yarn: its edges a little fuzzy, and its surface flecked.
360
+ `<filter id="${id}-fuzz" ${ROOM}>` +
361
+ `<feTurbulence type="fractalNoise" baseFrequency="1.6" numOctaves="2" seed="3" result="wobble"/>` +
362
+ `<feDisplacementMap in="SourceGraphic" in2="wobble" scale="0.32" xChannelSelector="R" yChannelSelector="G" result="fuzzy"/>` +
363
+ `<feTurbulence type="fractalNoise" baseFrequency="3.2" numOctaves="1" seed="7" result="fleck"/>` +
364
+ `<feColorMatrix in="fleck" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0 0 0 -0.18" result="specks"/>` +
365
+ `<feComposite in="specks" in2="fuzzy" operator="in" result="flecked"/>` +
366
+ `<feMerge><feMergeNode in="fuzzy"/><feMergeNode in="flecked"/></feMerge></filter>` +
367
+ // The light, up to the left: a ball for the head, a dome for the crown, a cylinder for the rest.
368
+ `<radialGradient id="${id}-ball" cx="0.42" cy="0.36" r="0.66">${stops([[0, WHITE, 0.26], [0.45, WHITE, 0.04], [0.8, BLACK, 0.1], [1, BLACK, 0.3]])}</radialGradient>` +
369
+ `<linearGradient id="${id}-under" x1="0" y1="0" x2="0" y2="1">${stops([[0.7, BLACK, 0], [1, BLACK, 0.26]])}</linearGradient>` +
370
+ `<radialGradient id="${id}-dome" cx="0.4" cy="0.3" r="0.8">${stops([[0, WHITE, 0.16], [0.4, WHITE, 0], [0.8, BLACK, 0.3], [1, BLACK, 0.5]])}</radialGradient>` +
371
+ `<linearGradient id="${id}-round" x1="0" y1="0" x2="1" y2="0">${stops([[0, BLACK, 0.5], [0.12, BLACK, 0.16], [0.3, WHITE, 0.1], [0.46, WHITE, 0.03], [0.7, BLACK, 0.08], [0.88, BLACK, 0.3], [1, BLACK, 0.55]])}</linearGradient>` +
372
+ `<linearGradient id="${id}-tee" x1="0" y1="0" x2="1" y2="0">${stops([[0, BLACK, 0.2], [0.3, BLACK, 0], [0.7, BLACK, 0.02], [1, BLACK, 0.26]])}</linearGradient>` +
373
+ `<radialGradient id="${id}-pom" cx="0.38" cy="0.32" r="0.7">${stops([[0, mix(c.beanie, WHITE, 0.3), 1], [0.55, c.beanie, 1], [1, mix(c.beanie, BLACK, 0.4), 1]])}</radialGradient>` +
374
+ // The skin in the holes, and the eyes: their whites in the lids' shade, dark pupils, lids lit from above.
375
+ `<linearGradient id="${id}-skin" x1="0" y1="0" x2="0" y2="1">${stops([[0, mix(c.skin, BLACK, 0.18), 1], [0.5, c.skin, 1], [1, mix(c.skin, WHITE, 0.12), 1]])}</linearGradient>` +
376
+ `<linearGradient id="${id}-white" x1="0" y1="0" x2="0" y2="1">${stops([[0, "#c9c3be", 1], [0.4, "#eae6e2", 1], [0.78, "#f8f6f3", 1], [1, "#ddd8d3", 1]])}</linearGradient>` +
377
+ `<radialGradient id="${id}-eyeball" cx="0.5" cy="0.6" r="0.62">${stops([[0, BLACK, 0], [0.72, BLACK, 0], [1, BLACK, 0.24]])}</radialGradient>` +
378
+ `<radialGradient id="${id}-pupil" cx="0.42" cy="0.38" r="0.65">${stops([[0, "#2b1a12", 1], [0.7, "#120a07", 1], [1, "#070403", 1]])}</radialGradient>` +
379
+ `<linearGradient id="${id}-lid" x1="0" y1="0" x2="0" y2="1">${stops([[0, mix(c.skin, WHITE, 0.14), 1], [0.55, c.skin, 1], [0.9, mix(c.skin, BLACK, 0.14), 1], [0.97, mix(c.skin, WHITE, 0.06), 1], [1, mix(c.skin, BLACK, 0.2), 1]])}</linearGradient>` +
380
+ `<linearGradient id="${id}-lash" x1="0" y1="0" x2="0" y2="1">${stops([[0, mix(c.skin, BLACK, 0.5), 1], [1, mix(c.skin, BLACK, 0.62), 1]])}</linearGradient>` +
381
+ `<linearGradient id="${id}-studio" x1="0" y1="0" x2="0" y2="1">${stops([[0, mix(c.tile, WHITE, 0.3), 1], [0.55, c.tile, 1], [1, mix(c.tile, BLACK, 0.1), 1]])}</linearGradient>` +
382
+ `<radialGradient id="${id}-light" cx="0.45" cy="0.35" r="0.7">${stops([[0, WHITE, 0.22], [1, WHITE, 0]])}</radialGradient>` +
383
+ `<clipPath id="${id}-tile"><rect width="100" height="100" rx="22.22"/></clipPath>` +
384
+ `<clipPath id="${id}-head"><path d="${HEAD}"/></clipPath>` +
385
+ // Used inside the holes' own enlarging group, so it isn't enlarged again here.
386
+ `<clipPath id="${id}-holes"><path d="${opening.d}"/></clipPath>` +
387
+ `</defs>`;
388
+ // The knit bunches in folds over the cheeks: a crease under each fold, and light on its top.
389
+ const folds = [52.6, 58.6, 64]
390
+ .map((y, i) => soft(id, `M19.5 ${y}Q23.5 ${y - 0.6} ${27 - i} ${y + 0.8}`, "#000000", 0.32, 1, "b8") +
391
+ soft(id, `M80.5 ${y}Q76.5 ${y - 0.6} ${73 + i} ${y + 0.8}`, "#000000", 0.38, 1, "b8") +
392
+ soft(id, `M18.6 ${y - 3}Q21.5 ${y - 3.6} 24.5 ${y - 2.6}`, "#ffffff", 0.2, 1.2, "b8") +
393
+ soft(id, `M81.4 ${y - 3}Q78.5 ${y - 3.6} 75.5 ${y - 2.6}`, "#ffffff", 0.1, 1.2, "b8"))
394
+ .join("");
395
+ // Bare, the mask's knit gathers to a point on top of the head.
396
+ const gather = band
397
+ ? ""
398
+ : [-1, -0.4, 0.4, 1].map((s) => soft(id, `M50 16.4Q${r2(50 + s * 6)} 18.5 ${r2(50 + s * 11)} ${r2(24 + Math.abs(s) * 2)}`, "#000000", 0.28, 0.7, "b4")).join("");
399
+ const head = `<g filter="url(#${id}-fuzz)">` +
400
+ `<path d="${NECK}" fill="${knit}"/><path d="${NECK}" fill="url(#${id}-round)"/>` +
401
+ soft(id, "M39.4 72.8Q50 75 60.6 72.8", "#ffffff", 0.14, 1.4, "b8") +
402
+ soft(id, "M39 75.2Q50 77.6 61 75.2", "#000000", 0.35, 1.2, "b4") +
403
+ `</g>` +
404
+ clothes.over +
405
+ // The chin's shadow on the neck.
406
+ `<ellipse cx="50" cy="70" rx="15" ry="3" fill="#000000" fill-opacity="0.42" filter="url(#${id}-b14)"/>` +
407
+ `<g filter="url(#${id}-fuzz)">` +
408
+ `<path d="${HEAD}" fill="${knit}"/><path d="${HEAD}" fill="url(#${id}-ball)"/><path d="${HEAD}" fill="url(#${id}-under)"/>` +
409
+ `<g clip-path="url(#${id}-head)">${folds}${gather}</g>` +
410
+ `</g>`;
411
+ // The skin seen through the holes, shaded where the knit's edge overhangs it, and round each eye's socket.
412
+ const skin = `<g ${OPEN}><path d="${opening.d}" fill="url(#${id}-skin)"/>` +
413
+ `<g clip-path="url(#${id}-holes)">` +
414
+ opening.eyes.map((e) => `<rect ${eyeRect(e, 0.3)} fill="none" stroke="${skinDark}" stroke-opacity="0.5" stroke-width="1.2" filter="url(#${id}-b4)"/>`).join("") +
415
+ opening.eyes.map((e) => `<rect ${eyeRect(e, 0.5)} transform="translate(0 0.6)" fill="none" stroke="#ffffff" stroke-opacity="0.22" stroke-width="0.8" filter="url(#${id}-b4)"/>`).join("") +
416
+ (opening.nose ? soft(id, "M48.5 52Q50 57 47.5 58.5Q50 59.8 52.5 58.5Q50 57 51.5 52", skinDark, 0.8, 0.7, "b4") : "") +
417
+ (opening.mouth ? `<path d="M45.5 ${opening.nose ? 61.5 : 61.8}Q50 ${opening.nose ? 63.4 : 63.2} 54.5 ${opening.nose ? 61.5 : 61.8}" fill="none" stroke="${mix(c.skin, "#7a2d2d", 0.55)}" stroke-width="0.9" stroke-linecap="round"/>` : "") +
418
+ `<path d="${opening.d}" fill="none" stroke="#000000" stroke-opacity="0.45" stroke-width="2.6" filter="url(#${id}-b8)"/>` +
419
+ `</g></g>`;
420
+ return {
421
+ defs,
422
+ back: `<g clip-path="url(#${id}-tile)"><rect width="100" height="100" fill="url(#${id}-studio)"/><rect width="100" height="100" fill="url(#${id}-light)"/>` +
423
+ clothes.under +
424
+ // The neck's shadow on the clothes.
425
+ `<ellipse cx="50" cy="76.6" rx="12.5" ry="2.6" fill="#000000" fill-opacity="0.4" filter="url(#${id}-b14)"/>` +
426
+ head +
427
+ skin +
428
+ `</g>`,
429
+ live: (band && hat === "pompom" ? pompomMarkup(id, band) : "") + `<g ${OPEN}>${eyesMarkup(id, mood, opening.eyes)}</g>`,
430
+ front: `<g clip-path="url(#${id}-tile)">` +
431
+ // The knit's cut edge, rolled a little thick round each hole.
432
+ `<g filter="url(#${id}-fuzz)"><g ${OPEN}><path d="${opening.d}" fill="none" stroke="${mix(c.mask, WHITE, 0.06)}" stroke-width="0.9"/>` +
433
+ `<path d="${opening.d}" fill="none" stroke="${mix(c.mask, BLACK, 0.35)}" stroke-width="0.35" stroke-opacity="0.5"/></g></g>` +
434
+ // The cuff's shadow, cast down over the face and the tops of the eyes.
435
+ (band ? `<g clip-path="url(#${id}-head)">${soft(id, `M${along(band.bottom, band.x0, band.x1, 1.2)}`, "#000000", 0.5, 3.6, "b14")}</g>` : "") +
436
+ (band ? hatMarkup(id, hat, band) : "") +
437
+ `</g>`,
438
+ blink: { period: r2(t.num("blink.period", 3.6, 6.2)), delay: r2(t.num("blink.delay", 0, 3)) },
439
+ };
440
+ }
441
+ /** The bust at rest, for thumbnails. */
442
+ export function knitSvg(name, overrides) {
443
+ const k = createKnit(name, overrides);
444
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${k.defs}${k.back}${k.live}${k.front}</svg>`;
445
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * What Possibilities calms on a mallow. Each type is its reference, so it calms the feeling,
3
+ * keeping the eight gentlest at the lowest, one for each variant, and the pose and the blush.
4
+ * Indices are into each type's lists, which are in the same order.
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,19 @@
1
+ /**
2
+ * What Possibilities calms on a mallow. Each type is its reference, so it calms the feeling,
3
+ * keeping the eight gentlest at the lowest, one for each variant, and the pose and the blush.
4
+ * Indices are into each type's lists, which are in the same order.
5
+ */
6
+ export const CALMING = {
7
+ lists: {
8
+ // Content, Bliss, Shy, Joy, Wink, Sleepy, Wow, Cheeky, then Laugh, Sad, Worried and Grumpy.
9
+ face: [12, 8, [0, 9, 8, 5, 7, 4, 3, 11, 1, 2, 10, 6]],
10
+ // Each type's reference pose and its quietest, then the raised ones.
11
+ pose: [4, 2],
12
+ },
13
+ rarer: ["blush"],
14
+ };
15
+ export const NOTES = [
16
+ "only gentle, happy feelings, each type's own eyes, its arms or hands at rest, and no blush.",
17
+ "more feelings, cheeky ones too, other types' eyes, and the odd wave.",
18
+ "every feeling, sad, worried and grumpy too, every eye and every pose.",
19
+ ];
@@ -0,0 +1,2 @@
1
+ import { type ControlBuilder } from "../controls.ts";
2
+ export declare const controls: ControlBuilder;