@originkit/avatars 0.0.0-stage → 0.1.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 +119 -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 +128 -0
- package/dist/controls.js +170 -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 +11 -0
- package/dist/variants.js +41 -0
- package/mcp/server.js +7 -0
- package/mcp/tools.js +70 -0
- package/package.json +54 -4
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cats, traced from the reference: a black cat in three-quarter view, its head tipped and
|
|
3
|
+
* coming in from the corner. Its ears have rounded tips and edges that bulge a little, and
|
|
4
|
+
* a dark inner ear. Each eye is an ellipse cut straight across by the lid through its
|
|
5
|
+
* middle, so the corner by the nose is sharp and the outer end round, and the lid's angle
|
|
6
|
+
* is most of the mood. The nose sits off towards the far eye, and the mouth is a thick
|
|
7
|
+
* grey line, longer on the near side, with a single fang.
|
|
8
|
+
*
|
|
9
|
+
* There are two views, each traced from its own reference: turned three-quarters, and nearly
|
|
10
|
+
* face on with a pink nose and an open mouth. Points are in tile units, traced from 1200 px
|
|
11
|
+
* drawings at a twelfth of their size. The face is drawn upright about the nose, then turned
|
|
12
|
+
* with the head by the view's tilt.
|
|
13
|
+
*/
|
|
14
|
+
import { mix, WHITE } from "../colors.js";
|
|
15
|
+
import { calmed } from "../possibilities.js";
|
|
16
|
+
import { createTraits } from "../traits.js";
|
|
17
|
+
import { CALMING } from "./calming.js";
|
|
18
|
+
export const TILES = ["#2fe370", "#ffd23f", "#ff5fa2", "#3fa9ff", "#ff8a2b", "#9b6bff", "#ff4b4b", "#16c8b0", "#b6f03c"];
|
|
19
|
+
export const FURS = ["#050505", "#26262b", "#4a4a55", "#e0893b", "#7a5136"];
|
|
20
|
+
export const EYE_COLOURS = ["#f3f7f3", "#ffe14d", "#c8f55a", "#8fe0ff", "#ffb347"];
|
|
21
|
+
const TONGUE = "#ff7aa8";
|
|
22
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
23
|
+
const point = (x, y) => `${round2(x)} ${round2(y)}`;
|
|
24
|
+
const at = ([x, y]) => point(x, y);
|
|
25
|
+
const lerp = (a, b, k) => [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k];
|
|
26
|
+
const polygon = (points) => `M${points.map(at).join("L")}Z`;
|
|
27
|
+
const distance = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
28
|
+
const ellipse = ([x, y], rx, ry, turn, fill) => `<ellipse cx="${round2(x)}" cy="${round2(y)}" rx="${round2(rx)}" ry="${round2(ry)}" transform="rotate(${round2(turn)} ${point(x, y)})" fill="${fill}"/>`;
|
|
29
|
+
/** Turns a vector clockwise on screen, in degrees. */
|
|
30
|
+
function turned([x, y], degrees) {
|
|
31
|
+
const r = (degrees * Math.PI) / 180;
|
|
32
|
+
return [x * Math.cos(r) - y * Math.sin(r), x * Math.sin(r) + y * Math.cos(r)];
|
|
33
|
+
}
|
|
34
|
+
/** A closed curve through every point, as a Catmull-Rom spline. */
|
|
35
|
+
function smooth(points) {
|
|
36
|
+
const n = points.length;
|
|
37
|
+
let d = `M${at(points[0])}`;
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const [p0, p1, p2, p3] = [points[(i + n - 1) % n], points[i], points[(i + 1) % n], points[(i + 2) % n]];
|
|
40
|
+
d += `C${point(p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6)} ${point(p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6)} ${at(p2)}`;
|
|
41
|
+
}
|
|
42
|
+
return `${d}Z`;
|
|
43
|
+
}
|
|
44
|
+
/** Neighbours are as unlike as possible, since each variant steps to the next. */
|
|
45
|
+
export const VIEWS = [
|
|
46
|
+
{
|
|
47
|
+
// Turned three-quarters, the head tipped and in from the corner, with a grey nose.
|
|
48
|
+
name: "Three-quarter",
|
|
49
|
+
centre: [55, 58],
|
|
50
|
+
nose: [42.8, 57.2],
|
|
51
|
+
tilt: -18,
|
|
52
|
+
frame: "",
|
|
53
|
+
head: [
|
|
54
|
+
[14.5, 46], [22, 37.5], [33, 31.5], [48, 26.5], [63, 25.5], [77.5, 28.8], [86.7, 37.5], [90.8, 46.7], [91.7, 54.2],
|
|
55
|
+
[90.8, 61], [87.5, 69.5], [80, 77], [62, 85], [45, 90], [35.8, 90], [27.5, 87.5], [19, 81.7], [12.8, 73.3], [10.8, 63.3], [11.6, 54],
|
|
56
|
+
],
|
|
57
|
+
cheeks: [7, 8, 9, 10, 15, 16, 17, 18, 19],
|
|
58
|
+
// A straight neck down the left, and the shoulder running off to the right.
|
|
59
|
+
body: [[35.8, 88], [35, 130], [130, 130], [130, 112], [100, 84], [86.7, 70.4]],
|
|
60
|
+
ears: [
|
|
61
|
+
{ inner: [29.2, 32.9], tip: [6.2, 29], outer: [12.3, 52.9] },
|
|
62
|
+
{ inner: [47.9, 25.8], tip: [60.2, 6.2], outer: [77.5, 28.8] },
|
|
63
|
+
],
|
|
64
|
+
whiskers: [
|
|
65
|
+
[[22.7, 65.8], [10.5, 64.6], [0.8, 66.7]],
|
|
66
|
+
[[22.9, 69.2], [11.5, 71.3], [0.8, 77.9]],
|
|
67
|
+
[[24, 73.2], [14.5, 78.2], [6.8, 86.3]],
|
|
68
|
+
[[70, 51.3], [83, 42.8], [94.2, 37.3]],
|
|
69
|
+
[[71.7, 55.4], [85, 51], [98.5, 49.2]],
|
|
70
|
+
[[73.3, 59.2], [86, 59.2], [95, 62.7]],
|
|
71
|
+
],
|
|
72
|
+
eyes: [
|
|
73
|
+
{ c: [-14.1, -6.6], rx: 9.4, ry: 7.2 },
|
|
74
|
+
{ c: [20.4, -6.6], rx: 11.6, ry: 8.6 },
|
|
75
|
+
],
|
|
76
|
+
noseShape: "M-2.9 -0.7Q0 -2 3 -1Q3.4 -0.4 2.5 0.5L0.6 1.7Q-0.2 2.2 -1.1 1.5L-2.7 0.3Q-3.4 -0.2 -2.9 -0.7Z",
|
|
77
|
+
noseTint: [WHITE, 0.17],
|
|
78
|
+
earTint: [WHITE, 0.2],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// Nearly face on and close up, cropped at the top and right, with a pink nose and lilac ears.
|
|
82
|
+
name: "Front",
|
|
83
|
+
centre: [66, 55],
|
|
84
|
+
nose: [55.8, 57],
|
|
85
|
+
tilt: 1,
|
|
86
|
+
frame: "translate(-2 1) translate(66 55) scale(0.95) translate(-66 -55)",
|
|
87
|
+
head: [
|
|
88
|
+
[31.7, 26.7], [45, 17.5], [61, 13.5], [75, 12.5], [84, 14.5], [96, 19], [106, 26], [112, 40], [114, 55],
|
|
89
|
+
[112, 70], [105, 82], [90, 90], [70, 93], [50, 90], [36, 83], [27, 76], [21.5, 67], [19.6, 58], [20.5, 48], [24.5, 37.5],
|
|
90
|
+
],
|
|
91
|
+
cheeks: [8, 9, 10, 15, 16, 17, 18, 19],
|
|
92
|
+
body: [[35, 82], [24.5, 130], [130, 130], [130, 60], [105, 82]],
|
|
93
|
+
ears: [
|
|
94
|
+
{ inner: [61, 13.5], tip: [41.5, -1], outer: [31.7, 27.5] },
|
|
95
|
+
{ inner: [84, 14.5], tip: [110, -6], outer: [108, 30] },
|
|
96
|
+
],
|
|
97
|
+
whiskers: [
|
|
98
|
+
[[31.9, 57.9], [20, 54.3], [9.2, 51]],
|
|
99
|
+
[[31.3, 62.1], [20, 61.9], [8.8, 61.7]],
|
|
100
|
+
[[31, 66.4], [21, 69], [10.8, 71.7]],
|
|
101
|
+
[[87.5, 61.7], [94, 60.4], [104, 58.5]],
|
|
102
|
+
[[88.3, 66.3], [95, 66.9], [104, 67.8]],
|
|
103
|
+
[[87.5, 70.8], [94, 73.4], [104, 77]],
|
|
104
|
+
],
|
|
105
|
+
eyes: [
|
|
106
|
+
{ c: [-14.9, -7.6], rx: 9.5, ry: 7.4 },
|
|
107
|
+
{ c: [23.8, -7], rx: 12.4, ry: 8.4 },
|
|
108
|
+
],
|
|
109
|
+
noseShape: "M-3.3 -1.3Q0 -2 3.3 -1.3Q3.9 -0.8 3.1 0L0.9 1.6Q0 2.2 -0.9 1.6L-3.1 0Q-3.9 -0.8 -3.3 -1.3Z",
|
|
110
|
+
noseTint: ["#f27ba3", 1],
|
|
111
|
+
earTint: ["#9b87b0", 0.42],
|
|
112
|
+
greyWhiskers: true,
|
|
113
|
+
tallPupils: true,
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
/** The first is the reference, in from the corner. */
|
|
117
|
+
export const POSES = [
|
|
118
|
+
{ name: "Corner", x: 0, y: 0, scale: 1, turn: 0 },
|
|
119
|
+
{ name: "Other corner", x: 0, y: 0, scale: 1, turn: 0, mirrored: true },
|
|
120
|
+
{ name: "Centre", x: -6, y: -2, scale: 0.78, turn: 8 },
|
|
121
|
+
{ name: "Peeking", x: -4, y: 24, scale: 0.9, turn: 10 },
|
|
122
|
+
{ name: "Close up", x: 4, y: 6, scale: 1.25, turn: 2 },
|
|
123
|
+
{ name: "Tilted", x: 2, y: 2, scale: 0.95, turn: -12 },
|
|
124
|
+
];
|
|
125
|
+
/** Neighbours are as unlike as possible, since each variant steps to the next. */
|
|
126
|
+
export const EARS = [
|
|
127
|
+
{ name: "Classic", height: 1, flare: 0, round: 3.4 },
|
|
128
|
+
{ name: "Folded", height: 0.5, flare: -12, round: 4.6, folded: true },
|
|
129
|
+
{ name: "Tall", height: 1.32, flare: -4, round: 2.2 },
|
|
130
|
+
{ name: "Notched", height: 1, flare: 0, round: 3.2, notch: true },
|
|
131
|
+
{ name: "Short", height: 0.72, flare: 8, round: 3.6 },
|
|
132
|
+
{ name: "Tufted", height: 1.12, flare: 0, round: 1.6, tuft: true },
|
|
133
|
+
{ name: "Mismatched", height: 1, flare: 0, round: 3.4, odd: true },
|
|
134
|
+
{ name: "Wide", height: 0.9, flare: 16, round: 3 },
|
|
135
|
+
];
|
|
136
|
+
/**
|
|
137
|
+
* The owl, traced from its own reference, at a twelfth and a half of 1254 px: one big round
|
|
138
|
+
* head on a broad body, with a notch at the neck either side, and horn-like tufts sweeping
|
|
139
|
+
* out from the top corners. Its "ears" are the tufts, by where they leave the head (the top
|
|
140
|
+
* edge, then the lower) and their tips.
|
|
141
|
+
*/
|
|
142
|
+
const OWL_VIEW = {
|
|
143
|
+
name: "Owl",
|
|
144
|
+
centre: [50.5, 53],
|
|
145
|
+
// Where the beak meets the face, which the face hangs from.
|
|
146
|
+
nose: [47.6, 54.5],
|
|
147
|
+
tilt: -10,
|
|
148
|
+
frame: "",
|
|
149
|
+
head: [
|
|
150
|
+
[28.5, 31], [40, 24], [55, 21.5], [64.5, 22.3], [76, 26.5], [85, 34], [89.5, 44], [90.6, 52], [88.5, 62], [82, 70.5],
|
|
151
|
+
[65, 78], [40, 80], [23.5, 77], [15.5, 70], [11, 60], [10.6, 52], [12, 45.5], [18, 37],
|
|
152
|
+
],
|
|
153
|
+
cheeks: [],
|
|
154
|
+
body: [[23.5, 76], [20.5, 92], [19.2, 130], [130, 130], [130, 118], [100, 95], [90, 80], [82, 70.5]],
|
|
155
|
+
ears: [
|
|
156
|
+
{ inner: [28.5, 31], tip: [6, 27], outer: [12, 45.5] },
|
|
157
|
+
{ inner: [64.5, 22.3], tip: [85.5, 12.8], outer: [85, 34] },
|
|
158
|
+
],
|
|
159
|
+
whiskers: [],
|
|
160
|
+
eyes: [
|
|
161
|
+
{ c: [-15.7, -6.6], rx: 10.8, ry: 9.4 },
|
|
162
|
+
{ c: [17.6, -7.4], rx: 12.4, ry: 10 },
|
|
163
|
+
],
|
|
164
|
+
noseShape: "",
|
|
165
|
+
noseTint: [WHITE, 0.9],
|
|
166
|
+
earTint: [WHITE, 0.2],
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* The dog, traced from its own reference at a twelfth and a half of 1254 px. It's turned
|
|
170
|
+
* three-quarters to the right, so its white muzzle stands out that side, with the far eye
|
|
171
|
+
* smaller beside it. Two big upright ears, very round at the tip, and the far cheek bulging
|
|
172
|
+
* below the left one. Its neck wears a collar.
|
|
173
|
+
*/
|
|
174
|
+
const DOG_VIEW = {
|
|
175
|
+
name: "Dog",
|
|
176
|
+
centre: [52, 50],
|
|
177
|
+
nose: [65.6, 53.8],
|
|
178
|
+
tilt: -12,
|
|
179
|
+
frame: "",
|
|
180
|
+
head: [
|
|
181
|
+
[41, 29.5], [48, 27.5], [55.8, 26.3], [66, 30], [79.3, 39], [84, 44], [87.3, 51.8], [86, 58], [81, 64], [74.6, 68.6],
|
|
182
|
+
[66, 73], [50, 75], [35, 73.5], [30, 72], [26.3, 67.4], [21, 70.2], [16.6, 66], [16, 60], [17.9, 55], [20, 48], [25, 40], [33, 33.5],
|
|
183
|
+
],
|
|
184
|
+
cheeks: [],
|
|
185
|
+
body: [[30, 72], [28.3, 79.7], [16, 100], [13, 130], [86, 130], [82.1, 100], [74.2, 76.6], [74.6, 68.6]],
|
|
186
|
+
ears: [
|
|
187
|
+
{ inner: [41, 29.5], tip: [23, 19.5], outer: [17.9, 55] },
|
|
188
|
+
{ inner: [55.8, 26.3], tip: [70.5, 12.8], outer: [79.3, 39] },
|
|
189
|
+
],
|
|
190
|
+
whiskers: [],
|
|
191
|
+
eyes: [
|
|
192
|
+
{ c: [-20, -8.7], rx: 9.7, ry: 7.8 },
|
|
193
|
+
{ c: [6.5, -9.1], rx: 7.6, ry: 6.2 },
|
|
194
|
+
],
|
|
195
|
+
noseShape: "M-4.8 -1.6Q-4.6 -3.3 0 -3.3Q4.8 -3.3 5 -1.6Q5 1 2.4 2.6Q0.6 3.4 -0.4 3.2Q-2.6 2.8 -4.2 0.8Q-5 -0.4 -4.8 -1.6Z",
|
|
196
|
+
noseTint: [WHITE, 0],
|
|
197
|
+
earTint: [WHITE, 0.22],
|
|
198
|
+
};
|
|
199
|
+
/** The dog's white muzzle, in the face's frame, round the nose and out to the near side. */
|
|
200
|
+
const DOG_MUZZLE = [[-23.7, 6], [-19.7, 3.2], [-7.8, -2.9], [0.55, -3.6], [7.3, -1.3], [9.6, 4.1], [6.7, 11.8], [0.3, 16.8], [-13.4, 16], [-22.1, 11.2]];
|
|
201
|
+
/** Its smug mouth: a short line down from the nose, then a long curve to the near side. */
|
|
202
|
+
const DOG_SMIRK = "M0.6 3.2Q0.6 4.6 -0.4 5Q-3 8.6 -6.8 8Q-10.6 7.4 -12.4 4.6Q-12.8 4 -13.2 3.6M1.2 4.2Q2 7 3.4 7.6Q4.4 8.1 5.4 8";
|
|
203
|
+
/** A fang under the long side of that smirk, shown only when it means it. */
|
|
204
|
+
const DOG_FANG = "M-6.2 8.1L-9.8 7.4Q-9.5 10.8 -8.4 12Q-7.9 12.5 -7.5 11.8Q-6.6 10 -6.2 8.1Z";
|
|
205
|
+
const MUZZLE = "#f5f5f5";
|
|
206
|
+
/** The collar round the dog's neck, and the bone on its tag, in tile units. */
|
|
207
|
+
const COLLAR = "M30.5 73.8Q52 83.4 74.2 73.4L74.2 76.6Q51 91 30.7 77.8Z";
|
|
208
|
+
const BONE = "M-4.6 -1.2H4.6A1.9 1.9 0 1 1 5.6 1.8L5.6 1.8A1.9 1.9 0 1 1 4.6 1.2H-4.6A1.9 1.9 0 1 1 -5.6 -1.8L-5.6 -1.8A1.9 1.9 0 1 1 -4.6 -1.2Z";
|
|
209
|
+
/** Collars come in a few colours; the first is the reference's grey, taken from the fur. */
|
|
210
|
+
export const COLLARS = ["", "#e2364c", "#2f7df6", "#f5b700", "#7a4fd6", "#16b38a"];
|
|
211
|
+
/** A tiger's head, which is the same either side: the left half is traced, the right mirrors it. */
|
|
212
|
+
const mirrored = (points) => points.map(([x, y]) => [100 - x, y]);
|
|
213
|
+
const TIGER_HALF = [[43, 25.5], [30, 27], [21, 33], [16.5, 40], [15.5, 48], [15.5, 56], [18, 63], [23, 69], [28.7, 75.8], [38, 80]];
|
|
214
|
+
const TIGER_WHISKERS = [
|
|
215
|
+
[[37.5, 48.4], [25, 48.8], [13.2, 50.6]],
|
|
216
|
+
[[37, 51.8], [24.5, 53.4], [12.8, 56.2]],
|
|
217
|
+
[[37.5, 55], [25.5, 58.4], [14.4, 63]],
|
|
218
|
+
];
|
|
219
|
+
/**
|
|
220
|
+
* The tiger, traced from its own reference at a twelfth and a half of 1254 px: face on,
|
|
221
|
+
* with big pointed ears, white inside, a ruff of spikes at each cheek, and grey stripes.
|
|
222
|
+
* Its eyes glare in towards a grey nose, over two white whisker pads and a white chin.
|
|
223
|
+
*/
|
|
224
|
+
const TIGER_VIEW = {
|
|
225
|
+
name: "Tiger",
|
|
226
|
+
centre: [50, 50],
|
|
227
|
+
nose: [50, 45.8],
|
|
228
|
+
tilt: 0,
|
|
229
|
+
frame: "",
|
|
230
|
+
head: [[50, 25], ...mirrored(TIGER_HALF), [50, 81], ...[...TIGER_HALF].reverse()],
|
|
231
|
+
cheeks: [],
|
|
232
|
+
body: [[28.7, 74], [21.5, 100], [19.5, 130], [80.5, 130], [78.5, 100], [71.3, 74]],
|
|
233
|
+
ears: [
|
|
234
|
+
{ inner: [43, 25.5], tip: [23, 12], outer: [17.5, 37] },
|
|
235
|
+
{ inner: [57, 25.5], tip: [77, 12], outer: [82.5, 37] },
|
|
236
|
+
],
|
|
237
|
+
whiskers: [...TIGER_WHISKERS, ...TIGER_WHISKERS.map(mirrored)],
|
|
238
|
+
eyes: [
|
|
239
|
+
{ c: [-14.1, -8.7], rx: 9.2, ry: 7.2 },
|
|
240
|
+
{ c: [14.1, -8.7], rx: 9.2, ry: 7.2 },
|
|
241
|
+
],
|
|
242
|
+
noseShape: "M-4.6 -2.4Q0 -2.9 4.6 -2.4Q5.2 -2.1 4.7 -1.4L0.9 2.4Q0 3.2 -0.9 2.4L-4.7 -1.4Q-5.2 -2.1 -4.6 -2.4Z",
|
|
243
|
+
noseTint: [WHITE, 0.33],
|
|
244
|
+
earTint: [WHITE, 1],
|
|
245
|
+
pupilsIn: true,
|
|
246
|
+
};
|
|
247
|
+
/** Drawn on the left in tile units, and again mirrored on the right. */
|
|
248
|
+
const bothSides = (d, paint) => `<path d="${d}" ${paint}/><path d="${d}" transform="translate(100 0) scale(-1 1)" ${paint}/>`;
|
|
249
|
+
/** The tiger's cheek ruffs, stripes, brows, the flicks of fur between its ears, and its chest. */
|
|
250
|
+
const TIGER_SPIKES = "M16.6 39.5L8.9 45.9L15.6 49ZM15.4 51.5L8.4 56.2L16 60Z";
|
|
251
|
+
const TIGER_STRIPES = "M16.5 44.5L21.5 40.8L22.8 46.5L27.8 43.5M19.8 64L24.5 60.5L26 67L32 63.5";
|
|
252
|
+
const TIGER_BROWS = "M32.3 30.4Q35 31.2 36.8 33.6M35.5 29.2Q38 30.6 39.2 33.6";
|
|
253
|
+
const TIGER_FLICKS = "M41.3 19.8Q43.4 21.4 43 24.2";
|
|
254
|
+
const TIGER_CHEST = "M27.9 88.2Q50 76.5 72.1 88.2Q74 95 74.6 102L25.4 102Q26 95 27.9 88.2Z";
|
|
255
|
+
/** A whisker pad, in the face's frame, left of the nose; the right is its mirror. */
|
|
256
|
+
const TIGER_PAD = "M0 3.2Q-2 0.8 -7 0.6Q-14.5 0.6 -17.6 4.6Q-17.4 9.8 -12 11.6Q-5.4 12.8 -1.4 9.6Q0.6 7.2 0 3.2Z";
|
|
257
|
+
/** The animals. Neighbours are as unlike as possible. */
|
|
258
|
+
export const SPECIES = [
|
|
259
|
+
{ name: "Cat", ears: "point", nose: "cat", whiskers: true, fangs: true },
|
|
260
|
+
{ name: "Dog", ears: "point", head: "dog", cheeks: "Smooth", nose: "dog", fangs: true, view: DOG_VIEW, earRound: 8, earBulge: 3.6 },
|
|
261
|
+
{ name: "Owl", ears: "tuft", head: "owl", cheeks: "Smooth", nose: "beak", view: OWL_VIEW },
|
|
262
|
+
{ name: "Bear", ears: "round", nose: "bear", cheeks: "Chubby" },
|
|
263
|
+
{ name: "Tiger", ears: "point", head: "tiger", cheeks: "Smooth", nose: "tiger", whiskers: true, fangs: true, view: TIGER_VIEW, earInset: 0.14 },
|
|
264
|
+
];
|
|
265
|
+
const BEAK = "#f1f1f1";
|
|
266
|
+
export const FUR_STYLES = [{ name: "Smooth" }, { name: "Fluffy" }, { name: "Chubby" }];
|
|
267
|
+
/** Every expression. The first two are the references. */
|
|
268
|
+
export const MOODS = [
|
|
269
|
+
{ name: "Annoyed", lids: [[20, 0], [20, 0]], pupil: "none", mouth: "none", fangs: 1 },
|
|
270
|
+
{ name: "Smug", lids: [[10, 0], [10, 0]], pupil: "hang", mouth: "smirk", fangs: 1 },
|
|
271
|
+
{ name: "Curious", lids: ["open", "open"], pupil: "big", mouth: "w" },
|
|
272
|
+
{ name: "Sleepy", lids: ["closed", "closed"], pupil: "none", mouth: "flat" },
|
|
273
|
+
{ name: "Happy", lids: ["happy", "happy"], pupil: "none", mouth: "smile" },
|
|
274
|
+
{ name: "Shocked", lids: ["open", "open"], pupil: "tiny", mouth: "oh" },
|
|
275
|
+
{ name: "Sly", lids: ["closed", [6, 0.1]], pupil: "hang", mouth: "smirk", fangs: 1 },
|
|
276
|
+
{ name: "Grumpy", lids: [[-7, 0.28], [-7, 0.28]], pupil: "hang", mouth: "frown" },
|
|
277
|
+
{ name: "Hungry", lids: ["open", "open"], pupil: "big", mouth: "tongue" },
|
|
278
|
+
{ name: "Smitten", lids: [[3, 0.12], [3, 0.12]], pupil: "heart", mouth: "w" },
|
|
279
|
+
{ name: "Menacing", lids: [[27, -0.05], [27, -0.05]], pupil: "slit", mouth: "smirk", fangs: 2 },
|
|
280
|
+
{ name: "Dizzy", lids: ["open", "open"], pupil: "cross", mouth: "oh" },
|
|
281
|
+
{ name: "Sassy", lids: [[15, 0], [15, 0]], pupil: "hang", mouth: "open", fangs: 1 },
|
|
282
|
+
];
|
|
283
|
+
/** In the face's frame: the nose at 0 0, the near side to the right. */
|
|
284
|
+
const MOUTHS = {
|
|
285
|
+
none: "",
|
|
286
|
+
smirk: "M-14.9 6.9Q-12.8 11 -9.1 10.7Q-3.5 10.2 -0.3 6.6Q6.5 11.2 13.4 11.2Q19.4 10.8 19.9 6.1",
|
|
287
|
+
w: "M-7 5.6Q-4.6 9.4 -0.4 5.8Q4.4 10 9.4 5.8",
|
|
288
|
+
smile: "M-10 5.8Q-5.5 12 -0.4 7.4Q6 13.6 14 6",
|
|
289
|
+
frown: "M-6.5 10.4Q2 4.6 10.5 10.4",
|
|
290
|
+
flat: "M-4.5 8.6Q2 9.6 8.5 8.4",
|
|
291
|
+
oh: "",
|
|
292
|
+
tongue: "M-7 5.6Q-4.6 9.4 -0.4 5.8Q4.4 10 9.4 5.8",
|
|
293
|
+
// Wide open, off to the near side, with a dip in the lip under the nose.
|
|
294
|
+
open: "M-7.3 8Q-3 4.6 0.4 5.6Q2.4 7.4 5.4 7Q13 6.4 19.6 7.2Q19.8 14.6 15 19.4Q11 22.4 6.7 22.2Q0.6 22 -3.8 17.6Q-7.6 13.4 -7.3 8Z",
|
|
295
|
+
};
|
|
296
|
+
const MOUTH_INSIDE = "#d43369";
|
|
297
|
+
/** The fang hangs from the near side of the mouth, its outer edge the straighter. */
|
|
298
|
+
const FANG = "M9.4 10.7L14.6 11.3Q14.4 15.2 12.8 16.4Q12.1 16.9 11.6 16.1Q10.2 13.6 9.4 10.7Z";
|
|
299
|
+
const FAR_FANG = "M-5.2 10.1L-8.6 10.6Q-8.4 13.6 -7.3 14.4Q-6.8 14.7 -6.5 14.2Q-5.7 12.3 -5.2 10.1Z";
|
|
300
|
+
const OPEN_FANG = "M11.6 8.1L15.2 7.8Q14.8 11.8 13.8 14Q13.3 14.8 12.8 13.9Q12.1 11.4 11.6 8.1Z";
|
|
301
|
+
export const viewOf = (t) => t.pick("view", VIEWS);
|
|
302
|
+
export const speciesOf = (t) => t.pick("animal", SPECIES);
|
|
303
|
+
export const moodOf = (t) => t.pick("mood", MOODS);
|
|
304
|
+
export const poseOf = (t) => t.pick("pose", POSES);
|
|
305
|
+
export const earsOf = (t) => t.pick("ears", EARS);
|
|
306
|
+
export const furStyleOf = (t) => t.pick("fur.style", FUR_STYLES);
|
|
307
|
+
export function coloursOf(t) {
|
|
308
|
+
return {
|
|
309
|
+
tile: t.color("tile") ?? t.pick("tile", TILES),
|
|
310
|
+
fur: t.color("fur") ?? FURS[0],
|
|
311
|
+
eye: t.color("eyes.color") ?? EYE_COLOURS[0],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** An ear, its rounded tip and bulging edges, and its dark inside. */
|
|
315
|
+
function drawEar(inner, tip, outer, centre, style, fur, shade, bulge = 1.7, inset = 0) {
|
|
316
|
+
const middle = lerp(inner, outer, 0.5);
|
|
317
|
+
const centroid = [(inner[0] + tip[0] + outer[0]) / 3, (inner[1] + tip[1] + outer[1]) / 3];
|
|
318
|
+
// Each edge bows outwards, away from the middle of the ear.
|
|
319
|
+
const bowed = (from, to, by) => {
|
|
320
|
+
const mid = lerp(from, to, 0.5);
|
|
321
|
+
const away = distance(mid, centroid) || 1;
|
|
322
|
+
return [mid[0] + ((mid[0] - centroid[0]) / away) * by, mid[1] + ((mid[1] - centroid[1]) / away) * by];
|
|
323
|
+
};
|
|
324
|
+
const a = lerp(tip, inner, Math.min(0.4, style.round / distance(tip, inner)));
|
|
325
|
+
const b = lerp(tip, outer, Math.min(0.4, style.round / distance(tip, outer)));
|
|
326
|
+
const outerEdge = style.notch
|
|
327
|
+
? (() => {
|
|
328
|
+
// A bite out of the outer edge, a little way down from the tip.
|
|
329
|
+
const dent = lerp(lerp(b, outer, 0.42), centroid, 0.22);
|
|
330
|
+
return `L${at(lerp(b, outer, 0.32))}L${at(dent)}L${at(lerp(b, outer, 0.52))}L${at(outer)}`;
|
|
331
|
+
})()
|
|
332
|
+
: `Q${at(bowed(b, outer, bulge + 0.1))} ${at(outer)}`;
|
|
333
|
+
const inside = lerp(middle, centre, 0.3);
|
|
334
|
+
const ear = `<path d="M${at(inner)}Q${at(bowed(inner, a, bulge - 0.1))} ${at(a)}Q${at(tip)} ${at(b)}${outerEdge}L${at(inside)}Z" fill="${fur}"/>`;
|
|
335
|
+
// A very round tip would leave the lining poking out of it, so its point drops lower.
|
|
336
|
+
const liningTip = style.round > 4 ? 0.17 + (style.round - 4) * 0.06 : 0.17;
|
|
337
|
+
const lining = style.folded
|
|
338
|
+
? ""
|
|
339
|
+
: `<path d="${polygon([lerp(tip, centroid, liningTip + inset), lerp(inner, centroid, 0.42 + inset), lerp(outer, centroid, 0.3 + inset)])}" fill="${shade}" stroke="${shade}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
340
|
+
const tufts = style.tuft
|
|
341
|
+
? [-12, 0, 12]
|
|
342
|
+
.map((spread) => {
|
|
343
|
+
const [x, y] = turned([tip[0] - middle[0], tip[1] - middle[1]], spread);
|
|
344
|
+
const length = Math.hypot(x, y);
|
|
345
|
+
return `M${at(tip)}l${point((x / length) * 4.2, (y / length) * 4.2)}`;
|
|
346
|
+
})
|
|
347
|
+
.join("")
|
|
348
|
+
: "";
|
|
349
|
+
return ear + lining + (tufts ? `<path d="${tufts}" stroke="${fur}" stroke-width="0.9" stroke-linecap="round" fill="none"/>` : "");
|
|
350
|
+
}
|
|
351
|
+
/** A bear's ear, placed by the same three points as a cat's. */
|
|
352
|
+
function drawRoundEar(inner, tip, outer, style, fur, shade) {
|
|
353
|
+
const middle = lerp(inner, outer, 0.5);
|
|
354
|
+
const base = distance(inner, outer);
|
|
355
|
+
{
|
|
356
|
+
const r = base * 0.42 * Math.sqrt(style.height) * (style.folded ? 0.8 : 1);
|
|
357
|
+
const [x, y] = lerp(middle, tip, 0.3);
|
|
358
|
+
return (`<circle cx="${round2(x)}" cy="${round2(y)}" r="${round2(r)}" fill="${fur}"/>` +
|
|
359
|
+
(style.folded ? "" : `<circle cx="${round2(x)}" cy="${round2(y)}" r="${round2(r * 0.55)}" fill="${shade}"/>`));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* An owl's tuft, from where it leaves the head to its rounded tip, and a grey crescent inside
|
|
364
|
+
* it, from near the tip back towards the head. A folded one droops, plain.
|
|
365
|
+
*/
|
|
366
|
+
function drawOwlTuft(inner, tip, outer, centre, style, fur, grey) {
|
|
367
|
+
const tuft = drawEar(inner, tip, outer, centre, { ...style, folded: true }, fur, grey, 1.3);
|
|
368
|
+
if (style.folded)
|
|
369
|
+
return tuft;
|
|
370
|
+
const centroid = [(inner[0] + tip[0] + outer[0]) / 3, (inner[1] + tip[1] + outer[1]) / 3];
|
|
371
|
+
const from = lerp(tip, outer, 0.1);
|
|
372
|
+
const to = lerp(outer, inner, 0.52);
|
|
373
|
+
const bend = lerp(lerp(tip, outer, 0.6), centroid, 0.05);
|
|
374
|
+
const back = lerp(bend, inner, 0.5);
|
|
375
|
+
return tuft + `<path d="M${at(from)}Q${at(bend)} ${at(to)}Q${at(back)} ${at(from)}Z" fill="${grey}"/>`;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* What shows of an eye:/**
|
|
379
|
+
* What shows of an eye: the ellipse below the lid, its straight top the lid's edge. Drawn
|
|
380
|
+
* as one shape, so no fur is laid over the white and no seam shows round the hidden half.
|
|
381
|
+
*/
|
|
382
|
+
function eyeShape(lid, rx, ry, inner) {
|
|
383
|
+
if (typeof lid === "string")
|
|
384
|
+
return `M${point(-rx, 0)}A${round2(rx)} ${round2(ry)} 0 1 0 ${point(rx, 0)}A${round2(rx)} ${round2(ry)} 0 1 0 ${point(-rx, 0)}Z`;
|
|
385
|
+
const [slope, depth] = lid;
|
|
386
|
+
// The lid is y = d + m x, lower towards the nose for a positive slope.
|
|
387
|
+
const d = depth * ry;
|
|
388
|
+
const m = Math.tan((slope * Math.PI) / 180) * inner;
|
|
389
|
+
const a = 1 / rx ** 2 + m ** 2 / ry ** 2;
|
|
390
|
+
const b = (2 * m * d) / ry ** 2;
|
|
391
|
+
const root = Math.sqrt(Math.max(0, b ** 2 - 4 * a * (d ** 2 / ry ** 2 - 1)));
|
|
392
|
+
const [x1, x2] = [(-b - root) / (2 * a), (-b + root) / (2 * a)];
|
|
393
|
+
// From the right end round the bottom to the left: the long way when the middle is uncovered.
|
|
394
|
+
return `M${point(x2, d + m * x2)}A${round2(rx)} ${round2(ry)} 0 ${d < 0 ? 1 : 0} 1 ${point(x1, d + m * x1)}Z`;
|
|
395
|
+
}
|
|
396
|
+
function drawPupil(pupil, lid, rx, ry, inner, gaze, ink, tall) {
|
|
397
|
+
if (pupil === "none")
|
|
398
|
+
return "";
|
|
399
|
+
if (pupil === "hang") {
|
|
400
|
+
// Just under the lid, so its top is cut off, looking aside.
|
|
401
|
+
const r = ry * 0.38;
|
|
402
|
+
const x = gaze * rx * 0.42;
|
|
403
|
+
const [slope, depth] = typeof lid === "string" ? [0, -0.3] : lid;
|
|
404
|
+
const y = depth * ry + Math.tan((slope * Math.PI) / 180) * x * inner + r * 0.45;
|
|
405
|
+
return tall
|
|
406
|
+
? `<ellipse cx="${round2(x)}" cy="${round2(y + r * 0.2)}" rx="${round2(r * 0.78)}" ry="${round2(r * 1.25)}" fill="${ink}"/>`
|
|
407
|
+
: `<circle cx="${round2(x)}" cy="${round2(y)}" r="${round2(r)}" fill="${ink}"/>`;
|
|
408
|
+
}
|
|
409
|
+
if (pupil === "big") {
|
|
410
|
+
const x = gaze * rx * 0.22;
|
|
411
|
+
return `<circle cx="${round2(x)}" cy="0" r="${round2(ry * 0.56)}" fill="${ink}"/><circle cx="${round2(x + ry * 0.2)}" cy="${round2(-ry * 0.2)}" r="${round2(ry * 0.16)}" fill="#fff"/>`;
|
|
412
|
+
}
|
|
413
|
+
if (pupil === "tiny")
|
|
414
|
+
return `<circle cx="0" cy="${round2(ry * 0.05)}" r="${round2(ry * 0.2)}" fill="${ink}"/>`;
|
|
415
|
+
if (pupil === "slit")
|
|
416
|
+
return `<ellipse cx="${round2(gaze * rx * 0.3)}" cy="${round2(ry * 0.15)}" rx="${round2(ry * 0.16)}" ry="${round2(ry * 0.8)}" fill="${ink}"/>`;
|
|
417
|
+
if (pupil === "heart") {
|
|
418
|
+
const s = ry * 0.075;
|
|
419
|
+
return `<path d="M0 ${round2(7 * s)}C${point(-10 * s, 0)} ${point(-7 * s, -8 * s)} 0 ${round2(-3.5 * s)}C${point(7 * s, -8 * s)} ${point(10 * s, 0)} 0 ${round2(7 * s)}Z" transform="translate(0 ${round2(ry * 0.15)})" fill="${ink}"/>`;
|
|
420
|
+
}
|
|
421
|
+
const k = ry * 0.42;
|
|
422
|
+
return `<path d="M${point(-k, -k)}L${point(k, k)}M${point(k, -k)}L${point(-k, k)}" stroke="${ink}" stroke-width="${round2(ry * 0.2)}" stroke-linecap="round"/>`;
|
|
423
|
+
}
|
|
424
|
+
/** Each animal's nose, in the face's frame. A bear's sits on a pale muzzle, a dog's on a white one. */
|
|
425
|
+
function drawNose(species, view, fur) {
|
|
426
|
+
if (species.nose === "cat")
|
|
427
|
+
return `<path d="${view.noseShape}" fill="${mix(fur, ...view.noseTint)}"/>`;
|
|
428
|
+
const big = "M-4.2 -1.8Q0 -3.2 4.2 -1.8Q5 -0.6 3.6 1Q1.6 2.8 0 2.8Q-1.6 2.8 -3.6 1Q-5 -0.6 -4.2 -1.8Z";
|
|
429
|
+
if (species.nose === "dog")
|
|
430
|
+
return `<path d="${smooth(DOG_MUZZLE)}" fill="${MUZZLE}"/><path d="${view.noseShape}" fill="${fur}"/>`;
|
|
431
|
+
return `<ellipse cx="2" cy="5" rx="11" ry="8.2" fill="${mix(fur, WHITE, 0.22)}"/><path d="${big}" fill="${fur}"/>`;
|
|
432
|
+
}
|
|
433
|
+
/** An owl's beak: rounded on top, fuller on the far side, coming to a point. Open, it parts. */
|
|
434
|
+
const drawBeak = (open) => open
|
|
435
|
+
? `<path d="M-4.2 0.6Q-3.6 -1.8 -0.4 -1.6Q2.8 -1.4 3.6 0.4Q2 3 -0.6 4.6Q-2.8 3 -4.2 0.6Z" fill="${BEAK}"/>` +
|
|
436
|
+
`<path d="M-2.6 6Q-0.6 5.2 1.4 6Q0.2 8.4 -1 9.6Q-1.8 7.8 -2.6 6Z" fill="${BEAK}"/>`
|
|
437
|
+
: `<path d="M-4.2 0.6Q-3.6 -1.8 -0.4 -1.6Q2.8 -1.4 3.6 0.4Q1.6 4.6 -1.1 8.6Q-1.6 8.9 -1.9 8.2Q-3 4.4 -4.2 0.6Z" fill="${BEAK}"/>`;
|
|
438
|
+
/**
|
|
439
|
+
* Round each of an owl's eyes, in the face's frame: a grey disc, thick at its outer foot and
|
|
440
|
+
* thin over the top, and inside it a black socket a little bigger than the eye.
|
|
441
|
+
*/
|
|
442
|
+
const drawOwlFace = (eyes, fur, grey) => eyes
|
|
443
|
+
.map(({ c, rx, ry }, i) => {
|
|
444
|
+
const side = i ? 1 : -1;
|
|
445
|
+
return ellipse([c[0] + side * rx * 0.12, c[1] + ry * 0.34], rx * 1.44, ry * 1.55, 0, grey) + ellipse([c[0], c[1] + ry * 0.06], rx + 1.8, ry * 0.86 + 1.8, 0, fur);
|
|
446
|
+
})
|
|
447
|
+
.join("");
|
|
448
|
+
/** An owl's chest feathers, two rows of grey crescents, and its wing, outlined in grey, in tile units. */
|
|
449
|
+
const OWL_CHEST = [
|
|
450
|
+
[32.3, 43.3, 79], [43.3, 54.2, 79], [28, 39.5, 88], [39.5, 51, 88],
|
|
451
|
+
]
|
|
452
|
+
.map(([a, b, y]) => `M${a} ${y}Q${round2((a + b) / 2)} ${y + 10} ${b} ${y}Q${round2((a + b) / 2)} ${y + 5} ${a} ${y}Z`)
|
|
453
|
+
.join("");
|
|
454
|
+
const OWL_WING = "M83.2 77.5C72 70.5 62 74 63 85C63.6 92 66.5 96 69.5 101L71.8 101C68.8 95.5 66.6 91 66.2 85C65.6 77.4 73 74 83.2 77.5Z" +
|
|
455
|
+
"M72.6 87.3Q76.5 93 80.5 101L82.4 101Q77.8 92.4 72.6 87.3Z";
|
|
456
|
+
/** The little marks an owl's mood makes round it: lines of joy or surprise, or a sparkle. */
|
|
457
|
+
function drawAccents(mood, fur) {
|
|
458
|
+
const dashes = (colour) => `<path d="${[-40, 0, 40].map((a) => { const [x, y] = turned([0, -1], a); return `M${point(90 + x * 5.5, 14 + y * 5.5)}L${point(90 + x * 9, 14 + y * 9)}`; }).join("")}" stroke="${colour}" stroke-width="1.6" stroke-linecap="round"/>`;
|
|
459
|
+
if (mood.name === "Happy" || mood.name === "Smitten")
|
|
460
|
+
return dashes("#fff");
|
|
461
|
+
if (mood.name === "Curious" || mood.name === "Shocked" || mood.name === "Dizzy")
|
|
462
|
+
return dashes(fur);
|
|
463
|
+
if (mood.name === "Smug" || mood.name === "Sly")
|
|
464
|
+
return `<path d="M7 66Q7.4 69.6 11 70Q7.4 70.4 7 74Q6.6 70.4 3 70Q6.6 69.6 7 66Z" fill="#fff"/>`;
|
|
465
|
+
return "";
|
|
466
|
+
}
|
|
467
|
+
/** Both whisker pads, in the face's frame. */
|
|
468
|
+
const bothPads = (paint) => `<path d="${TIGER_PAD}" ${paint}/><path d="${TIGER_PAD}" transform="scale(-1 1)" ${paint}/>`;
|
|
469
|
+
/**
|
|
470
|
+
* A tiger's nose, the whisker spots on its pads, and its chin: white, with a fang at the
|
|
471
|
+
* corner. When it roars, the chin opens into a mouth with a fang each side.
|
|
472
|
+
*/
|
|
473
|
+
function drawTigerFace(mood, view, fur, clip) {
|
|
474
|
+
const spots = [[-6.9, 4.3], [-8.5, 6.4], [-5, 6.4]].flatMap(([x, y]) => [[x, y], [-x, y]]);
|
|
475
|
+
const nose = spots.map(([x, y]) => `<circle cx="${x}" cy="${y}" r="0.55" fill="${fur}"/>`).join("") + `<path d="${view.noseShape}" fill="${mix(fur, ...view.noseTint)}"/>`;
|
|
476
|
+
const roars = mood.mouth === "open" || mood.mouth === "oh" || mood.mouth === "tongue";
|
|
477
|
+
if (roars) {
|
|
478
|
+
const mouth = "M-13 18.8Q-13 12.8 -7 12.8H7Q13 12.8 13 18.8V24Q13 30 7 30H-7Q-13 30 -13 24Z";
|
|
479
|
+
return (nose +
|
|
480
|
+
`<clipPath id="${clip}"><path d="${mouth}"/></clipPath><path d="${mouth}" fill="${MOUTH_INSIDE}"/>` +
|
|
481
|
+
`<ellipse cx="0" cy="28" rx="10" ry="5.5" fill="${TONGUE}" clip-path="url(#${clip})"/>` +
|
|
482
|
+
`<path d="M-10 12.8L-6.6 12.8L-8.4 17.6ZM10 12.8L6.6 12.8L8.4 17.6Z" fill="#fff"/>`);
|
|
483
|
+
}
|
|
484
|
+
const fang = "M-11.3 13.2L-8.5 13.2L-10.1 17.4Z";
|
|
485
|
+
return (nose +
|
|
486
|
+
`<rect x="-14.5" y="12.8" width="29.1" height="19.9" rx="5.5" fill="${MUZZLE}"/>` +
|
|
487
|
+
(mood.fangs ? `<path d="${fang}" fill="${MUZZLE}" stroke="${fur}" stroke-width="0.7" stroke-linejoin="round"/>` : "") +
|
|
488
|
+
(mood.fangs === 2 ? `<path d="${fang}" transform="scale(-1 1)" fill="${MUZZLE}" stroke="${fur}" stroke-width="0.7" stroke-linejoin="round"/>` : ""));
|
|
489
|
+
}
|
|
490
|
+
/** Cats draws only cats; Animals picks the animal too, with ids of its own so both can be on the page. */
|
|
491
|
+
export function createKitty(name, overrides, animals = false, ids = prefixOf(animals)) {
|
|
492
|
+
const t = calmed(createTraits(name, overrides), CALMING);
|
|
493
|
+
const id = (part) => `${ids}-${part}`;
|
|
494
|
+
const species = animals ? speciesOf(t) : SPECIES[0];
|
|
495
|
+
const view = species.view ?? viewOf(t);
|
|
496
|
+
const { centre } = view;
|
|
497
|
+
const mood = moodOf(t);
|
|
498
|
+
const pose = poseOf(t);
|
|
499
|
+
const style = earsOf(t);
|
|
500
|
+
const fluff = species.cheeks ?? furStyleOf(t).name;
|
|
501
|
+
const { tile, fur, eye: eyeColour } = coloursOf(t);
|
|
502
|
+
const shade = mix(fur, ...view.earTint);
|
|
503
|
+
const grey = mix(fur, WHITE, 0.23);
|
|
504
|
+
const ink = mix(fur, "#000000", 0.7);
|
|
505
|
+
// The head grows about its middle, and the ears and whiskers with it.
|
|
506
|
+
const sx = t.num("head.width", 0.94, 1.06);
|
|
507
|
+
const sy = t.num("head.height", 0.95, 1.05);
|
|
508
|
+
const grow = ([x, y]) => [centre[0] + (x - centre[0]) * sx, centre[1] + (y - centre[1]) * sy];
|
|
509
|
+
const outward = (p, by) => {
|
|
510
|
+
const away = distance(p, centre);
|
|
511
|
+
return [p[0] + ((p[0] - centre[0]) / away) * by, p[1] + ((p[1] - centre[1]) / away) * by];
|
|
512
|
+
};
|
|
513
|
+
const outline = view.head.map((p, i) => grow(fluff === "Chubby" && view.cheeks.includes(i) ? outward(p, 2.6) : p));
|
|
514
|
+
const headPath = smooth(outline);
|
|
515
|
+
const spikes = fluff === "Fluffy"
|
|
516
|
+
? view.cheeks.filter((i) => view.cheeks.includes(i + 1))
|
|
517
|
+
.map((i) => {
|
|
518
|
+
const [a, b] = [outline[i], outline[i + 1]];
|
|
519
|
+
return polygon([lerp(a, b, 0.15), outward(lerp(a, b, 0.5), 3.2 + t.num(`fluff.${i}`, 0, 1.6)), lerp(a, b, 0.85)]);
|
|
520
|
+
})
|
|
521
|
+
.join("")
|
|
522
|
+
: "";
|
|
523
|
+
const earSize = t.num("ear.size", 0.9, 1.1);
|
|
524
|
+
const ears = view.ears.map(({ inner, tip, outer }, i) => {
|
|
525
|
+
const out = i === 0 ? -1 : 1;
|
|
526
|
+
const odd = style.odd && i === 1;
|
|
527
|
+
const middle = lerp(inner, outer, 0.5);
|
|
528
|
+
const flare = style.flare + (odd ? 38 : 0) + t.jitter(`ear.lean${i}`, 4);
|
|
529
|
+
const reach = turned([tip[0] - middle[0], tip[1] - middle[1]], out * flare);
|
|
530
|
+
const height = style.height * earSize * (odd ? 0.85 : 1);
|
|
531
|
+
const tipAt = [middle[0] + reach[0] * height, middle[1] + reach[1] * height];
|
|
532
|
+
const kind = species.ears;
|
|
533
|
+
if (kind === "point") {
|
|
534
|
+
const rounded = { ...style, round: Math.max(style.round, species.earRound ?? 0) };
|
|
535
|
+
return drawEar(grow(inner), grow(tipAt), grow(outer), centre, rounded, fur, shade, species.earBulge, species.earInset);
|
|
536
|
+
}
|
|
537
|
+
if (kind === "tuft")
|
|
538
|
+
return drawOwlTuft(grow(inner), grow(tipAt), grow(outer), centre, style, fur, grey);
|
|
539
|
+
return drawRoundEar(grow(inner), grow(tipAt), grow(outer), style, fur, shade);
|
|
540
|
+
});
|
|
541
|
+
const length = t.num("whisker.length", 0.85, 1.15);
|
|
542
|
+
const keep = t.int("whisker.n", 2, 3);
|
|
543
|
+
const whiskers = !species.whiskers ? "" : view.whiskers.filter((_, i) => keep === 3 || i % 3 !== 1)
|
|
544
|
+
.map(([root, bend, end], i) => {
|
|
545
|
+
const spin = t.jitter(`whisker.${i}`, 5);
|
|
546
|
+
const from = grow(root);
|
|
547
|
+
const reach = (p) => {
|
|
548
|
+
const v = turned([(p[0] - root[0]) * length, (p[1] - root[1]) * length], spin);
|
|
549
|
+
return [from[0] + v[0], from[1] + v[1]];
|
|
550
|
+
};
|
|
551
|
+
return `M${at(from)}Q${at(reach(bend))} ${at(reach(end))}`;
|
|
552
|
+
})
|
|
553
|
+
.join("");
|
|
554
|
+
// The face hangs from the nose, upright, and turns with the head.
|
|
555
|
+
const faceAt = `translate(${point(...grow(view.nose))}) rotate(${view.tilt}) scale(${round2((sx + sy) / 2)})`;
|
|
556
|
+
const face = (inner) => `<g transform="${faceAt}">${inner}</g>`;
|
|
557
|
+
const eyeScale = t.num("eye.size", 0.9, 1.12);
|
|
558
|
+
const gap = t.num("eye.gap", -1.5, 1.5);
|
|
559
|
+
const lift = t.num("eye.y", -1, 1);
|
|
560
|
+
const gaze = t("gaze") < 0.75 ? 1 : -1;
|
|
561
|
+
const slopeShift = t.jitter("lid.slope", 3);
|
|
562
|
+
const eyes = view.eyes.map(({ c, rx: baseX, ry: baseY }, i) => {
|
|
563
|
+
const rx = baseX * eyeScale;
|
|
564
|
+
const ry = baseY * eyeScale;
|
|
565
|
+
const side = i === 0 ? -1 : 1;
|
|
566
|
+
const inner = -side;
|
|
567
|
+
const raw = mood.lids[i];
|
|
568
|
+
const lid = typeof raw === "string" ? raw : [raw[0] + slopeShift, raw[1]];
|
|
569
|
+
const move = `translate(${point(c[0] + side * gap, c[1] + lift)})`;
|
|
570
|
+
if (lid === "closed" || lid === "happy") {
|
|
571
|
+
const bend = lid === "happy" ? -ry * 1.1 : ry * 0.9;
|
|
572
|
+
const end = lid === "happy" ? ry * 0.3 : -ry * 0.1;
|
|
573
|
+
return `<g transform="${move}"><g data-widen=""><path d="M${point(-rx * 0.85, end)}Q0 ${round2(bend)} ${point(rx * 0.85, end)}" fill="none" stroke="${eyeColour}" stroke-width="1.8" stroke-linecap="round"/></g></g>`;
|
|
574
|
+
}
|
|
575
|
+
const shape = eyeShape(lid, rx, ry, inner);
|
|
576
|
+
return (`<g transform="${move}"><g data-widen=""><g data-blink=""><clipPath id="${id(`eye${i}`)}"><path d="${shape}"/></clipPath><path d="${shape}" fill="${eyeColour}"/>` +
|
|
577
|
+
`<g clip-path="url(#${id(`eye${i}`)})"><g data-pupil="">${drawPupil(mood.pupil, lid, rx, ry, inner, view.pupilsIn ? inner : gaze, ink, view.tallPupils)}</g></g></g></g></g>`);
|
|
578
|
+
}).join("");
|
|
579
|
+
// Open, the inside is dark pink, with the tongue filling the bottom of it.
|
|
580
|
+
const open = `<clipPath id="${id("mouth")}"><path d="${MOUTHS.open}"/></clipPath><path d="${MOUTHS.open}" fill="${MOUTH_INSIDE}"/>` +
|
|
581
|
+
`<ellipse cx="9.5" cy="20" rx="15" ry="9.5" transform="rotate(-10 9.5 20)" fill="${TONGUE}" clip-path="url(#${id("mouth")})"/>`;
|
|
582
|
+
// On a bear's or a dog's pale muzzle the mouth is drawn in fur, to show against it.
|
|
583
|
+
const dog = species.head === "dog";
|
|
584
|
+
const mouthInk = species.nose === "bear" || dog ? fur : grey;
|
|
585
|
+
const mouthWidth = dog ? 1.5 : 1.15;
|
|
586
|
+
const mouth = mood.mouth === "open"
|
|
587
|
+
? open
|
|
588
|
+
: mood.mouth === "oh"
|
|
589
|
+
? `<ellipse cx="1.8" cy="9.4" rx="2.4" ry="3" fill="${mix(fur, WHITE, 0.12)}" stroke="${mouthInk}" stroke-width="1"/>`
|
|
590
|
+
: MOUTHS[mood.mouth]
|
|
591
|
+
? `<path d="${MOUTHS[mood.mouth]}" fill="none" stroke="${mouthInk}" stroke-width="${mouthWidth}" stroke-linecap="round" stroke-linejoin="round"/>`
|
|
592
|
+
: "";
|
|
593
|
+
// A dog's tongue lolls out when it's happy, too.
|
|
594
|
+
const tongue = mood.mouth === "tongue" || (dog && (mood.mouth === "smile" || mood.mouth === "w")) ? `<path d="M-0.4 6.4Q2.2 14.6 5.2 7.4Z" fill="${TONGUE}"/>` : "";
|
|
595
|
+
const fangs = !species.fangs ? "" : (mood.fangs ? `<path d="${mood.mouth === "open" ? OPEN_FANG : FANG}" fill="#fff"/>` : "") + (mood.fangs === 2 ? `<path d="${FAR_FANG}" fill="#fff"/>` : "");
|
|
596
|
+
const eyeSpots = view.eyes.map(({ c, rx, ry }, i) => ({
|
|
597
|
+
c: [c[0] + (i ? 1 : -1) * gap, c[1] + lift],
|
|
598
|
+
rx: rx * eyeScale,
|
|
599
|
+
ry: ry * eyeScale,
|
|
600
|
+
}));
|
|
601
|
+
const behindEyes = species.head === "owl" ? drawOwlFace(eyeSpots, fur, grey) : "";
|
|
602
|
+
const owl = species.head === "owl"
|
|
603
|
+
? `<path d="${OWL_CHEST}${OWL_WING}" fill="${grey}"/>${drawAccents(mood, fur)}`
|
|
604
|
+
: "";
|
|
605
|
+
// The dog's collar sits behind its chin, with a white bone tag hanging from it, outlined in fur.
|
|
606
|
+
const collar = dog
|
|
607
|
+
? `<path d="${COLLAR}" fill="${t.pick("collar", COLLARS) || mix(fur, WHITE, 0.24)}"/>` +
|
|
608
|
+
`<path d="${BONE}" transform="translate(58.2 84.6) rotate(-5) scale(1.12)" fill="${MUZZLE}" stroke="${fur}" stroke-width="1.3" paint-order="stroke"/>`
|
|
609
|
+
: "";
|
|
610
|
+
const tiger = species.head === "tiger";
|
|
611
|
+
const stripes = tiger
|
|
612
|
+
? bothSides(TIGER_SPIKES, `fill="${fur}"`) +
|
|
613
|
+
bothSides(TIGER_STRIPES, `fill="none" stroke="${mix(fur, WHITE, 0.38)}" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"`) +
|
|
614
|
+
bothSides(TIGER_BROWS, `fill="none" stroke="#fff" stroke-width="0.8" stroke-linecap="round"`) +
|
|
615
|
+
bothSides(TIGER_FLICKS, `fill="none" stroke="${grey}" stroke-width="0.9" stroke-linecap="round"`) +
|
|
616
|
+
`<path d="${TIGER_CHEST}" fill="${mix(fur, WHITE, 0.2)}"/>`
|
|
617
|
+
: "";
|
|
618
|
+
const dogMouth = mood.mouth === "smirk" ? `<path d="${DOG_SMIRK}" fill="none" stroke="${fur}" stroke-width="${mouthWidth}" stroke-linecap="round" stroke-linejoin="round"/>` : "";
|
|
619
|
+
const scale = pose.scale * t.num("body.size", 0.96, 1.04);
|
|
620
|
+
const mirror = pose.mirrored ? "translate(100 0) scale(-1 1) " : "";
|
|
621
|
+
return {
|
|
622
|
+
defs: `<clipPath id="${id("face")}"><path d="${headPath}"/></clipPath>` +
|
|
623
|
+
(tiger ? `<clipPath id="${id("pads")}"><g transform="${faceAt}">${bothPads("")}</g></clipPath>` : "") +
|
|
624
|
+
`<filter id="${id("grain")}" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="2.2" numOctaves="1" seed="7"/>` +
|
|
625
|
+
`<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 -0.12"/></filter>`,
|
|
626
|
+
tile,
|
|
627
|
+
pose: `${mirror}translate(${point(50 + pose.x, 50 + pose.y)}) rotate(${round2(pose.turn + t.jitter("tilt", 5))}) scale(${round2(scale)}) translate(-50 -50) ${view.frame}`.trim(),
|
|
628
|
+
mirrored: Boolean(pose.mirrored),
|
|
629
|
+
body: `<path d="${polygon(view.body)}" fill="${fur}"/>${collar}<path d="${headPath}" fill="${fur}"/>` +
|
|
630
|
+
owl +
|
|
631
|
+
stripes +
|
|
632
|
+
(spikes ? `<path d="${spikes}" fill="${fur}"/>` : "") +
|
|
633
|
+
(tiger
|
|
634
|
+
? // A tiger's are fine and white, and turn to fur where they cross its white pads, which
|
|
635
|
+
// are drawn with the face; so they go over the face too, below.
|
|
636
|
+
""
|
|
637
|
+
: `<path d="${whiskers}" fill="none" stroke="${view.greyWhiskers ? grey : fur}" stroke-width="1" stroke-linecap="round"/>` +
|
|
638
|
+
// The same whiskers in grey where they cross the face, so they show on the fur.
|
|
639
|
+
`<g clip-path="url(#${id("face")})"><path d="${whiskers}" fill="none" stroke="${grey}" stroke-width="1" stroke-linecap="round"/></g>`),
|
|
640
|
+
ears,
|
|
641
|
+
// An owl's tufts lie over its head, so their crescents show where they meet it.
|
|
642
|
+
earsFront: species.ears === "tuft",
|
|
643
|
+
eyes: face(eyes),
|
|
644
|
+
discs: (behindEyes ? face(behindEyes) : "") + (tiger ? `<g transform="${faceAt}">${bothPads(`fill="${MUZZLE}"`)}</g>` : ""),
|
|
645
|
+
face: (tiger
|
|
646
|
+
? `<path d="${whiskers}" fill="none" stroke="#fff" stroke-width="0.7" stroke-linecap="round"/>` +
|
|
647
|
+
`<g clip-path="url(#${id("pads")})"><path d="${whiskers}" fill="none" stroke="${fur}" stroke-width="0.7" stroke-linecap="round"/></g>`
|
|
648
|
+
: "") +
|
|
649
|
+
face(species.nose === "beak"
|
|
650
|
+
? `<g>${drawBeak(mood.mouth === "open" || mood.mouth === "oh" || mood.mouth === "tongue")}</g>`
|
|
651
|
+
: dog
|
|
652
|
+
? drawNose(species, view, fur) + (dogMouth || `<g transform="scale(-1 1)">${tongue}${mouth}${fangs}</g>`) + (dogMouth && mood.fangs === 2 ? `<path d="${DOG_FANG}" fill="#fff"/>` : "")
|
|
653
|
+
: tiger
|
|
654
|
+
? drawTigerFace(mood, view, fur, id("roar"))
|
|
655
|
+
: drawNose(species, view, fur) + tongue + mouth + fangs),
|
|
656
|
+
blinkDelay: t.num("blink", 0, 4),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
export const TILE_SHAPE = `<rect width="100" height="100" rx="22.22"/>`;
|
|
660
|
+
/** The id prefix each section's drawing uses. */
|
|
661
|
+
export const prefixOf = (animals) => (animals ? "animal" : "kitty");
|
|
662
|
+
/** The tile and its grain, which sit still behind the animal. */
|
|
663
|
+
export const tileMarkup = (fill, ids) => TILE_SHAPE.replace("/>", ` fill="${fill}"/>`) +
|
|
664
|
+
TILE_SHAPE.replace("/>", ` filter="url(#${ids}-grain)" clip-path="url(#${ids}-tile)"/>`);
|
|
665
|
+
/** The animal at rest, for thumbnails. */
|
|
666
|
+
const still = (animals) => (name, overrides) => {
|
|
667
|
+
const cat = createKitty(name, overrides, animals);
|
|
668
|
+
const tile = `${prefixOf(animals)}-tile`;
|
|
669
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs>${cat.defs}<clipPath id="${tile}">${TILE_SHAPE}</clipPath></defs>` +
|
|
670
|
+
tileMarkup(cat.tile, prefixOf(animals)) +
|
|
671
|
+
`<g clip-path="url(#${tile})"><g transform="${cat.pose}">${cat.earsFront ? "" : cat.ears.join("")}${cat.body}${cat.discs}${cat.face}${cat.earsFront ? cat.ears.join("") : ""}${cat.eyes}</g></g></svg>`);
|
|
672
|
+
};
|
|
673
|
+
export const animalSvg = still(true);
|