@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,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auras: a rounded tile of colour, after the reference. Two to four colours melt into each
|
|
3
|
+
* other with no edge anywhere, in a formation of their own: a glow, a horizon, a streak, an
|
|
4
|
+
* arc, a swirl. A fine film grain lies over it all, which also hides any banding in the blend.
|
|
5
|
+
*
|
|
6
|
+
* The colour is a few soft shapes, blurred together as one layer. That layer is drawn once
|
|
7
|
+
* and only ever moved and turned, so the costly blur is never redone as it reacts.
|
|
8
|
+
*
|
|
9
|
+
* A sky is its other type, as the animals are each a type of their own: smooth gradient from
|
|
10
|
+
* top to bottom, after the second reference's times of day and weathers. Both are a rounded tile.
|
|
11
|
+
*/
|
|
12
|
+
import { mix } from "../colors.js";
|
|
13
|
+
import { calmed, keptOf, varianceOf, varianceSet } from "../possibilities.js";
|
|
14
|
+
import { createTraits } from "../traits.js";
|
|
15
|
+
import { CALMING, PALETTE_ORDER } from "./calming.js";
|
|
16
|
+
import { SKIES } from "./skies.js";
|
|
17
|
+
export { SKIES } from "./skies.js";
|
|
18
|
+
/** Each with a reference of its own: a tile of melted colour, or a circle of sky. */
|
|
19
|
+
export const TYPES = ["Aura", "Sky"];
|
|
20
|
+
/**
|
|
21
|
+
* Each as deep, mid, light and accent. The first nine are the first reference's, and the rest
|
|
22
|
+
* after the later ones: Craft's greens on cream, Flowy's sunrise, a fern green with pale rings,
|
|
23
|
+
* and three from the sprayed posters, lime on forest, mint on green and pale blue on cobalt.
|
|
24
|
+
*/
|
|
25
|
+
export const PALETTES = [
|
|
26
|
+
{ label: "Sunrise", colors: ["#e2545b", "#f6a47c", "#fff0c2", "#f7b7a3"] },
|
|
27
|
+
{ label: "Ember", colors: ["#1d1622", "#3a2232", "#ffd28a", "#ff7a1f"] },
|
|
28
|
+
{ label: "Meadow", colors: ["#3d9e62", "#a5d89a", "#f1f9cf", "#6fc08a"] },
|
|
29
|
+
{ label: "Blossom", colors: ["#e8447a", "#f6a3bf", "#fff1f4", "#f27aa3"] },
|
|
30
|
+
{ label: "Aurora", colors: ["#04120f", "#1f9f57", "#e5ffc9", "#34c5c2"] },
|
|
31
|
+
{ label: "Twilight", colors: ["#4757b3", "#a9b0ea", "#eceefe", "#7d86d6"] },
|
|
32
|
+
{ label: "Dream", colors: ["#b98ae6", "#f2b0dc", "#fff4c0", "#d8a6f0"] },
|
|
33
|
+
{ label: "Nebula", colors: ["#0d0b16", "#4b2a5e", "#ffb2df", "#c0549b"] },
|
|
34
|
+
{ label: "Sky", colors: ["#3f86dc", "#8fc3f2", "#e4f8ff", "#b8eff0"] },
|
|
35
|
+
{ label: "Craft", colors: ["#1f6b4e", "#3fd86b", "#ebe8e1", "#9fb3b0"] },
|
|
36
|
+
{ label: "Cobalt", colors: ["#071466", "#0f3fd6", "#3da4ff", "#1a6cf5"] },
|
|
37
|
+
{ label: "Flowy", colors: ["#f04f4b", "#c859b4", "#eef0f6", "#ffb347"] },
|
|
38
|
+
{ label: "Forest", colors: ["#146b10", "#2f8f1a", "#e3f00f", "#f6e21a"] },
|
|
39
|
+
{ label: "Fern", colors: ["#4f9a3a", "#86c06a", "#e9f2df", "#a9d48c"] },
|
|
40
|
+
{ label: "Mint", colors: ["#0f4f2b", "#169660", "#c8e8d8", "#4aae38"] },
|
|
41
|
+
];
|
|
42
|
+
/** Which of a palette's colours each part is, and each can be changed by hand. */
|
|
43
|
+
export const PARTS = ["deep", "mid", "light", "accent"];
|
|
44
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
45
|
+
const oval = (cx, cy, rx, ry, color, turn = 0, opacity) => ({ kind: "oval", cx, cy, rx, ry, turn, color, opacity });
|
|
46
|
+
/** A ring's outline, as a stroke. */
|
|
47
|
+
const ring = (cx, cy, r, width, color, opacity) => ({
|
|
48
|
+
kind: "stroke", d: `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0`, width, color, opacity,
|
|
49
|
+
});
|
|
50
|
+
/** The formations, after the reference's tiles. Neighbours are as unlike as they can be. */
|
|
51
|
+
export const FLOWS = [
|
|
52
|
+
{
|
|
53
|
+
// Warm at the top, melting into a light glow low in the middle.
|
|
54
|
+
name: "Glow",
|
|
55
|
+
base: (c) => c.mid,
|
|
56
|
+
blobs: (c, j) => [
|
|
57
|
+
oval(20 + j("a", 8), 10, 46, 30, c.deep, -15),
|
|
58
|
+
oval(55 + j("b", 8), 58 + j("c", 6), 26, 22, c.light),
|
|
59
|
+
oval(88, 92, 30, 26, c.accent),
|
|
60
|
+
oval(30, 62, 18, 16, c.light, 0, 0.8),
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
// A dark sky over a burning horizon.
|
|
65
|
+
name: "Horizon",
|
|
66
|
+
base: (c) => c.deep,
|
|
67
|
+
blobs: (c, j) => [
|
|
68
|
+
oval(50, 18, 70, 36, c.deep),
|
|
69
|
+
oval(50, 66, 80, 10, c.mid, 0, 0.8),
|
|
70
|
+
oval(50 + j("a", 10), 98, 76, 22, c.accent),
|
|
71
|
+
oval(50 + j("b", 12), 106, 48, 12, c.light),
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
// One bright streak cutting across the dark.
|
|
76
|
+
name: "Streak",
|
|
77
|
+
base: (c) => c.deep,
|
|
78
|
+
blobs: (c, j) => [
|
|
79
|
+
oval(40, 60, 22, 56, c.accent, 35, 0.5),
|
|
80
|
+
oval(52 + j("a", 6), 50, 13, 50, c.light, 35 + j("b", 10)),
|
|
81
|
+
oval(88, 90, 30, 26, c.mid),
|
|
82
|
+
oval(10, 8, 30, 24, c.deep),
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
// A pale arc rising from below, as a sky over a hill.
|
|
87
|
+
name: "Arc",
|
|
88
|
+
base: (c) => c.mid,
|
|
89
|
+
blobs: (c, j) => [
|
|
90
|
+
{ kind: "stroke", d: `M-10 ${round2(78 + j("a", 6))}Q50 ${round2(28 + j("b", 8))} 110 ${round2(78 + j("c", 6))}`, width: 17, color: c.light },
|
|
91
|
+
oval(50, 112, 70, 24, c.deep),
|
|
92
|
+
oval(94, 4, 34, 26, c.accent),
|
|
93
|
+
oval(6, 12, 26, 22, c.deep, 0, 0.7),
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
// A light ribbon curling through.
|
|
98
|
+
name: "Swirl",
|
|
99
|
+
base: (c) => c.mid,
|
|
100
|
+
blobs: (c, j) => [
|
|
101
|
+
oval(80, 20, 40, 30, c.deep, 20),
|
|
102
|
+
{ kind: "stroke", d: `M10 ${round2(88 + j("a", 6))}C30 50 ${round2(70 + j("b", 10))} 70 ${round2(86 + j("c", 6))} 30`, width: 19, color: c.light },
|
|
103
|
+
oval(14, 30, 22, 26, c.accent),
|
|
104
|
+
oval(60, 104, 34, 18, c.deep, 0, 0.8),
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
// Clouds of colour drifting over each other.
|
|
109
|
+
name: "Drift",
|
|
110
|
+
base: (c) => c.mid,
|
|
111
|
+
blobs: (c, j) => [
|
|
112
|
+
oval(24 + j("a", 8), 22 + j("b", 8), 26, 22, c.deep),
|
|
113
|
+
oval(74 + j("c", 8), 30 + j("d", 8), 22, 24, c.light),
|
|
114
|
+
oval(40 + j("e", 8), 70 + j("f", 8), 24, 20, c.accent),
|
|
115
|
+
oval(84, 84, 22, 20, c.deep, 0, 0.8),
|
|
116
|
+
oval(12, 90, 20, 18, c.light, 0, 0.9),
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
// A light far off in the dark, breaking through.
|
|
121
|
+
name: "Break",
|
|
122
|
+
base: (c) => c.deep,
|
|
123
|
+
blobs: (c, j) => [
|
|
124
|
+
oval(96 + j("a", 6), 2, 40, 30, c.mid),
|
|
125
|
+
oval(82, 54, 26, 30, c.accent, -20),
|
|
126
|
+
oval(28 + j("b", 8), 96, 30, 20, c.light),
|
|
127
|
+
oval(20, 26, 30, 30, c.deep),
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
// Two colours meeting along a soft diagonal.
|
|
132
|
+
name: "Split",
|
|
133
|
+
base: (c) => c.light,
|
|
134
|
+
blobs: (c, j) => [
|
|
135
|
+
oval(10, 10, 72, 40, c.deep, -35 + j("a", 10)),
|
|
136
|
+
oval(92, 92, 40, 30, c.accent, -35),
|
|
137
|
+
oval(60, 56, 18, 14, c.light),
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
// A soft ring of colour round a light middle.
|
|
142
|
+
name: "Bloom",
|
|
143
|
+
base: (c) => c.deep,
|
|
144
|
+
blobs: (c, j) => [
|
|
145
|
+
oval(50, 52, 40, 40, c.accent),
|
|
146
|
+
oval(50 + j("a", 6), 52 + j("b", 6), 26, 26, c.mid),
|
|
147
|
+
oval(50 + j("a", 6), 52 + j("b", 6), 12, 12, c.light),
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
// Craft: green breathing through a pale ground, a dark swirl low in it and a grey veil.
|
|
152
|
+
name: "Veil",
|
|
153
|
+
base: (c) => c.light,
|
|
154
|
+
blobs: (c, j) => [
|
|
155
|
+
oval(52 + j("a", 6), 62, 50, 22, c.mid, -8),
|
|
156
|
+
oval(24, 82 + j("b", 4), 20, 12, c.deep, -20),
|
|
157
|
+
oval(58, 76, 14, 9, c.accent),
|
|
158
|
+
oval(74, 58, 18, 9, c.deep, 15, 0.6),
|
|
159
|
+
oval(18, 48, 16, 14, c.mid, 0, 0.9),
|
|
160
|
+
oval(88, 88, 16, 10, c.mid, 0, 0.8),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
// Flowy: a sunrise of rings from below the middle, pale, lavender, magenta, red, orange and gold.
|
|
165
|
+
name: "Rise",
|
|
166
|
+
base: (c) => c.light,
|
|
167
|
+
blobs: (c, j) => {
|
|
168
|
+
const y = 110 + j("a", 6);
|
|
169
|
+
return [
|
|
170
|
+
oval(50, y, 82, 76, mix(c.light, c.mid, 0.45)),
|
|
171
|
+
oval(50, y, 66, 62, c.mid),
|
|
172
|
+
oval(50, y, 52, 48, c.deep),
|
|
173
|
+
oval(50, y, 36, 34, c.accent),
|
|
174
|
+
oval(50, y, 20, 18, mix(c.accent, "#fff6a8", 0.6)),
|
|
175
|
+
];
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
// One Beauty: pale rings curving through a soft green, lighter towards the top.
|
|
180
|
+
name: "Rings",
|
|
181
|
+
base: (c) => c.mid,
|
|
182
|
+
// Soft enough to glow, not so soft that the rings melt away.
|
|
183
|
+
softness: 0.55,
|
|
184
|
+
blobs: (c, j) => [
|
|
185
|
+
oval(30, 10, 50, 34, c.accent, 0, 0.8),
|
|
186
|
+
ring(26 + j("a", 6), 34, 30, 12, c.light),
|
|
187
|
+
ring(66 + j("b", 6), 14, 26, 10, c.light, 0.8),
|
|
188
|
+
oval(80, 96, 44, 26, c.deep),
|
|
189
|
+
oval(10, 96, 26, 18, c.deep, 0, 0.8),
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
/** A blur for the tile's size. Below about 9 the shapes show; above 16 they turn to mud. */
|
|
194
|
+
const SOFTNESS = [9, 16];
|
|
195
|
+
export const auraTraits = (name, overrides) => calmed(createTraits(name, overrides), CALMING);
|
|
196
|
+
/** The name a variant was made from, and how many steps on it is: 0 for the name itself. */
|
|
197
|
+
function named(name) {
|
|
198
|
+
const variant = /^(.*)#(\d+)$/.exec(name);
|
|
199
|
+
return { from: variant ? variant[1] : name, step: variant ? Number(variant[2]) - 1 : 0 };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Which of `count` options a name has under `key`, as the animals each have an animal: its own,
|
|
203
|
+
* from the name, and for a variant, named `name#n`, n steps on from it, keeping to those that
|
|
204
|
+
* Possibilities offers. So a strip of variants shows them all, no two side by side alike. Set by
|
|
205
|
+
* hand, it's that one for every variant.
|
|
206
|
+
*/
|
|
207
|
+
function stepped(name, overrides, key, count, order) {
|
|
208
|
+
const { from, step } = named(name);
|
|
209
|
+
const t = createTraits(from, overrides);
|
|
210
|
+
const value = t(key);
|
|
211
|
+
const own = Math.min(count - 1, Math.floor(value * count));
|
|
212
|
+
if (t.forced(key) !== undefined)
|
|
213
|
+
return own;
|
|
214
|
+
const variance = varianceSet(t);
|
|
215
|
+
const list = CALMING.lists?.[key];
|
|
216
|
+
const plainest = order ?? (Array.isArray(list?.[2]) ? list[2] : Array.from({ length: count }, (_, i) => i));
|
|
217
|
+
// Possibilities keeps the plainest few, so the name's own is one of those, and a step stays among them.
|
|
218
|
+
if (variance === undefined)
|
|
219
|
+
return (own + step) % count;
|
|
220
|
+
const plain = plainest.slice(0, keptOf(count, list?.[1] ?? Math.min(count, 8), variance));
|
|
221
|
+
return plain[(Math.floor(value * plain.length) + step) % plain.length];
|
|
222
|
+
}
|
|
223
|
+
/** A name's type. Its variants take turns, an aura then a sky, so a strip has both. */
|
|
224
|
+
export const typeOf = (name, overrides) => TYPES[stepped(name, overrides, "type", TYPES.length)];
|
|
225
|
+
/** An aura's formation. A strip of variants never repeats one. */
|
|
226
|
+
export const flowOf = (name, overrides) => FLOWS[stepped(name, overrides, "flow", FLOWS.length)];
|
|
227
|
+
/** Where a sky's four colours to pick by hand are taken from, among its stops: its top, a third and two thirds down, and its foot. */
|
|
228
|
+
const SKY_PICKS = [0, 7, 13, 20];
|
|
229
|
+
/** The palettes a type offers: an aura's, or the skies, with their four colours to pick by hand. */
|
|
230
|
+
export const palettesOf = (type) => type === "Sky" ? SKIES.map(({ label, stops }) => ({ label, stops, colors: SKY_PICKS.map((i) => stops[i]) })) : PALETTES;
|
|
231
|
+
/**
|
|
232
|
+
* How far the horizon is moved, from -1 (lower) to 1 (higher). It sits as in the reference
|
|
233
|
+
* until Possibilities is moved, and then strays from it by the name, further the higher it is.
|
|
234
|
+
* Set by hand, it's that.
|
|
235
|
+
*/
|
|
236
|
+
export function horizonOf(t) {
|
|
237
|
+
const set = t.forced("horizon");
|
|
238
|
+
if (set !== undefined)
|
|
239
|
+
return set * 2 - 1;
|
|
240
|
+
const variance = varianceSet(t);
|
|
241
|
+
return variance === undefined ? 0 : (t("horizon") * 2 - 1) * variance * 0.7;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* How grainy it is. The grain's range grows with Possibilities, from nearly smooth to heavy,
|
|
245
|
+
* so each name's grain moves with it. A sky starts smoother, as the reference's are.
|
|
246
|
+
*/
|
|
247
|
+
function grainOf(t, type) {
|
|
248
|
+
const variance = varianceOf(t);
|
|
249
|
+
const [low, calmest, wildest] = type === "Sky" ? [0.03, 0.07, 0.32] : [0.08, 0.2, 0.55];
|
|
250
|
+
return low + t("grain") * (calmest + (wildest - calmest) * variance - low);
|
|
251
|
+
}
|
|
252
|
+
/** Which of its type's palettes it has. Variants step on through them, each type's its own. */
|
|
253
|
+
export const paletteOf = (name, overrides, type) => stepped(name, overrides, "palette", palettesOf(type).length, PALETTE_ORDER[type]);
|
|
254
|
+
/** The palettes Possibilities offers a type, plainest first: all of them until it's moved. */
|
|
255
|
+
export function palettesOffered(t, type) {
|
|
256
|
+
const order = PALETTE_ORDER[type];
|
|
257
|
+
const variance = varianceSet(t);
|
|
258
|
+
return variance === undefined ? order : order.slice(0, keptOf(order.length, 8, variance));
|
|
259
|
+
}
|
|
260
|
+
/** The palette's colours, each of which can be changed by hand. A sky's are its stops, top to bottom. */
|
|
261
|
+
export function coloursOf(name, overrides, t, type) {
|
|
262
|
+
const { colors } = palettesOf(type)[paletteOf(name, overrides, type)];
|
|
263
|
+
return Object.fromEntries(PARTS.map((part, i) => [part, t.color(part) ?? colors[i]]));
|
|
264
|
+
}
|
|
265
|
+
/** A plain face, which is off unless picked: the reference's tiles have none. */
|
|
266
|
+
export const FACES = ["None", "Dots", "Calm", "Smile"];
|
|
267
|
+
export const faceOf = (t) => (t.forced("face") === undefined ? "None" : FACES[Math.min(FACES.length - 1, Math.floor(t("face") * FACES.length))]);
|
|
268
|
+
const FACE_MARKS = {
|
|
269
|
+
None: { eyes: "", mouth: "" },
|
|
270
|
+
Dots: { eyes: `<circle cx="-10" cy="-3" r="3.4"/><circle cx="10" cy="-3" r="3.4"/>`, mouth: `<path d="M-5 7Q0 11 5 7" fill="none"/>` },
|
|
271
|
+
Calm: { eyes: `<path d="M-14 -3Q-10 1 -6 -3M6 -3Q10 1 14 -3" fill="none"/>`, mouth: `<path d="M-4 7Q0 10 4 7" fill="none"/>` },
|
|
272
|
+
Smile: { eyes: `<circle cx="-10" cy="-4" r="3"/><circle cx="10" cy="-4" r="3"/>`, mouth: `<path d="M-9 5Q0 15 9 5" fill="none"/>` },
|
|
273
|
+
};
|
|
274
|
+
function blobMarkup(blob) {
|
|
275
|
+
const opacity = blob.opacity === undefined ? "" : ` opacity="${blob.opacity}"`;
|
|
276
|
+
if (blob.kind === "stroke") {
|
|
277
|
+
return `<path d="${blob.d}" fill="none" stroke="${blob.color}" stroke-width="${blob.width}" stroke-linecap="round"${opacity}/>`;
|
|
278
|
+
}
|
|
279
|
+
const turn = blob.turn ? ` transform="rotate(${round2(blob.turn)} ${round2(blob.cx)} ${round2(blob.cy)})"` : "";
|
|
280
|
+
return `<ellipse cx="${round2(blob.cx)}" cy="${round2(blob.cy)}" rx="${round2(blob.rx)}" ry="${round2(blob.ry)}" fill="${blob.color}"${turn}${opacity}/>`;
|
|
281
|
+
}
|
|
282
|
+
export function createAura(name, overrides) {
|
|
283
|
+
const t = auraTraits(name, overrides);
|
|
284
|
+
const type = typeOf(name, overrides);
|
|
285
|
+
const c = coloursOf(name, overrides, t, type);
|
|
286
|
+
const shared = {
|
|
287
|
+
seed: t.int("grain.seed", 1, 999),
|
|
288
|
+
face: FACE_MARKS[faceOf(t)],
|
|
289
|
+
blinkDelay: round2(t.num("blink", 0, 4)),
|
|
290
|
+
};
|
|
291
|
+
if (type === "Sky") {
|
|
292
|
+
// The reference's own stops, unless its colours are picked by hand, when they are those four.
|
|
293
|
+
const picked = PARTS.some((part) => t.color(part) !== undefined);
|
|
294
|
+
const colours = picked ? PARTS.map((part) => c[part]) : SKIES[paletteOf(name, overrides, type)].stops;
|
|
295
|
+
// Raising the horizon draws every stop up towards the top, and lowering it down towards
|
|
296
|
+
// the foot, smoothly, with the top and the foot left where they are.
|
|
297
|
+
const lift = 2 ** (horizonOf(t) * 1.2);
|
|
298
|
+
const stops = colours
|
|
299
|
+
.map((color, i) => `<stop offset="${round2((i / (colours.length - 1)) ** lift)}" stop-color="${color}"/>`)
|
|
300
|
+
.join("");
|
|
301
|
+
return {
|
|
302
|
+
type,
|
|
303
|
+
colour: (id) => `<defs><linearGradient id="${id}-sky" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="100">${stops}</linearGradient></defs>` +
|
|
304
|
+
`<rect x="-60" y="-60" width="220" height="220" fill="url(#${id}-sky)"/>`,
|
|
305
|
+
blur: 0,
|
|
306
|
+
grain: round2(grainOf(t, type)),
|
|
307
|
+
turn: 0,
|
|
308
|
+
...shared,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const flow = flowOf(name, overrides);
|
|
312
|
+
const j = (key, amount) => t.jitter(`flow.${key}`, amount);
|
|
313
|
+
const base = `<rect x="-60" y="-60" width="220" height="220" fill="${flow.base(c)}"/>`;
|
|
314
|
+
const shapes = flow.blobs(c, j);
|
|
315
|
+
const blur = round2(t.num("softness", ...SOFTNESS) * (flow.softness ?? 1));
|
|
316
|
+
return {
|
|
317
|
+
type,
|
|
318
|
+
colour: () => base + shapes.map(blobMarkup).join(""),
|
|
319
|
+
blur,
|
|
320
|
+
grain: round2(grainOf(t, type)),
|
|
321
|
+
turn: round2(t.jitter("turn", 30)),
|
|
322
|
+
...shared,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** The colour layer as drawn: blurred and turned for an aura, and as it is for a sky. */
|
|
326
|
+
export const colourLayer = (aura, id) => aura.blur
|
|
327
|
+
? `<defs>${blurFilter(`${id}-blur`, aura.blur)}</defs><g filter="url(#${id}-blur)"><g transform="rotate(${aura.turn} 50 50)">${aura.colour(id)}</g></g>`
|
|
328
|
+
: aura.colour(id);
|
|
329
|
+
/** Its corner radius, as a share of its side, the same as the other tiles'. */
|
|
330
|
+
const CORNER = 22.22;
|
|
331
|
+
/** How the face is drawn, over the colour. */
|
|
332
|
+
export const FACE_STYLE = `fill="#ffffff" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.92"`;
|
|
333
|
+
/** The grain as a filter, at `frequency` cycles a unit: fine, grey, and wholly opaque, to be laid on in overlay. */
|
|
334
|
+
export const grainFilter = (id, frequency, seed) => `<filter id="${id}" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">` +
|
|
335
|
+
`<feTurbulence type="fractalNoise" baseFrequency="${frequency}" numOctaves="3" seed="${seed}" stitchTiles="stitch"/>` +
|
|
336
|
+
`<feColorMatrix type="matrix" values="0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0 0 0 0 1"/></filter>`;
|
|
337
|
+
/** The colour layer's blur. Its region reaches well past the tile, so nothing is clipped before it's blurred. */
|
|
338
|
+
export const blurFilter = (id, blur) => `<filter id="${id}" filterUnits="userSpaceOnUse" x="-60" y="-60" width="220" height="220" color-interpolation-filters="sRGB">` +
|
|
339
|
+
`<feGaussianBlur stdDeviation="${blur}"/></filter>`;
|
|
340
|
+
/** The tile at rest, for thumbnails. */
|
|
341
|
+
export function auraSvg(name, overrides) {
|
|
342
|
+
const aura = createAura(name, overrides);
|
|
343
|
+
const face = aura.face.eyes
|
|
344
|
+
? `<g transform="translate(50 52)" ${FACE_STYLE}>${aura.face.eyes}${aura.face.mouth}</g>`
|
|
345
|
+
: "";
|
|
346
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs>` +
|
|
347
|
+
`<clipPath id="aura-tile"><rect width="100" height="100" rx="${CORNER}"/></clipPath>` +
|
|
348
|
+
// About two cycles a unit, so in a thumbnail the grain is near a pixel.
|
|
349
|
+
grainFilter("aura-grain", 2.2, aura.seed) +
|
|
350
|
+
`</defs><g clip-path="url(#aura-tile)">` +
|
|
351
|
+
colourLayer(aura, "aura") +
|
|
352
|
+
`<rect width="100" height="100" filter="url(#aura-grain)" opacity="${aura.grain}" style="mix-blend-mode:overlay"/>` +
|
|
353
|
+
face +
|
|
354
|
+
`</g></svg>`);
|
|
355
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skies: the reference's twelve skies, each a smooth gradient from top to bottom for a time of
|
|
3
|
+
* day or a weather, from a misty morning fading to nothing to a night with a last ember of sun
|
|
4
|
+
* along the horizon. Each is twenty-one colours, top to bottom and evenly spaced, measured down the
|
|
5
|
+
* middle of the reference's own from edge to edge, so the blend is the reference's and not a guess at it.
|
|
6
|
+
* The foot repeats the colour above it, as the last measure caught the circle's edge against the page.
|
|
7
|
+
*/
|
|
8
|
+
export interface Sky {
|
|
9
|
+
label: string;
|
|
10
|
+
stops: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Light and dark alternate, as each variant steps to the next. */
|
|
13
|
+
export declare const SKIES: Sky[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skies: the reference's twelve skies, each a smooth gradient from top to bottom for a time of
|
|
3
|
+
* day or a weather, from a misty morning fading to nothing to a night with a last ember of sun
|
|
4
|
+
* along the horizon. Each is twenty-one colours, top to bottom and evenly spaced, measured down the
|
|
5
|
+
* middle of the reference's own from edge to edge, so the blend is the reference's and not a guess at it.
|
|
6
|
+
* The foot repeats the colour above it, as the last measure caught the circle's edge against the page.
|
|
7
|
+
*/
|
|
8
|
+
/** Light and dark alternate, as each variant steps to the next. */
|
|
9
|
+
export const SKIES = [
|
|
10
|
+
{ label: "Clear", stops: ["#80bae2", "#82bae5", "#87bee7", "#8dc1e9", "#94c4ea", "#9bc7ea", "#9fcaec", "#a8cff0", "#aed1ef", "#b2d4f0", "#b9d7f3", "#bcdaf2", "#c2dcf3", "#c5dff6", "#c9e2f8", "#cbe4f8", "#cee8f9", "#d2e9f9", "#d6edfc", "#daeefc", "#daeefc"] },
|
|
11
|
+
{ label: "Evening", stops: ["#0e1120", "#111f3a", "#1b2946", "#213153", "#283a62", "#2e406e", "#31487e", "#374f7d", "#405783", "#485f88", "#54668a", "#607091", "#687a92", "#758497", "#878c90", "#9b9289", "#ae9984", "#c2a27c", "#d3aa74", "#e7b36a", "#e7b36a"] },
|
|
12
|
+
{ label: "Dawn", stops: ["#8492c4", "#7e96cc", "#849bd1", "#87a3d5", "#89a8d7", "#8eafdc", "#92b5dd", "#96bce3", "#98bfe8", "#9cc4e8", "#9fc6ed", "#a5cae7", "#adcde2", "#b8cfde", "#c2d4d8", "#cbd7d5", "#d5dbcf", "#dddfc9", "#eae4c3", "#f5e7bb", "#f5e7bb"] },
|
|
13
|
+
{ label: "Ember", stops: ["#283544", "#273a4e", "#293b53", "#2a3c54", "#2c3e56", "#2e4058", "#2d4159", "#2f435b", "#31475f", "#334961", "#314b64", "#345068", "#37516c", "#455167", "#5c4f61", "#774c55", "#9d4345", "#c63232", "#e64d2e", "#ef843e", "#ef843e"] },
|
|
14
|
+
{ label: "Pastel", stops: ["#9cb9be", "#afc4cf", "#c0cedb", "#cdd4e4", "#d6d9ec", "#dcdaef", "#dedbee", "#e4d9e9", "#e8d8e3", "#edd5e2", "#f3d3dd", "#f6d1d9", "#facfd6", "#facfd6", "#f8ced2", "#f4c9d2", "#edc2cc", "#e1b5c2", "#d2a5b5", "#bd8fa8", "#bd8fa8"] },
|
|
15
|
+
{ label: "Noon", stops: ["#174e8d", "#1462ae", "#1765b0", "#1768b2", "#116bb7", "#0b6fba", "#0672ba", "#0676be", "#107dc2", "#167fc1", "#2685c7", "#338bcb", "#3c92cf", "#4597d1", "#4d9cd5", "#54a1d9", "#5ba7db", "#63aae0", "#67afe1", "#69b4e4", "#69b4e4"] },
|
|
16
|
+
{ label: "Golden", stops: ["#976e8d", "#a27a96", "#ae86a0", "#b592a6", "#bf9eaf", "#c7a7b2", "#cdacb3", "#d1b2af", "#d8b8ac", "#debba7", "#e4c1a3", "#e9c69e", "#efcb9a", "#f7d096", "#fcd391", "#ffd490", "#fccd87", "#f3bd77", "#eaa863", "#de8f49", "#de8f49"] },
|
|
17
|
+
{ label: "Night", stops: ["#11111a", "#131623", "#151827", "#171a2b", "#181b2c", "#1b1e2f", "#1c1f32", "#1e2134", "#1f2136", "#212338", "#20253b", "#222a3f", "#283047", "#2c374d", "#35415b", "#3b4b64", "#58505f", "#755556", "#955b4f", "#b9663e", "#b9663e"] },
|
|
18
|
+
{ label: "Mist", stops: ["#ccd8ec", "#cddcf2", "#d4e1f2", "#d9e5f3", "#e0eaf6", "#e5ebf7", "#eaeef9", "#eef2fb", "#f0f4fd", "#f6f7fc", "#f7f8fc", "#f9fafe", "#fbfcfe", "#fdfdff", "#fdfdfd", "#fefefe", "#ffffff", "#ffffff", "#ffffff", "#ffffff", "#ffffff"] },
|
|
19
|
+
{ label: "Dusk", stops: ["#3e748e", "#5a88ab", "#7395b2", "#879eb5", "#99a9b9", "#aab3bc", "#babec1", "#c7c6c4", "#d2cdc7", "#d9d2c8", "#ddd3c9", "#e1d3c5", "#e7d1bb", "#eccdb1", "#f0c6a3", "#f7c297", "#fcba86", "#fcb482", "#f6a071", "#f5815e", "#f5815e"] },
|
|
20
|
+
{ label: "Orchid", stops: ["#d068a4", "#d36eac", "#cf75b2", "#cc7db6", "#c882b8", "#c585b9", "#c68abe", "#c58cbf", "#c28cbe", "#c38dbf", "#c28cbe", "#c58fc1", "#c690c1", "#c892c3", "#ca95c3", "#ce99c5", "#d09bc7", "#d49fc9", "#d8a4cb", "#dea9ce", "#dea9ce"] },
|
|
21
|
+
{ label: "Haze", stops: ["#546f82", "#7b94a5", "#8fa1ab", "#97a6ad", "#96a7ae", "#98a9b1", "#9aabb5", "#9eabbb", "#a0adc0", "#a5afc0", "#aeb0bd", "#b7b0b8", "#c1afb1", "#cbb0ad", "#d4b0a7", "#e0b0a2", "#ebb19c", "#f5b296", "#fbb592", "#fdc787", "#fdc787"] },
|
|
22
|
+
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blob colours.
|
|
3
|
+
*
|
|
4
|
+
* Colours are authored in OKLCh, where equal lightness looks equal across hues,
|
|
5
|
+
* then resolved to hex so the SVG also works in server-side rasterizers. The
|
|
6
|
+
* name picks a hue and one of six designed tones, never free lightness or
|
|
7
|
+
* chroma, which is what keeps every blob looking like part of one set. Only a
|
|
8
|
+
* colour picked by hand steps outside that.
|
|
9
|
+
*/
|
|
10
|
+
export interface Palette {
|
|
11
|
+
head: string;
|
|
12
|
+
eye: string;
|
|
13
|
+
}
|
|
14
|
+
/** The tones with their odds, in order, for the control panel. */
|
|
15
|
+
export declare const TONE_BANDS: {
|
|
16
|
+
label: string;
|
|
17
|
+
weight: number;
|
|
18
|
+
}[];
|
|
19
|
+
export declare function createPalette(hue: number, tone: number, picked?: string): Palette;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blob colours.
|
|
3
|
+
*
|
|
4
|
+
* Colours are authored in OKLCh, where equal lightness looks equal across hues,
|
|
5
|
+
* then resolved to hex so the SVG also works in server-side rasterizers. The
|
|
6
|
+
* name picks a hue and one of six designed tones, never free lightness or
|
|
7
|
+
* chroma, which is what keeps every blob looking like part of one set. Only a
|
|
8
|
+
* colour picked by hand steps outside that.
|
|
9
|
+
*/
|
|
10
|
+
import { inkOn } from "../colors.js";
|
|
11
|
+
/** `[upper edge, tone]`, so pale and mid tones dominate and ink stays rare. */
|
|
12
|
+
const TONES = [
|
|
13
|
+
[0.2, { l: 0.86, c: 0.085 }], // pastel
|
|
14
|
+
[0.36, { l: 0.9, c: 0.028 }], // pale neutral
|
|
15
|
+
[0.62, { l: 0.73, c: 0.135 }], // mid
|
|
16
|
+
[0.8, { l: 0.62, c: 0.165 }], // deep
|
|
17
|
+
[0.93, { l: 0.87, c: 0.16 }], // bright
|
|
18
|
+
[1, { l: 0.34, c: 0.035 }], // ink
|
|
19
|
+
];
|
|
20
|
+
/** The tones with their odds, in order, for the control panel. */
|
|
21
|
+
export const TONE_BANDS = [
|
|
22
|
+
{ label: "Pastel", weight: 0.2 },
|
|
23
|
+
{ label: "Pale", weight: 0.16 },
|
|
24
|
+
{ label: "Mid", weight: 0.26 },
|
|
25
|
+
{ label: "Deep", weight: 0.18 },
|
|
26
|
+
{ label: "Bright", weight: 0.13 },
|
|
27
|
+
{ label: "Ink", weight: 0.07 },
|
|
28
|
+
];
|
|
29
|
+
const DARK_SURFACE = { l: 0.145, c: 0, h: 0 };
|
|
30
|
+
const SURFACE_CONTRAST = 1.5;
|
|
31
|
+
const PLATE_CONTRAST = 1.25;
|
|
32
|
+
const EYE_CONTRAST = 4.5;
|
|
33
|
+
const cube = (v) => v * v * v;
|
|
34
|
+
/** OKLCh to linear sRGB. Components may fall outside [0, 1]. */
|
|
35
|
+
function toLinear({ l, c, h }) {
|
|
36
|
+
const radians = (h * Math.PI) / 180;
|
|
37
|
+
const a = c * Math.cos(radians);
|
|
38
|
+
const b = c * Math.sin(radians);
|
|
39
|
+
const lp = cube(l + 0.3963377774 * a + 0.2158037573 * b);
|
|
40
|
+
const mp = cube(l - 0.1055613458 * a - 0.0638541728 * b);
|
|
41
|
+
const sp = cube(l - 0.0894841775 * a - 1.291485548 * b);
|
|
42
|
+
return [
|
|
43
|
+
4.0767416621 * lp - 3.3077115913 * mp + 0.2309699292 * sp,
|
|
44
|
+
-1.2684380046 * lp + 2.6097574011 * mp - 0.3413193965 * sp,
|
|
45
|
+
-0.0041960863 * lp - 0.7034186147 * mp + 1.707614701 * sp,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
const inGamut = (rgb) => rgb.every((v) => v >= -1e-4 && v <= 1 + 1e-4);
|
|
49
|
+
/** Reduces chroma until the colour fits, because clipping channels shifts hue. */
|
|
50
|
+
function toDisplayable(color) {
|
|
51
|
+
let rgb = toLinear(color);
|
|
52
|
+
if (!inGamut(rgb)) {
|
|
53
|
+
let low = 0;
|
|
54
|
+
let high = color.c;
|
|
55
|
+
for (let i = 0; i < 12; i++) {
|
|
56
|
+
const mid = (low + high) / 2;
|
|
57
|
+
if (inGamut(toLinear({ ...color, c: mid })))
|
|
58
|
+
low = mid;
|
|
59
|
+
else
|
|
60
|
+
high = mid;
|
|
61
|
+
}
|
|
62
|
+
rgb = toLinear({ ...color, c: low });
|
|
63
|
+
}
|
|
64
|
+
return rgb.map((v) => Math.min(1, Math.max(0, v)));
|
|
65
|
+
}
|
|
66
|
+
function luminance(color) {
|
|
67
|
+
const [r, g, b] = toDisplayable(color);
|
|
68
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
69
|
+
}
|
|
70
|
+
function contrast(a, b) {
|
|
71
|
+
const x = luminance(a);
|
|
72
|
+
const y = luminance(b);
|
|
73
|
+
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
74
|
+
}
|
|
75
|
+
/** Pushes `fg` lightness away from `bg`, trying its current direction first. */
|
|
76
|
+
function ensureContrast(fg, bg, min) {
|
|
77
|
+
if (contrast(fg, bg) >= min)
|
|
78
|
+
return fg;
|
|
79
|
+
const lean = fg.l >= bg.l ? 1 : -1;
|
|
80
|
+
for (const direction of [lean, -lean]) {
|
|
81
|
+
const probe = { ...fg };
|
|
82
|
+
for (let i = 0; i < 60; i++) {
|
|
83
|
+
probe.l = Math.min(1, Math.max(0, probe.l + direction * 0.02));
|
|
84
|
+
if (contrast(probe, bg) >= min)
|
|
85
|
+
return probe;
|
|
86
|
+
if (probe.l === 0 || probe.l === 1)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const black = { ...fg, l: 0, c: 0 };
|
|
91
|
+
const white = { ...fg, l: 1, c: 0 };
|
|
92
|
+
return contrast(black, bg) >= contrast(white, bg) ? black : white;
|
|
93
|
+
}
|
|
94
|
+
function toHex(color) {
|
|
95
|
+
const channels = toDisplayable(color).map((v) => {
|
|
96
|
+
const encoded = v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
|
|
97
|
+
return Math.round(encoded * 255)
|
|
98
|
+
.toString(16)
|
|
99
|
+
.padStart(2, "0");
|
|
100
|
+
});
|
|
101
|
+
return `#${channels.join("")}`;
|
|
102
|
+
}
|
|
103
|
+
export function createPalette(hue, tone, picked) {
|
|
104
|
+
if (picked)
|
|
105
|
+
return { head: picked, eye: inkOn(picked) };
|
|
106
|
+
const swatch = (TONES.find(([edge]) => tone < edge) ?? TONES[0])[1];
|
|
107
|
+
const plate = { l: 0.965, c: 0.01, h: hue };
|
|
108
|
+
// Visible on a dark page and a pale one, with eyes chosen to read on the body.
|
|
109
|
+
const onDark = ensureContrast({ ...swatch, h: hue }, DARK_SURFACE, SURFACE_CONTRAST);
|
|
110
|
+
const head = ensureContrast(onDark, plate, PLATE_CONTRAST);
|
|
111
|
+
const eye = onDark.l >= 0.5 ? { l: 0.17, c: 0.02, h: hue } : { l: 0.97, c: 0.012, h: hue };
|
|
112
|
+
return { head: toHex(head), eye: toHex(ensureContrast(eye, head, EYE_CONTRAST)) };
|
|
113
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { choice, clearingColor, palette, slider } from "../controls.js";
|
|
2
|
+
import { createTraits } from "../traits.js";
|
|
3
|
+
import { createPalette, TONE_BANDS } from "./color.js";
|
|
4
|
+
import { containerControls } from "../container.js";
|
|
5
|
+
import { possibilitiesSlider } from "../possibilities.js";
|
|
6
|
+
import { NOTES, pickOf, shapeFor, shapesOffered } from "./variance.js";
|
|
7
|
+
/** How many hues are on offer as swatches. Any other is a pick by hand. */
|
|
8
|
+
const HUE_SWATCHES = 11;
|
|
9
|
+
/** The extras each silhouette is built from. */
|
|
10
|
+
const EXTRAS = {
|
|
11
|
+
Round: [["body.n", "Softness", 0, 100, "%"]],
|
|
12
|
+
Organic: [["body.pts", "Lumps", 6, 8]],
|
|
13
|
+
Boxy: [["body.n", "Squareness", 0, 100, "%"], ["body.rot", "Tilt", -20, 20, "°"]],
|
|
14
|
+
Capsule: [["capsule.squat", "Thickness", 0, 100, "%"]],
|
|
15
|
+
Nub: [["nub.n", "Nubs", 1, 2]],
|
|
16
|
+
Cloud: [["cloud.n", "Lobes", 4, 6]],
|
|
17
|
+
Droplet: [["droplet.tip", "Point", 0, 100, "%"]],
|
|
18
|
+
Hexagon: [["poly.round", "Corners", 0, 100, "%"], ["body.rot", "Tilt", -12, 12, "°"]],
|
|
19
|
+
Sun: [["sun.n", "Rays", 6, 9], ["sun.r", "Ray size", 0, 100, "%"]],
|
|
20
|
+
Triangle: [["poly.round", "Corners", 0, 100, "%"], ["body.rot", "Tilt", -5, 5, "°"]],
|
|
21
|
+
Oval: [],
|
|
22
|
+
Pill: [["pill.n", "Squareness", 0, 100, "%"]],
|
|
23
|
+
Spindle: [["spindle.squat", "Thickness", 0, 100, "%"]],
|
|
24
|
+
Diamond: [["diamond.n", "Squareness", 0, 100, "%"]],
|
|
25
|
+
Wide: [],
|
|
26
|
+
};
|
|
27
|
+
/** Lists the shapes the level offers, and the one picked by hand if it's outside them. */
|
|
28
|
+
function shapeControl(t) {
|
|
29
|
+
const shape = shapeFor(t);
|
|
30
|
+
const offered = shapesOffered(t);
|
|
31
|
+
const listed = offered.includes(shape) ? offered : [...offered, shape];
|
|
32
|
+
return {
|
|
33
|
+
type: "choice",
|
|
34
|
+
id: "shape",
|
|
35
|
+
label: "Shape",
|
|
36
|
+
layout: "list",
|
|
37
|
+
options: listed.map((entry) => ({ id: entry.name, label: entry.name, set: pickOf(entry) })),
|
|
38
|
+
selected: shape.name,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export const controls = (name, overrides) => {
|
|
42
|
+
const t = createTraits(name, overrides);
|
|
43
|
+
const shape = shapeFor(t);
|
|
44
|
+
const extras = EXTRAS[shape.name].map(([key, label, min, max, unit]) => Number.isInteger(min) && max - min < 10
|
|
45
|
+
? choice(t, key, label, max - min < 3 ? "segmented" : "list", Array.from({ length: max - min + 1 }, (_, i) => ({ label: String(min + i) })))
|
|
46
|
+
: slider(t, key, label, min, max, unit));
|
|
47
|
+
const hues = Array.from({ length: HUE_SWATCHES }, (_, i) => createPalette(((i + 0.5) * 360) / HUE_SWATCHES, t("tone")).head);
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
title: "Body",
|
|
51
|
+
controls: [
|
|
52
|
+
possibilitiesSlider(t, NOTES),
|
|
53
|
+
shapeControl(t),
|
|
54
|
+
slider(t, "body.r", "Body size", 0, 100, "%"),
|
|
55
|
+
slider(t, "body.ratio", "Stretch", 0, 100, "%"),
|
|
56
|
+
...extras,
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: "Colour",
|
|
61
|
+
// A tone only means something on the name's own colours, so choosing one goes back to them.
|
|
62
|
+
controls: [palette(t, "hue", "Colour", hues), clearingColor(choice(t, "tone", "Tone", "list", TONE_BANDS), "hue")],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: "Eyes",
|
|
66
|
+
controls: [
|
|
67
|
+
slider(t, "eye.rx", "Eye size", 0, 100, "%"),
|
|
68
|
+
slider(t, "eye.ratio", "Eye height", 0, 100, "%"),
|
|
69
|
+
slider(t, "eye.gap", "Gap", 0, 100, "%"),
|
|
70
|
+
slider(t, "eye.lean", "Lean", -100, 100, "%"),
|
|
71
|
+
slider(t, "gaze.x", "Look across", -100, 100, "%"),
|
|
72
|
+
slider(t, "gaze.y", "Look up", -100, 100, "%"),
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{ title: "Container", controls: containerControls(t) },
|
|
76
|
+
];
|
|
77
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Overrides } from "../traits.ts";
|
|
2
|
+
import type { AvatarParts } from "../types.ts";
|
|
3
|
+
export declare function blobParts(name: string, overrides?: Overrides): AvatarParts;
|
|
4
|
+
/** Renders the blob for a name as SVG markup. The same name always gives the same blob. */
|
|
5
|
+
export declare function generateBlob(name: string, overrides?: Overrides): string;
|