@originkit/avatars 0.1.0 → 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.
package/README.md CHANGED
@@ -30,6 +30,28 @@ In a web page, use an `<img>` with a data URI rather than inline SVG, because in
30
30
  <img src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`} alt="" width={48} height={48} />
31
31
  ```
32
32
 
33
+ ## Live avatars in React
34
+
35
+ ```jsx
36
+ import { Avatar } from "@originkit/avatars/react";
37
+
38
+ <Avatar style="glances" name="user-8f3a2c" size={48} />
39
+ ```
40
+
41
+ The same avatars as the studio: they blink, follow the pointer and react to taps. On the server, and until the page runs, `Avatar` shows the still SVG, so nothing jumps. Bots, Blobs, Gummies and Mallows draw in 3D on a WebGL canvas of their own, and show the still where WebGL can't run. Its CSS comes with it, with nothing to import.
42
+
43
+ | Prop | |
44
+ | --- | --- |
45
+ | `style` | A style id, as `styles` lists them |
46
+ | `name` | What it's generated from |
47
+ | `overrides` | The studio's settings, as its How to use guide prints them |
48
+ | `size` | Width and height, in pixels or any CSS length (96) |
49
+ | `follow` | Whether the eyes follow the pointer (on) |
50
+ | `animate` | Whether it blinks, moves and reacts to taps (on, and off for reduced motion) |
51
+ | `label` | Read out for the image (empty: an avatar beside a name adds nothing) |
52
+
53
+ React 18 or later. It's an optional peer: the library, the CLI and the MCP server work without it.
54
+
33
55
  ## CLI
34
56
 
35
57
  Run it with npx, or install it and call `avatars`.
@@ -86,10 +86,7 @@ export type ControlBuilder = (name: string, overrides: Overrides) => Section[];
86
86
  */
87
87
  export declare function splitEssentials(sections: Section[], ids: string[]): {
88
88
  essentials: Control[];
89
- advanced: {
90
- title: string;
91
- controls: Control[];
92
- }[];
89
+ advanced: Section[];
93
90
  };
