@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,168 @@
|
|
|
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 { BLACK, mix, WHITE } from "../colors.js";
|
|
8
|
+
export const GLOWS = [
|
|
9
|
+
{ label: "Mint", glow: "#3fe07a", accent: "#9df25c" },
|
|
10
|
+
{ label: "Sky", glow: "#4f8dff", accent: "#62e6ff" },
|
|
11
|
+
{ label: "Orchid", glow: "#a56bff", accent: "#ff7ee8" },
|
|
12
|
+
{ label: "Lagoon", glow: "#2fd6d0", accent: "#7fb4ff" },
|
|
13
|
+
{ label: "Peach", glow: "#ff9a5c", accent: "#ffd65c" },
|
|
14
|
+
{ label: "Rose", glow: "#ff6fa8", accent: "#ffb36b" },
|
|
15
|
+
{ label: "Lemon", glow: "#f2d33f", accent: "#8ff05c" },
|
|
16
|
+
{ label: "Iris", glow: "#6a63ff", accent: "#e07bff" },
|
|
17
|
+
{ label: "Coral", glow: "#ff6a6a", accent: "#ff9ee0" },
|
|
18
|
+
];
|
|
19
|
+
/** The first is the bun in the reference. Neighbours are as unlike as they can be. */
|
|
20
|
+
export const BODIES = [
|
|
21
|
+
{ name: "Bun", rx: 38, ry: 31, power: 2.5, taper: 0.1, foot: 0.86 },
|
|
22
|
+
{ name: "Tall", rx: 27, ry: 39, power: 2.2, taper: 0.14, foot: 0.92 },
|
|
23
|
+
{ name: "Ghost", rx: 30, ry: 36, power: 3, taper: 0.06, foot: 0.96, hem: [3.5, 0.075] },
|
|
24
|
+
{ name: "Mochi", rx: 44, ry: 23, power: 2.3, taper: 0.06, foot: 0.8 },
|
|
25
|
+
{ name: "Pear", rx: 31, ry: 37, power: 2.1, taper: 0.42, foot: 0.9 },
|
|
26
|
+
{ name: "Puff", rx: 35, ry: 32, power: 2, taper: 0.04, foot: 0.94, bumps: [7, 0.07] },
|
|
27
|
+
{ name: "Cube", rx: 33, ry: 31, power: 4.2, taper: 0.03, foot: 1, lean: 8 },
|
|
28
|
+
{ name: "Bell", rx: 33, ry: 35, power: 2.7, taper: 0.34, foot: 0.8 },
|
|
29
|
+
{ name: "Bean", rx: 39, ry: 26, power: 2.3, taper: 0.1, foot: 0.9, bumps: [2, 0.13], lean: 16 },
|
|
30
|
+
{ name: "Drop", rx: 29, ry: 36, power: 1.7, taper: 0.5, foot: 1 },
|
|
31
|
+
];
|
|
32
|
+
export const TOPPERS = [
|
|
33
|
+
{ name: "Crown", neck: 15, d: "M-17 12Q-12 6 -12 -2L-11 -8Q-10 -10 -8 -8L-3 -3L2 -9Q3 -10 4 -9L9 -3L12 -8Q13 -9 13 -7L13 -2Q13 6 18 12Z" },
|
|
34
|
+
{ name: "Horn", neck: 10, d: "M-13 12Q-5 6 -3.5 -3Q-2 -14 -1 -18Q0 -20 1 -18Q2 -14 3.5 -3Q5 6 13 12Z" },
|
|
35
|
+
{ name: "Ears", neck: 25, d: "M-27 16Q-21 2 -20 -6Q-19 -10 -16 -10Q-9 -6 -6 2Q-4 8 1 12H-1Q4 8 6 2Q9 -6 16 -10Q19 -10 20 -6Q21 2 27 16Z" },
|
|
36
|
+
{ name: "Sprout", neck: 8, d: "M-9 12Q-2 7 -1.3 0V-5Q-11 -5 -12 -14Q-3 -15 0 -7Q3 -15 12 -14Q11 -5 1.3 -5V0Q2 7 9 12Z" },
|
|
37
|
+
{ name: "Antenna", neck: 8, d: "M-9 12Q-2 7 -1.4 0V-11H1.4V0Q2 7 9 12ZM-5.4 -14.5a5.4 5.4 0 1 1 10.8 0a5.4 5.4 0 1 1 -10.8 0Z" },
|
|
38
|
+
{ name: "Flame", neck: 12, d: "M-15 12Q-7 7 -7 0Q-8 -5 -4 -8Q-4 -13 1 -18Q1 -11 6 -8Q9 -4 8 0Q8 7 15 12Z" },
|
|
39
|
+
{ name: "Horns", neck: 19, d: "M-21 14Q-15 6 -14 -4Q-14 -11 -13 -14Q-8 -9 -7 -2Q-5 8 1 12H-1Q5 8 7 -2Q8 -9 13 -14Q14 -11 14 -4Q15 6 21 14Z" },
|
|
40
|
+
{ name: "Bow", neck: 15, d: "M0 2Q-7 -10 -15 -8Q-17 -1 -15 6Q-7 7 0 2Q7 7 15 6Q17 -1 15 -8Q7 -10 0 2Z" },
|
|
41
|
+
{ name: "Bare", neck: 0, d: "" },
|
|
42
|
+
];
|
|
43
|
+
const OUTLINE_POINTS = 360;
|
|
44
|
+
/** Where the body's middle sits, low enough to leave room for a topper. */
|
|
45
|
+
const CENTRE_Y = 55;
|
|
46
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
47
|
+
const signedPower = (value, power) => Math.sign(value) * Math.abs(value) ** power;
|
|
48
|
+
export const bodyOf = (t) => t.pick("body", BODIES);
|
|
49
|
+
export const topperOf = (t) => t.pick("topper", TOPPERS);
|
|
50
|
+
/** The colours in use, each of which can be changed by hand. */
|
|
51
|
+
export function coloursOf(t) {
|
|
52
|
+
const picked = t.color("glow");
|
|
53
|
+
const pair = t.pick("glow", GLOWS);
|
|
54
|
+
const glow = picked ?? pair.glow;
|
|
55
|
+
return {
|
|
56
|
+
glow,
|
|
57
|
+
accent: t.color("accent") ?? (picked ? mix(picked, WHITE, 0.35) : pair.accent),
|
|
58
|
+
ink: t.color("ink") ?? mix(glow, BLACK, 0.2),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function drawOutline(t, body) {
|
|
62
|
+
const size = t.num("body.size", 0.92, 1.06);
|
|
63
|
+
const rx = body.rx * size;
|
|
64
|
+
const ry = body.ry * size;
|
|
65
|
+
const start = t.num("wave", 0, 2 * Math.PI);
|
|
66
|
+
const lean = (t.jitter("lean", body.lean ?? 0) * Math.PI) / 180;
|
|
67
|
+
const points = Array.from({ length: OUTLINE_POINTS }, (_, i) => {
|
|
68
|
+
const angle = (i * 2 * Math.PI) / OUTLINE_POINTS;
|
|
69
|
+
const spread = 1 + (body.bumps ? body.bumps[1] * Math.cos(body.bumps[0] * angle + start) : 0);
|
|
70
|
+
let x = rx * signedPower(Math.cos(angle), 2 / body.power) * spread;
|
|
71
|
+
let y = ry * signedPower(Math.sin(angle), 2 / body.power) * spread;
|
|
72
|
+
x *= 1 + (body.taper * y) / ry;
|
|
73
|
+
if (y > 0)
|
|
74
|
+
y *= body.foot;
|
|
75
|
+
if (body.hem && y > 0)
|
|
76
|
+
y += ry * body.hem[1] * Math.cos((body.hem[0] * 2 * Math.PI * x) / (rx * 2)) * (y / ry);
|
|
77
|
+
return [x * Math.cos(lean) - y * Math.sin(lean), x * Math.sin(lean) + y * Math.cos(lean) + CENTRE_Y];
|
|
78
|
+
});
|
|
79
|
+
const ys = points.map(([, y]) => y);
|
|
80
|
+
const top = Math.min(...ys);
|
|
81
|
+
const bottom = Math.max(...ys);
|
|
82
|
+
const peak = points[ys.indexOf(top)];
|
|
83
|
+
return {
|
|
84
|
+
d: `M${points.map(([x, y]) => `${round2(50 + x)} ${round2(y)}`).join("L")}Z`,
|
|
85
|
+
rx,
|
|
86
|
+
ry: (bottom - top) / 2,
|
|
87
|
+
middle: (top + bottom) / 2,
|
|
88
|
+
top,
|
|
89
|
+
peakX: 50 + peak[0],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function drawDefs(outline, glow, accent) {
|
|
93
|
+
const { d, ry, middle } = outline;
|
|
94
|
+
const top = round2(middle - ry);
|
|
95
|
+
const bottom = round2(middle + ry);
|
|
96
|
+
// One size for all of these, as a shape this flat would clip a blur sized to itself.
|
|
97
|
+
const blur = (id, amount) => `<filter id="${id}" filterUnits="userSpaceOnUse" x="-30" y="-30" width="160" height="160"><feGaussianBlur stdDeviation="${amount}"/></filter>`;
|
|
98
|
+
return (`<defs>` +
|
|
99
|
+
`<path id="jelly-shape" d="${d}"/>` +
|
|
100
|
+
`<mask id="jelly-inside" maskUnits="userSpaceOnUse" x="-30" y="-30" width="160" height="160"><use href="#jelly-shape" fill="#fff"/></mask>` +
|
|
101
|
+
`<linearGradient id="jelly-frost" gradientUnits="userSpaceOnUse" x1="50" y1="${top}" x2="50" y2="${bottom}">` +
|
|
102
|
+
`<stop offset="0" stop-color="${mix(glow, WHITE, 0.9)}"/><stop offset="1" stop-color="${mix(glow, WHITE, 0.72)}"/></linearGradient>` +
|
|
103
|
+
`<linearGradient id="jelly-sheen" gradientUnits="userSpaceOnUse" x1="50" y1="${top}" x2="50" y2="${round2(middle + ry * 0.1)}">` +
|
|
104
|
+
`<stop offset="0" stop-color="#fff" stop-opacity="0.85"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>` +
|
|
105
|
+
// Pearl: white where it meets the body, then shifting through the jelly's own colours.
|
|
106
|
+
`<linearGradient id="jelly-pearl" x1="0.5" y1="1" x2="0.5" y2="0">` +
|
|
107
|
+
`<stop offset="0.3" stop-color="#ffffff"/><stop offset="0.52" stop-color="${mix(accent, WHITE, 0.55)}"/>` +
|
|
108
|
+
`<stop offset="0.76" stop-color="${mix(glow, WHITE, 0.42)}"/><stop offset="1" stop-color="${mix(mix(glow, "#ff8ee0", 0.5), WHITE, 0.5)}"/></linearGradient>` +
|
|
109
|
+
blur("jelly-wide", 8.5) +
|
|
110
|
+
blur("jelly-mid", 4.6) +
|
|
111
|
+
blur("jelly-rim", 2.4) +
|
|
112
|
+
blur("jelly-edge", 0.5) +
|
|
113
|
+
`</defs>`);
|
|
114
|
+
}
|
|
115
|
+
/** A soft pool of light, drawn as a shape and blurred, which blends far better than stops. */
|
|
116
|
+
const pool = (cy, wide, tall, fill, blur, opacity = 1) => `<ellipse cx="50" cy="${round2(cy)}" rx="${round2(wide)}" ry="${round2(tall)}" fill="${fill}" opacity="${opacity}" filter="url(#${blur})"/>`;
|
|
117
|
+
/** Where everything goes: the outline, the face's place, size and tilt, and the topper's. */
|
|
118
|
+
export function layoutOf(t) {
|
|
119
|
+
const body = bodyOf(t);
|
|
120
|
+
const topper = topperOf(t);
|
|
121
|
+
const outline = drawOutline(t, body);
|
|
122
|
+
return {
|
|
123
|
+
body,
|
|
124
|
+
topper,
|
|
125
|
+
outline,
|
|
126
|
+
face: {
|
|
127
|
+
x: round2(50 + t.jitter("face.x", outline.rx * 0.2)),
|
|
128
|
+
y: round2(outline.middle + t.num("face.y", -0.42, -0.08) * outline.ry),
|
|
129
|
+
size: round2(t.num("face.size", 0.8, 1.1) * Math.min(1, outline.rx / 30)),
|
|
130
|
+
tilt: round2(t.jitter("face.tilt", 9)),
|
|
131
|
+
},
|
|
132
|
+
/** The topper's base, on the top of the body, and its scale. */
|
|
133
|
+
crown: { x: round2(outline.peakX), y: round2(outline.top), scale: round2(t.num("topper.size", 0.85, 1.1)) },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** The jelly's art, upright and without its face, which the gummy draws itself. */
|
|
137
|
+
export function jellyArt(t) {
|
|
138
|
+
const { topper, outline, crown: at } = layoutOf(t);
|
|
139
|
+
const { glow, accent } = coloursOf(t);
|
|
140
|
+
const crown = (paint) => topper.d
|
|
141
|
+
? `<path d="${topper.d}" transform="translate(${at.x} ${at.y}) scale(${at.scale})" ` +
|
|
142
|
+
`fill="${paint}" stroke="${paint}" stroke-width="1.2" stroke-linejoin="round"/>`
|
|
143
|
+
: "";
|
|
144
|
+
// Masked to the jelly and its topper, so the melt can't spill out as a halo on the canvas.
|
|
145
|
+
const join = topper.d
|
|
146
|
+
? `<mask id="jelly-whole" maskUnits="userSpaceOnUse" x="-30" y="-30" width="160" height="160"><use href="#jelly-shape" fill="#fff"/>${crown("#fff")}</mask>` +
|
|
147
|
+
`<g mask="url(#jelly-whole)"><ellipse cx="${round2(outline.peakX)}" cy="${round2(outline.top + 2)}" rx="${topper.neck}" ry="3.6" fill="#fff" filter="url(#jelly-rim)"/></g>`
|
|
148
|
+
: "";
|
|
149
|
+
const { rx, ry, middle } = outline;
|
|
150
|
+
const size = t.num("pool", 0.86, 1.08);
|
|
151
|
+
const light = pool(middle - ry * 0.5, rx * 0.8, ry * 0.42, accent, "jelly-wide", 0.22) +
|
|
152
|
+
pool(middle + ry * 0.2, rx * 0.8 * size, ry * 0.5 * size, glow, "jelly-wide") +
|
|
153
|
+
pool(middle + ry * 0.24, rx * 0.5 * size, ry * 0.22 * size, glow, "jelly-mid", 0.7) +
|
|
154
|
+
pool(middle + ry * 0.64, rx * 0.56, ry * 0.12, accent, "jelly-mid", 0.75) +
|
|
155
|
+
pool(middle + ry * 0.98, rx * 0.4, ry * 0.15, "#fff", "jelly-mid");
|
|
156
|
+
return (drawDefs(outline, glow, accent) +
|
|
157
|
+
crown("url(#jelly-pearl)") +
|
|
158
|
+
`<use href="#jelly-shape" fill="url(#jelly-frost)"/>` +
|
|
159
|
+
`<g mask="url(#jelly-inside)">` +
|
|
160
|
+
light +
|
|
161
|
+
`<use href="#jelly-shape" fill="url(#jelly-sheen)"/>` +
|
|
162
|
+
`<use href="#jelly-shape" fill="none" stroke="#fff" stroke-width="5.5" stroke-opacity="0.85" filter="url(#jelly-rim)"/>` +
|
|
163
|
+
`<use href="#jelly-shape" fill="none" stroke="#fff" stroke-width="1.4" stroke-opacity="0.7" filter="url(#jelly-edge)"/>` +
|
|
164
|
+
`</g>` +
|
|
165
|
+
join);
|
|
166
|
+
}
|
|
167
|
+
/** The attributes every line of a face shares. Colour comes through `color`. */
|
|
168
|
+
export const FACE_LINE = `fill="none" stroke-width="2.7" stroke-linecap="round" stroke-linejoin="round"`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Possibilities calms on a gummy: its bodies and toppers, its twelve faces, and the
|
|
3
|
+
* frost along with its proportions. Low keeps eight of each, one for each variant. Body and
|
|
4
|
+
* topper indices are into the lists in `art.ts`, face indices into `FACES`, 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 gummy: its bodies and toppers, its twelve faces, and the
|
|
3
|
+
* frost along with its proportions. Low keeps eight of each, one for each variant. Body and
|
|
4
|
+
* topper indices are into the lists in `art.ts`, face indices into `FACES`, plainest first.
|
|
5
|
+
*/
|
|
6
|
+
export const CALMING = {
|
|
7
|
+
features: /^(face|body|topper)\.(x|y|size|tilt)$|^(lean|pool|angle|frost)$/,
|
|
8
|
+
lists: {
|
|
9
|
+
// Bun, Mochi, Tall, Pear, Bell, Cube, Ghost, Puff, then the leaning Bean and Drop.
|
|
10
|
+
body: [10, 8, [0, 3, 1, 4, 7, 6, 2, 5, 8, 9]],
|
|
11
|
+
// Bare, Sprout, Bow, Ears, Crown, Antenna, Horn, Flame, then Horns.
|
|
12
|
+
topper: [9, 8, [8, 3, 7, 2, 0, 4, 1, 5, 6]],
|
|
13
|
+
// Happy, Sleepy, Cat, Shy, Wink, Smug, Grin, Wow, then Kiss, Pout, Deadpan and Dizzy.
|
|
14
|
+
face: [12, 8, [2, 4, 5, 11, 0, 8, 7, 3, 10, 9, 1, 6]],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export const NOTES = [
|
|
18
|
+
"rounded buns and bells with gentle faces, the frost and glow in their middle range.",
|
|
19
|
+
"more lean, and the face moves about more.",
|
|
20
|
+
"every body, face and topper, up to dizzy faces and horns, at any frost and glow.",
|
|
21
|
+
];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { containerControls } from "../container.js";
|
|
2
|
+
import { choice, palette, slider } from "../controls.js";
|
|
3
|
+
import { topperOf } from "./art.js";
|
|
4
|
+
import { narrowed, possibilitiesSlider } from "../possibilities.js";
|
|
5
|
+
import { colorKey } from "../traits.js";
|
|
6
|
+
import { CALMING, NOTES } from "./calming.js";
|
|
7
|
+
import { BODIES, coloursOf, FACES, GLOWS, gummyTraits, TOPPERS } from "./index.js";
|
|
8
|
+
const named = (list) => list.map(({ name: label }) => ({ label }));
|
|
9
|
+
export const controls = (name, overrides) => {
|
|
10
|
+
const t = gummyTraits(name, overrides);
|
|
11
|
+
const list = (control) => narrowed(control, t, CALMING);
|
|
12
|
+
const { accent, ink } = coloursOf(t);
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
title: "Face",
|
|
16
|
+
controls: [
|
|
17
|
+
list(choice(t, "face", "Face", "list", named(FACES))),
|
|
18
|
+
slider(t, "face.size", "Face size", 0, 100, "%"),
|
|
19
|
+
slider(t, "face.y", "Face height", 0, 100, "%"),
|
|
20
|
+
slider(t, "face.x", "Face across", -100, 100, "%"),
|
|
21
|
+
slider(t, "face.tilt", "Face tilt", -9, 9, "°"),
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: "Shape",
|
|
26
|
+
controls: [
|
|
27
|
+
possibilitiesSlider(t, NOTES),
|
|
28
|
+
list(choice(t, "body", "Body", "list", named(BODIES))),
|
|
29
|
+
list(choice(t, "topper", "Topper", "list", named(TOPPERS))),
|
|
30
|
+
slider(t, "body.size", "Body size", 0, 100, "%"),
|
|
31
|
+
...(topperOf(t).d ? [slider(t, "topper.size", "Topper size", 0, 100, "%")] : []),
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
title: "Colour",
|
|
36
|
+
controls: [
|
|
37
|
+
palette(t, "glow", "Colour", GLOWS.map(({ glow }) => glow)),
|
|
38
|
+
{
|
|
39
|
+
type: "colors",
|
|
40
|
+
id: "gummy.colors",
|
|
41
|
+
label: "Topper, face",
|
|
42
|
+
colors: [
|
|
43
|
+
{ id: colorKey("accent"), color: accent },
|
|
44
|
+
{ id: colorKey("ink"), color: ink },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
slider(t, "pool", "Glow", 0, 100, "%"),
|
|
48
|
+
slider(t, "frost", "Frost", 0, 100, "%"),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{ title: "Container", controls: containerControls(t) },
|
|
52
|
+
];
|
|
53
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gummies: the frosted buns, toppers and colours in `art.ts`, inflated into 3D. The body is a frosted
|
|
3
|
+
* shell over a glowing core, the topper a pearly piece set on top, and the face is inked on.
|
|
4
|
+
*
|
|
5
|
+
* The face is kept as strokes and dots, so the 3D stage can draw it from exact shapes that
|
|
6
|
+
* blink and follow the pointer, and the still picture draws the very same strokes.
|
|
7
|
+
*/
|
|
8
|
+
import { type Overrides, type Traits } from "../traits.ts";
|
|
9
|
+
import type { AvatarParts } from "../types.ts";
|
|
10
|
+
/** The name's traits, calmed to the Possibilities set, as the panel reads them too. */
|
|
11
|
+
export declare const gummyTraits: (name: string, overrides?: Overrides) => Traits;
|
|
12
|
+
export { BODIES, GLOWS, TOPPERS } from "./art.ts";
|
|
13
|
+
/** A dot, a line in M, L, Q and Z, or an oval ring, in the face's own units. */
|
|
14
|
+
type Mark = ["dot", number, number, number] | ["line", string] | ["ring", number, number, number, number];
|
|
15
|
+
interface Face {
|
|
16
|
+
name: string;
|
|
17
|
+
/** What blinks. */
|
|
18
|
+
eyes: Mark[];
|
|
19
|
+
/** What stays put through a blink: the mouth, and any brows. */
|
|
20
|
+
mouth: Mark[];
|
|
21
|
+
}
|
|
22
|
+
/** Drawn around the middle of the face. Neighbours are as unlike as they can be. */
|
|
23
|
+
export declare const FACES: Face[];
|
|
24
|
+
export declare const faceOf: (t: Traits) => Face;
|
|
25
|
+
/** How milky the shell is, from the colour it's tinted with towards white. */
|
|
26
|
+
export declare const frostOf: (t: Traits) => number;
|
|
27
|
+
/** The art's colours, with a deeper ink, as in 3D the face can sit over the glow. */
|
|
28
|
+
export declare function coloursOf(t: Traits): {
|
|
29
|
+
glow: string;
|
|
30
|
+
accent: string;
|
|
31
|
+
ink: string;
|
|
32
|
+
};
|
|
33
|
+
/** A topper's path, moved to its base and scaled. Its paths use M, L, Q, H, V, Z and relative arcs. */
|
|
34
|
+
export declare function placePath(d: string, x: number, y: number, scale: number): string;
|
|
35
|
+
export declare function gummyParts(name: string, overrides?: Overrides): AvatarParts;
|
|
36
|
+
/** The gummy at rest, for thumbnails and where 3D can't run: the jelly's art with this face. */
|
|
37
|
+
export declare function gummySvg(name: string, overrides?: Overrides): string;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gummies: the frosted buns, toppers and colours in `art.ts`, inflated into 3D. The body is a frosted
|
|
3
|
+
* shell over a glowing core, the topper a pearly piece set on top, and the face is inked on.
|
|
4
|
+
*
|
|
5
|
+
* The face is kept as strokes and dots, so the 3D stage can draw it from exact shapes that
|
|
6
|
+
* blink and follow the pointer, and the still picture draws the very same strokes.
|
|
7
|
+
*/
|
|
8
|
+
import { BLACK, mix, WHITE } from "../colors.js";
|
|
9
|
+
import { containerOf, inContainer } from "../container.js";
|
|
10
|
+
import { coloursOf as jellyColoursOf, FACE_LINE, jellyArt, layoutOf } from "./art.js";
|
|
11
|
+
import { calmed } from "../possibilities.js";
|
|
12
|
+
import { createTraits } from "../traits.js";
|
|
13
|
+
import { CALMING } from "./calming.js";
|
|
14
|
+
/** The name's traits, calmed to the Possibilities set, as the panel reads them too. */
|
|
15
|
+
export const gummyTraits = (name, overrides) => calmed(createTraits(name, overrides), CALMING);
|
|
16
|
+
export { BODIES, GLOWS, TOPPERS } from "./art.js";
|
|
17
|
+
const dot = (x, y, r = 2.7) => ["dot", x, y, r];
|
|
18
|
+
const line = (d) => ["line", d];
|
|
19
|
+
/** Drawn around the middle of the face. Neighbours are as unlike as they can be. */
|
|
20
|
+
export const FACES = [
|
|
21
|
+
{ name: "Wink", eyes: [dot(-9, -4), line("M12 -8L6 -4L12 0")], mouth: [line("M-11 7Q0 13 11 6"), line("M4 9.5Q5 16 10 14.5Q14 12 11 6")] },
|
|
22
|
+
{ name: "Deadpan", eyes: [line("M-16 -2L-5 -5"), line("M5 -5L16 -3")], mouth: [line("M0 3L5 11.5L-5 11.5Z")] },
|
|
23
|
+
{ name: "Happy", eyes: [line("M-13 0Q-9 -7 -5 0"), line("M5 0Q9 -7 13 0")], mouth: [line("M-5 6Q0 12 5 6")] },
|
|
24
|
+
{ name: "Wow", eyes: [dot(-9, -3, 3), dot(9, -3, 3)], mouth: [["ring", 0, 8, 3.4, 4.4]] },
|
|
25
|
+
{ name: "Sleepy", eyes: [line("M-13 -4Q-9 1 -5 -4"), line("M5 -4Q9 1 13 -4")], mouth: [dot(0, 7, 2)] },
|
|
26
|
+
{ name: "Cat", eyes: [dot(-10, -3), dot(10, -3)], mouth: [line("M-7 5Q-3.5 10 0 5Q3.5 10 7 5")] },
|
|
27
|
+
{ name: "Dizzy", eyes: [line("M-13 -6L-7 0"), line("M-7 -6L-13 0"), line("M7 -6L13 0"), line("M13 -6L7 0")], mouth: [line("M-5 8L5 8")] },
|
|
28
|
+
{ name: "Grin", eyes: [dot(-9, -4), dot(9, -4)], mouth: [line("M-8 4Q0 5 8 4Q6 13 0 13Q-6 13 -8 4Z")] },
|
|
29
|
+
{ name: "Smug", eyes: [line("M-14 -3Q-9 -1 -4 -3"), line("M4 -3Q9 -1 14 -3")], mouth: [line("M-6 7Q2 10.5 8 4")] },
|
|
30
|
+
{ name: "Pout", eyes: [dot(-9, -2), dot(9, -2)], mouth: [line("M-14 -8L-5 -6"), line("M5 -6L14 -8"), line("M-4 10Q0 6 4 10")] },
|
|
31
|
+
{ name: "Kiss", eyes: [line("M-13 -4Q-9 1 -5 -4"), dot(9, -3)], mouth: [line("M-1 3.5Q4.5 5 0.5 7.5Q4.5 10 -1 11.5")] },
|
|
32
|
+
{ name: "Shy", eyes: [dot(-8, -2, 2), dot(8, -2, 2)], mouth: [line("M-2.5 7Q0 9 2.5 7")] },
|
|
33
|
+
];
|
|
34
|
+
export const faceOf = (t) => t.pick("face", FACES);
|
|
35
|
+
/** How milky the shell is, from the colour it's tinted with towards white. */
|
|
36
|
+
export const frostOf = (t) => t.num("frost", 0.74, 0.94);
|
|
37
|
+
/** The glow's size, which the art calls its pool. */
|
|
38
|
+
const poolOf = (t) => t.num("pool", 0.86, 1.08);
|
|
39
|
+
/** The art's colours, with a deeper ink, as in 3D the face can sit over the glow. */
|
|
40
|
+
export function coloursOf(t) {
|
|
41
|
+
const colours = jellyColoursOf(t);
|
|
42
|
+
return { ...colours, ink: t.color("ink") ?? mix(colours.glow, BLACK, 0.55) };
|
|
43
|
+
}
|
|
44
|
+
/** Half the width of every line, as the still picture draws it. */
|
|
45
|
+
const LINE_RADIUS = 1.35;
|
|
46
|
+
/** How long each straight piece of a curve is at most, in the face's units. */
|
|
47
|
+
const PIECE = 2.5;
|
|
48
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
49
|
+
/** A line's path as the points along it, with its curves broken into short straight pieces. */
|
|
50
|
+
function pointsOf(d) {
|
|
51
|
+
const tokens = d.match(/[MLQZ]|-?\d*\.?\d+/g);
|
|
52
|
+
const runs = [];
|
|
53
|
+
let run = [];
|
|
54
|
+
let i = 0;
|
|
55
|
+
const next = () => Number(tokens[i++]);
|
|
56
|
+
while (i < tokens.length) {
|
|
57
|
+
const command = tokens[i++];
|
|
58
|
+
const from = run[run.length - 1];
|
|
59
|
+
if (command === "M") {
|
|
60
|
+
run = [[next(), next()]];
|
|
61
|
+
runs.push(run);
|
|
62
|
+
}
|
|
63
|
+
else if (command === "L") {
|
|
64
|
+
run.push([next(), next()]);
|
|
65
|
+
}
|
|
66
|
+
else if (command === "Q") {
|
|
67
|
+
const [cx, cy, x, y] = [next(), next(), next(), next()];
|
|
68
|
+
const pieces = Math.max(2, Math.ceil((Math.hypot(cx - from[0], cy - from[1]) + Math.hypot(x - cx, y - cy)) / PIECE));
|
|
69
|
+
for (let s = 1; s <= pieces; s++) {
|
|
70
|
+
const u = s / pieces;
|
|
71
|
+
const a = (1 - u) ** 2;
|
|
72
|
+
const b = 2 * u * (1 - u);
|
|
73
|
+
const c = u * u;
|
|
74
|
+
run.push([a * from[0] + b * cx + c * x, a * from[1] + b * cy + c * y]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (command === "Z") {
|
|
78
|
+
run.push(run[0]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return runs;
|
|
82
|
+
}
|
|
83
|
+
const ringPoints = (cx, cy, rx, ry) => {
|
|
84
|
+
const pieces = Math.ceil((2 * Math.PI * Math.max(rx, ry)) / PIECE);
|
|
85
|
+
return Array.from({ length: pieces + 1 }, (_, i) => {
|
|
86
|
+
const angle = (i / pieces) * 2 * Math.PI;
|
|
87
|
+
return [cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)];
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
/** From the face's units into the 100×100 frame, as `translate rotate scale` places it. */
|
|
91
|
+
const placer = ({ x, y, size, tilt }) => {
|
|
92
|
+
const turn = (tilt * Math.PI) / 180;
|
|
93
|
+
const [c, s] = [Math.cos(turn), Math.sin(turn)];
|
|
94
|
+
return ([px, py]) => [x + size * (c * px - s * py), y + size * (s * px + c * py)];
|
|
95
|
+
};
|
|
96
|
+
/** A line's path as its pieces: straight ones as two points, curves as three. */
|
|
97
|
+
function piecesOf(d) {
|
|
98
|
+
const tokens = d.match(/[MLQZ]|-?\d*\.?\d+/g);
|
|
99
|
+
const pieces = [];
|
|
100
|
+
let start = [0, 0];
|
|
101
|
+
let at = [0, 0];
|
|
102
|
+
let i = 0;
|
|
103
|
+
const next = () => Number(tokens[i++]);
|
|
104
|
+
while (i < tokens.length) {
|
|
105
|
+
const command = tokens[i++];
|
|
106
|
+
if (command === "M") {
|
|
107
|
+
at = start = [next(), next()];
|
|
108
|
+
}
|
|
109
|
+
else if (command === "L") {
|
|
110
|
+
const to = [next(), next()];
|
|
111
|
+
pieces.push([at, to]);
|
|
112
|
+
at = to;
|
|
113
|
+
}
|
|
114
|
+
else if (command === "Q") {
|
|
115
|
+
const control = [next(), next()];
|
|
116
|
+
const to = [next(), next()];
|
|
117
|
+
pieces.push([at, control, to]);
|
|
118
|
+
at = to;
|
|
119
|
+
}
|
|
120
|
+
else if (command === "Z") {
|
|
121
|
+
pieces.push([at, start]);
|
|
122
|
+
at = start;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return pieces;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A face's marks as the stage's shapes, each drawn exactly: a circle for a dot, a stroked
|
|
129
|
+
* oval for a ring, a rod for a straight piece and a curve for a curved one. Placing them
|
|
130
|
+
* moves, turns and scales, which keeps a curve a curve.
|
|
131
|
+
*/
|
|
132
|
+
function shapesOf(marks, place) {
|
|
133
|
+
const to = placer(place);
|
|
134
|
+
const r = round2(LINE_RADIUS * place.size);
|
|
135
|
+
const at = (point) => to(point).map(round2);
|
|
136
|
+
return marks.flatMap((mark) => {
|
|
137
|
+
if (mark[0] === "dot") {
|
|
138
|
+
const [cx, cy] = at([mark[1], mark[2]]);
|
|
139
|
+
return [{ kind: "circle", cx, cy, r: round2(mark[3] * place.size) }];
|
|
140
|
+
}
|
|
141
|
+
if (mark[0] === "ring") {
|
|
142
|
+
const [cx, cy] = at([mark[1], mark[2]]);
|
|
143
|
+
return [{ kind: "superellipse", cx, cy, rx: round2(mark[3] * place.size), ry: round2(mark[4] * place.size), n: 2, rot: place.tilt, stroke: r }];
|
|
144
|
+
}
|
|
145
|
+
return piecesOf(mark[1]).map((points) => {
|
|
146
|
+
const [[x1, y1], [x2, y2]] = [at(points[0]), at(points[points.length - 1])];
|
|
147
|
+
if (points.length === 2)
|
|
148
|
+
return { kind: "rod", x1, y1, x2, y2, r };
|
|
149
|
+
const [cx, cy] = at(points[1]);
|
|
150
|
+
return { kind: "curve", x1, y1, cx, cy, x2, y2, r };
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Every point a face's marks reach, in the face's own units. */
|
|
155
|
+
const reachOf = (marks) => marks.flatMap((mark) => {
|
|
156
|
+
if (mark[0] === "dot") {
|
|
157
|
+
const [, x, y, r] = mark;
|
|
158
|
+
return [[x - r, y], [x + r, y], [x, y - r], [x, y + r]];
|
|
159
|
+
}
|
|
160
|
+
if (mark[0] === "ring")
|
|
161
|
+
return ringPoints(mark[1], mark[2], mark[3], mark[4]);
|
|
162
|
+
return pointsOf(mark[1]).flat();
|
|
163
|
+
});
|
|
164
|
+
/** Room left round the face for its lines' width, as a share of its size. */
|
|
165
|
+
const MARGIN = 1.18;
|
|
166
|
+
const FIT_STEPS = 20;
|
|
167
|
+
/** Whether a point is inside an outline given as `M x yLx y…Z`. */
|
|
168
|
+
function insideOf(d) {
|
|
169
|
+
const outline = d.slice(1, -1).split("L").map((point) => point.split(" ").map(Number));
|
|
170
|
+
return (x, y) => {
|
|
171
|
+
let within = false;
|
|
172
|
+
for (let a = 0, b = outline.length - 1; a < outline.length; b = a++) {
|
|
173
|
+
const [xa, ya] = outline[a];
|
|
174
|
+
const [xb, yb] = outline[b];
|
|
175
|
+
if (ya > y !== yb > y && x < ((xb - xa) * (y - ya)) / (yb - ya) + xa)
|
|
176
|
+
within = !within;
|
|
177
|
+
}
|
|
178
|
+
return within;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The face where it was placed, if it fits on the body. Otherwise it shrinks and moves in
|
|
183
|
+
* towards the body's middle until it does, as in 3D anything off the body isn't drawn.
|
|
184
|
+
*/
|
|
185
|
+
function fitted(face, place, outline) {
|
|
186
|
+
const inside = insideOf(outline.d);
|
|
187
|
+
const reach = reachOf([...face.eyes, ...face.mouth]).map(([x, y]) => [x * MARGIN, y * MARGIN]);
|
|
188
|
+
let fit = place;
|
|
189
|
+
for (let step = 0; step < FIT_STEPS; step++) {
|
|
190
|
+
const to = placer(fit);
|
|
191
|
+
if (reach.every((point) => inside(...to(point))))
|
|
192
|
+
break;
|
|
193
|
+
fit = { ...fit, size: fit.size * 0.95, x: fit.x + (50 - fit.x) * 0.15, y: fit.y + (outline.middle - fit.y) * 0.15 };
|
|
194
|
+
}
|
|
195
|
+
return { x: round2(fit.x), y: round2(fit.y), size: round2(fit.size), tilt: fit.tilt };
|
|
196
|
+
}
|
|
197
|
+
/** The same marks as markup, for the still picture, inside the face's own transform. */
|
|
198
|
+
const marksMarkup = (marks) => marks
|
|
199
|
+
.map((mark) => mark[0] === "dot"
|
|
200
|
+
? `<circle cx="${mark[1]}" cy="${mark[2]}" r="${mark[3]}" fill="currentColor" stroke="none"/>`
|
|
201
|
+
: mark[0] === "ring"
|
|
202
|
+
? `<ellipse cx="${mark[1]}" cy="${mark[2]}" rx="${mark[3]}" ry="${mark[4]}"/>`
|
|
203
|
+
: `<path d="${mark[1]}"/>`)
|
|
204
|
+
.join("");
|
|
205
|
+
/** A topper's path, moved to its base and scaled. Its paths use M, L, Q, H, V, Z and relative arcs. */
|
|
206
|
+
export function placePath(d, x, y, scale) {
|
|
207
|
+
const tokens = d.match(/[a-zA-Z]|-?\d*\.?\d+/g) ?? [];
|
|
208
|
+
const out = [];
|
|
209
|
+
let i = 0;
|
|
210
|
+
const next = () => Number(tokens[i++]);
|
|
211
|
+
const at = (value, offset) => round2(value * scale + offset);
|
|
212
|
+
while (i < tokens.length) {
|
|
213
|
+
const command = tokens[i++];
|
|
214
|
+
if (command === "Z" || command === "z") {
|
|
215
|
+
out.push("Z");
|
|
216
|
+
}
|
|
217
|
+
else if (command === "H") {
|
|
218
|
+
out.push(`H${at(next(), x)}`);
|
|
219
|
+
}
|
|
220
|
+
else if (command === "V") {
|
|
221
|
+
out.push(`V${at(next(), y)}`);
|
|
222
|
+
}
|
|
223
|
+
else if (command === "a") {
|
|
224
|
+
// Relative, so its end only scales. Its radii scale with it.
|
|
225
|
+
const [rx, ry, turn, large, sweep, dx, dy] = Array.from({ length: 7 }, next);
|
|
226
|
+
out.push(`a${round2(rx * scale)} ${round2(ry * scale)} ${turn} ${large} ${sweep} ${round2(dx * scale)} ${round2(dy * scale)}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// M, L and Q: pairs of absolute points.
|
|
230
|
+
const pairs = command === "Q" ? 2 : 1;
|
|
231
|
+
const values = [];
|
|
232
|
+
for (let p = 0; p < pairs; p++)
|
|
233
|
+
values.push(at(next(), x), at(next(), y));
|
|
234
|
+
out.push(`${command}${values.join(" ")}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return out.join("");
|
|
238
|
+
}
|
|
239
|
+
export function gummyParts(name, overrides) {
|
|
240
|
+
const t = gummyTraits(name, overrides);
|
|
241
|
+
const { topper, outline, face: placed, crown } = layoutOf(t);
|
|
242
|
+
const face = faceOf(t);
|
|
243
|
+
const place = fitted(face, placed, outline);
|
|
244
|
+
const { glow, accent, ink } = coloursOf(t);
|
|
245
|
+
const eyeShapes = shapesOf(face.eyes, place);
|
|
246
|
+
const eyePoints = eyeShapes.flatMap((shape) => shape.kind === "rod" || shape.kind === "curve" ? [shape.y1, shape.y2] : "cy" in shape ? [shape.cy] : []);
|
|
247
|
+
return {
|
|
248
|
+
colors: { body: glow, accent, visor: glow, light: ink },
|
|
249
|
+
head: [outline.d],
|
|
250
|
+
accents: topper.d ? [placePath(topper.d, crown.x, crown.y, crown.scale)] : [],
|
|
251
|
+
visor: [],
|
|
252
|
+
visorShapes: [],
|
|
253
|
+
eyes: [],
|
|
254
|
+
eyeShapes,
|
|
255
|
+
lights: [],
|
|
256
|
+
lightShapes: shapesOf(face.mouth, place),
|
|
257
|
+
chrome: [],
|
|
258
|
+
chromeShapes: [],
|
|
259
|
+
studs: [],
|
|
260
|
+
studShapes: [],
|
|
261
|
+
speckle: 0,
|
|
262
|
+
eyeY: eyePoints.reduce((sum, y) => sum + y, 0) / eyePoints.length,
|
|
263
|
+
painted: true,
|
|
264
|
+
jelly: { frost: round2(frostOf(t)), pool: round2(poolOf(t)) },
|
|
265
|
+
container: containerOf(t),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** The gummy at rest, for thumbnails and where 3D can't run: the jelly's art with this face. */
|
|
269
|
+
export function gummySvg(name, overrides) {
|
|
270
|
+
const t = gummyTraits(name, overrides);
|
|
271
|
+
const { face: placed, outline } = layoutOf(t);
|
|
272
|
+
const face = faceOf(t);
|
|
273
|
+
const { glow, ink } = coloursOf(t);
|
|
274
|
+
const { x, y, size, tilt } = fitted(face, placed, outline);
|
|
275
|
+
// The art's frost runs from 0.9 to 0.72 white, top to foot. Here it follows Frost,
|
|
276
|
+
// and Frost's middle gives just those.
|
|
277
|
+
const frost = frostOf(t);
|
|
278
|
+
const shell = [0.06, -0.12].map((by) => mix(glow, WHITE, Math.min(1, frost + by)));
|
|
279
|
+
const art = jellyArt(t).replace(/(<linearGradient id="jelly-frost"[^>]*><stop offset="0" stop-color=")[^"]+("\/><stop offset="1" stop-color=")[^"]+/, `$1${shell[0]}$2${shell[1]}`) +
|
|
280
|
+
`<g transform="translate(${x} ${y}) rotate(${tilt}) scale(${size})" color="${ink}" stroke="${ink}" ${FACE_LINE}>` +
|
|
281
|
+
`${marksMarkup(face.eyes)}${marksMarkup(face.mouth)}</g>`;
|
|
282
|
+
return inContainer(art, containerOf(t), "gummy-card");
|
|
283
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Undo and redo for one avatar's overrides. */
|
|
2
|
+
import type { Patch } from "./controls.ts";
|
|
3
|
+
import type { Overrides } from "./traits.ts";
|
|
4
|
+
/** Changes to the same control this close together are one step, so a drag undoes as one. */
|
|
5
|
+
export declare const GROUP_MS = 500;
|
|
6
|
+
export interface History {
|
|
7
|
+
past: Overrides[];
|
|
8
|
+
present: Overrides;
|
|
9
|
+
future: Overrides[];
|
|
10
|
+
/** The control changed last, and when. */
|
|
11
|
+
group: string | null;
|
|
12
|
+
changedAt: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const emptyHistory: History;
|
|
15
|
+
export declare function patched(current: Overrides, patch: Patch): Overrides;
|
|
16
|
+
/** `alone` makes it a step of its own, as a typed number or an arrow press is, rather than part of a drag. */
|
|
17
|
+
export declare function change(history: History, patch: Patch, now: number, alone?: boolean): History;
|
|
18
|
+
/** Back to the name's own avatar, as a step that can itself be undone. */
|
|
19
|
+
export declare function clear(history: History): History;
|
|
20
|
+
export declare function undo(history: History): History;
|
|
21
|
+
export declare function redo(history: History): History;
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Undo and redo for one avatar's overrides. */
|
|
2
|
+
/** Changes to the same control this close together are one step, so a drag undoes as one. */
|
|
3
|
+
export const GROUP_MS = 500;
|
|
4
|
+
const MAX_STEPS = 100;
|
|
5
|
+
export const emptyHistory = { past: [], present: {}, future: [], group: null, changedAt: 0 };
|
|
6
|
+
export function patched(current, patch) {
|
|
7
|
+
const next = { ...current, ...patch };
|
|
8
|
+
for (const key of Object.keys(next))
|
|
9
|
+
if (next[key] === undefined)
|
|
10
|
+
delete next[key];
|
|
11
|
+
return next;
|
|
12
|
+
}
|
|
13
|
+
/** `alone` makes it a step of its own, as a typed number or an arrow press is, rather than part of a drag. */
|
|
14
|
+
export function change(history, patch, now, alone = false) {
|
|
15
|
+
const group = Object.keys(patch).join();
|
|
16
|
+
const continues = !alone && group === history.group && now - history.changedAt < GROUP_MS;
|
|
17
|
+
return {
|
|
18
|
+
past: continues ? history.past : [...history.past, history.present].slice(-MAX_STEPS),
|
|
19
|
+
present: patched(history.present, patch),
|
|
20
|
+
future: [],
|
|
21
|
+
group,
|
|
22
|
+
changedAt: now,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Back to the name's own avatar, as a step that can itself be undone. */
|
|
26
|
+
export function clear(history) {
|
|
27
|
+
if (Object.keys(history.present).length === 0)
|
|
28
|
+
return history;
|
|
29
|
+
return { past: [...history.past, history.present].slice(-MAX_STEPS), present: {}, future: [], group: null, changedAt: 0 };
|
|
30
|
+
}
|
|
31
|
+
export function undo(history) {
|
|
32
|
+
const previous = history.past[history.past.length - 1];
|
|
33
|
+
if (!previous)
|
|
34
|
+
return history;
|
|
35
|
+
return {
|
|
36
|
+
past: history.past.slice(0, -1),
|
|
37
|
+
present: previous,
|
|
38
|
+
future: [history.present, ...history.future],
|
|
39
|
+
group: null,
|
|
40
|
+
changedAt: 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function redo(history) {
|
|
44
|
+
const [next, ...future] = history.future;
|
|
45
|
+
if (!next)
|
|
46
|
+
return history;
|
|
47
|
+
return { past: [...history.past, history.present], present: next, future, group: null, changedAt: 0 };
|
|
48
|
+
}
|