@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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glyphs: a solid silhouette with two eyes, on a rounded tile. Smooth shapes are curves
|
|
3
|
+
* between tips around the centre, and pixel shapes are rows of cells. The art is markup
|
|
4
|
+
* rather than colours, so a tap can move the silhouette and leave the eyes.
|
|
5
|
+
*/
|
|
6
|
+
import { BLACK, inkOn, WHITE } from "../colors.js";
|
|
7
|
+
import { bandAt } from "../controls.js";
|
|
8
|
+
import { calmed } from "../possibilities.js";
|
|
9
|
+
import { createTraits } from "../traits.js";
|
|
10
|
+
import { CALMING } from "./calming.js";
|
|
11
|
+
export const TILES = [
|
|
12
|
+
"#ff2626", "#ffe45e", "#2bd467", "#2f87f2", "#b35cf6", "#ff8a1f",
|
|
13
|
+
"#ff5aa5", "#14c8bd", "#6a5cff", "#9be22d", "#f5efe0",
|
|
14
|
+
];
|
|
15
|
+
/** The tiles behind a bright ink. */
|
|
16
|
+
export const NIGHTS = ["#2a0f55", "#0d2a5e", "#4f1030", "#083f37", "#23252f"];
|
|
17
|
+
/** Most glyphs are black or a bright ink, with the other dark inks as the rare ones. */
|
|
18
|
+
export const FILLS = [
|
|
19
|
+
{ color: BLACK, weight: 6 },
|
|
20
|
+
{ color: "#1c1b4b", weight: 0.5 },
|
|
21
|
+
{ color: "#3d1f0f", weight: 0.5 },
|
|
22
|
+
{ color: "#0f3d2e", weight: 0.5 },
|
|
23
|
+
{ color: WHITE, weight: 0.5 },
|
|
24
|
+
...["#ffd23d", "#ff5a5f", "#ff5ad6", "#38f0c8", "#36b6ff", "#b6ff3d"].map((color) => ({ color, weight: 1, bright: true })),
|
|
25
|
+
];
|
|
26
|
+
export const EFFECTS = ["spin", "pop", "drop", "hop"];
|
|
27
|
+
const CELL = 5;
|
|
28
|
+
/** How far the silhouette's tips are rounded off. Pixel shapes stay square. */
|
|
29
|
+
const SOFTEN = 4;
|
|
30
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
31
|
+
function polar(degrees, radius) {
|
|
32
|
+
const radians = (degrees * Math.PI) / 180;
|
|
33
|
+
return `${round2(50 + radius * Math.cos(radians))} ${round2(50 + radius * Math.sin(radians))}`;
|
|
34
|
+
}
|
|
35
|
+
/** Curves from tip to tip around the centre, each bending towards `waist` on the way. */
|
|
36
|
+
function radial(tips, radius, waist, rotation = -90) {
|
|
37
|
+
const step = 360 / tips;
|
|
38
|
+
const curves = Array.from({ length: tips }, (_, i) => {
|
|
39
|
+
const next = (i + 1) % tips;
|
|
40
|
+
return `Q${polar(rotation + (i + 0.5) * step, waist)} ${polar(rotation + next * step, radius(next))}`;
|
|
41
|
+
});
|
|
42
|
+
return `M${polar(rotation, radius(0))}${curves.join("")}Z`;
|
|
43
|
+
}
|
|
44
|
+
/** True circular lobes between points on a ring, which is what makes a cloud a cloud. */
|
|
45
|
+
function lobes(count, ring, radius) {
|
|
46
|
+
const step = 360 / count;
|
|
47
|
+
const arcs = Array.from({ length: count }, (_, i) => {
|
|
48
|
+
const r = round2(radius(i));
|
|
49
|
+
return `A${r} ${r} 0 1 1 ${polar(-90 + (i + 1) * step, ring)}`;
|
|
50
|
+
});
|
|
51
|
+
return `M${polar(-90, ring)}${arcs.join("")}Z`;
|
|
52
|
+
}
|
|
53
|
+
/** Rows of cells around the centre, from each row's half width. */
|
|
54
|
+
function stepped(rows) {
|
|
55
|
+
const top = 50 - (rows.length * CELL) / 2;
|
|
56
|
+
return rows
|
|
57
|
+
.map((half, i) => `M${50 - half * CELL} ${top + i * CELL}h${half * 2 * CELL}v${CELL}h${-half * 2 * CELL}z`)
|
|
58
|
+
.join("");
|
|
59
|
+
}
|
|
60
|
+
export const SHAPES = [
|
|
61
|
+
{
|
|
62
|
+
name: "Sparkle",
|
|
63
|
+
pixel: false,
|
|
64
|
+
effect: "pop",
|
|
65
|
+
count: { key: "sparkle.n", label: "Points", min: 4, max: 6 },
|
|
66
|
+
draw: (t, count) => radial(count, () => 33, t.num("waist", 8, 13)),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "Wedge",
|
|
70
|
+
pixel: false,
|
|
71
|
+
effect: "drop",
|
|
72
|
+
eyeY: -8,
|
|
73
|
+
draw: (t) => radial(3, () => 31, t.num("waist", 12, 16), 90),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "Star",
|
|
77
|
+
pixel: false,
|
|
78
|
+
effect: "pop",
|
|
79
|
+
draw: (t) => radial(5, () => 31, t.num("waist", 12, 17)),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "Burst",
|
|
83
|
+
pixel: false,
|
|
84
|
+
effect: "spin",
|
|
85
|
+
count: { key: "burst.n", label: "Spikes", min: 12, max: 17 },
|
|
86
|
+
draw: (t, count) => radial(count, (i) => 31 + t.jitter(`burst.r${i}`, 3), t.num("waist", 17, 21)),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "Flower",
|
|
90
|
+
pixel: false,
|
|
91
|
+
effect: "spin",
|
|
92
|
+
count: { key: "flower.n", label: "Petals", min: 5, max: 7 },
|
|
93
|
+
draw: (t, count) => radial(count, () => 17, t.num("waist", 38, 45)),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "Cloud",
|
|
97
|
+
pixel: false,
|
|
98
|
+
effect: "drop",
|
|
99
|
+
count: { key: "cloud.n", label: "Lobes", min: 5, max: 6 },
|
|
100
|
+
draw: (t, count) => lobes(count, 13, (i) => 11.5 + t.num(`cloud.r${i}`, 0, 2.5) + t.num("waist", 0, 1)),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "Diamond",
|
|
104
|
+
pixel: true,
|
|
105
|
+
effect: "hop",
|
|
106
|
+
draw: (t) => {
|
|
107
|
+
const reach = t.int("waist", 5, 6);
|
|
108
|
+
return stepped(Array.from({ length: reach * 2 + 1 }, (_, i) => reach + 1 - Math.abs(i - reach)));
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "Chip",
|
|
113
|
+
pixel: true,
|
|
114
|
+
effect: "hop",
|
|
115
|
+
draw: (t) => {
|
|
116
|
+
const half = t.int("waist", 6, 7);
|
|
117
|
+
return stepped([half - 2, half - 1, half - 1, half, half, half - 1, half - 1, half - 2]);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "Cross",
|
|
122
|
+
pixel: true,
|
|
123
|
+
effect: "hop",
|
|
124
|
+
draw: (t) => {
|
|
125
|
+
const arm = t.int("waist", 2, 3);
|
|
126
|
+
const reach = arm + 4;
|
|
127
|
+
return stepped(Array.from({ length: reach * 2 }, (_, i) => {
|
|
128
|
+
const fromEnd = Math.min(i, reach * 2 - 1 - i);
|
|
129
|
+
return fromEnd < 3 ? arm : fromEnd === 3 ? reach - 1 : reach;
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
export const EYE_KINDS = ["Oval", "Square", "Pill"];
|
|
135
|
+
export const shapeOf = (t) => t.pick("shape", SHAPES);
|
|
136
|
+
/** What the name, or the panel, has filled the glyph with. */
|
|
137
|
+
export function fillOf(t) {
|
|
138
|
+
const paint = FILLS[bandAt(FILLS, t("fill"))];
|
|
139
|
+
const picked = t.color("fill");
|
|
140
|
+
return { bright: !picked && !!paint.bright, color: picked ?? paint.color };
|
|
141
|
+
}
|
|
142
|
+
/** The tiles that suit the fill, so a name can never end up black on black. */
|
|
143
|
+
export const tilesFor = (bright) => (bright ? NIGHTS : TILES);
|
|
144
|
+
/** Follows the rest of the glyph: pixel shapes get square eyes, and bright inks pills. */
|
|
145
|
+
export function eyeKindOf(t, shape) {
|
|
146
|
+
const forced = t.forced("eyes.kind");
|
|
147
|
+
if (forced !== undefined)
|
|
148
|
+
return EYE_KINDS[Math.floor(forced * EYE_KINDS.length)];
|
|
149
|
+
return shape.pixel ? "Square" : fillOf(t).bright ? "Pill" : "Oval";
|
|
150
|
+
}
|
|
151
|
+
function drawEyes(t, shape, paint) {
|
|
152
|
+
const kind = eyeKindOf(t, shape);
|
|
153
|
+
const scale = t.num("eye.size", 0.85, 1.25);
|
|
154
|
+
const gap = t.num("eye.gap", 5.5, 8.5);
|
|
155
|
+
const y = 50 + (shape.eyeY ?? 0);
|
|
156
|
+
const [width, height] = (kind === "Pill" ? [6.5, 13] : [4, 7]).map((v) => round2(v * scale));
|
|
157
|
+
return [-gap, gap]
|
|
158
|
+
.map((offset) => {
|
|
159
|
+
const x = round2(50 + offset);
|
|
160
|
+
if (kind === "Oval")
|
|
161
|
+
return `<ellipse cx="${x}" cy="${y}" rx="${round2(3.3 * scale)}" ry="${round2(4.8 * scale)}" fill="${paint}"/>`;
|
|
162
|
+
const corner = kind === "Pill" ? ` rx="${width / 2}"` : "";
|
|
163
|
+
return `<rect x="${round2(x - width / 2)}" y="${round2(y - height / 2)}" width="${width}" height="${height}"${corner} fill="${paint}"/>`;
|
|
164
|
+
})
|
|
165
|
+
.join("");
|
|
166
|
+
}
|
|
167
|
+
export const TILE_SHAPE = `<rect x="5" y="5" width="90" height="90" rx="20"/>`;
|
|
168
|
+
/** Frames the tile exactly, so it sits the same size as the other flat avatars. */
|
|
169
|
+
export const VIEW_BOX = "5 5 90 90";
|
|
170
|
+
export function createGlyph(name, overrides) {
|
|
171
|
+
const t = calmed(createTraits(name, overrides), CALMING);
|
|
172
|
+
const shape = shapeOf(t);
|
|
173
|
+
const { bright, color: paint } = fillOf(t);
|
|
174
|
+
const tile = t.color("tile") ?? t.pick("tile", tilesFor(bright));
|
|
175
|
+
const count = shape.count ? t.int(shape.count.key, shape.count.min, shape.count.max) : 0;
|
|
176
|
+
const size = round2(t.num("body.size", 0.92, 1.08));
|
|
177
|
+
const tilt = round2(t.jitter("tilt", 12));
|
|
178
|
+
// Pixel shapes stay on the grid: turned or scaled, their steps would stop being cells.
|
|
179
|
+
const finish = shape.pixel
|
|
180
|
+
? `shape-rendering="crispEdges"`
|
|
181
|
+
: `transform="translate(50 50) rotate(${tilt}) scale(${size}) translate(-50 -50)" stroke="${paint}" stroke-width="${SOFTEN}" stroke-linejoin="round"`;
|
|
182
|
+
return {
|
|
183
|
+
tile,
|
|
184
|
+
body: `<path d="${shape.draw(t, count)}" fill="${paint}" ${finish}/>`,
|
|
185
|
+
// On a bright ink the eyes are the tile showing through, as if cut out of the silhouette.
|
|
186
|
+
eyes: drawEyes(t, shape, bright ? tile : inkOn(paint)),
|
|
187
|
+
pixel: shape.pixel,
|
|
188
|
+
effect: shape.effect,
|
|
189
|
+
blinkDelay: t.num("blink", 0, 4),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** The glyph at rest, for thumbnails. */
|
|
193
|
+
export const glyphSvg = (name, overrides) => {
|
|
194
|
+
const glyph = createGlyph(name, overrides);
|
|
195
|
+
const tile = TILE_SHAPE.replace("/>", ` fill="${glyph.tile}"/>`);
|
|
196
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${VIEW_BOX}">${tile}${glyph.body}${glyph.eyes}</svg>`;
|
|
197
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Possibilities calms on a grin. Low keeps eight of the plainer eyes and mouths, one for
|
|
3
|
+
* each variant, and draws the eyes' size, gap and height and the mouth's size and height towards
|
|
4
|
+
* their middle. Indices are into `PAIRS` and `MOUTHS`, plainest first.
|
|
5
|
+
*/
|
|
6
|
+
import type { Calming } from "../possibilities.ts";
|
|
7
|
+
export declare const CALMING: Calming;
|
|
8
|
+
export declare const NOTES: [string, string, string];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Possibilities calms on a grin. Low keeps eight of the plainer eyes and mouths, one for
|
|
3
|
+
* each variant, and draws the eyes' size, gap and height and the mouth's size and height towards
|
|
4
|
+
* their middle. Indices are into `PAIRS` and `MOUTHS`, plainest first.
|
|
5
|
+
*/
|
|
6
|
+
export const CALMING = {
|
|
7
|
+
features: /^eye\.(size|gap|y)$|^mouth\.(size|y)$|^(left|right)\.(x|y)$/,
|
|
8
|
+
lists: {
|
|
9
|
+
// Round, Wink, Round & block, Block & round, Bubbles, Goggles, Lids, Other wink, then Round &
|
|
10
|
+
// lid, Blocks, Big & small and Googly.
|
|
11
|
+
eyes: [12, 8, [0, 4, 2, 5, 3, 1, 6, 9, 10, 8, 7, 11]],
|
|
12
|
+
// Grin, Gap, Front gap, Middle gap, Back left, Back right, Wide, Small, then Two gaps, Snaggle,
|
|
13
|
+
// and the chipped ones.
|
|
14
|
+
mouth: [12, 8, [0, 1, 4, 3, 2, 5, 6, 8, 7, 9, 10, 11]],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export const NOTES = [
|
|
18
|
+
"round and matching eyes over a full grin, a tooth missing now and then.",
|
|
19
|
+
"more eyes and gaps, and the eyes and grin a little bigger or smaller.",
|
|
20
|
+
"every pair of eyes, googly ones too, and every gap, at any size.",
|
|
21
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { choice, palette, slider } from "../controls.js";
|
|
2
|
+
import { narrowed, possibilitiesSlider } from "../possibilities.js";
|
|
3
|
+
import { CALMING, NOTES } from "./calming.js";
|
|
4
|
+
import { COLOURS, grinTraits, MOUTHS, PAIRS, roundnessOf } from "./index.js";
|
|
5
|
+
export const controls = (name, overrides) => {
|
|
6
|
+
const t = grinTraits(name, overrides);
|
|
7
|
+
const list = (control) => narrowed(control, t, CALMING);
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
title: "Face",
|
|
11
|
+
controls: [
|
|
12
|
+
list(choice(t, "eyes", "Eyes", "list", PAIRS.map(({ name: label }) => ({ label })))),
|
|
13
|
+
list(choice(t, "mouth", "Grin", "list", MOUTHS.map(({ name: label }) => ({ label })))),
|
|
14
|
+
possibilitiesSlider(t, NOTES),
|
|
15
|
+
// A tile until moved, and the reference's circle at the far end.
|
|
16
|
+
{
|
|
17
|
+
type: "slider",
|
|
18
|
+
id: "roundness",
|
|
19
|
+
label: "Roundness",
|
|
20
|
+
value: roundnessOf(t),
|
|
21
|
+
display: `${Math.round(roundnessOf(t) * 100)}%`,
|
|
22
|
+
range: [0, 100],
|
|
23
|
+
ends: ["Tile", "Circle"],
|
|
24
|
+
},
|
|
25
|
+
slider(t, "eye.size", "Eye size", 0, 100, "%"),
|
|
26
|
+
slider(t, "eye.gap", "Eye gap", 0, 100, "%"),
|
|
27
|
+
slider(t, "mouth.size", "Grin size", 0, 100, "%"),
|
|
28
|
+
choice(t, "teeth", "Teeth", "list", ["4", "5", "6"].map((label) => ({ label }))),
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{ title: "Colour", controls: [palette(t, "body", "Colour", COLOURS)] },
|
|
32
|
+
];
|
|
33
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grins: a bold round button of a face, after the reference. White eyes with black dot pupils
|
|
3
|
+
* that each look their own way, some of them one joined shape, and a big white grin split into
|
|
4
|
+
* teeth by thin lines of the face's colour, now and then with a tooth or a corner missing in
|
|
5
|
+
* black. It's a rounded tile until Roundness takes it towards the reference's full circle.
|
|
6
|
+
*
|
|
7
|
+
* Everything is drawn from numbers in the 100×100 frame, so the live avatar can move its
|
|
8
|
+
* pupils, blink and chomp, and the thumbnail is the very same picture at rest.
|
|
9
|
+
*/
|
|
10
|
+
import { type Overrides, type Traits } from "../traits.ts";
|
|
11
|
+
/** The reference's colours first, then more as bold. Neighbours are unlike. */
|
|
12
|
+
export declare const COLOURS: string[];
|
|
13
|
+
/** How the pupils sit in an eye, as shares of its reach: their own look before the pointer's. */
|
|
14
|
+
type Look = [number, number];
|
|
15
|
+
type Eye = {
|
|
16
|
+
kind: "round";
|
|
17
|
+
size: number;
|
|
18
|
+
look: Look;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "block";
|
|
21
|
+
look: Look;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "lid";
|
|
24
|
+
look: Look;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "wink";
|
|
27
|
+
};
|
|
28
|
+
type Pair = {
|
|
29
|
+
name: string;
|
|
30
|
+
left: Eye;
|
|
31
|
+
right: Eye;
|
|
32
|
+
}
|
|
33
|
+
/** One white shape for both eyes: flat along the top with a round lobe for each, or two bubbles run together. */
|
|
34
|
+
| {
|
|
35
|
+
name: string;
|
|
36
|
+
joined: "goggles" | "bubbles";
|
|
37
|
+
look: Look;
|
|
38
|
+
};
|
|
39
|
+
/** The reference's six first, mismatched pairs mirrored as it has them. Neighbours are unlike. */
|
|
40
|
+
export declare const PAIRS: Pair[];
|
|
41
|
+
type Tooth = "second" | "middle" | "fourth";
|
|
42
|
+
/** A mouth: its size as shares of the usual, and which teeth or corner are missing, if any. */
|
|
43
|
+
interface Mouth {
|
|
44
|
+
name: string;
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
/** Missing teeth, each gone entirely: the second from the left, the middle one, or the second from the right. */
|
|
48
|
+
gaps: Tooth[];
|
|
49
|
+
/** The tooth at the back at that end gone entirely, from its line out to the corner of the mouth. */
|
|
50
|
+
corner?: -1 | 1;
|
|
51
|
+
/** Teeth broken partway, their lower part gone below a rough, jagged break. */
|
|
52
|
+
chips?: Tooth[];
|
|
53
|
+
}
|
|
54
|
+
/** The reference's gaps first. Neighbours are unlike. */
|
|
55
|
+
export declare const MOUTHS: Mouth[];
|
|
56
|
+
export declare const grinTraits: (name: string, overrides?: Overrides) => Traits;
|
|
57
|
+
export declare const pairOf: (t: Traits) => Pair;
|
|
58
|
+
export declare const mouthOf: (t: Traits) => Mouth;
|
|
59
|
+
export declare function coloursOf(t: Traits): {
|
|
60
|
+
body: string;
|
|
61
|
+
};
|
|
62
|
+
/** How round the button is, from a tile at 0, which it is until set, to the reference's circle at 1. */
|
|
63
|
+
export declare const roundnessOf: (t: Traits) => number;
|
|
64
|
+
export interface Grin {
|
|
65
|
+
body: string;
|
|
66
|
+
/** Its corners' radius. */
|
|
67
|
+
corner: number;
|
|
68
|
+
eyes: string[];
|
|
69
|
+
mouth: string;
|
|
70
|
+
blink: {
|
|
71
|
+
period: number;
|
|
72
|
+
delay: number;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** `id` keeps each grin's clips its own where many share a page. */
|
|
76
|
+
export declare function createGrin(name: string, overrides: Overrides | undefined, id?: string): Grin;
|
|
77
|
+
/** The button, flat, from a rounded tile to a circle. */
|
|
78
|
+
export declare const buttonMarkup: (body: string, corner: number) => string;
|
|
79
|
+
/** The grin at rest, for thumbnails. */
|
|
80
|
+
export declare function grinSvg(name: string, overrides?: Overrides): string;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grins: a bold round button of a face, after the reference. White eyes with black dot pupils
|
|
3
|
+
* that each look their own way, some of them one joined shape, and a big white grin split into
|
|
4
|
+
* teeth by thin lines of the face's colour, now and then with a tooth or a corner missing in
|
|
5
|
+
* black. It's a rounded tile until Roundness takes it towards the reference's full circle.
|
|
6
|
+
*
|
|
7
|
+
* Everything is drawn from numbers in the 100×100 frame, so the live avatar can move its
|
|
8
|
+
* pupils, blink and chomp, and the thumbnail is the very same picture at rest.
|
|
9
|
+
*/
|
|
10
|
+
import { calmed } from "../possibilities.js";
|
|
11
|
+
import { createTraits } from "../traits.js";
|
|
12
|
+
import { CALMING } from "./calming.js";
|
|
13
|
+
const r2 = (value) => Math.round(value * 100) / 100;
|
|
14
|
+
/** The reference's colours first, then more as bold. Neighbours are unlike. */
|
|
15
|
+
export const COLOURS = ["#8a49bd", "#f76b4f", "#425fe6", "#f76cc0", "#f7cf2a", "#5ccd73", "#20b8c9", "#ff9a2e", "#e94158", "#7b5cf0"];
|
|
16
|
+
const INK = "#111114";
|
|
17
|
+
const WHITE = "#ffffff";
|
|
18
|
+
const round = (size = 1, look = [0, 0]) => ({ kind: "round", size, look });
|
|
19
|
+
/** The reference's six first, mismatched pairs mirrored as it has them. Neighbours are unlike. */
|
|
20
|
+
export const PAIRS = [
|
|
21
|
+
{ name: "Round", left: round(1, [0.1, 0]), right: round(1, [-0.1, 0]) },
|
|
22
|
+
{ name: "Goggles", joined: "goggles", look: [0, -1] },
|
|
23
|
+
{ name: "Round & block", left: round(1.05, [-0.6, 0.35]), right: { kind: "block", look: [-0.55, -0.1] } },
|
|
24
|
+
{ name: "Bubbles", joined: "bubbles", look: [0, -0.7] },
|
|
25
|
+
{ name: "Wink", left: round(1, [0, 0]), right: { kind: "wink" } },
|
|
26
|
+
{ name: "Block & round", left: { kind: "block", look: [0.35, 0.2] }, right: round(1.05, [0.55, -0.4]) },
|
|
27
|
+
{ name: "Lids", left: { kind: "lid", look: [0, -1] }, right: { kind: "lid", look: [0, -1] } },
|
|
28
|
+
{ name: "Big & small", left: round(1.2, [0.4, 0.2]), right: round(0.72, [-0.3, -0.3]) },
|
|
29
|
+
{ name: "Blocks", left: { kind: "block", look: [0.5, 0] }, right: { kind: "block", look: [0.5, 0] } },
|
|
30
|
+
{ name: "Other wink", left: { kind: "wink" }, right: round(1, [-0.2, 0.2]) },
|
|
31
|
+
{ name: "Round & lid", left: round(1, [0.3, -0.3]), right: { kind: "lid", look: [-0.3, -1] } },
|
|
32
|
+
{ name: "Googly", left: round(1.05, [-0.7, -0.5]), right: round(1.05, [0.7, 0.55]) },
|
|
33
|
+
];
|
|
34
|
+
/** The reference's gaps first. Neighbours are unlike. */
|
|
35
|
+
export const MOUTHS = [
|
|
36
|
+
{ name: "Grin", width: 1, height: 1, gaps: [] },
|
|
37
|
+
{ name: "Gap", width: 1, height: 1, gaps: ["fourth"] },
|
|
38
|
+
{ name: "Back left", width: 1, height: 1.04, gaps: [], corner: -1 },
|
|
39
|
+
{ name: "Middle gap", width: 0.96, height: 1.05, gaps: ["middle"] },
|
|
40
|
+
{ name: "Front gap", width: 1, height: 1, gaps: ["second"] },
|
|
41
|
+
{ name: "Back right", width: 1.04, height: 1, gaps: [], corner: 1 },
|
|
42
|
+
{ name: "Wide", width: 1.16, height: 0.9, gaps: [] },
|
|
43
|
+
{ name: "Two gaps", width: 1.04, height: 1.04, gaps: ["second", "fourth"] },
|
|
44
|
+
{ name: "Small", width: 0.78, height: 0.86, gaps: ["middle"] },
|
|
45
|
+
{ name: "Snaggle", width: 1, height: 1.06, gaps: ["second"], corner: 1 },
|
|
46
|
+
{ name: "Chipped", width: 1, height: 1.02, gaps: [], chips: ["second"] },
|
|
47
|
+
{ name: "Chipped back", width: 1.02, height: 1, gaps: [], chips: ["fourth"], corner: -1 },
|
|
48
|
+
];
|
|
49
|
+
export const grinTraits = (name, overrides) => calmed(createTraits(name, overrides), CALMING);
|
|
50
|
+
export const pairOf = (t) => t.pick("eyes", PAIRS);
|
|
51
|
+
export const mouthOf = (t) => t.pick("mouth", MOUTHS);
|
|
52
|
+
export function coloursOf(t) {
|
|
53
|
+
const body = t.color("body") ?? t.pick("body", COLOURS);
|
|
54
|
+
return { body };
|
|
55
|
+
}
|
|
56
|
+
/** How round the button is, from a tile at 0, which it is until set, to the reference's circle at 1. */
|
|
57
|
+
export const roundnessOf = (t) => t.forced("roundness") ?? 0;
|
|
58
|
+
/** The eye radius, the gap between their middles, and their height. */
|
|
59
|
+
const EYE = { r: 10.6, gap: 11.5, y: 33 };
|
|
60
|
+
/** One eye at `cx`, `cy`, with its pupil looking `look` of the way across it, and `skew` more from the name. */
|
|
61
|
+
function eye(spec, cx, cy, r, skew) {
|
|
62
|
+
const pupil = (look, reachX, reachY, size = r * 0.34) => ({
|
|
63
|
+
cx: r2(cx + (look[0] + skew[0]) * reachX),
|
|
64
|
+
cy: r2(cy + (look[1] + skew[1]) * reachY),
|
|
65
|
+
r: r2(size),
|
|
66
|
+
reach: r2(Math.min(reachX, reachY) * 0.5),
|
|
67
|
+
});
|
|
68
|
+
switch (spec.kind) {
|
|
69
|
+
case "round": {
|
|
70
|
+
const s = r * spec.size;
|
|
71
|
+
return { white: `<circle cx="${r2(cx)}" cy="${r2(cy)}" r="${r2(s)}"/>`, pupils: [pupil(spec.look, s * 0.5, s * 0.5, Math.max(2.8, s * 0.4))], cx, cy };
|
|
72
|
+
}
|
|
73
|
+
case "block": {
|
|
74
|
+
const [w, h] = [r * 2.3, r * 1.36];
|
|
75
|
+
return {
|
|
76
|
+
white: `<rect x="${r2(cx - w / 2)}" y="${r2(cy - h / 2)}" width="${r2(w)}" height="${r2(h)}" rx="0.6"/>`,
|
|
77
|
+
pupils: [pupil(spec.look, w * 0.34, h * 0.2, r * 0.4)],
|
|
78
|
+
cx,
|
|
79
|
+
cy,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "lid": {
|
|
83
|
+
// Cut flat across the top, as a lid half down, the pupil peeking from under it.
|
|
84
|
+
const top = cy - r * 0.32;
|
|
85
|
+
const side = Math.sqrt(r * r - (r * 0.32) ** 2);
|
|
86
|
+
return {
|
|
87
|
+
white: `<path d="M${r2(cx - side)} ${r2(top)}H${r2(cx + side)}A${r2(r)} ${r2(r)} 0 0 1 ${r2(cx - side)} ${r2(top)}Z"/>`,
|
|
88
|
+
pupils: [pupil(spec.look, r * 0.4, r * 0.3, r * 0.4)],
|
|
89
|
+
cx,
|
|
90
|
+
cy,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
case "wink":
|
|
94
|
+
// A thin white flick, as the reference's wink, its end curling down in one smooth flow.
|
|
95
|
+
return {
|
|
96
|
+
white: `<path d="M${r2(cx - r * 0.8)} ${r2(cy + r * 0.25)}C${r2(cx - r * 0.45)} ${r2(cy - r * 0.38)} ${r2(cx + r * 0.42)} ${r2(cy - r * 0.5)} ${r2(cx + r * 0.86)} ${r2(cy + r * 0.22)}" fill="none" stroke="${WHITE}" stroke-width="${r2(r * 0.2)}" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
97
|
+
pupils: [],
|
|
98
|
+
cx,
|
|
99
|
+
cy,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The pair: each eye drawn, or one joined white for both, with each pupil clipped to its white. */
|
|
104
|
+
function pairMarkup(id, pair, t) {
|
|
105
|
+
const r = EYE.r * t.num("eye.size", 0.9, 1.12);
|
|
106
|
+
const gap = EYE.gap * t.num("eye.gap", 0.95, 1.12);
|
|
107
|
+
const y = EYE.y + t.jitter("eye.y", 2.5);
|
|
108
|
+
const skew = (key) => [t.jitter(`${key}.x`, 0.25), t.jitter(`${key}.y`, 0.2)];
|
|
109
|
+
const blinks = [];
|
|
110
|
+
if ("joined" in pair) {
|
|
111
|
+
const look = [pair.look[0] + skew("left")[0], pair.look[1] + skew("left")[1]];
|
|
112
|
+
if (pair.joined === "goggles") {
|
|
113
|
+
// Flat across the top, a round lobe under each pupil, the pupils peeking down from its edge.
|
|
114
|
+
const top = y - r * 0.55;
|
|
115
|
+
const lobe = gap * 0.98;
|
|
116
|
+
const white = `<path d="M${r2(50 - gap - lobe)} ${r2(top)}H${r2(50 + gap + lobe)}V${r2(top + r * 0.25)}A${r2(lobe)} ${r2(lobe * 0.95)} 0 0 1 50 ${r2(top + r * 0.25)}A${r2(lobe)} ${r2(lobe * 0.95)} 0 0 1 ${r2(50 - gap - lobe)} ${r2(top + r * 0.25)}Z"/>`;
|
|
117
|
+
const pupils = [-1, 1].map((side) => ({ cx: r2(50 + side * gap + look[0] * 2.4), cy: r2(top + Math.max(0, look[1] + 1) * 3), r: r2(r * 0.36), reach: 1.4 }));
|
|
118
|
+
blinks.push({ white, pupils, cx: 50, cy: y });
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Two bubbles run together into one white.
|
|
122
|
+
const white = [-1, 1].map((side) => `<circle cx="${r2(50 + side * gap * 0.78)}" cy="${r2(y)}" r="${r2(r)}"/>`).join("");
|
|
123
|
+
const pupils = [-1, 1].map((side) => ({ cx: r2(50 + side * gap * 0.78 + look[0] * 4), cy: r2(y + look[1] * r * 0.52), r: r2(r * 0.34), reach: 2.6 }));
|
|
124
|
+
blinks.push({ white, pupils, cx: 50, cy: y });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
blinks.push(eye(pair.left, 50 - gap, y, r, skew("left")), eye(pair.right, 50 + gap, y, r, skew("right")));
|
|
129
|
+
}
|
|
130
|
+
return blinks.map((drawn, i) => {
|
|
131
|
+
const clip = `${id}-eye${i}`;
|
|
132
|
+
const stroke = drawn.white.includes("stroke=");
|
|
133
|
+
const pupils = drawn.pupils
|
|
134
|
+
.map((p) => `<circle class="pupil" style="--reach:${p.reach}" cx="${p.cx}" cy="${p.cy}" r="${p.r}" fill="${INK}"/>`)
|
|
135
|
+
.join("");
|
|
136
|
+
return (`<g class="eye" data-eye="${i}">` +
|
|
137
|
+
(stroke ? drawn.white : `<clipPath id="${clip}">${drawn.white}</clipPath><g fill="${WHITE}">${drawn.white}</g><g clip-path="url(#${clip})">${pupils}</g>`) +
|
|
138
|
+
`</g>`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The grin, as the reference's: a white crescent whose top dips gently to the middle between two
|
|
143
|
+
* round lobes, split into teeth by thin lines of the face's colour that fan out from a point
|
|
144
|
+
* above it. A missing tooth is everything between two of those lines, clipped to the outline, so
|
|
145
|
+
* it follows the mouth's curve exactly, and a missing back tooth is its whole end lobe.
|
|
146
|
+
*/
|
|
147
|
+
function mouthMarkup(id, mouth, t, lineColour) {
|
|
148
|
+
const w = 28 * mouth.width * t.num("mouth.size", 0.92, 1.08);
|
|
149
|
+
const h = 27 * mouth.height * t.num("mouth.size", 0.92, 1.08);
|
|
150
|
+
const top = 50 + t.jitter("mouth.y", 2);
|
|
151
|
+
const n = t.int("teeth", 4, 6);
|
|
152
|
+
const clip = `${id}-mouth`;
|
|
153
|
+
const lobe = w * 0.28;
|
|
154
|
+
const dip = h * 0.27;
|
|
155
|
+
// The dip leaves each lobe level, as the lobe's own top does, so they meet without a point.
|
|
156
|
+
const half = w - lobe;
|
|
157
|
+
const shape = `M${r2(50 - half)} ${r2(top)}C${r2(50 - half * 0.55)} ${r2(top)} ${r2(50 - half * 0.45)} ${r2(top + dip)} 50 ${r2(top + dip)}` +
|
|
158
|
+
`C${r2(50 + half * 0.45)} ${r2(top + dip)} ${r2(50 + half * 0.55)} ${r2(top)} ${r2(50 + half)} ${r2(top)}` +
|
|
159
|
+
`A${r2(lobe)} ${r2(lobe)} 0 0 1 ${r2(50 + w)} ${r2(top + lobe)}` +
|
|
160
|
+
`A${r2(w)} ${r2(h - lobe)} 0 0 1 ${r2(50 - w)} ${r2(top + lobe)}` +
|
|
161
|
+
`A${r2(lobe)} ${r2(lobe)} 0 0 1 ${r2(50 - w + lobe)} ${r2(top)}Z`;
|
|
162
|
+
const outline = `<path d="${shape}"/>`;
|
|
163
|
+
// Every line runs from a point well above the mouth, so the outer teeth lean out the most.
|
|
164
|
+
const focus = top - h * 2.2;
|
|
165
|
+
const [above, below] = [top - 4, top + h + 4];
|
|
166
|
+
const through = (x, y) => [50 + ((x - 50) * (y - focus)) / (top - focus), y];
|
|
167
|
+
const at = (i) => 50 - w + (2 * w * i) / n;
|
|
168
|
+
// At either end the edge is well outside the mouth, so a missing back tooth is its whole lobe.
|
|
169
|
+
const edge = (i) => (i <= 0 ? 50 - w - 12 : i >= n ? 50 + w + 12 : at(i));
|
|
170
|
+
const side = (i, f) => {
|
|
171
|
+
const [x1, y1] = through(edge(i), above);
|
|
172
|
+
const [x2, y2] = through(edge(i), below);
|
|
173
|
+
return [x1 + (x2 - x1) * f, y1 + (y2 - y1) * f];
|
|
174
|
+
};
|
|
175
|
+
const lines = Array.from({ length: n - 1 }, (_, i) => {
|
|
176
|
+
const [[x1, y1], [x2, y2]] = [side(i + 1, 0), side(i + 1, 1)];
|
|
177
|
+
return `<path d="M${r2(x1)} ${r2(y1)}L${r2(x2)} ${r2(y2)}" stroke="${lineColour}" stroke-width="0.95"/>`;
|
|
178
|
+
}).join("");
|
|
179
|
+
const tooth = (i) => {
|
|
180
|
+
const [a, b, c, d] = [side(i, 0), side(i + 1, 0), side(i + 1, 1), side(i, 1)];
|
|
181
|
+
return `<path d="M${r2(a[0])} ${r2(a[1])}L${r2(b[0])} ${r2(b[1])}L${r2(c[0])} ${r2(c[1])}L${r2(d[0])} ${r2(d[1])}Z" fill="${INK}"/>`;
|
|
182
|
+
};
|
|
183
|
+
const bottom = below;
|
|
184
|
+
// Broken partway: the lower part gone, below a softly uneven break across the tooth, its
|
|
185
|
+
// bumps low and rounded, as worn rather than shattered.
|
|
186
|
+
const chip = (i) => {
|
|
187
|
+
const [from, to] = [side(i, 0.5), side(i + 1, 0.6)];
|
|
188
|
+
const points = Array.from({ length: 9 }, (_, k) => {
|
|
189
|
+
const f = k / 8;
|
|
190
|
+
const lift = k === 0 || k === 8 ? 0 : (k % 2 ? -1 : 1) * h * 0.024 * (0.6 + t(`chip.${i}.${k}`) * 0.8);
|
|
191
|
+
return [from[0] + (to[0] - from[0]) * f, from[1] + (to[1] - from[1]) * f + lift];
|
|
192
|
+
});
|
|
193
|
+
// Curves through the midpoints between bumps, so the edge rolls rather than spikes.
|
|
194
|
+
const edgeLine = points
|
|
195
|
+
.slice(1, -1)
|
|
196
|
+
.map(([x, y], k) => {
|
|
197
|
+
const [nx, ny] = points[k + 2];
|
|
198
|
+
return `Q${r2(x)} ${r2(y)} ${r2((x + nx) / 2)} ${r2((y + ny) / 2)}`;
|
|
199
|
+
})
|
|
200
|
+
.join("");
|
|
201
|
+
const [low, high] = [side(i, 1), side(i + 1, 1)];
|
|
202
|
+
return `<path d="M${r2(from[0])} ${r2(from[1])}${edgeLine}L${r2(to[0])} ${r2(to[1])}L${r2(high[0])} ${r2(high[1] + 2)}L${r2(low[0])} ${r2(low[1] + 2)}Z" fill="${INK}"/>`;
|
|
203
|
+
};
|
|
204
|
+
// With only four teeth, the second and the second from the right would be side by side, so
|
|
205
|
+
// with both gone the gaps are kept apart, the second and the last.
|
|
206
|
+
const both = [...mouth.gaps, ...(mouth.chips ?? [])].includes("second") && n < 5;
|
|
207
|
+
const index = { second: 1, middle: Math.floor((n - 1) / 2), fourth: both ? n - 1 : n - 2 };
|
|
208
|
+
const gaps = mouth.gaps.map((gap) => tooth(index[gap])).join("") + (mouth.chips ?? []).map((gap) => chip(index[gap])).join("");
|
|
209
|
+
// A missing back tooth: its whole end lobe, out from the line beside it.
|
|
210
|
+
const back = (end) => tooth(end < 0 ? 0 : n - 1);
|
|
211
|
+
const corner = mouth.corner ? back(mouth.corner) : "";
|
|
212
|
+
return (`<clipPath id="${clip}">${outline}</clipPath>` +
|
|
213
|
+
`<g fill="${WHITE}">${outline}</g>` +
|
|
214
|
+
// The lines over the gaps, so each gap ends under the line beside it and never past it.
|
|
215
|
+
`<g clip-path="url(#${clip})">${gaps}${corner}${lines}</g>` +
|
|
216
|
+
// A hair of the face's colour round the edge, so neither the white beneath a gap nor a
|
|
217
|
+
// line's cut end shows as a fringe there, and the lines run on into the face unbroken.
|
|
218
|
+
`<path d="${shape}" fill="none" stroke="${lineColour}" stroke-width="0.5"/>`);
|
|
219
|
+
}
|
|
220
|
+
/** `id` keeps each grin's clips its own where many share a page. */
|
|
221
|
+
export function createGrin(name, overrides, id = "grin") {
|
|
222
|
+
const t = grinTraits(name, overrides);
|
|
223
|
+
const { body } = coloursOf(t);
|
|
224
|
+
return {
|
|
225
|
+
body,
|
|
226
|
+
// The other sections' tile corner, a little over a fifth of the side, up to half of it.
|
|
227
|
+
corner: r2(96 * (0.2222 + (0.5 - 0.2222) * roundnessOf(t))),
|
|
228
|
+
eyes: pairMarkup(id, pairOf(t), t),
|
|
229
|
+
// The lines between the teeth are the face showing through, so they are its colour.
|
|
230
|
+
mouth: mouthMarkup(id, mouthOf(t), t, body),
|
|
231
|
+
blink: { period: r2(t.num("blink.period", 3.2, 5.6)), delay: r2(t.num("blink.delay", 0, 3)) },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** The button, flat, from a rounded tile to a circle. */
|
|
235
|
+
export const buttonMarkup = (body, corner) => `<rect x="2" y="2" width="96" height="96" rx="${corner}" fill="${body}"/>`;
|
|
236
|
+
/** The grin at rest, for thumbnails. */
|
|
237
|
+
export function grinSvg(name, overrides) {
|
|
238
|
+
const g = createGrin(name, overrides);
|
|
239
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${buttonMarkup(g.body, g.corner)}${g.eyes.join("")}${g.mouth}</svg>`;
|
|
240
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gummies' bodies, toppers and colours, and their still picture: a frosted, translucent
|
|
3
|
+
* bun lit from inside, with a pearly topper. A pale body holds a soft pool of colour, a
|
|
4
|
+
* brighter band below it, and a white foot, under a frosty rim that follows the outline
|
|
5
|
+
* whatever the shape. The 3D gummy is inflated from the same outline.
|
|
6
|
+
*/
|
|
7
|
+
import type { Traits } from "../traits.ts";
|
|
8
|
+
export declare const GLOWS: {
|
|
9
|
+
label: string;
|
|
10
|
+
glow: string;
|
|
11
|
+
accent: string;
|
|
12
|
+
}[];
|
|
13
|
+
interface Body {
|
|
14
|
+
name: string;
|
|
15
|
+
/** Half its width and height, before the size is applied. */
|
|
16
|
+
rx: number;
|
|
17
|
+
ry: number;
|
|
18
|
+
/** 2 is an oval, and higher is squarer. */
|
|
19
|
+
power: number;
|
|
20
|
+
/** How much wider it gets towards the foot. */
|
|
21
|
+
taper: number;
|
|
22
|
+
/** How far the underside is pressed flat, where 1 leaves it round. */
|
|
23
|
+
foot: number;
|
|
24
|
+
/** A wavy hem along the bottom, and bumps all round, as `[count, depth]`. */
|
|
25
|
+
hem?: [number, number];
|
|
26
|
+
bumps?: [number, number];
|
|
27
|
+
/** How far it leans over, in degrees. */
|
|
28
|
+
lean?: number;
|
|
29
|
+
}
|
|
30
|
+
/** The first is the bun in the reference. Neighbours are as unlike as they can be. */
|
|
31
|
+
export declare const BODIES: Body[];
|
|
32
|
+
interface Topper {
|
|
33
|
+
name: string;
|
|
34
|
+
/** Drawn around its base, which sits on top of the body, with its foot tucked behind. */
|
|
35
|
+
d: string;
|
|
36
|
+
/** How wide it meets the body, which is where the join is melted away. */
|
|
37
|
+
neck: number;
|
|
38
|
+
}
|
|
39
|
+
export declare const TOPPERS: Topper[];
|
|
40
|
+
export declare const bodyOf: (t: Traits) => Body;
|
|
41
|
+
export declare const topperOf: (t: Traits) => Topper;
|
|
42
|
+
/** The colours in use, each of which can be changed by hand. */
|
|
43
|
+
export declare function coloursOf(t: Traits): {
|
|
44
|
+
glow: string;
|
|
45
|
+
accent: string;
|
|
46
|
+
ink: string;
|
|
47
|
+
};
|
|
48
|
+
/** Where everything goes: the outline, the face's place, size and tilt, and the topper's. */
|
|
49
|
+
export declare function layoutOf(t: Traits): {
|
|
50
|
+
body: Body;
|
|
51
|
+
topper: Topper;
|
|
52
|
+
outline: {
|
|
53
|
+
d: string;
|
|
54
|
+
rx: number;
|
|
55
|
+
ry: number;
|
|
56
|
+
middle: number;
|
|
57
|
+
top: number;
|
|
58
|
+
peakX: number;
|
|
59
|
+
};
|
|
60
|
+
face: {
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
size: number;
|
|
64
|
+
tilt: number;
|
|
65
|
+
};
|
|
66
|
+
/** The topper's base, on the top of the body, and its scale. */
|
|
67
|
+
crown: {
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
scale: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/** The jelly's art, upright and without its face, which the gummy draws itself. */
|
|
74
|
+
export declare function jellyArt(t: Traits): string;
|
|
75
|
+
/** The attributes every line of a face shares. Colour comes through `color`. */
|
|
76
|
+
export declare const FACE_LINE = "fill=\"none\" stroke-width=\"2.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
|
|
77
|
+
export {};
|