94
91
  export interface Entry {
95
92
  label: string;
package/dist/controls.js CHANGED
@@ -14,13 +14,63 @@ import { colorKey } from "./traits.js";
14
14
  export function splitEssentials(sections, ids) {
15
15
  const all = sections.flatMap((section) => section.controls);
16
16
  const essentials = ids.flatMap((id) => all.filter((control) => control.id === id));
17
- const advanced = sections
18
- .map((section) => ({
17
+ const rest = sections.map((section) => ({
19
18
  ...section,
20
19
  controls: section.controls.filter((control) => !essentials.includes(control)),
21
- }))
22
- .filter((section) => section.controls.length > 0);
23
- return { essentials, advanced };
20
+ }));
21
+ return { essentials, advanced: regroup(rest) };
22
+ }
23
+ /**
24
+ * More options, in the same groups and order for every type. Possibilities leads, with no
25
+ * heading of its own, as it governs all the rest.
26
+ */
27
+ const GROUPS = ["", "Shape", "Face", "Details", "Colour", "Container"];
28
+ /** Where a type's own sections land when nothing about a control says otherwise. */
29
+ const FROM_SECTION = {
30
+ Colour: "Colour",
31
+ Paint: "Colour",
32
+ Eyes: "Face",
33
+ Face: "Face",
34
+ Words: "Details",
35
+ Parts: "Details",
36
+ Accessories: "Details",
37
+ Hair: "Details",
38
+ Lines: "Details",
39
+ Reactions: "Details",
40
+ Container: "Container",
41
+ };
42
+ /** A control's group, read from what it sets. The type's own sections are only a fallback. */
43
+ function groupOf(control, section) {
44
+ const id = control.id;
45
+ if (id === "variance")
46
+ return "";
47
+ if (/container/.test(id))
48
+ return "Container";
49
+ // A type's Colour section holds only colours, so its word is taken as is.
50
+ if (FROM_SECTION[section] === "Colour")
51
+ return "Colour";
52
+ if (/colou?r|paint|tile|fill|skin|palette|^hue|^tone|ink|fur|^glow|frost|^pool|grain|blend|^mask|speckle/.test(id))
53
+ return "Colour";
54
+ if (/hover|effect|hat|glasses|earring|beard|^hair|mark|spots|^top$|^side|straps|studs|topper|holes|wobble|weight|gesture|title|lumps|clothing/.test(id))
55
+ return "Details";
56
+ if (/eye|brow|mouth|grin|teeth|^face|mood|feeling|expression|emotion|cheek|nose|blush|iris|formation|gaze|patch/.test(id))
57
+ return "Face";
58
+ if (/^pose|mirrored|roundness|angle|^body|^head|width|waist|softness|turn|depth|fluff|stretch/.test(id))
59
+ return "Shape";
60
+ return FROM_SECTION[section] ?? (GROUPS.includes(section) ? section : "Shape");
61
+ }
62
+ function regroup(sections) {
63
+ const groups = new Map();
64
+ for (const section of sections) {
65
+ for (const control of section.controls) {
66
+ const group = groupOf(control, section.title);
67
+ groups.set(group, [...(groups.get(group) ?? []), control]);
68
+ }
69
+ }
70
+ const rank = (title) => (GROUPS.includes(title) ? GROUPS.indexOf(title) : GROUPS.length);
71
+ return [...groups]
72
+ .map(([title, controls]) => ({ title, controls }))
73
+ .sort((a, b) => rank(a.title) - rank(b.title));
24
74
  }
25
75
  /** Just below 1, because a position of exactly 1 is past the end of every list. */
26
76
  const TOP = 0.999;
package/dist/kinds.js CHANGED
@@ -44,7 +44,7 @@ export const KINDS = [
44
44
  defaultName: "Nova",
45
45
  controls: botControls,
46
46
  // The container's size and colour join it only while it's on, which may make 8.
47
- essentials: ["head", "eye", "paint"],
47
+ essentials: ["head", "face", "eye", "mouth", "paint"],
48
48
  styleKeys: ["head"],
49
49
  displayKeys: ["container.size"],
50
50
  svg: generateBot,
@@ -72,7 +72,7 @@ export const KINDS = [
72
72
  defaultName: "Juju",
73
73
  controls: gummyControls,
74
74
  // Its colour controls and Possibilities were asked for up front, so it shows 9.
75
- essentials: ["body", "face", "glow"],
75
+ essentials: ["body", "topper", "face", "glow", "gummy.colors"],
76
76
  styleKeys: ["body", "face", "topper", "glow"],
77
77
  svg: gummySvg,
78
78
  parts: gummyParts,
@@ -86,7 +86,7 @@ export const KINDS = [
86
86
  controls: mallowControls,
87
87
  // Eyes and Pose were asked for up front beside the rest, so it shows 8. As with bots, the
88
88
  // container's size and colour join it only while it's on.
89
- essentials: ["type", "face", "colour"],
89
+ essentials: ["type", "face", "eyes", "colour", "mallow.ink"],
90
90
  // One bun for every name, as asked, so only its feeling sets variants apart.
91
91
  styleKeys: ["face"],
92
92
  displayKeys: ["container.size"],
@@ -100,7 +100,7 @@ export const KINDS = [
100
100
  hint: "Its eyes follow you. Tap it to see what it does",
101
101
  defaultName: "Maya",
102
102
  controls: faceControls,
103
- essentials: ["eyes", "mouth", "skin"],
103
+ essentials: ["eyes", "eyebrows", "mouth", "glasses", "skin"],
104
104
  styleKeys: ["mouth"],
105
105
  svg: faceSvg,
106
106
  },
@@ -111,7 +111,7 @@ export const KINDS = [
111
111
  hint: "Its eyes follow you. Write anything on it",
112
112
  defaultName: "Felix",
113
113
  controls: bubbleControls,
114
- essentials: ["shape", "mouth", "palette"],
114
+ essentials: ["shape", "eye.kind", "mouth", "palette", "bubble.colors"],
115
115
  styleKeys: ["shape", "palette"],
116
116
  svg: bubbleSvg,
117
117
  },
@@ -122,7 +122,7 @@ export const KINDS = [
122
122
  hint: "Its eyes follow you. Hover over it and it perks up",
123
123
  defaultName: "Finn",
124
124
  controls: colorControls,
125
- essentials: ["hair", "hair.color", "hat.color", "skin"],
125
+ essentials: ["hair", "hat", "skin", "hair.color", "hat.color"],
126
126
  styleKeys: ["hair"],
127
127
  svg: (name, overrides) => pixelSvg(name, "color", overrides),
128
128
  },
@@ -133,7 +133,7 @@ export const KINDS = [
133
133
  hint: "Its eyes follow you. Hover over it and it grows",
134
134
  defaultName: "Priya",
135
135
  controls: glyphControls,
136
- essentials: ["shape", "fill", "tile"],
136
+ essentials: ["shape", "eyes.kind", "fill", "tile"],
137
137
  styleKeys: ["shape"],
138
138
  svg: glyphSvg,
139
139
  },
@@ -145,7 +145,7 @@ export const KINDS = [
145
145
  defaultName: "Milo",
146
146
  controls: peekControls,
147
147
  // Possibilities was asked for up front beside the rest, so it shows 7.
148
- essentials: ["body", "eye.kind", "tile"],
148
+ essentials: ["body", "pose", "eye.kind", "tile", "peek.colors"],
149
149
  styleKeys: ["body", "pose", "tile"],
150
150
  svg: peekSvg,
151
151
  },
@@ -156,7 +156,7 @@ export const KINDS = [
156
156
  hint: "Its eyes follow you. Hover over it and it perks up",
157
157
  defaultName: "Kiko",
158
158
  controls: doodleControls,
159
- essentials: ["mood", "tile", "colourful"],
159
+ essentials: ["mood", "tile", "colourful", "ink"],
160
160
  // Colourful is off until switched on, so Colourway isn't always there to step through.
161
161
  styleKeys: ["mood", "tile"],
162
162
  svg: doodleSvg,
@@ -168,7 +168,7 @@ export const KINDS = [
168
168
  hint: "A cat, a dog, an owl, a bear or a tiger, drawn like the cats. Hover over it and its ears twitch",
169
169
  defaultName: "Biscuit",
170
170
  controls: animalControls,
171
- essentials: ["animal", "mood", "tile"],
171
+ essentials: ["animal", "mood", "eyes.color", "tile", "fur"],
172
172
  styleKeys: ["mood", "ears", "tile"],
173
173
  svg: animalSvg,
174
174
  },
@@ -181,7 +181,7 @@ export const KINDS = [
181
181
  hint: "A tabby or a calico, up to something. Hover over it and its eyes widen",
182
182
  defaultName: "Mochi",
183
183
  controls: buddyControls,
184
- essentials: ["character", "mood", "tile"],
184
+ essentials: ["character", "mood", "gesture", "tile", "fur"],
185
185
  styleKeys: ["gesture", "mood", "tile"],
186
186
  svg: buddySvg,
187
187
  },
@@ -192,7 +192,7 @@ export const KINDS = [
192
192
  hint: "A face put together from bold shapes. Its eyes follow you. Tap it and they hop",
193
193
  defaultName: "Lumi",
194
194
  controls: shapeControls,
195
- essentials: ["eye.left", "eye.right", "mouth", "tile"],
195
+ essentials: ["eye.left", "eye.right", "mouth", "tile", "colourway"],
196
196
  styleKeys: ["eye.left", "eye.right", "mouth", "colourway"],
197
197
  svg: shapeSvg,
198
198
  },
@@ -203,7 +203,7 @@ export const KINDS = [
203
203
  hint: "A monster, a cyclops, a bear, a duck, a chick, an imp or a grump, up close. It watches you and blinks, and pops when tapped",
204
204
  defaultName: "Gobo",
205
205
  controls: critterControls,
206
- essentials: ["kind", "feeling", "body"],
206
+ essentials: ["kind", "feeling", "eye.style", "body", "iris"],
207
207
  // Variants step through the creatures in the generator, as the animals' are different animals.
208
208
  styleKeys: ["feeling", "body"],
209
209
  svg: critterSvg,
@@ -215,7 +215,7 @@ export const KINDS = [
215
215
  hint: "A button of a face, a tile or round, with a big toothy grin. Its googly eyes follow you and blink, and it chomps when tapped",
216
216
  defaultName: "Zuzu",
217
217
  controls: grinControls,
218
- essentials: ["eyes", "mouth", "body"],
218
+ essentials: ["eyes", "mouth", "teeth", "body"],
219
219
  styleKeys: ["eyes", "mouth", "body"],
220
220
  svg: grinSvg,
221
221
  },
@@ -239,7 +239,7 @@ export const KINDS = [
239
239
  hint: "Just two eyes. They follow you, glance about and blink, and pop when tapped",
240
240
  defaultName: "Nyx",
241
241
  controls: glanceControls,
242
- essentials: ["eye.left", "eye.right", "tile"],
242
+ essentials: ["eye.left", "eye.right", "tile", "ink"],
243
243
  styleKeys: ["eye.left", "eye.right", "tile"],
244
244
  svg: glanceSvg,
245
245
  },
@@ -254,7 +254,7 @@ export const KINDS = [
254
254
  controls: auraControls,
255
255
  // The type and a sky's horizon were asked for up front beside the rest, as the animals'
256
256
  // type is. Formation is only an aura's and Horizon only a sky's, so it shows 7.
257
- essentials: ["type", "flow", "palette", "face"],
257
+ essentials: ["type", "flow", "palette", "aura.colors", "face"],
258
258
  // Variants step through the types, formations and palettes themselves, as Mallows do,
259
259
  // since each type has palettes of its own. See `stepped` in auras/index.ts.
260
260
  styleKeys: [],
package/dist/share.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * query string. Whatever comes back in is untrusted, so anything that doesn't check out is
4
4
  * dropped rather than thrown on.
5
5
  */
6
- import { VARIANT_COUNT } from "./variants.js";
6
+ import { MAX_VARIANT } from "./variants.js";
7
7
  /** As long as the name box allows. */
8
8
  const MAX_NAME = 256;
9
9
  const MAX_STRING = 40;
@@ -75,7 +75,7 @@ export function decodeShare(search, sections) {
75
75
  return {
76
76
  section,
77
77
  name: (params.get("n") ?? "").trim().slice(0, MAX_NAME),
78
- variant: Number.isInteger(variant) && variant >= 0 && variant < VARIANT_COUNT ? variant : 0,
78
+ variant: Number.isInteger(variant) && variant >= 0 && variant < MAX_VARIANT ? variant : 0,
79
79
  overrides,
80
80
  };
81
81
  }
@@ -2,6 +2,8 @@
2
2
  import type { AvatarKind } from "./kinds.ts";
3
3
  import type { Overrides } from "./traits.ts";
4
4
  export declare const VARIANT_COUNT = 8;
5
+ /** The most variants the open tray scrolls to, or a share link may ask for. */
6
+ export declare const MAX_VARIANT = 10000;
5
7
  /** The name a variant is generated from. The first is the name itself. */
6
8
  export declare const variantName: (name: string, variant: number) => string;
7
9
  /**
package/dist/variants.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /** Other takes on the selected avatar, shown in a strip beneath it. */
2
2
  import { patched } from "./history.js";
3
3
  export const VARIANT_COUNT = 8;
4
+ /** The most variants the open tray scrolls to, or a share link may ask for. */
5
+ export const MAX_VARIANT = 10_000;
4
6
  /** Enough for a few names' worth of variants before a typing session starts over. */
5
7
  const MAX_REMEMBERED = 256;
6
8
  /** The name a variant is generated from. The first is the name itself. */
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@originkit/avatars",
3
- "version": "0.1.0",
4
- "description": "OriginKit Avatars: turn any name into an avatar. 18 styles, SVG out, with a CLI and an MCP server.",
3
+ "version": "0.2.0",
4
+ "description": "OriginKit Avatars: turn any name into an avatar. 18 styles, SVG out, live React avatars, a CLI and an MCP server.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./dist/index.d.ts",
9
9
  "default": "./dist/index.js"
10
+ },
11
+ "./react": {
12
+ "types": "./react/index.d.ts",
13
+ "default": "./react/index.js"
10
14
  }
11
15
  },
12
16
  "bin": {
@@ -14,6 +18,7 @@
14
18
  },
15
19
  "files": [
16
20
  "dist",
21
+ "react",
17
22
  "bin",
18
23
  "mcp",
19
24
  "GUIDE.md",
@@ -21,7 +26,7 @@
21
26
  "README.md"
22
27
  ],
23
28
  "scripts": {
24
- "build": "rm -rf dist && tsc -p tsconfig.json && cp ../../LICENSE .",
29
+ "build": "rm -rf dist && tsc -p tsconfig.json && node scripts/build-react.mjs && cp ../../LICENSE .",
25
30
  "test": "node --test \"test/*.test.js\"",
26
31
  "prepublishOnly": "npm run build && npm test"
27
32
  },
@@ -29,6 +34,15 @@
29
34
  "@modelcontextprotocol/sdk": "^1.32.2",
30
35
  "zod": "^3.25 || ^4.0"
31
36
  },
37
+ "peerDependencies": {
38
+ "react": ">=18"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "react": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "sideEffects": false,
32
46
  "publishConfig": {
33
47
  "access": "public"
34
48
  },
@@ -0,0 +1,28 @@
1
+ import type { JSX } from "react";
2
+
3
+ /** The studio's settings for an avatar, as its How to use guide prints them. */
4
+ export type Overrides = Record<string, number | string>;
5
+
6
+ export interface AvatarProps {
7
+ /** A style id, as `styles` from "@originkit/avatars" lists them, such as "bots" or "glances". */
8
+ style: string;
9
+ /** What it's generated from. The same name always gives the same avatar. */
10
+ name: string;
11
+ /** The studio's settings for it. */
12
+ overrides?: Overrides;
13
+ /** Width and height, in pixels or any CSS length. 96 by default. */
14
+ size?: number | string;
15
+ /** Whether the eyes follow the pointer. On by default. */
16
+ follow?: boolean;
17
+ /** Whether it blinks, moves and reacts to taps. On by default, and off for reduced motion. */
18
+ animate?: boolean;
19
+ /** Read out for the image. Empty by default, as an avatar beside a name adds nothing. */
20
+ label?: string;
21
+ className?: string;
22
+ }
23
+
24
+ /**
25
+ * A live avatar: it blinks, follows the pointer and reacts to taps. On the server, and until
26
+ * the page is running, it shows the still SVG of the same avatar.
27
+ */
28
+ export function Avatar(props: AvatarProps): JSX.Element;