@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.
- package/GUIDE.md +44 -0
- package/LICENSE +58 -0
- package/README.md +141 -2
- package/bin/avatars.js +65 -0
- package/dist/art.d.ts +43 -0
- package/dist/art.js +35 -0
- package/dist/auras/calming.d.ts +18 -0
- package/dist/auras/calming.js +30 -0
- package/dist/auras/controls.d.ts +2 -0
- package/dist/auras/controls.js +60 -0
- package/dist/auras/index.d.ts +119 -0
- package/dist/auras/index.js +355 -0
- package/dist/auras/skies.d.ts +13 -0
- package/dist/auras/skies.js +22 -0
- package/dist/blobs/color.d.ts +19 -0
- package/dist/blobs/color.js +113 -0
- package/dist/blobs/controls.d.ts +2 -0
- package/dist/blobs/controls.js +77 -0
- package/dist/blobs/index.d.ts +5 -0
- package/dist/blobs/index.js +107 -0
- package/dist/blobs/paths.d.ts +33 -0
- package/dist/blobs/paths.js +102 -0
- package/dist/blobs/shapes.d.ts +46 -0
- package/dist/blobs/shapes.js +186 -0
- package/dist/blobs/variance.d.ts +22 -0
- package/dist/blobs/variance.js +42 -0
- package/dist/bots/attachments.d.ts +24 -0
- package/dist/bots/attachments.js +119 -0
- package/dist/bots/calming.d.ts +4 -0
- package/dist/bots/calming.js +19 -0
- package/dist/bots/controls.d.ts +2 -0
- package/dist/bots/controls.js +78 -0
- package/dist/bots/faces.d.ts +23 -0
- package/dist/bots/faces.js +122 -0
- package/dist/bots/heads.d.ts +15 -0
- package/dist/bots/heads.js +66 -0
- package/dist/bots/index.d.ts +5 -0
- package/dist/bots/index.js +47 -0
- package/dist/bots/palette.d.ts +15 -0
- package/dist/bots/palette.js +30 -0
- package/dist/bots/paths.d.ts +25 -0
- package/dist/bots/paths.js +89 -0
- package/dist/bots/trim.d.ts +12 -0
- package/dist/bots/trim.js +37 -0
- package/dist/bubbles/calming.d.ts +7 -0
- package/dist/bubbles/calming.js +19 -0
- package/dist/bubbles/controls.d.ts +2 -0
- package/dist/bubbles/controls.js +69 -0
- package/dist/bubbles/index.d.ts +65 -0
- package/dist/bubbles/index.js +279 -0
- package/dist/buddies/calming.d.ts +8 -0
- package/dist/buddies/calming.js +18 -0
- package/dist/buddies/controls.d.ts +2 -0
- package/dist/buddies/controls.js +35 -0
- package/dist/buddies/index.d.ts +113 -0
- package/dist/buddies/index.js +363 -0
- package/dist/colors.d.ts +20 -0
- package/dist/colors.js +41 -0
- package/dist/combinations.d.ts +9 -0
- package/dist/combinations.js +28 -0
- package/dist/container.d.ts +14 -0
- package/dist/container.js +67 -0
- package/dist/controls.d.ts +125 -0
- package/dist/controls.js +220 -0
- package/dist/critters/calming.d.ts +8 -0
- package/dist/critters/calming.js +18 -0
- package/dist/critters/controls.d.ts +2 -0
- package/dist/critters/controls.js +49 -0
- package/dist/critters/index.d.ts +113 -0
- package/dist/critters/index.js +307 -0
- package/dist/doodles/calming.d.ts +7 -0
- package/dist/doodles/calming.js +18 -0
- package/dist/doodles/controls.d.ts +2 -0
- package/dist/doodles/controls.js +50 -0
- package/dist/doodles/index.d.ts +66 -0
- package/dist/doodles/index.js +236 -0
- package/dist/faces/adventurer-neutral.json +3168 -0
- package/dist/faces/calming.d.ts +8 -0
- package/dist/faces/calming.js +20 -0
- package/dist/faces/controls.d.ts +2 -0
- package/dist/faces/controls.js +38 -0
- package/dist/faces/index.d.ts +44 -0
- package/dist/faces/index.js +108 -0
- package/dist/glances/calming.d.ts +8 -0
- package/dist/glances/calming.js +16 -0
- package/dist/glances/controls.d.ts +2 -0
- package/dist/glances/controls.js +27 -0
- package/dist/glances/index.d.ts +54 -0
- package/dist/glances/index.js +79 -0
- package/dist/glyphs/calming.d.ts +8 -0
- package/dist/glyphs/calming.js +17 -0
- package/dist/glyphs/controls.d.ts +2 -0
- package/dist/glyphs/controls.js +55 -0
- package/dist/glyphs/index.d.ts +62 -0
- package/dist/glyphs/index.js +197 -0
- package/dist/grins/calming.d.ts +8 -0
- package/dist/grins/calming.js +21 -0
- package/dist/grins/controls.d.ts +2 -0
- package/dist/grins/controls.js +33 -0
- package/dist/grins/index.d.ts +81 -0
- package/dist/grins/index.js +240 -0
- package/dist/gummies/art.d.ts +77 -0
- package/dist/gummies/art.js +168 -0
- package/dist/gummies/calming.d.ts +8 -0
- package/dist/gummies/calming.js +21 -0
- package/dist/gummies/controls.d.ts +2 -0
- package/dist/gummies/controls.js +53 -0
- package/dist/gummies/index.d.ts +37 -0
- package/dist/gummies/index.js +283 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +48 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +16 -0
- package/dist/kinds.d.ts +30 -0
- package/dist/kinds.js +266 -0
- package/dist/kitties/calming.d.ts +8 -0
- package/dist/kitties/calming.js +19 -0
- package/dist/kitties/controls.d.ts +2 -0
- package/dist/kitties/controls.js +43 -0
- package/dist/kitties/index.d.ts +181 -0
- package/dist/kitties/index.js +673 -0
- package/dist/knits/calming.d.ts +7 -0
- package/dist/knits/calming.js +19 -0
- package/dist/knits/controls.d.ts +2 -0
- package/dist/knits/controls.js +32 -0
- package/dist/knits/index.d.ts +74 -0
- package/dist/knits/index.js +445 -0
- package/dist/mallows/calming.d.ts +8 -0
- package/dist/mallows/calming.js +19 -0
- package/dist/mallows/controls.d.ts +2 -0
- package/dist/mallows/controls.js +45 -0
- package/dist/mallows/index.d.ts +111 -0
- package/dist/mallows/index.js +454 -0
- package/dist/peeks/calming.d.ts +8 -0
- package/dist/peeks/calming.js +21 -0
- package/dist/peeks/controls.d.ts +2 -0
- package/dist/peeks/controls.js +54 -0
- package/dist/peeks/index.d.ts +80 -0
- package/dist/peeks/index.js +225 -0
- package/dist/pixels/calming.d.ts +8 -0
- package/dist/pixels/calming.js +22 -0
- package/dist/pixels/controls.d.ts +3 -0
- package/dist/pixels/controls.js +82 -0
- package/dist/pixels/index.d.ts +34 -0
- package/dist/pixels/index.js +174 -0
- package/dist/pixels/palette.d.ts +29 -0
- package/dist/pixels/palette.js +107 -0
- package/dist/pixels/sprites.d.ts +42 -0
- package/dist/pixels/sprites.js +154 -0
- package/dist/possibilities.d.ts +39 -0
- package/dist/possibilities.js +88 -0
- package/dist/shapes/calming.d.ts +7 -0
- package/dist/shapes/calming.js +19 -0
- package/dist/shapes/controls.d.ts +2 -0
- package/dist/shapes/controls.js +41 -0
- package/dist/shapes/index.d.ts +44 -0
- package/dist/shapes/index.js +104 -0
- package/dist/share.d.ts +21 -0
- package/dist/share.js +83 -0
- package/dist/traits.d.ts +34 -0
- package/dist/traits.js +58 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/variants.d.ts +13 -0
- package/dist/variants.js +43 -0
- package/mcp/server.js +7 -0
- package/mcp/tools.js +70 -0
- package/package.json +68 -4
- package/react/index.d.ts +28 -0
- package/react/index.js +869 -0
- package/react/styles.js +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { choice, clearingColor, palette, slider } from "../controls.js";
|
|
2
|
+
import { COLOURWAYS } from "../doodles/index.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, EYES, MOUTHS, PARTS, TILES } from "./index.js";
|
|
7
|
+
const named = (list) => list.map(({ name: label }) => ({ label }));
|
|
8
|
+
export const controls = (name, overrides) => {
|
|
9
|
+
const t = calmed(createTraits(name, overrides), CALMING);
|
|
10
|
+
const list = (control) => narrowed(control, t, CALMING);
|
|
11
|
+
const { parts } = coloursOf(t);
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
title: "Face",
|
|
15
|
+
controls: [
|
|
16
|
+
list(choice(t, "eye.left", "Left eye", "list", named(EYES))),
|
|
17
|
+
list(choice(t, "eye.right", "Right eye", "list", named(EYES))),
|
|
18
|
+
list(choice(t, "mouth", "Mouth", "list", named(MOUTHS))),
|
|
19
|
+
possibilitiesSlider(t, NOTES),
|
|
20
|
+
slider(t, "eye.size", "Eye size", 0, 100, "%"),
|
|
21
|
+
slider(t, "eye.gap", "Eye gap", 0, 100, "%"),
|
|
22
|
+
slider(t, "mouth.size", "Mouth size", 0, 100, "%"),
|
|
23
|
+
slider(t, "mouth.y", "Mouth drop", 0, 100, "%"),
|
|
24
|
+
slider(t, "weight", "Weight", 0, 100, "%"),
|
|
25
|
+
slider(t, "tilt", "Tilt", -5, 5, "°"),
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: "Colour",
|
|
30
|
+
controls: [
|
|
31
|
+
palette(t, "tile", "Tile", TILES),
|
|
32
|
+
// A new colourway starts from its own colours, not the ones set by hand for the last.
|
|
33
|
+
PARTS.reduce(clearingColor, choice(t, "colourway", "Colourway", "swatches", COLOURWAYS.map(({ label, colors }) => ({
|
|
34
|
+
label,
|
|
35
|
+
color: `conic-gradient(${colors.map((color, i) => `${color} 0 ${(i + 1) * 25}%`).join(", ")})`,
|
|
36
|
+
})))),
|
|
37
|
+
{ type: "colors", id: "colours", label: "Colours", colors: PARTS.map((part) => ({ id: colorKey(part), color: parts[part] })) },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shapes: a face put together from three bold shapes, as in the reference. Each eye and
|
|
3
|
+
* the mouth is one fat stroke with round ends along a short skeleton, all at one weight,
|
|
4
|
+
* so a cloud, a donut and a smile read as one set. Each part comes from its own list in
|
|
5
|
+
* its own colour, so a few short lists make a great many faces.
|
|
6
|
+
*/
|
|
7
|
+
import { type Overrides, type Traits } from "../traits.ts";
|
|
8
|
+
/** Paper. The first is the reference's. */
|
|
9
|
+
export declare const TILES: string[];
|
|
10
|
+
/** Scales a skeleton drawn in units of its reach. */
|
|
11
|
+
type At = (x: number, y: number) => string;
|
|
12
|
+
interface Part {
|
|
13
|
+
name: string;
|
|
14
|
+
/** How far it reaches from its middle, stroke and all: across for an eye, up for a mouth. */
|
|
15
|
+
span(a: number, w: number): number;
|
|
16
|
+
/** Markup around the origin, from the skeleton's reach `a` and the stroke's width `w`. */
|
|
17
|
+
draw(at: At, a: number, w: number): string;
|
|
18
|
+
}
|
|
19
|
+
/** Drawn facing right, towards the nose, for the left eye. The right eye is a mirror image. */
|
|
20
|
+
export declare const EYES: Part[];
|
|
21
|
+
/** In units of the mouth's half width. Neighbours are unlike, as each variant steps to the next. */
|
|
22
|
+
export declare const MOUTHS: Part[];
|
|
23
|
+
/** Each with a colour of its own, which can be picked by hand. */
|
|
24
|
+
export declare const PARTS: string[];
|
|
25
|
+
/** Each name takes three of its colourway's four, in its own order, as the reference does. */
|
|
26
|
+
export declare function coloursOf(t: Traits): {
|
|
27
|
+
tile: string;
|
|
28
|
+
parts: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
export interface ShapeFace {
|
|
31
|
+
tile: string;
|
|
32
|
+
/** Where the face sits and its tilt. */
|
|
33
|
+
face: string;
|
|
34
|
+
/** The one stroke weight every shape is drawn in. */
|
|
35
|
+
weight: number;
|
|
36
|
+
/** Each part sits in a group that places it, around one that is free to move about its middle. */
|
|
37
|
+
eyes: string;
|
|
38
|
+
mouth: string;
|
|
39
|
+
blinkDelay: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function createShapeFace(name: string, overrides?: Overrides): ShapeFace;
|
|
42
|
+
/** The face at rest, for thumbnails. */
|
|
43
|
+
export declare const shapeSvg: (name: string, overrides?: Overrides) => string;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shapes: a face put together from three bold shapes, as in the reference. Each eye and
|
|
3
|
+
* the mouth is one fat stroke with round ends along a short skeleton, all at one weight,
|
|
4
|
+
* so a cloud, a donut and a smile read as one set. Each part comes from its own list in
|
|
5
|
+
* its own colour, so a few short lists make a great many faces.
|
|
6
|
+
*/
|
|
7
|
+
import { contrast } from "../colors.js";
|
|
8
|
+
import { COLOURWAYS, INK_STYLE } from "../doodles/index.js";
|
|
9
|
+
import { calmed } from "../possibilities.js";
|
|
10
|
+
import { createTraits } from "../traits.js";
|
|
11
|
+
import { CALMING } from "./calming.js";
|
|
12
|
+
/** Paper. The first is the reference's. */
|
|
13
|
+
export const TILES = ["#f9f8f4", "#ffffff", "#fbefe6", "#edf5ef", "#ecf1f8", "#f4eef8", "#fff5d6", "#f1efe9"];
|
|
14
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
15
|
+
const line = (d) => `<path d="${d}" fill="none"/>`;
|
|
16
|
+
/** Filled as well as stroked. Left open, the stroke skips the closing edge, as a heart's notch needs. */
|
|
17
|
+
const solid = (d) => `<path d="${d}"/>`;
|
|
18
|
+
/** A ring always keeps a hole, however heavy the stroke. */
|
|
19
|
+
const ringRadius = (r, w) => Math.max(r, w / 2 + 2.2);
|
|
20
|
+
const ring = (r, w) => `<circle r="${round2(ringRadius(r, w))}" fill="none"/>`;
|
|
21
|
+
const by = (reach) => (a, w) => reach * a + w / 2;
|
|
22
|
+
/** Drawn facing right, towards the nose, for the left eye. The right eye is a mirror image. */
|
|
23
|
+
export const EYES = [
|
|
24
|
+
{ name: "Donut", span: (a, w) => ringRadius(a, w) + w / 2, draw: (_at, a, w) => ring(a, w) },
|
|
25
|
+
{ name: "Chevron", span: by(0.45), draw: (at) => line(`M${at(-0.45, -0.85)}L${at(0.45, 0)}L${at(-0.45, 0.85)}`) },
|
|
26
|
+
{ name: "Disc", span: by(0.85), draw: (_at, a) => `<circle r="${round2(a * 0.85)}"/>` },
|
|
27
|
+
// The reference's: a flat bar with a bump, sloping down to its end.
|
|
28
|
+
{ name: "Cloud", span: by(1.15), draw: (at) => line(`M${at(-1.15, 0.5)}L${at(1.15, 0.5)}L${at(0.15, -0.5)}`) },
|
|
29
|
+
{ name: "Plus", span: by(0.85), draw: (at) => line(`M${at(-0.85, 0)}L${at(0.85, 0)}M${at(0, -0.85)}L${at(0, 0.85)}`) },
|
|
30
|
+
{ name: "Arch", span: by(1), draw: (at) => line(`M${at(-1, 0.35)}Q${at(0, -1.05)} ${at(1, 0.35)}`) },
|
|
31
|
+
{ name: "Square", span: by(0.6), draw: (at) => solid(`M${at(-0.6, -0.6)}L${at(0.6, -0.6)}L${at(0.6, 0.6)}L${at(-0.6, 0.6)}Z`) },
|
|
32
|
+
{ name: "Pill", span: by(0), draw: (at) => line(`M${at(0, -0.75)}L${at(0, 0.75)}`) },
|
|
33
|
+
{ name: "Heart", span: by(0.7), draw: (at) => solid(`M${at(-0.7, -0.3)}L${at(0, 0.5)}L${at(0.7, -0.3)}`) },
|
|
34
|
+
{ name: "Dash", span: by(0.8), draw: (at) => line(`M${at(-0.8, 0)}L${at(0.8, 0)}`) },
|
|
35
|
+
{ name: "Triangle", span: by(0.8), draw: (at) => solid(`M${at(0, -0.75)}L${at(0.8, 0.55)}L${at(-0.8, 0.55)}Z`) },
|
|
36
|
+
{ name: "Leaf", span: by(0.75), draw: (at) => solid(`M${at(-0.75, 0.75)}Q${at(-0.6, -0.6)} ${at(0.75, -0.75)}Q${at(0.6, 0.6)} ${at(-0.75, 0.75)}Z`) },
|
|
37
|
+
{ name: "Dome", span: by(0.85), draw: (at, a) => solid(`M${at(-0.85, 0.42)}A${round2(a * 0.85)} ${round2(a * 0.85)} 0 0 1 ${at(0.85, 0.42)}Z`) },
|
|
38
|
+
];
|
|
39
|
+
/** In units of the mouth's half width. Neighbours are unlike, as each variant steps to the next. */
|
|
40
|
+
export const MOUTHS = [
|
|
41
|
+
{ name: "Smile", span: by(0.2), draw: (at, a) => line(`M${at(-1, -0.2)}A${round2(a * 1.4)} ${round2(a * 1.4)} 0 0 0 ${at(1, -0.2)}`) },
|
|
42
|
+
{ name: "Oh", span: (a, w) => a * 0.1 + w, draw: (_at, a, w) => ring(a * 0.1 + w / 2, w) },
|
|
43
|
+
{ name: "Flat", span: by(0), draw: (at) => line(`M${at(-0.65, 0)}L${at(0.65, 0)}`) },
|
|
44
|
+
{ name: "Grin", span: by(0.25), draw: (at, a) => solid(`M${at(-0.8, -0.25)}L${at(0.8, -0.25)}A${round2(a * 0.8)} ${round2(a * 0.55)} 0 0 1 ${at(-0.8, -0.25)}Z`) },
|
|
45
|
+
{ name: "Wave", span: by(0.25), draw: (at) => line(`M${at(-1, 0)}Q${at(-0.5, -0.45)} ${at(0, 0)}T${at(1, 0)}`) },
|
|
46
|
+
{ name: "Peep", span: by(0.12), draw: (_at, a) => `<circle r="${round2(a * 0.12)}"/>` },
|
|
47
|
+
{ name: "Frown", span: by(0.15), draw: (at, a) => line(`M${at(-0.7, 0.25)}A${round2(a)} ${round2(a)} 0 0 1 ${at(0.7, 0.25)}`) },
|
|
48
|
+
{ name: "Shout", span: by(0.25), draw: (at) => solid(`M${at(-0.6, -0.25)}L${at(0.6, -0.25)}L${at(0.3, 0.25)}L${at(-0.3, 0.25)}Z`) },
|
|
49
|
+
{ name: "Smirk", span: by(0.25), draw: (at) => line(`M${at(-0.6, 0.05)}Q${at(0.25, 0.3)} ${at(0.7, -0.25)}`) },
|
|
50
|
+
{ name: "Cat", span: by(0.1), draw: (at) => line(`M${at(-0.8, -0.1)}Q${at(-0.4, 0.35)} ${at(0, 0)}Q${at(0.4, 0.35)} ${at(0.8, -0.1)}`) },
|
|
51
|
+
];
|
|
52
|
+
/** Each with a colour of its own, which can be picked by hand. */
|
|
53
|
+
export const PARTS = ["left", "right", "mouth"];
|
|
54
|
+
/** Each name takes three of its colourway's four, in its own order, as the reference does. */
|
|
55
|
+
export function coloursOf(t) {
|
|
56
|
+
const tile = t.color("tile") ?? t.pick("tile", TILES);
|
|
57
|
+
// The spare colour stands in for one that would fade into the tile, as yellow does on yellow.
|
|
58
|
+
const order = t.pick("colourway", COLOURWAYS).colors
|
|
59
|
+
.map((color, i) => [t(`colourway.${i}`) + (contrast(color, tile) < 1.5 ? 1 : 0), color])
|
|
60
|
+
.sort(([a], [b]) => a - b);
|
|
61
|
+
return { tile, parts: Object.fromEntries(PARTS.map((part, i) => [part, t.color(part) ?? order[i][1]])) };
|
|
62
|
+
}
|
|
63
|
+
const scaled = (a) => (x, y) => `${round2(x * a)} ${round2(y * a)}`;
|
|
64
|
+
export function createShapeFace(name, overrides) {
|
|
65
|
+
const t = calmed(createTraits(name, overrides), CALMING);
|
|
66
|
+
const { tile, parts } = coloursOf(t);
|
|
67
|
+
const size = t.num("eye.size", 13, 17);
|
|
68
|
+
// In proportion to the eyes, so a heavy stroke never fills a small shape in.
|
|
69
|
+
const w = size * t.num("weight", 0.68, 0.9);
|
|
70
|
+
const gap = t.num("eye.gap", 1.5, 6);
|
|
71
|
+
const eyes = [-1, 1].map((side, i) => {
|
|
72
|
+
const shape = t.pick(i ? "eye.right" : "eye.left", EYES);
|
|
73
|
+
const r = size * (1 + t.jitter(`eye${i}.size`, 0.14));
|
|
74
|
+
const a = Math.max(r - w / 2, r * 0.45);
|
|
75
|
+
return { side, shape, r, a, dy: t.jitter(`eye${i}.y`, 1.8), x: side * (gap + Math.max(shape.span(a, w), r * 0.8)) };
|
|
76
|
+
});
|
|
77
|
+
const mouth = t.pick("mouth", MOUTHS);
|
|
78
|
+
const m = t.num("mouth.size", 19, 28);
|
|
79
|
+
const rise = mouth.span(m, w);
|
|
80
|
+
const above = Math.max(...eyes.map(({ r, dy }) => r - dy));
|
|
81
|
+
const drop = Math.max(...eyes.map(({ r, dy }) => r + dy)) + t.num("mouth.y", 5, 13) + rise;
|
|
82
|
+
// Taking the mouth to be about as deep as it is tall above its middle. A wide or tall
|
|
83
|
+
// face is shrunk to fit the tile, stroke and all, so it keeps its proportions.
|
|
84
|
+
const across = Math.max(...eyes.map(({ x, shape, a }) => Math.abs(x) + shape.span(a, w)), m + w / 2) * 2;
|
|
85
|
+
const fit = Math.min(1, 80 / across, 76 / (above + drop + rise));
|
|
86
|
+
const placed = (transform, color, markup) => `<g transform="${transform}" color="${color}"><g>${markup}</g></g>`;
|
|
87
|
+
const flip = t("mouth.flip") < 0.5 ? -1 : 1;
|
|
88
|
+
return {
|
|
89
|
+
tile,
|
|
90
|
+
face: `translate(50 50) scale(${round2(fit)}) rotate(${round2(t.jitter("tilt", 5))}) translate(0 ${round2((above - drop - rise) / 2)})`,
|
|
91
|
+
weight: round2(w),
|
|
92
|
+
eyes: eyes
|
|
93
|
+
.map(({ side, shape, a, x, dy }, i) => placed(`translate(${round2(x)} ${round2(dy)}) rotate(${round2(t.jitter(`eye${i}.turn`, 10))}) scale(${-side} 1)`, parts[PARTS[i]], shape.draw(scaled(a), a, w)))
|
|
94
|
+
.join(""),
|
|
95
|
+
mouth: placed(`translate(0 ${round2(drop)}) scale(${flip} 1)`, parts.mouth, mouth.draw(scaled(m), m, w)),
|
|
96
|
+
blinkDelay: t.num("blink", 0, 4),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** The face at rest, for thumbnails. */
|
|
100
|
+
export const shapeSvg = (name, overrides) => {
|
|
101
|
+
const face = createShapeFace(name, overrides);
|
|
102
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="22.22" fill="${face.tile}"/>` +
|
|
103
|
+
`<g transform="${face.face}" ${INK_STYLE(face.weight)}>${face.eyes}${face.mouth}</g></svg>`);
|
|
104
|
+
};
|
package/dist/share.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selected avatar as a link: its section, name, variant and hand-set overrides in the
|
|
3
|
+
* query string. Whatever comes back in is untrusted, so anything that doesn't check out is
|
|
4
|
+
* dropped rather than thrown on.
|
|
5
|
+
*/
|
|
6
|
+
import type { Overrides } from "./traits.ts";
|
|
7
|
+
export interface Shared {
|
|
8
|
+
section: string;
|
|
9
|
+
/** The typed name, or empty for the section's own. */
|
|
10
|
+
name: string;
|
|
11
|
+
variant: number;
|
|
12
|
+
overrides: Overrides;
|
|
13
|
+
}
|
|
14
|
+
/** The query string, without its `?`. */
|
|
15
|
+
export declare function encodeShare({ section, name, variant, overrides }: Shared): string;
|
|
16
|
+
/** Only the values that are safe to draw with: see the rules above each branch. */
|
|
17
|
+
export declare function cleanOverrides(raw: unknown): Overrides;
|
|
18
|
+
/** What a query string says, for a section in `sections`, or null if it names none. */
|
|
19
|
+
export declare function decodeShare(search: string, sections: readonly string[]): Shared | null;
|
|
20
|
+
/** `<section>-<name>.<ext>`, with anything a file system might refuse swapped for dashes. */
|
|
21
|
+
export declare const fileName: (section: string, name: string, ext: string) => string;
|
package/dist/share.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selected avatar as a link: its section, name, variant and hand-set overrides in the
|
|
3
|
+
* query string. Whatever comes back in is untrusted, so anything that doesn't check out is
|
|
4
|
+
* dropped rather than thrown on.
|
|
5
|
+
*/
|
|
6
|
+
import { MAX_VARIANT } from "./variants.js";
|
|
7
|
+
/** As long as the name box allows. */
|
|
8
|
+
const MAX_NAME = 256;
|
|
9
|
+
const MAX_STRING = 40;
|
|
10
|
+
const MAX_KEYS = 64;
|
|
11
|
+
/** Well past any real set of overrides, so a giant link isn't decoded at all. */
|
|
12
|
+
const MAX_ENCODED = 4096;
|
|
13
|
+
const KEY = /^[a-z][\w.:-]{0,63}$/i;
|
|
14
|
+
const TOP = 1 - Number.EPSILON;
|
|
15
|
+
const toBase64Url = (text) => btoa(String.fromCharCode(...new TextEncoder().encode(text))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
16
|
+
const fromBase64Url = (code) => new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(atob(code.replace(/-/g, "+").replace(/_/g, "/")), (char) => char.charCodeAt(0)));
|
|
17
|
+
/** The query string, without its `?`. */
|
|
18
|
+
export function encodeShare({ section, name, variant, overrides }) {
|
|
19
|
+
const params = new URLSearchParams({ s: section });
|
|
20
|
+
if (name)
|
|
21
|
+
params.set("n", name);
|
|
22
|
+
if (variant)
|
|
23
|
+
params.set("v", String(variant));
|
|
24
|
+
if (Object.keys(overrides).length)
|
|
25
|
+
params.set("o", toBase64Url(JSON.stringify(overrides)));
|
|
26
|
+
return params.toString();
|
|
27
|
+
}
|
|
28
|
+
/** Only the values that are safe to draw with: see the rules above each branch. */
|
|
29
|
+
export function cleanOverrides(raw) {
|
|
30
|
+
const clean = {};
|
|
31
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
32
|
+
return clean;
|
|
33
|
+
for (const [key, value] of Object.entries(raw).slice(0, MAX_KEYS)) {
|
|
34
|
+
if (!KEY.test(key))
|
|
35
|
+
continue;
|
|
36
|
+
if (key.startsWith("color:")) {
|
|
37
|
+
// A colour picked by hand is 0xRRGGBB.
|
|
38
|
+
if (Number.isInteger(value) && value >= 0 && value <= 0xffffff)
|
|
39
|
+
clean[key] = value;
|
|
40
|
+
}
|
|
41
|
+
else if (typeof value === "number") {
|
|
42
|
+
// Every other number is a position in [0, 1).
|
|
43
|
+
if (Number.isFinite(value))
|
|
44
|
+
clean[key] = Math.min(TOP, Math.max(0, value));
|
|
45
|
+
}
|
|
46
|
+
else if (typeof value === "string") {
|
|
47
|
+
clean[key] = value.slice(0, MAX_STRING);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return clean;
|
|
51
|
+
}
|
|
52
|
+
/** What a query string says, for a section in `sections`, or null if it names none. */
|
|
53
|
+
export function decodeShare(search, sections) {
|
|
54
|
+
let params;
|
|
55
|
+
try {
|
|
56
|
+
params = new URLSearchParams(search);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const section = params.get("s") ?? "";
|
|
62
|
+
if (!sections.includes(section))
|
|
63
|
+
return null;
|
|
64
|
+
const variant = Number(params.get("v") ?? 0);
|
|
65
|
+
let overrides = {};
|
|
66
|
+
const code = params.get("o");
|
|
67
|
+
if (code && code.length <= MAX_ENCODED) {
|
|
68
|
+
try {
|
|
69
|
+
overrides = cleanOverrides(JSON.parse(fromBase64Url(code)));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// A broken link still opens the section, just without its tweaks.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
section,
|
|
77
|
+
name: (params.get("n") ?? "").trim().slice(0, MAX_NAME),
|
|
78
|
+
variant: Number.isInteger(variant) && variant >= 0 && variant < MAX_VARIANT ? variant : 0,
|
|
79
|
+
overrides,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** `<section>-<name>.<ext>`, with anything a file system might refuse swapped for dashes. */
|
|
83
|
+
export const fileName = (section, name, ext) => `${section}-${name.replace(/[\\/:*?"<>|\s\u0000-\u001f]+/g, "-").slice(0, 64) || "avatar"}.${ext}`;
|
package/dist/traits.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a name into independent random-looking values.
|
|
3
|
+
*
|
|
4
|
+
* The name is hashed once, then each trait key continues from that state, so
|
|
5
|
+
* every trait is stable and unrelated to the others. Adding a new trait later
|
|
6
|
+
* cannot change an existing avatar.
|
|
7
|
+
*/
|
|
8
|
+
export interface Traits {
|
|
9
|
+
/** Uniform float in [0, 1). */
|
|
10
|
+
(key: string): number;
|
|
11
|
+
/** Uniform float in [min, max). */
|
|
12
|
+
num(key: string, min: number, max: number): number;
|
|
13
|
+
/** Uniform integer in [min, max]. */
|
|
14
|
+
int(key: string, min: number, max: number): number;
|
|
15
|
+
/** Symmetric offset in [-amount, amount). */
|
|
16
|
+
jitter(key: string, amount: number): number;
|
|
17
|
+
/** Uniform choice. Changing `options` remaps every name, so treat lists as frozen. */
|
|
18
|
+
pick<T>(key: string, options: readonly T[]): T;
|
|
19
|
+
/** The value fixed for `key`, if any, for things that are otherwise worked out. */
|
|
20
|
+
forced(key: string): number | undefined;
|
|
21
|
+
/** The colour picked by hand for `key`, if any, which wins over whatever the name gives. */
|
|
22
|
+
color(key: string): string | undefined;
|
|
23
|
+
/** The words typed in for `key`, if any. */
|
|
24
|
+
text(key: string): string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Traits fixed by hand, by key. Each value is the position in [0, 1) the name would
|
|
28
|
+
* otherwise have hashed to, so a generator cannot tell the two apart. The exceptions
|
|
29
|
+
* are a colour picked by hand, kept beside its trait as `0xrrggbb` under `colorKey`,
|
|
30
|
+
* and typed words, which are strings.
|
|
31
|
+
*/
|
|
32
|
+
export type Overrides = Record<string, number | string>;
|
|
33
|
+
export declare const colorKey: (key: string) => string;
|
|
34
|
+
export declare function createTraits(name: string, overrides?: Overrides): Traits;
|
package/dist/traits.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a name into independent random-looking values.
|
|
3
|
+
*
|
|
4
|
+
* The name is hashed once, then each trait key continues from that state, so
|
|
5
|
+
* every trait is stable and unrelated to the others. Adding a new trait later
|
|
6
|
+
* cannot change an existing avatar.
|
|
7
|
+
*/
|
|
8
|
+
import { intToHex } from "./colors.js";
|
|
9
|
+
const SEPARATOR = 0xff;
|
|
10
|
+
const utf8 = new TextEncoder();
|
|
11
|
+
function feed(hash, bytes) {
|
|
12
|
+
for (const byte of bytes) {
|
|
13
|
+
hash = Math.imul(hash ^ byte, 3432918353);
|
|
14
|
+
hash = (hash << 13) | (hash >>> 19);
|
|
15
|
+
}
|
|
16
|
+
return hash;
|
|
17
|
+
}
|
|
18
|
+
/** murmur3 fmix32: makes one changed letter produce an unrelated result. */
|
|
19
|
+
function finalize(hash) {
|
|
20
|
+
hash = Math.imul(hash ^ (hash >>> 16), 2246822507);
|
|
21
|
+
hash = Math.imul(hash ^ (hash >>> 13), 3266489909);
|
|
22
|
+
return (hash ^ (hash >>> 16)) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
/** Names a person would call equal must hash equal. */
|
|
25
|
+
function normalize(name) {
|
|
26
|
+
return name.normalize("NFC").trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
const COLOR_PREFIX = "color:";
|
|
29
|
+
export const colorKey = (key) => `${COLOR_PREFIX}${key}`;
|
|
30
|
+
export function createTraits(name, overrides = {}) {
|
|
31
|
+
const seed = normalize(name);
|
|
32
|
+
const state = feed(1779033703 ^ seed.length, utf8.encode(seed));
|
|
33
|
+
// Overrides can come from a link or an API call, so anything that isn't the type
|
|
34
|
+
// a generator expects is treated as unset rather than reaching the markup.
|
|
35
|
+
const fixed = (key) => {
|
|
36
|
+
const value = overrides[key];
|
|
37
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : undefined;
|
|
38
|
+
};
|
|
39
|
+
const t = ((key) => fixed(key) ?? finalize(feed(feed(state, Uint8Array.of(SEPARATOR)), utf8.encode(key))) / 2 ** 32);
|
|
40
|
+
// A slider at its far end fixes exactly 1, which must still land on the last option.
|
|
41
|
+
const index = (key, count) => Math.min(count - 1, Math.floor(t(key) * count));
|
|
42
|
+
t.num = (key, min, max) => min + t(key) * (max - min);
|
|
43
|
+
t.int = (key, min, max) => min + index(key, max - min + 1);
|
|
44
|
+
t.jitter = (key, amount) => (t(key) * 2 - 1) * amount;
|
|
45
|
+
t.pick = (key, options) => options[index(key, options.length)];
|
|
46
|
+
t.forced = fixed;
|
|
47
|
+
t.color = (key) => {
|
|
48
|
+
const picked = overrides[colorKey(key)];
|
|
49
|
+
return Number.isInteger(picked) && picked >= 0 && picked <= 0xffffff
|
|
50
|
+
? intToHex(picked)
|
|
51
|
+
: undefined;
|
|
52
|
+
};
|
|
53
|
+
t.text = (key) => {
|
|
54
|
+
const words = overrides[key];
|
|
55
|
+
return typeof words === "string" ? words : undefined;
|
|
56
|
+
};
|
|
57
|
+
return t;
|
|
58
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface Colors {
|
|
2
|
+
body: string;
|
|
3
|
+
accent: string;
|
|
4
|
+
visor: string;
|
|
5
|
+
light: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A part as numbers, in the 100×100 viewBox, so the 3D shader can draw its outline exactly
|
|
9
|
+
* rather than from a picture of it. Degrees for `rot`, clockwise. A superellipse with a
|
|
10
|
+
* `stroke` is only its outline, that thick either side. A curve is a quadratic Bézier
|
|
11
|
+
* stroke with round ends, through `cx` `cy` as its control point.
|
|
12
|
+
*/
|
|
13
|
+
export type Shape = {
|
|
14
|
+
kind: "superellipse";
|
|
15
|
+
cx: number;
|
|
16
|
+
cy: number;
|
|
17
|
+
rx: number;
|
|
18
|
+
ry: number;
|
|
19
|
+
n: number;
|
|
20
|
+
rot: number;
|
|
21
|
+
stroke?: number;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "crescent";
|
|
24
|
+
cx: number;
|
|
25
|
+
cy: number;
|
|
26
|
+
width: number;
|
|
27
|
+
lift: number;
|
|
28
|
+
thickness: number;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "rod";
|
|
31
|
+
x1: number;
|
|
32
|
+
y1: number;
|
|
33
|
+
x2: number;
|
|
34
|
+
y2: number;
|
|
35
|
+
r: number;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "circle";
|
|
38
|
+
cx: number;
|
|
39
|
+
cy: number;
|
|
40
|
+
r: number;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "curve";
|
|
43
|
+
x1: number;
|
|
44
|
+
y1: number;
|
|
45
|
+
cx: number;
|
|
46
|
+
cy: number;
|
|
47
|
+
x2: number;
|
|
48
|
+
y2: number;
|
|
49
|
+
r: number;
|
|
50
|
+
};
|
|
51
|
+
/** An avatar as layers of paths, which is everything the 3D renderer needs. */
|
|
52
|
+
export interface AvatarParts {
|
|
53
|
+
colors: Colors;
|
|
54
|
+
head: string[];
|
|
55
|
+
accents: string[];
|
|
56
|
+
visor: string[];
|
|
57
|
+
/** The lights that blink, as paths for still pictures and as shapes for 3D. */
|
|
58
|
+
eyes: string[];
|
|
59
|
+
eyeShapes: Shape[];
|
|
60
|
+
/** Any other lights, such as a mouth, which stay put through a blink. */
|
|
61
|
+
lights: string[];
|
|
62
|
+
lightShapes: Shape[];
|
|
63
|
+
/** The dark glass, as shapes for 3D. */
|
|
64
|
+
visorShapes: Shape[];
|
|
65
|
+
/** Polished metal, raised slightly from the skin. */
|
|
66
|
+
chrome: string[];
|
|
67
|
+
chromeShapes: Shape[];
|
|
68
|
+
/** Small dark domes. */
|
|
69
|
+
studs: string[];
|
|
70
|
+
studShapes: Shape[];
|
|
71
|
+
/** How thickly the paint is flecked, from 0 to 1. */
|
|
72
|
+
speckle: number;
|
|
73
|
+
/** The height of the eyes, which a blink closes towards. */
|
|
74
|
+
eyeY: number;
|
|
75
|
+
/** A card behind the avatar, which it stays inside: its side as a share of the slot's. */
|
|
76
|
+
container?: {
|
|
77
|
+
size: number;
|
|
78
|
+
color: string;
|
|
79
|
+
};
|
|
80
|
+
/** The lights are eyes painted on the skin, rather than glowing behind a visor. */
|
|
81
|
+
painted?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* A frosted shell lit from inside, with the accent as a pearly topper, rather than paint:
|
|
84
|
+
* how milky the shell is, from 0 (the body's colour) to 1 (white), and the glow's size.
|
|
85
|
+
*/
|
|
86
|
+
jelly?: {
|
|
87
|
+
frost: number;
|
|
88
|
+
pool: number;
|
|
89
|
+
};
|
|
90
|
+
/** Soft-touch clay: pastel and matte, with no gloss. */
|
|
91
|
+
matte?: boolean;
|
|
92
|
+
/** Clay pieces pressed onto the front of the body, such as hands, which stand proud of it. */
|
|
93
|
+
raised?: string[];
|
|
94
|
+
/** On clay, white glints that blink and look with the eyes, and marks in the visor's colour. */
|
|
95
|
+
shineShapes?: Shape[];
|
|
96
|
+
tintShapes?: Shape[];
|
|
97
|
+
/** On clay, the whites of the eyes, under the eyes themselves, which blink but don't look about. */
|
|
98
|
+
whiteShapes?: Shape[];
|
|
99
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Other takes on the selected avatar, shown in a strip beneath it. */
|
|
2
|
+
import type { AvatarKind } from "./kinds.ts";
|
|
3
|
+
import type { Overrides } from "./traits.ts";
|
|
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;
|
|
7
|
+
/** The name a variant is generated from. The first is the name itself. */
|
|
8
|
+
export declare const variantName: (name: string, variant: number) => string;
|
|
9
|
+
/**
|
|
10
|
+
* The overrides to draw a variant with. Anything set by hand wins over its style. The
|
|
11
|
+
* result is remembered, so the same variant is the same object and memoised work holds.
|
|
12
|
+
*/
|
|
13
|
+
export declare function variantOverrides(kind: AvatarKind, name: string, variant: number, overrides: Overrides): Overrides;
|
package/dist/variants.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Other takes on the selected avatar, shown in a strip beneath it. */
|
|
2
|
+
import { patched } from "./history.js";
|
|
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;
|
|
6
|
+
/** Enough for a few names' worth of variants before a typing session starts over. */
|
|
7
|
+
const MAX_REMEMBERED = 256;
|
|
8
|
+
/** The name a variant is generated from. The first is the name itself. */
|
|
9
|
+
export const variantName = (name, variant) => (variant === 0 ? name : `${name}#${variant + 1}`);
|
|
10
|
+
/**
|
|
11
|
+
* Steps along the type's main styles from the name's own, so no two variants share one.
|
|
12
|
+
* The controls are read with the visitor's settings, as those can narrow what's on offer.
|
|
13
|
+
*/
|
|
14
|
+
function variantStyle(kind, name, variant, overrides) {
|
|
15
|
+
const controls = kind.controls(name, overrides).flatMap((section) => section.controls);
|
|
16
|
+
return kind.styleKeys.reduce((style, key) => {
|
|
17
|
+
const control = controls.find(({ id }) => id === key);
|
|
18
|
+
if (control?.type !== "choice")
|
|
19
|
+
return style;
|
|
20
|
+
const own = control.options.findIndex((option) => option.id === control.selected);
|
|
21
|
+
return patched(style, control.options[(own + variant) % control.options.length].set);
|
|
22
|
+
}, {});
|
|
23
|
+
}
|
|
24
|
+
const remembered = new WeakMap();
|
|
25
|
+
/**
|
|
26
|
+
* The overrides to draw a variant with. Anything set by hand wins over its style. The
|
|
27
|
+
* result is remembered, so the same variant is the same object and memoised work holds.
|
|
28
|
+
*/
|
|
29
|
+
export function variantOverrides(kind, name, variant, overrides) {
|
|
30
|
+
if (variant === 0)
|
|
31
|
+
return overrides;
|
|
32
|
+
const seen = remembered.get(overrides) ?? new Map();
|
|
33
|
+
remembered.set(overrides, seen);
|
|
34
|
+
if (seen.size >= MAX_REMEMBERED)
|
|
35
|
+
seen.clear();
|
|
36
|
+
const key = `${kind.id}|${name}|${variant}`;
|
|
37
|
+
const known = seen.get(key);
|
|
38
|
+
if (known)
|
|
39
|
+
return known;
|
|
40
|
+
const result = { ...variantStyle(kind, name, variant, overrides), ...overrides };
|
|
41
|
+
seen.set(key, result);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
package/mcp/server.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { createServer } from "./tools.js";
|
|
5
|
+
|
|
6
|
+
const server = createServer(readFileSync(new URL("../GUIDE.md", import.meta.url), "utf8"));
|
|
7
|
+
await server.connect(new StdioServerTransport());
|
package/mcp/tools.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { createAvatar, styles } from "../dist/index.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const INSTRUCTIONS = `OriginKit Avatars turns a name into an avatar: the same name and style always give the same SVG. There are ${styles.length} styles.
|
|
6
|
+
- list_styles: the style ids and labels.
|
|
7
|
+
- create_avatar: style and name in, SVG out. Use it to see an avatar or to write one file. It is not how an app should draw avatars at runtime.
|
|
8
|
+
- usage_guide (or the resource avatars://guide): how to add avatars to a codebase through the CLI, the npm package or MCP.
|
|
9
|
+
Call usage_guide before adding avatars to a codebase.`;
|
|
10
|
+
|
|
11
|
+
// The tools, shared by the stdio server and the hosted /mcp endpoint. `markdown` is GUIDE.md: each caller loads it its own way.
|
|
12
|
+
export function createServer(markdown) {
|
|
13
|
+
// One guide, served as a resource and as a tool. The styles come from the package, so they can't go stale.
|
|
14
|
+
const guide = () =>
|
|
15
|
+
`${markdown}\n## Styles\n\n${styles.map((each) => `- \`${each.id}\`: ${each.label}`).join("\n")}\n`;
|
|
16
|
+
|
|
17
|
+
const server = new McpServer({ name: "avatars", version: "0.1.0" }, { instructions: INSTRUCTIONS });
|
|
18
|
+
|
|
19
|
+
server.registerResource(
|
|
20
|
+
"guide",
|
|
21
|
+
"avatars://guide",
|
|
22
|
+
{ description: "How to add OriginKit Avatars to a project", mimeType: "text/markdown" },
|
|
23
|
+
async (uri) => ({ contents: [{ uri: uri.href, mimeType: "text/markdown", text: guide() }] }),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
server.registerTool(
|
|
27
|
+
"usage_guide",
|
|
28
|
+
{
|
|
29
|
+
description:
|
|
30
|
+
"How to use OriginKit Avatars in your own project: which surface to pick, recipes, and the styles. Read it before adding avatars to a codebase.",
|
|
31
|
+
},
|
|
32
|
+
async () => ({ content: [{ type: "text", text: guide() }] }),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
server.registerTool(
|
|
36
|
+
"list_styles",
|
|
37
|
+
{ description: "List the avatar styles, as ids to pass to create_avatar and their labels." },
|
|
38
|
+
async () => ({ content: [{ type: "text", text: JSON.stringify(styles) }] }),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
server.registerTool(
|
|
42
|
+
"create_avatar",
|
|
43
|
+
{
|
|
44
|
+
description: "Turn a name into an SVG avatar. The same name and style always give the same avatar.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
style: z.enum(styles.map((each) => each.id)).describe("A style id from list_styles"),
|
|
47
|
+
name: z.string().min(1).max(100).describe("The name to generate the avatar from"),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
async ({ style, name }) => {
|
|
51
|
+
try {
|
|
52
|
+
const svg = createAvatar(style, name);
|
|
53
|
+
const uri = `avatars://${style}/${encodeURIComponent(name)}.svg`;
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{ type: "text", text: svg },
|
|
57
|
+
{ type: "resource", resource: { uri, mimeType: "image/svg+xml", text: svg } },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
isError: true,
|
|
63
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return server;
|
|
70
|
+
}
|