@markmiro/hairy 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/README.md +127 -2
- package/dist/chunk-JIET4PY7.js +2641 -0
- package/dist/chunk-JIET4PY7.js.map +1 -0
- package/dist/index.d.ts +1298 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +17 -0
- package/dist/react.js +29 -0
- package/dist/react.js.map +1 -0
- package/package.json +60 -3
|
@@ -0,0 +1,2641 @@
|
|
|
1
|
+
// src/color.ts
|
|
2
|
+
var clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
3
|
+
function rgb(hex) {
|
|
4
|
+
if (!/^#(?:[\da-f]{3}|[\da-f]{6})$/i.test(hex))
|
|
5
|
+
throw new TypeError(`Expected an opaque hex color, got ${hex}`);
|
|
6
|
+
const n = parseInt(hex.slice(1), 16);
|
|
7
|
+
if (hex.length === 4)
|
|
8
|
+
return [(n >> 8 & 15) / 15, (n >> 4 & 15) / 15, (n & 15) / 15];
|
|
9
|
+
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
10
|
+
}
|
|
11
|
+
var linear = (v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
12
|
+
var gamma = (v) => v <= 31308e-7 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
|
|
13
|
+
var lightnessToe = 6 / 29;
|
|
14
|
+
var lightnessOf = (L) => L > lightnessToe ? 1.16 * L - 0.16 : 24389 / 2700 * L ** 3;
|
|
15
|
+
var fromLightness = (v) => v > 0.08 ? (v + 0.16) / 1.16 : Math.cbrt(v * 2700 / 24389);
|
|
16
|
+
function lightness(hex) {
|
|
17
|
+
return lightnessOf(toOklab(hex)[0]);
|
|
18
|
+
}
|
|
19
|
+
function apcaY(hex) {
|
|
20
|
+
const [r, g, b] = rgb(hex);
|
|
21
|
+
let y = 0.2126729 * r ** 2.4 + 0.7151522 * g ** 2.4 + 0.072175 * b ** 2.4;
|
|
22
|
+
if (y < 0.022) y += (0.022 - y) ** 1.414;
|
|
23
|
+
return y;
|
|
24
|
+
}
|
|
25
|
+
function apca(text, background) {
|
|
26
|
+
return apcaFromY(apcaY(text), apcaY(background));
|
|
27
|
+
}
|
|
28
|
+
function apcaFromY(t, b) {
|
|
29
|
+
if (Math.abs(b - t) < 5e-4) return 0;
|
|
30
|
+
const sapc = b > t ? (b ** 0.56 - t ** 0.57) * 1.14 : (b ** 0.65 - t ** 0.62) * 1.14;
|
|
31
|
+
return Math.sign(sapc) * Math.max(0, Math.abs(sapc) - 0.027) * 100;
|
|
32
|
+
}
|
|
33
|
+
function oklabFromRgb(r, g, b) {
|
|
34
|
+
r = linear(r), g = linear(g), b = linear(b);
|
|
35
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
36
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
37
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
38
|
+
return [
|
|
39
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
40
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
41
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function toOklab(hex) {
|
|
45
|
+
return oklabFromRgb(...rgb(hex));
|
|
46
|
+
}
|
|
47
|
+
function fromLab([L, a, b]) {
|
|
48
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
49
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
50
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
51
|
+
return [
|
|
52
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
53
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
54
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
var fits = ([r, g, b]) => r >= -1e-7 && r <= 1 + 1e-7 && g >= -1e-7 && g <= 1 + 1e-7 && b >= -1e-7 && b <= 1 + 1e-7;
|
|
58
|
+
function rgbFromOklab([L, a, b]) {
|
|
59
|
+
L = clamp01(L);
|
|
60
|
+
let channels = fromLab([L, a, b]);
|
|
61
|
+
if (!fits(channels)) {
|
|
62
|
+
let low = 0, high = 1;
|
|
63
|
+
for (let i = 0; i < 20; i++) {
|
|
64
|
+
const mid = (low + high) / 2;
|
|
65
|
+
if (fits(fromLab([L, a * mid, b * mid]))) low = mid;
|
|
66
|
+
else high = mid;
|
|
67
|
+
}
|
|
68
|
+
channels = fromLab([L, a * low, b * low]);
|
|
69
|
+
}
|
|
70
|
+
return [clamp01(gamma(channels[0])), clamp01(gamma(channels[1])), clamp01(gamma(channels[2]))];
|
|
71
|
+
}
|
|
72
|
+
var hexBytes = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
73
|
+
function fromOklab(lab) {
|
|
74
|
+
const [r, g, b] = rgbFromOklab(lab);
|
|
75
|
+
return "#" + hexBytes[Math.round(r * 255)] + hexBytes[Math.round(g * 255)] + hexBytes[Math.round(b * 255)];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/gl.ts
|
|
79
|
+
var hairFloats = 15;
|
|
80
|
+
var rampWidth = 64;
|
|
81
|
+
var rampRows = 4;
|
|
82
|
+
var flakesPerDensity = 2;
|
|
83
|
+
var maxGlitterDensity = 10;
|
|
84
|
+
var largest = 4096;
|
|
85
|
+
var patterns = (
|
|
86
|
+
/* glsl */
|
|
87
|
+
`
|
|
88
|
+
uniform int uPattern; // 0 none, 1 zebra, 2 tiger, 3 dalmatian, 4 leopard, 5 giraffe
|
|
89
|
+
uniform float uPatternScale; // pattern units per CSS px is 1 / (uScale * uPatternScale)
|
|
90
|
+
uniform vec4 uBody; // the element's center, then half its size, in canvas CSS px
|
|
91
|
+
uniform float uScale;
|
|
92
|
+
uniform sampler2D uRamp;
|
|
93
|
+
|
|
94
|
+
vec3 ramp(float row, float u) {
|
|
95
|
+
return textureLod(uRamp, vec2((clamp(u, 0.0, 1.0) * ${rampWidth - 1}.0 + 0.5) / ${rampWidth}.0, (row + 0.5) / ${rampRows}.0), 0.0).rgb;
|
|
96
|
+
}
|
|
97
|
+
/** The fur's shade \`u\`, in the pattern's colors: ground, then markings, then the third color on top. */
|
|
98
|
+
vec3 coatColor(float u, vec2 marks) {
|
|
99
|
+
return mix(mix(ramp(0.0, u), ramp(2.0, u), marks.x), ramp(3.0, u), marks.y);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
uint pcg(uint v) {
|
|
103
|
+
uint s = v * 747796405u + 2891336453u;
|
|
104
|
+
uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u;
|
|
105
|
+
return (w >> 22u) ^ w;
|
|
106
|
+
}
|
|
107
|
+
float rand(ivec2 c, uint k) { return float(pcg(pcg(uint(c.x) ^ k) + uint(c.y))) / 4294967295.0; }
|
|
108
|
+
vec2 rand2(ivec2 c) { return vec2(rand(c, 0x2545f491u), rand(c, 0x9e3779b9u)); }
|
|
109
|
+
// Gradient noise, about -0.7 to 0.7.
|
|
110
|
+
float gnoise(vec2 p) {
|
|
111
|
+
ivec2 i = ivec2(floor(p));
|
|
112
|
+
vec2 f = fract(p);
|
|
113
|
+
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
|
114
|
+
float a = rand(i, 11u) * 6.2831853, b = rand(i + ivec2(1, 0), 11u) * 6.2831853;
|
|
115
|
+
float c = rand(i + ivec2(0, 1), 11u) * 6.2831853, d = rand(i + ivec2(1, 1), 11u) * 6.2831853;
|
|
116
|
+
return mix(
|
|
117
|
+
mix(dot(vec2(cos(a), sin(a)), f), dot(vec2(cos(b), sin(b)), f - vec2(1.0, 0.0)), u.x),
|
|
118
|
+
mix(dot(vec2(cos(c), sin(c)), f - vec2(0.0, 1.0)), dot(vec2(cos(d), sin(d)), f - vec2(1.0)), u.x),
|
|
119
|
+
u.y);
|
|
120
|
+
}
|
|
121
|
+
float fbm(vec2 p) { return 0.6 * gnoise(p) + 0.28 * gnoise(p * 2.07 + 17.3) + 0.12 * gnoise(p * 4.13 - 31.9); }
|
|
122
|
+
vec2 warp(vec2 p, float size, float amount) {
|
|
123
|
+
return p + amount * vec2(fbm(p / size), fbm(p / size + vec2(5.2, 1.3)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Zebra: bold black stripes, as wide as the white between them, running up
|
|
127
|
+
// and down and bending as they go, forking where they get wide.
|
|
128
|
+
vec2 zebra(vec2 q, vec2 n) {
|
|
129
|
+
vec2 w = warp(q, 34.0, 9.0);
|
|
130
|
+
// Leaning back toward the ends, like the stripes on a flank.
|
|
131
|
+
float x = w.x + 0.28 * w.y * clamp(n.x, -1.0, 1.0);
|
|
132
|
+
float a = 6.2831853 * x / 13.0;
|
|
133
|
+
// A second harmonic splits a stripe in two where it's strong: the forks.
|
|
134
|
+
float fork = smoothstep(0.15, 0.45, gnoise(q / 26.0 + 9.1));
|
|
135
|
+
float v = cos(a) + fork * 0.9 * cos(2.0 * a + 3.1);
|
|
136
|
+
float th = 0.05 + 0.35 * gnoise(q / 22.0 - 4.7);
|
|
137
|
+
return vec2(smoothstep(th - 0.12, th + 0.12, v), 0.0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Tiger: narrow, broken black stripes on orange, from the back down, each
|
|
141
|
+
// tapering to a point at its own length, and often doubled.
|
|
142
|
+
vec2 tiger(vec2 q, vec2 n) {
|
|
143
|
+
vec2 w = warp(q, 30.0, 8.0);
|
|
144
|
+
float period = 17.0;
|
|
145
|
+
float x = w.x + 0.22 * w.y + 0.35 * w.y * clamp(n.x, -1.0, 1.0);
|
|
146
|
+
float id = floor(x / period);
|
|
147
|
+
float d = abs(fract(x / period) - 0.5) * period;
|
|
148
|
+
// Each stripe its own width along its length; where it runs out, it breaks.
|
|
149
|
+
float width = 3.6 * (0.45 + 1.1 * gnoise(vec2(id * 3.7, w.y / 11.0)));
|
|
150
|
+
// Narrowing to a point on the way down: some end partway, some run off the bottom.
|
|
151
|
+
float reach = 0.45 + 0.75 * rand(ivec2(int(id), 0), 13u);
|
|
152
|
+
width *= 1.0 - smoothstep(reach - 0.7, reach, n.y + 0.15 * gnoise(q / 14.0));
|
|
153
|
+
float mark = 1.0 - smoothstep(width - 0.7, width + 0.7, d);
|
|
154
|
+
// Doubled: a pale line down the middle of some.
|
|
155
|
+
float split = smoothstep(0.1, 0.3, gnoise(vec2(id * 5.3 + 2.0, w.y / 18.0)));
|
|
156
|
+
mark *= 1.0 - split * (1.0 - smoothstep(0.25 * width, 0.25 * width + 0.6, d));
|
|
157
|
+
return vec2(mark, 0.0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Dalmatian: round black spots, well apart and of every size, with ragged edges.
|
|
161
|
+
vec2 dalmatian(vec2 q, vec2 n) {
|
|
162
|
+
float cell = 19.0;
|
|
163
|
+
vec2 p = q / cell;
|
|
164
|
+
ivec2 i = ivec2(floor(p));
|
|
165
|
+
float edge = 1.1 * fbm(q / 4.0);
|
|
166
|
+
float mark = 0.0;
|
|
167
|
+
for (int y = -1; y <= 1; y++)
|
|
168
|
+
for (int x = -1; x <= 1; x++) {
|
|
169
|
+
ivec2 c = i + ivec2(x, y);
|
|
170
|
+
if (rand(c, 3u) < 0.12) continue;
|
|
171
|
+
vec2 at = (vec2(c) + 0.15 + 0.7 * rand2(c)) * cell;
|
|
172
|
+
float s = rand(c, 5u);
|
|
173
|
+
float r = 2.6 + 5.4 * s * s;
|
|
174
|
+
// Slightly oval, each its own way.
|
|
175
|
+
float t = rand(c, 7u) * 6.2831853;
|
|
176
|
+
vec2 d = q - at;
|
|
177
|
+
d = vec2(dot(d, vec2(cos(t), sin(t))), dot(d, vec2(-sin(t), cos(t)))) * vec2(1.0, 1.0 + 0.3 * rand(c, 9u));
|
|
178
|
+
float dist = length(d) + edge;
|
|
179
|
+
mark = max(mark, 1.0 - smoothstep(r - 0.5, r + 0.5, dist));
|
|
180
|
+
}
|
|
181
|
+
return vec2(mark, 0.0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Leopard: rosettes packed close but each its own shape, a ring of black
|
|
185
|
+
// blotches around a deeper tawny middle, sometimes with a dot of black in it,
|
|
186
|
+
// and a few solid spots. Each rosette fills a Voronoi cell, its ring part way
|
|
187
|
+
// out to the cell's edge, so the rings follow the cells' uneven shapes and
|
|
188
|
+
// leave a band of ground between neighbors.
|
|
189
|
+
vec2 leopard(vec2 q, vec2 n) {
|
|
190
|
+
float cell = 19.0;
|
|
191
|
+
vec2 w = warp(q, 20.0, 3.0) + 0.8 * vec2(gnoise(q / 3.0), gnoise(q / 3.0 + 7.7));
|
|
192
|
+
vec2 p = w / cell;
|
|
193
|
+
ivec2 i = ivec2(floor(p));
|
|
194
|
+
// The nearest rosette's middle, then the distance to its cell's edge.
|
|
195
|
+
vec2 near = vec2(0.0);
|
|
196
|
+
ivec2 id = i;
|
|
197
|
+
float best = 1e9;
|
|
198
|
+
for (int y = -1; y <= 1; y++)
|
|
199
|
+
for (int x = -1; x <= 1; x++) {
|
|
200
|
+
ivec2 c = i + ivec2(x, y);
|
|
201
|
+
vec2 at = vec2(c) + 0.5 + 0.85 * (rand2(c) - 0.5);
|
|
202
|
+
float d = dot(p - at, p - at);
|
|
203
|
+
if (d < best) { best = d; near = at; id = c; }
|
|
204
|
+
}
|
|
205
|
+
float edge = 1e9;
|
|
206
|
+
for (int y = -2; y <= 2; y++)
|
|
207
|
+
for (int x = -2; x <= 2; x++) {
|
|
208
|
+
ivec2 c = i + ivec2(x, y);
|
|
209
|
+
vec2 at = vec2(c) + 0.5 + 0.85 * (rand2(c) - 0.5);
|
|
210
|
+
vec2 to = at - near;
|
|
211
|
+
if (dot(to, to) < 1e-6) continue;
|
|
212
|
+
edge = min(edge, dot(0.5 * (near + at) - p, normalize(to)));
|
|
213
|
+
}
|
|
214
|
+
vec2 from = (p - near) * cell;
|
|
215
|
+
float f = length(from), e = max(0.0, edge) * cell;
|
|
216
|
+
// 0 at the rosette's middle, 1 at the edge it shares with the next.
|
|
217
|
+
float r = f / max(f + e, 1e-3);
|
|
218
|
+
float kind = rand(id, 3u);
|
|
219
|
+
if (kind < 0.16) {
|
|
220
|
+
// A solid spot.
|
|
221
|
+
float size = 0.32 + 0.12 * rand(id, 5u) + 0.05 * gnoise(q / 2.0);
|
|
222
|
+
return vec2(1.0 - smoothstep(size - 0.05, size + 0.05, r), 0.0);
|
|
223
|
+
}
|
|
224
|
+
// The ring, breaking into three to five blotches around the middle.
|
|
225
|
+
vec2 around = from / max(f, 1e-3);
|
|
226
|
+
float g = gnoise(around * 1.35 + rand2(id) * 40.0) + 0.3 * gnoise(q / 2.2);
|
|
227
|
+
float open = smoothstep(-0.3, -0.12, g);
|
|
228
|
+
float inner = 0.54 + 0.1 * gnoise(q / 3.2 + 3.0) + 0.06 * (rand(id, 9u) - 0.5);
|
|
229
|
+
float outer = 0.78 + 0.07 * gnoise(q / 3.2 - 5.0);
|
|
230
|
+
float ring = smoothstep(inner - 0.05, inner + 0.03, r) * (1.0 - smoothstep(outer - 0.03, outer + 0.05, r)) * open;
|
|
231
|
+
float inside = 1.0 - smoothstep(inner - 0.04, inner + 0.04, r);
|
|
232
|
+
// A dot or two of black in the middle of most.
|
|
233
|
+
for (int k = 0; k < 2; k++) {
|
|
234
|
+
uint key = 21u + uint(k) * 4u;
|
|
235
|
+
if (rand(id, key) > (k == 0 ? 0.65 : 0.25)) continue;
|
|
236
|
+
float a = rand(id, key + 1u) * 6.2831853;
|
|
237
|
+
vec2 o = vec2(cos(a), sin(a)) * (f + e) * inner * 0.45 * rand(id, key + 2u);
|
|
238
|
+
float size = 0.9 + 0.7 * rand(id, key + 3u) - 0.2 * float(k);
|
|
239
|
+
float spot = length(from - o) + 0.35 * gnoise(q / 1.8);
|
|
240
|
+
ring = max(ring, 1.0 - smoothstep(size - 0.4, size + 0.4, spot));
|
|
241
|
+
}
|
|
242
|
+
return vec2(ring, inside * (1.0 - ring) * 0.9);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Giraffe: big brown patches, darker toward their middles, in a net of
|
|
246
|
+
// narrow cream lines with jagged edges.
|
|
247
|
+
vec2 giraffe(vec2 q, vec2 n) {
|
|
248
|
+
float cell = 24.0;
|
|
249
|
+
vec2 w = warp(q, 20.0, 5.0) + 1.1 * vec2(gnoise(q / 2.8), gnoise(q / 2.8 + 7.7));
|
|
250
|
+
vec2 p = w / cell;
|
|
251
|
+
ivec2 i = ivec2(floor(p));
|
|
252
|
+
// The nearest patch's middle, then the distance to its edge with the next.
|
|
253
|
+
vec2 near = vec2(0.0);
|
|
254
|
+
float best = 1e9;
|
|
255
|
+
for (int y = -1; y <= 1; y++)
|
|
256
|
+
for (int x = -1; x <= 1; x++) {
|
|
257
|
+
ivec2 c = i + ivec2(x, y);
|
|
258
|
+
vec2 at = vec2(c) + 0.1 + 0.8 * rand2(c);
|
|
259
|
+
float d = dot(p - at, p - at);
|
|
260
|
+
if (d < best) { best = d; near = at; }
|
|
261
|
+
}
|
|
262
|
+
float edge = 1e9;
|
|
263
|
+
for (int y = -2; y <= 2; y++)
|
|
264
|
+
for (int x = -2; x <= 2; x++) {
|
|
265
|
+
ivec2 c = i + ivec2(x, y);
|
|
266
|
+
vec2 at = vec2(c) + 0.1 + 0.8 * rand2(c);
|
|
267
|
+
vec2 to = at - near;
|
|
268
|
+
if (dot(to, to) < 1e-6) continue;
|
|
269
|
+
edge = min(edge, dot(0.5 * (near + at) - p, normalize(to)));
|
|
270
|
+
}
|
|
271
|
+
edge *= cell;
|
|
272
|
+
float line = 1.9 + 0.7 * gnoise(q / 9.0);
|
|
273
|
+
float tile = smoothstep(line - 0.5, line + 0.5, edge);
|
|
274
|
+
return vec2(tile, tile * smoothstep(3.0, 11.0, edge) * 0.75);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The pattern's weights at \`pos\`, in canvas CSS px: markings, then the third color. */
|
|
278
|
+
vec2 patternAt(vec2 pos) {
|
|
279
|
+
if (uPattern == 0) return vec2(0.0);
|
|
280
|
+
vec2 q = (pos - uBody.xy) / max(1e-3, uScale * uPatternScale);
|
|
281
|
+
vec2 n = (pos - uBody.xy) / max(vec2(1.0), uBody.zw);
|
|
282
|
+
if (uPattern == 1) return zebra(q, n);
|
|
283
|
+
if (uPattern == 2) return tiger(q, n);
|
|
284
|
+
if (uPattern == 3) return dalmatian(q, n);
|
|
285
|
+
if (uPattern == 4) return leopard(q, n);
|
|
286
|
+
return giraffe(q, n);
|
|
287
|
+
}
|
|
288
|
+
`
|
|
289
|
+
);
|
|
290
|
+
var common = (
|
|
291
|
+
/* glsl */
|
|
292
|
+
`#version 300 es
|
|
293
|
+
precision highp float;
|
|
294
|
+
precision highp int;
|
|
295
|
+
|
|
296
|
+
layout(location = 0) in vec4 aRootRest; // root x, y; the direction it lies in at rest
|
|
297
|
+
layout(location = 1) in vec4 aLeanStand; // how far it leans out of the outline; the direction it stands in
|
|
298
|
+
layout(location = 2) in vec4 aHair; // length, curl, sway phase, rim (1 on the outline)
|
|
299
|
+
layout(location = 3) in vec3 aLook; // shade (0 to 1), kind (0 underfur, 1 guard hair, 2 sheen), seed (0 to 1)
|
|
300
|
+
layout(location = 4) in vec2 aBend; // how far the pointer has brushed it
|
|
301
|
+
|
|
302
|
+
uniform vec2 uView;
|
|
303
|
+
uniform float uDpr;
|
|
304
|
+
uniform float uLift, uSway, uTime, uHovering, uNear;
|
|
305
|
+
uniform vec2 uPointer;
|
|
306
|
+
uniform float uBase, uHoverTop, uHoverStretch, uClicked, uGrown, uDroop;
|
|
307
|
+
uniform float uThickness, uTaper, uSoftness;
|
|
308
|
+
uniform int uType;
|
|
309
|
+
uniform float uTypeAmount, uTightness;
|
|
310
|
+
uniform int uTip;
|
|
311
|
+
uniform float uMaterialAmount;
|
|
312
|
+
uniform float uGlitterDensity, uGlitterSize, uGlitterVariation;
|
|
313
|
+
${patterns}
|
|
314
|
+
const float TAU = 6.2831853;
|
|
315
|
+
|
|
316
|
+
float hash(float x) { return fract(sin(x * 78.233 + 0.31) * 43758.5453); }
|
|
317
|
+
float vnoise(float x) {
|
|
318
|
+
float i = floor(x), f = fract(x);
|
|
319
|
+
return mix(hash(i), hash(i + 1.0), f * f * (3.0 - 2.0 * f)) * 2.0 - 1.0;
|
|
320
|
+
}
|
|
321
|
+
/** The pattern where this hair grows, from a point a little off its root, so the markings' edges are ragged hair by hair. */
|
|
322
|
+
vec2 hairMarks() {
|
|
323
|
+
if (uPattern == 0) return vec2(0.0);
|
|
324
|
+
vec2 off = vec2(hash(aLook.z * 13.7) - 0.5, hash(aLook.z * 29.1) - 0.5) * 2.4 * uScale;
|
|
325
|
+
return patternAt(aRootRest.xy + off);
|
|
326
|
+
}
|
|
327
|
+
vec2 clip(vec2 p) { return vec2(p.x / uView.x * 2.0 - 1.0, 1.0 - p.y / uView.y * 2.0); }
|
|
328
|
+
|
|
329
|
+
// This hair's curve this frame: a cubic from its root, and its length and lift.
|
|
330
|
+
vec2 P0, P1, P2, P3;
|
|
331
|
+
float LEN, LIFT;
|
|
332
|
+
|
|
333
|
+
void shape() {
|
|
334
|
+
vec2 root = aRootRest.xy, rest = aRootRest.zw;
|
|
335
|
+
float rim = aHair.w;
|
|
336
|
+
// Below 0 (a bounce swinging past rest), the hairs press flatter than they lie.
|
|
337
|
+
float l = max(-0.5, uLift);
|
|
338
|
+
// A little higher right under the pointer.
|
|
339
|
+
if (uHovering > 0.5) {
|
|
340
|
+
vec2 d = root - uPointer;
|
|
341
|
+
l += 0.12 * exp(-dot(d, d) / uNear);
|
|
342
|
+
}
|
|
343
|
+
float L = min(1.0, l);
|
|
344
|
+
float a = uSway * sin(uTime * 2.2 + aHair.z);
|
|
345
|
+
vec2 dir = (rest + aLeanStand.xy) * (1.0 - L) + aLeanStand.zw * L + aBend + vec2(-rest.y, rest.x) * a;
|
|
346
|
+
if (uType == 3) {
|
|
347
|
+
// Frizz: hairs fly off from the combed direction, less as they stand.
|
|
348
|
+
float turn = (hash(aLook.z * 7.13) - 0.5) * 1.2 * uTypeAmount * (1.0 - 0.5 * L);
|
|
349
|
+
float c = cos(turn), s = sin(turn);
|
|
350
|
+
dir = vec2(dir.x * c - dir.y * s, dir.x * s + dir.y * c);
|
|
351
|
+
}
|
|
352
|
+
float dl = length(dir);
|
|
353
|
+
dir = dl > 1e-5 ? dir / dl : vec2(0.0, 1.0);
|
|
354
|
+
// Stretch: hovering stretches hairs as they rise from rest to the hover
|
|
355
|
+
// lift, a click further along its own rise and fall.
|
|
356
|
+
float hovered = uHoverTop - uBase > 1e-3 ? (l - uBase) / (uHoverTop - uBase) : 0.0;
|
|
357
|
+
float stretch = uHoverStretch * clamp(hovered, 0.0, 1.3) + uClicked;
|
|
358
|
+
// Outline hairs reach out as they stand; middle ones point at you, so they look shorter.
|
|
359
|
+
float len = aHair.x * uGrown * (1.0 + stretch) * (1.0 + rim * 0.3 * l - (1.0 - rim) * 0.35 * L);
|
|
360
|
+
float curl = aHair.y * (1.0 - 0.5 * L);
|
|
361
|
+
// Hairs droop under their weight until they stand.
|
|
362
|
+
float droop = 0.2 * uDroop * (1.0 - L) * len;
|
|
363
|
+
vec2 side = vec2(-dir.y, dir.x);
|
|
364
|
+
P0 = root;
|
|
365
|
+
P1 = root + dir * len * 0.34;
|
|
366
|
+
P2 = root + dir * len * 0.68 + side * curl * len * 0.3 + vec2(0.0, droop * 0.4);
|
|
367
|
+
P3 = root + dir * len * 0.92 + side * curl * len * 0.55 + vec2(0.0, droop);
|
|
368
|
+
LEN = len;
|
|
369
|
+
LIFT = L;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
vec2 bezier(float t) {
|
|
373
|
+
float u = 1.0 - t;
|
|
374
|
+
return u * u * u * P0 + 3.0 * u * u * t * P1 + 3.0 * u * t * t * P2 + t * t * t * P3;
|
|
375
|
+
}
|
|
376
|
+
vec2 slope(float t) {
|
|
377
|
+
float u = 1.0 - t;
|
|
378
|
+
return 3.0 * u * u * (P1 - P0) + 6.0 * u * t * (P2 - P1) + 3.0 * t * t * (P3 - P2);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// The point \`t\` of the way along the hair, with its type's waves, coils or frizz.
|
|
382
|
+
vec2 along(float t) {
|
|
383
|
+
vec2 p = bezier(t);
|
|
384
|
+
if (uType == 0 || uTypeAmount <= 0.0) return p;
|
|
385
|
+
vec2 T = slope(t);
|
|
386
|
+
float tl = length(T);
|
|
387
|
+
T = tl > 1e-5 ? T / tl : vec2(1.0, 0.0);
|
|
388
|
+
vec2 N = vec2(-T.y, T.x);
|
|
389
|
+
float seed = aLook.z;
|
|
390
|
+
if (uType == 1) {
|
|
391
|
+
// Wavy: ripples side to side, growing in from the root.
|
|
392
|
+
float A = 0.13 * LEN * uTypeAmount * smoothstep(0.0, 0.3, t);
|
|
393
|
+
return p + N * A * sin(TAU * (1.4 * uTightness * t + seed));
|
|
394
|
+
}
|
|
395
|
+
if (uType == 2) {
|
|
396
|
+
// Curly: a coil seen from the side, looping back on itself, turning either way.
|
|
397
|
+
float R = 0.14 * LEN * uTypeAmount * smoothstep(0.0, 0.12, t);
|
|
398
|
+
float th = TAU * 2.5 * uTightness * t;
|
|
399
|
+
float spin = seed > 0.5 ? 1.0 : -1.0;
|
|
400
|
+
return p + R * (N * spin * sin(th) + T * (cos(th) - 1.0));
|
|
401
|
+
}
|
|
402
|
+
// Frizzy: kinks at random, broad bends with fine zigzags on them, more toward the tip.
|
|
403
|
+
float A = 0.2 * LEN * uTypeAmount * pow(t, 0.7);
|
|
404
|
+
float f = 7.0 * uTightness;
|
|
405
|
+
float kink = 0.6 * vnoise(t * f + seed * 97.0) + 0.4 * vnoise(t * f * 3.1 + seed * 31.0);
|
|
406
|
+
return p + N * A * kink + T * A * 0.35 * vnoise(t * f * 1.3 + seed * 53.0);
|
|
407
|
+
}
|
|
408
|
+
`
|
|
409
|
+
);
|
|
410
|
+
var hairVertex = (
|
|
411
|
+
/* glsl */
|
|
412
|
+
`${common}
|
|
413
|
+
uniform int uSegments;
|
|
414
|
+
out vec4 vColor;
|
|
415
|
+
out float vAcross;
|
|
416
|
+
out float vHalf;
|
|
417
|
+
out float vAlong;
|
|
418
|
+
flat out float vRound;
|
|
419
|
+
flat out float vFade;
|
|
420
|
+
|
|
421
|
+
void main() {
|
|
422
|
+
shape();
|
|
423
|
+
// Two vertices per step along the hair, then two past the tip for its end.
|
|
424
|
+
int k = gl_VertexID / 2;
|
|
425
|
+
float side = gl_VertexID % 2 == 0 ? -1.0 : 1.0;
|
|
426
|
+
bool end = k > uSegments;
|
|
427
|
+
float t = end ? 1.0 : float(k) / float(uSegments);
|
|
428
|
+
vec2 p = along(t);
|
|
429
|
+
float e = 0.5 / float(uSegments);
|
|
430
|
+
vec2 T = along(min(1.0, t + e)) - along(max(0.0, t - e));
|
|
431
|
+
float tl = length(T);
|
|
432
|
+
T = tl > 1e-6 ? T / tl : vec2(1.0, 0.0);
|
|
433
|
+
vec2 N = vec2(-T.y, T.x);
|
|
434
|
+
|
|
435
|
+
float kind = aLook.y;
|
|
436
|
+
bool sheen = kind > 1.5;
|
|
437
|
+
// Narrowing from root to tip by the taper; a sharp end narrows on to a point.
|
|
438
|
+
float width = sheen ? 0.7 * uScale * uThickness : (kind > 0.5 ? 1.1 : 0.95) * uScale * uThickness * mix(1.0, uTaper, t);
|
|
439
|
+
if (!sheen && uTip == 1) width *= clamp((1.0 - t) / 0.35, 0.0, 1.0);
|
|
440
|
+
// Half the width in device px, and the strip's half width with room for the
|
|
441
|
+
// soft edge: a pixel, plus the softness, centered on the hair's edge.
|
|
442
|
+
float hw = 0.5 * width * uDpr;
|
|
443
|
+
float fade = 1.0 + uSoftness * uScale * uDpr;
|
|
444
|
+
float g = max(hw, 0.5) + 0.5 * fade + 0.5;
|
|
445
|
+
bool rounded = sheen || uTip == 0;
|
|
446
|
+
vec2 pos = p + N * side * g / uDpr;
|
|
447
|
+
float to = -(1.0 - t) * LEN * uDpr;
|
|
448
|
+
if (end) {
|
|
449
|
+
// A round end reaches half the width past the tip; the others just the soft edge.
|
|
450
|
+
float past = rounded ? g : 0.5 * fade + 0.5;
|
|
451
|
+
pos += T * past / uDpr;
|
|
452
|
+
to = past;
|
|
453
|
+
}
|
|
454
|
+
gl_Position = vec4(clip(pos), 0.0, 1.0);
|
|
455
|
+
vAcross = side * g;
|
|
456
|
+
vHalf = hw;
|
|
457
|
+
vAlong = to;
|
|
458
|
+
vRound = rounded ? 1.0 : 0.0;
|
|
459
|
+
vFade = fade;
|
|
460
|
+
|
|
461
|
+
// Darker at the root, lighter and a little see-through toward the tip.
|
|
462
|
+
float shade = aLook.x;
|
|
463
|
+
vec2 marks = hairMarks();
|
|
464
|
+
if (sheen) {
|
|
465
|
+
// On markings, the shine is their own highlight.
|
|
466
|
+
vec3 gloss = mix(mix(ramp(1.0, shade), ramp(2.0, 1.0), marks.x), ramp(3.0, 1.0), marks.y);
|
|
467
|
+
vColor = vec4(gloss, 0.35);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
float m = smoothstep(0.3, 0.7, t);
|
|
471
|
+
float u = shade + mix(-1.0, 1.0, m) / 23.0;
|
|
472
|
+
vColor = vec4(coatColor(u, marks), mix(1.0, 0.75, m));
|
|
473
|
+
}
|
|
474
|
+
`
|
|
475
|
+
);
|
|
476
|
+
var hairFragment = (
|
|
477
|
+
/* glsl */
|
|
478
|
+
`#version 300 es
|
|
479
|
+
precision highp float;
|
|
480
|
+
in vec4 vColor;
|
|
481
|
+
in float vAcross;
|
|
482
|
+
in float vHalf;
|
|
483
|
+
in float vAlong;
|
|
484
|
+
flat in float vRound;
|
|
485
|
+
flat in float vFade;
|
|
486
|
+
out vec4 outColor;
|
|
487
|
+
|
|
488
|
+
void main() {
|
|
489
|
+
// How much of this pixel the hair covers: an edge fading over vFade px
|
|
490
|
+
// (one, unless softened), a hw circle past a round tip, and hairs thinner
|
|
491
|
+
// than a pixel fade instead.
|
|
492
|
+
float hw = max(vHalf, 0.5);
|
|
493
|
+
float d = vRound > 0.5 && vAlong > 0.0 ? length(vec2(vAlong, vAcross)) : abs(vAcross);
|
|
494
|
+
float cover = clamp((hw + 0.5 * vFade - d) / vFade, 0.0, 1.0);
|
|
495
|
+
if (vRound < 0.5) cover *= clamp((0.5 * vFade - vAlong) / vFade, 0.0, 1.0);
|
|
496
|
+
float a = vColor.a * cover * min(1.0, vHalf * 2.0);
|
|
497
|
+
if (a < 0.002) discard;
|
|
498
|
+
outColor = vec4(vColor.rgb * a, a);
|
|
499
|
+
}
|
|
500
|
+
`
|
|
501
|
+
);
|
|
502
|
+
var flakeVertex = (
|
|
503
|
+
/* glsl */
|
|
504
|
+
`${common}
|
|
505
|
+
out vec2 vUV;
|
|
506
|
+
out float vBright;
|
|
507
|
+
out float vFade;
|
|
508
|
+
out vec3 vColor;
|
|
509
|
+
|
|
510
|
+
void main() {
|
|
511
|
+
shape();
|
|
512
|
+
// Six vertices per flake: a square around a point on the hair.
|
|
513
|
+
int k = gl_VertexID / 6, c = gl_VertexID % 6;
|
|
514
|
+
float s = aLook.z * 91.7 + float(k) * 13.1;
|
|
515
|
+
// Each candidate shows by its own chance, so more density adds flakes and
|
|
516
|
+
// keeps the ones already there; the last candidate fades in by its share.
|
|
517
|
+
float keep = clamp(uGlitterDensity * ${flakesPerDensity}.0 - float(k), 0.0, 1.0);
|
|
518
|
+
bool show = aLook.y < 1.5 && hash(s + 1.7) < 0.03 * keep;
|
|
519
|
+
float t = mix(0.25, 1.0, hash(s));
|
|
520
|
+
vec2 p = along(t);
|
|
521
|
+
vec2 T = along(min(1.0, t + 0.04)) - along(max(0.0, t - 0.04));
|
|
522
|
+
// A flake flashes when its hair turns it toward the light, so it twinkles as the fur moves.
|
|
523
|
+
float glint = 0.5 + 0.5 * cos(atan(T.y, T.x) * 3.0 + hash(s + 3.1) * TAU + uTime * 1.3);
|
|
524
|
+
float flash = pow(glint, 12.0);
|
|
525
|
+
// Its own size: up to 2^variation times bigger or smaller, evenly spread between.
|
|
526
|
+
float own = exp2(uGlitterVariation * (2.0 * hash(s + 5.3) - 1.0));
|
|
527
|
+
float size = show ? (0.5 + 1.4 * flash) * uScale * (0.7 + 0.3 * uThickness) * uGlitterSize * own : 0.0;
|
|
528
|
+
// The amount sets how bright the flashes are.
|
|
529
|
+
float bright = flash * uMaterialAmount;
|
|
530
|
+
vec2 corner = vec2(c == 1 || c == 2 || c == 4 ? 1.0 : -1.0, c == 2 || c == 4 || c == 5 ? 1.0 : -1.0);
|
|
531
|
+
// The square reaches past the flake for its rays.
|
|
532
|
+
float reach = 3.0;
|
|
533
|
+
gl_Position = vec4(clip(p + corner * size * reach), 0.0, 1.0);
|
|
534
|
+
vUV = corner * reach;
|
|
535
|
+
vBright = bright;
|
|
536
|
+
vFade = min(1.0, uMaterialAmount);
|
|
537
|
+
// Tinted like the fur's sheen; the brightest flash nearly white.
|
|
538
|
+
vColor = mix(ramp(1.0, 1.0), vec3(1.0), clamp(0.25 + 0.6 * bright, 0.0, 1.0));
|
|
539
|
+
}
|
|
540
|
+
`
|
|
541
|
+
);
|
|
542
|
+
var flakeFragment = (
|
|
543
|
+
/* glsl */
|
|
544
|
+
`#version 300 es
|
|
545
|
+
precision highp float;
|
|
546
|
+
in vec2 vUV;
|
|
547
|
+
in float vBright;
|
|
548
|
+
in float vFade;
|
|
549
|
+
in vec3 vColor;
|
|
550
|
+
out vec4 outColor;
|
|
551
|
+
|
|
552
|
+
void main() {
|
|
553
|
+
float core = exp(-dot(vUV, vUV) * 2.0);
|
|
554
|
+
float rays = (exp(-abs(vUV.x) * 2.5 - vUV.y * vUV.y * 0.4) + exp(-abs(vUV.y) * 2.5 - vUV.x * vUV.x * 0.4)) * vBright;
|
|
555
|
+
float a = clamp(core * (0.55 * vFade + 0.45 * min(vBright, 1.0)) + rays * 0.7, 0.0, 1.0);
|
|
556
|
+
if (a < 0.003) discard;
|
|
557
|
+
// A little lighter than covering, so sparkles brighten what's under them.
|
|
558
|
+
outColor = vec4(vColor * a, a * 0.8);
|
|
559
|
+
}
|
|
560
|
+
`
|
|
561
|
+
);
|
|
562
|
+
var coatVertex = (
|
|
563
|
+
/* glsl */
|
|
564
|
+
`#version 300 es
|
|
565
|
+
precision highp float;
|
|
566
|
+
uniform vec2 uView;
|
|
567
|
+
out vec2 vPos;
|
|
568
|
+
void main() {
|
|
569
|
+
vec2 c = vec2(gl_VertexID % 2 == 0 ? 0.0 : 1.0, gl_VertexID < 2 ? 0.0 : 1.0);
|
|
570
|
+
vPos = c * uView;
|
|
571
|
+
gl_Position = vec4(c.x * 2.0 - 1.0, 1.0 - c.y * 2.0, 0.0, 1.0);
|
|
572
|
+
}
|
|
573
|
+
`
|
|
574
|
+
);
|
|
575
|
+
var coatFragment = (
|
|
576
|
+
/* glsl */
|
|
577
|
+
`#version 300 es
|
|
578
|
+
precision highp float;
|
|
579
|
+
precision highp int;
|
|
580
|
+
uniform vec4 uBox;
|
|
581
|
+
uniform float uRadius, uBlur;
|
|
582
|
+
uniform vec2 uGradient;
|
|
583
|
+
in vec2 vPos;
|
|
584
|
+
out vec4 outColor;
|
|
585
|
+
${patterns}
|
|
586
|
+
|
|
587
|
+
void main() {
|
|
588
|
+
// The shade deep in the fur, between the hairs: a rounded box, blurred.
|
|
589
|
+
vec2 q = abs(vPos - (uBox.xy + uBox.zw * 0.5)) - (uBox.zw * 0.5 - uRadius);
|
|
590
|
+
float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - uRadius;
|
|
591
|
+
// Clamped, as tanh overflows far from the edge on some GPUs.
|
|
592
|
+
float x = clamp(sd / (max(uBlur, 0.01) * 1.41421356), -4.0, 4.0);
|
|
593
|
+
float cover = 0.5 - 0.5 * tanh(x * (1.1283792 + 0.1009 * x * x));
|
|
594
|
+
if (cover < 0.002) discard;
|
|
595
|
+
float g = clamp((vPos.y - uGradient.x) / max(1.0, uGradient.y - uGradient.x), 0.0, 1.0);
|
|
596
|
+
vec2 marks = patternAt(vPos);
|
|
597
|
+
outColor = vec4(mix(coatColor(0.46, marks), coatColor(0.3, marks), g) * cover, cover);
|
|
598
|
+
}
|
|
599
|
+
`
|
|
600
|
+
);
|
|
601
|
+
var shared;
|
|
602
|
+
function compile(gl, vertex, fragment) {
|
|
603
|
+
const program = gl.createProgram();
|
|
604
|
+
for (const [type, source] of [[gl.VERTEX_SHADER, vertex], [gl.FRAGMENT_SHADER, fragment]]) {
|
|
605
|
+
const shader = gl.createShader(type);
|
|
606
|
+
gl.shaderSource(shader, source);
|
|
607
|
+
gl.compileShader(shader);
|
|
608
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) && !gl.isContextLost()) {
|
|
609
|
+
throw new Error(`hairy: shader failed to compile: ${gl.getShaderInfoLog(shader)}`);
|
|
610
|
+
}
|
|
611
|
+
gl.attachShader(program, shader);
|
|
612
|
+
gl.deleteShader(shader);
|
|
613
|
+
}
|
|
614
|
+
gl.linkProgram(program);
|
|
615
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS) && !gl.isContextLost()) {
|
|
616
|
+
throw new Error(`hairy: shaders failed to link: ${gl.getProgramInfoLog(program)}`);
|
|
617
|
+
}
|
|
618
|
+
const u = {};
|
|
619
|
+
const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
|
|
620
|
+
for (let i = 0; i < count; i++) {
|
|
621
|
+
const name = gl.getActiveUniform(program, i).name;
|
|
622
|
+
u[name] = gl.getUniformLocation(program, name);
|
|
623
|
+
}
|
|
624
|
+
return { program, u };
|
|
625
|
+
}
|
|
626
|
+
function build(s) {
|
|
627
|
+
const { gl } = s;
|
|
628
|
+
return {
|
|
629
|
+
hair: compile(gl, hairVertex, hairFragment),
|
|
630
|
+
flake: compile(gl, flakeVertex, flakeFragment),
|
|
631
|
+
coat: compile(gl, coatVertex, coatFragment),
|
|
632
|
+
empty: gl.createVertexArray()
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function connect() {
|
|
636
|
+
if (shared !== void 0) return shared;
|
|
637
|
+
shared = null;
|
|
638
|
+
if (typeof document === "undefined") return null;
|
|
639
|
+
const canvas = document.createElement("canvas");
|
|
640
|
+
canvas.width = canvas.height = 256;
|
|
641
|
+
let gl = null;
|
|
642
|
+
try {
|
|
643
|
+
gl = canvas.getContext("webgl2", {
|
|
644
|
+
alpha: true,
|
|
645
|
+
premultipliedAlpha: true,
|
|
646
|
+
antialias: false,
|
|
647
|
+
depth: false,
|
|
648
|
+
stencil: false,
|
|
649
|
+
preserveDrawingBuffer: false,
|
|
650
|
+
// Without a real GPU, the fur would be too slow to animate: leave it off.
|
|
651
|
+
failIfMajorPerformanceCaveat: true
|
|
652
|
+
});
|
|
653
|
+
} catch {
|
|
654
|
+
gl = null;
|
|
655
|
+
}
|
|
656
|
+
if (!gl) return null;
|
|
657
|
+
let gamut = "srgb";
|
|
658
|
+
if ("drawingBufferColorSpace" in gl) {
|
|
659
|
+
try {
|
|
660
|
+
gl.drawingBufferColorSpace = "display-p3";
|
|
661
|
+
gamut = gl.drawingBufferColorSpace === "display-p3" ? "display-p3" : "srgb";
|
|
662
|
+
} catch {
|
|
663
|
+
gamut = "srgb";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const viewport = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
|
|
667
|
+
const size = Math.min(largest, gl.getParameter(gl.MAX_RENDERBUFFER_SIZE), viewport[0], viewport[1]);
|
|
668
|
+
let programs;
|
|
669
|
+
try {
|
|
670
|
+
programs = build({ gl });
|
|
671
|
+
} catch (error) {
|
|
672
|
+
console.warn(error);
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
const s = { canvas, gl, gamut, largest: size, lost: false, ...programs, layers: /* @__PURE__ */ new Set() };
|
|
676
|
+
canvas.addEventListener("webglcontextlost", (event) => {
|
|
677
|
+
event.preventDefault();
|
|
678
|
+
s.lost = true;
|
|
679
|
+
});
|
|
680
|
+
canvas.addEventListener("webglcontextrestored", () => {
|
|
681
|
+
try {
|
|
682
|
+
Object.assign(s, build(s));
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.warn(error);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (s.gamut === "display-p3") gl.drawingBufferColorSpace = "display-p3";
|
|
688
|
+
s.lost = false;
|
|
689
|
+
for (const layer of s.layers) layer.restore();
|
|
690
|
+
});
|
|
691
|
+
return shared = s;
|
|
692
|
+
}
|
|
693
|
+
var gpuSupported = () => connect() !== null;
|
|
694
|
+
var GpuLayer = class {
|
|
695
|
+
count = 0;
|
|
696
|
+
rampBytes = new Uint8Array(rampWidth * rampRows * 4);
|
|
697
|
+
vao = null;
|
|
698
|
+
hairBuffer = null;
|
|
699
|
+
bendBuffer = null;
|
|
700
|
+
texture = null;
|
|
701
|
+
disposed = false;
|
|
702
|
+
s;
|
|
703
|
+
onRestore;
|
|
704
|
+
constructor(s, onRestore) {
|
|
705
|
+
this.s = s;
|
|
706
|
+
this.onRestore = onRestore;
|
|
707
|
+
this.create();
|
|
708
|
+
s.layers.add(this);
|
|
709
|
+
}
|
|
710
|
+
get gamut() {
|
|
711
|
+
return this.s.gamut;
|
|
712
|
+
}
|
|
713
|
+
get largest() {
|
|
714
|
+
return this.s.largest;
|
|
715
|
+
}
|
|
716
|
+
create() {
|
|
717
|
+
const { gl } = this.s;
|
|
718
|
+
this.vao = gl.createVertexArray();
|
|
719
|
+
this.hairBuffer = gl.createBuffer();
|
|
720
|
+
this.bendBuffer = gl.createBuffer();
|
|
721
|
+
gl.bindVertexArray(this.vao);
|
|
722
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.hairBuffer);
|
|
723
|
+
const stride = hairFloats * 4;
|
|
724
|
+
[[0, 4, 0], [1, 4, 16], [2, 4, 32], [3, 3, 48]].forEach(([location, size, offset]) => {
|
|
725
|
+
gl.enableVertexAttribArray(location);
|
|
726
|
+
gl.vertexAttribPointer(location, size, gl.FLOAT, false, stride, offset);
|
|
727
|
+
gl.vertexAttribDivisor(location, 1);
|
|
728
|
+
});
|
|
729
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bendBuffer);
|
|
730
|
+
gl.enableVertexAttribArray(4);
|
|
731
|
+
gl.vertexAttribPointer(4, 2, gl.FLOAT, false, 8, 0);
|
|
732
|
+
gl.vertexAttribDivisor(4, 1);
|
|
733
|
+
gl.bindVertexArray(null);
|
|
734
|
+
this.texture = gl.createTexture();
|
|
735
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
736
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
737
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
738
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
739
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
740
|
+
this.uploadRamp();
|
|
741
|
+
}
|
|
742
|
+
restore() {
|
|
743
|
+
if (this.disposed) return;
|
|
744
|
+
this.create();
|
|
745
|
+
this.count = 0;
|
|
746
|
+
this.onRestore();
|
|
747
|
+
}
|
|
748
|
+
uploadRamp() {
|
|
749
|
+
const { gl } = this.s;
|
|
750
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
751
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
752
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, rampWidth, rampRows, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.rampBytes);
|
|
753
|
+
}
|
|
754
|
+
setHairs(data, count) {
|
|
755
|
+
if (this.s.lost) return;
|
|
756
|
+
this.count = count;
|
|
757
|
+
const { gl } = this.s;
|
|
758
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.hairBuffer);
|
|
759
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
|
760
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bendBuffer);
|
|
761
|
+
gl.bufferData(gl.ARRAY_BUFFER, count * 2 * 4, gl.DYNAMIC_DRAW);
|
|
762
|
+
}
|
|
763
|
+
setBends(bends) {
|
|
764
|
+
if (this.s.lost) return;
|
|
765
|
+
const { gl } = this.s;
|
|
766
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bendBuffer);
|
|
767
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, bends);
|
|
768
|
+
}
|
|
769
|
+
setRamp(bytes) {
|
|
770
|
+
this.rampBytes = bytes;
|
|
771
|
+
if (!this.s.lost) this.uploadRamp();
|
|
772
|
+
}
|
|
773
|
+
render(target, f) {
|
|
774
|
+
const s = this.s;
|
|
775
|
+
if (s.lost || this.disposed) return false;
|
|
776
|
+
const { gl, canvas } = s;
|
|
777
|
+
const width = target.canvas.width, height = target.canvas.height;
|
|
778
|
+
if (!width || !height) return false;
|
|
779
|
+
if (canvas.width < width || canvas.height < height) {
|
|
780
|
+
canvas.width = Math.min(s.largest, Math.max(canvas.width, Math.ceil(width / 128) * 128));
|
|
781
|
+
canvas.height = Math.min(s.largest, Math.max(canvas.height, Math.ceil(height / 128) * 128));
|
|
782
|
+
}
|
|
783
|
+
const y = canvas.height - height;
|
|
784
|
+
gl.viewport(0, y, width, height);
|
|
785
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
786
|
+
gl.scissor(0, y, width, height);
|
|
787
|
+
gl.clearColor(0, 0, 0, 0);
|
|
788
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
789
|
+
gl.enable(gl.BLEND);
|
|
790
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
791
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
792
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
793
|
+
const c = f.coat;
|
|
794
|
+
gl.useProgram(s.coat.program);
|
|
795
|
+
let u = s.coat.u;
|
|
796
|
+
gl.uniform2f(u.uView, f.width, f.height);
|
|
797
|
+
gl.uniform4f(u.uBox, c.x, c.y, Math.max(0, c.w), Math.max(0, c.h));
|
|
798
|
+
gl.uniform1f(u.uRadius, Math.max(0, Math.min(c.radius, c.w / 2, c.h / 2)));
|
|
799
|
+
gl.uniform1f(u.uBlur, c.blur);
|
|
800
|
+
gl.uniform2f(u.uGradient, c.top, c.bottom);
|
|
801
|
+
this.patternUniforms(u, f);
|
|
802
|
+
gl.bindVertexArray(s.empty);
|
|
803
|
+
if (c.w > 0 && c.h > 0) gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
804
|
+
if (this.count) {
|
|
805
|
+
gl.bindVertexArray(this.vao);
|
|
806
|
+
const segments = segmentsFor(f);
|
|
807
|
+
gl.useProgram(s.hair.program);
|
|
808
|
+
this.uniforms(s.hair.u, f);
|
|
809
|
+
gl.uniform1i(s.hair.u.uSegments, segments);
|
|
810
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, (segments + 1) * 2 + 2, this.count);
|
|
811
|
+
const flakes = Math.ceil(f.glitterDensity * flakesPerDensity);
|
|
812
|
+
if (f.material === 1 && f.materialAmount > 0 && f.glitterSize > 0 && flakes > 0) {
|
|
813
|
+
gl.useProgram(s.flake.program);
|
|
814
|
+
this.uniforms(s.flake.u, f);
|
|
815
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, flakes * 6, this.count);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
gl.bindVertexArray(null);
|
|
819
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
820
|
+
target.setTransform(1, 0, 0, 1, 0, 0);
|
|
821
|
+
target.clearRect(0, 0, width, height);
|
|
822
|
+
target.drawImage(canvas, 0, 0, width, height, 0, 0, width, height);
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
uniforms(u, f) {
|
|
826
|
+
const { gl } = this.s;
|
|
827
|
+
const set1 = (name, value) => u[name] && gl.uniform1f(u[name], value);
|
|
828
|
+
const seti = (name, value) => u[name] && gl.uniform1i(u[name], value);
|
|
829
|
+
gl.uniform2f(u.uView, f.width, f.height);
|
|
830
|
+
set1("uDpr", f.dpr);
|
|
831
|
+
set1("uLift", f.lift);
|
|
832
|
+
set1("uSway", f.sway);
|
|
833
|
+
set1("uTime", f.time);
|
|
834
|
+
set1("uHovering", f.hovering ? 1 : 0);
|
|
835
|
+
set1("uNear", f.near);
|
|
836
|
+
if (u.uPointer) gl.uniform2f(u.uPointer, f.pointerX, f.pointerY);
|
|
837
|
+
set1("uBase", f.base);
|
|
838
|
+
set1("uHoverTop", f.hoverTop);
|
|
839
|
+
set1("uHoverStretch", f.hoverStretch);
|
|
840
|
+
set1("uClicked", f.clicked);
|
|
841
|
+
set1("uGrown", f.grown);
|
|
842
|
+
set1("uDroop", f.droop);
|
|
843
|
+
set1("uThickness", f.thickness);
|
|
844
|
+
set1("uTaper", f.taper);
|
|
845
|
+
set1("uSoftness", f.softness);
|
|
846
|
+
seti("uType", f.hairType);
|
|
847
|
+
set1("uTypeAmount", f.typeAmount);
|
|
848
|
+
set1("uTightness", f.tightness);
|
|
849
|
+
seti("uTip", f.tip);
|
|
850
|
+
set1("uMaterialAmount", f.materialAmount);
|
|
851
|
+
set1("uGlitterDensity", f.glitterDensity);
|
|
852
|
+
set1("uGlitterSize", f.glitterSize);
|
|
853
|
+
set1("uGlitterVariation", f.glitterSizeVariation);
|
|
854
|
+
this.patternUniforms(u, f);
|
|
855
|
+
}
|
|
856
|
+
patternUniforms(u, f) {
|
|
857
|
+
const { gl } = this.s;
|
|
858
|
+
if (u.uRamp) gl.uniform1i(u.uRamp, 0);
|
|
859
|
+
if (u.uPattern) gl.uniform1i(u.uPattern, f.pattern);
|
|
860
|
+
if (u.uPatternScale) gl.uniform1f(u.uPatternScale, f.patternScale);
|
|
861
|
+
if (u.uScale) gl.uniform1f(u.uScale, f.scale);
|
|
862
|
+
if (u.uBody) gl.uniform4f(u.uBody, f.body.x, f.body.y, f.body.w, f.body.h);
|
|
863
|
+
}
|
|
864
|
+
dispose() {
|
|
865
|
+
if (this.disposed) return;
|
|
866
|
+
this.disposed = true;
|
|
867
|
+
this.s.layers.delete(this);
|
|
868
|
+
if (this.s.lost) return;
|
|
869
|
+
const { gl } = this.s;
|
|
870
|
+
gl.deleteVertexArray(this.vao);
|
|
871
|
+
gl.deleteBuffer(this.hairBuffer);
|
|
872
|
+
gl.deleteBuffer(this.bendBuffer);
|
|
873
|
+
gl.deleteTexture(this.texture);
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
function segmentsFor(f) {
|
|
877
|
+
const tight = Math.max(0, f.tightness);
|
|
878
|
+
if (f.hairType === 0 || f.typeAmount <= 0) return 8;
|
|
879
|
+
if (f.hairType === 1) return Math.min(48, Math.ceil(8 + 14 * tight));
|
|
880
|
+
if (f.hairType === 2) return Math.min(64, Math.ceil(6 + 25 * tight));
|
|
881
|
+
return Math.min(64, Math.ceil(8 + 30 * tight));
|
|
882
|
+
}
|
|
883
|
+
function createLayer(onRestore) {
|
|
884
|
+
const s = connect();
|
|
885
|
+
if (!s || s.lost) return null;
|
|
886
|
+
return new GpuLayer(s, onRestore);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/settings.ts
|
|
890
|
+
var eases = {
|
|
891
|
+
linear: (t) => t,
|
|
892
|
+
/** Speeds up steadily, like something dropping. */
|
|
893
|
+
gravity: (t) => t * t,
|
|
894
|
+
"ease-in": (t) => t ** 3,
|
|
895
|
+
"ease-out": (t) => 1 - (1 - t) ** 3,
|
|
896
|
+
"ease-in-out": (t) => t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2,
|
|
897
|
+
"back-out": (t) => 1 + 2.70158 * (t - 1) ** 3 + 1.70158 * (t - 1) ** 2,
|
|
898
|
+
"elastic-out": (t) => t <= 0 ? 0 : t >= 1 ? 1 : 2 ** (-10 * t) * Math.sin((10 * t - 0.75) * 2 * Math.PI / 3) + 1
|
|
899
|
+
};
|
|
900
|
+
var settingChoices = {
|
|
901
|
+
/** Straight hairs follow one smooth curve; wavy ones ripple side to side; curly ones coil into loops; frizzy ones kink at random and fly away. */
|
|
902
|
+
hairType: ["straight", "wavy", "curly", "frizzy"],
|
|
903
|
+
/** Round tips end in a half circle; sharp ones narrow to a fine point; square ones are cut straight across. */
|
|
904
|
+
tipShape: ["round", "sharp", "square"],
|
|
905
|
+
/** Natural fur, or glitter, with sparkling flakes. */
|
|
906
|
+
material: ["natural", "glitter"],
|
|
907
|
+
/** A coat pattern, as on an animal: none, zebra stripes, tiger stripes, dalmatian spots, leopard rosettes or giraffe patches. */
|
|
908
|
+
pattern: ["none", "zebra", "tiger", "dalmatian", "leopard", "giraffe"],
|
|
909
|
+
/** The pattern's colors: the animal's own, or tints of the fur's color. */
|
|
910
|
+
patternColors: ["natural", "tinted"],
|
|
911
|
+
/** When the label gets a halo: "auto" only when a pattern needs one, or "always" or "never". */
|
|
912
|
+
halo: ["auto", "always", "never"]
|
|
913
|
+
};
|
|
914
|
+
var defaultSettings = {
|
|
915
|
+
/** Palette: where its middle sits (L*, 0–1, from the color), how far it reaches below and above that. */
|
|
916
|
+
shift: -0.13,
|
|
917
|
+
darker: 0.4,
|
|
918
|
+
lighter: 0.22,
|
|
919
|
+
/** Hairs per area (regrows). */
|
|
920
|
+
density: 1.65,
|
|
921
|
+
/** Hair length. Changing it grows or shrinks the hairs with the hover spring. */
|
|
922
|
+
length: 0.8,
|
|
923
|
+
thickness: 0.75,
|
|
924
|
+
/** How much hairs curl (regrows). */
|
|
925
|
+
curl: 0.95,
|
|
926
|
+
/** How much hairs droop under their weight. */
|
|
927
|
+
droop: 0.4,
|
|
928
|
+
/** How much hair lengths vary, 0 all the same (regrows). */
|
|
929
|
+
lengthVariation: 1,
|
|
930
|
+
/** How much hair directions vary from the combed flow (regrows). */
|
|
931
|
+
messiness: 1,
|
|
932
|
+
/** How far hairs along the outline lean out, making the fuzzy edge (regrows). */
|
|
933
|
+
fuzz: 2,
|
|
934
|
+
/**
|
|
935
|
+
* How far from the button's edge the fur starts, in px at scale 1 (regrows):
|
|
936
|
+
* above 0 the outline hairs and the fur's body sit out past the edge, below
|
|
937
|
+
* 0 they move in, eating into the button.
|
|
938
|
+
*/
|
|
939
|
+
distance: 0,
|
|
940
|
+
/** Tip width relative to the root. */
|
|
941
|
+
taper: 0.31,
|
|
942
|
+
/**
|
|
943
|
+
* How soft each hair's edge is, in px at scale 1: 0 is the crisp
|
|
944
|
+
* pixel-wide edge, higher fades it out over that many more pixels, up to 2. Each
|
|
945
|
+
* hair gets wider to hold the fade, so it costs a little more to draw.
|
|
946
|
+
*/
|
|
947
|
+
softness: 0,
|
|
948
|
+
/** The hairs' pattern: straight, wavy, curly or frizzy. */
|
|
949
|
+
hairType: "straight",
|
|
950
|
+
/** How strong the waves, curls or frizz are: 0 is straight, 1 the usual amount. */
|
|
951
|
+
typeAmount: 1,
|
|
952
|
+
/** How many waves, turns or kinks fit along each hair: higher is tighter. */
|
|
953
|
+
tightness: 1,
|
|
954
|
+
/** How each hair ends: round, sharp or square. */
|
|
955
|
+
tipShape: "round",
|
|
956
|
+
/** What the hair looks made of: natural or glitter. */
|
|
957
|
+
material: "natural",
|
|
958
|
+
/** How bright glitter's flashes are. */
|
|
959
|
+
materialAmount: 1,
|
|
960
|
+
/** Glitter: how many flakes there are, 1 the usual amount. */
|
|
961
|
+
glitterDensity: 1,
|
|
962
|
+
/** Glitter: how big the flakes are, 1 the usual size. */
|
|
963
|
+
glitterSize: 1,
|
|
964
|
+
/** Glitter: how much flake sizes differ, 0 all the same; at 1 they range from half to twice the size. */
|
|
965
|
+
glitterSizeVariation: 0,
|
|
966
|
+
/** A coat pattern, as on an animal: "none", "zebra", "tiger", "dalmatian", "leopard" or "giraffe". */
|
|
967
|
+
pattern: "none",
|
|
968
|
+
/** The pattern's colors: "natural" takes the animal's own, "tinted" makes them from the fur's color. */
|
|
969
|
+
patternColors: "natural",
|
|
970
|
+
/** How big the pattern's stripes, spots or patches are, 1 the usual size. */
|
|
971
|
+
patternScale: 1,
|
|
972
|
+
/**
|
|
973
|
+
* A soft glow behind the label, in the fur's base color, so it reads where
|
|
974
|
+
* the fur alone doesn't, as on a zebra's white and black at once: "auto"
|
|
975
|
+
* adds it only when a pattern needs one, or "always" or "never".
|
|
976
|
+
*/
|
|
977
|
+
halo: "auto",
|
|
978
|
+
/** How far the halo's shade moves from the base color, away from the label's: darker under a light label, lighter under a dark one (L, 0–1). */
|
|
979
|
+
haloShade: 0.3,
|
|
980
|
+
/** How far the halo's glow spreads round the letters, 1 the usual size. */
|
|
981
|
+
haloSize: 1,
|
|
982
|
+
/** How strong the halo is, 1 the usual; 0 has none. */
|
|
983
|
+
haloStrength: 1,
|
|
984
|
+
/** How many hairs catch the light (regrows). */
|
|
985
|
+
sheen: 0.1,
|
|
986
|
+
/** Where the band of shine sits, 0 at the top to 1 at the bottom, and how far it spreads (regrows). */
|
|
987
|
+
sheenPosition: 0.1,
|
|
988
|
+
sheenSpread: 0.32,
|
|
989
|
+
/** How dark the hairs along the top edge are, turned away from the light; also keeps the shine off it (regrows). */
|
|
990
|
+
edgeShadow: 0,
|
|
991
|
+
/** On wide-gamut screens, how far colors move out toward the more vivid colors sRGB can't show (0 to 1). */
|
|
992
|
+
vividness: 0,
|
|
993
|
+
/** How far the hairs stand at rest, with no hover: 0 is combed flat, 1 standing up. */
|
|
994
|
+
static: 0,
|
|
995
|
+
/** How much further the hairs stand when hovered, on top of `static` (up to 1). */
|
|
996
|
+
hover: 0.35,
|
|
997
|
+
/**
|
|
998
|
+
* The hover spring: how fast it moves (rad/s) and its damping ratio (lower
|
|
999
|
+
* bounces more, 1 doesn't overshoot). Hovering out and clicking move on
|
|
1000
|
+
* springs like it.
|
|
1001
|
+
*/
|
|
1002
|
+
hoverSpeed: 22,
|
|
1003
|
+
hoverDamping: 0.9,
|
|
1004
|
+
/** The hover-out spring (without a click playing), back down to rest: speed (rad/s) and damping ratio. */
|
|
1005
|
+
leaveSpeed: 7,
|
|
1006
|
+
leaveDamping: 0.25,
|
|
1007
|
+
// Hovering out along a curve, set aside for now: it springs instead.
|
|
1008
|
+
// /**
|
|
1009
|
+
// * Hovering out (without a click playing): the fur falls at `leaveFallSpeed`
|
|
1010
|
+
// * along `leaveEase`, then lands and wobbles out like
|
|
1011
|
+
// * e^(−settle·t)·cos(bounce·t).
|
|
1012
|
+
// */
|
|
1013
|
+
// leaveFallSpeed: 1.55,
|
|
1014
|
+
// leaveEase: "gravity" as Ease,
|
|
1015
|
+
// leaveSettle: 1.4,
|
|
1016
|
+
// leaveBounce: 5.5,
|
|
1017
|
+
/** How much the hairs sway while hovered. */
|
|
1018
|
+
sway: 0.05,
|
|
1019
|
+
/**
|
|
1020
|
+
* How much longer the hairs grow as they rise to the hover lift: 0.5 makes
|
|
1021
|
+
* them half as long again (regrows, to make room).
|
|
1022
|
+
*/
|
|
1023
|
+
hoverStretch: 0,
|
|
1024
|
+
/** How much lighter the fur gets as it rises to the hover lift, in L* (0–1); below 0 darkens. */
|
|
1025
|
+
hoverLight: 0.05,
|
|
1026
|
+
/**
|
|
1027
|
+
* A click poofs the fur up by `poofHeight` on the rise spring, hangs there
|
|
1028
|
+
* for `poofHold` seconds, then comes back down to where it rests (hovered,
|
|
1029
|
+
* or flat if the pointer has left) on the fall spring, which also springs
|
|
1030
|
+
* the click's stretch back. Speeds in rad/s, then damping ratios (lower
|
|
1031
|
+
* bounces more, 1 doesn't overshoot).
|
|
1032
|
+
*/
|
|
1033
|
+
clickRiseSpeed: 49,
|
|
1034
|
+
clickRiseDamping: 0.9,
|
|
1035
|
+
clickFallSpeed: 8.5,
|
|
1036
|
+
clickFallDamping: 0.2,
|
|
1037
|
+
// A click along curves, set aside for now: it springs instead.
|
|
1038
|
+
// /**
|
|
1039
|
+
// * A click poofs the fur up by `poofHeight` over `poofRise` seconds, hangs
|
|
1040
|
+
// * there for `poofHold` seconds, then falls at `poofFallSpeed` back to
|
|
1041
|
+
// * where it rests (hovered, or flat if the pointer has left). It lands with
|
|
1042
|
+
// * whatever speed the fall curve ends with, overshoots, and wobbles out like
|
|
1043
|
+
// * e^(−settle·t)·cos(bounce·t).
|
|
1044
|
+
// */
|
|
1045
|
+
poofHeight: 0.53,
|
|
1046
|
+
/** How much longer still the hairs grow at the top of a click, on top of `hoverStretch` (regrows). */
|
|
1047
|
+
clickStretch: 0,
|
|
1048
|
+
/** How much lighter still a click makes the fur, on top of `hoverLight`, springing back with the click. */
|
|
1049
|
+
clickLight: 0,
|
|
1050
|
+
// poofRise: 0.13,
|
|
1051
|
+
// poofSettle: 3.1,
|
|
1052
|
+
// poofBounce: 15,
|
|
1053
|
+
// /** The curve of the poof's rise. */
|
|
1054
|
+
// poofEase: "ease-out" as Ease,
|
|
1055
|
+
/** Seconds the fur hangs at the top of the poof. */
|
|
1056
|
+
poofHold: 0,
|
|
1057
|
+
// /**
|
|
1058
|
+
// * How fast the fall back down goes, and its curve. Speeds are in lift per
|
|
1059
|
+
// * second: a full drop from upright (1) to flat (0) takes 1 ÷ speed seconds,
|
|
1060
|
+
// * and a shorter drop takes proportionally less.
|
|
1061
|
+
// */
|
|
1062
|
+
// poofFallSpeed: 1.45,
|
|
1063
|
+
// poofFallEase: "gravity" as Ease,
|
|
1064
|
+
/** How strongly the pointer brushes the hairs. */
|
|
1065
|
+
brush: 1.05,
|
|
1066
|
+
/** How brushed hairs spring back: speed (rad/s) and damping ratio (lower bounces more). */
|
|
1067
|
+
brushSpeed: 6,
|
|
1068
|
+
brushDamping: 0.32
|
|
1069
|
+
};
|
|
1070
|
+
function isSettingValue(key, value) {
|
|
1071
|
+
if (!(key in defaultSettings)) return false;
|
|
1072
|
+
if (typeof defaultSettings[key] === "number") return typeof value === "number" && Number.isFinite(value);
|
|
1073
|
+
const choices = settingChoices[key];
|
|
1074
|
+
return typeof value === "string" && (!choices || choices.includes(value));
|
|
1075
|
+
}
|
|
1076
|
+
var regrowSettings = /* @__PURE__ */ new Set([
|
|
1077
|
+
"density",
|
|
1078
|
+
"curl",
|
|
1079
|
+
"lengthVariation",
|
|
1080
|
+
"messiness",
|
|
1081
|
+
"fuzz",
|
|
1082
|
+
"distance",
|
|
1083
|
+
"sheen",
|
|
1084
|
+
"sheenPosition",
|
|
1085
|
+
"sheenSpread",
|
|
1086
|
+
"edgeShadow",
|
|
1087
|
+
// These two size the canvas margin the hairs can reach into.
|
|
1088
|
+
"hoverStretch",
|
|
1089
|
+
"clickStretch"
|
|
1090
|
+
]);
|
|
1091
|
+
var presetSettings = {
|
|
1092
|
+
// Most related first, from Plush (the default) out to long hair; then the fun ones.
|
|
1093
|
+
/** Dense, short, neat fur with a springy click. */
|
|
1094
|
+
Plush: { ...defaultSettings },
|
|
1095
|
+
/** Sparse, thick, glossy strands, with a smooth hover that brightens, a slight bounce as it leaves and a soft, bouncy click that darkens. */
|
|
1096
|
+
Teddy: {
|
|
1097
|
+
...defaultSettings,
|
|
1098
|
+
shift: -0.27,
|
|
1099
|
+
darker: 0.2,
|
|
1100
|
+
lighter: 0.22,
|
|
1101
|
+
density: 0.8,
|
|
1102
|
+
length: 0.4,
|
|
1103
|
+
thickness: 2.05,
|
|
1104
|
+
curl: 0.25,
|
|
1105
|
+
droop: 0,
|
|
1106
|
+
lengthVariation: 1,
|
|
1107
|
+
messiness: 2.5,
|
|
1108
|
+
fuzz: 0,
|
|
1109
|
+
distance: 0,
|
|
1110
|
+
taper: 0.58,
|
|
1111
|
+
sheen: 1.3,
|
|
1112
|
+
sheenPosition: 0.3,
|
|
1113
|
+
sheenSpread: 0.59,
|
|
1114
|
+
edgeShadow: 0.31,
|
|
1115
|
+
vividness: 0,
|
|
1116
|
+
static: 0,
|
|
1117
|
+
hover: 0.34,
|
|
1118
|
+
hoverSpeed: 22,
|
|
1119
|
+
hoverDamping: 0.95,
|
|
1120
|
+
leaveSpeed: 15,
|
|
1121
|
+
leaveDamping: 0.55,
|
|
1122
|
+
// leaveFallSpeed: 0.8,
|
|
1123
|
+
// leaveEase: "ease-out",
|
|
1124
|
+
// leaveSettle: 3.7,
|
|
1125
|
+
// leaveBounce: 0,
|
|
1126
|
+
sway: 0.14,
|
|
1127
|
+
hoverStretch: 0,
|
|
1128
|
+
hoverLight: 0.09,
|
|
1129
|
+
poofHeight: 0.29,
|
|
1130
|
+
clickStretch: 0.37,
|
|
1131
|
+
clickLight: -0.1,
|
|
1132
|
+
clickRiseSpeed: 22,
|
|
1133
|
+
clickRiseDamping: 0.75,
|
|
1134
|
+
clickFallSpeed: 11,
|
|
1135
|
+
clickFallDamping: 0.4,
|
|
1136
|
+
// poofRise: 0.15,
|
|
1137
|
+
// poofSettle: 11.7,
|
|
1138
|
+
// poofBounce: 0,
|
|
1139
|
+
// poofEase: "ease-out",
|
|
1140
|
+
poofHold: 0,
|
|
1141
|
+
// poofFallSpeed: 1.35,
|
|
1142
|
+
// poofFallEase: "ease-out",
|
|
1143
|
+
brush: 1.05,
|
|
1144
|
+
brushSpeed: 6,
|
|
1145
|
+
brushDamping: 0.32
|
|
1146
|
+
},
|
|
1147
|
+
/** Curly, messy fur that stands up on its own, with a springy, rippling hover and click. */
|
|
1148
|
+
Fluffy: {
|
|
1149
|
+
...defaultSettings,
|
|
1150
|
+
shift: -0.18,
|
|
1151
|
+
darker: 0.31,
|
|
1152
|
+
lighter: 0.17,
|
|
1153
|
+
density: 1,
|
|
1154
|
+
length: 0.95,
|
|
1155
|
+
thickness: 0.8,
|
|
1156
|
+
curl: 0.9,
|
|
1157
|
+
droop: 0,
|
|
1158
|
+
lengthVariation: 2,
|
|
1159
|
+
messiness: 1.35,
|
|
1160
|
+
fuzz: 0,
|
|
1161
|
+
distance: 0,
|
|
1162
|
+
taper: 0.31,
|
|
1163
|
+
sheen: 2.5,
|
|
1164
|
+
sheenPosition: 0.71,
|
|
1165
|
+
sheenSpread: 0.23,
|
|
1166
|
+
edgeShadow: 0,
|
|
1167
|
+
vividness: 0,
|
|
1168
|
+
static: 0.6,
|
|
1169
|
+
hover: 0.1,
|
|
1170
|
+
hoverSpeed: 26.5,
|
|
1171
|
+
hoverDamping: 0.45,
|
|
1172
|
+
leaveSpeed: 14,
|
|
1173
|
+
leaveDamping: 0.45,
|
|
1174
|
+
// leaveFallSpeed: 1.3,
|
|
1175
|
+
// leaveEase: "gravity",
|
|
1176
|
+
// leaveSettle: 3.2,
|
|
1177
|
+
// leaveBounce: 6,
|
|
1178
|
+
sway: 0.14,
|
|
1179
|
+
hoverStretch: 0.1,
|
|
1180
|
+
hoverLight: 0.07,
|
|
1181
|
+
poofHeight: 0.55,
|
|
1182
|
+
clickStretch: 0.34,
|
|
1183
|
+
clickLight: -0.08,
|
|
1184
|
+
clickRiseSpeed: 49,
|
|
1185
|
+
clickRiseDamping: 0.9,
|
|
1186
|
+
clickFallSpeed: 9,
|
|
1187
|
+
clickFallDamping: 0.3,
|
|
1188
|
+
// poofRise: 0.13,
|
|
1189
|
+
// poofSettle: 3.3,
|
|
1190
|
+
// poofBounce: 10,
|
|
1191
|
+
// poofEase: "ease-out",
|
|
1192
|
+
poofHold: 0,
|
|
1193
|
+
// poofFallSpeed: 1.2,
|
|
1194
|
+
// poofFallEase: "gravity",
|
|
1195
|
+
brush: 2.2,
|
|
1196
|
+
brushSpeed: 14.5,
|
|
1197
|
+
brushDamping: 0.3
|
|
1198
|
+
},
|
|
1199
|
+
/** Long, thick, curly, heavily drooping fur set deep into the button, with sharp tips, a little shine and a quick, bouncy hover that stretches it. */
|
|
1200
|
+
Fuzzy: {
|
|
1201
|
+
...defaultSettings,
|
|
1202
|
+
shift: -0.17,
|
|
1203
|
+
darker: 0.7,
|
|
1204
|
+
lighter: 0.13,
|
|
1205
|
+
density: 0.55,
|
|
1206
|
+
length: 1.1,
|
|
1207
|
+
thickness: 2.5,
|
|
1208
|
+
curl: 1.4,
|
|
1209
|
+
droop: 2.55,
|
|
1210
|
+
lengthVariation: 2.5,
|
|
1211
|
+
messiness: 1,
|
|
1212
|
+
fuzz: 2,
|
|
1213
|
+
distance: -10,
|
|
1214
|
+
taper: 0.21,
|
|
1215
|
+
hairType: "curly",
|
|
1216
|
+
typeAmount: 0.15,
|
|
1217
|
+
tightness: 1.2,
|
|
1218
|
+
tipShape: "sharp",
|
|
1219
|
+
sheen: 1.5,
|
|
1220
|
+
sheenPosition: 0.24,
|
|
1221
|
+
sheenSpread: 0.29,
|
|
1222
|
+
edgeShadow: 0.41,
|
|
1223
|
+
vividness: 1,
|
|
1224
|
+
static: 0.58,
|
|
1225
|
+
hover: 0.06,
|
|
1226
|
+
hoverSpeed: 25.5,
|
|
1227
|
+
hoverDamping: 0.2,
|
|
1228
|
+
leaveSpeed: 7,
|
|
1229
|
+
leaveDamping: 0.2,
|
|
1230
|
+
// leaveFallSpeed: 0.35,
|
|
1231
|
+
// leaveEase: "gravity",
|
|
1232
|
+
// leaveSettle: 1,
|
|
1233
|
+
// leaveBounce: 5.5,
|
|
1234
|
+
sway: 0.23,
|
|
1235
|
+
hoverStretch: 0.23,
|
|
1236
|
+
hoverLight: 0.05,
|
|
1237
|
+
poofHeight: 0.12,
|
|
1238
|
+
clickStretch: 0,
|
|
1239
|
+
clickLight: -0.11,
|
|
1240
|
+
clickRiseSpeed: 35.5,
|
|
1241
|
+
clickRiseDamping: 0.9,
|
|
1242
|
+
clickFallSpeed: 8,
|
|
1243
|
+
clickFallDamping: 0.2,
|
|
1244
|
+
// poofRise: 0.15,
|
|
1245
|
+
// poofSettle: 3.1,
|
|
1246
|
+
// poofBounce: 15,
|
|
1247
|
+
// poofEase: "ease-out",
|
|
1248
|
+
poofHold: 0,
|
|
1249
|
+
// poofFallSpeed: 1,
|
|
1250
|
+
// poofFallEase: "gravity",
|
|
1251
|
+
brush: 0.45,
|
|
1252
|
+
brushSpeed: 4.5,
|
|
1253
|
+
brushDamping: 0.45
|
|
1254
|
+
},
|
|
1255
|
+
/** Thin, long, curly, droopy hair with lots of shine and a softer click. */
|
|
1256
|
+
Shaggy: {
|
|
1257
|
+
...defaultSettings,
|
|
1258
|
+
shift: -0.12,
|
|
1259
|
+
darker: 0.4,
|
|
1260
|
+
lighter: 0.22,
|
|
1261
|
+
density: 1.25,
|
|
1262
|
+
length: 0.95,
|
|
1263
|
+
thickness: 0.3,
|
|
1264
|
+
curl: 1.7,
|
|
1265
|
+
droop: 2.05,
|
|
1266
|
+
lengthVariation: 1.4,
|
|
1267
|
+
messiness: 2.3,
|
|
1268
|
+
fuzz: 2,
|
|
1269
|
+
taper: 1,
|
|
1270
|
+
sheen: 4,
|
|
1271
|
+
sheenPosition: 0.21,
|
|
1272
|
+
sheenSpread: 0.05,
|
|
1273
|
+
edgeShadow: 0.47,
|
|
1274
|
+
static: 0,
|
|
1275
|
+
hover: 0.34,
|
|
1276
|
+
hoverSpeed: 7.5,
|
|
1277
|
+
hoverDamping: 0.5,
|
|
1278
|
+
leaveSpeed: 6,
|
|
1279
|
+
leaveDamping: 0.25,
|
|
1280
|
+
// leaveFallSpeed: 1.15,
|
|
1281
|
+
// leaveEase: "gravity",
|
|
1282
|
+
// leaveSettle: 1.4,
|
|
1283
|
+
// leaveBounce: 5.5,
|
|
1284
|
+
sway: 0.05,
|
|
1285
|
+
hoverStretch: 0,
|
|
1286
|
+
hoverLight: 0,
|
|
1287
|
+
poofHeight: 0.19,
|
|
1288
|
+
clickStretch: 0,
|
|
1289
|
+
clickRiseSpeed: 49.5,
|
|
1290
|
+
clickRiseDamping: 0.9,
|
|
1291
|
+
clickFallSpeed: 8.5,
|
|
1292
|
+
clickFallDamping: 0.3,
|
|
1293
|
+
// poofRise: 0.16,
|
|
1294
|
+
// poofSettle: 2.5,
|
|
1295
|
+
// poofBounce: 7.5,
|
|
1296
|
+
// poofEase: "ease-in-out",
|
|
1297
|
+
poofHold: 0.06,
|
|
1298
|
+
// poofFallSpeed: 0.9,
|
|
1299
|
+
// poofFallEase: "gravity",
|
|
1300
|
+
brush: 1.05,
|
|
1301
|
+
brushSpeed: 4.5,
|
|
1302
|
+
brushDamping: 0.32
|
|
1303
|
+
},
|
|
1304
|
+
/** Long, sparse, messy, gently wavy strands with a bright band of shine along the top, a bouncy hover, and barely react to a click. */
|
|
1305
|
+
Mane: {
|
|
1306
|
+
...defaultSettings,
|
|
1307
|
+
shift: -0.15,
|
|
1308
|
+
darker: 1,
|
|
1309
|
+
lighter: 0.26,
|
|
1310
|
+
density: 0.7,
|
|
1311
|
+
length: 2.22,
|
|
1312
|
+
thickness: 1,
|
|
1313
|
+
curl: 0.25,
|
|
1314
|
+
droop: 0,
|
|
1315
|
+
lengthVariation: 1,
|
|
1316
|
+
messiness: 2.2,
|
|
1317
|
+
fuzz: 0,
|
|
1318
|
+
taper: 0.32,
|
|
1319
|
+
hairType: "wavy",
|
|
1320
|
+
typeAmount: 0.15,
|
|
1321
|
+
tightness: 0.95,
|
|
1322
|
+
sheen: 2.3,
|
|
1323
|
+
sheenPosition: 0,
|
|
1324
|
+
sheenSpread: 0.06,
|
|
1325
|
+
edgeShadow: 0.3,
|
|
1326
|
+
vividness: 0,
|
|
1327
|
+
static: 0.19,
|
|
1328
|
+
hover: 0.17,
|
|
1329
|
+
hoverSpeed: 20.5,
|
|
1330
|
+
hoverDamping: 0.3,
|
|
1331
|
+
leaveSpeed: 6,
|
|
1332
|
+
leaveDamping: 0.8,
|
|
1333
|
+
// leaveFallSpeed: 0.9,
|
|
1334
|
+
// leaveEase: "gravity",
|
|
1335
|
+
// leaveSettle: 4.1,
|
|
1336
|
+
// leaveBounce: 3,
|
|
1337
|
+
sway: 0.05,
|
|
1338
|
+
hoverStretch: 0.1,
|
|
1339
|
+
hoverLight: 0,
|
|
1340
|
+
poofHeight: 0.17,
|
|
1341
|
+
clickStretch: 0.05,
|
|
1342
|
+
clickRiseSpeed: 58,
|
|
1343
|
+
clickRiseDamping: 0.75,
|
|
1344
|
+
clickFallSpeed: 9.5,
|
|
1345
|
+
clickFallDamping: 0.25,
|
|
1346
|
+
// poofRise: 0.11,
|
|
1347
|
+
// poofSettle: 3.1,
|
|
1348
|
+
// poofBounce: 15,
|
|
1349
|
+
// poofEase: "ease-out",
|
|
1350
|
+
poofHold: 0,
|
|
1351
|
+
// poofFallSpeed: 1.35,
|
|
1352
|
+
// poofFallEase: "gravity",
|
|
1353
|
+
brush: 0.45,
|
|
1354
|
+
brushSpeed: 6.5,
|
|
1355
|
+
brushDamping: 0.6
|
|
1356
|
+
},
|
|
1357
|
+
/** Sparse, thick, very messy glittering curls with square ends set into the button, like tinsel, with slow, gentle motion that darkens on hover and click. */
|
|
1358
|
+
Tinsel: {
|
|
1359
|
+
...defaultSettings,
|
|
1360
|
+
shift: -0.3,
|
|
1361
|
+
darker: 1,
|
|
1362
|
+
lighter: 0.34,
|
|
1363
|
+
density: 0.25,
|
|
1364
|
+
length: 1.3,
|
|
1365
|
+
thickness: 2.9,
|
|
1366
|
+
curl: 1.45,
|
|
1367
|
+
droop: 0.7,
|
|
1368
|
+
lengthVariation: 0.9,
|
|
1369
|
+
messiness: 3,
|
|
1370
|
+
fuzz: 0,
|
|
1371
|
+
distance: -6,
|
|
1372
|
+
taper: 1,
|
|
1373
|
+
typeAmount: 0.85,
|
|
1374
|
+
tightness: 3,
|
|
1375
|
+
tipShape: "square",
|
|
1376
|
+
material: "glitter",
|
|
1377
|
+
materialAmount: 2,
|
|
1378
|
+
glitterDensity: 2.75,
|
|
1379
|
+
glitterSize: 0.25,
|
|
1380
|
+
glitterSizeVariation: 0.85,
|
|
1381
|
+
sheen: 1.1,
|
|
1382
|
+
sheenPosition: 0.23,
|
|
1383
|
+
sheenSpread: 0.35,
|
|
1384
|
+
edgeShadow: 0.44,
|
|
1385
|
+
vividness: 1,
|
|
1386
|
+
static: 0.42,
|
|
1387
|
+
hover: 0.05,
|
|
1388
|
+
hoverSpeed: 22,
|
|
1389
|
+
hoverDamping: 0.5,
|
|
1390
|
+
leaveSpeed: 38.5,
|
|
1391
|
+
leaveDamping: 0.9,
|
|
1392
|
+
// leaveFallSpeed: 0.4,
|
|
1393
|
+
// leaveEase: "ease-in-out",
|
|
1394
|
+
// leaveSettle: 1.4,
|
|
1395
|
+
// leaveBounce: 5.5,
|
|
1396
|
+
sway: 0.05,
|
|
1397
|
+
hoverStretch: 0.32,
|
|
1398
|
+
hoverLight: -0.08,
|
|
1399
|
+
poofHeight: 0.23,
|
|
1400
|
+
clickStretch: 0,
|
|
1401
|
+
clickLight: -0.15,
|
|
1402
|
+
clickRiseSpeed: 28.5,
|
|
1403
|
+
clickRiseDamping: 0.9,
|
|
1404
|
+
clickFallSpeed: 8.5,
|
|
1405
|
+
clickFallDamping: 0.9,
|
|
1406
|
+
// poofRise: 0.17,
|
|
1407
|
+
// poofSettle: 2.5,
|
|
1408
|
+
// poofBounce: 3,
|
|
1409
|
+
// poofEase: "ease-in-out",
|
|
1410
|
+
poofHold: 0,
|
|
1411
|
+
// poofFallSpeed: 0.3,
|
|
1412
|
+
// poofFallEase: "ease-out",
|
|
1413
|
+
brush: 0.3,
|
|
1414
|
+
brushSpeed: 3.5,
|
|
1415
|
+
brushDamping: 0.65
|
|
1416
|
+
},
|
|
1417
|
+
/** A few thick, tightly curled, pointed strands that spring up on hover and stretch out on a click. */
|
|
1418
|
+
Coil: {
|
|
1419
|
+
...defaultSettings,
|
|
1420
|
+
shift: 0,
|
|
1421
|
+
darker: 0.15,
|
|
1422
|
+
lighter: 0.12,
|
|
1423
|
+
density: 0.2,
|
|
1424
|
+
length: 0.5,
|
|
1425
|
+
thickness: 2,
|
|
1426
|
+
curl: 2.25,
|
|
1427
|
+
droop: 0,
|
|
1428
|
+
lengthVariation: 2,
|
|
1429
|
+
messiness: 1.9,
|
|
1430
|
+
fuzz: 0,
|
|
1431
|
+
distance: 0,
|
|
1432
|
+
taper: 1,
|
|
1433
|
+
sheen: 0.6,
|
|
1434
|
+
sheenPosition: 0.38,
|
|
1435
|
+
sheenSpread: 0.65,
|
|
1436
|
+
edgeShadow: 0.48,
|
|
1437
|
+
vividness: 1,
|
|
1438
|
+
static: 0.58,
|
|
1439
|
+
hover: 1,
|
|
1440
|
+
hoverSpeed: 28,
|
|
1441
|
+
hoverDamping: 0.55,
|
|
1442
|
+
leaveSpeed: 9,
|
|
1443
|
+
leaveDamping: 0.9,
|
|
1444
|
+
// leaveFallSpeed: 0.6,
|
|
1445
|
+
// leaveEase: "ease-out",
|
|
1446
|
+
// leaveSettle: 4.1,
|
|
1447
|
+
// leaveBounce: 3,
|
|
1448
|
+
sway: 0.3,
|
|
1449
|
+
hoverStretch: 1.35,
|
|
1450
|
+
hoverLight: 0.05,
|
|
1451
|
+
poofHeight: 1,
|
|
1452
|
+
clickStretch: 0.86,
|
|
1453
|
+
clickLight: -0.04,
|
|
1454
|
+
clickRiseSpeed: 41.5,
|
|
1455
|
+
clickRiseDamping: 0.7,
|
|
1456
|
+
clickFallSpeed: 11.5,
|
|
1457
|
+
clickFallDamping: 0.3,
|
|
1458
|
+
// poofRise: 0.12,
|
|
1459
|
+
// poofSettle: 4.6,
|
|
1460
|
+
// poofBounce: 9.5,
|
|
1461
|
+
// poofEase: "ease-out",
|
|
1462
|
+
poofHold: 0.05,
|
|
1463
|
+
// poofFallSpeed: 1.9,
|
|
1464
|
+
// poofFallEase: "ease-out",
|
|
1465
|
+
brush: 0.75,
|
|
1466
|
+
brushSpeed: 8,
|
|
1467
|
+
brushDamping: 1.05
|
|
1468
|
+
},
|
|
1469
|
+
/** Long, thin, glossy, loosely wavy hairs with sharp ends that droop heavily from just inside the button, with a quick, bouncy hover and click and a light brush. */
|
|
1470
|
+
Mustache: {
|
|
1471
|
+
...defaultSettings,
|
|
1472
|
+
shift: -0.07,
|
|
1473
|
+
darker: 0.28,
|
|
1474
|
+
lighter: 0.29,
|
|
1475
|
+
density: 0.8,
|
|
1476
|
+
length: 0.85,
|
|
1477
|
+
thickness: 0.65,
|
|
1478
|
+
curl: 0.05,
|
|
1479
|
+
droop: 3.35,
|
|
1480
|
+
lengthVariation: 1.15,
|
|
1481
|
+
messiness: 1.05,
|
|
1482
|
+
fuzz: 0.65,
|
|
1483
|
+
distance: -5,
|
|
1484
|
+
taper: 0.36,
|
|
1485
|
+
hairType: "wavy",
|
|
1486
|
+
typeAmount: 1.45,
|
|
1487
|
+
tightness: 0.2,
|
|
1488
|
+
tipShape: "sharp",
|
|
1489
|
+
materialAmount: 0.6,
|
|
1490
|
+
glitterSize: 0.6,
|
|
1491
|
+
glitterSizeVariation: 1.1,
|
|
1492
|
+
sheen: 2.8,
|
|
1493
|
+
sheenPosition: 0.28,
|
|
1494
|
+
sheenSpread: 0.26,
|
|
1495
|
+
edgeShadow: 0.63,
|
|
1496
|
+
vividness: 0,
|
|
1497
|
+
static: 0,
|
|
1498
|
+
hover: 0.1,
|
|
1499
|
+
hoverSpeed: 21,
|
|
1500
|
+
hoverDamping: 0.3,
|
|
1501
|
+
leaveSpeed: 12.5,
|
|
1502
|
+
leaveDamping: 0.9,
|
|
1503
|
+
// leaveFallSpeed: 0.2,
|
|
1504
|
+
// leaveEase: "ease-out",
|
|
1505
|
+
// leaveSettle: 1.6,
|
|
1506
|
+
// leaveBounce: 0,
|
|
1507
|
+
sway: 0.25,
|
|
1508
|
+
hoverStretch: 0.95,
|
|
1509
|
+
hoverLight: 0.04,
|
|
1510
|
+
poofHeight: 0.25,
|
|
1511
|
+
clickStretch: 0.09,
|
|
1512
|
+
clickLight: -0.1,
|
|
1513
|
+
clickRiseSpeed: 55.5,
|
|
1514
|
+
clickRiseDamping: 0.65,
|
|
1515
|
+
clickFallSpeed: 17,
|
|
1516
|
+
clickFallDamping: 0.35,
|
|
1517
|
+
// poofRise: 0.09,
|
|
1518
|
+
// poofSettle: 1,
|
|
1519
|
+
// poofBounce: 0,
|
|
1520
|
+
// poofEase: "ease-in-out",
|
|
1521
|
+
poofHold: 0,
|
|
1522
|
+
// poofFallSpeed: 0.2,
|
|
1523
|
+
// poofFallEase: "ease-out",
|
|
1524
|
+
brush: 0.15,
|
|
1525
|
+
brushSpeed: 3,
|
|
1526
|
+
brushDamping: 0.7
|
|
1527
|
+
},
|
|
1528
|
+
/** Sparse, thick, drooping bristles with square ends that stand up on their own, a quick hover, a high, snappy click and a strong brush. */
|
|
1529
|
+
Bristle: {
|
|
1530
|
+
...defaultSettings,
|
|
1531
|
+
shift: -0.13,
|
|
1532
|
+
darker: 0.4,
|
|
1533
|
+
lighter: 0.22,
|
|
1534
|
+
density: 0.3,
|
|
1535
|
+
length: 0.71,
|
|
1536
|
+
thickness: 1.65,
|
|
1537
|
+
curl: 1.05,
|
|
1538
|
+
droop: 1.8,
|
|
1539
|
+
lengthVariation: 0.7,
|
|
1540
|
+
messiness: 1,
|
|
1541
|
+
fuzz: 2,
|
|
1542
|
+
distance: 0,
|
|
1543
|
+
taper: 1,
|
|
1544
|
+
typeAmount: 1.45,
|
|
1545
|
+
tightness: 0.6,
|
|
1546
|
+
tipShape: "square",
|
|
1547
|
+
sheen: 0.1,
|
|
1548
|
+
sheenPosition: 0.1,
|
|
1549
|
+
sheenSpread: 0.32,
|
|
1550
|
+
edgeShadow: 0,
|
|
1551
|
+
vividness: 0,
|
|
1552
|
+
static: 0.86,
|
|
1553
|
+
hover: 0.07,
|
|
1554
|
+
hoverSpeed: 27,
|
|
1555
|
+
hoverDamping: 0.9,
|
|
1556
|
+
leaveSpeed: 7,
|
|
1557
|
+
leaveDamping: 0.25,
|
|
1558
|
+
sway: 0.11,
|
|
1559
|
+
hoverStretch: 0.16,
|
|
1560
|
+
hoverLight: 0.07,
|
|
1561
|
+
poofHeight: 1,
|
|
1562
|
+
clickStretch: 0.1,
|
|
1563
|
+
clickLight: 0,
|
|
1564
|
+
clickRiseSpeed: 53,
|
|
1565
|
+
clickRiseDamping: 0.9,
|
|
1566
|
+
clickFallSpeed: 27.5,
|
|
1567
|
+
clickFallDamping: 0.4,
|
|
1568
|
+
poofHold: 0,
|
|
1569
|
+
brush: 2.35,
|
|
1570
|
+
brushSpeed: 13.5,
|
|
1571
|
+
brushDamping: 0.8
|
|
1572
|
+
}
|
|
1573
|
+
// Velvet is set aside for now. To bring it back, uncomment it and its
|
|
1574
|
+
// big-button scale (1.2) in the playground's presetBigScale.
|
|
1575
|
+
// /** Very short, thick, neat pile that grows out long when hovered or clicked. */
|
|
1576
|
+
// Velvet: {
|
|
1577
|
+
// ...defaultSettings,
|
|
1578
|
+
// shift: 0,
|
|
1579
|
+
// darker: 0.37,
|
|
1580
|
+
// lighter: 0.12,
|
|
1581
|
+
// density: 0.8,
|
|
1582
|
+
// length: 0.15,
|
|
1583
|
+
// thickness: 2.2,
|
|
1584
|
+
// curl: 0.25,
|
|
1585
|
+
// droop: 0,
|
|
1586
|
+
// lengthVariation: 1,
|
|
1587
|
+
// messiness: 0,
|
|
1588
|
+
// fuzz: 0,
|
|
1589
|
+
// taper: 0.32,
|
|
1590
|
+
// sheen: 1.2,
|
|
1591
|
+
// sheenPosition: 0.04,
|
|
1592
|
+
// sheenSpread: 0.52,
|
|
1593
|
+
// edgeShadow: 0.21,
|
|
1594
|
+
// vividness: 0.38,
|
|
1595
|
+
// static: 0.32,
|
|
1596
|
+
// hover: 0.68,
|
|
1597
|
+
// hoverSpeed: 12.5,
|
|
1598
|
+
// hoverDamping: 0.45,
|
|
1599
|
+
// leaveFallSpeed: 0.9,
|
|
1600
|
+
// leaveEase: "gravity",
|
|
1601
|
+
// leaveSettle: 4.1,
|
|
1602
|
+
// leaveBounce: 3,
|
|
1603
|
+
// sway: 0.14,
|
|
1604
|
+
// hoverStretch: 0.83,
|
|
1605
|
+
// hoverLight: 0,
|
|
1606
|
+
// poofHeight: 1.29,
|
|
1607
|
+
// clickStretch: 0.59,
|
|
1608
|
+
// poofRise: 0.11,
|
|
1609
|
+
// poofSettle: 3.1,
|
|
1610
|
+
// poofBounce: 15,
|
|
1611
|
+
// poofEase: "ease-out",
|
|
1612
|
+
// poofHold: 0,
|
|
1613
|
+
// poofFallSpeed: 1.35,
|
|
1614
|
+
// poofFallEase: "gravity",
|
|
1615
|
+
// brush: 2.6,
|
|
1616
|
+
// brushSpeed: 3,
|
|
1617
|
+
// brushDamping: 0.6,
|
|
1618
|
+
// },
|
|
1619
|
+
};
|
|
1620
|
+
var presets = Object.fromEntries(
|
|
1621
|
+
Object.entries(presetSettings).map(([name, settings]) => [name.toLowerCase(), settings])
|
|
1622
|
+
);
|
|
1623
|
+
var presetNames = Object.fromEntries(
|
|
1624
|
+
Object.keys(presetSettings).map((name) => [name.toLowerCase(), name])
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
// src/fur.ts
|
|
1628
|
+
var halo = 32;
|
|
1629
|
+
var emBase = 16;
|
|
1630
|
+
function scaleOf(value, fontSize) {
|
|
1631
|
+
if (typeof value === "number") return value;
|
|
1632
|
+
const n = parseFloat(value ?? "");
|
|
1633
|
+
if (!n) return void 0;
|
|
1634
|
+
return /^\s*[\d.eE+-]+em\s*$/.test(value) ? n * fontSize / emBase : n;
|
|
1635
|
+
}
|
|
1636
|
+
var levels = 24;
|
|
1637
|
+
var sheenLevels = 4;
|
|
1638
|
+
var reduceMotionQuery;
|
|
1639
|
+
var wideScreenQuery;
|
|
1640
|
+
var reduceMotion = () => (reduceMotionQuery ??= matchMedia("(prefers-reduced-motion: reduce)")).matches;
|
|
1641
|
+
var wideScreen = () => (wideScreenQuery ??= matchMedia("(color-gamut: p3)")).matches;
|
|
1642
|
+
var supportsWideGamut = () => typeof matchMedia === "function" && wideScreen();
|
|
1643
|
+
var probe;
|
|
1644
|
+
function hexOf(css) {
|
|
1645
|
+
probe ??= document.createElement("canvas").getContext("2d", { willReadFrequently: true });
|
|
1646
|
+
probe.clearRect(0, 0, 1, 1);
|
|
1647
|
+
probe.fillStyle = "#000";
|
|
1648
|
+
probe.fillStyle = css;
|
|
1649
|
+
probe.fillRect(0, 0, 1, 1);
|
|
1650
|
+
const [r, g, b] = probe.getImageData(0, 0, 1, 1).data;
|
|
1651
|
+
return "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
1652
|
+
}
|
|
1653
|
+
function shadeOf(color, L, middle, saturation) {
|
|
1654
|
+
const [, a, b] = toOklab(color);
|
|
1655
|
+
const depth = clamp((middle - L) / 0.2, 0, 1);
|
|
1656
|
+
const k = saturation * (L < middle ? 1 + 0.6 * (middle - L) : 1);
|
|
1657
|
+
let hue = Math.atan2(b, a);
|
|
1658
|
+
const degrees = hue * 180 / Math.PI;
|
|
1659
|
+
if (degrees > 60 && degrees < 125) hue -= 22 * Math.PI / 180 * depth * smoothstep(125, 95, degrees);
|
|
1660
|
+
const chroma = Math.hypot(a, b) * k;
|
|
1661
|
+
return [fromLightness(clamp(L, 0, 1)), chroma * Math.cos(hue), chroma * Math.sin(hue)];
|
|
1662
|
+
}
|
|
1663
|
+
function linearSrgb([L, a, b]) {
|
|
1664
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
1665
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
1666
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
1667
|
+
return [
|
|
1668
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
1669
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
1670
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
1671
|
+
];
|
|
1672
|
+
}
|
|
1673
|
+
var linearP3 = ([r, g, b]) => [
|
|
1674
|
+
0.8224621 * r + 0.177538 * g,
|
|
1675
|
+
0.0331941 * r + 0.9668058 * g,
|
|
1676
|
+
0.0170827 * r + 0.0723974 * g + 0.9105199 * b
|
|
1677
|
+
];
|
|
1678
|
+
var linearIn = (lab, gamut) => gamut === "srgb" ? linearSrgb(lab) : linearP3(linearSrgb(lab));
|
|
1679
|
+
var fitsIn = (lab, gamut) => linearIn(lab, gamut).every((v) => v >= -1e-6 && v <= 1 + 1e-6);
|
|
1680
|
+
function room(lab, gamut) {
|
|
1681
|
+
if (Math.hypot(lab[1], lab[2]) < 1e-5) return 1;
|
|
1682
|
+
let lo = 0, hi = 4;
|
|
1683
|
+
for (let i = 0; i < 18; i++) {
|
|
1684
|
+
const mid = (lo + hi) / 2;
|
|
1685
|
+
if (fitsIn([lab[0], lab[1] * mid, lab[2] * mid], gamut)) lo = mid;
|
|
1686
|
+
else hi = mid;
|
|
1687
|
+
}
|
|
1688
|
+
return lo;
|
|
1689
|
+
}
|
|
1690
|
+
var encode = (v) => v <= 31308e-7 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
|
|
1691
|
+
function encodeIn(lab, gamut, vivid, buffer) {
|
|
1692
|
+
let c = lab;
|
|
1693
|
+
if (gamut !== "srgb" && vivid > 0) {
|
|
1694
|
+
const spread = room(c, gamut) / Math.max(1e-3, room(c, "srgb"));
|
|
1695
|
+
const k = 1 + clamp(vivid, 0, 1) * (Math.min(spread, 3) - 1);
|
|
1696
|
+
if (k > 1) c = [c[0], c[1] * k, c[2] * k];
|
|
1697
|
+
}
|
|
1698
|
+
const fit = room(c, gamut);
|
|
1699
|
+
if (fit < 1) c = [c[0], c[1] * fit, c[2] * fit];
|
|
1700
|
+
const [r, g, b] = linearIn(c, buffer);
|
|
1701
|
+
return [encode(clamp(r, 0, 1)), encode(clamp(g, 0, 1)), encode(clamp(b, 0, 1))];
|
|
1702
|
+
}
|
|
1703
|
+
function rng(seed) {
|
|
1704
|
+
return () => {
|
|
1705
|
+
seed |= 0;
|
|
1706
|
+
seed = seed + 1831565813 | 0;
|
|
1707
|
+
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
|
1708
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1709
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
1713
|
+
var smoothstep = (a, b, x) => {
|
|
1714
|
+
const t = clamp((x - a) / (b - a), 0, 1);
|
|
1715
|
+
return t * t * (3 - 2 * t);
|
|
1716
|
+
};
|
|
1717
|
+
function bandOf(color, S, light) {
|
|
1718
|
+
const Ls = lightness(color);
|
|
1719
|
+
const middle = clamp(Ls + S.shift + light, 0.05, 0.95);
|
|
1720
|
+
const low = Math.max(0.02, middle - S.darker), high = Math.min(0.97, middle + S.lighter);
|
|
1721
|
+
const at = (u) => u < 0.6 ? middle - (0.6 - u) / 0.6 * (middle - low) : middle + (u - 0.6) / 0.4 * (high - middle);
|
|
1722
|
+
return { middle, high, at };
|
|
1723
|
+
}
|
|
1724
|
+
function labelShades(color, S) {
|
|
1725
|
+
const shades = [];
|
|
1726
|
+
for (const light of [0, S.hoverLight]) {
|
|
1727
|
+
const { middle, at } = bandOf(color, S, light);
|
|
1728
|
+
for (const u of [0.35, 0.6, 0.85]) shades.push(fromOklab(shadeOf(color, at(u), middle, 1)));
|
|
1729
|
+
}
|
|
1730
|
+
return shades;
|
|
1731
|
+
}
|
|
1732
|
+
function labelContrast(label, color, S) {
|
|
1733
|
+
return Math.min(...labelShades(color, S).map((shade) => Math.abs(apca(label, shade))));
|
|
1734
|
+
}
|
|
1735
|
+
var labelTarget = (text) => text.size >= 24 || text.size >= 16 && text.weight >= 600 ? 60 : 75;
|
|
1736
|
+
function autoLabel(color, S, text = { size: 16, weight: 700 }) {
|
|
1737
|
+
return pickLabel(labelShades(color, S), color, labelTarget(text));
|
|
1738
|
+
}
|
|
1739
|
+
function pickLabel(shades, color, target) {
|
|
1740
|
+
const worst = (label) => Math.min(...shades.map((shade) => Math.abs(apca(label, shade))));
|
|
1741
|
+
const [, a, b] = toOklab(color);
|
|
1742
|
+
const chroma = Math.hypot(a, b), hue = Math.atan2(b, a);
|
|
1743
|
+
const tint = (L, c) => fromOklab([fromLightness(L), c * Math.cos(hue), c * Math.sin(hue)]);
|
|
1744
|
+
const tints = chroma > 0.02 ? [tint(0.96, Math.min(chroma, 0.035)), tint(0.16, Math.min(chroma, 0.05))] : [];
|
|
1745
|
+
const scored = (list) => list.map((label) => ({ label, lc: worst(label) })).sort((x, y) => y.lc - x.lc);
|
|
1746
|
+
const [bestTint] = scored(tints);
|
|
1747
|
+
if (bestTint && bestTint.lc >= target) return bestTint;
|
|
1748
|
+
return scored([...tints, "#ffffff", "#000000"])[0];
|
|
1749
|
+
}
|
|
1750
|
+
var coatColors = (coat) => [.../* @__PURE__ */ new Set([coat.ground, coat.marks, coat.third])];
|
|
1751
|
+
function labelLook(coat, S, label, own, text) {
|
|
1752
|
+
const target = labelTarget(text);
|
|
1753
|
+
const colors = coatColors(coat);
|
|
1754
|
+
const shades = colors.flatMap((color2) => labelShades(color2, S));
|
|
1755
|
+
let color;
|
|
1756
|
+
if (label === "auto") {
|
|
1757
|
+
const whole = pickLabel(shades, coat.label, target);
|
|
1758
|
+
color = whole.lc >= target || colors.length === 1 ? whole.label : pickLabel(labelShades(coat.label, S), coat.label, target).label;
|
|
1759
|
+
} else color = label ?? own;
|
|
1760
|
+
const bare = Math.min(...shades.map((shade) => Math.abs(apca(color, shade))));
|
|
1761
|
+
const halo2 = S.haloStrength > 0 && (S.halo === "always" || S.halo === "auto" && colors.length > 1 && bare < target);
|
|
1762
|
+
if (!halo2) return { color, contrast: bare, halo: halo2, shadow: "" };
|
|
1763
|
+
const backing = haloOf(coat.label, color, S.haloShade);
|
|
1764
|
+
return {
|
|
1765
|
+
color,
|
|
1766
|
+
// The halo backs every letter, so the label reads against it.
|
|
1767
|
+
contrast: Math.max(bare, Math.abs(apca(color, backing))),
|
|
1768
|
+
halo: halo2,
|
|
1769
|
+
shadow: haloShadow(backing, S.haloSize, S.haloStrength)
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function haloOf(base, label, shade) {
|
|
1773
|
+
const [L, a, b] = toOklab(base);
|
|
1774
|
+
const step = lightness(label) > lightness(base) ? shade : -shade;
|
|
1775
|
+
return fromOklab([clamp(L - step, 0, 1), a, b]);
|
|
1776
|
+
}
|
|
1777
|
+
function haloShadow(color, size, strength) {
|
|
1778
|
+
const [r, g, b] = [1, 3, 5].map((i) => parseInt(color.slice(i, i + 2), 16));
|
|
1779
|
+
const alpha = (a) => `rgb(${r} ${g} ${b} / ${+Math.min(1, a * strength).toFixed(3)})`;
|
|
1780
|
+
const edge = Array.from({ length: 12 }, (_, i) => {
|
|
1781
|
+
const angle = i / 12 * Math.PI * 2;
|
|
1782
|
+
return `${(Math.cos(angle) * 0.03).toFixed(3)}em ${(Math.sin(angle) * 0.03).toFixed(3)}em 0.01em ${alpha(1)}`;
|
|
1783
|
+
});
|
|
1784
|
+
const glow = ([blur, a]) => `0 0 ${+(blur * size).toFixed(3)}em ${alpha(a)}`;
|
|
1785
|
+
return [...edge, ...[[0.35, 0.7], [0.6, 0.45], [0.95, 0.25]].map(glow)].join(", ");
|
|
1786
|
+
}
|
|
1787
|
+
var coats = {
|
|
1788
|
+
zebra: { ground: "#eeeae1", marks: "#17140f", third: "#eeeae1", own: "ground" },
|
|
1789
|
+
tiger: { ground: "#d4701c", marks: "#1b110a", third: "#d4701c", own: "ground" },
|
|
1790
|
+
dalmatian: { ground: "#f2efe7", marks: "#1c1a18", third: "#f2efe7", own: "ground" },
|
|
1791
|
+
leopard: { ground: "#d9a85c", marks: "#21170f", third: "#a9661f", own: "ground" },
|
|
1792
|
+
giraffe: { ground: "#ecdcb8", marks: "#91501f", third: "#673312", own: "marks" }
|
|
1793
|
+
};
|
|
1794
|
+
function coatOf(color, S) {
|
|
1795
|
+
if (S.pattern === "none" || !(S.pattern in coats)) return { ground: color, marks: color, third: color, label: color };
|
|
1796
|
+
const coat = coats[S.pattern];
|
|
1797
|
+
if (S.patternColors !== "tinted") return { ...coat, label: coat[coat.own] };
|
|
1798
|
+
const from = lightness(coat[coat.own]), base = lightness(color);
|
|
1799
|
+
const tint = (hex) => {
|
|
1800
|
+
const L = clamp(base + lightness(hex) - from, 0.03, 0.97);
|
|
1801
|
+
return fromOklab(shadeOf(color, L, L, 1 - 0.6 * smoothstep(0.15, 0.6, Math.abs(L - base))));
|
|
1802
|
+
};
|
|
1803
|
+
const pick = (key) => key === coat.own ? color : tint(coat[key]);
|
|
1804
|
+
return { ground: pick("ground"), marks: pick("marks"), third: pick("third"), label: color };
|
|
1805
|
+
}
|
|
1806
|
+
function grow(w, h, radius, seed, scale, pad, S) {
|
|
1807
|
+
const random = rng(seed);
|
|
1808
|
+
const cx = w / 2, cy = h / 2, r = Math.min(radius, w / 2, h / 2);
|
|
1809
|
+
const sdf = (x, y) => {
|
|
1810
|
+
const qx = Math.abs(x - cx) - (w / 2 - r), qy = Math.abs(y - cy) - (h / 2 - r);
|
|
1811
|
+
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - r;
|
|
1812
|
+
};
|
|
1813
|
+
const off = S.distance * scale, reach = Math.max(0, off);
|
|
1814
|
+
const max = Math.max(0, Math.round((w * h / scale ** 1.6 + (w + h) * 7 / scale) * S.density));
|
|
1815
|
+
const f = () => new Float32Array(max);
|
|
1816
|
+
const s = {
|
|
1817
|
+
x: f(),
|
|
1818
|
+
y: f(),
|
|
1819
|
+
restX: f(),
|
|
1820
|
+
restY: f(),
|
|
1821
|
+
leanX: f(),
|
|
1822
|
+
leanY: f(),
|
|
1823
|
+
standX: f(),
|
|
1824
|
+
standY: f(),
|
|
1825
|
+
len: f(),
|
|
1826
|
+
curl: f(),
|
|
1827
|
+
phase: f(),
|
|
1828
|
+
rim: f()
|
|
1829
|
+
};
|
|
1830
|
+
const shadeLevel = f(), kindOf = f(), seedOf = f();
|
|
1831
|
+
const seedRandom = rng(seed ^ 1540483477);
|
|
1832
|
+
const lists = [0, 1].map(() => Array.from({ length: levels }, () => []));
|
|
1833
|
+
const sheen = Array.from({ length: sheenLevels }, () => []);
|
|
1834
|
+
let n = 0;
|
|
1835
|
+
for (let tries = 0; n < max && tries < max * 4; tries++) {
|
|
1836
|
+
const x = random() * (w + 4 + reach * 2) - 2 - reach, y = random() * (h + 4 + reach * 2) - 2 - reach;
|
|
1837
|
+
const d = sdf(x, y) - off;
|
|
1838
|
+
if (d > 0.5) continue;
|
|
1839
|
+
const rim = smoothstep(-9 * scale, 0, d);
|
|
1840
|
+
if (random() > 0.4 + 0.6 * rim) continue;
|
|
1841
|
+
const gx = sdf(x + 0.5, y) - sdf(x - 0.5, y), gy = sdf(x, y + 0.5) - sdf(x, y - 0.5);
|
|
1842
|
+
const g = Math.hypot(gx, gy) || 1;
|
|
1843
|
+
const outX = gx / g, outY = gy / g;
|
|
1844
|
+
const fromX = x - cx, fromY = y - cy;
|
|
1845
|
+
const side = Math.tanh(fromX / (12 * scale));
|
|
1846
|
+
const wantX = side * 0.6, wantY = 1;
|
|
1847
|
+
const along = wantX * outX + wantY * outY;
|
|
1848
|
+
let ax = (wantX - along * outX) * 1.4;
|
|
1849
|
+
let ay = (wantY - along * outY) * 1.4 + 0.3;
|
|
1850
|
+
const al = Math.hypot(ax, ay) || 1;
|
|
1851
|
+
ax /= al;
|
|
1852
|
+
ay /= al;
|
|
1853
|
+
const swirl = side * 0.2 * Math.sin((Math.abs(fromX) * 0.05 + y * 0.035) / scale) + 0.5 * S.messiness * (random() - 0.5);
|
|
1854
|
+
s.restX[n] = ax * Math.cos(swirl) - ay * Math.sin(swirl);
|
|
1855
|
+
s.restY[n] = ax * Math.sin(swirl) + ay * Math.cos(swirl);
|
|
1856
|
+
const lean = 0.32 * S.fuzz * rim * (0.25 + 0.75 * smoothstep(-1, 0.2, outY));
|
|
1857
|
+
s.leanX[n] = outX * lean;
|
|
1858
|
+
s.leanY[n] = outY * lean;
|
|
1859
|
+
const stand = random() * Math.PI * 2;
|
|
1860
|
+
const guard = random() < 0.3 + 0.3 * rim ? 1 : 0;
|
|
1861
|
+
s.x[n] = x + pad;
|
|
1862
|
+
s.y[n] = y + pad;
|
|
1863
|
+
s.standX[n] = outX * rim + Math.cos(stand) * (1 - rim);
|
|
1864
|
+
s.standY[n] = outY * rim + Math.sin(stand) * (1 - rim);
|
|
1865
|
+
const vary = (spread2) => (random() - 0.5) * spread2 * S.lengthVariation;
|
|
1866
|
+
const base = (guard ? 9.5 + vary(5) : 5.5 + vary(3)) + rim * (4 + vary(4));
|
|
1867
|
+
s.len[n] = Math.max(1, base) * scale;
|
|
1868
|
+
s.curl[n] = (random() * 2 - 1) * (guard ? 0.9 : 0.6) * S.curl;
|
|
1869
|
+
s.phase[n] = random() * Math.PI * 2;
|
|
1870
|
+
s.rim[n] = rim;
|
|
1871
|
+
const px = fromX / (w / 2), py = fromY / (h / 2);
|
|
1872
|
+
const occlusion = rim * Math.max(0, py) * 0.15;
|
|
1873
|
+
const topEdge = rim * smoothstep(-0.2, -0.9, outY) * clamp(S.edgeShadow, 0, 1);
|
|
1874
|
+
const shade = clamp(0.6 - 0.18 * py - 0.06 * px * px - occlusion - 0.4 * topEdge + (random() - 0.5) * 0.32 + (guard ? 0.06 : -0.08), 0, 1);
|
|
1875
|
+
const center = clamp(S.sheenPosition, 0, 1) * 2 - 1, spread = Math.max(0.03, S.sheenSpread);
|
|
1876
|
+
const band = Math.exp(-((py - center) ** 2) / (2 * spread * spread));
|
|
1877
|
+
const shine = (0.14 * band + 0.04 * smoothstep(0.4, -0.9, py) * rim * (1 - topEdge)) * S.sheen;
|
|
1878
|
+
seedOf[n] = seedRandom();
|
|
1879
|
+
if (random() < shine) {
|
|
1880
|
+
const level = Math.floor(random() * sheenLevels);
|
|
1881
|
+
sheen[level].push(n);
|
|
1882
|
+
shadeLevel[n] = level / (sheenLevels - 1);
|
|
1883
|
+
kindOf[n] = 2;
|
|
1884
|
+
} else {
|
|
1885
|
+
const level = Math.round(shade * (levels - 1));
|
|
1886
|
+
lists[guard][level].push(n);
|
|
1887
|
+
shadeLevel[n] = level / (levels - 1);
|
|
1888
|
+
kindOf[n] = guard;
|
|
1889
|
+
}
|
|
1890
|
+
n++;
|
|
1891
|
+
}
|
|
1892
|
+
const order = [...lists[0].flat(), ...lists[1].flat(), ...sheen.flat()];
|
|
1893
|
+
const count = order.length;
|
|
1894
|
+
const data = new Float32Array(count * hairFloats);
|
|
1895
|
+
const roots = new Float32Array(count * 2);
|
|
1896
|
+
order.forEach((i, j) => {
|
|
1897
|
+
data.set([
|
|
1898
|
+
s.x[i],
|
|
1899
|
+
s.y[i],
|
|
1900
|
+
s.restX[i],
|
|
1901
|
+
s.restY[i],
|
|
1902
|
+
s.leanX[i],
|
|
1903
|
+
s.leanY[i],
|
|
1904
|
+
s.standX[i],
|
|
1905
|
+
s.standY[i],
|
|
1906
|
+
s.len[i],
|
|
1907
|
+
s.curl[i],
|
|
1908
|
+
s.phase[i],
|
|
1909
|
+
s.rim[i],
|
|
1910
|
+
shadeLevel[i],
|
|
1911
|
+
kindOf[i],
|
|
1912
|
+
seedOf[i]
|
|
1913
|
+
], j * hairFloats);
|
|
1914
|
+
roots[j * 2] = s.x[i];
|
|
1915
|
+
roots[j * 2 + 1] = s.y[i];
|
|
1916
|
+
});
|
|
1917
|
+
return { n: count, roots, data };
|
|
1918
|
+
}
|
|
1919
|
+
var inert = { update() {
|
|
1920
|
+
}, play() {
|
|
1921
|
+
}, label: null, reach: 0, destroy() {
|
|
1922
|
+
} };
|
|
1923
|
+
function mountFur(button, initial) {
|
|
1924
|
+
let config = initial;
|
|
1925
|
+
let S = config.settings;
|
|
1926
|
+
let destroyed = false;
|
|
1927
|
+
const layer = createLayer(() => {
|
|
1928
|
+
if (destroyed) return;
|
|
1929
|
+
plant();
|
|
1930
|
+
draw();
|
|
1931
|
+
});
|
|
1932
|
+
if (!layer) return inert;
|
|
1933
|
+
const events = new AbortController();
|
|
1934
|
+
const on = (type, listener) => button.addEventListener(type, listener, { signal: events.signal });
|
|
1935
|
+
const before = {
|
|
1936
|
+
position: button.style.position,
|
|
1937
|
+
isolation: button.style.isolation,
|
|
1938
|
+
backgroundColor: button.style.backgroundColor,
|
|
1939
|
+
color: button.style.color,
|
|
1940
|
+
textShadow: button.style.textShadow
|
|
1941
|
+
};
|
|
1942
|
+
const computed = getComputedStyle(button);
|
|
1943
|
+
const ownBackground = computed.backgroundColor;
|
|
1944
|
+
if (computed.position === "static") button.style.position = "relative";
|
|
1945
|
+
button.style.isolation = "isolate";
|
|
1946
|
+
let canvas, ctx;
|
|
1947
|
+
let setup = "";
|
|
1948
|
+
const gamutNow = () => config.wideGamut && layer.gamut === "display-p3" && wideScreen() ? "display-p3" : "srgb";
|
|
1949
|
+
function makeCanvas() {
|
|
1950
|
+
const gamut = gamutNow();
|
|
1951
|
+
setup = gamut;
|
|
1952
|
+
const next = document.createElement("canvas");
|
|
1953
|
+
next.setAttribute("aria-hidden", "true");
|
|
1954
|
+
next.setAttribute("data-hairy", "");
|
|
1955
|
+
Object.assign(next.style, { position: "absolute", pointerEvents: "none", zIndex: "-1" });
|
|
1956
|
+
ctx = next.getContext("2d", { colorSpace: gamut });
|
|
1957
|
+
if (canvas) canvas.replaceWith(next);
|
|
1958
|
+
else button.prepend(next);
|
|
1959
|
+
canvas = next;
|
|
1960
|
+
}
|
|
1961
|
+
makeCanvas();
|
|
1962
|
+
let scale = 1, pad = halo;
|
|
1963
|
+
let w = 0, h = 0, radius = 0, dpr = 1;
|
|
1964
|
+
let shift = 0;
|
|
1965
|
+
let strands = null, brush = null;
|
|
1966
|
+
let lift = clamp(S.static, 0, 1), velocity = 0, target = lift, hovering = false;
|
|
1967
|
+
let stage = null, stageTime = 0;
|
|
1968
|
+
let risePeak = 0, rose = false;
|
|
1969
|
+
let poof = 0, poofVelocity = 0, recoil = false;
|
|
1970
|
+
let grown = S.length, growth = 0;
|
|
1971
|
+
let spring = "hover";
|
|
1972
|
+
let pointerX = -1e3, pointerY = -1e3, brushing = 0;
|
|
1973
|
+
let frame = 0, last = 0, time = 0;
|
|
1974
|
+
let label = null;
|
|
1975
|
+
const seed = [...button.textContent ?? ""].reduce((a, c) => a * 31 + c.charCodeAt(0) | 0, 7);
|
|
1976
|
+
let surface = "#000";
|
|
1977
|
+
let coat = coatOf(surface, S);
|
|
1978
|
+
const shades = /* @__PURE__ */ new Map();
|
|
1979
|
+
let uploaded = -1;
|
|
1980
|
+
function readColors() {
|
|
1981
|
+
surface = hexOf(config.color ?? ownBackground);
|
|
1982
|
+
coat = coatOf(surface, S);
|
|
1983
|
+
shades.clear();
|
|
1984
|
+
uploaded = -1;
|
|
1985
|
+
useShades(0);
|
|
1986
|
+
button.style.backgroundColor = "transparent";
|
|
1987
|
+
button.style.color = before.color;
|
|
1988
|
+
const style = getComputedStyle(button);
|
|
1989
|
+
const text = { size: parseFloat(style.fontSize) || 16, weight: parseFloat(style.fontWeight) || 400 };
|
|
1990
|
+
const wanted = config.label === void 0 || config.label === "auto" ? config.label : hexOf(config.label);
|
|
1991
|
+
const look = labelLook(coat, S, wanted, hexOf(style.color), text);
|
|
1992
|
+
button.style.textShadow = look.shadow || before.textShadow;
|
|
1993
|
+
label = config.label === void 0 ? null : { color: look.color, contrast: look.contrast, halo: look.halo };
|
|
1994
|
+
if (label) button.style.color = label.color;
|
|
1995
|
+
}
|
|
1996
|
+
function useShades(light) {
|
|
1997
|
+
const key = Math.round(light * 1e3);
|
|
1998
|
+
if (key === uploaded) return;
|
|
1999
|
+
let ramps = shades.get(key);
|
|
2000
|
+
if (!ramps) {
|
|
2001
|
+
if (shades.size > 2e3) shades.clear();
|
|
2002
|
+
ramps = rampsAt(key / 1e3);
|
|
2003
|
+
shades.set(key, ramps);
|
|
2004
|
+
}
|
|
2005
|
+
layer.setRamp(ramps);
|
|
2006
|
+
uploaded = key;
|
|
2007
|
+
}
|
|
2008
|
+
function rampsAt(light) {
|
|
2009
|
+
const { middle, high } = bandOf(coat.ground, S, light);
|
|
2010
|
+
const gamut = gamutNow(), buffer = layer.gamut;
|
|
2011
|
+
const bytes = new Uint8Array(rampWidth * rampRows * 4);
|
|
2012
|
+
const palette = (color) => {
|
|
2013
|
+
const band = bandOf(color, S, light);
|
|
2014
|
+
return (u) => shadeOf(color, band.at(u), band.middle, 1);
|
|
2015
|
+
};
|
|
2016
|
+
const fur = palette(coat.ground);
|
|
2017
|
+
const patterned = S.pattern !== "none";
|
|
2018
|
+
const rows = [
|
|
2019
|
+
fur,
|
|
2020
|
+
(u) => shadeOf(coat.ground, Math.min(0.97, high + 0.03 + 0.06 * u), middle, 0.8),
|
|
2021
|
+
patterned ? palette(coat.marks) : null,
|
|
2022
|
+
patterned ? palette(coat.third) : null
|
|
2023
|
+
];
|
|
2024
|
+
rows.forEach((row, r) => {
|
|
2025
|
+
if (!row) {
|
|
2026
|
+
bytes.copyWithin(r * rampWidth * 4, 0, rampWidth * 4);
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
for (let i = 0; i < rampWidth; i++) {
|
|
2030
|
+
const [cr, cg, cb] = encodeIn(row(i / (rampWidth - 1)), gamut, S.vividness, buffer);
|
|
2031
|
+
const k = (r * rampWidth + i) * 4;
|
|
2032
|
+
bytes[k] = Math.round(cr * 255);
|
|
2033
|
+
bytes[k + 1] = Math.round(cg * 255);
|
|
2034
|
+
bytes[k + 2] = Math.round(cb * 255);
|
|
2035
|
+
bytes[k + 3] = 255;
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
return bytes;
|
|
2039
|
+
}
|
|
2040
|
+
function lightNow() {
|
|
2041
|
+
if (!S.hoverLight && !S.clickLight) return 0;
|
|
2042
|
+
const base = clamp(S.static, 0, 1), hoverTop = Math.min(1, base + S.hover);
|
|
2043
|
+
const hovered = hoverTop - base > 1e-3 ? clamp((lift - base) / (hoverTop - base), 0, 1) : 0;
|
|
2044
|
+
return S.hoverLight * hovered + S.clickLight * clamp(poof, -0.5, 1);
|
|
2045
|
+
}
|
|
2046
|
+
const padFor = (scale2) => Math.round(
|
|
2047
|
+
halo * scale2 * Math.max(1, S.length / 1.4) * (1 + Math.max(0, S.hoverStretch) + Math.max(0, S.clickStretch)) + Math.max(0, S.distance) * scale2
|
|
2048
|
+
);
|
|
2049
|
+
const reachFor = (scale2) => {
|
|
2050
|
+
const longest = (13.5 + 4.5 * Math.max(0, S.lengthVariation)) * scale2 * Math.max(0, S.length);
|
|
2051
|
+
const resting = longest * (1 + 0.2 * Math.max(0, S.droop));
|
|
2052
|
+
const standing = longest * 1.3 * (1 + 1.3 * Math.max(0, S.hoverStretch) + Math.max(0, S.clickStretch));
|
|
2053
|
+
return Math.min(pad, Math.max(0, Math.round(Math.max(resting, standing) + S.distance * scale2)));
|
|
2054
|
+
};
|
|
2055
|
+
function layout() {
|
|
2056
|
+
const b = button.getBoundingClientRect();
|
|
2057
|
+
const style = getComputedStyle(button);
|
|
2058
|
+
w = b.width;
|
|
2059
|
+
h = b.height;
|
|
2060
|
+
const fontSize = parseFloat(style.fontSize) || emBase;
|
|
2061
|
+
scale = scaleOf(config.scale, fontSize) ?? scaleOf(style.getPropertyValue("--hairy-scale"), fontSize) ?? 1;
|
|
2062
|
+
pad = padFor(scale);
|
|
2063
|
+
Object.assign(canvas.style, { inset: `-${pad}px`, width: `calc(100% + ${pad * 2}px)`, height: `calc(100% + ${pad * 2}px)` });
|
|
2064
|
+
radius = parseFloat(style.borderTopLeftRadius) || 0;
|
|
2065
|
+
place();
|
|
2066
|
+
dpr = config.pixelRatio ?? Math.min(2, devicePixelRatio || 1);
|
|
2067
|
+
dpr = Math.min(dpr, layer.largest / Math.max(1, w + pad * 2, h + pad * 2));
|
|
2068
|
+
canvas.width = Math.round((w + pad * 2) * dpr);
|
|
2069
|
+
canvas.height = Math.round((h + pad * 2) * dpr);
|
|
2070
|
+
plant();
|
|
2071
|
+
}
|
|
2072
|
+
function plant() {
|
|
2073
|
+
const { data, ...rest } = grow(w, h, radius, seed, scale, pad, S);
|
|
2074
|
+
strands = rest;
|
|
2075
|
+
brush = null;
|
|
2076
|
+
layer.setHairs(data, rest.n);
|
|
2077
|
+
}
|
|
2078
|
+
const choice = (key, value) => Math.max(0, settingChoices[key].indexOf(value));
|
|
2079
|
+
function frameNow() {
|
|
2080
|
+
const up = Math.max(-0.5, lift);
|
|
2081
|
+
const base = clamp(S.static, 0, 1);
|
|
2082
|
+
const sparse = 1 - clamp(S.density, 0, 1);
|
|
2083
|
+
const rr = Math.min(radius, w / 2, h / 2);
|
|
2084
|
+
const inset = Math.min((3 - S.distance - 5 * sparse) * scale, Math.min(w, h) / 2 - 1);
|
|
2085
|
+
return {
|
|
2086
|
+
width: canvas.width / dpr,
|
|
2087
|
+
height: canvas.height / dpr,
|
|
2088
|
+
dpr,
|
|
2089
|
+
lift,
|
|
2090
|
+
sway: reduceMotion() ? 0 : S.sway * Math.min(1, Math.abs(up) * 3),
|
|
2091
|
+
time,
|
|
2092
|
+
hovering,
|
|
2093
|
+
pointerX,
|
|
2094
|
+
pointerY,
|
|
2095
|
+
near: 2 * (22 * scale) ** 2,
|
|
2096
|
+
base,
|
|
2097
|
+
// Stretch: hovering stretches hairs as they rise from rest to the hover
|
|
2098
|
+
// lift (and a little past it in a bouncy overshoot). A click stretches
|
|
2099
|
+
// them further along its own rise and fall, `poof`, so it works even
|
|
2100
|
+
// when the fur already stands fully upright.
|
|
2101
|
+
hoverTop: Math.min(1, base + S.hover),
|
|
2102
|
+
hoverStretch: S.hoverStretch,
|
|
2103
|
+
clicked: S.clickStretch * clamp(poof, -0.5, 1),
|
|
2104
|
+
grown,
|
|
2105
|
+
droop: S.droop,
|
|
2106
|
+
scale,
|
|
2107
|
+
thickness: S.thickness,
|
|
2108
|
+
taper: clamp(S.taper, 0.05, 1),
|
|
2109
|
+
softness: clamp(S.softness, 0, 2),
|
|
2110
|
+
hairType: choice("hairType", S.hairType),
|
|
2111
|
+
typeAmount: Math.max(0, S.typeAmount),
|
|
2112
|
+
tightness: clamp(S.tightness, 0.1, 4),
|
|
2113
|
+
tip: choice("tipShape", S.tipShape),
|
|
2114
|
+
material: choice("material", S.material),
|
|
2115
|
+
materialAmount: Math.max(0, S.materialAmount),
|
|
2116
|
+
glitterDensity: clamp(S.glitterDensity, 0, maxGlitterDensity),
|
|
2117
|
+
glitterSize: Math.max(0, S.glitterSize),
|
|
2118
|
+
glitterSizeVariation: clamp(S.glitterSizeVariation, 0, 3),
|
|
2119
|
+
pattern: choice("pattern", S.pattern),
|
|
2120
|
+
patternScale: clamp(S.patternScale, 0.1, 10),
|
|
2121
|
+
body: { x: pad + w / 2, y: pad + h / 2, w: w / 2, h: h / 2 },
|
|
2122
|
+
coat: {
|
|
2123
|
+
x: pad + inset,
|
|
2124
|
+
y: pad + inset,
|
|
2125
|
+
w: w - inset * 2,
|
|
2126
|
+
h: h - inset * 2,
|
|
2127
|
+
radius: Math.max(0, rr - inset),
|
|
2128
|
+
// In CSS pixels, the same at any pixel ratio: divided by it, it was
|
|
2129
|
+
// twice as wide on 1x screens and when zoomed out, and showed past the hairs.
|
|
2130
|
+
blur: (1.25 + sparse) * scale,
|
|
2131
|
+
top: pad,
|
|
2132
|
+
bottom: pad + h
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
function place() {
|
|
2137
|
+
shift = Number.isFinite(config.translate) ? config.translate : 0;
|
|
2138
|
+
canvas.style.translate = shift ? `0 ${shift}px` : "";
|
|
2139
|
+
}
|
|
2140
|
+
function draw() {
|
|
2141
|
+
if (!strands || destroyed) return;
|
|
2142
|
+
useShades(lightNow());
|
|
2143
|
+
layer.render(ctx, frameNow());
|
|
2144
|
+
}
|
|
2145
|
+
function relax(dt) {
|
|
2146
|
+
if (!brush || brushing <= 0) return false;
|
|
2147
|
+
const { bends, speeds } = brush;
|
|
2148
|
+
const k = S.brushSpeed ** 2, damping = 2 * S.brushSpeed * S.brushDamping;
|
|
2149
|
+
let moving = 0;
|
|
2150
|
+
for (let i = 0; i < bends.length; i += 2) {
|
|
2151
|
+
let bx = bends[i], by = bends[i + 1], vx = speeds[i], vy = speeds[i + 1];
|
|
2152
|
+
if (bx === 0 && by === 0 && vx === 0 && vy === 0) continue;
|
|
2153
|
+
vx += (-k * bx - damping * vx) * dt;
|
|
2154
|
+
vy += (-k * by - damping * vy) * dt;
|
|
2155
|
+
bx += vx * dt;
|
|
2156
|
+
by += vy * dt;
|
|
2157
|
+
if (Math.abs(bx) + Math.abs(by) + Math.abs(vx) + Math.abs(vy) < 2e-3) bx = by = vx = vy = 0;
|
|
2158
|
+
else moving++;
|
|
2159
|
+
bends[i] = bx;
|
|
2160
|
+
bends[i + 1] = by;
|
|
2161
|
+
speeds[i] = vx;
|
|
2162
|
+
speeds[i + 1] = vy;
|
|
2163
|
+
}
|
|
2164
|
+
layer.setBends(bends);
|
|
2165
|
+
brushing = moving;
|
|
2166
|
+
if (!moving) brush = null;
|
|
2167
|
+
return moving > 0;
|
|
2168
|
+
}
|
|
2169
|
+
const springOf = (speed, ratio) => [speed * speed, 2 * speed * ratio];
|
|
2170
|
+
function springLift(to, k, damping, dt) {
|
|
2171
|
+
const steps = Math.ceil(dt * 240);
|
|
2172
|
+
for (let i = 0; i < steps; i++) {
|
|
2173
|
+
velocity += (k * (to - lift) - damping * velocity) * (dt / steps);
|
|
2174
|
+
lift += velocity * (dt / steps);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
function springPoof(to, k, damping, dt) {
|
|
2178
|
+
const steps = Math.ceil(dt * 240);
|
|
2179
|
+
for (let i = 0; i < steps; i++) {
|
|
2180
|
+
poofVelocity += (k * (to - poof) - damping * poofVelocity) * (dt / steps);
|
|
2181
|
+
poof += poofVelocity * (dt / steps);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function tick(now) {
|
|
2185
|
+
const dt = Math.min(0.05, last ? (now - last) / 1e3 : 1 / 60);
|
|
2186
|
+
last = now;
|
|
2187
|
+
time += dt;
|
|
2188
|
+
if (reduceMotion()) {
|
|
2189
|
+
lift = target;
|
|
2190
|
+
velocity = 0;
|
|
2191
|
+
stage = null;
|
|
2192
|
+
poof = 0;
|
|
2193
|
+
poofVelocity = 0;
|
|
2194
|
+
recoil = false;
|
|
2195
|
+
grown = S.length;
|
|
2196
|
+
growth = 0;
|
|
2197
|
+
} else if (stage === "rise" || stage === "hold") {
|
|
2198
|
+
stageTime += dt;
|
|
2199
|
+
const [k, damping] = springOf(S.clickRiseSpeed, S.clickRiseDamping);
|
|
2200
|
+
springLift(risePeak, k, damping, dt);
|
|
2201
|
+
springPoof(1, k, damping, dt);
|
|
2202
|
+
if (stage === "rise") {
|
|
2203
|
+
if (velocity > 0.01) rose = true;
|
|
2204
|
+
const there = Math.abs(risePeak - lift) < 3e-3 && Math.abs(velocity) < 0.05;
|
|
2205
|
+
if (rose && velocity <= 0 || there || stageTime > 2) {
|
|
2206
|
+
stage = "hold";
|
|
2207
|
+
stageTime = 0;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
if (stage === "hold" && stageTime >= S.poofHold) {
|
|
2211
|
+
stage = null;
|
|
2212
|
+
spring = "click";
|
|
2213
|
+
recoil = true;
|
|
2214
|
+
}
|
|
2215
|
+
} else {
|
|
2216
|
+
const [k, damping] = spring === "click" ? springOf(S.clickFallSpeed, S.clickFallDamping) : spring === "leave" ? springOf(S.leaveSpeed, S.leaveDamping) : springOf(S.hoverSpeed, S.hoverDamping);
|
|
2217
|
+
springLift(target, k, damping, dt);
|
|
2218
|
+
}
|
|
2219
|
+
if (recoil) {
|
|
2220
|
+
const [k, damping] = springOf(S.clickFallSpeed, S.clickFallDamping);
|
|
2221
|
+
springPoof(0, k, damping, dt);
|
|
2222
|
+
if (Math.abs(poof) < 2e-3 && Math.abs(poofVelocity) < 0.01) {
|
|
2223
|
+
poof = 0;
|
|
2224
|
+
poofVelocity = 0;
|
|
2225
|
+
recoil = false;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (!reduceMotion() && (grown !== S.length || growth !== 0)) {
|
|
2229
|
+
const [k, damping] = springOf(S.hoverSpeed, S.hoverDamping);
|
|
2230
|
+
const steps = Math.ceil(dt * 240);
|
|
2231
|
+
for (let i = 0; i < steps; i++) {
|
|
2232
|
+
growth += (k * (S.length - grown) - damping * growth) * (dt / steps);
|
|
2233
|
+
grown += growth * (dt / steps);
|
|
2234
|
+
}
|
|
2235
|
+
if (Math.abs(S.length - grown) < 2e-3 && Math.abs(growth) < 2e-3) {
|
|
2236
|
+
grown = S.length;
|
|
2237
|
+
growth = 0;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const moving = relax(dt);
|
|
2241
|
+
draw();
|
|
2242
|
+
const settled = stage === null && !recoil && growth === 0 && grown === S.length && Math.abs(target - lift) < 2e-3 && Math.abs(velocity) < 2e-3 && !moving;
|
|
2243
|
+
if (settled) spring = "hover";
|
|
2244
|
+
if (settled && (!hovering || reduceMotion())) {
|
|
2245
|
+
lift = target;
|
|
2246
|
+
draw();
|
|
2247
|
+
frame = 0;
|
|
2248
|
+
last = 0;
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
frame = requestAnimationFrame(tick);
|
|
2252
|
+
}
|
|
2253
|
+
const run = () => {
|
|
2254
|
+
if (!frame) frame = requestAnimationFrame(tick);
|
|
2255
|
+
};
|
|
2256
|
+
const focused = () => {
|
|
2257
|
+
try {
|
|
2258
|
+
return button.matches(":focus-visible");
|
|
2259
|
+
} catch {
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
const aim = () => {
|
|
2264
|
+
const rest = clamp(S.static, 0, 1);
|
|
2265
|
+
target = hovering || focused() ? Math.min(1, rest + S.hover) : rest;
|
|
2266
|
+
run();
|
|
2267
|
+
};
|
|
2268
|
+
const startPoof = () => {
|
|
2269
|
+
if (reduceMotion()) return;
|
|
2270
|
+
stage = "rise";
|
|
2271
|
+
stageTime = 0;
|
|
2272
|
+
rose = false;
|
|
2273
|
+
recoil = false;
|
|
2274
|
+
risePeak = Math.max(lift, Math.min(1, Math.max(lift, target) + S.poofHeight));
|
|
2275
|
+
spring = "click";
|
|
2276
|
+
run();
|
|
2277
|
+
};
|
|
2278
|
+
const hoverIn = () => {
|
|
2279
|
+
hovering = true;
|
|
2280
|
+
if (spring === "leave") spring = "hover";
|
|
2281
|
+
aim();
|
|
2282
|
+
};
|
|
2283
|
+
const hoverOut = () => {
|
|
2284
|
+
hovering = false;
|
|
2285
|
+
pointerX = pointerY = -1e3;
|
|
2286
|
+
aim();
|
|
2287
|
+
if (stage === null && spring === "hover") spring = "leave";
|
|
2288
|
+
};
|
|
2289
|
+
let lastX = 0, lastY = 0;
|
|
2290
|
+
const local = (event) => {
|
|
2291
|
+
const rect = button.getBoundingClientRect();
|
|
2292
|
+
pointerX = event.clientX - rect.left + pad;
|
|
2293
|
+
pointerY = event.clientY - rect.top + pad - shift;
|
|
2294
|
+
};
|
|
2295
|
+
on("pointerenter", (event) => {
|
|
2296
|
+
local(event);
|
|
2297
|
+
lastX = pointerX;
|
|
2298
|
+
lastY = pointerY;
|
|
2299
|
+
hoverIn();
|
|
2300
|
+
});
|
|
2301
|
+
on("pointermove", (event) => {
|
|
2302
|
+
local(event);
|
|
2303
|
+
if (!strands || reduceMotion()) return;
|
|
2304
|
+
let mx = pointerX - lastX, my = pointerY - lastY;
|
|
2305
|
+
lastX = pointerX;
|
|
2306
|
+
lastY = pointerY;
|
|
2307
|
+
const speed = Math.hypot(mx, my);
|
|
2308
|
+
if (speed < 0.01) return;
|
|
2309
|
+
const most = 16 * scale;
|
|
2310
|
+
if (speed > most) {
|
|
2311
|
+
mx *= most / speed;
|
|
2312
|
+
my *= most / speed;
|
|
2313
|
+
}
|
|
2314
|
+
mx /= scale;
|
|
2315
|
+
my /= scale;
|
|
2316
|
+
const clicking = stage === "rise" || stage === "hold" || stage === "fall" || spring === "click";
|
|
2317
|
+
const { roots, n } = strands, reach = 34 * scale, push = 0.09 * S.brush * (clicking ? 0.3 : 1);
|
|
2318
|
+
brush ??= { bends: new Float32Array(n * 2), speeds: new Float32Array(n * 2) };
|
|
2319
|
+
const { bends, speeds } = brush;
|
|
2320
|
+
for (let i = 0; i < n * 2; i += 2) {
|
|
2321
|
+
const dx = roots[i] - pointerX, dy = roots[i + 1] - pointerY;
|
|
2322
|
+
const d2 = dx * dx + dy * dy;
|
|
2323
|
+
if (d2 > reach * reach * 4) continue;
|
|
2324
|
+
const f = Math.exp(-d2 / (2 * reach * reach)) * push;
|
|
2325
|
+
speeds[i] += mx * f * 4;
|
|
2326
|
+
speeds[i + 1] += my * f * 4;
|
|
2327
|
+
const v = Math.hypot(speeds[i], speeds[i + 1]);
|
|
2328
|
+
if (v > 8) {
|
|
2329
|
+
speeds[i] *= 8 / v;
|
|
2330
|
+
speeds[i + 1] *= 8 / v;
|
|
2331
|
+
}
|
|
2332
|
+
const b = Math.hypot(bends[i], bends[i + 1]);
|
|
2333
|
+
if (b > 1.1) {
|
|
2334
|
+
bends[i] *= 1.1 / b;
|
|
2335
|
+
bends[i + 1] *= 1.1 / b;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
brushing = 1;
|
|
2339
|
+
run();
|
|
2340
|
+
});
|
|
2341
|
+
on("pointerleave", hoverOut);
|
|
2342
|
+
on("pointerdown", (event) => {
|
|
2343
|
+
local(event);
|
|
2344
|
+
startPoof();
|
|
2345
|
+
});
|
|
2346
|
+
on("keydown", (event) => {
|
|
2347
|
+
if (event.repeat || event.key !== "Enter" && event.key !== " ") return;
|
|
2348
|
+
startPoof();
|
|
2349
|
+
});
|
|
2350
|
+
on("focus", () => {
|
|
2351
|
+
if (!focused() || hovering) return;
|
|
2352
|
+
aim();
|
|
2353
|
+
});
|
|
2354
|
+
on("blur", aim);
|
|
2355
|
+
const refresh = () => {
|
|
2356
|
+
if (setup !== gamutNow()) {
|
|
2357
|
+
makeCanvas();
|
|
2358
|
+
layout();
|
|
2359
|
+
} else if (padFor(scale) !== pad) layout();
|
|
2360
|
+
readColors();
|
|
2361
|
+
place();
|
|
2362
|
+
draw();
|
|
2363
|
+
aim();
|
|
2364
|
+
};
|
|
2365
|
+
const regrow = () => {
|
|
2366
|
+
layout();
|
|
2367
|
+
refresh();
|
|
2368
|
+
};
|
|
2369
|
+
const resize = new ResizeObserver(regrow);
|
|
2370
|
+
resize.observe(button);
|
|
2371
|
+
layout();
|
|
2372
|
+
readColors();
|
|
2373
|
+
draw();
|
|
2374
|
+
return {
|
|
2375
|
+
update(next) {
|
|
2376
|
+
const regrows = next.scale !== config.scale || next.pixelRatio !== config.pixelRatio || Object.keys(next.settings).some((key) => regrowSettings.has(key) && next.settings[key] !== S[key]);
|
|
2377
|
+
config = next;
|
|
2378
|
+
S = next.settings;
|
|
2379
|
+
if (regrows) regrow();
|
|
2380
|
+
else refresh();
|
|
2381
|
+
},
|
|
2382
|
+
play(motion) {
|
|
2383
|
+
if (motion === "click") startPoof();
|
|
2384
|
+
else if (motion === "hover") hoverIn();
|
|
2385
|
+
else hoverOut();
|
|
2386
|
+
},
|
|
2387
|
+
get label() {
|
|
2388
|
+
return label;
|
|
2389
|
+
},
|
|
2390
|
+
get reach() {
|
|
2391
|
+
return reachFor(scale);
|
|
2392
|
+
},
|
|
2393
|
+
destroy() {
|
|
2394
|
+
destroyed = true;
|
|
2395
|
+
layer.dispose();
|
|
2396
|
+
events.abort();
|
|
2397
|
+
resize.disconnect();
|
|
2398
|
+
if (frame) cancelAnimationFrame(frame);
|
|
2399
|
+
frame = 0;
|
|
2400
|
+
canvas.remove();
|
|
2401
|
+
Object.assign(button.style, before);
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// src/options.ts
|
|
2407
|
+
var optionGroups = {
|
|
2408
|
+
shade: {
|
|
2409
|
+
/** Moves the fur's middle shade lighter or darker than the color, in L* (0–1). */
|
|
2410
|
+
lightness: "shift",
|
|
2411
|
+
/** How much darker the darkest hairs are than the middle shade. */
|
|
2412
|
+
shadows: "darker",
|
|
2413
|
+
/** How much lighter the lightest hairs are than the middle shade. */
|
|
2414
|
+
highlights: "lighter",
|
|
2415
|
+
/** On wide-gamut (P3) screens, how far colors move out toward the more vivid ones sRGB can't show (0 to 1). */
|
|
2416
|
+
vividness: "vividness"
|
|
2417
|
+
},
|
|
2418
|
+
shine: {
|
|
2419
|
+
/** How many glossy hairs catch the light. 0 is matte. */
|
|
2420
|
+
amount: "sheen",
|
|
2421
|
+
/** Where the band of shine sits: 0 at the top, 1 at the bottom. */
|
|
2422
|
+
position: "sheenPosition",
|
|
2423
|
+
/** How wide the band of shine is. */
|
|
2424
|
+
spread: "sheenSpread",
|
|
2425
|
+
/** Darkens the hairs along the top edge, where the surface turns away from the light. */
|
|
2426
|
+
edgeShadow: "edgeShadow"
|
|
2427
|
+
},
|
|
2428
|
+
hair: {
|
|
2429
|
+
density: "density",
|
|
2430
|
+
length: "length",
|
|
2431
|
+
thickness: "thickness",
|
|
2432
|
+
/** Tip width compared to the root. */
|
|
2433
|
+
taper: "taper",
|
|
2434
|
+
/** How soft each hair's edge is, in px: 0 is crisp, higher fades it over more pixels, up to 2. */
|
|
2435
|
+
softness: "softness",
|
|
2436
|
+
curl: "curl",
|
|
2437
|
+
droop: "droop",
|
|
2438
|
+
/** How much hair lengths differ. 0 makes them all the same. */
|
|
2439
|
+
lengthVariation: "lengthVariation",
|
|
2440
|
+
/** How far hairs stray from the combed direction. */
|
|
2441
|
+
messiness: "messiness",
|
|
2442
|
+
/** How far hairs along the outline lean out, softening the edge. */
|
|
2443
|
+
fuzz: "fuzz",
|
|
2444
|
+
/** Where the fur starts from the element's edge, in px. Below 0 it eats into the element. */
|
|
2445
|
+
distance: "distance",
|
|
2446
|
+
/** The hairs' pattern: "straight", "wavy", "curly" or "frizzy". */
|
|
2447
|
+
type: "hairType",
|
|
2448
|
+
/** How strong the waves, curls or frizz are: 0 is straight, 1 the usual amount. */
|
|
2449
|
+
typeAmount: "typeAmount",
|
|
2450
|
+
/** How many waves, turns or kinks fit along each hair: higher is tighter. */
|
|
2451
|
+
tightness: "tightness",
|
|
2452
|
+
/** How each hair ends: "round", "sharp" or "square". */
|
|
2453
|
+
end: "tipShape"
|
|
2454
|
+
},
|
|
2455
|
+
material: {
|
|
2456
|
+
/** What the hair looks made of: "natural" or "glitter". */
|
|
2457
|
+
type: "material",
|
|
2458
|
+
/** How bright glitter's flashes are. */
|
|
2459
|
+
amount: "materialAmount"
|
|
2460
|
+
},
|
|
2461
|
+
pattern: {
|
|
2462
|
+
/** A coat pattern, as on an animal: "none", "zebra", "tiger", "dalmatian", "leopard" or "giraffe". */
|
|
2463
|
+
type: "pattern",
|
|
2464
|
+
/** "natural" takes the animal's own colors; "tinted" makes them from the fur's color. */
|
|
2465
|
+
colors: "patternColors",
|
|
2466
|
+
/** How big the stripes, spots or patches are: 1 the usual size. */
|
|
2467
|
+
scale: "patternScale"
|
|
2468
|
+
},
|
|
2469
|
+
halo: {
|
|
2470
|
+
/** When the label gets a halo: "auto" only when a pattern needs one, or "always" or "never". */
|
|
2471
|
+
when: "halo",
|
|
2472
|
+
/** How far its shade moves from the base color, away from the label's: darker under a light label, lighter under a dark one (L, 0–1). */
|
|
2473
|
+
shade: "haloShade",
|
|
2474
|
+
/** How far the glow spreads round the letters: 1 the usual size. */
|
|
2475
|
+
size: "haloSize",
|
|
2476
|
+
/** How strong it is: 1 the usual, 0 none. */
|
|
2477
|
+
strength: "haloStrength"
|
|
2478
|
+
},
|
|
2479
|
+
glitter: {
|
|
2480
|
+
/** How many flakes there are: 1 the usual amount, 0 none. */
|
|
2481
|
+
density: "glitterDensity",
|
|
2482
|
+
/** How big the flakes are: 1 the usual size. */
|
|
2483
|
+
size: "glitterSize",
|
|
2484
|
+
/** How much flake sizes differ: 0 all the same; at 1 they range from half to twice the size. */
|
|
2485
|
+
sizeVariation: "glitterSizeVariation"
|
|
2486
|
+
},
|
|
2487
|
+
rest: {
|
|
2488
|
+
/** How far the hairs stand up on their own: 0 lies combed flat, 1 stands straight up. */
|
|
2489
|
+
static: "static"
|
|
2490
|
+
},
|
|
2491
|
+
hover: {
|
|
2492
|
+
/** How much further the hairs stand up on hover, on top of the rest's static. */
|
|
2493
|
+
static: "hover",
|
|
2494
|
+
/** How much longer the hairs grow as they rise. 1 doubles them. */
|
|
2495
|
+
stretch: "hoverStretch",
|
|
2496
|
+
/** How much lighter the fur gets as it rises, in L*. Below 0 darkens. */
|
|
2497
|
+
lightness: "hoverLight",
|
|
2498
|
+
/** How far each hair rocks from side to side while hovered. */
|
|
2499
|
+
sway: "sway",
|
|
2500
|
+
/** How quickly the fur rises (rad/s). */
|
|
2501
|
+
speed: "hoverSpeed",
|
|
2502
|
+
/** How much the rise bounces: lower overshoots, 1 doesn't. */
|
|
2503
|
+
damping: "hoverDamping"
|
|
2504
|
+
},
|
|
2505
|
+
leave: {
|
|
2506
|
+
/** How quickly the fur springs back to rest when the pointer leaves (rad/s). */
|
|
2507
|
+
speed: "leaveSpeed",
|
|
2508
|
+
/** How much it bounces past rest: lower overshoots, 1 doesn't. */
|
|
2509
|
+
damping: "leaveDamping"
|
|
2510
|
+
// Falling along a curve, set aside for now: hovering out springs instead.
|
|
2511
|
+
// /** How fast the fur falls when the pointer leaves: a full drop takes 1 ÷ this many seconds. */
|
|
2512
|
+
// fallSpeed: "leaveFallSpeed",
|
|
2513
|
+
// /** The curve of the fall. */
|
|
2514
|
+
// easing: "leaveEase",
|
|
2515
|
+
// /** How quickly the landing bounce fades. */
|
|
2516
|
+
// settle: "leaveSettle",
|
|
2517
|
+
// /** How fast the fur wobbles after it lands. 0 doesn't wobble. */
|
|
2518
|
+
// bounce: "leaveBounce",
|
|
2519
|
+
},
|
|
2520
|
+
click: {
|
|
2521
|
+
/** How much further a click makes the fur stand up, on top of hovering. */
|
|
2522
|
+
static: "poofHeight",
|
|
2523
|
+
/** How much longer still the hairs grow at the top of a click. */
|
|
2524
|
+
stretch: "clickStretch",
|
|
2525
|
+
/** How much lighter still a click makes the fur, in L*. */
|
|
2526
|
+
lightness: "clickLight",
|
|
2527
|
+
/** How quickly the fur poofs up (rad/s). */
|
|
2528
|
+
riseSpeed: "clickRiseSpeed",
|
|
2529
|
+
/** How far the poof overshoots its peak: lower overshoots more, 1 doesn't. */
|
|
2530
|
+
riseDamping: "clickRiseDamping",
|
|
2531
|
+
/** Seconds the fur hangs at the top. */
|
|
2532
|
+
hold: "poofHold",
|
|
2533
|
+
/** How quickly the fur comes back down, and the stretch springs back (rad/s). */
|
|
2534
|
+
fallSpeed: "clickFallSpeed",
|
|
2535
|
+
/** How much it bounces on the way down: lower bounces more, 1 doesn't. */
|
|
2536
|
+
fallDamping: "clickFallDamping"
|
|
2537
|
+
// Rising and falling along curves, set aside for now: a click springs instead.
|
|
2538
|
+
// /** Seconds the fur takes to poof up. */
|
|
2539
|
+
// rise: "poofRise",
|
|
2540
|
+
// /** The curve of the rise. */
|
|
2541
|
+
// riseEasing: "poofEase",
|
|
2542
|
+
// /** How fast the fall comes down: a full drop takes 1 ÷ this many seconds. */
|
|
2543
|
+
// fallSpeed: "poofFallSpeed",
|
|
2544
|
+
// /** The curve of the fall. */
|
|
2545
|
+
// fallEasing: "poofFallEase",
|
|
2546
|
+
// /** How quickly the landing bounce fades. */
|
|
2547
|
+
// settle: "poofSettle",
|
|
2548
|
+
// /** How fast the fur wobbles after it lands. 0 doesn't wobble. */
|
|
2549
|
+
// bounce: "poofBounce",
|
|
2550
|
+
},
|
|
2551
|
+
brush: {
|
|
2552
|
+
/** How strongly moving the pointer across the fur pushes the hairs. 0 turns brushing off. */
|
|
2553
|
+
amount: "brush",
|
|
2554
|
+
/** How quickly brushed hairs spring back. */
|
|
2555
|
+
speed: "brushSpeed",
|
|
2556
|
+
/** How much brushed hairs wobble on the way back. */
|
|
2557
|
+
damping: "brushDamping"
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
function resolveSettings(options) {
|
|
2561
|
+
const base = options.preset && presets[options.preset] || defaultSettings;
|
|
2562
|
+
const settings = { ...base };
|
|
2563
|
+
for (const [group, names] of Object.entries(optionGroups)) {
|
|
2564
|
+
const values = options[group];
|
|
2565
|
+
if (!values) continue;
|
|
2566
|
+
for (const [name, key] of Object.entries(names)) {
|
|
2567
|
+
const value = values[name];
|
|
2568
|
+
if (!isSettingValue(key, value)) continue;
|
|
2569
|
+
Object.assign(settings, { [key]: value });
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
return settings;
|
|
2573
|
+
}
|
|
2574
|
+
function diffOptions(settings, preset = "plush") {
|
|
2575
|
+
const base = presets[preset] ?? defaultSettings;
|
|
2576
|
+
const out = {};
|
|
2577
|
+
for (const [group, names] of Object.entries(optionGroups)) {
|
|
2578
|
+
for (const [name, key] of Object.entries(names)) {
|
|
2579
|
+
if (settings[key] === base[key]) continue;
|
|
2580
|
+
(out[group] ??= {})[name] = settings[key];
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return out;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// src/index.ts
|
|
2587
|
+
var configOf = (options) => ({
|
|
2588
|
+
settings: resolveSettings(options),
|
|
2589
|
+
color: options.color,
|
|
2590
|
+
label: options.label,
|
|
2591
|
+
scale: options.scale,
|
|
2592
|
+
translate: options.translate,
|
|
2593
|
+
pixelRatio: options.pixelRatio,
|
|
2594
|
+
wideGamut: options.wideGamut ?? true
|
|
2595
|
+
});
|
|
2596
|
+
function hairy(element, options = {}) {
|
|
2597
|
+
let current = { ...options };
|
|
2598
|
+
const fur = mountFur(element, configOf(current));
|
|
2599
|
+
return {
|
|
2600
|
+
update(next) {
|
|
2601
|
+
const merged = { ...current, ...next };
|
|
2602
|
+
for (const group of Object.keys(optionGroups)) {
|
|
2603
|
+
if (next[group]) Object.assign(merged, { [group]: { ...current[group], ...next[group] } });
|
|
2604
|
+
}
|
|
2605
|
+
current = merged;
|
|
2606
|
+
fur.update(configOf(current));
|
|
2607
|
+
},
|
|
2608
|
+
set(next) {
|
|
2609
|
+
current = { ...next };
|
|
2610
|
+
fur.update(configOf(current));
|
|
2611
|
+
},
|
|
2612
|
+
play: (motion) => fur.play(motion),
|
|
2613
|
+
get label() {
|
|
2614
|
+
return fur.label;
|
|
2615
|
+
},
|
|
2616
|
+
get reach() {
|
|
2617
|
+
return fur.reach;
|
|
2618
|
+
},
|
|
2619
|
+
destroy: () => fur.destroy()
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
export {
|
|
2624
|
+
apca,
|
|
2625
|
+
gpuSupported,
|
|
2626
|
+
eases,
|
|
2627
|
+
settingChoices,
|
|
2628
|
+
defaultSettings,
|
|
2629
|
+
isSettingValue,
|
|
2630
|
+
regrowSettings,
|
|
2631
|
+
presets,
|
|
2632
|
+
presetNames,
|
|
2633
|
+
supportsWideGamut,
|
|
2634
|
+
labelContrast,
|
|
2635
|
+
autoLabel,
|
|
2636
|
+
optionGroups,
|
|
2637
|
+
resolveSettings,
|
|
2638
|
+
diffOptions,
|
|
2639
|
+
hairy
|
|
2640
|
+
};
|
|
2641
|
+
//# sourceMappingURL=chunk-JIET4PY7.js.map
|